📄 Huge intl feature

This commit is contained in:
RJ
2025-12-03 00:17:34 +02:00
parent 8b05aae5a8
commit 7e8b82f571
48 changed files with 955 additions and 138 deletions

View File

@@ -1,12 +1,19 @@
import { Metadata } from 'next'
import { Navbar } from '@/components/blog/navbar'
import {setRequestLocale} from 'next-intl/server'
export const metadata: Metadata = {
title: 'About',
description: 'Learn more about me and this blog',
}
export default function AboutPage() {
type Props = {
params: Promise<{locale: string}>
}
export default async function AboutPage({params}: Props) {
const {locale} = await params
setRequestLocale(locale)
return (
<>
<Navbar />

View File

@@ -1,26 +1,29 @@
import Link from 'next/link'
import { useTranslations } from 'next-intl'
import { Link } from '@/i18n/navigation'
export default function NotFound() {
const t = useTranslations('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>
<h2 className="text-2xl font-semibold mb-4">{t('title')}</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.
{t('description')}
</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
{t('goHome')}
</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ă
{t('goHome')}
</Link>
</div>
</div>

View File

@@ -1,26 +1,36 @@
import { Metadata } from 'next'
import { notFound } from 'next/navigation'
import Link from 'next/link'
import { Link } from '@/src/i18n/navigation'
import { getAllPosts, getPostBySlug, getRelatedPosts } from '@/lib/markdown'
import { formatDate, formatRelativeDate } from '@/lib/utils'
import { TableOfContents } from '@/components/blog/table-of-contents'
import { ReadingProgress } from '@/components/blog/reading-progress'
import { StickyFooter } from '@/components/blog/sticky-footer'
import MarkdownRenderer from '@/components/blog/markdown-renderer'
import { setRequestLocale } from 'next-intl/server'
import { routing } from '@/src/i18n/routing'
export async function generateStaticParams() {
const posts = await getAllPosts()
return posts.map(post => ({ slug: post.slug.split('/') }))
const locales = ['en', 'ro']
const allParams: Array<{ locale: string; slug: string[] }> = []
for (const locale of locales) {
const posts = await getAllPosts(locale)
posts.forEach(post => {
allParams.push({ locale, slug: post.slug.split('/') })
})
}
return allParams
}
export async function generateMetadata({
params,
}: {
params: Promise<{ slug: string[] }>
params: Promise<{ locale: string; slug: string[] }>
}): Promise<Metadata> {
const { slug } = await params
const { slug, locale } = await params
const slugPath = slug.join('/')
const post = await getPostBySlug(slugPath)
const post = await getPostBySlug(slugPath, locale)
if (!post) {
return { title: 'Articol negăsit' }
@@ -65,10 +75,14 @@ function extractHeadings(content: string) {
return headings
}
export default async function BlogPostPage({ params }: { params: Promise<{ slug: string[] }> }) {
const { slug } = await params
export default async function BlogPostPage({
params,
}: {
params: Promise<{ locale: string; slug: string[] }>
}) {
const { slug, locale } = await params
const slugPath = slug.join('/')
const post = await getPostBySlug(slugPath)
const post = await getPostBySlug(slugPath, locale)
if (!post) {
notFound()

View File

@@ -1,6 +1,7 @@
'use client'
import { useMemo, useState } from 'react'
import { useTranslations } from 'next-intl'
import { Post } from '@/lib/types/frontmatter'
import { BlogCard } from '@/components/blog/blog-card'
import { SearchBar } from '@/components/blog/search-bar'
@@ -16,6 +17,7 @@ interface BlogPageClientProps {
type SortOption = 'newest' | 'oldest' | 'title'
export default function BlogPageClient({ posts, allTags }: BlogPageClientProps) {
const t = useTranslations('BlogListing')
const [searchQuery, setSearchQuery] = useState('')
const [selectedTags, setSelectedTags] = useState<string[]>([])
const [sortBy, setSortBy] = useState<SortOption>('newest')
@@ -67,10 +69,10 @@ export default function BlogPageClient({ posts, allTags }: BlogPageClientProps)
{/* Header */}
<div className="border-l border-[var(--neon-cyan)] pl-6 mb-12">
<p className="font-mono text-xs text-[rgb(var(--text-muted))] uppercase tracking-widest mb-2">
DATABASE QUERY // SEARCH RESULTS
{t("subtitle")}
</p>
<h1 className="text-4xl md:text-6xl font-mono font-bold text-[rgb(var(--text-primary))] uppercase tracking-tight">
&gt; BLOG ARCHIVE_
&gt; {t("title")}_
</h1>
</div>
@@ -102,8 +104,8 @@ export default function BlogPageClient({ posts, allTags }: BlogPageClientProps)
{/* Results Count */}
<div className="mb-6">
<p className="font-mono text-sm text-[rgb(var(--text-muted))] uppercase">
FOUND {filteredAndSortedPosts.length}{' '}
{filteredAndSortedPosts.length === 1 ? 'POST' : 'POSTS'}
{t("foundPosts", {count: filteredAndSortedPosts.length})}{' '}
</p>
</div>
@@ -126,7 +128,7 @@ export default function BlogPageClient({ posts, allTags }: BlogPageClientProps)
) : (
<div className="border border-[rgb(var(--border-primary))] bg-[rgb(var(--bg-secondary))] p-12 text-center">
<p className="font-mono text-lg text-[rgb(var(--text-muted))] uppercase">
NO POSTS FOUND // TRY DIFFERENT SEARCH TERMS
{t("noPosts")}
</p>
</div>
)}

View File

@@ -1,5 +1,6 @@
import { getAllPosts } from '@/lib/markdown'
import BlogPageClient from './blog-client'
import {setRequestLocale} from 'next-intl/server'
export default async function BlogPage() {
const posts = await getAllPosts()

35
app/[locale]/layout.tsx Normal file
View File

@@ -0,0 +1,35 @@
import {notFound} from 'next/navigation'
import {setRequestLocale} from 'next-intl/server'
import {routing} from '@/src/i18n/routing'
import {ReactNode} from 'react'
type Props = {
children: ReactNode
breadcrumbs: ReactNode
params: Promise<{locale: string}>
}
export function generateStaticParams() {
return routing.locales.map((locale) => ({locale}))
}
export default async function LocaleLayout({
children,
breadcrumbs,
params
}: Props) {
const {locale} = await params
if (!routing.locales.includes(locale as any)) {
notFound()
}
setRequestLocale(locale)
return (
<>
{breadcrumbs}
<main>{children}</main>
</>
)
}

View File

@@ -1,8 +1,9 @@
import Link from 'next/link'
import {Link} from '@/src/i18n/navigation'
import Image from 'next/image'
import { getAllPosts } from '@/lib/markdown'
import { formatDate } from '@/lib/utils'
import { ThemeToggle } from '@/components/theme-toggle'
import {setRequestLocale} from 'next-intl/server'
export default async function HomePage() {
const allPosts = await getAllPosts()

View File

@@ -1,9 +1,11 @@
import { Metadata } from 'next'
import { notFound } from 'next/navigation'
import Link from 'next/link'
import {Link} from '@/src/i18n/navigation'
import { getAllTags, getPostsByTag, getTagInfo, getRelatedTags } from '@/lib/tags'
import { TagList } from '@/components/blog/tag-list'
import { formatDate } from '@/lib/utils'
import {setRequestLocale} from 'next-intl/server'
import {routing} from '@/src/i18n/routing'
export async function generateStaticParams() {
const tags = await getAllTags()
@@ -13,7 +15,7 @@ export async function generateStaticParams() {
export async function generateMetadata({
params,
}: {
params: Promise<{ tag: string }>
params: Promise<{ locale: string; tag: string }>
}): Promise<Metadata> {
const { tag } = await params
const tagInfo = await getTagInfo(tag)

View File

@@ -1,15 +1,22 @@
import { Metadata } from 'next'
import Link from 'next/link'
import {Link} from '@/src/i18n/navigation'
import { getAllTags, getTagCloud } from '@/lib/tags'
import { TagCloud } from '@/components/blog/tag-cloud'
import { TagBadge } from '@/components/blog/tag-badge'
import {setRequestLocale} from 'next-intl/server'
export const metadata: Metadata = {
title: 'Tag-uri',
description: 'Explorează articolele după tag-uri',
}
export default async function TagsPage() {
type Props = {
params: Promise<{locale: string}>
}
export default async function TagsPage({params}: Props) {
const {locale} = await params
setRequestLocale(locale)
const allTags = await getAllTags()
const tagCloud = await getTagCloud()

View File

@@ -3,7 +3,7 @@ import { NextResponse } from 'next/server'
export async function GET() {
const baseUrl = process.env.NEXT_PUBLIC_SITE_URL || 'http://localhost:3030'
const posts = await getAllPosts(false)
const posts = await getAllPosts("en", false)
const rss = `<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">

View File

@@ -17,6 +17,7 @@
--text-muted: 113 113 122;
--border-primary: 212 212 216;
--border-subtle: 228 228 231;
--text-color: #1f1f1f;
/* Desaturated cyberpunk for light mode - darker for readability */
--neon-pink: #7a3d52;
@@ -35,6 +36,7 @@
--text-muted: 100 116 139;
--border-primary: 71 85 105;
--border-subtle: 30 41 59;
--text-color: #d4d4d8;
/* Desaturated cyberpunk for dark mode */
--neon-pink: #8a5568;
@@ -313,7 +315,7 @@
/* Cyberpunk Prose Styling */
.cyberpunk-prose {
color: rgb(212 212 216);
color: var(--text-color);
}
.cyberpunk-prose h1,
@@ -347,7 +349,6 @@
}
.cyberpunk-prose p {
color: rgb(212 212 216);
line-height: 1.625;
margin-bottom: 1.5rem;
font-size: 1.125rem;
@@ -366,7 +367,6 @@
.cyberpunk-prose ul,
.cyberpunk-prose ol {
color: rgb(212 212 216);
padding-left: 1.5rem;
margin-bottom: 1.5rem;
}

View File

@@ -2,7 +2,9 @@ import type { Metadata } from 'next'
import { JetBrains_Mono } from 'next/font/google'
import './globals.css'
import { ThemeProvider } from '@/providers/providers'
import '@/lib/env-validation' // Validate environment variables
import '@/lib/env-validation'
import {NextIntlClientProvider} from 'next-intl'
import {getMessages} from 'next-intl/server'
const jetbrainsMono = JetBrains_Mono({ subsets: ['latin'], variable: '--font-mono' })
@@ -17,7 +19,6 @@ export const metadata: Metadata = {
keywords: ['blog', 'dezvoltare web', 'nextjs', 'react', 'typescript', 'terminal'],
openGraph: {
type: 'website',
locale: 'ro_RO',
siteName: 'Terminal Blog',
},
robots: {
@@ -29,9 +30,15 @@ export const metadata: Metadata = {
},
}
export default function RootLayout({ children }: { children: React.ReactNode }) {
export default async function RootLayout({
children
}: {
children: React.ReactNode
}) {
const messages = await getMessages()
return (
<html lang="ro" suppressHydrationWarning className={jetbrainsMono.variable}>
<html suppressHydrationWarning className={jetbrainsMono.variable}>
<body className="font-mono bg-zinc-50 text-slate-900 dark:bg-zinc-900 dark:text-slate-100 transition-colors duration-300">
<ThemeProvider
attribute="class"
@@ -40,22 +47,23 @@ export default function RootLayout({ children }: { children: React.ReactNode })
storageKey="blog-theme"
disableTransitionOnChange={false}
>
<div className="flex flex-col min-h-screen">
<div className="flex-1">{children}</div>
<NextIntlClientProvider messages={messages}>
<div className="flex flex-col min-h-screen">
<div className="flex-1">{children}</div>
{/* Footer - from worktree-agent-1 */}
<footer className="mt-auto border-t-4 border-slate-300 dark:border-slate-800 bg-zinc-100 dark:bg-slate-900 transition-colors duration-300">
<div className="container mx-auto px-4 py-8">
<div className="border-2 border-slate-300 dark:border-slate-800 p-6">
<p className="text-center text-slate-500 dark:text-slate-500 font-mono text-xs uppercase tracking-wider">
© 2025 <span style={{ color: 'var(--neon-cyan)' }}>//</span> BLOG &{' '}
<span style={{ color: 'var(--neon-pink)' }}>RANDOM THOUGHTS</span>{' '}
<span style={{ color: 'var(--neon-cyan)' }}>//</span> ALL RIGHTS RESERVED
</p>
<footer className="mt-auto border-t-4 border-slate-300 dark:border-slate-800 bg-zinc-100 dark:bg-slate-900 transition-colors duration-300">
<div className="container mx-auto px-4 py-8">
<div className="border-2 border-slate-300 dark:border-slate-800 p-6">
<p className="text-center text-slate-500 dark:text-slate-500 font-mono text-xs uppercase tracking-wider">
© 2025 <span style={{ color: 'var(--neon-cyan)' }}>//</span> BLOG &{' '}
<span style={{ color: 'var(--neon-pink)' }}>RANDOM THOUGHTS</span>{' '}
<span style={{ color: 'var(--neon-cyan)' }}>//</span> ALL RIGHTS RESERVED
</p>
</div>
</div>
</div>
</footer>
</div>
</footer>
</div>
</NextIntlClientProvider>
</ThemeProvider>
</body>
</html>

View File

@@ -5,7 +5,7 @@ export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
const baseUrl = process.env.NEXT_PUBLIC_SITE_URL || 'http://localhost:3030'
// Get all blog posts
const posts = await getAllPosts(false)
const posts = await getAllPosts("en", false)
// Generate sitemap entries for blog posts
const blogPosts: MetadataRoute.Sitemap = posts.map(post => ({