diff --git a/src/app/[locale]/blog/page.tsx b/src/app/[locale]/blog/page.tsx index 1b33fbc..7bbd402 100644 --- a/src/app/[locale]/blog/page.tsx +++ b/src/app/[locale]/blog/page.tsx @@ -1,16 +1,12 @@ import { setRequestLocale } from 'next-intl/server'; import { getTranslations } from 'next-intl/server'; -import { ArrowRight, Calendar, Tag, ExternalLink, ChevronLeft, ChevronRight } from 'lucide-react'; -import Link from 'next/link'; -import Image from 'next/image'; +import { ArrowRight } from 'lucide-react'; import type { Metadata } from 'next'; import { getAllBlogPosts, BlogPost } from '@/lib/blog'; - -const POSTS_PER_PAGE = 12; +import { BlogList } from '@/components/blog/BlogList'; type Props = { params: Promise<{ locale: string }>; - searchParams: Promise<{ page?: string }>; }; // Legacy blog posts with translations (these have manual translations) @@ -307,277 +303,9 @@ export async function generateMetadata({ params }: Props): Promise { }; } -function getFormattedDate(date: string, locale: string) { - const languageMap: Record = { - de: 'de-DE', - en: 'en-US', - sr: 'sr-RS', - }; - return new Date(date).toLocaleDateString(languageMap[locale] || 'de-DE', { - year: 'numeric', - month: 'long', - day: 'numeric', - }); -} - -function getImagePath(coverImage: string) { - if (coverImage.startsWith('http')) { - return coverImage; - } - return coverImage.replace('/blog/', '/images/posts/'); -} - -// Placeholder image for posts without cover images -const PLACEHOLDER_IMAGE = 'data:image/svg+xml,' + encodeURIComponent(` - - - - - - - -`); - -// Known existing images (from public/images/posts/) -const EXISTING_IMAGES = new Set([ - '/images/posts/erp-integration-breuninger/cover.jpg', - '/images/posts/fullstack-development-timetracking/cover.jpg', - '/images/posts/rfid-automation/cover.jpg', - '/images/posts/automated-ad-creatives/cover.jpg', -]); - -// Blog image component with fallback -function BlogImage({ src, alt, fallback }: { src: string; alt: string; fallback: string }) { - const imageSrc = EXISTING_IMAGES.has(src) ? src : fallback; - - return ( - {alt} - ); -} - -function BlogPostCard({ post, locale }: { post: BlogPost; locale: string }) { - return ( - -
- {/* Image Container */} -
- -
-
- - {/* Content */} -
-
-
- - {getFormattedDate(post.date, locale)} -
- {post.tags && post.tags.length > 0 && ( -
- - {post.tags.length} Tags -
- )} -
- -

- {post.title} -

-

- {post.excerpt} -

