- fixed routing
This commit is contained in:
180
app/blog/blog-client.tsx
Normal file
180
app/blog/blog-client.tsx
Normal file
@@ -0,0 +1,180 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
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[]
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
)
|
||||||
|
|
||||||
|
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-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">
|
||||||
|
> 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"
|
||||||
|
>
|
||||||
|
< 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 >
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,15 +1,10 @@
|
|||||||
import { Metadata } from 'next'
|
import { Metadata } from 'next'
|
||||||
import { getAllPosts } from '@/lib/markdown'
|
|
||||||
import BlogPageClient from './page'
|
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: 'Blog',
|
title: 'Blog',
|
||||||
description: 'Toate articolele din blog',
|
description: 'Toate articolele din blog',
|
||||||
}
|
}
|
||||||
|
|
||||||
export default async function BlogLayout() {
|
export default function BlogLayout({ children }: { children: React.ReactNode }) {
|
||||||
const posts = await getAllPosts()
|
return <>{children}</>
|
||||||
const allTags = Array.from(new Set(posts.flatMap((post) => post.frontmatter.tags))).sort()
|
|
||||||
|
|
||||||
return <BlogPageClient posts={posts} allTags={allTags} />
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,180 +1,9 @@
|
|||||||
'use client'
|
import { getAllPosts } from '@/lib/markdown'
|
||||||
|
import BlogPageClient from './blog-client'
|
||||||
|
|
||||||
import { useMemo, useState } from 'react'
|
export default async function BlogPage() {
|
||||||
import { Post } from '@/lib/types/frontmatter'
|
const posts = await getAllPosts()
|
||||||
import { BlogCard } from '@/components/blog/blog-card'
|
const allTags = Array.from(new Set(posts.flatMap((post) => post.frontmatter.tags))).sort()
|
||||||
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 {
|
return <BlogPageClient posts={posts} allTags={allTags} />
|
||||||
posts: Post[]
|
|
||||||
allTags: string[]
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
|
||||||
)
|
|
||||||
|
|
||||||
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-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">
|
|
||||||
> 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"
|
|
||||||
>
|
|
||||||
< 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 >
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ export function validateFrontmatter(data: any): FrontMatter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function getPostBySlug(slug: string | string[]): Post | null {
|
export function getPostBySlug(slug: string | string[]): Post | null {
|
||||||
const slugArray = Array.isArray(slug) ? slug : [slug];
|
const slugArray = Array.isArray(slug) ? slug : slug.split('/');
|
||||||
const sanitized = slugArray.map(s => sanitizePath(s));
|
const sanitized = slugArray.map(s => sanitizePath(s));
|
||||||
const fullPath = path.join(POSTS_PATH, ...sanitized) + '.md';
|
const fullPath = path.join(POSTS_PATH, ...sanitized) + '.md';
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user