'use client'; import { useState, useMemo, useEffect } from 'react'; import { Calendar, Tag, ExternalLink, ChevronLeft, ChevronRight } from 'lucide-react'; import Link from 'next/link'; import Image from 'next/image'; import { useRouter, usePathname } from 'next/navigation'; 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 ( ); } 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 ( {/* Previous button */} {currentPage > 1 ? ( onPageChange(currentPage - 1)} className="flex items-center gap-1 px-3 py-2 text-sm text-zinc-400 hover:text-white bg-zinc-800/50 hover:bg-zinc-800 rounded-lg ring-1 ring-zinc-700/50 hover:ring-zinc-600 transition-all duration-200" aria-label={t('ui.pagination.previous')} > {t('ui.pagination.previous')} ) : ( {t('ui.pagination.previous')} )} {/* Page numbers */} {getPageNumbers().map((page, index) => { if (page === 'ellipsis') { return ( ... ); } const isCurrentPage = page === currentPage; return ( onPageChange(page)} className={`min-w-[40px] h-10 flex items-center justify-center text-sm rounded-lg ring-1 transition-all duration-200 ${isCurrentPage ? 'bg-[#697565] text-white ring-[#697565] font-medium' : 'text-zinc-400 hover:text-white bg-zinc-800/50 hover:bg-zinc-800 ring-zinc-700/50 hover:ring-zinc-600' }`} aria-current={isCurrentPage ? 'page' : undefined} > {page} ); })} {/* Next button */} {currentPage < totalPages ? ( onPageChange(currentPage + 1)} className="flex items-center gap-1 px-3 py-2 text-sm text-zinc-400 hover:text-white bg-zinc-800/50 hover:bg-zinc-800 rounded-lg ring-1 ring-zinc-700/50 hover:ring-zinc-600 transition-all duration-200" aria-label={t('ui.pagination.next')} > {t('ui.pagination.next')} ) : ( {t('ui.pagination.next')} )} ); } interface BlogListProps { posts: BlogPost[]; locale: string; initialSearch?: string; initialCategory?: string | null; translations: { searchPlaceholder: string; showingText: (start: number, end: number, total: number) => string; noPostsText: string; paginationPrevious: string; paginationNext: string; }; } export function BlogList({ posts, locale, initialSearch = '', initialCategory = null, translations }: BlogListProps) { const router = useRouter(); const pathname = usePathname(); const [searchQuery, setSearchQuery] = useState(initialSearch); const [selectedCategory, setSelectedCategory] = useState(initialCategory); const [currentPage, setCurrentPage] = useState(1); // Update URL when filters change useEffect(() => { const params = new URLSearchParams(); if (searchQuery.trim()) { params.set('search', searchQuery.trim()); } if (selectedCategory) { params.set('category', selectedCategory); } const queryString = params.toString(); const newUrl = queryString ? `${pathname}?${queryString}` : pathname; router.push(newUrl, { scroll: false }); }, [searchQuery, selectedCategory, pathname, router]); // 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} )} ); }
{post.excerpt}
{translations.showingText( startIndex + 1, Math.min(endIndex, filteredPosts.length), filteredPosts.length )}
{translations.noPostsText}