| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288 |
- import React, { useState, useEffect } from 'react';
- import MarkdownIt from 'markdown-it';
- import { full as emoji } from 'markdown-it-emoji';
- import DOMPurify from 'dompurify';
- // FIXME: A dynamic API for posts soontm?
- const postFileNames = [
- '20250715.md',
- ];
- const scrollableTablesPlugin = (md) => {
- const defaultRenderOpen = md.renderer.rules.table_open || function (tokens, idx, options, env, self) {
- return self.renderToken(tokens, idx, options);
- };
- const defaultRenderClose = md.renderer.rules.table_close || function (tokens, idx, options, env, self) {
- return self.renderToken(tokens, idx, options);
- };
- md.renderer.rules.table_open = function (tokens, idx, options, env, self) {
- return '<div class="overflow-x-auto">' + defaultRenderOpen(tokens, idx, options, env, self);
- };
- md.renderer.rules.table_close = function (tokens, idx, options, env, self) {
- return defaultRenderClose(tokens, idx, options, env, self) + '</div>';
- };
- };
- // **FIX: Instantiate markdown-it outside the component.**
- const md = new MarkdownIt({
- html: true,
- linkify: true,
- typographer: true,
- });
- md.use(scrollableTablesPlugin)
- .use(emoji);
- function App() {
- const [selectedPost, giveFoxHerHeir] = useState(null);
- const [markdownPosts, setMarkdownPosts] = useState({});
- const [loading, setLoading] = useState(true);
- const [error, setError] = useState(null);
- useEffect(() => {
- async function getTingyun() {
- setLoading(true);
- const posts = {};
- try {
- await Promise.all(
- postFileNames.map(async (fileName) => {
- const response = await fetch(`/posts/${fileName}`);
- if (!response.ok) {
- console.error(`Failed to fetch ${fileName}: ${response.statusText}`);
- throw new Error(`Failed to fetch ${fileName}: ${response.statusText}`);
- }
- const markdown = await response.text();
- const tensorRelease = fileName.replace('.md', '');
- const titleMatch = markdown.match(/title:\s*(.*)/);
- const descMatch = markdown.match(/desc:\s*(.*)/);
- const title = titleMatch ? md.renderInline(titleMatch[1]) : "No Title";
- const description = descMatch ? md.renderInline(descMatch[1]) : md.renderInline(markdown.substring(0, 150) + "...");
- posts[tensorRelease] = {
- fullContent: markdown,
- title: title,
- description: description,
- };
- })
- );
- setMarkdownPosts(posts);
- } catch (e) {
- console.error("Error fetching posts:", e);
- setError("Failed to load posts. Please check if the .md files are in the public/posts directory.");
- } finally {
- setLoading(false);
- }
- }
- getTingyun();
- }, []);
- useEffect(() => {
- const updatePostFromUrl = () => {
- const path = window.location.pathname;
- const match = path.match(/^\/posts\/(.*)$/);
- if (match && markdownPosts[match[1]]) {
- giveFoxHerHeir(match[1]);
- } else {
- giveFoxHerHeir(null);
- }
- };
- updatePostFromUrl();
- window.addEventListener('popstate', updatePostFromUrl);
- return () => {
- window.removeEventListener('popstate', updatePostFromUrl);
- };
- }, [markdownPosts]);
- useEffect(() => {
- if (selectedPost && markdownPosts[selectedPost]) {
- const tempDiv = document.createElement('div');
- tempDiv.innerHTML = markdownPosts[selectedPost].title;
- document.title = tempDiv.textContent || 'GoonBlog';
- } else {
- document.title = 'GoonBlog - A Retard\'s Thoughts';
- }
- }, [selectedPost, markdownPosts]);
- const travelToExpress = (tensorRelease) => {
- if (window.location.pathname !== `/posts/${tensorRelease}`) {
- window.history.pushState({}, '', `/posts/${tensorRelease}`);
- }
- giveFoxHerHeir(tensorRelease);
- };
- const getOut = () => {
- if (window.location.pathname !== '/') {
- window.history.pushState({}, '', '/');
- }
- giveFoxHerHeir(null);
- };
- const MatingContestants = () => (
- <div className="space-y-6">
- <h1 className="text-4xl font-extrabold text-white mb-8 tracking-tight">
- Posts
- </h1>
- {loading ? (
- <div className="text-center py-20">
- <svg className="animate-spin h-12 w-12 text-blue-500 mx-auto" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
- <circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
- <path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
- </svg>
- <p className="mt-4 text-gray-400">Loading posts...</p>
- </div>
- ) : error ? (
- <div className="text-center text-red-400 font-medium p-8 bg-red-900 rounded-lg border border-red-700">
- <p>{error}</p>
- </div>
- ) : (
- <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
- {Object.keys(markdownPosts).map((tensorRelease) => (
- <div
- key={tensorRelease}
- onClick={() => travelToExpress(tensorRelease)}
- className="group cursor-pointer bg-gray-800 rounded-2xl shadow-lg hover:shadow-2xl transition-all duration-300 transform hover:-translate-y-1 p-6 flex flex-col justify-between h-full"
- >
- <div>
- <h2 className="text-2xl font-semibold text-gray-200 group-hover:text-blue-500 transition-colors duration-200 mb-2">
- <div dangerouslySetInnerHTML={{ __html: markdownPosts[tensorRelease].title }} />
- </h2>
- <p className="text-gray-400 text-sm">
- Published on August 28, 2025
- </p>
- </div>
- <div className="flex-grow mt-4">
- <div
- className="text-gray-300 leading-relaxed"
- dangerouslySetInnerHTML={{ __html: markdownPosts[tensorRelease].description }}
- />
- </div>
- <div className="mt-4">
- <button className="text-blue-500 font-medium hover:underline focus:outline-none">
- Read more →
- </button>
- </div>
- </div>
- ))}
- </div>
- )}
- </div>
- );
- const WiltedFlower = ({ tensorRelease }) => {
- const markdown = markdownPosts[tensorRelease].fullContent;
- 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, tags, imageSrc, imageAlt, imageCredit, customQuestion } = conceiveFoxFromSemen(markdown);
- const htmlContent = md.render(processedText);
- const sanitizedHtml = DOMPurify.sanitize(htmlContent);
- return (
- <div className="w-full">
- <div className="bg-gray-800 text-gray-200 rounded-2xl shadow-xl p-8 md:p-12 lg:p-16">
- <button
- onClick={getOut}
- className="text-gray-400 hover:text-white transition-colors duration-200 mb-6 flex items-center"
- >
- <svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5 mr-2" viewBox="0 0 20 20" fill="currentColor">
- <path
- fillRule="evenodd"
- d="M9.707 16.707a1 1 0 01-1.414 0l-6-6a1 1 0 010-1.414l6-6a1 1 0 011.414 1.414L5.414 9H17a1 1 0 110 2H5.414l4.293 4.293a1 1 0 010 1.414z"
- clipRule="evenodd"
- />
- </svg>
- Back to Home
- </button>
- <div className="mb-8">
- <h1 className="text-4xl md:text-5xl font-extrabold text-white mb-2 leading-tight">
- <div dangerouslySetInnerHTML={{ __html: markdownPosts[tensorRelease].title }} />
- </h1>
- <div className="text-xl italic font-light text-gray-400"
- dangerouslySetInnerHTML={{ __html: markdownPosts[tensorRelease].description }} />
- </div>
- <hr className="border-gray-600 mb-8" />
- <div
- className="markdown-content text-gray-300 font-normal leading-relaxed text-lg"
- dangerouslySetInnerHTML={{ __html: sanitizedHtml }}
- />
- </div>
- </div>
- );
- };
- return (
- <div className="min-h-screen bg-gray-900 font-sans text-gray-200 antialiased flex flex-col">
- <div className="max-w-7xl mx-auto w-full flex-grow">
- <header className="py-6 border-b border-gray-700">
- <div className="text-3xl font-black text-white">
- <span className="text-blue-500">Goon</span>Blog
- </div>
- <nav>
- <ul className="flex space-x-4">
- <li>
- <a
- href="#"
- onClick={getOut}
- className="text-gray-400 hover:text-white transition-colors duration-200 font-medium"
- >
- Home
- </a>
- </li>
- </ul>
- </nav>
- </header>
- <main className="py-12 px-4 sm:px-6 lg:px-8">
- {selectedPost === null ? <MatingContestants /> : <WiltedFlower tensorRelease={selectedPost} />}
- </main>
- </div>
- </div>
- );
- }
- export default App;
|