Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
156 changes: 67 additions & 89 deletions src/App.jsx
Original file line number Diff line number Diff line change
@@ -1,125 +1,103 @@
import { useState, useEffect } from 'react';
import ReactMarkdown from 'react-markdown';
import remarkGfm from 'remark-gfm';
import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';
import { vscDarkPlus } from 'react-syntax-highlighter/dist/esm/styles/prism';
import { useState, useEffect, useCallback } from 'react';
import Analytics from './components/Analytics';
import NotFound from './pages/NotFound/NotFound';
import Home from './pages/Home/Home';
import Header from './components/Header/Header';
import Footer from './components/Footer/Footer';
import MarkdownRenderer from './components/MarkdownRenderer';
import siteConfig from './data/config.json';
import './styles/App.css';

export default function App() {
const [content, setContent] = useState('');
const [posts , setPosts] = useState([]);
const [status , setStatus] = useState('loading');

useEffect(() => {
const fetchMarkdown = useCallback((url, title) => {
fetch(url)
.then(res => res.text())
.then(text => {
document.title = title;
setContent(text);
setStatus('post');
})
.catch(() => setStatus('404'));
}, []);

const handleRouting = useCallback((allPosts) => {
const params = new URLSearchParams(window.location.search);
const redirectedPath = params.get('p');

let currentPath = redirectedPath || window.location.pathname;
const currentPath = redirectedPath || window.location.pathname;

if (redirectedPath) {
window.history.replaceState(null, '', redirectedPath);
}

fetch('/posts.json')
.then(res => res.json())
.then(data => {
setPosts(data);
const pathClean = currentPath.replace(/\.html$/, '');
const parts = pathClean.split('/').filter(Boolean);

if (parts.length === 0 || (parts.length === 1 && parts[0] === 'index')) {
document.title = siteConfig.siteName;
setStatus('home');
return;
}

const pathClean = currentPath.replace(/\.html$/, '');
const parts = pathClean.split('/').filter(Boolean);
if (parts.length === 1 && parts[0] === 'about') {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

To allow for differentiated rendering in renderContent, such as conditionally hiding the back link, consider using a more specific status for the 'about' page. For example, setStatus('about') could be used here.

      setStatus('about');

fetchMarkdown('/about.md', `About | ${siteConfig.siteName}`);
return;
}

if (parts.length === 1 && parts[0] === 'about') {
fetch('/about.md')
.then(res => res.text())
.then(text => {
setContent(text);
setStatus('post');
})
.catch(() => setStatus('404'));
return;
}
if (parts.length === 4) {
const [year, month, day, slug] = parts;

if (parts.length === 0 || (parts.length === 1 && parts[0] === 'index')) {
setStatus('home');
return;
}
const found = allPosts.find(p =>
p.year === year && p.month === month && p.day === day && p.slug === slug
);

if (parts.length === 4) {
const [year, month, day, slug] = parts;
if (found) {
fetchMarkdown(`/posts/${year}/${found.originalName}.md`, `${found.title} | ${siteConfig.siteName}`);

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

To differentiate regular blog posts from other markdown content like the 'about' page, use a more specific status. This enables granular control over rendering options, such as the showBackLink prop in MarkdownRenderer. For example, setStatus('blogPost') could be used here.

        setStatus('blogPost');

return;
}
}

setStatus('404');
}, [fetchMarkdown]);

const found = data.find(p =>
p.year === year &&
p.month === month &&
p.day === day &&
p.slug === slug
);

if (found) {
fetch(`/posts/${year}/${found.originalName}.md`)
.then(res => res.text())
.then(text => {
setContent(text);
setStatus('post');
})
.catch(() => setStatus('404'));
} else {
setStatus('404');
}
} else {
setStatus('404');
}
useEffect(() => {
fetch('/posts.json')
.then(res => res.json())
.then(data => {
setPosts(data);
handleRouting(data);
})
.catch(() => setStatus('404'));
}, []);

if (status === 'loading') {
return <div className="app-shell">Loading...</div>;
}
const onPopState = () => {
fetch('/posts.json').then(res => res.json()).then(handleRouting);

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛑 Crash Risk: Missing error handling in the popstate event handler. If the fetch request fails, the promise chain will result in an unhandled rejection, potentially causing runtime errors and poor user experience during browser navigation.

Suggested change
fetch('/posts.json').then(res => res.json()).then(handleRouting);
fetch('/posts.json').then(res => res.json()).then(handleRouting).catch(() => setStatus('404'));

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The onPopState handler currently refetches posts.json every time the browser history changes. If the posts.json file is static and its content does not change during the application's runtime, this leads to redundant network requests. It would be more efficient to reuse the posts data that has already been fetched and stored in the component's state. This change also requires adding posts to the useEffect dependency array on line 81.

      handleRouting(posts);

};

window.addEventListener('popstate', onPopState);
return () => window.removeEventListener('popstate', onPopState);
}, [handleRouting]);

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

To ensure the onPopState handler (line 76) can access the latest posts state when handleRouting(posts) is called, posts must be included in the useEffect's dependency array. This will cause the effect to re-run and re-attach the event listener if posts changes, which is acceptable since posts is expected to change only once after the initial fetch.

  }, [handleRouting, posts]);


const renderContent = () => {
switch (status) {
case 'loading': return <div className="loading">Loading...</div>;
case '404': return <NotFound />;
case 'home': return <Home posts={posts} />;
case 'post': return <MarkdownRenderer content={content} />;

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

With the introduction of more specific statuses (e.g., 'about' and 'blogPost') in handleRouting, the renderContent function can now conditionally render the MarkdownRenderer with different props. For instance, the 'about' page might not need a "Back to Home" link.

      case 'about':   return <MarkdownRenderer content={content} showBackLink={false} />;
      case 'blogPost':return <MarkdownRenderer content={content} />;

default: return <NotFound />;
}
};

return (
<>
<Analytics />
<div className="app-shell">
<Header />
{status === '404' ? (
<NotFound />
) : status === 'home' ? (
<Home posts={posts} />
) : (
<article className="markdown-body">
<ReactMarkdown
remarkPlugins={[remarkGfm]}
components={{
code({ node, inline, className, children, ...props }) {
const match = /language-(\w+)/.exec(className || '');
return !inline && match ? (
<SyntaxHighlighter
style={vscDarkPlus}
language={match[1]}
PreTag="div"
{...props}
>
{String(children).replace(/\n$/, '')}
</SyntaxHighlighter>
) : (
<code className={className} {...props}>
{children}
</code>
);
}
}}
>
{content}
</ReactMarkdown>
<hr />
<a href="/" className="back-link">← Back to Home</a>
</article>
)}
<main className="main-container">
{renderContent()}
</main>
<Footer />
</div>
</>
Expand Down
42 changes: 42 additions & 0 deletions src/components/MarkdownRenderer.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import ReactMarkdown from 'react-markdown';
import remarkGfm from 'remark-gfm';
import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';
import { vscDarkPlus } from 'react-syntax-highlighter/dist/esm/styles/prism';

export default function MarkdownRenderer({ content, showBackLink = true }) {
return (
<article className="markdown-body">
<ReactMarkdown
remarkPlugins={[remarkGfm]}
components={{
code({ node, inline, className, children, ...props }) {
const match = /language-(\w+)/.exec(className || '');
return !inline && match ? (
<SyntaxHighlighter
style={vscDarkPlus}
language={match[1]}
PreTag="div"
{...props}
>
{String(children).replace(/\n$/, '')}
</SyntaxHighlighter>
) : (
<code className={className} {...props}>
{children}
</code>
);
}
}}
>
{content}
</ReactMarkdown>

{showBackLink && (
<>
<hr />
<a href="/" className="back-link">← Back to Home</a>
</>
)}
</article>
);
}
Loading