redhead/app.js

350 lines
9.3 KiB
JavaScript

// 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-card";
postElement.onclick = () => {
window.location.href = post.url;
};
postElement.style.cursor = "pointer";
// Create the content container
const content = document.createElement("div");
content.className = "post-content";
const title = document.createElement("a");
title.href = post.url;
title.className = "post-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 buttons container
const buttonsContainer = document.createElement("div");
buttonsContainer.className = "post-buttons";
// Single button
const singleButton = document.createElement("button");
singleButton.onclick = (e) => {
e.stopPropagation();
window.location.href = post.url + "/singlefile";
};
singleButton.className = "post-button";
singleButton.textContent = "Single";
// HTML button
const htmlButton = document.createElement("button");
htmlButton.onclick = (e) => {
e.stopPropagation();
window.location.href = post.url + "/html";
};
htmlButton.className = "post-button";
htmlButton.textContent = "HTML";
// PDF button
const pdfButton = document.createElement("button");
pdfButton.onclick = (e) => {
e.stopPropagation();
window.location.href = post.url + "/pdf";
};
pdfButton.className = "post-button";
pdfButton.textContent = "PDF";
buttonsContainer.appendChild(singleButton);
buttonsContainer.appendChild(htmlButton);
buttonsContainer.appendChild(pdfButton);
// Assemble the complete post
content.appendChild(title);
content.appendChild(buttonsContainer);
postElement.appendChild(content);
return postElement;
}
function renderPosts(posts) {
const container = document.getElementById("posts-container");
posts.forEach((post) => {
const postElement = createPostElement(post);
container.appendChild(postElement);
});
}
// 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");
if (!response.ok) {
throw new Error("Failed to fetch posts");
}
const posts = await response.json();
return posts;
} catch (error) {
console.error("Error fetching initial posts:", error);
// Return empty array on error
return [];
}
}
// Fetch more posts from our API
async function fetchMorePosts(currentCount) {
try {
const response = await fetch(`/posts/more?count=${currentCount}`);
if (!response.ok) {
throw new Error("Failed to fetch more posts");
}
const posts = await response.json();
return posts;
} catch (error) {
console.error("Error fetching more posts:", error);
// Return empty array on error
return [];
}
}
async function loadInitialPosts() {
// 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 <= 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) {
console.error("Failed to load initial posts:", error);
}
}
async function loadMorePosts() {
if (isLoading) {
return;
}
isLoading = true;
document.getElementById("loading").classList.remove("hidden");
try {
const morePosts = await fetchMorePosts(currentPostCount);
if (morePosts.length > 0) {
renderPosts(morePosts);
currentPostCount += morePosts.length;
} else {
// No more posts to load
document.getElementById("no-more").classList.remove("hidden");
document.getElementById("loading").classList.add("hidden");
}
} catch (error) {
console.error("Failed to load more posts:", error);
} finally {
isLoading = false;
document.getElementById("loading").classList.add("hidden");
}
}
// Set up infinite scroll with Intersection Observer
function setupInfiniteScroll() {
const container = document.getElementById("posts-container");
// Use Intersection Observer API for efficient infinite scrolling
const observer = new IntersectionObserver(
(entries) => {
if (entries[0].isIntersecting && !isLoading) {
loadMorePosts();
}
},
{
root: null,
rootMargin: "20px",
threshold: 0.1,
},
);
// Create a sentinel element to observe
const sentinel = document.createElement("div");
sentinel.id = "sentinel";
container.appendChild(sentinel);
observer.observe(sentinel);
}
// Initialize the app when the DOM is loaded
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 (
window.innerHeight + window.scrollY >=
document.body.offsetHeight - 1000 &&
!isLoading
) {
loadMorePosts();
}
});
});