diff --git a/src/server/parse_archive.py b/src/server/parse_archive.py index 4b6dc41..26d4a83 100644 --- a/src/server/parse_archive.py +++ b/src/server/parse_archive.py @@ -35,7 +35,7 @@ class ArchiveParser: cmd = f"ls -1 {self.archive_dir} | tail -{start_index + count + 1} | head -{count}" print("Running cmd: " + cmd) result = subprocess.run( - cmd, shell=True, capture_output=True, text=True, check=True, timeout=30 + cmd, shell=True, capture_output=True, text=True, check=True, timeout=300 ) # Parse the output (directory names) @@ -118,6 +118,44 @@ class ArchiveParser: print(f"Error getting total posts: {e}") return 0 + def _extract_real_title_from_url(self, base_url: str) -> str: + """Extract the real title from base_url when title is 'Reddit - Prove your humanity'.""" + if not base_url or not isinstance(base_url, str): + return "" + + # Check if the URL contains reddit pattern + if "reddit.com/" in base_url: + try: + # Parse URL to extract path component + parsed_url = urllib.parse.urlparse(base_url) + path_parts = parsed_url.path.strip("/").split("/") + + if len(path_parts) >= 2: + # Get the last part which should contain the title + last_part = path_parts[-1] + + # Handle cases like "why_did_the_dday_beach_landing_soldiers_carry_all/#2025-10-14T23:33:16+00:00" + if "#" in last_part: + title_part = last_part.split("#")[0] + else: + title_part = last_part + + # Convert URL-encoded characters back to readable text + title_part = urllib.parse.unquote(title_part) + + # Replace underscores with spaces and convert to title case + title = title_part.replace("_", " ") + + # Capitalize first letter of each word for proper title formatting + title = " ".join(word.capitalize() for word in title.split()) + + return title if title else "" + except Exception as e: + print(f"Error extracting real title from URL: {e}") + return "" + + return "" + def _create_post(self, index) -> Dict[str, Any]: """Convert an index entry to a properly formatted post with ID and default image.""" try: @@ -128,6 +166,13 @@ class ArchiveParser: post["title"] = index.get("title", "Untitled Post") post["url"] = "http://archive2.home.ms/" + index.get("timestamp", "#") + # Check if title is the placeholder and extract real title from base_url + if post["title"] == "Reddit - Prove your humanity": + base_url = index.get("base_url", "") + real_title = self._extract_real_title_from_url(base_url) + if real_title: + post["title"] = real_title + # Set default image to reddit_logo.webp if no image is provided post["image"] = ( "/Reddit_Logo.webp" # Default to the logo in website directory diff --git a/src/website/app.js b/src/website/app.js index 1acffe6..8d44280 100644 --- a/src/website/app.js +++ b/src/website/app.js @@ -1,27 +1,57 @@ // Global variables let isLoading = false; let currentPostCount = 0; +const POSTS_CACHE_KEY = "cached_posts_100"; +const CACHE_EXPIRY_TIME = 5 * 60 * 1000; // 5 minutes + +// Check for saved dark mode preference on page load +function initializeDarkMode() { + const savedMode = localStorage.getItem("darkMode"); + const prefersDark = window.matchMedia("(prefers-color-scheme: dark)").matches; + + if (savedMode === "enabled") { + enableDarkMode(); + } else if (savedMode === "disabled") { + disableDarkMode(); + } else if (prefersDark) { + // If no preference saved and system prefers dark, enable it + enableDarkMode(); + } +} + +function enableDarkMode() { + document.body.classList.add("dark-mode"); + document.querySelector(".dark-mode-toggle .dark-mode-icon").textContent = + "☀️"; + localStorage.setItem("darkMode", "enabled"); +} + +function disableDarkMode() { + document.body.classList.remove("dark-mode"); + document.querySelector(".dark-mode-toggle .dark-mode-icon").textContent = + "🌙"; + localStorage.setItem("darkMode", "disabled"); +} + +function toggleDarkMode() { + if (document.body.classList.contains("dark-mode")) { + disableDarkMode(); + } else { + enableDarkMode(); + } +} + +// Initialize dark mode on load +document.addEventListener("DOMContentLoaded", initializeDarkMode); function createPostElement(post) { const postElement = document.createElement("div"); - postElement.className = "post"; + postElement.className = "post-card"; postElement.onclick = () => { window.location.href = post.url; }; postElement.style.cursor = "pointer"; - // Create the image container - const imageContainer = document.createElement("div"); - imageContainer.className = "post-image-container"; - - const image = document.createElement("img"); - image.src = post.image || "/Reddit_Logo.webp"; - image.alt = post.title; - image.className = "post-image"; - image.loading = "lazy"; - - imageContainer.appendChild(image); - // Create the content container const content = document.createElement("div"); content.className = "post-content"; @@ -29,11 +59,20 @@ function createPostElement(post) { const title = document.createElement("a"); title.href = post.url; title.className = "post-title"; - title.textContent = post.title; - content.appendChild(title); + // Check if this is a "Reddit - Prove your humanity" type title and extract real title from base_url + if (post.title && post.title.includes("Reddit - Prove your humanity")) { + const realTitle = extractRealTitleFromUrl(post.base_url); + if (realTitle) { + title.textContent = realTitle; + } else { + title.textContent = post.title; + } + } else { + title.textContent = post.title; + } - // Create the buttons container + // Create buttons container const buttonsContainer = document.createElement("div"); buttonsContainer.className = "post-buttons"; @@ -69,9 +108,10 @@ function createPostElement(post) { buttonsContainer.appendChild(pdfButton); // Assemble the complete post - postElement.appendChild(imageContainer); + content.appendChild(title); + content.appendChild(buttonsContainer); + postElement.appendChild(content); - postElement.appendChild(buttonsContainer); return postElement; } @@ -85,7 +125,80 @@ function renderPosts(posts) { }); } -// Fetch the initial 10 posts from our API +// Cache management functions +function getCachedPosts() { + try { + const cached = localStorage.getItem(POSTS_CACHE_KEY); + if (cached) { + const { posts, timestamp } = JSON.parse(cached); + + // Check if cache is stale (more than 5 minutes old) + if (Date.now() - timestamp < CACHE_EXPIRY_TIME) { + return posts; + } else { + // Clear stale cache + clearCachedPosts(); + } + } + } catch (error) { + console.error("Error retrieving cached posts:", error); + } + return null; +} + +function cachePosts(posts) { + try { + const cacheData = { + posts: posts, + timestamp: Date.now(), + }; + localStorage.setItem(POSTS_CACHE_KEY, JSON.stringify(cacheData)); + } catch (error) { + console.error("Error caching posts:", error); + } +} + +function clearCachedPosts() { + try { + localStorage.removeItem(POSTS_CACHE_KEY); + } catch (error) { + console.error("Error clearing cached posts:", error); + } +} + +// Extract real title from base_url +function extractRealTitleFromUrl(baseUrl) { + if (!baseUrl || typeof baseUrl !== "string") { + return ""; + } + + // Check if the URL contains the pattern we're looking for + if (baseUrl.includes("reddit.com/")) { + try { + // Parse URL to extract path component + const urlParts = baseUrl.split("/"); + const lastPart = urlParts[urlParts.length - 1]; + + // Handle cases like "why_did_the_dday_beach_landing_soldiers_carry_all/#2025-10-14T23:33:16+00:00" + let title = lastPart.split("#")[0]; + + // Convert URL-encoded characters back to readable text + title = decodeURIComponent(title); + + // Replace underscores with spaces and capitalize first letter of each word + title = title.replace(/_/g, " ").replace(/\b\w/g, (l) => l.toUpperCase()); + + return title; + } catch (error) { + console.error("Error extracting real title from URL:", error); + return ""; + } + } + + return ""; +} + +// Fetch the initial 100 posts from our API async function fetchRedditPosts() { try { const response = await fetch("/posts"); @@ -118,15 +231,40 @@ async function fetchMorePosts(currentCount) { } async function loadInitialPosts() { - try { - const posts = await fetchRedditPosts(); - renderPosts(posts); - currentPostCount = posts.length; + // First try to load from cache + const cachedPosts = getCachedPosts(); + + if (cachedPosts && cachedPosts.length > 0) { + console.log("Loading posts from cache"); + renderPosts(cachedPosts); + currentPostCount = cachedPosts.length; // Check if we have more posts or not const totalResponse = await fetch("/posts/total"); const totalData = await totalResponse.json(); - if (totalData.total <= 10) { + if (totalData.total <= 100) { + document.getElementById("no-more").classList.remove("hidden"); + } + + return; + } + + // If no cache, fetch from API + try { + console.log("Fetching posts from API"); + const posts = await fetchRedditPosts(); + renderPosts(posts); + currentPostCount = posts.length; + + // Cache the posts + if (posts.length > 0) { + cachePosts(posts); + } + + // Check if we have more posts or not + const totalResponse = await fetch("/posts/total"); + const totalData = await totalResponse.json(); + if (totalData.total <= 100) { document.getElementById("no-more").classList.remove("hidden"); } } catch (error) { @@ -161,7 +299,7 @@ async function loadMorePosts() { } } -// Infinite scroll implementation +// Set up infinite scroll with Intersection Observer function setupInfiniteScroll() { const container = document.getElementById("posts-container"); @@ -192,6 +330,12 @@ document.addEventListener("DOMContentLoaded", () => { loadInitialPosts(); setupInfiniteScroll(); + // Add event listener to dark mode toggle button + const darkModeToggle = document.getElementById("dark-mode-toggle"); + if (darkModeToggle) { + darkModeToggle.addEventListener("click", toggleDarkMode); + } + // Also handle scroll manually for browsers that don't support Intersection Observer window.addEventListener("scroll", () => { if ( diff --git a/src/website/index.html b/src/website/index.html index eec5189..e74a9cf 100644 --- a/src/website/index.html +++ b/src/website/index.html @@ -10,6 +10,13 @@

