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 { usePageMeta } from "@/hooks/use-page-meta"; 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, 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>({ queryKey: ["/api/focal-points"], staleTime: Infinity, }); return data || {}; } function SmartImage({ src, alt, className = "", focalPoints }: { src: string; alt: string; className?: string; focalPoints?: Record }) { const imgRef = useRef(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 (
{alt}
); } return ( {alt} ); } 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 }) { const isVideo = article.category === "Video"; return (
{isVideo && (
)}
{article.category} {timeAgo(new Date(article.publishedAt))}

{article.title}

{article.excerpt}

); } 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 (
{images[idx].fileName}
Fotogalerie

Backstage & Events

{images.length} exklusive Fotos aus der Welt der Volksmusik

); } function getObjectPosition(coverImage: string | null, focalPoints?: Record): 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 }) { const isVideo = article.category === "Video"; const objPos = getObjectPosition(article.coverImage, focalPoints); return (
{article.title}
{isVideo && (
)}
{article.author} {timeAgo(new Date(article.publishedAt))}

{article.title}

{article.excerpt}

{article.views.toLocaleString()}
); } function WideCard({ article, focalPoints }: { article: Article; focalPoints?: Record }) { const isVideo = article.category === "Video"; const objPos = getObjectPosition(article.coverImage, focalPoints); return (
{article.title} {isVideo && (
)}
{article.author} {timeAgo(new Date(article.publishedAt))}

{article.title}

{article.views.toLocaleString()}
); } function WideCardClassic({ article, focalPoints }: { article: Article; focalPoints?: Record }) { const isVideo = article.category === "Video"; const objPos = getObjectPosition(article.coverImage, focalPoints); return (
{article.title}
{isVideo && (
)}
{article.author} {timeAgo(new Date(article.publishedAt))}

{article.title}

{article.excerpt}

{article.views.toLocaleString()}
); } function BlogCard({ article, focalPoints }: { article: Article; focalPoints?: Record }) { const isVideo = article.category === "Video"; const objPos = getObjectPosition(article.coverImage, focalPoints); return (
{article.title}
{isVideo && (
)}
{article.author} {timeAgo(new Date(article.publishedAt))}

{article.title}

{article.excerpt}

{article.views.toLocaleString()}
); } function SideCard({ article, focalPoints }: { article: Article; focalPoints?: Record }) { const isVideo = article.category === "Video"; const objPos = getObjectPosition(article.coverImage, focalPoints); return (
{article.title}
{isVideo && (
)}
{article.author} {timeAgo(new Date(article.publishedAt))}

{article.title}

{article.views.toLocaleString()}
); } function NativeAdCard() { return (
); } function TopStoriesList({ articles, className }: { articles: Article[]; className?: string }) { return (

Zuletzt hinzugefügt

{articles.slice(0, 5).map((article) => (

{article.title}

{article.author} {timeAgo(new Date(article.publishedAt))}
))}
); } function FeaturedHeroCard({ article, focalPoints }: { article: Article; focalPoints?: Record }) { const isVideo = article.category === "Video"; const objPos = getObjectPosition(article.coverImage, focalPoints); return (
{article.title} {isVideo && (
)}
{article.category} {timeAgo(new Date(article.publishedAt))}

{article.title}

{article.excerpt}

{article.views.toLocaleString()}
); } function FeaturedCarousel({ articles, popular, galleryImages, focalPoints }: { articles: Article[]; popular?: Article[]; galleryImages?: GalleryImage[]; focalPoints?: Record }) { const pageSize = 3; const totalPages = Math.max(1, Math.ceil(Math.min(articles.length, 9) / pageSize)); const [page, setPage] = useState(0); const [displayPage, setDisplayPage] = useState(0); const [fading, setFading] = useState(false); const [paused, setPaused] = useState(false); const changePage = useCallback((newPage: number) => { if (newPage === displayPage) return; setFading(true); setTimeout(() => { setDisplayPage(newPage); setFading(false); }, 300); }, [displayPage]); const next = useCallback(() => { setPage((p) => { const np = (p + 1) % totalPages; return np; }); }, [totalPages]); useEffect(() => { changePage(page); }, [page]); useEffect(() => { if (paused || totalPages <= 1) return; const timer = setInterval(next, 8000); return () => clearInterval(timer); }, [paused, next, totalPages]); const start = displayPage * pageSize; const hero = articles[start]; const sideCards = articles.slice(start + 1, start + 3); const nextPageStart = ((displayPage + 1) % totalPages) * pageSize; const nextPageArticles = articles.slice(nextPageStart, nextPageStart + pageSize); return (
setPaused(true)} onMouseLeave={() => setPaused(false)}> {nextPageArticles.length > 0 && ( )}
{hero && }
{sideCards.map((a) => ( ))}
{totalPages > 1 && (
{Array.from({ length: totalPages }).map((_, i) => (
)}
); } function BentoSkeleton() { return (
); } export default function Home() { usePageMeta("Volksmusik & Schlager News", "FOLX TV – Aktuelle Nachrichten, Musikvideos und Interviews aus der Welt der Volksmusik und des Schlagers."); const { data: articles, isLoading } = useQuery({ queryKey: ["/api/articles"], }); const { data: popular } = useQuery({ queryKey: ["/api/articles/popular"], }); const { data: galleryImages } = useQuery({ 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: }, { id: "news", el:
}, { id: "gallery", el: }, { id: "recipe", el: }, { id: "breaking", el:
}, { id: "gallery2", el: }, { id: "horoscope2", el: }, { id: "news2", el:
}, { id: "recipe2", el: }, { id: "breaking2", el:
}, ], []); 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, 9, 11, 13]; 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 extraBottomArticles = useMemo(() => { if (!articles || articles.length < 15) return []; const usedIds = new Set([ ...articles.slice(0, 9).map((a) => a.id), ...widePickedArticles.map((a) => a.id), ...bottomArticles.map((a) => a.id), ]); return articles.filter((a) => !usedIds.has(a.id)).slice(0, 6); }, [articles, widePickedArticles, bottomArticles]); const bottomSection = useMemo(() => { const items: { type: "widget" | "ad" | "article"; el: JSX.Element }[] = [ { type: "widget", el: }, { type: "widget", el: }, { type: "widget", el: }, { type: "widget", el: }, ...bottomArticles.map((a) => ({ type: "article" as const, el: , })), { type: "ad", el:
}, { type: "widget", el: }, { type: "ad", el:
}, ...extraBottomArticles.slice(0, 3).map((a) => ({ type: "article" as const, el: , })), ]; return items; }, [bottomArticles, extraBottomArticles, focalPoints]); if (isLoading || !articles) { return (
); } return (
{articles && articles.length > 0 && new Date(b.publishedAt).getTime() - new Date(a.publishedAt).getTime()).slice(0, 5)} />}
{widePickedArticles.length > 0 && (
{widePickedArticles.map((a) => ( ))}
)} {gridRows.map((row, ri) => (
{row.map((item) => item.type === "widget" ?
{item.widget!.el}
: item.type === "ad" ?
: item.article ? : null )} {ri === gridRows.length - 1 && widePickedArticles.length > 0 && (
{widePickedArticles[1] && }
)}
{ri % 3 === 2 && ri < gridRows.length - 1 && }
))}
{bottomSection.map((item, i) => (
{item.el}
))}
{extraBottomArticles.length > 3 && (
{extraBottomArticles.slice(3, 6).map((a) => ( ))}
)}
); }