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
82 changes: 82 additions & 0 deletions src/components/TableOfContents.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import { useEffect, useState } from "react";

export default function TableOfContents({ className = "" }) {
const [headings, setHeadings] = useState([]);
const [activeId, setActiveId] = useState("");

useEffect(() => {
// Find all headings inside the prose container
const elements = Array.from(
document.querySelectorAll(".prose h2, .prose h3"),
);

const headingData = elements
.map((element) => ({
id: element.id,
text: element.innerText,
level: Number(element.tagName.substring(1)),
}))
.filter((h) => h.id); // Only include headings with IDs

setHeadings(headingData);

const callback = (entries) => {
// Find the entry that is currently intersecting
const visibleEntries = entries.filter((entry) => entry.isIntersecting);

if (visibleEntries.length > 0) {
// Get the top-most visible heading
setActiveId(visibleEntries[0].target.id);
}
};

const observer = new IntersectionObserver(callback, {
rootMargin: "0px 0px -80% 0px",
threshold: 1.0,
});

elements.forEach((element) => {
observer.observe(element);
});

return () => observer.disconnect();
}, []);

if (headings.length === 0) {
return null;
}

return (
<nav className={`toc ${className}`}>
<h4 className="text-label-lg font-headline text-on-surface mb-4">
Table of Contents
</h4>
<ul className="space-y-2 border-l border-surface-stroke">
{headings.map((heading) => (
<li
key={heading.id}
className={`transition-colors ${heading.level === 3 ? "ml-4" : ""}`}
>
<a
href={`#${heading.id}`}
className={`block py-1 pl-3 text-sm -ml-[1px] border-l-2 transition-all hover:text-primary-fixed ${
activeId === heading.id
? "border-primary-fixed text-primary-fixed font-medium"
: "border-transparent text-on-surface-variant hover:border-primary-fixed/50"
}`}
onClick={(e) => {
e.preventDefault();
document.getElementById(heading.id)?.scrollIntoView({
behavior: "smooth",
});
setActiveId(heading.id);
}}
>
{heading.text}
</a>
</li>
))}
</ul>
</nav>
);
}
163 changes: 86 additions & 77 deletions src/pages/BlogPostPage.jsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { Helmet } from "react-helmet-async";
import { Link, useParams } from "react-router-dom";
import { ArticleSchema } from "../components/seo/JsonLd";
import TableOfContents from "../components/TableOfContents";
import { SITE_URL } from "../constants/seo";
import { getAllPosts, getPostBySlug } from "../utils/blog";

