📄 Huge intl feature
Some checks failed
Some checks failed
This commit was merged in pull request #10.
This commit is contained in:
@@ -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 />
|
||||
@@ -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>
|
||||
@@ -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()
|
||||
@@ -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">
|
||||
> BLOG ARCHIVE_
|
||||
> {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>
|
||||
)}
|
||||
@@ -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
35
app/[locale]/layout.tsx
Normal 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>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -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()
|
||||
@@ -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)
|
||||
@@ -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()
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 => ({
|
||||
|
||||
Reference in New Issue
Block a user