114 lines
4.8 KiB
TypeScript
114 lines
4.8 KiB
TypeScript
import { useQuery } from "@tanstack/react-query";
|
|
import { useParams, Link } from "wouter";
|
|
import { type Article } from "@shared/schema";
|
|
import { format } from "date-fns";
|
|
import { de } from "date-fns/locale";
|
|
import { Eye, ArrowLeft } from "lucide-react";
|
|
import { Button } from "@/components/ui/button";
|
|
import { Skeleton } from "@/components/ui/skeleton";
|
|
import Header from "@/components/header";
|
|
import Footer from "@/components/footer";
|
|
import { ArticleCardAd } from "@/components/adsense";
|
|
|
|
export default function CategoryPage() {
|
|
const { category } = useParams<{ category: string }>();
|
|
|
|
const { data: articles, isLoading } = useQuery<Article[]>({
|
|
queryKey: ["/api/articles/category", category],
|
|
});
|
|
|
|
return (
|
|
<div className="min-h-screen bg-background">
|
|
<Header />
|
|
<main className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
|
<Link href="/">
|
|
<Button variant="ghost" size="sm" className="mb-6 gap-2" data-testid="button-back">
|
|
<ArrowLeft className="w-4 h-4" />
|
|
Zurück
|
|
</Button>
|
|
</Link>
|
|
|
|
<h1 className="text-2xl font-bold text-foreground mb-6" data-testid="text-category-title">
|
|
{category}
|
|
</h1>
|
|
|
|
{isLoading ? (
|
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
|
{Array.from({ length: 6 }).map((_, i) => (
|
|
<div key={i} className="bg-card rounded-md border border-card-border">
|
|
<Skeleton className="w-full aspect-video rounded-t-md" />
|
|
<div className="p-4 space-y-3">
|
|
<Skeleton className="h-3 w-2/3" />
|
|
<Skeleton className="h-5 w-full" />
|
|
<Skeleton className="h-4 w-3/4" />
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
) : articles && articles.length > 0 ? (
|
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
|
{articles.flatMap((article, index) => {
|
|
const items = [
|
|
<Link key={article.id} href={`/article/${article.slug}`}>
|
|
<article
|
|
className="group cursor-pointer bg-card rounded-md border border-card-border transition-all duration-300 h-full"
|
|
data-testid={`card-article-${article.id}`}
|
|
>
|
|
<div className="relative rounded-t-md">
|
|
<div className="overflow-hidden rounded-t-md">
|
|
<img
|
|
src={article.coverImage || "/images/article-1.png"}
|
|
alt={article.title}
|
|
className="w-full aspect-video object-cover transition-transform duration-500 group-hover:scale-105"
|
|
style={{ objectPosition: "center 25%" }}
|
|
loading="lazy"
|
|
/>
|
|
</div>
|
|
</div>
|
|
<div className="p-4">
|
|
<div className="flex items-center gap-2 text-muted-foreground text-xs mb-2">
|
|
<span>{article.author}</span>
|
|
<span>·</span>
|
|
<span>{format(new Date(article.publishedAt), "d. MMMM yyyy", { locale: de })}</span>
|
|
</div>
|
|
<h3 className="font-semibold text-card-foreground mb-2 line-clamp-2 group-hover:text-primary transition-colors">
|
|
{article.title}
|
|
</h3>
|
|
<p className="text-muted-foreground text-sm line-clamp-3 mb-3">
|
|
{article.excerpt}
|
|
</p>
|
|
<div className="flex items-center justify-between gap-2">
|
|
<span className="flex items-center gap-1 text-xs text-muted-foreground">
|
|
<Eye className="w-3.5 h-3.5" />
|
|
{article.views.toLocaleString()}
|
|
</span>
|
|
<Button size="sm" data-testid={`button-read-${article.id}`}>
|
|
Weiterlesen
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
</article>
|
|
</Link>,
|
|
];
|
|
if (index === 2) {
|
|
items.push(<ArticleCardAd key="ad-cat-1" />);
|
|
}
|
|
return items;
|
|
})}
|
|
</div>
|
|
) : (
|
|
<div className="text-center py-16">
|
|
<p className="text-muted-foreground text-lg">
|
|
Keine Artikel in dieser Kategorie gefunden.
|
|
</p>
|
|
<Link href="/">
|
|
<Button className="mt-4" data-testid="button-back-home">Zur Startseite</Button>
|
|
</Link>
|
|
</div>
|
|
)}
|
|
</main>
|
|
<Footer />
|
|
</div>
|
|
);
|
|
}
|