📄 Huge intl feature
This commit is contained in:
175
app/[locale]/blog/blog-client.tsx
Normal file
175
app/[locale]/blog/blog-client.tsx
Normal 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">
|
||||
> {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"
|
||||
>
|
||||
< 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 >
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user