Dark mode, slimmer post profiles, adapting titles

This commit is contained in:
Jarian Cottingham 2025-11-04 23:27:26 -06:00
parent da06d91930
commit ce8b3c716d
4 changed files with 440 additions and 119 deletions

View File

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

View File

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

View File

@ -10,6 +10,13 @@
<div class="container">
<header class="header">
<h1>Red Head</h1>
<button
id="dark-mode-toggle"
class="dark-mode-toggle"
aria-label="Toggle dark mode"
>
<span class="dark-mode-icon">🌙</span>
</button>
</header>
<main class="content">

View File

@ -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));
}
}