📄 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

@@ -0,0 +1,175 @@
'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'
import { SortDropdown } from '@/components/blog/sort-dropdown'
import { TagFilter } from '@/components/blog/tag-filter'
import { Navbar } from '@/components/blog/navbar'
interface BlogPageClientProps {
posts: Post[]
allTags: string[]
}
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')
const [currentPage, setCurrentPage] = useState(1)
const postsPerPage = 9
const filteredAndSortedPosts = useMemo(() => {
const result = posts.filter(post => {
const matchesSearch =
searchQuery === '' ||
post.frontmatter.title.toLowerCase().includes(searchQuery.toLowerCase()) ||
post.frontmatter.description.toLowerCase().includes(searchQuery.toLowerCase())
const matchesTags =
selectedTags.length === 0 || selectedTags.every(tag => post.frontmatter.tags.includes(tag))
return matchesSearch && matchesTags
})
result.sort((a, b) => {
switch (sortBy) {
case 'oldest':
return new Date(a.frontmatter.date).getTime() - new Date(b.frontmatter.date).getTime()
case 'title':
return a.frontmatter.title.localeCompare(b.frontmatter.title)
case 'newest':
default:
return new Date(b.frontmatter.date).getTime() - new Date(a.frontmatter.date).getTime()
}
})
return result
}, [posts, searchQuery, selectedTags, sortBy])
const totalPages = Math.ceil(filteredAndSortedPosts.length / postsPerPage)
const paginatedPosts = filteredAndSortedPosts.slice(
(currentPage - 1) * postsPerPage,
currentPage * postsPerPage
)
const toggleTag = (tag: string) => {
setSelectedTags(prev => (prev.includes(tag) ? prev.filter(t => t !== tag) : [...prev, tag]))
setCurrentPage(1)
}
return (
<div className="min-h-screen bg-[rgb(var(--bg-primary))]">
<div className="max-w-7xl mx-auto px-6 py-12">
{/* 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">
{t("subtitle")}
</p>
<h1 className="text-4xl md:text-6xl font-mono font-bold text-[rgb(var(--text-primary))] uppercase tracking-tight">
&gt; {t("title")}_
</h1>
</div>
{/* Search Bar */}
<div className="border border-[rgb(var(--border-primary))] bg-[rgb(var(--bg-secondary))] p-6 mb-8">
<div className="flex flex-col lg:flex-row gap-4">
<SearchBar
searchQuery={searchQuery}
onSearchChange={value => {
setSearchQuery(value)
setCurrentPage(1)
}}
/>
<SortDropdown sortBy={sortBy} onSortChange={setSortBy} />
</div>
</div>
{/* Tag Filters */}
<TagFilter
allTags={allTags}
selectedTags={selectedTags}
onToggleTag={toggleTag}
onClearTags={() => {
setSelectedTags([])
setCurrentPage(1)
}}
/>
{/* Results Count */}
<div className="mb-6">
<p className="font-mono text-sm text-[rgb(var(--text-muted))] uppercase">
{t("foundPosts", {count: filteredAndSortedPosts.length})}{' '}
</p>
</div>
{/* Blog Grid */}
{paginatedPosts.length > 0 ? (
<div className="grid gap-8 lg:grid-cols-3 md:grid-cols-2 grid-cols-1 mb-12">
{paginatedPosts.map((post, index) => {
const hasImage = !!post.frontmatter.image
let variant: 'image-top' | 'image-side' | 'text-only'
if (!hasImage) {
variant = 'text-only'
} else {
variant = index % 3 === 1 ? 'image-side' : 'image-top'
}
return <BlogCard key={post.slug} post={post} variant={variant} />
})}
</div>
) : (
<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">
{t("noPosts")}
</p>
</div>
)}
{/* Pagination */}
{totalPages > 1 && (
<div className="border border-[rgb(var(--border-primary))] bg-[rgb(var(--bg-secondary))] p-6">
<div className="flex items-center justify-between">
<button
onClick={() => setCurrentPage(p => Math.max(1, p - 1))}
disabled={currentPage === 1}
className="px-6 py-3 font-mono text-sm uppercase border border-[rgb(var(--border-primary))] text-[rgb(var(--text-primary))] disabled:opacity-30 disabled:cursor-not-allowed hover:border-[var(--neon-cyan)] hover:text-[var(--neon-cyan)] transition-colors cursor-pointer"
>
&lt; PREV
</button>
<div className="flex items-center gap-2">
{Array.from({ length: totalPages }, (_, i) => i + 1).map(page => (
<button
key={page}
onClick={() => setCurrentPage(page)}
className={`w-12 h-12 font-mono text-sm border transition-colors cursor-pointer ${
currentPage === page
? 'bg-[var(--neon-cyan)] border-[var(--neon-cyan)] text-white'
: 'border-[rgb(var(--border-primary))] text-[rgb(var(--text-muted))] hover:border-[var(--neon-cyan)] hover:text-[var(--neon-cyan)]'
}`}
>
{String(page).padStart(2, '0')}
</button>
))}
</div>
<button
onClick={() => setCurrentPage(p => Math.min(totalPages, p + 1))}
disabled={currentPage === totalPages}
className="px-6 py-3 font-mono text-sm uppercase border border-[rgb(var(--border-primary))] text-[rgb(var(--text-primary))] disabled:opacity-30 disabled:cursor-not-allowed hover:border-[var(--neon-cyan)] hover:text-[var(--neon-cyan)] transition-colors cursor-pointer"
>
NEXT &gt;
</button>
</div>
</div>
)}
</div>
</div>
)
}