folx-tv/client/src/pages/aktuelles.tsx

202 lines
8.0 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { useQuery } from "@tanstack/react-query";
import { Link, useLocation, useSearch } from "wouter";
import { type Article } from "@shared/schema";
import { useEffect } from "react";
import { usePageMeta } from "@/hooks/use-page-meta";
import { Eye, ChevronLeft, ChevronRight } 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 { PageSideAds, TopBannerAd, MultiplexAd, ArticleCardAd } from "@/components/adsense";
import { format } from "date-fns";
import { de } from "date-fns/locale";
interface Res {
articles: Article[];
total: number;
page: number;
totalPages: number;
hasMore: boolean;
rubriken: string[];
}
const ROW1 = ["Politik", "Promis", "Sport", "Musik & Film"];
const ROW2 = ["Wetter", "Finanzen"];
export default function AktuellesPage() {
const search = useSearch();
const [, setLocation] = useLocation();
const params = new URLSearchParams(search);
const page = Math.max(1, parseInt(params.get("page") || "1"));
const rubrik = params.get("rubrik") || "";
usePageMeta(
`Aktuelles${rubrik ? ` ${rubrik}` : ""}${page > 1 ? ` Seite ${page}` : ""} | FOLX TV`,
"Aktuelle Nachrichten aus Politik, Sport, Promis sowie Musik und Film täglich neu bei FOLX TV."
);
const { data, isLoading } = useQuery<Res>({
queryKey: ["/api/articles/aktuelles", rubrik, page],
queryFn: async () => {
const q = new URLSearchParams({ page: String(page), limit: "12" });
if (rubrik) q.set("rubrik", rubrik);
const r = await fetch(`/api/articles/aktuelles?${q}`);
if (!r.ok) throw new Error("laden fehlgeschlagen");
return r.json();
},
});
useEffect(() => {
window.scrollTo({ top: 0 });
}, [page, rubrik]);
const go = (r: string, p = 1) => {
const q = new URLSearchParams();
if (r) q.set("rubrik", r);
if (p > 1) q.set("page", String(p));
setLocation(`/aktuelles${q.toString() ? `?${q}` : ""}`);
};
const items = data?.articles || [];
return (
<div className="min-h-screen bg-background">
<Header />
<PageSideAds />
{/* Vrstici rubrik kot na ONE - brez horoskopa in brez Auf diesen Tag */}
<nav className="border-t border-card-border bg-card" data-testid="nav-aktuelles-rubriken">
<div className="max-w-7xl mx-auto grid grid-cols-2 sm:grid-cols-4">
{ROW1.map((r) => (
<button
key={r}
onClick={() => go(r)}
className={`px-3 py-3 text-[13px] sm:text-sm font-bold uppercase tracking-wide text-center border-r border-b border-card-border transition-colors ${
rubrik === r ? "bg-primary text-primary-foreground" : "text-foreground/80 hover:text-primary"
}`}
data-testid={`link-rubrik-${r.toLowerCase().replace(/[^a-z]+/g, "-")}`}
>
{r}
</button>
))}
</div>
</nav>
<nav className="border-b border-card-border bg-card" data-testid="nav-aktuelles-rubriken-2">
<div className="max-w-7xl mx-auto grid grid-cols-3">
{ROW2.map((r) => (
<button
key={r}
onClick={() => go(r)}
className={`px-3 py-3 text-[13px] sm:text-sm font-bold uppercase tracking-wide text-center border-r border-card-border transition-colors ${
rubrik === r ? "bg-primary text-primary-foreground" : "text-foreground/80 hover:text-primary"
}`}
data-testid={`link-rubrik-${r.toLowerCase()}`}
>
{r}
</button>
))}
<Link href="/ueber-uns">
<span
className="block px-3 py-3 text-[13px] sm:text-sm font-bold uppercase tracking-wide text-center text-foreground/80 hover:text-primary transition-colors cursor-pointer"
data-testid="link-rubrik-ueber-uns"
>
Über uns
</span>
</Link>
</div>
</nav>
<main className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-4">
<TopBannerAd />
<h1 className="text-2xl md:text-3xl font-bold text-foreground mb-4" data-testid="text-aktuelles-title">
{rubrik || "Aktuelles"}
</h1>
{isLoading ? (
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
{Array.from({ length: 6 }).map((_, i) => (
<Skeleton key={i} className="w-full h-64 rounded-lg" />
))}
</div>
) : items.length === 0 ? (
<p className="text-muted-foreground py-12">In dieser Rubrik gibt es derzeit keine Beiträge.</p>
) : (
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
{items.map((a, idx) => (
<div key={a.id} className="contents">
{idx > 0 && idx % 6 === 0 && (
<div className="sm:col-span-2 lg:col-span-3">
<ArticleCardAd />
</div>
)}
<Link href={`/article/${a.slug}`}>
<article
className="group cursor-pointer rounded-lg overflow-hidden bg-card border border-card-border h-full flex flex-col"
data-testid={`card-aktuelles-${a.id}`}
>
{a.coverImage && (
<div className="aspect-[16/9] overflow-hidden">
<img
src={a.coverImage}
alt={a.title}
loading="lazy"
style={{ objectPosition: "center 25%" }} className="w-full h-full object-cover transition-transform duration-500 group-hover:scale-105"
/>
</div>
)}
<div className="p-4 flex flex-col flex-1">
<div className="flex items-center gap-2 mb-1.5">
<span className="text-xs font-semibold text-primary uppercase">{a.category}</span>
<span className="text-xs text-muted-foreground">
{format(new Date(a.publishedAt), "d. MMM yyyy", { locale: de })}
</span>
</div>
<h2 className="font-bold text-card-foreground leading-snug line-clamp-2 group-hover:text-primary transition-colors">
{a.title}
</h2>
<p className="text-sm text-muted-foreground mt-2 leading-relaxed line-clamp-3 flex-1">
{a.excerpt}
</p>
<div className="flex items-center gap-1.5 mt-3 text-muted-foreground text-[11px]">
<Eye className="w-3 h-3" />
{a.views.toLocaleString()}
</div>
</div>
</article>
</Link>
</div>
))}
</div>
)}
{data && data.totalPages > 1 && (
<div className="flex items-center justify-center gap-2 mt-8" data-testid="pagination-aktuelles">
<Button variant="outline" size="sm" disabled={page <= 1} onClick={() => go(rubrik, page - 1)}>
<ChevronLeft className="w-4 h-4" />
</Button>
<span className="text-sm text-muted-foreground px-2">
Seite {page} von {data.totalPages}
</span>
<Button
variant="outline"
size="sm"
disabled={page >= data.totalPages}
onClick={() => go(rubrik, page + 1)}
>
<ChevronRight className="w-4 h-4" />
</Button>
</div>
)}
<div className="mt-8">
<div className="text-[9px] text-muted-foreground/40 text-center pb-1 uppercase tracking-widest">Anzeige</div>
<MultiplexAd />
</div>
</main>
<Footer />
</div>
);
}