import React, { useState, useEffect } from "react"; import { BrowserRouter as Router, Routes, Route, Link, useParams, } from "react-router-dom"; import DOMPurify from "dompurify"; import { AuthProvider, useAuth } from "./contexts/AuthContext"; import { ThemeProvider } from "./contexts/ThemeContext"; // Lazy load components for performance const AdminDashboard = React.lazy(() => import("./components/AdminDashboard")); const PostEditor = React.lazy(() => import("./components/PostEditor")); const LoginForm = React.lazy(() => import("./components/LoginForm")); const ThemesManager = React.lazy(() => import("./components/ThemesManager")); const ThemeEditor = React.lazy(() => import("./components/ThemeEditor")); const MediaManager = React.lazy(() => import("./components/MediaManager")); import ProtectedRoute from "./components/ProtectedRoute"; import { createMarkdownParser } from "./utils/markdownParser"; import { API_BASE } from "./config"; // Initialize the shared markdown parser const md = createMarkdownParser(); // Loading Fallback Component const LoadingSpinner = () => (
); // Lightbox Component const Lightbox = ({ src, alt, onClose }) => { if (!src) return null; return (
× {alt} e.stopPropagation()} // Prevent closing when clicking the image />
); }; // Navigation Header Component function NavHeader() { const { isAdmin, user, logout } = useAuth(); const handleLogout = async () => { await logout(); }; return (
GoonBlog
); } // Layout ComponentWrapper const Layout = ({ children }) => (
{children}
); // Skeleton Components const SkeletonCard = () => (
); const SkeletonPost = () => (

); // Blog Home Component function BlogHome() { const [posts, setPosts] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); useEffect(() => { async function getTingyun() { setLoading(true); try { const response = await fetch(`${API_BASE}/posts`); if (!response.ok) throw new Error( `Failed to fetch posts: ${response.statusText}`, ); const postsData = await response.json(); setPosts(postsData); } catch (e) { console.error("Error fetching posts:", e); setError( "Failed to load posts. Please check if the backend server is running.", ); } finally { setLoading(false); } } getTingyun(); }, []); if (loading) { return (
{[1, 2, 3, 4, 5, 6].map((i) => ( ))}
); } if (error) { return (

Error

{error}

); } return (
{posts.map((post) => (

{post.title}

{post.description}
Read more →
))}
); } // Post View Component function PostView({ onImageClick }) { const { slug } = useParams(); const [post, setPost] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); useEffect(() => { async function fetchPost() { try { setLoading(true); const response = await fetch(`${API_BASE}/posts/${slug}`); if (!response.ok) throw new Error("Post not found"); const postData = await response.json(); setPost(postData); // Update document title document.title = postData.title || "GoonBlog"; } catch (e) { console.error("Error fetching post:", e); setError(e.message); } finally { setLoading(false); } } if (slug) { fetchPost(); } }, [slug]); useEffect(() => { if (post) { const setMeta = (name, content) => { let element = document.querySelector(`meta[name="${name}"]`); if (!element) { element = document.createElement("meta"); element.setAttribute("name", name); document.head.appendChild(element); } element.setAttribute("content", content); }; setMeta("og:title", post.title); setMeta("og:description", post.description); setMeta("og:type", "article"); setMeta("og:url", window.location.href); setMeta("twitter:title", post.title); setMeta("twitter:description", post.description); setMeta("twitter:card", "summary"); setMeta("twitter:url", window.location.href); } return () => { const metaTags = [ "og:title", "og:description", "og:type", "og:url", "og:image", "twitter:title", "twitter:description", "twitter:card", "twitter:url", "twitter:image", ]; metaTags.forEach((name) => { const element = document.querySelector(`meta[name="${name}"]`); if (element) { element.remove(); } }); }; }, [post]); useEffect(() => { // Reset title when component unmounts return () => { document.title = "GoonBlog - A Retard's Thoughts"; }; }, []); // Effect to handle interactions (Comparison Slider, Zoom Reel & Lightbox) useEffect(() => { if (!post) return; // 1. Handle Comparison Sliders (Updated to use clip-path) const sliders = document.querySelectorAll(".comparison-slider"); const handleSliderInput = (e) => { const container = e.target.closest(".comparison-wrapper"); const topImage = container.querySelector(".comparison-top"); const handle = container.querySelector(".slider-handle"); const val = e.target.value; // Use clip-path inset(top right bottom left) // We want to clip the right side based on the slider value. // If slider is at 50%, we want to show 50% of the image from the left. // So we clip 50% from the right. // Inset right value = 100 - val if (topImage) topImage.style.clipPath = `inset(0 ${100 - val}% 0 0)`; if (handle) handle.style.left = `${val}%`; }; sliders.forEach(slider => { slider.addEventListener("input", handleSliderInput); }); // 2. Handle Zoom Reels const zoomReels = document.querySelectorAll(".interactive-zoom-reel"); const cleanupZoomReels = []; // To store cleanup functions for each reel zoomReels.forEach(reel => { const images = reel.querySelectorAll(".zoom-reel-img"); const viewports = reel.querySelectorAll(".zoom-reel-viewport"); const slider = reel.querySelector(".zoom-slider"); const resetBtn = reel.querySelector(".reset-zoom"); let state = { zoom: 1, panX: 0, panY: 0, isDragging: false, startX: 0, startY: 0, initialPanX: 0, initialPanY: 0 }; const updateTransform = () => { images.forEach(img => { img.style.transform = `translate(${state.panX}px, ${state.panY}px) scale(${state.zoom})`; }); }; const handleZoomInput = (e) => { state.zoom = parseFloat(e.target.value); // Reset pan if zoom is 1 if (state.zoom === 1) { state.panX = 0; state.panY = 0; } updateTransform(); }; const handleReset = () => { state.zoom = 1; state.panX = 0; state.panY = 0; if (slider) slider.value = 1; updateTransform(); }; // Drag Logic for Viewports const handleMouseDown = (e) => { if (state.zoom <= 1) return; // Only pan if zoomed in e.preventDefault(); // Prevent standard drag state.isDragging = true; state.startX = e.clientX; state.startY = e.clientY; state.initialPanX = state.panX; state.initialPanY = state.panY; viewports.forEach(vp => vp.style.cursor = "grabbing"); }; const handleMouseMove = (e) => { if (!state.isDragging) return; e.preventDefault(); const dx = e.clientX - state.startX; const dy = e.clientY - state.startY; state.panX = state.initialPanX + dx; state.panY = state.initialPanY + dy; updateTransform(); }; const handleMouseUp = () => { state.isDragging = false; viewports.forEach(vp => vp.style.cursor = "grab"); }; if (slider) slider.addEventListener("input", handleZoomInput); if (resetBtn) resetBtn.addEventListener("click", handleReset); viewports.forEach(vp => { vp.addEventListener("mousedown", handleMouseDown); }); // We listen to document for move/up to handle drag going outside viewport document.addEventListener("mousemove", handleMouseMove); document.addEventListener("mouseup", handleMouseUp); cleanupZoomReels.push(() => { if (slider) slider.removeEventListener("input", handleZoomInput); if (resetBtn) resetBtn.removeEventListener("click", handleReset); viewports.forEach(vp => vp.removeEventListener("mousedown", handleMouseDown)); document.removeEventListener("mousemove", handleMouseMove); document.removeEventListener("mouseup", handleMouseUp); }); }); // 3. Handle Lightbox clicks const lightboxImages = document.querySelectorAll(".markdown-content img"); const handleImageClick = (e) => { // Ignore images inside comparison slider or zoom reel if (e.target.closest(".comparison-wrapper") || e.target.closest(".zoom-reel-container")) return; onImageClick(e.target.src, e.target.alt); }; lightboxImages.forEach(img => { img.addEventListener("click", handleImageClick); }); return () => { sliders.forEach(slider => { slider.removeEventListener("input", handleSliderInput); }); lightboxImages.forEach(img => { img.removeEventListener("click", handleImageClick); }); cleanupZoomReels.forEach(cleanup => cleanup()); }; }, [post, onImageClick]); if (loading) { return ( ); } if (error || !post) { return (

