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, PageSideAds } 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 PushPromptBanner from "@/components/push-prompt-banner"; import { useState, useEffect, useCallback, useRef, useMemo, lazy, Suspense } 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.jpg"; 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 MosaicTile({ article, focalPoints, tall = false }: { article: Article; focalPoints?: Record; tall?: boolean }) { const objPos = getObjectPosition(article.coverImage, focalPoints); return (
{article.title}
{article.category || "News"}

{article.title}

); } function MosaicHeading({ children }: { children: React.ReactNode }) { return (

{children}

); } function MobileMosaic({ latest, popular, focalPoints }: { latest: Article[]; popular?: Article[]; focalPoints?: Record }) { const pageSize = 3; const pool = (latest || []).slice(0, 9); const totalPages = Math.max(1, Math.ceil(pool.length / 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]); useEffect(() => { changePage(page); }, [page]); useEffect(() => { if (paused || totalPages <= 1) return; const timer = setInterval(() => setPage((p) => (p + 1) % totalPages), 8000); return () => clearInterval(timer); }, [paused, totalPages]); const touchX = useRef(null); const resumeTimer = useRef(null); const goTo = useCallback((dir: number) => { setPage((p) => (p + dir + totalPages) % totalPages); }, [totalPages]); const onTouchStart = useCallback((e: React.TouchEvent) => { touchX.current = e.touches[0].clientX; setPaused(true); if (resumeTimer.current) clearTimeout(resumeTimer.current); }, []); const onTouchEnd = useCallback((e: React.TouchEvent) => { const x0 = touchX.current; touchX.current = null; if (x0 !== null) { const dx = e.changedTouches[0].clientX - x0; if (Math.abs(dx) > 40) goTo(dx < 0 ? 1 : -1); } if (resumeTimer.current) clearTimeout(resumeTimer.current); resumeTimer.current = setTimeout(() => setPaused(false), 6000); }, [goTo]); useEffect(() => () => { if (resumeTimer.current) clearTimeout(resumeTimer.current); }, []); const l = pool.slice(displayPage * pageSize, displayPage * pageSize + pageSize); if (l.length === 0) return null; const usedIds = new Set(pool.map((a) => a.id)); const p = (popular || []).filter((a) => !usedIds.has(a.id)).slice(0, 3); const nextStart = ((displayPage + 1) % totalPages) * pageSize; const preload = pool.slice(nextStart, nextStart + pageSize); return (
Neueste Artikel {preload.length > 0 && ( )}
{l.slice(0, 2).map((a) => ( ))}
{l[2] && (
)}
{totalPages > 1 && (
{Array.from({ length: totalPages }).map((_, i) => (
)}
{p.length > 0 && (
Beliebt {p.length > 1 && (
{p.slice(1, 3).map((a) => ( ))}
)}
)}
); } 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 LazySection({ children, minHeight = "200px" }: { children: JSX.Element; minHeight?: string }) { const ref = useRef(null); const [visible, setVisible] = useState(false); useEffect(() => { const el = ref.current; if (!el) return; const obs = new IntersectionObserver( ([entry]) => { if (entry.isIntersecting) { setVisible(true); obs.disconnect(); } }, { rootMargin: "300px" } ); obs.observe(el); return () => obs.disconnect(); }, []); return (
{visible ? children : null}
); } function BentoSkeleton() { return (
); } export default function Home() { usePageMeta("Volksmusik & Schlager TV", "FOLX TV – Ihr Fernsehsender Nr. 1 für Volksmusik und Schlager. Aktuelle Nachrichten, Musikvideos, Künstlerporträts, Live-Shows und Interviews aus der Welt der volkstümlichen Musik."); useEffect(() => { const jsonLd = { "@context": "https://schema.org", "@type": "WebSite", "name": "Folx Music Television", "alternateName": ["FOLX TV", "Volksmusik TV"], "url": "https://folx.tv", "description": "FOLX TV – Ihr Fernsehsender Nr. 1 für Volksmusik und Schlager. Aktuelle Nachrichten, Musikvideos, Künstlerporträts, Live-Shows und Interviews aus der Welt der volkstümlichen Musik.", "publisher": { "@type": "Organization", "name": "Folx Music Television", "url": "https://folx.tv", "logo": { "@type": "ImageObject", "url": `${window.location.origin}/favicon.png` }, "sameAs": [ "https://www.facebook.com/folxtvmusic", "https://www.instagram.com/folxtv", "https://www.youtube.com/@FolxTV", "https://www.tiktok.com/@folx.tv" ] }, "potentialAction": { "@type": "SearchAction", "target": "https://folx.tv/search?q={search_term_string}", "query-input": "required name=search_term_string" }, "inLanguage": "de" }; const script = document.createElement("script"); script.type = "application/ld+json"; script.id = "home-jsonld"; script.textContent = JSON.stringify(jsonLd); const existing = document.getElementById("home-jsonld"); if (existing) existing.remove(); document.head.appendChild(script); return () => { const el = document.getElementById("home-jsonld"); if (el) el.remove(); }; }, []); const { data: articles, isLoading } = useQuery({ queryKey: ["/api/articles"], }); const { data: popular } = useQuery({ queryKey: ["/api/articles/popular"], }); const { data: galleryImages } = useQuery({ queryKey: ["/api/gallery", "home"], queryFn: () => fetch("/api/gallery?limit=20").then((r) => r.json()), }); 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++; } } let adCount = 0; const totalRows = items.length / 4; for (let r = 0; r < totalRows; r++) { const rowStart = r * 4; const hasWidget = items.slice(rowStart, rowStart + 4).some((it) => it.type === "widget"); if (!hasWidget) { const pos = adCount % 2 === 0 ? 0 : 3; items[rowStart + pos] = { type: "ad", key: `ad-row-${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 === 0) return []; const carouselIds = new Set(articles.slice(0, 9).map((a) => a.id)); const nonCarousel = articles.filter((a) => !carouselIds.has(a.id)); const pool = nonCarousel.length > 0 ? nonCarousel : articles; const result: Article[] = []; let idx = 0; while (result.length < 9) { result.push(pool[idx % pool.length]); idx++; } return result; }, [articles]); const bottomSection = useMemo(() => { const items: { type: "widget" | "ad" | "article"; el: JSX.Element }[] = [ { type: "widget", el: }, { type: "widget", el: }, { type: "widget", el: }, { type: "article", el: }, { type: "ad", el:
}, { type: "article", el: }, { type: "widget", el: }, { type: "article", el: }, { type: "article", el: }, { type: "article", el: }, { type: "ad", el:
}, { type: "article", el: }, ]; return items; }, [bottomArticles, focalPoints]); if (isLoading || !articles) { return (
); } return (

FOLX TV – Volksmusik & Schlager Fernsehsender: Nachrichten, Videos und Live-Shows

a.featured)} popular={popular} galleryImages={galleryImages} focalPoints={focalPoints} />
new Date(b.publishedAt).getTime() - new Date(a.publishedAt).getTime())} popular={popular} focalPoints={focalPoints} />
{articles && articles.length > 0 && new Date(b.publishedAt).getTime() - new Date(a.publishedAt).getTime()).slice(0, 5)} />}
{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] && }
)}
))}
{bottomSection.map((item, i) => (
{item.el}
))}
); }