130 lines
5.0 KiB
TypeScript
130 lines
5.0 KiB
TypeScript
import { Metadata } from 'next'
|
|
import { notFound } from 'next/navigation'
|
|
import Link from 'next/link'
|
|
import { getAllPosts, getPostBySlug, getRelatedPosts } from '@/lib/markdown'
|
|
import { formatDate, formatRelativeDate } from '@/lib/utils'
|
|
|
|
export async function generateStaticParams() {
|
|
const posts = await getAllPosts()
|
|
return posts.map((post) => ({ slug: post.slug.split('/') }))
|
|
}
|
|
|
|
export async function generateMetadata({ params }: { params: Promise<{ slug: string[] }> }): Promise<Metadata> {
|
|
const { slug } = await params
|
|
const slugPath = slug.join('/')
|
|
const post = getPostBySlug(slugPath)
|
|
|
|
if (!post) {
|
|
return { title: 'Articol negăsit' }
|
|
}
|
|
|
|
return {
|
|
title: post.frontmatter.title,
|
|
description: post.frontmatter.description,
|
|
authors: [{ name: post.frontmatter.author }],
|
|
openGraph: {
|
|
title: post.frontmatter.title,
|
|
description: post.frontmatter.description,
|
|
type: 'article',
|
|
publishedTime: post.frontmatter.date,
|
|
authors: [post.frontmatter.author],
|
|
images: post.frontmatter.image ? [post.frontmatter.image] : [],
|
|
},
|
|
twitter: {
|
|
card: 'summary_large_image',
|
|
title: post.frontmatter.title,
|
|
description: post.frontmatter.description,
|
|
images: post.frontmatter.image ? [post.frontmatter.image] : [],
|
|
},
|
|
}
|
|
}
|
|
|
|
function AuthorInfo({ author, date }: { author: string; date: string }) {
|
|
return (
|
|
<div className="flex items-center space-x-4 py-6 border-y border-gray-200 dark:border-gray-700">
|
|
<div className="w-12 h-12 bg-primary-100 dark:bg-primary-900 rounded-full flex items-center justify-center">
|
|
<span className="text-xl font-bold text-primary-600 dark:text-primary-400">
|
|
{author.charAt(0).toUpperCase()}
|
|
</span>
|
|
</div>
|
|
<div>
|
|
<p className="font-semibold">{author}</p>
|
|
<p className="text-sm text-gray-500">
|
|
Publicat {formatRelativeDate(date)} • {formatDate(date)}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
function RelatedPosts({ posts }: { posts: any[] }) {
|
|
if (posts.length === 0) return null
|
|
|
|
return (
|
|
<section className="mt-12 pt-8 border-t border-gray-200 dark:border-gray-700">
|
|
<h2 className="text-2xl font-bold mb-6">Articole similare</h2>
|
|
<div className="grid gap-6 md:grid-cols-3">
|
|
{posts.map((post) => (
|
|
<Link key={post.slug} href={`/blog/${post.slug}`} className="block p-4 border border-gray-200 dark:border-gray-700 rounded-lg hover:shadow-lg transition">
|
|
<h3 className="font-semibold mb-2 line-clamp-2">{post.frontmatter.title}</h3>
|
|
<p className="text-sm text-gray-600 dark:text-gray-400 line-clamp-2">{post.frontmatter.description}</p>
|
|
<p className="text-xs text-gray-500 mt-2">{formatDate(post.frontmatter.date)}</p>
|
|
</Link>
|
|
))}
|
|
</div>
|
|
</section>
|
|
)
|
|
}
|
|
|
|
export default async function BlogPostPage({ params }: { params: Promise<{ slug: string[] }> }) {
|
|
const { slug } = await params
|
|
const slugPath = slug.join('/')
|
|
const post = getPostBySlug(slugPath)
|
|
|
|
if (!post) {
|
|
notFound()
|
|
}
|
|
|
|
const relatedPosts = await getRelatedPosts(slugPath)
|
|
|
|
return (
|
|
<article className="max-w-4xl mx-auto">
|
|
<header className="mb-8">
|
|
{post.frontmatter.image && (
|
|
<img src={post.frontmatter.image} alt={post.frontmatter.title} className="w-full h-64 md:h-96 object-cover rounded-lg mb-8" />
|
|
)}
|
|
<h1 className="text-4xl md:text-5xl font-bold mb-4">{post.frontmatter.title}</h1>
|
|
<p className="text-xl text-gray-600 dark:text-gray-400 mb-6">{post.frontmatter.description}</p>
|
|
{post.frontmatter.tags && post.frontmatter.tags.length > 0 && (
|
|
<div className="flex flex-wrap gap-2 mb-6">
|
|
{post.frontmatter.tags.map((tag: string) => (
|
|
<Link key={tag} href={`/tags/${tag.toLowerCase().replace(/\s+/g, '-')}`} className="px-3 py-1 bg-primary-100 dark:bg-primary-900 text-primary-700 dark:text-primary-300 rounded-full text-sm hover:bg-primary-200 dark:hover:bg-primary-800 transition">
|
|
#{tag}
|
|
</Link>
|
|
))}
|
|
</div>
|
|
)}
|
|
<AuthorInfo author={post.frontmatter.author} date={post.frontmatter.date} />
|
|
</header>
|
|
|
|
<div className="prose dark:prose-invert max-w-none">
|
|
<div className="flex items-center justify-between text-sm text-gray-500 mb-6">
|
|
<span>Timp estimat de citire: {post.readingTime} minute</span>
|
|
</div>
|
|
<div dangerouslySetInnerHTML={{ __html: post.content }} />
|
|
</div>
|
|
|
|
<nav className="flex justify-between items-center mt-12 pt-8 border-t border-gray-200 dark:border-gray-700">
|
|
<Link href="/blog" className="flex items-center text-primary-600 hover:text-primary-700 transition">
|
|
<svg className="mr-2 w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 19l-7-7 7-7" />
|
|
</svg>
|
|
Înapoi la blog
|
|
</Link>
|
|
</nav>
|
|
|
|
<RelatedPosts posts={relatedPosts} />
|
|
</article>
|
|
)
|
|
}
|