Add pagination to article categories and video pages, and enhance home page with more content and ads

Implement paginated API endpoints for articles and videos, add pagination UI to category and video pages, and inject additional ads and content widgets into the home page.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 517dfa7b-26ac-463d-a6e1-a58c6df97188
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Event-Id: c201f10b-bbd3-4402-9212-e0b79bfa670f
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/f209e72a-0939-48fa-84fc-57854de71967/517dfa7b-26ac-463d-a6e1-a58c6df97188/EtK2Sno
Replit-Helium-Checkpoint-Created: true
This commit is contained in:
sebastjanartic
2026-03-05 09:48:56 +00:00
parent 365161a381
commit 3d672766b8
12 changed files with 1258 additions and 633 deletions
+111 -17
View File
@@ -1,14 +1,24 @@
import { useQuery } from "@tanstack/react-query";
import { Link } from "wouter";
import { Link, useLocation, useSearch } from "wouter";
import { type Article } from "@shared/schema";
import { format } from "date-fns";
import { de } from "date-fns/locale";
import { usePageMeta } from "@/hooks/use-page-meta";
import { Play, ArrowLeft } from "lucide-react";
import { Play, ArrowLeft, 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 { ArticleCardAd } from "@/components/adsense";
import { useEffect } from "react";
interface PaginatedResponse {
articles: Article[];
total: number;
page: number;
totalPages: number;
hasMore: boolean;
}
function VideoCard({ article }: { article: Article }) {
const thumbSrc = article.coverImage
@@ -63,10 +73,48 @@ function VideoCardSkeleton() {
export default function VideosPage() {
usePageMeta("Volksmusik & Schlager Videos", "Musikvideos und Live-Auftritte aus der Volksmusik- und Schlagerszene bei FOLX TV.");
const { data: articles, isLoading } = useQuery<Article[]>({
queryKey: ["/api/articles/category/Video"],
const searchString = useSearch();
const [, setLocation] = useLocation();
const params = new URLSearchParams(searchString);
const currentPage = Math.max(1, parseInt(params.get("page") || "1"));
const limit = 12;
const { data, isLoading } = useQuery<PaginatedResponse>({
queryKey: [`/api/articles/category/Video?page=${currentPage}&limit=${limit}`],
});
useEffect(() => {
window.scrollTo({ top: 0, behavior: "smooth" });
}, [currentPage]);
const goToPage = (page: number) => {
if (page === 1) {
setLocation("/videos");
} else {
setLocation(`/videos?page=${page}`);
}
};
const totalPages = data?.totalPages || 1;
const articles = data?.articles || [];
const getPageNumbers = () => {
const pages: (number | "...")[] = [];
if (totalPages <= 7) {
for (let i = 1; i <= totalPages; i++) pages.push(i);
} else {
pages.push(1);
if (currentPage > 3) pages.push("...");
const start = Math.max(2, currentPage - 1);
const end = Math.min(totalPages - 1, currentPage + 1);
for (let i = start; i <= end; i++) pages.push(i);
if (currentPage < totalPages - 2) pages.push("...");
pages.push(totalPages);
}
return pages;
};
return (
<div className="min-h-screen bg-background">
<Header />
@@ -78,24 +126,70 @@ export default function VideosPage() {
</button>
</Link>
<h1 className="text-2xl font-bold text-foreground mb-6" data-testid="text-videos-title">
Videos
</h1>
{isLoading ? (
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-5">
{Array.from({ length: 4 }).map((_, i) => (
{Array.from({ length: 8 }).map((_, i) => (
<VideoCardSkeleton key={i} />
))}
</div>
) : articles && articles.length > 0 ? (
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-5">
{articles.flatMap((article, index) => {
const items = [
<VideoCard key={article.id} article={article} />,
];
if (index === 2) {
items.push(<ArticleCardAd key="ad-video-1" />);
}
return items;
})}
</div>
) : articles.length > 0 ? (
<>
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-5">
{articles.flatMap((article, index) => {
const items: JSX.Element[] = [
<VideoCard key={article.id} article={article} />,
];
if ((index + 1) % 3 === 0) {
items.push(<ArticleCardAd key={`ad-video-${index}`} />);
}
return items;
})}
</div>
{totalPages > 1 && (
<nav className="flex items-center justify-center gap-1 mt-10" data-testid="pagination-videos">
<Button
variant="outline"
size="icon"
onClick={() => goToPage(currentPage - 1)}
disabled={currentPage <= 1}
data-testid="button-prev-page"
>
<ChevronLeft className="w-4 h-4" />
</Button>
{getPageNumbers().map((p, i) =>
p === "..." ? (
<span key={`ellipsis-${i}`} className="px-2 text-muted-foreground">...</span>
) : (
<Button
key={p}
variant={p === currentPage ? "default" : "outline"}
size="sm"
onClick={() => goToPage(p as number)}
data-testid={`button-page-${p}`}
>
{p}
</Button>
)
)}
<Button
variant="outline"
size="icon"
onClick={() => goToPage(currentPage + 1)}
disabled={currentPage >= totalPages}
data-testid="button-next-page"
>
<ChevronRight className="w-4 h-4" />
</Button>
</nav>
)}
</>
) : (
<div className="text-center py-16">
<p className="text-muted-foreground">Noch keine Videos vorhanden.</p>