import { Link, Outlet, createFileRoute, useMatch } from '@tanstack/react-router' import { useTranslation } from 'react-i18next' import { useEffect, useState } from 'react' import { ArrowRight, Calendar, Search, Tag } from 'lucide-react' import { Input } from '@components/ui/input' import { Badge } from '@components/ui/badge' import { Skeleton } from '@components/ui/skeleton' import { cn } from '@lib/utils' import { useDebounce } from '@/hooks/use-debounce' import { useArticleTags, useArticles } from '@/hooks/use-articles' import { useAppSearchParams } from '@/lib/use-app-search-params' export const Route = createFileRoute('/blog')({ component: BlogLayout, }) function BlogLayout() { const match = useMatch({ from: '/blog/$slug', shouldThrow: false }) if (match) { return } return } function BlogPage() { const { getParam, setParam } = useAppSearchParams() const { t } = useTranslation() const [page, setPage] = useState(1) const searchParam = getParam('search') const tagParam = getParam('tag') const [searchInput, setSearchInput] = useState(searchParam ?? '') const [selectedTag, setSelectedTag] = useState(tagParam) const debouncedSearch = useDebounce(searchInput, 400) const debouncedTag = useDebounce(selectedTag ?? '', 200) useEffect(() => { const normalizedParam = searchParam ?? '' setSearchInput((current) => current === normalizedParam ? current : normalizedParam, ) }, [searchParam]) useEffect(() => { const normalizedParam = searchParam ?? '' if (debouncedSearch === normalizedParam) { return } setParam('search', debouncedSearch) setPage(1) }, [debouncedSearch, searchParam, setParam]) useEffect(() => { setSelectedTag(tagParam) }, [tagParam]) useEffect(() => { const normalizedTag = tagParam ?? '' if (debouncedTag === normalizedTag) { return } if (debouncedTag) { setParam('tag', debouncedTag) } else { setParam('tag', '') } setPage(1) }, [debouncedTag, tagParam, setParam]) const { data, isLoading } = useArticles({ page, limit: 9, search: searchParam || undefined, tag: tagParam || undefined, }) const { data: tags } = useArticleTags() const handleTagClick = (tag: string) => { setSelectedTag((current) => (current === tag ? null : tag)) } const totalPages = data ? Math.ceil(data.total / 9) : 0 return (

{t('common.blog')}

{t( 'blog.description', 'Thoughts, tutorials, and insights about web development and technology.', )}

setSearchInput(e.target.value)} className="pl-9" />
{tags && tags.length > 0 && (
{tags.map((tag) => ( handleTagClick(tag)} > {tag} ))} {selectedTag && ( setSelectedTag(null)} > {t('blog.clearFilter', 'Clear filter')} )}
)}
{isLoading ? (
{[1, 2, 3, 4, 5, 6].map((i) => ( ))}
) : data?.articles.length === 0 ? (

{searchParam || selectedTag ? t( 'blog.noResults', 'No articles found matching your criteria.', ) : t( 'blog.noArticles', 'No articles published yet. Check back soon!', )}

) : ( <>
{data?.articles.map((article) => ( ))}
{totalPages > 1 && (
{page} / {totalPages}
)} )}
) } interface ArticleCardProps { article: { id: string title: string slug: string preview: string coverImage?: string tags: Array createdAt: string } } function ArticleCard({ article }: ArticleCardProps) { return ( {article.coverImage ? (
{article.title}
) : (
{article.title.charAt(0)}
)}
{article.tags.length > 0 && (
{article.tags.slice(0, 3).map((tag) => ( {tag} ))}
)}

{article.title}

{article.preview}

{new Date(article.createdAt).toLocaleDateString()}
Read more
) } function ArticleCardSkeleton() { return (
) }