Post Not Found

{error || "The requested post could not be found."}

← Back to Home
); } const conceiveFoxFromSemen = (rawMarkdown) => { let processedText = rawMarkdown; let tags = null; let imageCredit = null; let imageSrc = null; let imageAlt = null; let customQuestion = null; const tagsRegex = /tags: (.*)/; const tagsMatch = processedText.match(tagsRegex); if (tagsMatch) { tags = tagsMatch[1].split(",").map((tag) => tag.trim()); processedText = processedText.replace(tagsRegex, "").trim(); } const imageRegex = /!\[(.*?)\]\((.*?)\)\n_Image credit: (.*?)_/; const imageMatch = processedText.match(imageRegex); if (imageMatch) { imageAlt = imageMatch[1]; imageSrc = imageMatch[2]; imageCredit = imageMatch[3]; processedText = processedText.replace(imageRegex, "").trim(); } const questionRegex = /\?\?\? "(.*?)"/; const questionMatch = processedText.match(questionRegex); if (questionMatch) { customQuestion = questionMatch[1]; processedText = processedText.replace(questionRegex, "").trim(); } processedText = processedText .replace(/^title:.*$/m, "") .replace(/^desc:.*$/m, ""); return { processedText, tags, imageSrc, imageAlt, imageCredit, customQuestion, }; }; const { processedText } = conceiveFoxFromSemen(post.content); const htmlContent = md.render(processedText); const sanitizedHtml = DOMPurify.sanitize(htmlContent, { ADD_TAGS: ["input"], // Allow input tags for the slider ADD_ATTR: ["type", "min", "max", "value", "step", "checked"], }); return (
← Back to Home

{post.title}

{post.description}

); } function App() { const [lightboxOpen, setLightboxOpen] = useState(false); const [lightboxImage, setLightboxImage] = useState({ src: "", alt: "" }); const openLightbox = (src, alt) => { setLightboxImage({ src, alt }); setLightboxOpen(true); }; const closeLightbox = () => { setLightboxOpen(false); }; return ( {lightboxOpen && ( )} }> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> ); } export default App;