Implement new routes for articles, categories, and individual articles. Update the UI to display articles with improved content rendering, including safe HTML and media embeds. Refactor storage to use a database and add image upload capabilities. Replit-Commit-Author: Agent Replit-Commit-Session-Id: 413891e8-d784-4bea-b9f5-91a5a68316b4 Replit-Commit-Checkpoint-Type: full_checkpoint Replit-Commit-Event-Id: b96b221e-0ed6-418f-80df-e4670bf5ba4b Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/f209e72a-0939-48fa-84fc-57854de71967/413891e8-d784-4bea-b9f5-91a5a68316b4/cftwqyT Replit-Helium-Checkpoint-Created: true
108 lines
4.5 KiB
TypeScript
108 lines
4.5 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 { Badge } from "@/components/ui/badge";
|
|
import { Button } from "@/components/ui/button";
|
|
import { Skeleton } from "@/components/ui/skeleton";
|
|
import Header from "@/components/header";
|
|
import Footer from "@/components/footer";
|
|
|
|
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\u00fcck
|
|
</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.map((article) => (
|
|
<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"
|
|
data-testid={`card-article-${article.id}`}
|
|
>
|
|
<div className="relative 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"
|
|
loading="lazy"
|
|
/>
|
|
<Badge className="absolute top-3 left-3 text-xs no-default-active-elevate">
|
|
{article.category}
|
|
</Badge>
|
|
</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>
|
|
))}
|
|
</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>
|
|
);
|
|
}
|