Aktuelles kot na ONE: vrstica rubrik Politik, Promis, Sport, Musik & Film (brez horoskopa), lastna stran in zdruzen API

This commit is contained in:
FOLX Bot
2026-08-16 18:33:50 +02:00
parent 164775f2b8
commit 056e1d08d6
4 changed files with 221 additions and 2 deletions
+189
View File
@@ -0,0 +1,189 @@
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 RUBRIKEN = ["Politik", "Promis", "Sport", "Musik & Film"];
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 />
{/* Vrstica rubrik kot na ONE — brez horoskopa, ta ima na folx.tv svojo stran */}
<nav
className="border-y border-card-border bg-card overflow-x-auto"
style={{ scrollbarWidth: "none" }}
data-testid="nav-aktuelles-rubriken"
>
<div className="max-w-7xl mx-auto flex items-stretch w-max md:w-full">
<button
onClick={() => go("")}
className={`px-5 py-3 text-sm font-bold uppercase tracking-wide whitespace-nowrap border-r border-card-border transition-colors ${
!rubrik ? "bg-primary text-primary-foreground" : "text-foreground/80 hover:text-primary"
}`}
data-testid="link-rubrik-alle"
>
Alle
</button>
{RUBRIKEN.map((r) => (
<button
key={r}
onClick={() => go(r)}
className={`px-5 py-3 text-sm font-bold uppercase tracking-wide whitespace-nowrap 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().replace(/[^a-z]+/g, "-")}`}
>
{r}
</button>
))}
</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"
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>
);
}