Red Head

+
diff --git a/src/website/styles.css b/src/website/styles.css index 07072fa..90b9a90 100644 --- a/src/website/styles.css +++ b/src/website/styles.css @@ -12,6 +12,14 @@ body { line-height: 1.6; color: #333; background-color: #f0f0f0; + transition: + background-color 0.3s, + color 0.3s; +} + +body.dark-mode { + background-color: #1a1a1a; + color: #e0e0e0; } .container { @@ -27,6 +35,14 @@ body { position: sticky; top: 0; z-index: 100; + display: flex; + justify-content: space-between; + align-items: center; +} + +.header.dark-mode { + background-color: #2d2d2d; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.3); } .header h1 { @@ -35,56 +51,126 @@ body { color: #ff4500; } +.header.dark-mode h1 { + color: #ff6b35; +} + .content { padding: 20px 0; } .posts-container { - display: grid; - gap: 20px; + display: flex; + flex-direction: column; + gap: 12px; } -.post { +/* New rectangular post card design */ +.post-card { background-color: white; - border-radius: 8px; - box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); + border-radius: 4px; + box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1); overflow: hidden; transition: transform 0.2s, box-shadow 0.2s; + display: flex; + flex-direction: column; + min-height: 100px; } -.post:hover { - transform: translateY(-2px); - box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15); +.post-card.dark-mode { + background-color: #2d2d2d; + box-shadow: 0 1px 2px rgba(0, 0, 0, 0.3); +} + +.post-card:hover { + transform: translateY(-1px); + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.15); +} + +.post-card.dark-mode:hover { + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.5); } .post-content { - padding: 20px; + padding: 12px; + flex-grow: 1; + display: flex; + flex-direction: column; } .post-title { - font-size: 1.3rem; - margin-bottom: 15px; + font-size: 1.1rem; + margin-bottom: 8px; color: #000; text-decoration: none; display: block; + flex-grow: 1; +} + +.post-title.dark-mode { + color: #e0e0e0; } .post-title:hover { color: #ff4500; } -.post-image-container { - width: 100%; - height: 250px; - overflow: hidden; - background-color: #f8f8f8; - border-radius: 4px; +.post-title.dark-mode:hover { + color: #ff6b35; +} + +/* Dark mode toggle button */ +.dark-mode-toggle { + background-color: #f0f0f0; + border: none; + border-radius: 50%; + width: 40px; + height: 40px; + cursor: pointer; display: flex; align-items: center; justify-content: center; - margin-bottom: 15px; + transition: background-color 0.3s; + box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1); +} + +.dark-mode-toggle.dark-mode { + background-color: #404040; + box-shadow: 0 1px 2px rgba(0, 0, 0, 0.3); +} + +.dark-mode-toggle:hover { + background-color: #e0e0e0; +} + +.dark-mode-toggle.dark-mode:hover { + background-color: #505050; +} + +.dark-mode-icon { + font-size: 1.2rem; + transition: transform 0.3s; +} + +.dark-mode-toggle.dark-mode .dark-mode-icon { + transform: rotate(180deg); +} + +.post-image-container { + width: 100%; + height: 130px; + overflow: hidden; + background-color: #f8f8f8; + display: flex; + align-items: center; + justify-content: center; + margin-bottom: 8px; +} + +.post-image-container.dark-mode { + background-color: #3a3a3a; } .post-image { @@ -94,7 +180,7 @@ body { transition: transform 0.3s; } -.post:hover .post-image { +.post-card:hover .post-image { transform: scale(1.05); } @@ -103,6 +189,114 @@ body { font-size: 1.2rem; } +.post-content.dark-mode { + background-color: #2d2d2d; +} + +.post-buttons { + display: flex; + gap: 6px; + padding: 0 12px 12px; + flex-wrap: wrap; + align-self: flex-start; + margin-top: 4px; +} + +.post-button { + background-color: #ff4500; + color: white; + text-decoration: none; + padding: 6px 10px; + border-radius: 3px; + font-size: 0.8rem; + transition: background-color 0.2s; + border: none; + cursor: pointer; + flex-shrink: 0; +} + +.post-button.dark-mode { + background-color: #ff6b35; +} + +.post-button:hover { + background-color: #e03d00; +} + +.post-button.dark-mode:hover { + background-color: #ff7b45; +} + +/* Limit body length on wider displays */ +@media (min-width: 769px) { + .post-content { + max-height: 15vh; /* About 1/6 screen height for wide displays */ + overflow-y: auto; + padding: 12px; + } + + .post-card { + min-height: 100px; + } +} + +/* Mobile responsive design */ +@media (max-width: 768px) { + .container { + padding: 0 10px; + } + + .header { + padding: 10px 0; + } + + .header h1 { + font-size: 1.5rem; + } + + .post-content { + padding: 10px; + } + + .post-title { + font-size: 1rem; + margin-bottom: 6px; + } + + .post-image-container { + height: 100px; + margin-bottom: 6px; + } + + .post-buttons { + padding: 0 10px 10px; + } + + .post-button { + padding: 5px 8px; + font-size: 0.75rem; + } +} + +@media (max-width: 480px) { + .post-image-container { + height: 80px; + } + + .header h1 { + font-size: 1.3rem; + } + + .post-title { + font-size: 0.9rem; + } + + .post-card { + min-height: 90px; + } +} + +/* Loading and no more messages */ .loading, .no-more { text-align: center; @@ -111,80 +305,11 @@ body { color: #666; } +.loading.dark-mode, +.no-more.dark-mode { + color: #aaa; +} + .hidden { display: none; } - -/* Responsive design */ -@media (max-width: 768px) { - .container { - padding: 0 10px; - } - - .header h1 { - font-size: 1.5rem; - } - - .post-content { - padding: 15px; - } - - .post-title { - font-size: 1.1rem; - } - - .post-image-container { - height: 200px; - } -} - -@media (max-width: 480px) { - .post-image-container { - height: 150px; - } - - .header h1 { - font-size: 1.3rem; - } - - .post-title { - font-size: 1rem; - } -} - -.post-buttons { - display: flex; - gap: 10px; - padding: 0 20px 20px; - flex-wrap: wrap; -} - -.post-button { - background-color: #ff4500; - color: white; - text-decoration: none; - padding: 8px 16px; - border-radius: 4px; - font-size: 0.9rem; - transition: background-color 0.2s; - border: none; - cursor: pointer; -} - -.post-button:hover { - background-color: #e03d00; -} - -/* Grid layout for desktop */ -@media (min-width: 769px) { - .posts-container { - grid-template-columns: repeat(auto-fill, minmax(350px, 1fr)); - } -} - -/* Tablet view */ -@media (min-width: 481px) and (max-width: 768px) { - .posts-container { - grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); - } -}