diff --git a/app/blog/blog-client.tsx b/app/blog/blog-client.tsx new file mode 100644 index 0000000..3183960 --- /dev/null +++ b/app/blog/blog-client.tsx @@ -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([]) + const [sortBy, setSortBy] = useState('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 ( +
+ + +
+ {/* Header */} +
+

+ DATABASE QUERY // SEARCH RESULTS +

+

+ > BLOG ARCHIVE_ +

+
+ + {/* Search Bar */} +
+
+ { + setSearchQuery(value) + setCurrentPage(1) + }} + /> + +
+
+ + {/* Tag Filters */} + { + setSelectedTags([]) + setCurrentPage(1) + }} + /> + + {/* Results Count */} +
+

+ FOUND {filteredAndSortedPosts.length} {filteredAndSortedPosts.length === 1 ? 'POST' : 'POSTS'} +

+
+ + {/* Blog Grid */} + {paginatedPosts.length > 0 ? ( +
+ {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 + })} +
+ ) : ( +
+

+ NO POSTS FOUND // TRY DIFFERENT SEARCH TERMS +

+
+ )} + + {/* Pagination */} + {totalPages > 1 && ( +
+
+ +
+ {Array.from({ length: totalPages }, (_, i) => i + 1).map((page) => ( + + ))} +
+ +
+
+ )} +
+
+ ) +} diff --git a/app/blog/layout.tsx b/app/blog/layout.tsx index 5a06427..ed2682a 100644 --- a/app/blog/layout.tsx +++ b/app/blog/layout.tsx @@ -1,15 +1,10 @@ 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 +export default function BlogLayout({ children }: { children: React.ReactNode }) { + return <>{children} } diff --git a/app/blog/page.tsx b/app/blog/page.tsx index 3183960..37399c4 100644 --- a/app/blog/page.tsx +++ b/app/blog/page.tsx @@ -1,180 +1,9 @@ -'use client' +import { getAllPosts } from '@/lib/markdown' +import BlogPageClient from './blog-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' +export default async function BlogPage() { + const posts = await getAllPosts() + const allTags = Array.from(new Set(posts.flatMap((post) => post.frontmatter.tags))).sort() -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([]) - const [sortBy, setSortBy] = useState('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 ( -
- - -
- {/* Header */} -
-

- DATABASE QUERY // SEARCH RESULTS -

-

- > BLOG ARCHIVE_ -

-
- - {/* Search Bar */} -
-
- { - setSearchQuery(value) - setCurrentPage(1) - }} - /> - -
-
- - {/* Tag Filters */} - { - setSelectedTags([]) - setCurrentPage(1) - }} - /> - - {/* Results Count */} -
-

- FOUND {filteredAndSortedPosts.length} {filteredAndSortedPosts.length === 1 ? 'POST' : 'POSTS'} -

-
- - {/* Blog Grid */} - {paginatedPosts.length > 0 ? ( -
- {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 - })} -
- ) : ( -
-

- NO POSTS FOUND // TRY DIFFERENT SEARCH TERMS -

-
- )} - - {/* Pagination */} - {totalPages > 1 && ( -
-
- -
- {Array.from({ length: totalPages }, (_, i) => i + 1).map((page) => ( - - ))} -
- -
-
- )} -
-
- ) + return } diff --git a/lib/markdown.ts b/lib/markdown.ts index f540562..62134bd 100644 --- a/lib/markdown.ts +++ b/lib/markdown.ts @@ -53,7 +53,7 @@ export function validateFrontmatter(data: any): FrontMatter { } 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 fullPath = path.join(POSTS_PATH, ...sanitized) + '.md';