Introduce `PageSideAds` component and integrate it across various pages including Home, Article, Category, Videos, Gallery, Horoscope, Recipes, Search, About, Impressum, Datenschutz, and Empfang. This component displays vertical ads on screens wider than 2xl. Additionally, the Home page's existing ad implementation is refactored to use this new component. Replit-Commit-Author: Agent Replit-Commit-Session-Id: 1f7e7e89-a520-4970-9645-37daadc466dc Replit-Commit-Checkpoint-Type: full_checkpoint Replit-Commit-Event-Id: 6355ba60-5043-4119-a09f-5437b272e829 Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/f209e72a-0939-48fa-84fc-57854de71967/1f7e7e89-a520-4970-9645-37daadc466dc/zWnrdl3 Replit-Helium-Checkpoint-Created: true
204 lines
7.0 KiB
TypeScript
204 lines
7.0 KiB
TypeScript
import { useQuery } from "@tanstack/react-query";
|
|
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, 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, PageSideAds } 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
|
|
? article.coverImage.replace(".webp", "-thumb.webp")
|
|
: "/images/article-1.jpg";
|
|
|
|
return (
|
|
<Link href={`/article/${article.slug}`}>
|
|
<article
|
|
className="group cursor-pointer bg-card rounded-md border border-card-border transition-all duration-300 h-full flex flex-col"
|
|
data-testid={`card-video-${article.id}`}
|
|
>
|
|
<div className="relative rounded-t-md">
|
|
<div className="overflow-hidden rounded-t-md">
|
|
<img
|
|
src={thumbSrc}
|
|
alt={article.title}
|
|
className="w-full aspect-video object-cover transition-transform duration-500 group-hover:scale-105"
|
|
loading="lazy"
|
|
/>
|
|
</div>
|
|
<div className="absolute inset-0 flex items-center justify-center">
|
|
<div className="w-12 h-12 rounded-full bg-primary/90 flex items-center justify-center group-hover:bg-primary transition-colors">
|
|
<Play className="w-5 h-5 text-white ml-0.5" fill="white" />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div className="p-4 flex flex-col flex-1">
|
|
<h3 className="font-semibold text-card-foreground mb-1 line-clamp-2 group-hover:text-primary transition-colors text-sm leading-snug">
|
|
{article.title}
|
|
</h3>
|
|
<p className="text-xs text-muted-foreground mt-auto">
|
|
{format(new Date(article.publishedAt), "d. MMMM yyyy", { locale: de })}
|
|
</p>
|
|
</div>
|
|
</article>
|
|
</Link>
|
|
);
|
|
}
|
|
|
|
function VideoCardSkeleton() {
|
|
return (
|
|
<div className="bg-card rounded-md border border-card-border">
|
|
<Skeleton className="w-full aspect-video rounded-t-md" />
|
|
<div className="p-4 space-y-2">
|
|
<Skeleton className="h-4 w-full" />
|
|
<Skeleton className="h-3 w-1/2" />
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
export default function VideosPage() {
|
|
usePageMeta("Volksmusik & Schlager Videos", "Musikvideos und Live-Auftritte aus der Volksmusik- und Schlagerszene bei FOLX TV.");
|
|
|
|
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 />
|
|
<PageSideAds />
|
|
<main className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
|
<Link href="/">
|
|
<button className="flex items-center gap-2 text-muted-foreground hover:text-foreground transition-colors mb-6 text-sm" 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-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: 8 }).map((_, i) => (
|
|
<VideoCardSkeleton key={i} />
|
|
))}
|
|
</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>
|
|
</div>
|
|
)}
|
|
</main>
|
|
<Footer />
|
|
</div>
|
|
);
|
|
}
|