folx-tv/client/src/pages/home.tsx
sebastjanartic 8f709e181d Add an advertisement to the blog post grid layout
Insert an ad into the fifth row of the blog post grid, replacing an article at the second position.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 517dfa7b-26ac-463d-a6e1-a58c6df97188
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Event-Id: c5a7bcf0-0aea-463e-aa1c-d846d197312b
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/f209e72a-0939-48fa-84fc-57854de71967/517dfa7b-26ac-463d-a6e1-a58c6df97188/nFw7xof
Replit-Helium-Checkpoint-Created: true
2026-03-02 18:31:35 +00:00

633 lines
28 KiB
TypeScript

import { useQuery } from "@tanstack/react-query";
import { Link } from "wouter";
import { type Article } from "@shared/schema";
import { format } from "date-fns";
import { de } from "date-fns/locale";
import { Eye, Play, Images } from "lucide-react";
import { Skeleton } from "@/components/ui/skeleton";
import Header from "@/components/header";
import Footer from "@/components/footer";
import AdSense, { ArticleCardAd, MultiplexAd, SidebarAd } from "@/components/adsense";
import { PhotoGalleryWidget } from "@/components/photo-gallery";
import { HoroscopeWidget } from "@/components/horoscope-widget";
import { RecipeWidget } from "@/components/recipe-widget";
import { NewsWidget } from "@/components/news-widget";
import { SidebarWeatherWidget } from "@/components/weather-widget";
import { BreakingNewsWidget } from "@/components/breaking-news-widget";
import { useState, useEffect, useCallback, useRef, useMemo } from "react";
function useFocalPoints() {
const { data } = useQuery<Record<string, { x: number; y: number }>>({
queryKey: ["/api/focal-points"],
staleTime: Infinity,
});
return data || {};
}
function SmartImage({ src, alt, className = "", focalPoints }: { src: string; alt: string; className?: string; focalPoints?: Record<string, { x: number; y: number }> }) {
const imgRef = useRef<HTMLImageElement>(null);
const [isPortrait, setIsPortrait] = useState(false);
useEffect(() => {
const img = new window.Image();
img.onload = () => {
if (img.naturalHeight > img.naturalWidth * 1.2) {
setIsPortrait(true);
}
};
img.src = src;
}, [src]);
const fp = focalPoints?.[src];
const posStyle = fp
? { objectPosition: `${fp.x}% ${fp.y}%` }
: { objectPosition: "center 20%" };
if (isPortrait) {
return (
<div className="absolute inset-0 overflow-hidden">
<img src={src} alt="" className="absolute inset-0 w-full h-full object-cover scale-150 blur-2xl brightness-50" aria-hidden="true" />
<img ref={imgRef} src={src} alt={alt} className={`relative w-full h-full object-contain z-[1] ${className}`} />
</div>
);
}
return (
<img ref={imgRef} src={src} alt={alt} className={`w-full h-full object-cover absolute inset-0 ${className}`} style={posStyle} />
);
}
interface GalleryImage {
folder: string;
fileName: string;
thumb: string;
large: string;
}
function thumbUrl(src: string | null): string {
if (!src) return "/images/article-1.png";
if (src.endsWith(".webp")) return src.replace(".webp", "-thumb.webp");
return src;
}
function timeAgo(date: Date): string {
const now = new Date();
const diffMs = now.getTime() - date.getTime();
const diffH = Math.floor(diffMs / 3600000);
const diffD = Math.floor(diffMs / 86400000);
if (diffH < 1) return "Gerade eben";
if (diffH < 24) return `vor ${diffH} Std.`;
if (diffD < 7) return `vor ${diffD} T.`;
return format(date, "d. MMM yyyy", { locale: de });
}
function HeroCard({ article, focalPoints }: { article: Article; focalPoints?: Record<string, { x: number; y: number }> }) {
const isVideo = article.category === "Video";
return (
<Link href={`/article/${article.slug}`}>
<div className="relative group cursor-pointer rounded-lg overflow-hidden h-full" data-testid={`card-hero-${article.id}`}>
<div className="relative h-full min-h-[280px]">
<SmartImage src={article.coverImage || "/images/article-1.png"} alt={article.title} className="transition-transform duration-700 group-hover:scale-105" focalPoints={focalPoints} />
{isVideo && (
<div className="absolute inset-0 flex items-center justify-center z-10">
<div className="w-14 h-14 rounded-full bg-primary/90 flex items-center justify-center group-hover:bg-primary transition-colors shadow-lg">
<Play className="w-6 h-6 text-white ml-0.5" fill="white" />
</div>
</div>
)}
<div className="absolute inset-0 bg-gradient-to-t from-black/90 via-black/30 to-transparent" />
<div className="absolute bottom-0 left-0 right-0 p-5">
<div className="flex items-center gap-2 mb-2">
<span className="text-xs font-medium text-primary bg-primary/20 px-2 py-0.5 rounded">{article.category}</span>
<span className="text-white/60 text-xs">{timeAgo(new Date(article.publishedAt))}</span>
</div>
<h3 className="text-white font-bold text-lg md:text-xl leading-tight line-clamp-3">{article.title}</h3>
<p className="text-white/50 text-sm mt-1.5 line-clamp-2 max-w-lg hidden md:block">{article.excerpt}</p>
</div>
</div>
</div>
</Link>
);
}
function GalleryHeroCard({ images }: { images: GalleryImage[] }) {
const [idx, setIdx] = useState(0);
useEffect(() => {
const timer = setInterval(() => setIdx((i) => (i + 1) % images.length), 10000);
return () => clearInterval(timer);
}, [images.length]);
return (
<Link href="/gallery">
<div className="relative group cursor-pointer rounded-lg overflow-hidden h-full" data-testid="card-hero-gallery">
<div className="relative h-full min-h-[300px]">
<img src={images[idx].large || images[idx].thumb} alt={images[idx].fileName} className="w-full h-full object-cover absolute inset-0 transition-opacity duration-1000" />
<div className="absolute inset-0 bg-gradient-to-t from-black/90 via-black/30 to-transparent" />
<div className="absolute bottom-0 left-0 right-0 p-5">
<div className="flex items-center gap-2 mb-2">
<span className="text-xs font-medium text-primary bg-primary/20 px-2 py-0.5 rounded flex items-center gap-1">
<Images className="w-3 h-3" /> Fotogalerie
</span>
</div>
<h3 className="text-white font-bold text-lg md:text-xl leading-tight">Backstage & Events</h3>
<p className="text-white/50 text-sm mt-1.5 hidden md:block">{images.length} exklusive Fotos aus der Welt der Volksmusik</p>
</div>
</div>
</div>
</Link>
);
}
function getObjectPosition(coverImage: string | null, focalPoints?: Record<string, { x: number; y: number }>): string {
if (!coverImage || !focalPoints) return "center 20%";
const fp = focalPoints[coverImage];
if (!fp) return "center 20%";
return `${fp.x}% ${fp.y}%`;
}
function MediumCard({ article, focalPoints }: { article: Article; focalPoints?: Record<string, { x: number; y: number }> }) {
const isVideo = article.category === "Video";
const objPos = getObjectPosition(article.coverImage, focalPoints);
return (
<Link href={`/article/${article.slug}`}>
<div className="relative group cursor-pointer rounded-lg overflow-hidden h-full bg-card border border-card-border flex flex-col" data-testid={`card-medium-${article.id}`}>
<div className="relative flex-shrink-0">
<div className="overflow-hidden">
<img src={thumbUrl(article.coverImage)} alt={article.title} className="w-full aspect-video object-cover transition-transform duration-500 group-hover:scale-105" style={{ objectPosition: objPos }} loading="lazy" />
</div>
{isVideo && (
<div className="absolute inset-0 flex items-center justify-center">
<div className="w-10 h-10 rounded-full bg-primary/90 flex items-center justify-center group-hover:bg-primary transition-colors">
<Play className="w-4 h-4 text-white ml-0.5" fill="white" />
</div>
</div>
)}
</div>
<div className="p-3.5 flex flex-col flex-1">
<div className="flex items-center gap-2 mb-1.5">
<span className="text-[10px] font-medium text-primary">{article.author}</span>
<span className="text-muted-foreground text-[10px]">{timeAgo(new Date(article.publishedAt))}</span>
</div>
<h3 className="font-semibold text-card-foreground text-sm leading-snug line-clamp-2 group-hover:text-primary transition-colors">{article.title}</h3>
<p className="text-xs text-muted-foreground mt-1.5 leading-relaxed flex-1">{article.excerpt}</p>
<div className="flex items-center gap-2 mt-2 text-muted-foreground text-[10px]">
<Eye className="w-3 h-3" />
{article.views.toLocaleString()}
</div>
</div>
</div>
</Link>
);
}
function WideCard({ article, focalPoints }: { article: Article; focalPoints?: Record<string, { x: number; y: number }> }) {
const isVideo = article.category === "Video";
const objPos = getObjectPosition(article.coverImage, focalPoints);
return (
<Link href={`/article/${article.slug}`}>
<div className="relative group cursor-pointer rounded-lg overflow-hidden bg-card border border-card-border h-full" data-testid={`card-wide-${article.id}`}>
<div className="relative aspect-[4/3] overflow-hidden">
<img src={thumbUrl(article.coverImage)} alt={article.title} className="w-full h-full object-cover transition-transform duration-500 group-hover:scale-105" style={{ objectPosition: objPos }} loading="lazy" />
{isVideo && (
<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 className="absolute inset-0 bg-gradient-to-t from-black/80 via-black/20 to-transparent" />
<div className="absolute bottom-0 left-0 right-0 p-4">
<div className="flex items-center gap-2 mb-1.5">
<span className="text-xs font-medium text-primary">{article.author}</span>
<span className="text-white/60 text-xs">{timeAgo(new Date(article.publishedAt))}</span>
</div>
<h3 className="font-bold text-white text-base leading-snug line-clamp-2 group-hover:text-primary transition-colors">{article.title}</h3>
<div className="flex items-center gap-2 mt-2 text-white/50 text-[10px]">
<Eye className="w-3 h-3" />
{article.views.toLocaleString()}
</div>
</div>
</div>
</div>
</Link>
);
}
function BlogCard({ article, focalPoints }: { article: Article; focalPoints?: Record<string, { x: number; y: number }> }) {
const isVideo = article.category === "Video";
const objPos = getObjectPosition(article.coverImage, focalPoints);
return (
<Link href={`/article/${article.slug}`}>
<div className="relative group cursor-pointer rounded-lg overflow-hidden bg-card border border-card-border flex flex-col" data-testid={`card-blog-${article.id}`}>
<div className="relative flex-shrink-0">
<div className="overflow-hidden">
<img src={article.coverImage} alt={article.title} className="w-full aspect-[16/9] object-cover transition-transform duration-500 group-hover:scale-105" style={{ objectPosition: objPos }} loading="lazy" />
</div>
{isVideo && (
<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">
<div className="flex items-center gap-2 mb-2">
<span className="text-xs font-medium text-primary">{article.author}</span>
<span className="text-muted-foreground text-xs">{timeAgo(new Date(article.publishedAt))}</span>
</div>
<h3 className="font-bold text-card-foreground text-base leading-snug line-clamp-2 group-hover:text-primary transition-colors">{article.title}</h3>
<p className="text-sm text-muted-foreground mt-2 leading-relaxed line-clamp-3 flex-1">{article.excerpt}</p>
<div className="flex items-center gap-2 mt-3 text-muted-foreground text-xs">
<Eye className="w-3.5 h-3.5" />
{article.views.toLocaleString()}
</div>
</div>
</div>
</Link>
);
}
function SideCard({ article, focalPoints }: { article: Article; focalPoints?: Record<string, { x: number; y: number }> }) {
const isVideo = article.category === "Video";
const objPos = getObjectPosition(article.coverImage, focalPoints);
return (
<Link href={`/article/${article.slug}`}>
<div className="relative group cursor-pointer rounded-lg overflow-hidden bg-card border border-card-border h-full flex flex-col" data-testid={`card-side-${article.id}`}>
<div className="relative flex-1 min-h-0">
<div className="overflow-hidden h-full">
<img src={thumbUrl(article.coverImage)} alt={article.title} className="w-full h-full object-cover transition-transform duration-500 group-hover:scale-105" style={{ objectPosition: objPos }} loading="lazy" />
</div>
{isVideo && (
<div className="absolute inset-0 flex items-center justify-center">
<div className="w-10 h-10 rounded-full bg-primary/90 flex items-center justify-center group-hover:bg-primary transition-colors">
<Play className="w-4 h-4 text-white ml-0.5" fill="white" />
</div>
</div>
)}
</div>
<div className="p-3 flex-shrink-0">
<div className="flex items-center gap-2 mb-1">
<span className="text-xs font-medium text-primary">{article.author}</span>
<span className="text-muted-foreground text-xs">{timeAgo(new Date(article.publishedAt))}</span>
</div>
<h3 className="font-bold text-card-foreground text-sm leading-snug line-clamp-2 group-hover:text-primary transition-colors">{article.title}</h3>
<div className="flex items-center gap-1.5 mt-2 text-muted-foreground text-xs">
<Eye className="w-3 h-3" />
{article.views.toLocaleString()}
</div>
</div>
</div>
</Link>
);
}
function NativeAdCard() {
return (
<div className="relative rounded-lg overflow-hidden h-full bg-card border border-card-border" data-testid="card-native-ad">
<div className="relative">
<div className="overflow-hidden aspect-video bg-muted">
<AdSense
slot="3854634730"
format="fluid"
layoutKey="-7o+et-x-1e+6a"
style={{ display: "block" }}
className="w-full h-full min-h-[160px]"
/>
</div>
</div>
</div>
);
}
function TopStoriesList({ articles, className }: { articles: Article[]; className?: string }) {
return (
<div className={`bg-card rounded-lg border border-card-border p-4 ${className || ""}`} data-testid="sidebar-top-stories">
<h3 className="font-bold text-card-foreground text-sm mb-3 flex items-center gap-2">
<span className="w-1 h-4 bg-primary rounded-full" />
Zuletzt hinzugefügt
</h3>
<div className="space-y-0">
{articles.slice(0, 5).map((article) => (
<Link key={article.id} href={`/article/${article.slug}`}>
<div className="group cursor-pointer py-1.5 border-b border-card-border last:border-0" data-testid={`card-top-${article.id}`}>
<h4 className="text-xs font-medium text-card-foreground line-clamp-2 group-hover:text-primary transition-colors leading-snug">{article.title}</h4>
<div className="flex items-center gap-1.5 mt-0.5">
<span className="text-[10px] text-muted-foreground">{article.author}</span>
<span className="text-muted-foreground/50 text-[10px]">{timeAgo(new Date(article.publishedAt))}</span>
</div>
</div>
</Link>
))}
</div>
</div>
);
}
function FeaturedHeroCard({ article, focalPoints }: { article: Article; focalPoints?: Record<string, { x: number; y: number }> }) {
const isVideo = article.category === "Video";
const objPos = getObjectPosition(article.coverImage, focalPoints);
return (
<Link href={`/article/${article.slug}`}>
<div className="relative group cursor-pointer rounded-lg overflow-hidden bg-card border border-card-border h-full flex flex-col" data-testid={`card-featured-hero-${article.id}`}>
<div className="relative flex-1 min-h-[300px]">
<img src={article.coverImage || ""} alt={article.title} className="w-full h-full object-cover absolute inset-0 transition-transform duration-700 group-hover:scale-105" style={{ objectPosition: objPos }} loading="lazy" />
{isVideo && (
<div className="absolute inset-0 flex items-center justify-center z-10">
<div className="w-14 h-14 rounded-full bg-primary/90 flex items-center justify-center group-hover:bg-primary transition-colors shadow-lg">
<Play className="w-6 h-6 text-white ml-0.5" fill="white" />
</div>
</div>
)}
</div>
<div className="p-4 flex-shrink-0">
<div className="flex items-center gap-2 mb-1.5">
<span className="text-xs font-medium text-primary">{article.category}</span>
<span className="text-muted-foreground text-xs">{timeAgo(new Date(article.publishedAt))}</span>
</div>
<h3 className="font-bold text-card-foreground text-lg leading-tight line-clamp-3 group-hover:text-primary transition-colors">{article.title}</h3>
<p className="text-sm text-muted-foreground mt-2 leading-relaxed line-clamp-3">{article.excerpt}</p>
<div className="flex items-center gap-2 mt-3 text-muted-foreground text-xs">
<Eye className="w-3.5 h-3.5" />
{article.views.toLocaleString()}
</div>
</div>
</div>
</Link>
);
}
function FeaturedCarousel({ articles, popular, galleryImages, focalPoints }: { articles: Article[]; popular?: Article[]; galleryImages?: GalleryImage[]; focalPoints?: Record<string, { x: number; y: number }> }) {
const pageSize = 3;
const totalPages = Math.max(1, Math.ceil(Math.min(articles.length, 9) / pageSize));
const [page, setPage] = useState(0);
const [paused, setPaused] = useState(false);
const next = useCallback(() => {
setPage((p) => (p + 1) % totalPages);
}, [totalPages]);
useEffect(() => {
if (paused || totalPages <= 1) return;
const timer = setInterval(next, 8000);
return () => clearInterval(timer);
}, [paused, next, totalPages]);
const start = page * pageSize;
const hero = articles[start];
const sideCards = articles.slice(start + 1, start + 3);
return (
<section data-testid="featured-carousel" onMouseEnter={() => setPaused(true)} onMouseLeave={() => setPaused(false)}>
<div className="grid grid-cols-1 lg:grid-cols-5 gap-4">
<div className="lg:col-span-3">
{hero && <FeaturedHeroCard article={hero} focalPoints={focalPoints} />}
</div>
<div className="lg:col-span-2 grid grid-rows-2 gap-4">
{sideCards.map((a) => (
<SideCard key={a.id} article={a} focalPoints={focalPoints} />
))}
</div>
</div>
{totalPages > 1 && (
<div className="flex justify-center gap-2 mt-3" data-testid="carousel-dots">
{Array.from({ length: totalPages }).map((_, i) => (
<button key={i} onClick={() => setPage(i)} className={`w-2 h-2 rounded-full transition-all duration-300 ${i === page ? "bg-primary w-5" : "bg-muted-foreground/30 hover:bg-muted-foreground/50"}`} data-testid={`button-carousel-dot-${i}`} />
))}
</div>
)}
</section>
);
}
function BentoSkeleton() {
return (
<div className="space-y-4">
<div className="grid grid-cols-6 gap-4">
<div className="col-span-6 lg:col-span-3"><Skeleton className="w-full h-[380px] rounded-lg" /></div>
<div className="col-span-6 lg:col-span-3 grid grid-cols-2 gap-4">
<Skeleton className="w-full h-[180px] rounded-lg" />
<Skeleton className="w-full h-[180px] rounded-lg" />
<Skeleton className="w-full h-[180px] rounded-lg" />
<Skeleton className="w-full h-[180px] rounded-lg" />
</div>
</div>
</div>
);
}
export default function Home() {
const { data: articles, isLoading } = useQuery<Article[]>({
queryKey: ["/api/articles"],
});
const { data: popular } = useQuery<Article[]>({
queryKey: ["/api/articles/popular"],
});
const { data: galleryImages } = useQuery<GalleryImage[]>({
queryKey: ["/api/gallery"],
});
const focalPoints = useFocalPoints();
const shuffled = useMemo(() => {
if (!articles) return [];
const arr = [...articles];
for (let i = arr.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[arr[i], arr[j]] = [arr[j], arr[i]];
}
return arr;
}, [articles]);
const widgets = useMemo(() => [
{ id: "horoscope", el: <HoroscopeWidget key="horoscope" /> },
{ id: "news", el: <div key="news" className="flex flex-col gap-4"><NewsWidget /></div> },
{ id: "gallery", el: <PhotoGalleryWidget key="gallery" /> },
{ id: "recipe", el: <RecipeWidget key="recipe" /> },
{ id: "breaking", el: <div key="breaking" className="flex flex-col gap-4"><BreakingNewsWidget /></div> },
{ id: "gallery2", el: <PhotoGalleryWidget key="gallery2" reverseOrder={true} /> },
], []);
const gridItems = useMemo(() => {
const items: { type: "article" | "widget" | "ad"; key: string; article?: Article; widget?: typeof widgets[0] }[] = [];
let ai = 0;
let wi = 0;
const widgetRows = Math.ceil(widgets.length / 2);
for (let r = 0; r < widgetRows; r++) {
if (wi < widgets.length) items.push({ type: "widget", key: `w-${widgets[wi].id}`, widget: widgets[wi++] });
if (wi < widgets.length) items.push({ type: "widget", key: `w-${widgets[wi].id}`, widget: widgets[wi++] });
if (ai < shuffled.length) items.push({ type: "article", key: `a-${shuffled[ai].id}`, article: shuffled[ai++] });
if (ai < shuffled.length) items.push({ type: "article", key: `a-${shuffled[ai].id}`, article: shuffled[ai++] });
}
while (ai < shuffled.length) {
items.push({ type: "article", key: `a-${shuffled[ai].id}`, article: shuffled[ai++] });
}
const remainder = items.length % 4;
if (remainder > 0 && shuffled.length > 0) {
let fill = 0;
while (items.length % 4 !== 0) {
const art = shuffled[fill % shuffled.length];
items.push({ type: "article", key: `fill-${fill}`, article: art });
fill++;
}
}
const totalRows = items.length / 4;
const adRows = [1, 3, 5, 7];
let adCount = 0;
for (const row of adRows) {
if (row >= totalRows) continue;
const rowStart = row * 4;
for (let col = 3; col >= 0; col--) {
if (items[rowStart + col] && items[rowStart + col].type === "article") {
items[rowStart + col] = { type: "ad", key: `ad-${adCount++}` };
break;
}
}
}
if (4 < totalRows) {
const rowStart = 4 * 4;
if (items[rowStart + 1] && items[rowStart + 1].type === "article") {
items[rowStart + 1] = { type: "ad", key: `ad-${adCount++}` };
}
}
return items;
}, [shuffled, widgets]);
const gridRows: (typeof gridItems)[] = useMemo(() => {
const rows: (typeof gridItems)[] = [];
for (let i = 0; i < gridItems.length; i += 4) {
rows.push(gridItems.slice(i, i + 4));
}
return rows;
}, [gridItems]);
const widePickedArticles = useMemo(() => {
if (!articles || articles.length < 3) return [];
const carouselIds = new Set(articles.slice(0, 9).map((a) => a.id));
const candidates = articles.filter((a) => !carouselIds.has(a.id));
const pool = candidates.length >= 2 ? candidates : articles;
const copy = [...pool];
for (let i = copy.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[copy[i], copy[j]] = [copy[j], copy[i]];
}
return copy.slice(0, 2);
}, [articles]);
const bottomArticles = useMemo(() => {
if (!articles || articles.length < 10) return [];
const usedIds = new Set([
...articles.slice(0, 9).map((a) => a.id),
...widePickedArticles.map((a) => a.id),
]);
return articles.filter((a) => !usedIds.has(a.id)).slice(0, 3);
}, [articles, widePickedArticles]);
const bottomSection = useMemo(() => {
const items: { type: "widget" | "ad" | "article"; el: JSX.Element }[] = [
{ type: "widget", el: <NewsWidget key="bottom-news" /> },
{ type: "widget", el: <HoroscopeWidget key="bottom-horoscope" /> },
{ type: "widget", el: <BreakingNewsWidget key="bottom-breaking" /> },
{ type: "widget", el: <RecipeWidget key="bottom-recipe" /> },
...bottomArticles.map((a) => ({
type: "article" as const,
el: <BlogCard key={`bottom-art-${a.id}`} article={a} focalPoints={focalPoints} />,
})),
{ type: "ad", el: <div key="ad-bottom-1"><ArticleCardAd /></div> },
{ type: "widget", el: <PhotoGalleryWidget key="bottom-gallery" /> },
];
return items;
}, [bottomArticles, focalPoints]);
if (isLoading || !articles) {
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-4"><BentoSkeleton /></main>
<Footer />
</div>
);
}
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-4 space-y-4">
<div className="grid grid-cols-1 lg:grid-cols-4 gap-4">
<div className="lg:col-span-3 space-y-4">
<FeaturedCarousel articles={articles} popular={popular} galleryImages={galleryImages} focalPoints={focalPoints} />
</div>
<div className="lg:col-span-1 space-y-4">
<SidebarWeatherWidget />
{articles && articles.length > 0 && <TopStoriesList articles={[...articles].sort((a, b) => new Date(b.publishedAt).getTime() - new Date(a.publishedAt).getTime()).slice(0, 5)} />}
<SidebarAd />
</div>
</div>
{widePickedArticles.length > 0 && (
<div className="grid grid-cols-1 lg:grid-cols-4 gap-4">
<div className="hidden lg:block rounded-lg overflow-hidden bg-card border border-card-border">
<AdSense
slot="3854634730"
format="fluid"
layoutKey="-6r+cy-10+8a-3"
style={{ display: "block" }}
className="w-full h-full min-h-[250px]"
/>
</div>
<div className="lg:col-span-3">
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
{widePickedArticles.map((a) => (
<WideCard key={`wide-top-${a.id}`} article={a} focalPoints={focalPoints} />
))}
</div>
</div>
</div>
)}
{gridRows.map((row, ri) => (
<div key={`row-${ri}`} className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4">
{row.map((item) =>
item.type === "widget"
? <div key={item.key}>{item.widget!.el}</div>
: item.type === "ad"
? <div key={item.key} className="h-full" data-testid={`ad-grid-${item.key}`}><ArticleCardAd /></div>
: item.article
? <MediumCard key={item.key} article={item.article} focalPoints={focalPoints} />
: null
)}
{ri === gridRows.length - 1 && widePickedArticles.length > 0 && (
<div className="sm:col-span-2 lg:col-span-4 grid grid-cols-1 sm:grid-cols-2 gap-4">
<WideCard article={widePickedArticles[0]} focalPoints={focalPoints} />
{widePickedArticles[1] && <WideCard article={widePickedArticles[1]} focalPoints={focalPoints} />}
</div>
)}
</div>
))}
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
{bottomSection.map((item, i) => (
<div key={`bottom-${i}`}>
{item.el}
</div>
))}
</div>
<MultiplexAd />
</main>
<Footer />
</div>
);
}