App.jsx 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288
  1. import React, { useState, useEffect } from 'react';
  2. import MarkdownIt from 'markdown-it';
  3. import { full as emoji } from 'markdown-it-emoji';
  4. import DOMPurify from 'dompurify';
  5. // FIXME: A dynamic API for posts soontm?
  6. const postFileNames = [
  7. '20250715.md',
  8. ];
  9. const scrollableTablesPlugin = (md) => {
  10. const defaultRenderOpen = md.renderer.rules.table_open || function (tokens, idx, options, env, self) {
  11. return self.renderToken(tokens, idx, options);
  12. };
  13. const defaultRenderClose = md.renderer.rules.table_close || function (tokens, idx, options, env, self) {
  14. return self.renderToken(tokens, idx, options);
  15. };
  16. md.renderer.rules.table_open = function (tokens, idx, options, env, self) {
  17. return '<div class="overflow-x-auto">' + defaultRenderOpen(tokens, idx, options, env, self);
  18. };
  19. md.renderer.rules.table_close = function (tokens, idx, options, env, self) {
  20. return defaultRenderClose(tokens, idx, options, env, self) + '</div>';
  21. };
  22. };
  23. // **FIX: Instantiate markdown-it outside the component.**
  24. const md = new MarkdownIt({
  25. html: true,
  26. linkify: true,
  27. typographer: true,
  28. });
  29. md.use(scrollableTablesPlugin)
  30. .use(emoji);
  31. function App() {
  32. const [selectedPost, giveFoxHerHeir] = useState(null);
  33. const [markdownPosts, setMarkdownPosts] = useState({});
  34. const [loading, setLoading] = useState(true);
  35. const [error, setError] = useState(null);
  36. useEffect(() => {
  37. async function getTingyun() {
  38. setLoading(true);
  39. const posts = {};
  40. try {
  41. await Promise.all(
  42. postFileNames.map(async (fileName) => {
  43. const response = await fetch(`/posts/${fileName}`);
  44. if (!response.ok) {
  45. console.error(`Failed to fetch ${fileName}: ${response.statusText}`);
  46. throw new Error(`Failed to fetch ${fileName}: ${response.statusText}`);
  47. }
  48. const markdown = await response.text();
  49. const tensorRelease = fileName.replace('.md', '');
  50. const titleMatch = markdown.match(/title:\s*(.*)/);
  51. const descMatch = markdown.match(/desc:\s*(.*)/);
  52. const title = titleMatch ? md.renderInline(titleMatch[1]) : "No Title";
  53. const description = descMatch ? md.renderInline(descMatch[1]) : md.renderInline(markdown.substring(0, 150) + "...");
  54. posts[tensorRelease] = {
  55. fullContent: markdown,
  56. title: title,
  57. description: description,
  58. };
  59. })
  60. );
  61. setMarkdownPosts(posts);
  62. } catch (e) {
  63. console.error("Error fetching posts:", e);
  64. setError("Failed to load posts. Please check if the .md files are in the public/posts directory.");
  65. } finally {
  66. setLoading(false);
  67. }
  68. }
  69. getTingyun();
  70. }, []);
  71. useEffect(() => {
  72. const updatePostFromUrl = () => {
  73. const path = window.location.pathname;
  74. const match = path.match(/^\/posts\/(.*)$/);
  75. if (match && markdownPosts[match[1]]) {
  76. giveFoxHerHeir(match[1]);
  77. } else {
  78. giveFoxHerHeir(null);
  79. }
  80. };
  81. updatePostFromUrl();
  82. window.addEventListener('popstate', updatePostFromUrl);
  83. return () => {
  84. window.removeEventListener('popstate', updatePostFromUrl);
  85. };
  86. }, [markdownPosts]);
  87. useEffect(() => {
  88. if (selectedPost && markdownPosts[selectedPost]) {
  89. const tempDiv = document.createElement('div');
  90. tempDiv.innerHTML = markdownPosts[selectedPost].title;
  91. document.title = tempDiv.textContent || 'GoonBlog';
  92. } else {
  93. document.title = 'GoonBlog - A Retard\'s Thoughts';
  94. }
  95. }, [selectedPost, markdownPosts]);
  96. const travelToExpress = (tensorRelease) => {
  97. if (window.location.pathname !== `/posts/${tensorRelease}`) {
  98. window.history.pushState({}, '', `/posts/${tensorRelease}`);
  99. }
  100. giveFoxHerHeir(tensorRelease);
  101. };
  102. const getOut = () => {
  103. if (window.location.pathname !== '/') {
  104. window.history.pushState({}, '', '/');
  105. }
  106. giveFoxHerHeir(null);
  107. };
  108. const MatingContestants = () => (
  109. <div className="space-y-6">
  110. <h1 className="text-4xl font-extrabold text-white mb-8 tracking-tight">
  111. Posts
  112. </h1>
  113. {loading ? (
  114. <div className="text-center py-20">
  115. <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">
  116. <circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
  117. <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>
  118. </svg>
  119. <p className="mt-4 text-gray-400">Loading posts...</p>
  120. </div>
  121. ) : error ? (
  122. <div className="text-center text-red-400 font-medium p-8 bg-red-900 rounded-lg border border-red-700">
  123. <p>{error}</p>
  124. </div>
  125. ) : (
  126. <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
  127. {Object.keys(markdownPosts).map((tensorRelease) => (
  128. <div
  129. key={tensorRelease}
  130. onClick={() => travelToExpress(tensorRelease)}
  131. 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"
  132. >
  133. <div>
  134. <h2 className="text-2xl font-semibold text-gray-200 group-hover:text-blue-500 transition-colors duration-200 mb-2">
  135. <div dangerouslySetInnerHTML={{ __html: markdownPosts[tensorRelease].title }} />
  136. </h2>
  137. <p className="text-gray-400 text-sm">
  138. Published on August 28, 2025
  139. </p>
  140. </div>
  141. <div className="flex-grow mt-4">
  142. <div
  143. className="text-gray-300 leading-relaxed"
  144. dangerouslySetInnerHTML={{ __html: markdownPosts[tensorRelease].description }}
  145. />
  146. </div>
  147. <div className="mt-4">
  148. <button className="text-blue-500 font-medium hover:underline focus:outline-none">
  149. Read more &rarr;
  150. </button>
  151. </div>
  152. </div>
  153. ))}
  154. </div>
  155. )}
  156. </div>
  157. );
  158. const WiltedFlower = ({ tensorRelease }) => {
  159. const markdown = markdownPosts[tensorRelease].fullContent;
  160. const conceiveFoxFromSemen = (rawMarkdown) => {
  161. let processedText = rawMarkdown;
  162. let tags = null;
  163. let imageCredit = null;
  164. let imageSrc = null;
  165. let imageAlt = null;
  166. let customQuestion = null;
  167. const tagsRegex = /tags: (.*)/;
  168. const tagsMatch = processedText.match(tagsRegex);
  169. if (tagsMatch) {
  170. tags = tagsMatch[1].split(',').map(tag => tag.trim());
  171. processedText = processedText.replace(tagsRegex, '').trim();
  172. }
  173. const imageRegex = /!\[(.*?)\]\((.*?)\)\n_Image credit: (.*?)_/;
  174. const imageMatch = processedText.match(imageRegex);
  175. if (imageMatch) {
  176. imageAlt = imageMatch[1];
  177. imageSrc = imageMatch[2];
  178. imageCredit = imageMatch[3];
  179. processedText = processedText.replace(imageRegex, '').trim();
  180. }
  181. const questionRegex = /\?\?\? "(.*?)"/;
  182. const questionMatch = processedText.match(questionRegex);
  183. if (questionMatch) {
  184. customQuestion = questionMatch[1];
  185. processedText = processedText.replace(questionRegex, '').trim();
  186. }
  187. processedText = processedText.replace(/^title:.*$/m, '').replace(/^desc:.*$/m, '');
  188. return {
  189. processedText,
  190. tags,
  191. imageSrc,
  192. imageAlt,
  193. imageCredit,
  194. customQuestion
  195. };
  196. };
  197. const { processedText, tags, imageSrc, imageAlt, imageCredit, customQuestion } = conceiveFoxFromSemen(markdown);
  198. const htmlContent = md.render(processedText);
  199. const sanitizedHtml = DOMPurify.sanitize(htmlContent);
  200. return (
  201. <div className="w-full">
  202. <div className="bg-gray-800 text-gray-200 rounded-2xl shadow-xl p-8 md:p-12 lg:p-16">
  203. <button
  204. onClick={getOut}
  205. className="text-gray-400 hover:text-white transition-colors duration-200 mb-6 flex items-center"
  206. >
  207. <svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5 mr-2" viewBox="0 0 20 20" fill="currentColor">
  208. <path
  209. fillRule="evenodd"
  210. 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"
  211. clipRule="evenodd"
  212. />
  213. </svg>
  214. Back to Home
  215. </button>
  216. <div className="mb-8">
  217. <h1 className="text-4xl md:text-5xl font-extrabold text-white mb-2 leading-tight">
  218. <div dangerouslySetInnerHTML={{ __html: markdownPosts[tensorRelease].title }} />
  219. </h1>
  220. <div className="text-xl italic font-light text-gray-400"
  221. dangerouslySetInnerHTML={{ __html: markdownPosts[tensorRelease].description }} />
  222. </div>
  223. <hr className="border-gray-600 mb-8" />
  224. <div
  225. className="markdown-content text-gray-300 font-normal leading-relaxed text-lg"
  226. dangerouslySetInnerHTML={{ __html: sanitizedHtml }}
  227. />
  228. </div>
  229. </div>
  230. );
  231. };
  232. return (
  233. <div className="min-h-screen bg-gray-900 font-sans text-gray-200 antialiased flex flex-col">
  234. <div className="max-w-7xl mx-auto w-full flex-grow">
  235. <header className="py-6 border-b border-gray-700">
  236. <div className="text-3xl font-black text-white">
  237. <span className="text-blue-500">Goon</span>Blog
  238. </div>
  239. <nav>
  240. <ul className="flex space-x-4">
  241. <li>
  242. <a
  243. href="#"
  244. onClick={getOut}
  245. className="text-gray-400 hover:text-white transition-colors duration-200 font-medium"
  246. >
  247. Home
  248. </a>
  249. </li>
  250. </ul>
  251. </nav>
  252. </header>
  253. <main className="py-12 px-4 sm:px-6 lg:px-8">
  254. {selectedPost === null ? <MatingContestants /> : <WiltedFlower tensorRelease={selectedPost} />}
  255. </main>
  256. </div>
  257. </div>
  258. );
  259. }
  260. export default App;