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

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 }) => (
);
// 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 (
);
}
return (
{posts.map((post) => (
))}
);
}
// 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;