Restored to '3905f52571b4fa7e048e16d7a3b94ee66307d072'
Replit-Restored-To: 3905f52571
This commit is contained in:
@@ -0,0 +1,52 @@
|
||||
import { Tv } from "lucide-react";
|
||||
import Header from "@/components/header";
|
||||
import Footer from "@/components/footer";
|
||||
import { usePageMeta } from "@/hooks/use-page-meta";
|
||||
|
||||
export default function AboutPage() {
|
||||
usePageMeta("Über FOLX TV - Volksmusik & Schlager Fernsehsender", "Alles über FOLX TV – Ihren Fernsehsender für Volksmusik und Schlager seit 2013.");
|
||||
return (
|
||||
<div className="min-h-screen bg-background">
|
||||
<Header />
|
||||
<main className="max-w-3xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||||
<div className="flex items-center gap-3 mb-8">
|
||||
<Tv className="w-7 h-7 text-primary" />
|
||||
<h1 className="text-3xl font-bold text-foreground" data-testid="text-about-title">
|
||||
Über FOLX TV
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
<article className="prose-custom space-y-5 text-[15px] leading-relaxed text-muted-foreground" data-testid="section-about-content">
|
||||
<p>
|
||||
<span className="text-foreground font-semibold">FOLX TV</span> ist Ihr Fernsehsender für volkstümliche Musik und Schlager. Der Sender präsentiert exklusiven Content aus der Welt der volkstümlichen Musik und des Schlagers. Über 90 % des Programms werden von FOLX NETWORK selbst produziert und bieten den Zuschauern eine abwechslungsreiche Auswahl an Musikvideos, Live-Shows und Unterhaltung mit den beliebtesten Stars der Szene.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
FOLX TV ist heute über IPTV-Plattformen und Kabelnetze in Deutschland, Österreich und der Schweiz empfangbar, unter anderem über MagentaTV, Zattoo, O2 TV, A1 Xplore TV, Swisscom blue TV sowie über verschiedene regionale Kabelnetze. Zusätzlich kann FOLX TV über den offiziellen Livestream im Internet verfolgt werden.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Als Teil des FOLX NETWORK steht der Sender für hochwertige Musikunterhaltung, authentische Volksmusik und modernen Schlager – produziert mit Leidenschaft und großer Nähe zur Musikszene.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Zu den beliebtesten Eigenproduktionen zählt der <span className="text-foreground font-medium">FOLX Stadl</span> mit Moderator Hanzi Berger, der regelmäßig Stars der volkstümlichen Musik und des Schlagers auf die Bühne bringt. Ein weiteres Highlight ist der <span className="text-foreground font-medium">Gipfelstammtisch</span>, der spannende Gespräche und musikalische Beiträge aus der Welt der Alpen präsentiert. In der Sendung <span className="text-foreground font-medium">Die Geschichte des Liedes</span> erhalten Zuschauer zudem faszinierende Einblicke in die Entstehung und Bedeutung bekannter volkstümlicher Melodien.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
FOLX TV wurde am 15. April 2013 gegründet und hat sich seitdem zu einem der wichtigsten Fernsehsender für volkstümliche Musik und Schlager im deutschsprachigen Raum entwickelt.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Unsere Vision ist es, die musikalische Tradition des Alpenraums und der Schlagerwelt in moderner TV-Produktion weiterzuführen und ein internationales Publikum für diese einzigartige Musik zu begeistern.
|
||||
</p>
|
||||
|
||||
<p className="text-primary font-semibold">
|
||||
Schalten Sie ein und erleben Sie die Welt der Volksmusik und des Schlagers – nur bei FOLX TV.
|
||||
</p>
|
||||
</article>
|
||||
</main>
|
||||
<Footer />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import { type Article } from "@shared/schema";
|
||||
import { format } from "date-fns";
|
||||
import { de } from "date-fns/locale";
|
||||
import { ArrowLeft, Eye, Calendar, User, Clock } from "lucide-react";
|
||||
import { usePageMeta } from "@/hooks/use-page-meta";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
import Header from "@/components/header";
|
||||
@@ -100,6 +101,11 @@ export default function ArticlePage() {
|
||||
queryKey: ["/api/articles", slug],
|
||||
});
|
||||
|
||||
usePageMeta(
|
||||
article ? `${article.title} - Volksmusik & Schlager` : "Volksmusik & Schlager Artikel",
|
||||
article?.excerpt || "Aktuelle Nachrichten aus der Volksmusik- und Schlagerszene bei FOLX TV."
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
window.scrollTo(0, 0);
|
||||
}, [slug]);
|
||||
|
||||
+160
-56
@@ -1,22 +1,115 @@
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { useParams, Link } from "wouter";
|
||||
import { useParams, Link, useLocation, useSearch } from "wouter";
|
||||
import { type Article } from "@shared/schema";
|
||||
import { format } from "date-fns";
|
||||
import { de } from "date-fns/locale";
|
||||
import { Eye, ArrowLeft } from "lucide-react";
|
||||
import { usePageMeta } from "@/hooks/use-page-meta";
|
||||
import { Eye, 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;
|
||||
}
|
||||
|
||||
export default function CategoryPage() {
|
||||
const { category } = useParams<{ category: string }>();
|
||||
const searchString = useSearch();
|
||||
const [, setLocation] = useLocation();
|
||||
|
||||
const { data: articles, isLoading } = useQuery<Article[]>({
|
||||
queryKey: ["/api/articles/category", category],
|
||||
const searchParams = new URLSearchParams(searchString);
|
||||
const currentPage = Math.max(1, parseInt(searchParams.get("page") || "1"));
|
||||
|
||||
usePageMeta(
|
||||
`${category}${currentPage > 1 ? ` – Seite ${currentPage}` : ""} - Volksmusik & Schlager`,
|
||||
`Aktuelle ${category}-Beiträge aus der Volksmusik- und Schlagerszene bei FOLX TV.`
|
||||
);
|
||||
|
||||
const { data, isLoading } = useQuery<PaginatedResponse>({
|
||||
queryKey: ["/api/articles/category", category, { page: currentPage }],
|
||||
queryFn: async () => {
|
||||
const res = await fetch(`/api/articles/category/${category}?page=${currentPage}&limit=12`);
|
||||
if (!res.ok) throw new Error("Failed to fetch articles");
|
||||
return res.json();
|
||||
},
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
window.scrollTo({ top: 0, behavior: "smooth" });
|
||||
}, [currentPage]);
|
||||
|
||||
const goToPage = (page: number) => {
|
||||
if (page === 1) {
|
||||
setLocation(`/category/${category}`);
|
||||
} else {
|
||||
setLocation(`/category/${category}?page=${page}`);
|
||||
}
|
||||
};
|
||||
|
||||
const renderPagination = () => {
|
||||
if (!data || data.totalPages <= 1) return null;
|
||||
|
||||
const pages: (number | string)[] = [];
|
||||
const total = data.totalPages;
|
||||
const current = data.page;
|
||||
|
||||
pages.push(1);
|
||||
if (current > 3) pages.push("...");
|
||||
for (let i = Math.max(2, current - 1); i <= Math.min(total - 1, current + 1); i++) {
|
||||
pages.push(i);
|
||||
}
|
||||
if (current < total - 2) pages.push("...");
|
||||
if (total > 1) pages.push(total);
|
||||
|
||||
return (
|
||||
<nav className="flex items-center justify-center gap-1 mt-10" data-testid="pagination-nav">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon"
|
||||
onClick={() => goToPage(current - 1)}
|
||||
disabled={current <= 1}
|
||||
data-testid="button-prev-page"
|
||||
>
|
||||
<ChevronLeft className="w-4 h-4" />
|
||||
</Button>
|
||||
{pages.map((p, i) =>
|
||||
typeof p === "string" ? (
|
||||
<span key={`ellipsis-${i}`} className="px-2 text-muted-foreground">
|
||||
{p}
|
||||
</span>
|
||||
) : (
|
||||
<Button
|
||||
key={p}
|
||||
variant={p === current ? "default" : "outline"}
|
||||
size="sm"
|
||||
onClick={() => goToPage(p)}
|
||||
data-testid={`button-page-${p}`}
|
||||
>
|
||||
{p}
|
||||
</Button>
|
||||
)
|
||||
)}
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon"
|
||||
onClick={() => goToPage(current + 1)}
|
||||
disabled={!data.hasMore}
|
||||
data-testid="button-next-page"
|
||||
>
|
||||
<ChevronRight className="w-4 h-4" />
|
||||
</Button>
|
||||
</nav>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-background">
|
||||
<Header />
|
||||
@@ -28,9 +121,17 @@ export default function CategoryPage() {
|
||||
</Button>
|
||||
</Link>
|
||||
|
||||
<h1 className="text-2xl font-bold text-foreground mb-6" data-testid="text-category-title">
|
||||
{category}
|
||||
</h1>
|
||||
<div className="flex items-center justify-between gap-2 mb-6 flex-wrap">
|
||||
<h1 className="text-2xl font-bold text-foreground" data-testid="text-category-title">
|
||||
{category}
|
||||
</h1>
|
||||
{data && data.total > 0 && (
|
||||
<span className="text-sm text-muted-foreground" data-testid="text-article-count">
|
||||
{data.total} Beiträge
|
||||
{data.totalPages > 1 && ` · Seite ${data.page} von ${data.totalPages}`}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{isLoading ? (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
@@ -45,57 +146,60 @@ export default function CategoryPage() {
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : articles && articles.length > 0 ? (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
{articles.flatMap((article, index) => {
|
||||
const items = [
|
||||
<Link key={article.id} href={`/article/${article.slug}`}>
|
||||
<article
|
||||
className="group cursor-pointer bg-card rounded-md border border-card-border transition-all duration-300 h-full"
|
||||
data-testid={`card-article-${article.id}`}
|
||||
>
|
||||
<div className="relative rounded-t-md">
|
||||
<div className="overflow-hidden rounded-t-md">
|
||||
<img
|
||||
src={article.coverImage || "/images/article-1.png"}
|
||||
alt={article.title}
|
||||
className="w-full aspect-video object-cover transition-transform duration-500 group-hover:scale-105"
|
||||
style={{ objectPosition: "center 25%" }}
|
||||
loading="lazy"
|
||||
/>
|
||||
) : data && data.articles.length > 0 ? (
|
||||
<>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
{data.articles.flatMap((article, index) => {
|
||||
const items = [
|
||||
<Link key={article.id} href={`/article/${article.slug}`}>
|
||||
<article
|
||||
className="group cursor-pointer bg-card rounded-md border border-card-border transition-all duration-300 h-full"
|
||||
data-testid={`card-article-${article.id}`}
|
||||
>
|
||||
<div className="relative rounded-t-md">
|
||||
<div className="overflow-hidden rounded-t-md">
|
||||
<img
|
||||
src={article.coverImage || "/images/article-1.png"}
|
||||
alt={article.title}
|
||||
className="w-full aspect-video object-cover transition-transform duration-500 group-hover:scale-105"
|
||||
style={{ objectPosition: "center 25%" }}
|
||||
loading="lazy"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="p-4">
|
||||
<div className="flex items-center gap-2 text-muted-foreground text-xs mb-2">
|
||||
<span>{article.author}</span>
|
||||
<span>·</span>
|
||||
<span>{format(new Date(article.publishedAt), "d. MMMM yyyy", { locale: de })}</span>
|
||||
<div className="p-4">
|
||||
<div className="flex items-center gap-2 text-muted-foreground text-xs mb-2">
|
||||
<span>{article.author}</span>
|
||||
<span>·</span>
|
||||
<span>{format(new Date(article.publishedAt), "d. MMMM yyyy", { locale: de })}</span>
|
||||
</div>
|
||||
<h3 className="font-semibold text-card-foreground mb-2 line-clamp-2 group-hover:text-primary transition-colors">
|
||||
{article.title}
|
||||
</h3>
|
||||
<p className="text-muted-foreground text-sm line-clamp-3 mb-3">
|
||||
{article.excerpt}
|
||||
</p>
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<span className="flex items-center gap-1 text-xs text-muted-foreground">
|
||||
<Eye className="w-3.5 h-3.5" />
|
||||
{article.views.toLocaleString()}
|
||||
</span>
|
||||
<Button size="sm" data-testid={`button-read-${article.id}`}>
|
||||
Weiterlesen
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<h3 className="font-semibold text-card-foreground mb-2 line-clamp-2 group-hover:text-primary transition-colors">
|
||||
{article.title}
|
||||
</h3>
|
||||
<p className="text-muted-foreground text-sm line-clamp-3 mb-3">
|
||||
{article.excerpt}
|
||||
</p>
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<span className="flex items-center gap-1 text-xs text-muted-foreground">
|
||||
<Eye className="w-3.5 h-3.5" />
|
||||
{article.views.toLocaleString()}
|
||||
</span>
|
||||
<Button size="sm" data-testid={`button-read-${article.id}`}>
|
||||
Weiterlesen
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
</Link>,
|
||||
];
|
||||
if (index === 2) {
|
||||
items.push(<ArticleCardAd key="ad-cat-1" />);
|
||||
}
|
||||
return items;
|
||||
})}
|
||||
</div>
|
||||
</article>
|
||||
</Link>,
|
||||
];
|
||||
if ((index + 1) % 3 === 0) {
|
||||
items.push(<ArticleCardAd key={`ad-cat-${index}`} />);
|
||||
}
|
||||
return items;
|
||||
})}
|
||||
</div>
|
||||
{renderPagination()}
|
||||
</>
|
||||
) : (
|
||||
<div className="text-center py-16">
|
||||
<p className="text-muted-foreground text-lg">
|
||||
|
||||
@@ -0,0 +1,237 @@
|
||||
import { Shield } from "lucide-react";
|
||||
import Header from "@/components/header";
|
||||
import Footer from "@/components/footer";
|
||||
import { usePageMeta } from "@/hooks/use-page-meta";
|
||||
|
||||
export default function DatenschutzPage() {
|
||||
usePageMeta("Datenschutz - FOLX TV", "Datenschutzerklärung von FOLX TV – Ihr Volksmusik & Schlager Fernsehsender.");
|
||||
return (
|
||||
<div className="min-h-screen bg-background">
|
||||
<Header />
|
||||
<main className="max-w-3xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||||
<div className="flex items-center gap-3 mb-8">
|
||||
<Shield className="w-7 h-7 text-primary" />
|
||||
<h1 className="text-3xl font-bold text-foreground" data-testid="text-datenschutz-title">
|
||||
Datenschutzerklärung
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
<article className="space-y-6 text-[15px] leading-relaxed text-muted-foreground" data-testid="section-datenschutz-content">
|
||||
<section>
|
||||
<h2 className="text-foreground font-semibold text-lg mb-2">Allgemeiner Hinweis und Pflichtinformationen</h2>
|
||||
<h3 className="text-foreground font-medium mb-2">Benennung der verantwortlichen Stelle</h3>
|
||||
<p>Die verantwortliche Stelle für die Datenverarbeitung auf dieser Website ist:</p>
|
||||
<p className="mt-2">
|
||||
BoldFrame Productions d.o.o.
|
||||
<br />
|
||||
Sokolska ulica 46
|
||||
<br />
|
||||
2000 Maribor
|
||||
</p>
|
||||
<p className="mt-2">
|
||||
Die verantwortliche Stelle entscheidet allein oder gemeinsam mit anderen über die Zwecke und Mittel der Verarbeitung von personenbezogenen Daten (z.B. Namen, Kontaktdaten o. Ä.).
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2 className="text-foreground font-semibold text-lg mb-2">Widerruf Ihrer Einwilligung zur Datenverarbeitung</h2>
|
||||
<p>
|
||||
Nur mit Ihrer ausdrücklichen Einwilligung sind einige Vorgänge der Datenverarbeitung möglich. Ein Widerruf Ihrer bereits erteilten Einwilligung ist jederzeit möglich. Für den Widerruf genügt eine formlose Mitteilung per E-Mail. Die Rechtmäßigkeit der bis zum Widerruf erfolgten Datenverarbeitung bleibt vom Widerruf unberührt.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2 className="text-foreground font-semibold text-lg mb-2">Recht auf Beschwerde bei der zuständigen Aufsichtsbehörde</h2>
|
||||
<p>
|
||||
Als Betroffener steht Ihnen im Falle eines datenschutzrechtlichen Verstoßes ein Beschwerderecht bei der zuständigen Aufsichtsbehörde zu. Zuständige Aufsichtsbehörde bezüglich datenschutzrechtlicher Fragen ist der Landesdatenschutzbeauftragte des Bundeslandes, in dem sich der Sitz unseres Unternehmens befindet. Der folgende Link stellt eine Liste der Datenschutzbeauftragten sowie deren Kontaktdaten bereit:{" "}
|
||||
<a href="https://www.bfdi.bund.de/DE/Infothek/Anschriften_Links/anschriften_links-node.html" target="_blank" rel="noopener noreferrer" className="text-primary hover:underline break-all">
|
||||
https://www.bfdi.bund.de/DE/Infothek/Anschriften_Links/anschriften_links-node.html
|
||||
</a>.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2 className="text-foreground font-semibold text-lg mb-2">Recht auf Datenübertragbarkeit</h2>
|
||||
<p>
|
||||
Ihnen steht das Recht zu, Daten, die wir auf Grundlage Ihrer Einwilligung oder in Erfüllung eines Vertrags automatisiert verarbeiten, an sich oder an Dritte aushändigen zu lassen. Die Bereitstellung erfolgt in einem maschinenlesbaren Format. Sofern Sie die direkte Übertragung der Daten an einen anderen Verantwortlichen verlangen, erfolgt dies nur, soweit es technisch machbar ist.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2 className="text-foreground font-semibold text-lg mb-2">Recht auf Auskunft, Berichtigung, Sperrung, Löschung</h2>
|
||||
<p>
|
||||
Sie haben jederzeit im Rahmen der geltenden gesetzlichen Bestimmungen das Recht auf unentgeltliche Auskunft über Ihre gespeicherten personenbezogenen Daten, Herkunft der Daten, deren Empfänger und den Zweck der Datenverarbeitung und ggf. ein Recht auf Berichtigung, Sperrung oder Löschung dieser Daten. Diesbezüglich und auch zu weiteren Fragen zum Thema personenbezogene Daten können Sie sich jederzeit über die im Impressum aufgeführten Kontaktmöglichkeiten an uns wenden.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2 className="text-foreground font-semibold text-lg mb-2">SSL- bzw. TLS-Verschlüsselung</h2>
|
||||
<p>
|
||||
Aus Sicherheitsgründen und zum Schutz der Übertragung vertraulicher Inhalte, die Sie an uns als Seitenbetreiber senden, nutzt unsere Website eine SSL-bzw. TLS-Verschlüsselung. Damit sind Daten, die Sie über diese Website übermitteln, für Dritte nicht mitlesbar. Sie erkennen eine verschlüsselte Verbindung an der „https://" Adresszeile Ihres Browsers und am Schloss-Symbol in der Browserzeile.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2 className="text-foreground font-semibold text-lg mb-2">Kontaktformular</h2>
|
||||
<p>
|
||||
Per Kontaktformular übermittelte Daten werden einschließlich Ihrer Kontaktdaten gespeichert, um Ihre Anfrage bearbeiten zu können oder um für Anschlussfragen bereitzustehen. Eine Weitergabe dieser Daten findet ohne Ihre Einwilligung nicht statt.
|
||||
</p>
|
||||
<p className="mt-2">
|
||||
Die Verarbeitung der in das Kontaktformular eingegebenen Daten erfolgt ausschließlich auf Grundlage Ihrer Einwilligung (Art. 6 Abs. 1 lit. a DSGVO). Ein Widerruf Ihrer bereits erteilten Einwilligung ist jederzeit möglich. Für den Widerruf genügt eine formlose Mitteilung per E-Mail. Die Rechtmäßigkeit der bis zum Widerruf erfolgten Datenverarbeitungsvorgänge bleibt vom Widerruf unberührt.
|
||||
</p>
|
||||
<p className="mt-2">
|
||||
Über das Kontaktformular übermittelte Daten verbleiben bei uns, bis Sie uns zur Löschung auffordern, Ihre Einwilligung zur Speicherung widerrufen oder keine Notwendigkeit der Datenspeicherung mehr besteht. Zwingende gesetzliche Bestimmungen – insbesondere Aufbewahrungsfristen – bleiben unberührt.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2 className="text-foreground font-semibold text-lg mb-2">Speicherdauer von Beiträgen und Kommentaren</h2>
|
||||
<p>
|
||||
Beiträge und Kommentare sowie damit in Verbindung stehende Daten, wie beispielsweise IP-Adressen, werden gespeichert. Der Inhalt verbleibt auf unserer Website, bis er vollständig gelöscht wurde oder aus rechtlichen Gründen gelöscht werden musste.
|
||||
</p>
|
||||
<p className="mt-2">
|
||||
Die Speicherung der Beiträge und Kommentare erfolgt auf Grundlage Ihrer Einwilligung (Art. 6 Abs. 1 lit. a DSGVO). Ein Widerruf Ihrer bereits erteilten Einwilligung ist jederzeit möglich. Für den Widerruf genügt eine formlose Mitteilung per E-Mail. Die Rechtmäßigkeit bereits erfolgter Datenverarbeitungsvorgänge bleibt vom Widerruf unberührt.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2 className="text-foreground font-semibold text-lg mb-2">YouTube</h2>
|
||||
<p>
|
||||
Für Integration und Darstellung von Videoinhalten nutzt unsere Website Plugins von YouTube. Anbieter des Videoportals ist die YouTube, LLC, 901 Cherry Ave., San Bruno, CA 94066, USA.
|
||||
</p>
|
||||
<p className="mt-2">
|
||||
Bei Aufruf einer Seite mit integriertem YouTube-Plugin wird eine Verbindung zu den Servern von YouTube hergestellt. YouTube erfährt hierdurch, welche unserer Seiten Sie aufgerufen haben.
|
||||
</p>
|
||||
<p className="mt-2">
|
||||
YouTube kann Ihr Surfverhalten direkt Ihrem persönlichen Profil zuzuordnen, sollten Sie in Ihrem YouTube Konto eingeloggt sein. Durch vorheriges Ausloggen haben Sie die Möglichkeit, dies zu unterbinden.
|
||||
</p>
|
||||
<p className="mt-2">
|
||||
Die Nutzung von YouTube erfolgt im Interesse einer ansprechenden Darstellung unserer Online-Angebote. Dies stellt ein berechtigtes Interesse im Sinne von Art. 6 Abs. 1 lit. f DSGVO dar.
|
||||
</p>
|
||||
<p className="mt-2">
|
||||
Einzelheiten zum Umgang mit Nutzerdaten finden Sie in der Datenschutzerklärung von YouTube unter:{" "}
|
||||
<a href="https://www.google.de/intl/de/policies/privacy" target="_blank" rel="noopener noreferrer" className="text-primary hover:underline break-all">
|
||||
https://www.google.de/intl/de/policies/privacy
|
||||
</a>.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2 className="text-foreground font-semibold text-lg mb-2">Cookies</h2>
|
||||
<p>
|
||||
Unsere Website verwendet Cookies. Das sind kleine Textdateien, die Ihr Webbrowser auf Ihrem Endgerät speichert. Cookies helfen uns dabei, unser Angebot nutzerfreundlicher, effektiver und sicherer zu machen.
|
||||
</p>
|
||||
<p className="mt-2">
|
||||
Einige Cookies sind „Session-Cookies." Solche Cookies werden nach Ende Ihrer Browser-Sitzung von selbst gelöscht. Hingegen bleiben andere Cookies auf Ihrem Endgerät bestehen, bis Sie diese selbst löschen. Solche Cookies helfen uns, Sie bei Rückkehr auf unserer Website wiederzuerkennen.
|
||||
</p>
|
||||
<p className="mt-2">
|
||||
Mit einem modernen Webbrowser können Sie das Setzen von Cookies überwachen, einschränken oder unterbinden. Viele Webbrowser lassen sich so konfigurieren, dass Cookies mit dem Schließen des Programms von selbst gelöscht werden. Die Deaktivierung von Cookies kann eine eingeschränkte Funktionalität unserer Website zur Folge haben.
|
||||
</p>
|
||||
<p className="mt-2">
|
||||
Das Setzen von Cookies, die zur Ausübung elektronischer Kommunikationsvorgänge oder der Bereitstellung bestimmter, von Ihnen erwünschter Funktionen (z.B. Warenkorb) notwendig sind, erfolgt auf Grundlage von Art. 6 Abs. 1 lit. f DSGVO. Als Betreiber dieser Website haben wir ein berechtigtes Interesse an der Speicherung von Cookies zur technisch fehlerfreien und reibungslosen Bereitstellung unserer Dienste. Sofern die Setzung anderer Cookies (z.B. für Analyse-Funktionen) erfolgt, werden diese in dieser Datenschutzerklärung separat behandelt.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2 className="text-foreground font-semibold text-lg mb-2">Google Analytics</h2>
|
||||
<p>
|
||||
Unsere Website verwendet Funktionen des Webanalysedienstes Google Analytics. Anbieter des Webanalysedienstes ist die Google Inc., 1600 Amphitheatre Parkway, Mountain View, CA 94043, USA.
|
||||
</p>
|
||||
<p className="mt-2">
|
||||
Google Analytics verwendet „Cookies." Das sind kleine Textdateien, die Ihr Webbrowser auf Ihrem Endgerät speichert und eine Analyse der Website-Benutzung ermöglichen. Mittels Cookie erzeugte Informationen über Ihre Benutzung unserer Website werden an einen Server von Google übermittelt und dort gespeichert. Server-Standort ist im Regelfall die USA.
|
||||
</p>
|
||||
<p className="mt-2">
|
||||
Das Setzen von Google-Analytics-Cookies erfolgt auf Grundlage von Art. 6 Abs. 1 lit. f DSGVO. Als Betreiber dieser Website haben wir ein berechtigtes Interesse an der Analyse des Nutzerverhaltens, um unser Webangebot und ggf. auch Werbung zu optimieren.
|
||||
</p>
|
||||
|
||||
<h3 className="text-foreground font-medium mt-4 mb-2">IP-Anonymisierung</h3>
|
||||
<p>
|
||||
Wir setzen Google Analytics in Verbindung mit der Funktion IP-Anonymisierung ein. Sie gewährleistet, dass Google Ihre IP-Adresse innerhalb von Mitgliedstaaten der Europäischen Union oder in anderen Vertragsstaaten des Abkommens über den Europäischen Wirtschaftsraum vor der Übermittlung in die USA kürzt. Es kann Ausnahmefälle geben, in denen Google die volle IP-Adresse an einen Server in den USA überträgt und dort kürzt. In unserem Auftrag wird Google diese Informationen benutzen, um Ihre Nutzung der Website auszuwerten, um Reports über Websiteaktivitäten zu erstellen und um weitere mit der Websitenutzung und der Internetnutzung verbundene Dienstleistungen gegenüber uns zu erbringen. Es findet keine Zusammenführung der von Google Analytics übermittelten IP-Adresse mit anderen Daten von Google statt.
|
||||
</p>
|
||||
|
||||
<h3 className="text-foreground font-medium mt-4 mb-2">Browser Plugin</h3>
|
||||
<p>
|
||||
Das Setzen von Cookies durch Ihren Webbrowser ist verhinderbar. Einige Funktionen unserer Website könnten dadurch jedoch eingeschränkt werden. Ebenso können Sie die Erfassung von Daten bezüglich Ihrer Website-Nutzung einschließlich Ihrer IP-Adresse mitsamt anschließender Verarbeitung durch Google unterbinden. Dies ist möglich, indem Sie das über folgenden Link erreichbare Browser-Plugin herunterladen und installieren:{" "}
|
||||
<a href="https://tools.google.com/dlpage/gaoptout?hl=de" target="_blank" rel="noopener noreferrer" className="text-primary hover:underline break-all">
|
||||
https://tools.google.com/dlpage/gaoptout?hl=de
|
||||
</a>.
|
||||
</p>
|
||||
|
||||
<h3 className="text-foreground font-medium mt-4 mb-2">Widerspruch gegen die Datenerfassung</h3>
|
||||
<p>
|
||||
Sie können die Erfassung Ihrer Daten durch Google Analytics verhindern, indem Sie auf folgenden Link klicken. Es wird ein Opt-Out-Cookie gesetzt, der die Erfassung Ihrer Daten bei zukünftigen Besuchen unserer Website verhindert:{" "}
|
||||
<a href="https://tools.google.com/dlpage/gaoptout?hl=de" target="_blank" rel="noopener noreferrer" className="text-primary hover:underline break-all">
|
||||
Google Analytics deaktivieren
|
||||
</a>.
|
||||
</p>
|
||||
<p className="mt-2">
|
||||
Einzelheiten zum Umgang mit Nutzerdaten bei Google Analytics finden Sie in der Datenschutzerklärung von Google:{" "}
|
||||
<a href="https://support.google.com/analytics/answer/6004245?hl=de" target="_blank" rel="noopener noreferrer" className="text-primary hover:underline break-all">
|
||||
https://support.google.com/analytics/answer/6004245?hl=de
|
||||
</a>.
|
||||
</p>
|
||||
|
||||
<h3 className="text-foreground font-medium mt-4 mb-2">Auftragsverarbeitung</h3>
|
||||
<p>
|
||||
Zur vollständigen Erfüllung der gesetzlichen Datenschutzvorgaben haben wir mit Google einen Vertrag über die Auftragsverarbeitung abgeschlossen.
|
||||
</p>
|
||||
|
||||
<h3 className="text-foreground font-medium mt-4 mb-2">Demografische Merkmale bei Google Analytics</h3>
|
||||
<p>
|
||||
Unsere Website verwendet die Funktion „demografische Merkmale" von Google Analytics. Mit ihr lassen sich Berichte erstellen, die Aussagen zu Alter, Geschlecht und Interessen der Seitenbesucher enthalten. Diese Daten stammen aus interessenbezogener Werbung von Google sowie aus Besucherdaten von Drittanbietern. Eine Zuordnung der Daten zu einer bestimmten Person ist nicht möglich. Sie können diese Funktion jederzeit deaktivieren. Dies ist über die Anzeigeneinstellungen in Ihrem Google-Konto möglich oder indem Sie die Erfassung Ihrer Daten durch Google Analytics, wie im Punkt „Widerspruch gegen die Datenerfassung" erläutert, generell untersagen.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2 className="text-foreground font-semibold text-lg mb-2">Google AdSense</h2>
|
||||
<p>
|
||||
Unsere Website verwendet Google AdSense. Anbieter ist die Google Inc., 1600 Amphitheatre Parkway, Mountain View, CA 94043, USA.
|
||||
</p>
|
||||
<p className="mt-2">
|
||||
Google AdSense dient der Einbindung von Werbeanzeigen und setzt Cookies. Cookies sind kleine Textdateien, die Ihr Webbrowser auf Ihrem Endgerät speichert, um die Nutzung der Website analysieren. Google AdSense setzt außerdem Web Beacons ein. Web Beacons sind unsichtbare Grafiken, die eine Analyse des Besucherverkehrs auf unserer Website ermöglichen.
|
||||
</p>
|
||||
<p className="mt-2">
|
||||
Durch Cookies und Web Beacons erzeugten Informationen werden an Server von Google übertragen und dort gespeichert. Serverstandort sind die USA. Google kann diese Informationen an Vertragspartner weiterreichen. Ihre IP-Adresse wird Google jedoch nicht mit anderen von Ihnen gespeicherten Daten zusammenführen.
|
||||
</p>
|
||||
<p className="mt-2">
|
||||
Die Speicherung von AdSense-Cookies erfolgt auf Grundlage von Art. 6 Abs. 1 lit. f DSGVO. Wir als Websitebetreiber haben ein berechtigtes Interesse an der Analyse des Nutzerverhaltens, um unser Webangebot und unsere Werbung zu optimieren.
|
||||
</p>
|
||||
<p className="mt-2">
|
||||
Mit einem modernen Webbrowser können Sie das Setzen von Cookies überwachen, einschränken und unterbinden. Die Deaktivierung von Cookies kann eine eingeschränkte Funktionalität unserer Website zur Folge haben. Durch die Nutzung unserer Website erklären Sie sich mit der Bearbeitung der über Sie erhobenen Daten durch Google in der zuvor beschriebenen Art und Weise sowie dem zuvor benannten Zweck einverstanden.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2 className="text-foreground font-semibold text-lg mb-2">Google AdWords und Google Conversion-Tracking</h2>
|
||||
<p>
|
||||
Unsere Website verwendet Google AdWords. Anbieter ist die Google Inc., 1600 Amphitheatre Parkway, Mountain View, CA 94043, United States.
|
||||
</p>
|
||||
<p className="mt-2">
|
||||
AdWords ist ein Online-Werbeprogramm. Im Rahmen des Online-Werbeprogramms arbeiten wir mit Conversion-Tracking. Nach einem Klick auf eine von Google geschaltete Anzeige wird ein Cookie für das Conversion-Tracking gesetzt. Cookies sind kleine Textdateien, die Ihr Webbrowser auf Ihrem Endgerät speichert. Google AdWords Cookies verlieren nach 30 Tagen ihre Gültigkeit und dienen nicht der persönlichen Identifizierung der Nutzer. Am Cookie können Google und wir erkennen, dass Sie auf eine Anzeige geklickt haben und zu unserer Website weitergeleitet wurden.
|
||||
</p>
|
||||
<p className="mt-2">
|
||||
Jeder Google AdWords-Kunde erhält ein anderes Cookie. Die Cookies sind nicht über Websites von AdWords-Kunden nachverfolgbar. Mit Conversion-Cookies werden Conversion-Statistiken für AdWords-Kunden, die Conversion-Tracking einsetzen, erstellt. AdWords-Kunden erfahren wie viele Nutzer auf ihre Anzeige geklickt haben und auf Seiten mit Conversion-Tracking-Tag weitergeleitet wurden. AdWords-Kunden erhalten jedoch keine Informationen, die eine persönliche Identifikation der Nutzer ermöglichen. Wenn Sie nicht am Tracking teilnehmen möchten, können Sie einer Nutzung widersprechen. Hier ist das Conversion-Cookie in den Nutzereinstellungen des Browsers zu deaktivieren. So findet auch keine Aufnahme in die Conversion-Tracking Statistiken statt.
|
||||
</p>
|
||||
<p className="mt-2">
|
||||
Die Speicherung von „Conversion-Cookies" erfolgt auf Grundlage von Art. 6 Abs. 1 lit. f DSGVO. Wir als Websitebetreiber haben ein berechtigtes Interesse an der Analyse des Nutzerverhaltens, um unser Webangebot und unsere Werbung zu optimieren.
|
||||
</p>
|
||||
<p className="mt-2">
|
||||
Einzelheiten zu Google AdWords und Google Conversion-Tracking finden Sie in den Datenschutzbestimmungen von Google:{" "}
|
||||
<a href="https://www.google.de/policies/privacy/" target="_blank" rel="noopener noreferrer" className="text-primary hover:underline break-all">
|
||||
https://www.google.de/policies/privacy/
|
||||
</a>.
|
||||
</p>
|
||||
<p className="mt-2">
|
||||
Mit einem modernen Webbrowser können Sie das Setzen von Cookies überwachen, einschränken oder unterbinden. Die Deaktivierung von Cookies kann eine eingeschränkte Funktionalität unserer Website zur Folge haben.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<p className="text-xs text-muted-foreground/60 pt-4">
|
||||
Quelle: Datenschutz-Konfigurator von mein-datenschutzbeauftragter.de
|
||||
</p>
|
||||
</article>
|
||||
</main>
|
||||
<Footer />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
import { Tv, Globe, MapPin } from "lucide-react";
|
||||
import Header from "@/components/header";
|
||||
import Footer from "@/components/footer";
|
||||
import { usePageMeta } from "@/hooks/use-page-meta";
|
||||
|
||||
const COUNTRIES = [
|
||||
{
|
||||
country: "Deutschland",
|
||||
flag: "DE",
|
||||
providers: [
|
||||
{ name: "MagentaTV", detail: "Telekom Deutschland" },
|
||||
{ name: "Zattoo", detail: null },
|
||||
{ name: "O2 TV", detail: null },
|
||||
],
|
||||
},
|
||||
{
|
||||
country: "Österreich",
|
||||
flag: "AT",
|
||||
providers: [
|
||||
{ name: "A1 Xplore TV", detail: null },
|
||||
{ name: "Salzburg AG CableLink", detail: null },
|
||||
{ name: "Zattoo", detail: null },
|
||||
],
|
||||
},
|
||||
{
|
||||
country: "Schweiz",
|
||||
flag: "CH",
|
||||
providers: [
|
||||
{ name: "Swisscom blue TV", detail: null },
|
||||
{ name: "Zattoo", detail: null },
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
export default function EmpfangPage() {
|
||||
usePageMeta("Empfang FOLX TV - Volksmusik & Schlager Sender empfangen", "So empfangen Sie FOLX TV – Ihren Volksmusik & Schlager Sender in Deutschland, Österreich und der Schweiz.");
|
||||
return (
|
||||
<div className="min-h-screen bg-background">
|
||||
<Header />
|
||||
<main className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||||
<div className="flex items-center gap-3 mb-2">
|
||||
<Tv className="w-7 h-7 text-primary" />
|
||||
<h1 className="text-3xl font-bold text-foreground" data-testid="text-empfang-title">
|
||||
Empfang von FOLX TV
|
||||
</h1>
|
||||
</div>
|
||||
<p className="text-muted-foreground mb-8" data-testid="text-empfang-subtitle">
|
||||
So empfangen Sie Folx Music Television in Ihrem Land
|
||||
</p>
|
||||
|
||||
<div className="space-y-6 mb-10">
|
||||
{COUNTRIES.map((c) => (
|
||||
<section
|
||||
key={c.country}
|
||||
className="bg-card rounded-lg border border-card-border overflow-hidden"
|
||||
data-testid={`section-country-${c.flag.toLowerCase()}`}
|
||||
>
|
||||
<div className="flex items-center gap-3 px-5 py-4 border-b border-card-border bg-muted/20">
|
||||
<MapPin className="w-4 h-4 text-primary" />
|
||||
<h2 className="text-lg font-bold text-card-foreground">{c.country}</h2>
|
||||
</div>
|
||||
<div className="px-5 py-4">
|
||||
<div className="flex flex-wrap items-center gap-x-2 gap-y-1">
|
||||
{c.providers.map((p, i) => (
|
||||
<span key={p.name} className="flex items-center gap-x-2">
|
||||
<span className="text-card-foreground font-medium" data-testid={`text-provider-${p.name.toLowerCase().replace(/\s+/g, "-")}`}>
|
||||
{p.name}
|
||||
{p.detail && (
|
||||
<span className="text-muted-foreground font-normal text-sm ml-1">
|
||||
({p.detail})
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
{i < c.providers.length - 1 && (
|
||||
<span className="text-muted-foreground/40 select-none" aria-hidden="true">·</span>
|
||||
)}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<section className="bg-card rounded-lg border border-card-border overflow-hidden" data-testid="section-online">
|
||||
<div className="flex items-center gap-3 px-5 py-4 border-b border-card-border bg-muted/20">
|
||||
<Globe className="w-4 h-4 text-primary" />
|
||||
<h2 className="text-lg font-bold text-card-foreground">Online</h2>
|
||||
</div>
|
||||
<div className="px-5 py-4">
|
||||
<p className="text-muted-foreground text-sm leading-relaxed">
|
||||
FOLX TV kann auch über den offiziellen Livestream im Internet empfangen werden.
|
||||
Besuchen Sie{" "}
|
||||
<a
|
||||
href="https://www.folx.tv"
|
||||
className="text-primary hover:underline font-medium"
|
||||
data-testid="link-livestream"
|
||||
>
|
||||
www.folx.tv
|
||||
</a>{" "}
|
||||
für den direkten Zugang.
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
<Footer />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -3,8 +3,11 @@ import Footer from "@/components/footer";
|
||||
import GalleryPage from "@/components/photo-gallery";
|
||||
import { ArrowLeft } from "lucide-react";
|
||||
import { Link } from "wouter";
|
||||
import { usePageMeta } from "@/hooks/use-page-meta";
|
||||
import { InArticleAd } from "@/components/adsense";
|
||||
|
||||
export default function GalleryPageWrapper() {
|
||||
usePageMeta("Fotogalerie - Volksmusik & Schlager Bilder", "Fotos und Bilder von Volksmusik- und Schlager-Stars bei FOLX TV.");
|
||||
return (
|
||||
<div className="min-h-screen bg-background">
|
||||
<Header />
|
||||
@@ -18,7 +21,9 @@ export default function GalleryPageWrapper() {
|
||||
<h1 className="text-2xl font-bold text-foreground mb-6" data-testid="text-gallery-title">
|
||||
Fotogalerie
|
||||
</h1>
|
||||
<InArticleAd />
|
||||
<GalleryPage />
|
||||
<InArticleAd />
|
||||
</main>
|
||||
<Footer />
|
||||
</div>
|
||||
|
||||
+102
-41
@@ -3,12 +3,12 @@ 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, MultiplexAd, SidebarAd } from "@/components/adsense";
|
||||
import ArtistPatternBg from "@/components/artist-pattern-bg";
|
||||
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";
|
||||
@@ -398,25 +398,54 @@ function FeaturedCarousel({ articles, popular, galleryImages, focalPoints }: { a
|
||||
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) => (p + 1) % totalPages);
|
||||
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 = page * pageSize;
|
||||
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 (
|
||||
<section data-testid="featured-carousel" onMouseEnter={() => setPaused(true)} onMouseLeave={() => setPaused(false)}>
|
||||
<div className="grid grid-cols-1 lg:grid-cols-5 gap-4">
|
||||
{nextPageArticles.length > 0 && (
|
||||
<div className="hidden" aria-hidden="true">
|
||||
{nextPageArticles.map((a) => a.coverImage && <img key={`preload-${a.id}`} src={a.coverImage} alt="" />)}
|
||||
</div>
|
||||
)}
|
||||
<div
|
||||
className="grid grid-cols-1 lg:grid-cols-5 gap-4 transition-opacity duration-300"
|
||||
style={{ opacity: fading ? 0 : 1 }}
|
||||
>
|
||||
<div className="lg:col-span-3">
|
||||
{hero && <FeaturedHeroCard article={hero} focalPoints={focalPoints} />}
|
||||
</div>
|
||||
@@ -454,6 +483,7 @@ function BentoSkeleton() {
|
||||
}
|
||||
|
||||
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<Article[]>({
|
||||
queryKey: ["/api/articles"],
|
||||
});
|
||||
@@ -485,6 +515,10 @@ export default function Home() {
|
||||
{ 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} /> },
|
||||
{ id: "horoscope2", el: <HoroscopeWidget key="horoscope2" /> },
|
||||
{ id: "news2", el: <div key="news2" className="flex flex-col gap-4"><NewsWidget /></div> },
|
||||
{ id: "recipe2", el: <RecipeWidget key="recipe2" /> },
|
||||
{ id: "breaking2", el: <div key="breaking2" className="flex flex-col gap-4"><BreakingNewsWidget /></div> },
|
||||
], []);
|
||||
|
||||
const gridItems = useMemo(() => {
|
||||
@@ -515,7 +549,7 @@ export default function Home() {
|
||||
}
|
||||
|
||||
const totalRows = items.length / 4;
|
||||
const adRows = [1, 3, 5, 7];
|
||||
const adRows = [1, 3, 5, 7, 9, 11, 13];
|
||||
let adCount = 0;
|
||||
for (const row of adRows) {
|
||||
if (row >= totalRows) continue;
|
||||
@@ -568,6 +602,16 @@ export default function Home() {
|
||||
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: <NewsWidget key="bottom-news" /> },
|
||||
@@ -580,9 +624,14 @@ export default function Home() {
|
||||
})),
|
||||
{ type: "ad", el: <div key="ad-bottom-1"><ArticleCardAd /></div> },
|
||||
{ type: "widget", el: <PhotoGalleryWidget key="bottom-gallery" /> },
|
||||
{ type: "ad", el: <div key="ad-bottom-2"><ArticleCardAd /></div> },
|
||||
...extraBottomArticles.slice(0, 3).map((a) => ({
|
||||
type: "article" as const,
|
||||
el: <BlogCard key={`bottom-extra-${a.id}`} article={a} focalPoints={focalPoints} />,
|
||||
})),
|
||||
];
|
||||
return items;
|
||||
}, [bottomArticles, focalPoints]);
|
||||
}, [bottomArticles, extraBottomArticles, focalPoints]);
|
||||
|
||||
if (isLoading || !articles) {
|
||||
return (
|
||||
@@ -630,47 +679,41 @@ export default function Home() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ArticleCardAd />
|
||||
|
||||
{widePickedArticles.length > 0 && (
|
||||
<div className="grid grid-cols-1 lg:grid-cols-4 gap-4">
|
||||
<ArtistPatternBg className="hidden lg:block rounded-lg overflow-hidden bg-card border border-card-border" seed={99}>
|
||||
<AdSense
|
||||
slot="3854634730"
|
||||
format="fluid"
|
||||
layoutKey="-6r+cy-10+8a-3"
|
||||
style={{ display: "block" }}
|
||||
className="w-full h-full min-h-[250px]"
|
||||
/>
|
||||
</ArtistPatternBg>
|
||||
<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 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>
|
||||
)}
|
||||
|
||||
{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">
|
||||
<WideCardClassic article={widePickedArticles[0]} focalPoints={focalPoints} />
|
||||
{widePickedArticles[1] && <WideCardClassic article={widePickedArticles[1]} focalPoints={focalPoints} />}
|
||||
</div>
|
||||
)}
|
||||
<div key={`row-group-${ri}`}>
|
||||
<div 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">
|
||||
<WideCardClassic article={widePickedArticles[0]} focalPoints={focalPoints} />
|
||||
{widePickedArticles[1] && <WideCardClassic article={widePickedArticles[1]} focalPoints={focalPoints} />}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{ri % 3 === 2 && ri < gridRows.length - 1 && <ArticleCardAd />}
|
||||
</div>
|
||||
))}
|
||||
|
||||
<ArticleCardAd />
|
||||
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
{bottomSection.map((item, i) => (
|
||||
<div key={`bottom-${i}`}>
|
||||
@@ -679,7 +722,25 @@ export default function Home() {
|
||||
))}
|
||||
</div>
|
||||
|
||||
<MultiplexAd />
|
||||
<ArticleCardAd />
|
||||
|
||||
{extraBottomArticles.length > 3 && (
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
{extraBottomArticles.slice(3, 6).map((a) => (
|
||||
<BlogCard key={`extra-bottom-${a.id}`} article={a} focalPoints={focalPoints} />
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<ArticleCardAd />
|
||||
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
<PhotoGalleryWidget key="extra-gallery" />
|
||||
<NewsWidget key="extra-news" />
|
||||
<RecipeWidget key="extra-recipe" />
|
||||
</div>
|
||||
|
||||
<ArticleCardAd />
|
||||
|
||||
</main>
|
||||
<Footer />
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useState, useEffect, useRef } from "react";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { Link, useParams } from "wouter";
|
||||
import { usePageMeta } from "@/hooks/use-page-meta";
|
||||
import {
|
||||
Star,
|
||||
Heart,
|
||||
@@ -368,6 +369,7 @@ function SignDetail({ signIndex, onNavigate, aiHoroscopes }: { signIndex: number
|
||||
}
|
||||
|
||||
export default function HoroscopePage() {
|
||||
usePageMeta("Horoskop - Volksmusik & Schlager", "Tägliches Horoskop für Volksmusik- und Schlager-Fans bei FOLX TV.");
|
||||
const params = useParams<{ sign?: string }>();
|
||||
const [selected, setSelected] = useState<number | null>(null);
|
||||
const detailRef = useRef<HTMLDivElement>(null);
|
||||
@@ -418,6 +420,8 @@ export default function HoroscopePage() {
|
||||
|
||||
<AstroEventsSection />
|
||||
|
||||
<InArticleAd />
|
||||
|
||||
<SignGrid onSelect={handleSelect} selectedIndex={selected} aiHoroscopes={aiHoroscopes} />
|
||||
|
||||
<div ref={detailRef}>
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
import { Scale } from "lucide-react";
|
||||
import Header from "@/components/header";
|
||||
import Footer from "@/components/footer";
|
||||
import { usePageMeta } from "@/hooks/use-page-meta";
|
||||
|
||||
export default function ImpressumPage() {
|
||||
usePageMeta("Impressum - FOLX TV", "Impressum und rechtliche Informationen zu FOLX TV – Volksmusik & Schlager Fernsehsender.");
|
||||
return (
|
||||
<div className="min-h-screen bg-background">
|
||||
<Header />
|
||||
<main className="max-w-3xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||||
<div className="flex items-center gap-3 mb-8">
|
||||
<Scale className="w-7 h-7 text-primary" />
|
||||
<h1 className="text-3xl font-bold text-foreground" data-testid="text-impressum-title">
|
||||
Impressum
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
<article className="space-y-8 text-[15px] leading-relaxed text-muted-foreground" data-testid="section-impressum-content">
|
||||
<section>
|
||||
<h2 className="text-foreground font-semibold text-lg mb-3">Herausgeber</h2>
|
||||
<p>
|
||||
BoldFrame Productions d.o.o.
|
||||
<br />
|
||||
Sokolska ulica 46
|
||||
<br />
|
||||
2000 Maribor
|
||||
<br />
|
||||
Slowenien
|
||||
</p>
|
||||
<p className="mt-2">
|
||||
UID-Nr.: SI38853507
|
||||
<br />
|
||||
E-Mail:{" "}
|
||||
<a href="mailto:office@boldframe.productions" className="text-primary hover:underline" data-testid="link-email">
|
||||
office@boldframe.productions
|
||||
</a>
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2 className="text-foreground font-semibold text-lg mb-3">Offenlegung nach Mediengesetz</h2>
|
||||
<p>
|
||||
Medieninhaber: BoldFrame Productions d.o.o.
|
||||
<br />
|
||||
Unternehmensgegenstand: Privatfernsehveranstalter, Film, TV und Videoproduktionen
|
||||
<br />
|
||||
Sokolska ulica 46
|
||||
<br />
|
||||
2000 Maribor
|
||||
<br />
|
||||
Slowenien
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2 className="text-foreground font-semibold text-lg mb-3">Copyright</h2>
|
||||
<p>
|
||||
Fotos stammen aus eigenen Archiven oder wurden von den Künstlern, Produzenten und Musikfirmen zugeliefert. Die Rechte für sämtliche Abbildungen, Grafiken, Texte liegen, sofern nicht anders gekennzeichnet, bei BoldFrame Productions d.o.o. Unerlaubtes Kopieren oder Verwenden von Bildern und Texten (auch teilweise) bedarf der vorherigen schriftlichen Genehmigung. Alle Rechte vorbehalten.
|
||||
</p>
|
||||
<p className="mt-3">
|
||||
Wir widersprechen jeder kommerziellen Verwendung und jeder sonstigen Weitergabe und anderweitigen Veröffentlichung dieser Daten.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2 className="text-foreground font-semibold text-lg mb-3">Links</h2>
|
||||
<p>
|
||||
BoldFrame Productions d.o.o. übernimmt keine Verantwortung oder Haftung hinsichtlich des Zugriffs auf bzw. des Materials in Websites, auf die von dieser Site aus zugegriffen wird. Zum Zeitpunkt der Verlinkung bestand nach eingehender Prüfung der gelinkten Seiten keinerlei Verdacht auf Inhalte, die nach jeweils geltendem Landesrecht gegen Gesetze verstoßen hätten. BoldFrame Productions d.o.o. distanziert sich daher ausdrücklich und unwiderruflich von jeglichen rechtswidrigen Inhalten auf den verlinkten Seiten.
|
||||
</p>
|
||||
</section>
|
||||
</article>
|
||||
</main>
|
||||
<Footer />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useState } from "react";
|
||||
import { Link } from "wouter";
|
||||
import { ChefHat, Clock, Users, X, ChevronLeft } from "lucide-react";
|
||||
import { usePageMeta } from "@/hooks/use-page-meta";
|
||||
import Header from "@/components/header";
|
||||
import Footer from "@/components/footer";
|
||||
import { InArticleAd } from "@/components/adsense";
|
||||
@@ -295,6 +296,7 @@ function RecipeModal({ recipe, onClose }: { recipe: Recipe; onClose: () => void
|
||||
}
|
||||
|
||||
export default function RecipesPage() {
|
||||
usePageMeta("Rezepte - Alpenküche & Schlager", "Traditionelle Rezepte aus der Alpenküche bei FOLX TV – kochen wie die Volksmusik-Stars.");
|
||||
const [selectedRecipe, setSelectedRecipe] = useState<Recipe | null>(null);
|
||||
|
||||
return (
|
||||
@@ -348,7 +350,7 @@ export default function RecipesPage() {
|
||||
))}
|
||||
</div>
|
||||
|
||||
{ri % 2 === 0 && <InArticleAd />}
|
||||
<InArticleAd />
|
||||
</div>
|
||||
))}
|
||||
</main>
|
||||
|
||||
@@ -2,6 +2,7 @@ import { useQuery } from "@tanstack/react-query";
|
||||
import { useState, useEffect, useRef } from "react";
|
||||
import { Link, useSearch, useLocation } from "wouter";
|
||||
import { Search, Play, FileText, ArrowLeft } from "lucide-react";
|
||||
import { usePageMeta } from "@/hooks/use-page-meta";
|
||||
import Header from "@/components/header";
|
||||
|
||||
interface SearchResult {
|
||||
@@ -25,6 +26,7 @@ interface SearchResult {
|
||||
}
|
||||
|
||||
export default function SearchPage() {
|
||||
usePageMeta("Suche - Volksmusik & Schlager", "Durchsuchen Sie FOLX TV nach Volksmusik- und Schlager-Inhalten.");
|
||||
const searchString = useSearch();
|
||||
const initialQuery = new URLSearchParams(searchString).get("q") || "";
|
||||
const [query, setQuery] = useState(initialQuery);
|
||||
|
||||
+113
-17
@@ -1,13 +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 { Play, ArrowLeft } from "lucide-react";
|
||||
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 } 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
|
||||
@@ -61,10 +72,49 @@ function VideoCardSkeleton() {
|
||||
}
|
||||
|
||||
export default function VideosPage() {
|
||||
const { data: articles, isLoading } = useQuery<Article[]>({
|
||||
queryKey: ["/api/articles/category/Video"],
|
||||
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 />
|
||||
@@ -76,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>
|
||||
|
||||
Reference in New Issue
Block a user