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 @@