🖼️ blog listing styles updates

This commit is contained in:
RJ
2025-11-11 16:07:33 +02:00
parent fb25989be9
commit 4ccd8fd759
12 changed files with 529 additions and 94 deletions

15
app/blog/layout.tsx Normal file
View File

@@ -0,0 +1,15 @@
import { Metadata } from 'next'
import { getAllPosts } from '@/lib/markdown'
import BlogPageClient from './page'
export const metadata: Metadata = {
title: 'Blog',
description: 'Toate articolele din blog',
}
export default async function BlogLayout() {
const posts = await getAllPosts()
const allTags = Array.from(new Set(posts.flatMap((post) => post.frontmatter.tags))).sort()
return <BlogPageClient posts={posts} allTags={allTags} />
}

View File

@@ -1,94 +1,179 @@
import { Metadata } from 'next'
import Link from 'next/link'
import { getAllPosts } from '@/lib/markdown'
import { formatDate } from '@/lib/utils'
'use client'
export const metadata: Metadata = {
title: 'Blog',
description: 'Toate articolele din blog',
import { useMemo, useState } from 'react'
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[]
}
function PostCard({ post }: { post: any }) {
return (
<article className="border-b border-gray-200 dark:border-gray-700 pb-8 mb-8 last:border-0">
<div className="flex flex-col lg:flex-row gap-6">
{post.frontmatter.image && (
<div className="lg:w-1/3">
<img src={post.frontmatter.image} alt={post.frontmatter.title} className="w-full h-48 lg:h-full object-cover rounded-lg" />
</div>
)}
<div className={post.frontmatter.image ? 'lg:w-2/3' : 'w-full'}>
<div className="flex items-center gap-4 text-sm text-gray-500 mb-2">
<time dateTime={post.frontmatter.date}>{formatDate(post.frontmatter.date)}</time>
<span></span>
<span>{post.readingTime} min citire</span>
<span></span>
<span>{post.frontmatter.author}</span>
</div>
<h2 className="text-2xl font-bold mb-3">
<Link href={`/blog/${post.slug}`} className="hover:text-primary-600 transition">
{post.frontmatter.title}
</Link>
</h2>
<p className="text-gray-600 dark:text-gray-400 mb-4">{post.frontmatter.description}</p>
{post.frontmatter.tags && post.frontmatter.tags.length > 0 && (
<div className="flex flex-wrap gap-2 mb-4">
{post.frontmatter.tags.map((tag: string) => (
<span key={tag} className="px-3 py-1 bg-gray-100 dark:bg-gray-800 text-sm rounded-full">
#{tag}
</span>
))}
</div>
)}
<Link href={`/blog/${post.slug}`} className="inline-flex items-center text-primary-600 hover:text-primary-700 transition">
Citește articolul complet
<svg className="ml-2 w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
</svg>
</Link>
</div>
</div>
</article>
type SortOption = 'newest' | 'oldest' | 'title'
export default function BlogPageClient({ posts, allTags }: BlogPageClientProps) {
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(() => {
let 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
)
}
function BlogFilters({ totalPosts }: { totalPosts: number }) {
return (
<div className="bg-gray-50 dark:bg-gray-800 rounded-lg p-6 mb-8">
<div className="flex items-center justify-between">
<div>
<h1 className="text-3xl font-bold mb-2">Articole Blog</h1>
<p className="text-gray-600 dark:text-gray-400">
{totalPosts} {totalPosts === 1 ? 'articol' : 'articole'} publicate
</p>
</div>
</div>
</div>
)
}
export default async function BlogPage() {
const posts = await getAllPosts()
if (posts.length === 0) {
return (
<div className="text-center py-12">
<h1 className="text-3xl font-bold mb-4">Blog</h1>
<p className="text-gray-600 dark:text-gray-400 mb-8">Nu există articole publicate încă.</p>
<Link href="/" className="inline-block px-6 py-3 bg-primary-600 text-white rounded-lg hover:bg-primary-700 transition">
Înapoi la pagina principală
</Link>
</div>
const toggleTag = (tag: string) => {
setSelectedTags((prev) =>
prev.includes(tag) ? prev.filter((t) => t !== tag) : [...prev, tag]
)
setCurrentPage(1)
}
return (
<div className="max-w-4xl mx-auto">
<BlogFilters totalPosts={posts.length} />
<div>
{posts.map((post) => (
<PostCard key={post.slug} post={post} />
))}
<div className="min-h-screen bg-zinc-900">
<Navbar />
<div className="max-w-7xl mx-auto px-6 py-12">
{/* Header */}
<div className="border-l-4 border-cyan-400 pl-6 mb-12">
<p className="font-mono text-xs text-zinc-500 uppercase tracking-widest mb-2">
DATABASE QUERY // SEARCH RESULTS
</p>
<h1 className="text-4xl md:text-6xl font-mono font-bold text-zinc-100 uppercase tracking-tight">
&gt; BLOG ARCHIVE_
</h1>
</div>
{/* Search Bar */}
<div className="border-4 border-slate-700 bg-slate-900 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-zinc-500 uppercase">
FOUND {filteredAndSortedPosts.length} {filteredAndSortedPosts.length === 1 ? 'POST' : 'POSTS'}
</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-4 border-slate-700 bg-slate-900 p-12 text-center">
<p className="font-mono text-lg text-zinc-400 uppercase">
NO POSTS FOUND // TRY DIFFERENT SEARCH TERMS
</p>
</div>
)}
{/* Pagination */}
{totalPages > 1 && (
<div className="border-4 border-slate-700 bg-slate-900 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-2 border-slate-700 text-zinc-100 disabled:opacity-30 disabled:cursor-not-allowed hover:border-cyan-400 hover:text-cyan-400 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-2 transition-colors cursor-pointer ${
currentPage === page
? 'bg-cyan-400 border-cyan-400 text-slate-900'
: 'border-slate-700 text-zinc-400 hover:border-cyan-400 hover:text-cyan-400'
}`}
>
{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-2 border-slate-700 text-zinc-100 disabled:opacity-30 disabled:cursor-not-allowed hover:border-cyan-400 hover:text-cyan-400 transition-colors cursor-pointer"
>
NEXT &gt;
</button>
</div>
</div>
)}
</div>
</div>
)