import React, { useState, useEffect } from "react"; import { Link, useParams } from "react-router-dom"; import DOMPurify from "dompurify"; import { API_BASE } from "../config"; import { createMarkdownParser } from "../utils/markdownParser"; import Layout, { SkeletonPost } from "./Layout"; import { createRoot } from "react-dom/client"; import CsvGraph from "./CsvGraph"; const md = createMarkdownParser(); 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"; }; }, []); useEffect(() => { if (!post) return; 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; if (topImage) topImage.style.clipPath = `inset(0 ${100 - val}% 0 0)`; if (handle) handle.style.left = `${val}%`; }; sliders.forEach(slider => { slider.addEventListener("input", handleSliderInput); }); const zoomReels = document.querySelectorAll(".interactive-zoom-reel"); const cleanupZoomReels = []; 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); }); // 4. Mount CSV Graphs const graphWrappers = document.querySelectorAll('.csv-graph-wrapper'); const graphRoots = []; graphWrappers.forEach(wrapper => { // Check if already mounted to avoid double mount if (wrapper.dataset.mounted) return; const dataElement = wrapper.querySelector('.csv-graph-data'); if (dataElement) { const rawData = dataElement.textContent; // 1. Clear and prepare container wrapper.innerHTML = ''; const container = document.createElement('div'); container.style.width = '100%'; wrapper.appendChild(container); // 2. Create root and render const root = createRoot(container); root.render(); wrapper.dataset.mounted = "true"; graphRoots.push(root); } }); return () => { sliders.forEach(slider => { slider.removeEventListener("input", handleSliderInput); }); lightboxImages.forEach(img => { img.removeEventListener("click", handleImageClick); }); cleanupZoomReels.forEach(cleanup => cleanup()); // Unmount graphs graphRoots.forEach(root => setTimeout(() => root.unmount(), 0)); }; }, [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", "style", "data-line"], }); return ( ← Back to Home {post.title} {post.description} ); } export default PostView;
{error || "The requested post could not be found."}