direct thought/story url
This commit is contained in:
156
src/app/stories/[slug]/page.tsx
Normal file
156
src/app/stories/[slug]/page.tsx
Normal file
@@ -0,0 +1,156 @@
|
|||||||
|
import { notFound } from 'next/navigation';
|
||||||
|
import Link from 'next/link';
|
||||||
|
import { loadStories } from '@/utils/mdxLoader';
|
||||||
|
import { formatReadingTime } from '@/utils/readingTime';
|
||||||
|
|
||||||
|
interface StoryPageProps {
|
||||||
|
params: {
|
||||||
|
slug: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
import fs from 'fs';
|
||||||
|
|
||||||
|
export async function generateStaticParams() {
|
||||||
|
const storiesDirectory = process.cwd() + '/src/content/stories';
|
||||||
|
|
||||||
|
try {
|
||||||
|
const filenames = fs.readdirSync(storiesDirectory);
|
||||||
|
return filenames.map((filename: string) => ({
|
||||||
|
slug: filename.replace(/\.mdx$/, ''),
|
||||||
|
}));
|
||||||
|
} catch {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function generateMetadata({ params }: StoryPageProps) {
|
||||||
|
const story = await getStoryBySlug(params.slug);
|
||||||
|
|
||||||
|
if (!story) {
|
||||||
|
return {
|
||||||
|
title: 'Story Not Found',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
title: `${story.title} | Mainasara Tsowa`,
|
||||||
|
description: story.excerpt,
|
||||||
|
openGraph: {
|
||||||
|
title: `${story.title} | Mainasara Tsowa`,
|
||||||
|
description: story.excerpt,
|
||||||
|
url: `https://mainasara.dev/stories/${params.slug}`,
|
||||||
|
},
|
||||||
|
twitter: {
|
||||||
|
title: `${story.title} | Mainasara Tsowa`,
|
||||||
|
description: story.excerpt,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getStoryBySlug(slug: string) {
|
||||||
|
const stories = await loadStories();
|
||||||
|
return stories.find(story => story.id === slug.replace(/[^a-zA-Z0-9]/g, '-'));
|
||||||
|
}
|
||||||
|
|
||||||
|
export default async function StoryPage({ params }: StoryPageProps) {
|
||||||
|
const story = await getStoryBySlug(params.slug);
|
||||||
|
|
||||||
|
if (!story) {
|
||||||
|
notFound();
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-gradient-to-br from-yellow-50 via-orange-50 to-red-50 font-mono">
|
||||||
|
<div className="max-w-4xl mx-auto px-6 py-12">
|
||||||
|
<header className="text-center mb-16">
|
||||||
|
<div className="mb-8">
|
||||||
|
<div className="w-32 h-32 mx-auto bg-black rounded-full flex items-center justify-center border-4 border-yellow-300">
|
||||||
|
<div className="text-4xl font-bold text-yellow-300">📝</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<h1 className="text-4xl md:text-5xl font-bold text-gray-900 mb-4 uppercase tracking-wide">
|
||||||
|
Mini Stories
|
||||||
|
</h1>
|
||||||
|
<p className="text-lg text-gray-700 max-w-2xl mx-auto leading-relaxed font-semibold">
|
||||||
|
Capturing moments in brief narratives
|
||||||
|
</p>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<nav className="mb-16">
|
||||||
|
<div className="bg-white border-4 border-black rounded-none p-6 shadow-brutal">
|
||||||
|
<ul className="flex flex-wrap justify-center gap-4 md:gap-8">
|
||||||
|
<li>
|
||||||
|
<Link
|
||||||
|
href="/"
|
||||||
|
className="text-black font-bold hover:bg-green-200 px-4 py-2 transition-colors border-2 border-black hover:border-green-400"
|
||||||
|
>
|
||||||
|
Home
|
||||||
|
</Link>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<Link
|
||||||
|
href="/digital-art"
|
||||||
|
className="text-black font-bold hover:bg-purple-200 px-4 py-2 transition-colors border-2 border-black hover:border-purple-400"
|
||||||
|
>
|
||||||
|
Digital Art
|
||||||
|
</Link>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<Link
|
||||||
|
href="/stories"
|
||||||
|
className="border-yellow-200 bg-yellow-300 px-4 py-2 border-2 text-black"
|
||||||
|
>
|
||||||
|
All Stories
|
||||||
|
</Link>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<Link
|
||||||
|
href="/thoughts"
|
||||||
|
className="text-black font-bold hover:bg-teal-200 px-4 py-2 transition-colors border-2 border-black hover:border-teal-400"
|
||||||
|
>
|
||||||
|
Thoughts
|
||||||
|
</Link>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<main>
|
||||||
|
<article className="bg-white border-4 border-black rounded-none p-8 shadow-brutal">
|
||||||
|
<div className="mb-8">
|
||||||
|
<h1 className="text-3xl md:text-4xl font-bold text-gray-900 uppercase mb-4">
|
||||||
|
{story.title}
|
||||||
|
</h1>
|
||||||
|
|
||||||
|
<div className="flex gap-4 text-sm mb-6">
|
||||||
|
<span className="bg-yellow-200 text-black px-3 py-1 border-2 border-black font-bold">
|
||||||
|
{formatReadingTime(story.readTime)} read
|
||||||
|
</span>
|
||||||
|
<span className="bg-gray-200 text-black px-3 py-1 border-2 border-black font-bold">
|
||||||
|
{new Date(story.createdAt).toLocaleDateString('en-US', {
|
||||||
|
year: 'numeric',
|
||||||
|
month: 'long',
|
||||||
|
day: 'numeric'
|
||||||
|
})}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="prose prose-lg max-w-none">
|
||||||
|
<div className="text-gray-800 leading-relaxed whitespace-pre-line">
|
||||||
|
{story.content}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<footer className="mt-20 text-center">
|
||||||
|
<div className="bg-black text-white p-4 font-bold">
|
||||||
|
<p className="text-sm uppercase">Built with brutalist pastels</p>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
159
src/app/thoughts/[slug]/page.tsx
Normal file
159
src/app/thoughts/[slug]/page.tsx
Normal file
@@ -0,0 +1,159 @@
|
|||||||
|
import { notFound } from 'next/navigation';
|
||||||
|
import Link from 'next/link';
|
||||||
|
import { loadThoughts } from '@/utils/mdxLoader';
|
||||||
|
import { formatReadingTime } from '@/utils/readingTime';
|
||||||
|
|
||||||
|
interface ThoughtPageProps {
|
||||||
|
params: {
|
||||||
|
slug: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
import fs from 'fs';
|
||||||
|
|
||||||
|
export async function generateStaticParams() {
|
||||||
|
const thoughtsDirectory = process.cwd() + '/src/content/thoughts';
|
||||||
|
|
||||||
|
try {
|
||||||
|
const filenames = fs.readdirSync(thoughtsDirectory);
|
||||||
|
return filenames.map((filename: string) => ({
|
||||||
|
slug: filename.replace(/\.mdx$/, ''),
|
||||||
|
}));
|
||||||
|
} catch {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function generateMetadata({ params }: ThoughtPageProps) {
|
||||||
|
const thought = await getThoughtBySlug(params.slug);
|
||||||
|
|
||||||
|
if (!thought) {
|
||||||
|
return {
|
||||||
|
title: 'Thought Not Found',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
title: `${thought.title} | Mainasara Tsowa`,
|
||||||
|
description: thought.excerpt,
|
||||||
|
openGraph: {
|
||||||
|
title: `${thought.title} | Mainasara Tsowa`,
|
||||||
|
description: thought.excerpt,
|
||||||
|
url: `https://mainasara.dev/thoughts/${params.slug}`,
|
||||||
|
},
|
||||||
|
twitter: {
|
||||||
|
title: `${thought.title} | Mainasara Tsowa`,
|
||||||
|
description: thought.excerpt,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getThoughtBySlug(slug: string) {
|
||||||
|
const thoughts = await loadThoughts();
|
||||||
|
return thoughts.find(thought => thought.id === slug.replace(/[^a-zA-Z0-9]/g, '-'));
|
||||||
|
}
|
||||||
|
|
||||||
|
export default async function ThoughtPage({ params }: ThoughtPageProps) {
|
||||||
|
const thought = await getThoughtBySlug(params.slug);
|
||||||
|
|
||||||
|
if (!thought) {
|
||||||
|
notFound();
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-gradient-to-br from-teal-50 via-green-50 to-blue-50 font-mono">
|
||||||
|
<div className="max-w-4xl mx-auto px-6 py-12">
|
||||||
|
<header className="text-center mb-16">
|
||||||
|
<div className="mb-8">
|
||||||
|
<div className="w-32 h-32 mx-auto bg-black rounded-full flex items-center justify-center border-4 border-teal-300">
|
||||||
|
<div className="text-4xl font-bold text-teal-300">🌸</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<h1 className="text-4xl md:text-5xl font-bold text-gray-900 mb-4 uppercase tracking-wide">
|
||||||
|
Thoughts
|
||||||
|
</h1>
|
||||||
|
<p className="text-lg text-gray-700 max-w-2xl mx-auto leading-relaxed font-semibold">
|
||||||
|
Reflections on technology and life
|
||||||
|
</p>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<nav className="mb-16">
|
||||||
|
<div className="bg-white border-4 border-black rounded-none p-6 shadow-brutal">
|
||||||
|
<ul className="flex flex-wrap justify-center gap-4 md:gap-8">
|
||||||
|
<li>
|
||||||
|
<Link
|
||||||
|
href="/"
|
||||||
|
className="text-black font-bold hover:bg-green-200 px-4 py-2 transition-colors border-2 border-black hover:border-green-400"
|
||||||
|
>
|
||||||
|
Home
|
||||||
|
</Link>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<Link
|
||||||
|
href="/digital-art"
|
||||||
|
className="text-black font-bold hover:bg-purple-200 px-4 py-2 transition-colors border-2 border-black hover:border-purple-400"
|
||||||
|
>
|
||||||
|
Digital Art
|
||||||
|
</Link>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<Link
|
||||||
|
href="/stories"
|
||||||
|
className="text-black font-bold hover:bg-yellow-200 px-4 py-2 transition-colors border-2 border-black hover:border-yellow-400"
|
||||||
|
>
|
||||||
|
Stories
|
||||||
|
</Link>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<Link
|
||||||
|
href="/thoughts"
|
||||||
|
className="border-teal-200 bg-teal-300 px-4 py-2 border-2 text-black"
|
||||||
|
>
|
||||||
|
All Thoughts
|
||||||
|
</Link>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<main>
|
||||||
|
<article className="bg-white border-4 border-black rounded-none p-8 shadow-brutal">
|
||||||
|
<div className="mb-8">
|
||||||
|
<h1 className="text-3xl md:text-4xl font-bold text-gray-900 uppercase mb-4">
|
||||||
|
{thought.title}
|
||||||
|
</h1>
|
||||||
|
|
||||||
|
<div className="flex gap-4 text-sm mb-6">
|
||||||
|
<span className="bg-teal-200 text-black px-3 py-1 border-2 border-black font-bold">
|
||||||
|
{thought.category}
|
||||||
|
</span>
|
||||||
|
<span className="bg-teal-200 text-black px-3 py-1 border-2 border-black font-bold">
|
||||||
|
{formatReadingTime(thought.readTime)} read
|
||||||
|
</span>
|
||||||
|
<span className="bg-gray-200 text-black px-3 py-1 border-2 border-black font-bold">
|
||||||
|
{new Date(thought.createdAt).toLocaleDateString('en-US', {
|
||||||
|
year: 'numeric',
|
||||||
|
month: 'long',
|
||||||
|
day: 'numeric'
|
||||||
|
})}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="prose prose-lg max-w-none">
|
||||||
|
<div className="text-gray-800 leading-relaxed whitespace-pre-line">
|
||||||
|
{thought.content}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<footer className="mt-20 text-center">
|
||||||
|
<div className="bg-black text-white p-4 font-bold">
|
||||||
|
<p className="text-sm uppercase">Built with brutalist pastels</p>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user