🪛 03 routing

This commit is contained in:
RJ
2025-11-07 16:55:58 +02:00
parent 651beb2de6
commit 05390016b2
8 changed files with 441 additions and 14 deletions

View File

@@ -0,0 +1,23 @@
import Link from 'next/link'
export default function NotFound() {
return (
<div className="min-h-[60vh] flex items-center justify-center">
<div className="text-center">
<h1 className="text-6xl font-bold text-gray-300 dark:text-gray-700 mb-4">404</h1>
<h2 className="text-2xl font-semibold mb-4">Articolul nu a fost găsit</h2>
<p className="text-gray-600 dark:text-gray-400 mb-8">
Ne pare rău, dar articolul pe care îl cauți nu există sau a fost mutat.
</p>
<div className="space-x-4">
<Link href="/blog" className="inline-block px-6 py-3 bg-primary-600 text-white rounded-lg hover:bg-primary-700 transition">
Vezi toate articolele
</Link>
<Link href="/" className="inline-block px-6 py-3 border border-gray-300 dark:border-gray-600 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-800 transition">
Pagina principală
</Link>
</div>
</div>
</div>
)
}

129
app/blog/[...slug]/page.tsx Normal file
View File

@@ -0,0 +1,129 @@
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>
)
}

95
app/blog/page.tsx Normal file
View File

@@ -0,0 +1,95 @@
import { Metadata } from 'next'
import Link from 'next/link'
import { getAllPosts } from '@/lib/markdown'
import { formatDate } from '@/lib/utils'
export const metadata: Metadata = {
title: 'Blog',
description: 'Toate articolele din blog',
}
function PostCard({ post }: { post: any }) {
return (
<article className="border-b border-gray-200 dark:border-gray-700 pb-8 mb-8 last:border-0">
<div className="flex flex-col lg:flex-row gap-6">
{post.frontmatter.image && (
<div className="lg:w-1/3">
<img src={post.frontmatter.image} alt={post.frontmatter.title} className="w-full h-48 lg:h-full object-cover rounded-lg" />
</div>
)}
<div className={post.frontmatter.image ? 'lg:w-2/3' : 'w-full'}>
<div className="flex items-center gap-4 text-sm text-gray-500 mb-2">
<time dateTime={post.frontmatter.date}>{formatDate(post.frontmatter.date)}</time>
<span></span>
<span>{post.readingTime} min citire</span>
<span></span>
<span>{post.frontmatter.author}</span>
</div>
<h2 className="text-2xl font-bold mb-3">
<Link href={`/blog/${post.slug}`} className="hover:text-primary-600 transition">
{post.frontmatter.title}
</Link>
</h2>
<p className="text-gray-600 dark:text-gray-400 mb-4">{post.frontmatter.description}</p>
{post.frontmatter.tags && post.frontmatter.tags.length > 0 && (
<div className="flex flex-wrap gap-2 mb-4">
{post.frontmatter.tags.map((tag: string) => (
<span key={tag} className="px-3 py-1 bg-gray-100 dark:bg-gray-800 text-sm rounded-full">
#{tag}
</span>
))}
</div>
)}
<Link href={`/blog/${post.slug}`} className="inline-flex items-center text-primary-600 hover:text-primary-700 transition">
Citește articolul complet
<svg className="ml-2 w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
</svg>
</Link>
</div>
</div>
</article>
)
}
function BlogFilters({ totalPosts }: { totalPosts: number }) {
return (
<div className="bg-gray-50 dark:bg-gray-800 rounded-lg p-6 mb-8">
<div className="flex items-center justify-between">
<div>
<h1 className="text-3xl font-bold mb-2">Articole Blog</h1>
<p className="text-gray-600 dark:text-gray-400">
{totalPosts} {totalPosts === 1 ? 'articol' : 'articole'} publicate
</p>
</div>
</div>
</div>
)
}
export default async function BlogPage() {
const posts = await getAllPosts()
if (posts.length === 0) {
return (
<div className="text-center py-12">
<h1 className="text-3xl font-bold mb-4">Blog</h1>
<p className="text-gray-600 dark:text-gray-400 mb-8">Nu există articole publicate încă.</p>
<Link href="/" className="inline-block px-6 py-3 bg-primary-600 text-white rounded-lg hover:bg-primary-700 transition">
Înapoi la pagina principală
</Link>
</div>
)
}
return (
<div className="max-w-4xl mx-auto">
<BlogFilters totalPosts={posts.length} />
<div>
{posts.map((post) => (
<PostCard key={post.slug} post={post} />
))}
</div>
</div>
)
}