- - {/* Tags */} -
- {post.tags?.slice(0, 3).map((tag, index) => ( - - {tag} - - ))} - {post.tags && post.tags.length > 3 && ( - - +{post.tags.length - 3} more - - )} -
- - {/* Link Icon */} -
- -
-
-
- - ); -} - -// Pagination component -function Pagination({ - currentPage, - totalPages, - locale, - t, -}: { - currentPage: number; - totalPages: number; - locale: string; - t: (key: string) => string; -}) { - const getPageNumbers = () => { - const pages: (number | 'ellipsis')[] = []; - - if (totalPages <= 7) { - // Show all pages if 7 or fewer - for (let i = 1; i <= totalPages; i++) { - pages.push(i); - } - } else { - // Always show first page - pages.push(1); - - if (currentPage > 3) { - pages.push('ellipsis'); - } - - // Show pages around current - const start = Math.max(2, currentPage - 1); - const end = Math.min(totalPages - 1, currentPage + 1); - - for (let i = start; i <= end; i++) { - pages.push(i); - } - - if (currentPage < totalPages - 2) { - pages.push('ellipsis'); - } - - // Always show last page - pages.push(totalPages); - } - - return pages; - }; - - if (totalPages <= 1) return null; - - return ( - - ); -} - -export default async function BlogPage({ params, searchParams }: Props) { +export default async function BlogPage({ params }: Props) { const { locale } = await params; - const { page } = await searchParams; setRequestLocale(locale); const t = await getTranslations('blog'); @@ -595,15 +323,6 @@ export default async function BlogPage({ params, searchParams }: Props) { ...legacyPosts, ].sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime()); - // Pagination - const currentPage = Math.max(1, parseInt(page || '1', 10) || 1); - const totalPages = Math.ceil(allPosts.length / POSTS_PER_PAGE); - const validPage = Math.min(currentPage, totalPages || 1); - - const startIndex = (validPage - 1) * POSTS_PER_PAGE; - const endIndex = startIndex + POSTS_PER_PAGE; - const posts = allPosts.slice(startIndex, endIndex); - return (
@@ -619,37 +338,21 @@ export default async function BlogPage({ params, searchParams }: Props) {

{t('meta.header.subtitle')}

- {/* Post count */} -

- {t('ui.pagination.showing', { - start: startIndex + 1, - end: Math.min(endIndex, allPosts.length), - total: allPosts.length, - })} -

- {/* Blog Posts Grid */} -
- {posts.map((post) => ( - - ))} -
- - {/* Pagination */} - t(key)} + translations={{ + searchPlaceholder: t('ui.search.placeholder'), + showingText: (start: number, end: number, total: number) => + t('ui.pagination.showing', { start, end, total }), + noPostsText: t('ui.errors.posts'), + paginationPrevious: t('ui.pagination.previous'), + paginationNext: t('ui.pagination.next'), + }} /> - - {/* Empty State */} - {posts.length === 0 && ( -
-

{t('ui.errors.posts')}

-
- )}
); diff --git a/src/components/blog/BlogList.tsx b/src/components/blog/BlogList.tsx new file mode 100644 index 0000000..381b537 --- /dev/null +++ b/src/components/blog/BlogList.tsx @@ -0,0 +1,403 @@ +'use client'; + +import { useState, useMemo } from 'react'; +import { Calendar, Tag, ExternalLink, ChevronLeft, ChevronRight } from 'lucide-react'; +import Link from 'next/link'; +import Image from 'next/image'; +import { BlogPost } from '@/lib/blog'; +import { SearchBar } from './SearchBar'; +import { CategoryFilter } from './CategoryFilter'; + +const POSTS_PER_PAGE = 12; + +// Placeholder image for posts without cover images +const PLACEHOLDER_IMAGE = 'data:image/svg+xml,' + encodeURIComponent(` + + + + + + + +`); + +// Known existing images (from public/images/posts/) +const EXISTING_IMAGES = new Set([ + '/images/posts/erp-integration-breuninger/cover.jpg', + '/images/posts/fullstack-development-timetracking/cover.jpg', + '/images/posts/rfid-automation/cover.jpg', + '/images/posts/automated-ad-creatives/cover.jpg', +]); + +function getFormattedDate(date: string, locale: string) { + const languageMap: Record = { + de: 'de-DE', + en: 'en-US', + sr: 'sr-RS', + }; + + return new Date(date).toLocaleDateString(languageMap[locale] || 'de-DE', { + year: 'numeric', + month: 'long', + day: 'numeric', + }); +} + +function getImagePath(coverImage: string) { + if (coverImage.startsWith('http')) { + return coverImage; + } + return coverImage.replace('/blog/', '/images/posts/'); +} + +// Blog image component with fallback +function BlogImage({ src, alt, fallback }: { src: string; alt: string; fallback: string }) { + const imageSrc = EXISTING_IMAGES.has(src) ? src : fallback; + + return ( + {alt} + ); +} + +function BlogPostCard({ post, locale }: { post: BlogPost; locale: string }) { + return ( + +
+ {/* Image Container */} +
+ +
+
+ + {/* Content */} +
+
+
+ + {getFormattedDate(post.date, locale)} +
+ {post.tags && post.tags.length > 0 && ( +
+ + {post.tags.length} Tags +
+ )} +
+ +

+ {post.title} +

+

+ {post.excerpt} +

