import { useCallback, useEffect, useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { Link } from 'react-router-dom'; import Layout from '../components/Layout'; import { listBlogPosts, listBlogTags } from '../blog/api'; import type { BlogPostSummary, BlogTag } from '../blog/types'; import '../App.css'; const PAGE_SIZE = 6; function formatDate(timestamp: number): string { if (!timestamp) { return ''; } const millis = timestamp < 1_000_000_000_000 ? timestamp * 1000 : timestamp; return new Date(millis).toLocaleDateString(undefined, { year: 'numeric', month: 'short', day: 'numeric', }); } function Blog() { const { t } = useTranslation(); const [tags, setTags] = useState([]); const [isLoadingTags, setIsLoadingTags] = useState(false); const [tagsError, setTagsError] = useState(null); const [selectedTags, setSelectedTags] = useState([]); const [posts, setPosts] = useState([]); const [page, setPage] = useState(1); const [hasMore, setHasMore] = useState(false); const [isLoadingPosts, setIsLoadingPosts] = useState(false); const [postError, setPostError] = useState(null); const isInitialLoading = isLoadingPosts && posts.length === 0; useEffect(() => { let active = true; (async () => { setIsLoadingTags(true); setTagsError(null); try { const response = await listBlogTags(); if (!active) { return; } setTags(response.tags ?? []); } catch (error) { if (!active) { return; } const message = error instanceof Error ? error.message : 'Failed to load tags'; setTagsError(message); console.error('Failed to load blog tags:', error); } finally { if (active) { setIsLoadingTags(false); } } })(); return () => { active = false; }; }, []); const fetchPosts = useCallback( async (pageToLoad: number, replace = false) => { setIsLoadingPosts(true); setPostError(null); if (replace) { setPosts([]); } try { const response = await listBlogPosts({ page: pageToLoad, pageSize: PAGE_SIZE, tagIds: selectedTags, }); const total = response.totalCount ?? response.posts.length; setPage(pageToLoad); setPosts((previous) => { const updated = replace ? response.posts : [...previous, ...response.posts]; setHasMore(updated.length < total); return updated; }); } catch (error) { const message = error instanceof Error ? error.message : 'Failed to load posts'; setPostError(message); console.error('Failed to load blog posts:', error); } finally { setIsLoadingPosts(false); } }, [selectedTags] ); useEffect(() => { fetchPosts(1, true); }, [fetchPosts]); const featuredPost = useMemo(() => (posts.length > 0 ? posts[0] : null), [posts]); const otherPosts = useMemo(() => (featuredPost ? posts.slice(1) : posts), [posts, featuredPost]); const handleLoadMore = () => { if (isLoadingPosts || !hasMore) { return; } fetchPosts(page + 1, false); }; const toggleTag = (tagId: string) => { setSelectedTags((current) => { if (current.includes(tagId)) { return current.filter((id) => id !== tagId); } return [...current, tagId]; }); }; const clearTags = () => { setSelectedTags([]); }; return (

{t('blog.title')}

{t('blog.subtitle')}

{ event.currentTarget.style.transform = 'translateY(-2px)'; event.currentTarget.style.boxShadow = '0 6px 18px var(--accent-shadow)'; }} onMouseLeave={(event) => { event.currentTarget.style.transform = 'translateY(0)'; event.currentTarget.style.boxShadow = 'none'; }} > {t('blog.readMore')} {selectedTags.length > 0 && ( )}

Tags

{isLoadingTags && Loading...}
{tagsError && (
{tagsError}
)}
{tags.map((tag) => { const isSelected = selectedTags.includes(tag.tagId); return ( ); })} {!isLoadingTags && tags.length === 0 && !tagsError && ( No tags available yet. )}
{postError && (
{postError}
)} {featuredPost && (
Featured

{featuredPost.title}

{formatDate(featuredPost.createdAt)} Updated {formatDate(featuredPost.updatedAt)}
{ event.currentTarget.style.transform = 'translateY(-2px)'; }} onMouseLeave={(event) => { event.currentTarget.style.transform = 'translateY(0)'; }} > {'Read article ->'}
)} {!isInitialLoading && posts.length === 0 && !postError && (
No posts yet. Be the first to share something with the community!
)}
{otherPosts.map((post) => (

{post.title}

Published {formatDate(post.createdAt)}
{'Read more ->'}
))}
{isInitialLoading && (
{Array.from({ length: 3 }).map((_, index) => (
))}
)} {hasMore && (
)}
); } export default Blog;