Expand Down Expand Up @@ -97,93 +98,101 @@ export default function BlogPostPage() {
tags={tags}
/>

<section className="relative overflow-hidden pt-36 pb-24 md:pt-44 md:pb-32">
<section className="relative pt-36 pb-24 md:pt-44 md:pb-32">
{/* Aurora Glows for premium aesthetics */}
<div className="absolute inset-0 pointer-events-none">
<div className="absolute inset-0 pointer-events-none overflow-hidden">
<div className="absolute -top-48 -right-48 w-[600px] h-[600px] aurora-glow-yellow opacity-10 animate-float-slow" />
<div className="absolute top-1/3 -left-48 w-[600px] h-[600px] aurora-glow-teal opacity-5 animate-float-slower" />
</div>

<div className="section-container relative z-10">
<article className="mx-auto max-w-3xl">
<header className="mb-12">
<Link
to="/blog"
className="text-sm text-on-surface-variant hover:text-primary-fixed transition-colors mb-6 inline-block"
>
← Back to Blog
</Link>

{tags?.length > 0 && (
<div className="flex flex-wrap gap-2 mb-4 animate-fade-in-scale">
{tags.map((tag) => (
<div className="mx-auto max-w-5xl flex flex-col lg:flex-row gap-12 items-start">
<article className="flex-1 w-full max-w-3xl mx-auto lg:mx-0">
<header className="mb-12">
<Link
to="/blog"
className="text-sm text-on-surface-variant hover:text-primary-fixed transition-colors mb-6 inline-block"
>
← Back to Blog
</Link>

{tags?.length > 0 && (
<div className="flex flex-wrap gap-2 mb-4 animate-fade-in-scale">
{tags.map((tag) => (
<Link
key={tag}
to={`/blog?tag=${encodeURIComponent(tag)}`}
className="rounded-full bg-primary-fixed/10 px-3 py-0.5 text-xs text-primary-fixed hover:bg-primary-fixed/20 transition-colors"
>
{tag}
</Link>
))}
</div>
)}

<h1 className="text-display-lg text-gradient mb-4 animate-reveal-up animation-delay-200">
{title}
</h1>

<div className="flex flex-wrap items-center gap-x-4 gap-y-1 text-sm text-on-surface-variant animate-reveal-up animation-delay-300">
<span>{author}</span>
<span aria-hidden="true">·</span>
<time dateTime={publishedAt}>{formatDate(date)}</time>
{readingTime && (
<>
<span aria-hidden="true">·</span>
<span>{readingTime}</span>
</>
)}
</div>
</header>

<TableOfContents className="block lg:hidden mb-12" />

<div className="prose prose-invert prose-headings:font-headline prose-headings:text-on-surface prose-p:text-on-surface-variant prose-a:text-primary-fixed prose-a:no-underline hover:prose-a:underline prose-strong:text-on-surface prose-code:text-secondary-fixed prose-code:bg-surface-container prose-code:px-1.5 prose-code:py-0.5 prose-code:rounded prose-code:text-sm prose-pre:bg-surface-container prose-pre:border prose-pre:border-surface-stroke prose-blockquote:border-l-primary-fixed prose-blockquote:text-on-surface-variant prose-li:text-on-surface-variant prose-th:text-on-surface prose-td:text-on-surface-variant max-w-none">
{Component ? <Component /> : null}
</div>

<nav className="mt-16 border-t border-surface-stroke pt-8">
<div className="flex flex-col sm:flex-row justify-between gap-4">
{prevPost ? (
<Link
to={`/blog/${prevPost.slug}`}
className="group text-left"
>
<span className="text-label-sm text-on-surface-variant">
← Previous
</span>
<p className="text-body-md text-on-surface group-hover:text-primary-fixed transition-colors mt-1">
{prevPost.title}
</p>
</Link>
) : (
<div />
)}
{nextPost ? (
<Link
key={tag}
to={`/blog?tag=${encodeURIComponent(tag)}`}
className="rounded-full bg-primary-fixed/10 px-3 py-0.5 text-xs text-primary-fixed hover:bg-primary-fixed/20 transition-colors"
to={`/blog/${nextPost.slug}`}
className="group text-right"
>
{tag}
<span className="text-label-sm text-on-surface-variant">
Next →
</span>
<p className="text-body-md text-on-surface group-hover:text-primary-fixed transition-colors mt-1">
{nextPost.title}
</p>
</Link>
))}
) : (
<div />
)}
</div>
)}

<h1 className="text-display-lg text-gradient mb-4 animate-reveal-up animation-delay-200">
{title}
</h1>

<div className="flex flex-wrap items-center gap-x-4 gap-y-1 text-sm text-on-surface-variant animate-reveal-up animation-delay-300">
<span>{author}</span>
<span aria-hidden="true">·</span>
<time dateTime={publishedAt}>{formatDate(date)}</time>
{readingTime && (
<>
<span aria-hidden="true">·</span>
<span>{readingTime}</span>
</>
)}
</div>
</header>

<div className="prose prose-invert prose-headings:font-headline prose-headings:text-on-surface prose-p:text-on-surface-variant prose-a:text-primary-fixed prose-a:no-underline hover:prose-a:underline prose-strong:text-on-surface prose-code:text-secondary-fixed prose-code:bg-surface-container prose-code:px-1.5 prose-code:py-0.5 prose-code:rounded prose-code:text-sm prose-pre:bg-surface-container prose-pre:border prose-pre:border-surface-stroke prose-blockquote:border-l-primary-fixed prose-blockquote:text-on-surface-variant prose-li:text-on-surface-variant prose-th:text-on-surface prose-td:text-on-surface-variant max-w-none">
{Component ? <Component /> : null}
</div>

<nav className="mt-16 border-t border-surface-stroke pt-8">
<div className="flex flex-col sm:flex-row justify-between gap-4">
{prevPost ? (
<Link
to={`/blog/${prevPost.slug}`}
className="group text-left"
>
<span className="text-label-sm text-on-surface-variant">
← Previous
</span>
<p className="text-body-md text-on-surface group-hover:text-primary-fixed transition-colors mt-1">
{prevPost.title}
</p>
</Link>
) : (
<div />
)}
{nextPost ? (
<Link
to={`/blog/${nextPost.slug}`}
className="group text-right"
>
<span className="text-label-sm text-on-surface-variant">
Next →
</span>
<p className="text-body-md text-on-surface group-hover:text-primary-fixed transition-colors mt-1">
{nextPost.title}
</p>
</Link>
) : (
<div />
)}
</div>
</nav>
</article>
</nav>
</article>

<aside className="hidden lg:block w-64 sticky top-32 shrink-0">
<TableOfContents />
</aside>
</div>
</div>
</section>
</>
Expand Down
Loading