🏷️ added tags system
This commit is contained in:
@@ -101,12 +101,13 @@ export default async function BlogPostPage({ params }: { params: Promise<{ slug:
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-2 mb-2">
|
||||
{post.frontmatter.tags.map((tag: string) => (
|
||||
<span
|
||||
<Link
|
||||
key={tag}
|
||||
href={`/tags/${tag}`}
|
||||
className="px-3 py-1 bg-cyan-500/5 border border-[var(--neon-cyan)] text-cyan-400 text-xs font-mono uppercase shadow-[0_0_8px_rgba(90,139,149,0.3)] hover:shadow-[0_0_12px_rgba(90,139,149,0.5)] transition-all"
|
||||
>
|
||||
#{tag}
|
||||
</span>
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
14
app/page.tsx
14
app/page.tsx
@@ -140,16 +140,22 @@ export default async function HomePage() {
|
||||
))}
|
||||
</div>
|
||||
|
||||
{allPosts.length > 6 && (
|
||||
<div className="mt-12 text-center">
|
||||
<div className="mt-12 flex gap-4 justify-center flex-wrap">
|
||||
{allPosts.length > 6 && (
|
||||
<Link
|
||||
href="/blog"
|
||||
className="inline-flex items-center px-8 py-4 bg-transparent text-slate-700 dark:text-slate-300 border-2 border-slate-400 dark:border-slate-700 font-mono font-bold uppercase text-sm tracking-wider hover:bg-slate-200 dark:hover:bg-slate-800 hover:border-slate-500 dark:hover:border-slate-600 transition-colors duration-200"
|
||||
>
|
||||
[VEZI TOATE ARTICOLELE] >>
|
||||
</Link>
|
||||
</div>
|
||||
)}
|
||||
)}
|
||||
<Link
|
||||
href="/tags"
|
||||
className="inline-flex items-center px-8 py-4 bg-transparent text-slate-700 dark:text-slate-300 border-2 border-slate-400 dark:border-slate-700 font-mono font-bold uppercase text-sm tracking-wider hover:bg-slate-200 dark:hover:bg-slate-800 hover:border-slate-500 dark:hover:border-slate-600 transition-colors duration-200"
|
||||
>
|
||||
[VEZI TOATE TAG-URILE] >>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
|
||||
40
app/tags/[tag]/not-found.tsx
Normal file
40
app/tags/[tag]/not-found.tsx
Normal file
@@ -0,0 +1,40 @@
|
||||
import Link from 'next/link';
|
||||
|
||||
export default function TagNotFound() {
|
||||
return (
|
||||
<div className="min-h-screen bg-slate-950 text-slate-100 flex items-center justify-center">
|
||||
<div className="max-w-2xl mx-auto px-6">
|
||||
<div className="border-4 border-red-900 bg-red-950 p-12 text-center">
|
||||
<div className="border-2 border-red-800 bg-red-900 p-4 mb-6 inline-block">
|
||||
<p className="font-mono text-6xl font-bold text-red-400">404</p>
|
||||
</div>
|
||||
<p className="font-mono text-xs text-red-600 uppercase tracking-widest mb-2">
|
||||
ERROR: TAG NOT FOUND
|
||||
</p>
|
||||
<h1 className="font-mono text-3xl font-bold uppercase mb-4 text-red-400">
|
||||
TAG DOES NOT EXIST
|
||||
</h1>
|
||||
<p className="font-mono text-sm text-red-300 mb-8 max-w-md mx-auto">
|
||||
> THE REQUESTED TAG COULD NOT BE LOCATED IN THE DATABASE
|
||||
<br />
|
||||
> IT MAY HAVE BEEN REMOVED OR NEVER EXISTED
|
||||
</p>
|
||||
<div className="flex gap-4 justify-center flex-wrap">
|
||||
<Link
|
||||
href="/tags"
|
||||
className="inline-block px-6 py-3 font-mono text-xs uppercase border-2 border-cyan-400 bg-cyan-900 text-cyan-100 hover:bg-cyan-800 transition"
|
||||
>
|
||||
> VIEW ALL TAGS
|
||||
</Link>
|
||||
<Link
|
||||
href="/blog"
|
||||
className="inline-block px-6 py-3 font-mono text-xs uppercase border-2 border-slate-600 bg-slate-900 text-slate-300 hover:bg-slate-800 transition"
|
||||
>
|
||||
> VIEW ALL POSTS
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
203
app/tags/[tag]/page.tsx
Normal file
203
app/tags/[tag]/page.tsx
Normal file
@@ -0,0 +1,203 @@
|
||||
import { Metadata } from 'next';
|
||||
import { notFound } from 'next/navigation';
|
||||
import Link from 'next/link';
|
||||
import {
|
||||
getAllTags,
|
||||
getPostsByTag,
|
||||
getTagInfo,
|
||||
getRelatedTags
|
||||
} from '@/lib/tags';
|
||||
import { TagList } from '@/components/blog/tag-list';
|
||||
import { formatDate } from '@/lib/utils';
|
||||
|
||||
export async function generateStaticParams() {
|
||||
const tags = await getAllTags();
|
||||
return tags.map(tag => ({ tag: tag.slug }));
|
||||
}
|
||||
|
||||
export async function generateMetadata({
|
||||
params,
|
||||
}: {
|
||||
params: Promise<{ tag: string }>;
|
||||
}): Promise<Metadata> {
|
||||
const { tag } = await params;
|
||||
const tagInfo = await getTagInfo(tag);
|
||||
|
||||
if (!tagInfo) {
|
||||
return { title: 'Tag negăsit' };
|
||||
}
|
||||
|
||||
return {
|
||||
title: `Tag: ${tagInfo.name}`,
|
||||
description: `Articole marcate cu #${tagInfo.name}. ${tagInfo.count} articole disponibile.`,
|
||||
openGraph: {
|
||||
title: `Tag: ${tagInfo.name}`,
|
||||
description: `Explorează ${tagInfo.count} articole despre ${tagInfo.name}`,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function PostCard({ post }: { post: any }) {
|
||||
return (
|
||||
<article className="border-2 border-slate-700 bg-slate-900 p-6 hover:border-cyan-400 transition">
|
||||
{post.frontmatter.image && (
|
||||
<img
|
||||
src={post.frontmatter.image}
|
||||
alt={post.frontmatter.title}
|
||||
className="w-full h-48 object-cover mb-4 border-2 border-slate-800"
|
||||
/>
|
||||
)}
|
||||
|
||||
<div className="flex items-center gap-2 font-mono text-xs text-zinc-500 mb-3 uppercase">
|
||||
<time dateTime={post.frontmatter.date}>
|
||||
{formatDate(post.frontmatter.date)}
|
||||
</time>
|
||||
<span>></span>
|
||||
<span>{post.readingTime} min</span>
|
||||
</div>
|
||||
|
||||
<h2 className="font-mono text-lg font-bold mb-3 uppercase">
|
||||
<Link
|
||||
href={`/blog/${post.slug}`}
|
||||
className="text-cyan-400 hover:text-cyan-300 transition"
|
||||
>
|
||||
{post.frontmatter.title}
|
||||
</Link>
|
||||
</h2>
|
||||
|
||||
<p className="text-zinc-400 mb-4 line-clamp-3">
|
||||
{post.frontmatter.description}
|
||||
</p>
|
||||
|
||||
{post.frontmatter.tags && (
|
||||
<TagList tags={post.frontmatter.tags} variant="minimal" />
|
||||
)}
|
||||
</article>
|
||||
);
|
||||
}
|
||||
|
||||
export default async function TagPage({
|
||||
params,
|
||||
}: {
|
||||
params: Promise<{ tag: string }>;
|
||||
}) {
|
||||
const { tag } = await params;
|
||||
const tagInfo = await getTagInfo(tag);
|
||||
|
||||
if (!tagInfo) {
|
||||
notFound();
|
||||
}
|
||||
|
||||
const posts = await getPostsByTag(tag);
|
||||
const relatedTags = await getRelatedTags(tag);
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-slate-950 text-slate-100">
|
||||
<div className="max-w-6xl mx-auto px-6 py-12">
|
||||
<div className="border-4 border-cyan-800 bg-cyan-950 p-8 mb-8">
|
||||
<div className="flex items-center justify-between flex-wrap gap-4">
|
||||
<div>
|
||||
<p className="font-mono text-xs text-cyan-600 uppercase tracking-widest mb-2">
|
||||
TAG ARCHIVE
|
||||
</p>
|
||||
<h1 className="font-mono text-3xl font-bold uppercase mb-2">
|
||||
<span className="text-cyan-400">#{tagInfo.name}</span>
|
||||
</h1>
|
||||
<p className="font-mono text-sm text-cyan-300">
|
||||
> {tagInfo.count} {tagInfo.count === 1 ? 'DOCUMENT' : 'DOCUMENTS'}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<Link
|
||||
href="/tags"
|
||||
className="inline-flex items-center px-4 py-2 font-mono text-xs uppercase border-2 border-cyan-400 bg-slate-900 text-cyan-400 hover:bg-cyan-900 transition"
|
||||
>
|
||||
> ALL TAGS
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-8 lg:grid-cols-4">
|
||||
<div className="lg:col-span-3">
|
||||
{posts.length === 0 ? (
|
||||
<div className="border-4 border-slate-800 bg-slate-900 p-12 text-center">
|
||||
<p className="font-mono text-zinc-400 mb-6 uppercase">
|
||||
> NO DOCUMENTS FOUND
|
||||
</p>
|
||||
<Link
|
||||
href="/blog"
|
||||
className="inline-block px-6 py-3 font-mono text-sm uppercase border-2 border-cyan-400 bg-cyan-900 text-cyan-100 hover:bg-cyan-800 transition"
|
||||
>
|
||||
> VIEW ALL POSTS
|
||||
</Link>
|
||||
</div>
|
||||
) : (
|
||||
<div className="grid gap-6">
|
||||
{posts.map(post => (
|
||||
<PostCard key={post.slug} post={post} />
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<aside className="lg:col-span-1 space-y-6">
|
||||
{relatedTags.length > 0 && (
|
||||
<div className="border-2 border-slate-700 bg-slate-900 p-6">
|
||||
<div className="border-b-2 border-slate-700 pb-3 mb-4">
|
||||
<h2 className="font-mono text-sm font-bold uppercase text-cyan-400">
|
||||
RELATED TAGS
|
||||
</h2>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
{relatedTags.map(tag => (
|
||||
<Link
|
||||
key={tag.slug}
|
||||
href={`/tags/${tag.slug}`}
|
||||
className="flex items-center justify-between p-2 border border-slate-700 hover:border-cyan-400 hover:bg-slate-800 transition"
|
||||
>
|
||||
<span className="font-mono text-xs uppercase text-zinc-300">
|
||||
#{tag.name}
|
||||
</span>
|
||||
<span className="font-mono text-xs text-zinc-500">
|
||||
[{tag.count}]
|
||||
</span>
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="border-2 border-slate-700 bg-slate-900 p-6">
|
||||
<div className="border-b-2 border-slate-700 pb-3 mb-4">
|
||||
<h2 className="font-mono text-sm font-bold uppercase text-cyan-400">
|
||||
QUICK NAV
|
||||
</h2>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Link
|
||||
href="/blog"
|
||||
className="block p-2 border border-slate-700 hover:border-cyan-400 hover:bg-slate-800 transition font-mono text-xs uppercase"
|
||||
>
|
||||
> ALL POSTS
|
||||
</Link>
|
||||
<Link
|
||||
href="/tags"
|
||||
className="block p-2 border border-slate-700 hover:border-cyan-400 hover:bg-slate-800 transition font-mono text-xs uppercase"
|
||||
>
|
||||
> ALL TAGS
|
||||
</Link>
|
||||
<Link
|
||||
href="/"
|
||||
className="block p-2 border border-slate-700 hover:border-cyan-400 hover:bg-slate-800 transition font-mono text-xs uppercase"
|
||||
>
|
||||
> HOME
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
16
app/tags/layout.tsx
Normal file
16
app/tags/layout.tsx
Normal file
@@ -0,0 +1,16 @@
|
||||
import { Metadata } from 'next'
|
||||
import { Navbar } from '@/components/blog/navbar'
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'Tag-uri',
|
||||
description: 'Explorează articolele după tag-uri',
|
||||
}
|
||||
|
||||
export default function TagsLayout({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<>
|
||||
<Navbar />
|
||||
{children}
|
||||
</>
|
||||
)
|
||||
}
|
||||
146
app/tags/page.tsx
Normal file
146
app/tags/page.tsx
Normal file
@@ -0,0 +1,146 @@
|
||||
import { Metadata } from 'next';
|
||||
import Link from 'next/link';
|
||||
import { getAllTags, getTagCloud } from '@/lib/tags';
|
||||
import { TagCloud } from '@/components/blog/tag-cloud';
|
||||
import { TagBadge } from '@/components/blog/tag-badge';
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'Tag-uri',
|
||||
description: 'Explorează articolele după tag-uri',
|
||||
};
|
||||
|
||||
export default async function TagsPage() {
|
||||
const allTags = await getAllTags();
|
||||
const tagCloud = await getTagCloud();
|
||||
|
||||
if (allTags.length === 0) {
|
||||
return (
|
||||
<div className="min-h-screen bg-slate-950 text-slate-100">
|
||||
<div className="max-w-6xl mx-auto px-6 py-12">
|
||||
<div className="border-4 border-slate-800 bg-slate-900 p-12 text-center">
|
||||
<h1 className="font-mono text-3xl font-bold uppercase mb-4 text-cyan-400">
|
||||
TAG DATABASE
|
||||
</h1>
|
||||
<p className="font-mono text-zinc-400 mb-8">
|
||||
> NO TAGS AVAILABLE
|
||||
</p>
|
||||
<Link
|
||||
href="/blog"
|
||||
className="inline-block px-6 py-3 font-mono text-sm uppercase border-2 border-cyan-400 bg-cyan-900 text-cyan-100 hover:bg-cyan-800 transition"
|
||||
>
|
||||
> VIEW ALL POSTS
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const groupedTags = allTags.reduce((acc, tag) => {
|
||||
const firstLetter = tag.name[0].toUpperCase();
|
||||
if (!acc[firstLetter]) {
|
||||
acc[firstLetter] = [];
|
||||
}
|
||||
acc[firstLetter].push(tag);
|
||||
return acc;
|
||||
}, {} as Record<string, typeof allTags>);
|
||||
|
||||
const sortedLetters = Object.keys(groupedTags).sort();
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-slate-950 text-slate-100">
|
||||
<div className="max-w-6xl mx-auto px-6 py-12">
|
||||
<div className="border-4 border-slate-800 bg-slate-900 p-8 mb-8">
|
||||
<p className="font-mono text-xs text-zinc-500 uppercase tracking-widest mb-2">
|
||||
DOCUMENT TYPE: TAG DATABASE
|
||||
</p>
|
||||
<h1 className="font-mono text-4xl font-bold uppercase text-cyan-400 mb-4">
|
||||
TAG REGISTRY
|
||||
</h1>
|
||||
<p className="font-mono text-lg text-zinc-400">
|
||||
> TOTAL TAGS: {allTags.length}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<section className="mb-12">
|
||||
<div className="border-2 border-slate-700 bg-slate-900 p-6 mb-2">
|
||||
<p className="font-mono text-xs text-zinc-500 uppercase tracking-widest mb-4">
|
||||
SECTION: TAG CLOUD VISUALIZATION
|
||||
</p>
|
||||
</div>
|
||||
<div className="border-2 border-slate-700 bg-zinc-950 p-8">
|
||||
<TagCloud tags={tagCloud} />
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="mb-12">
|
||||
<div className="border-2 border-slate-700 bg-slate-900 p-6 mb-2">
|
||||
<p className="font-mono text-xs text-zinc-500 uppercase tracking-widest">
|
||||
SECTION: ALPHABETICAL INDEX
|
||||
</p>
|
||||
</div>
|
||||
<div className="space-y-8">
|
||||
{sortedLetters.map(letter => (
|
||||
<div key={letter}>
|
||||
<div className="border-2 border-cyan-700 bg-cyan-950 p-4 mb-4">
|
||||
<h3 className="font-mono text-xl font-bold text-cyan-400 uppercase">
|
||||
> [{letter}]
|
||||
</h3>
|
||||
</div>
|
||||
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
|
||||
{groupedTags[letter].map(tag => (
|
||||
<Link
|
||||
key={tag.slug}
|
||||
href={`/tags/${tag.slug}`}
|
||||
className="flex items-center justify-between p-4 border-2 border-slate-700 bg-slate-900 hover:border-cyan-400 hover:bg-slate-800 transition"
|
||||
>
|
||||
<span className="font-mono text-sm uppercase">#{tag.name}</span>
|
||||
<TagBadge count={tag.count} />
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="border-4 border-cyan-800 bg-cyan-950 p-8">
|
||||
<div className="mb-6">
|
||||
<p className="font-mono text-xs text-cyan-600 uppercase tracking-widest mb-2">
|
||||
DOCUMENT STATISTICS
|
||||
</p>
|
||||
<h2 className="font-mono text-2xl font-bold uppercase text-cyan-400">
|
||||
TAG METRICS
|
||||
</h2>
|
||||
</div>
|
||||
<div className="grid gap-6 sm:grid-cols-3">
|
||||
<div className="border-2 border-cyan-700 bg-slate-900 p-6 text-center">
|
||||
<div className="font-mono text-3xl font-bold text-cyan-400">
|
||||
{allTags.length}
|
||||
</div>
|
||||
<div className="font-mono text-xs text-zinc-400 uppercase mt-2">
|
||||
TOTAL TAGS
|
||||
</div>
|
||||
</div>
|
||||
<div className="border-2 border-cyan-700 bg-slate-900 p-6 text-center">
|
||||
<div className="font-mono text-3xl font-bold text-cyan-400">
|
||||
{Math.max(...allTags.map(t => t.count))}
|
||||
</div>
|
||||
<div className="font-mono text-xs text-zinc-400 uppercase mt-2">
|
||||
MAX POSTS/TAG
|
||||
</div>
|
||||
</div>
|
||||
<div className="border-2 border-cyan-700 bg-slate-900 p-6 text-center">
|
||||
<div className="font-mono text-3xl font-bold text-cyan-400">
|
||||
{Math.round(allTags.reduce((sum, t) => sum + t.count, 0) / allTags.length)}
|
||||
</div>
|
||||
<div className="font-mono text-xs text-zinc-400 uppercase mt-2">
|
||||
AVG POSTS/TAG
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user