+ + {/* Tags */} +
+ {post.tags?.slice(0, 3).map((tag, index) => ( + + {tag} + + ))} + {post.tags && post.tags.length > 3 && ( + + +{post.tags.length - 3} more + + )} +
+ + {/* Link Icon */} +
+ +
+
+
+ + ); +} + +// Pagination component +function Pagination({ + currentPage, + totalPages, + onPageChange, + t, +}: { + currentPage: number; + totalPages: number; + onPageChange: (page: number) => void; + t: (key: string) => string; +}) { + const getPageNumbers = () => { + const pages: (number | 'ellipsis')[] = []; + + if (totalPages <= 7) { + // Show all pages if 7 or fewer + for (let i = 1; i <= totalPages; i++) { + pages.push(i); + } + } else { + // Always show first page + pages.push(1); + + if (currentPage > 3) { + pages.push('ellipsis'); + } + + // Show pages around current + const start = Math.max(2, currentPage - 1); + const end = Math.min(totalPages - 1, currentPage + 1); + + for (let i = start; i <= end; i++) { + pages.push(i); + } + + if (currentPage < totalPages - 2) { + pages.push('ellipsis'); + } + + // Always show last page + pages.push(totalPages); + } + + return pages; + }; + + if (totalPages <= 1) return null; + + return ( + + ); +} + +interface BlogListProps { + posts: BlogPost[]; + locale: string; + translations: { + searchPlaceholder: string; + showingText: (start: number, end: number, total: number) => string; + noPostsText: string; + paginationPrevious: string; + paginationNext: string; + }; +} + +export function BlogList({ posts, locale, translations }: BlogListProps) { + const [searchQuery, setSearchQuery] = useState(''); + const [selectedCategory, setSelectedCategory] = useState(null); + const [currentPage, setCurrentPage] = useState(1); + + // Extract unique categories from all posts + const allCategories = useMemo(() => { + const categories = new Set(posts.map(post => post.category).filter(Boolean)); + return Array.from(categories).sort(); + }, [posts]); + + // Filter posts based on search query and category + const filteredPosts = useMemo(() => { + let filtered = posts; + + // Apply search filter (case-insensitive search in title, excerpt, and tags) + if (searchQuery.trim()) { + const query = searchQuery.toLowerCase(); + filtered = filtered.filter(post => { + const titleMatch = post.title.toLowerCase().includes(query); + const excerptMatch = post.excerpt.toLowerCase().includes(query); + const tagsMatch = post.tags?.some(tag => tag.toLowerCase().includes(query)) || false; + return titleMatch || excerptMatch || tagsMatch; + }); + } + + // Apply category filter + if (selectedCategory) { + filtered = filtered.filter(post => post.category === selectedCategory); + } + + return filtered; + }, [posts, searchQuery, selectedCategory]); + + // Reset to first page when filters change + useMemo(() => { + setCurrentPage(1); + }, [searchQuery, selectedCategory]); + + // Paginate filtered posts + const totalPages = Math.ceil(filteredPosts.length / POSTS_PER_PAGE); + const validPage = Math.min(Math.max(1, currentPage), totalPages || 1); + + const startIndex = (validPage - 1) * POSTS_PER_PAGE; + const endIndex = startIndex + POSTS_PER_PAGE; + const paginatedPosts = filteredPosts.slice(startIndex, endIndex); + + const handlePageChange = (page: number) => { + setCurrentPage(page); + // Scroll to top of the page + window.scrollTo({ top: 0, behavior: 'smooth' }); + }; + + return ( +
+ {/* Search and Filter Section */} +
+ {/* Search Bar */} + + + {/* Category Filter */} + +
+ + {/* Results count */} +

+ {translations.showingText( + startIndex + 1, + Math.min(endIndex, filteredPosts.length), + filteredPosts.length + )} +

+ + {/* Blog Posts Grid */} + {paginatedPosts.length > 0 ? ( + <> +
+ {paginatedPosts.map((post) => ( + + ))} +
+ + {/* Pagination */} + { + if (key === 'ui.pagination.previous') return translations.paginationPrevious; + if (key === 'ui.pagination.next') return translations.paginationNext; + return ''; + }} + /> + + ) : ( + /* Empty State */ +
+

{translations.noPostsText}

+
+ )} +
+ ); +} diff --git a/src/messages/de.json b/src/messages/de.json index 0f68cc7..bf4479f 100644 --- a/src/messages/de.json +++ b/src/messages/de.json @@ -463,12 +463,15 @@ }, "ui": { "readMore": "Weiterlesen", + "search": { + "placeholder": "Beiträge durchsuchen..." + }, "loading": { "posts": "Blogbeiträge werden geladen...", "post": "Blogbeitrag wird geladen..." }, "errors": { - "posts": "Fehler beim Laden der Blogbeiträge", + "posts": "Keine Beiträge gefunden", "post": "Der gesuchte Blogbeitrag existiert nicht oder wurde verschoben." }, "backToBlog": "Zurück zum Blog", diff --git a/src/messages/en.json b/src/messages/en.json index 6ffe764..f462997 100644 --- a/src/messages/en.json +++ b/src/messages/en.json @@ -478,12 +478,15 @@ }, "ui": { "readMore": "Read More", + "search": { + "placeholder": "Search posts..." + }, "loading": { "posts": "Loading blog posts...", "post": "Loading blog post..." }, "errors": { - "posts": "Error loading blog posts", + "posts": "No posts found", "post": "The requested blog post does not exist or has been moved." }, "backToBlog": "Back to Blog", diff --git a/src/messages/sr.json b/src/messages/sr.json index ac5b343..9db01e4 100644 --- a/src/messages/sr.json +++ b/src/messages/sr.json @@ -485,12 +485,15 @@ }, "ui": { "readMore": "Procitaj vise", + "search": { + "placeholder": "Pretrazi postove..." + }, "loading": { "posts": "Ucitavanje blog postova...", "post": "Ucitavanje blog posta..." }, "errors": { - "posts": "Greska pri ucitavanju blog postova", + "posts": "Nema pronađenih postova", "post": "Trazeni blog post ne postoji ili je premesten." }, "backToBlog": "Nazad na Blog",