118 lines
4.1 KiB
TypeScript
118 lines
4.1 KiB
TypeScript
"use client"
|
|
|
|
import { useEffect, useState } from "react"
|
|
import { formatDistanceToNow, format } from "date-fns"
|
|
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card"
|
|
import { Badge } from "@/components/ui/badge"
|
|
import Link from "next/link"
|
|
import strings from "@/strings.json"
|
|
|
|
interface Category {
|
|
id: number
|
|
name: string
|
|
description: string
|
|
slug: string
|
|
}
|
|
|
|
interface Post {
|
|
category: string
|
|
date: number
|
|
}
|
|
|
|
export default function Home() {
|
|
const [posts, setPosts] = useState<Post[]>([])
|
|
const [categories, setCategories] = useState<Category[]>([])
|
|
const [error, setError] = useState("")
|
|
const [loading, setLoading] = useState(true)
|
|
|
|
useEffect(() => {
|
|
Promise.all([
|
|
fetch("http://localhost:3001/api/categories/fetchList").then((res) => res.json()),
|
|
fetch("http://localhost:3001/api/posts/fetchList").then((res) => res.json()),
|
|
])
|
|
.then(([categoriesData, postsData]) => {
|
|
setCategories(categoriesData.categories)
|
|
setPosts(postsData.posts)
|
|
setLoading(false)
|
|
})
|
|
.catch((error) => {
|
|
console.error("[!] Error fetching data:", error)
|
|
setError("Failed to fetch data")
|
|
setLoading(false)
|
|
})
|
|
}, [])
|
|
|
|
const formatDate = (timestamp: number) => {
|
|
const date = new Date(timestamp * 1000)
|
|
const now = new Date()
|
|
if (date.getFullYear() !== now.getFullYear()) {
|
|
return format(date, "d MMMM, yyyy")
|
|
} else {
|
|
return formatDistanceToNow(date, { addSuffix: true })
|
|
}
|
|
}
|
|
|
|
const getCategoryPostCount = (categoryName: string) => {
|
|
return posts.filter((post) => post.category === categoryName).length
|
|
}
|
|
|
|
const getLastUpdatedDate = (categoryName: string) => {
|
|
const categoryPosts = posts.filter((post) => post.category === categoryName)
|
|
if (categoryPosts.length === 0) return null
|
|
return Math.max(...categoryPosts.map((post) => post.date))
|
|
}
|
|
|
|
return (
|
|
<div className="space-y-8">
|
|
<h1 className="text-4xl font-bold text-primary">{strings.categoriesHeader}</h1>
|
|
{loading ? (
|
|
<div className="flex items-center justify-center h-[80vh]">
|
|
<div className="animate-spin rounded-full h-16 w-16 border-4 border-t-slate-800 border-white"></div>
|
|
</div>
|
|
) : error ? (
|
|
<div className="text-red-500">{error}</div>
|
|
) : (
|
|
<div className="grid gap-6 sm:grid-cols-2 lg:grid-cols-3">
|
|
{categories.map((category) => {
|
|
const postCount = getCategoryPostCount(category.name)
|
|
const lastUpdated = getLastUpdatedDate(category.name)
|
|
return (
|
|
<Card
|
|
key={category.id}
|
|
className="flex flex-col justify-between border-border/40 hover:border-border/60 transition-colors"
|
|
>
|
|
<CardHeader>
|
|
<div className="flex justify-between items-start">
|
|
<CardTitle className="text-xl text-primary">{category.name}</CardTitle>
|
|
<Badge variant="secondary" className="ml-2">
|
|
{postCount}{" "}
|
|
{postCount === 1 ? strings.categoriesPostUnitSingle : strings.categoriesPostUnitPlural}
|
|
</Badge>
|
|
</div>
|
|
<CardDescription className="mt-2">{category.description}</CardDescription>
|
|
</CardHeader>
|
|
<CardContent>
|
|
{lastUpdated && (
|
|
<p className="text-sm text-muted-foreground">
|
|
{strings.categoriesLastUpdatedLabel} {formatDate(lastUpdated)}
|
|
</p>
|
|
)}
|
|
</CardContent>
|
|
<CardFooter className="flex justify-end">
|
|
<Link
|
|
href={`/category/${category.slug}`}
|
|
className="text-sm font-medium text-primary hover:underline"
|
|
>
|
|
{strings.categoriesViewPostsFromLinkText}
|
|
</Link>
|
|
</CardFooter>
|
|
</Card>
|
|
)
|
|
})}
|
|
</div>
|
|
)}
|
|
</div>
|
|
)
|
|
}
|
|
|