folx-tv/client/src/pages/horoscope.tsx

420 lines
18 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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,
Briefcase,
TrendingUp,
ChevronLeft,
ChevronRight,
Calendar,
CalendarDays,
Sparkles,
Lightbulb,
AlertTriangle,
Moon,
Sun,
ArrowRight,
Loader2,
} from "lucide-react";
import Header from "@/components/header";
import Footer from "@/components/footer";
import InterstitialAd from "@/components/interstitial-ad";
import { InArticleAd, PageSideAds } from "@/components/adsense";
import {
SIGNS,
ELEMENT_COLORS,
getHoroscope as getStaticHoroscope,
getRating,
getLuckyNumbers,
getDailyColor,
} from "@/lib/horoscope-data";
interface AIHoroscope {
signIndex: number;
signName: string;
general: string;
love: string;
career: string;
health: string;
tip: string;
weekly: string;
monthly: string;
imageUrl?: string | null;
}
function StarRating({ count }: { count: number }) {
return (
<div className="flex gap-0.5">
{Array.from({ length: 5 }).map((_, i) => (
<Star key={i} className={`w-3.5 h-3.5 ${i < count ? "fill-primary text-primary" : "text-muted-foreground/30"}`} />
))}
</div>
);
}
function SignGrid({ onSelect, selectedIndex, aiHoroscopes }: { onSelect: (i: number) => void; selectedIndex: number | null; aiHoroscopes: AIHoroscope[] }) {
return (
<section className="mb-10" data-testid="section-sign-grid">
<h2 className="text-lg font-bold text-foreground flex items-center gap-2 mb-4">
<Star className="w-5 h-5 text-primary" />
Ihr Sternzeichen wählen
</h2>
<div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-6 gap-3">
{SIGNS.map((sign, i) => {
const ec = ELEMENT_COLORS[sign.element];
const aiH = aiHoroscopes.find(h => h.signIndex === i);
const horoscope = aiH || getStaticHoroscope(i);
const isSelected = selectedIndex === i;
return (
<button
key={sign.name}
onClick={() => onSelect(i)}
className={`text-left rounded-xl p-4 transition-all duration-200 border ${
isSelected
? `${ec.bg} ${ec.border} shadow-lg ${ec.glow} ring-1 ring-primary/30`
: "bg-card border-card-border hover:border-primary/40 hover:shadow-md"
}`}
data-testid={`button-sign-grid-${sign.name.toLowerCase()}`}
>
{(horoscope as any).imageUrl && (
<img
src={(horoscope as any).imageUrl}
alt={`Sternzeichen ${sign.name}`}
loading="lazy"
className="w-full h-24 object-cover rounded-lg mb-3"
/>
)}
<div className="flex items-center gap-2 mb-2">
<span className="text-2xl">{sign.symbol}</span>
<div>
<span className={`text-sm font-semibold block ${isSelected ? ec.text : "text-foreground"}`}>{sign.name}</span>
<span className="text-[10px] text-muted-foreground">{sign.date}</span>
</div>
</div>
<p className="text-[11px] text-foreground/60 leading-relaxed line-clamp-3">{horoscope.general.substring(0, 100)}...</p>
<div className="flex items-center gap-1 mt-2 text-[10px] text-primary">
Lesen <ArrowRight className="w-3 h-3" />
</div>
</button>
);
})}
</div>
</section>
);
}
function SignDetail({ signIndex, onNavigate, aiHoroscopes }: { signIndex: number; onNavigate: (i: number) => void; aiHoroscopes: AIHoroscope[] }) {
const [tab, setTab] = useState<"daily" | "weekly" | "monthly">("daily");
const sign = SIGNS[signIndex];
const ec = ELEMENT_COLORS[sign.element];
const aiH = aiHoroscopes.find(h => h.signIndex === signIndex);
const horoscope = aiH || getStaticHoroscope(signIndex);
const luckyNums = getLuckyNumbers(signIndex);
const dailyColor = getDailyColor(signIndex);
const detailRef = useRef<HTMLDivElement>(null);
const isAI = !!aiH;
const prevIndex = (signIndex - 1 + SIGNS.length) % SIGNS.length;
const nextIndex = (signIndex + 1) % SIGNS.length;
useEffect(() => {
setTab("daily");
}, [signIndex]);
return (
<section ref={detailRef} data-testid={`section-sign-detail-${sign.name.toLowerCase()}`}>
<div className="flex items-center justify-between mb-4">
<button
onClick={() => onNavigate(prevIndex)}
className="flex items-center gap-1.5 text-sm text-muted-foreground hover:text-foreground transition-colors bg-card border border-card-border rounded-lg px-3 py-2"
data-testid="button-sign-prev"
>
<ChevronLeft className="w-4 h-4" />
<span className="text-lg">{SIGNS[prevIndex].symbol}</span>
<span className="hidden sm:inline">{SIGNS[prevIndex].name}</span>
</button>
<h2 className="text-lg font-bold text-foreground flex items-center gap-2">
<span className="text-3xl">{sign.symbol}</span>
{sign.name}
</h2>
<button
onClick={() => onNavigate(nextIndex)}
className="flex items-center gap-1.5 text-sm text-muted-foreground hover:text-foreground transition-colors bg-card border border-card-border rounded-lg px-3 py-2"
data-testid="button-sign-next"
>
<span className="hidden sm:inline">{SIGNS[nextIndex].name}</span>
<span className="text-lg">{SIGNS[nextIndex].symbol}</span>
<ChevronRight className="w-4 h-4" />
</button>
</div>
<div className="space-y-0">
<div className="bg-card rounded-xl border border-card-border p-6 md:p-8" data-testid={`card-horoscope-${sign.name.toLowerCase()}`}>
{(horoscope as any).imageUrl && (
<img
src={(horoscope as any).imageUrl}
alt={`Tageshoroskop ${sign.name}`}
loading="lazy"
className="w-full rounded-xl mb-6 object-cover aspect-[3/2]"
data-testid="img-horoscope-sign"
/>
)}
<div className="flex items-start gap-4 mb-6">
<div className={`w-20 h-20 rounded-2xl ${ec.bg} border ${ec.border} flex items-center justify-center flex-shrink-0`}>
<span className="text-5xl">{sign.symbol}</span>
</div>
<div className="flex-1">
<h3 className={`text-2xl font-bold ${ec.text}`}>{sign.name}</h3>
<p className="text-sm text-muted-foreground">{sign.date}</p>
<div className="flex flex-wrap gap-2 mt-2">
<span className={`text-[10px] font-medium px-2 py-0.5 rounded ${ec.bg} ${ec.text} border ${ec.border}`}>{sign.element}</span>
<span className="text-[10px] bg-muted px-2 py-0.5 rounded text-muted-foreground">Planet: {sign.planet}</span>
<span className="text-[10px] bg-muted px-2 py-0.5 rounded text-muted-foreground">Farbe: {sign.color}</span>
<span className="text-[10px] bg-muted px-2 py-0.5 rounded text-muted-foreground">Stein: {sign.stone}</span>
</div>
</div>
</div>
<div className="grid grid-cols-3 sm:grid-cols-6 gap-3 mb-6">
<div className="bg-muted/30 rounded-lg p-3 text-center">
<Heart className="w-4 h-4 text-red-400 mx-auto mb-1" />
<p className="text-[10px] text-muted-foreground mb-1">Liebe</p>
<StarRating count={getRating(signIndex, "love")} />
</div>
<div className="bg-muted/30 rounded-lg p-3 text-center">
<Briefcase className="w-4 h-4 text-amber-400 mx-auto mb-1" />
<p className="text-[10px] text-muted-foreground mb-1">Beruf</p>
<StarRating count={getRating(signIndex, "career")} />
</div>
<div className="bg-muted/30 rounded-lg p-3 text-center">
<TrendingUp className="w-4 h-4 text-emerald-400 mx-auto mb-1" />
<p className="text-[10px] text-muted-foreground mb-1">Gesundheit</p>
<StarRating count={getRating(signIndex, "health")} />
</div>
<div className="bg-muted/30 rounded-lg p-3 text-center">
<Sparkles className="w-4 h-4 text-violet-400 mx-auto mb-1" />
<p className="text-[10px] text-muted-foreground mb-1">Glückszahlen</p>
<p className="text-xs font-bold text-foreground" data-testid="text-lucky-numbers">{luckyNums.join(", ")}</p>
</div>
<div className="bg-muted/30 rounded-lg p-3 text-center">
<div className="w-4 h-4 rounded-full bg-primary/60 mx-auto mb-1" />
<p className="text-[10px] text-muted-foreground mb-1">Tagesfarbe</p>
<p className="text-xs font-bold text-foreground" data-testid="text-daily-color">{dailyColor}</p>
</div>
<div className="bg-muted/30 rounded-lg p-3 text-center">
<Heart className="w-4 h-4 text-pink-400 mx-auto mb-1" />
<p className="text-[10px] text-muted-foreground mb-1">Kompatibel</p>
<p className="text-[10px] font-medium text-foreground leading-tight" data-testid="text-compatible-signs">{sign.compatible.join(", ")}</p>
</div>
</div>
<div className="flex gap-1 mb-6 bg-muted/30 rounded-lg p-1">
{(["daily", "weekly", "monthly"] as const).map((t) => (
<button
key={t}
onClick={() => setTab(t)}
className={`flex-1 text-xs font-medium py-2 px-3 rounded-md transition-all flex items-center justify-center gap-1.5 ${
tab === t ? "bg-primary text-white shadow" : "text-muted-foreground hover:text-foreground"
}`}
data-testid={`button-tab-${t}`}
>
{t === "daily" && <Star className="w-3 h-3" />}
{t === "weekly" && <Calendar className="w-3 h-3" />}
{t === "monthly" && <CalendarDays className="w-3 h-3" />}
{t === "daily" ? "Heute" : t === "weekly" ? "Woche" : "Monat"}
</button>
))}
</div>
<div style={{ minHeight: "180px" }}>
{tab === "daily" && (
<div className="space-y-5" data-testid="section-horoscope-daily">
<div>
<h3 className="font-semibold text-foreground flex items-center gap-2 mb-2">
<Star className="w-4 h-4 text-primary" /> Allgemein
</h3>
<p className="text-sm text-foreground/80 leading-relaxed" data-testid="text-horoscope-general">{horoscope.general}</p>
</div>
<div>
<h3 className="font-semibold text-foreground flex items-center gap-2 mb-2">
<Heart className="w-4 h-4 text-red-400" /> Liebe & Partnerschaft
</h3>
<p className="text-sm text-foreground/80 leading-relaxed" data-testid="text-horoscope-love">{horoscope.love}</p>
</div>
</div>
)}
{tab === "weekly" && (
<div data-testid="section-horoscope-weekly">
<h3 className="font-semibold text-foreground flex items-center gap-2 mb-2">
<Calendar className="w-4 h-4 text-primary" /> Wochenhoroskop
</h3>
<p className="text-sm text-foreground/80 leading-relaxed" data-testid="text-horoscope-weekly">{horoscope.weekly}</p>
</div>
)}
{tab === "monthly" && (
<div data-testid="section-horoscope-monthly">
<h3 className="font-semibold text-foreground flex items-center gap-2 mb-2">
<CalendarDays className="w-4 h-4 text-primary" /> Monatshoroskop
</h3>
<p className="text-sm text-foreground/80 leading-relaxed" data-testid="text-horoscope-monthly">{horoscope.monthly}</p>
</div>
)}
</div>
</div>
<InArticleAd />
{tab === "daily" && (
<div className="bg-card rounded-xl border border-card-border p-6 md:p-8" data-testid="card-horoscope-detail-2">
<div className="space-y-5">
<div>
<h3 className="font-semibold text-foreground flex items-center gap-2 mb-2">
<Briefcase className="w-4 h-4 text-amber-400" /> Beruf & Finanzen
</h3>
<p className="text-sm text-foreground/80 leading-relaxed">{horoscope.career}</p>
</div>
<div>
<h3 className="font-semibold text-foreground flex items-center gap-2 mb-2">
<TrendingUp className="w-4 h-4 text-emerald-400" /> Gesundheit & Wohlbefinden
</h3>
<p className="text-sm text-foreground/80 leading-relaxed">{horoscope.health}</p>
</div>
<div className="bg-primary/10 border border-primary/20 rounded-lg p-4" data-testid="card-horoscope-tip">
<p className="text-sm text-foreground/90 font-medium flex items-start gap-2">
<Lightbulb className="w-5 h-5 text-primary flex-shrink-0 mt-0.5" />
<span><strong>Tipp des Tages:</strong> {horoscope.tip}</span>
</p>
</div>
</div>
</div>
)}
<InArticleAd />
<div className="bg-card rounded-xl border border-card-border p-6" data-testid="card-horoscope-others">
<h3 className="font-semibold text-foreground mb-4">Weitere Sternzeichen entdecken</h3>
<div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 gap-3">
{SIGNS.filter((_, i) => i !== signIndex).map((s) => {
const origIdx = SIGNS.findIndex((x) => x.name === s.name);
const sEc = ELEMENT_COLORS[s.element];
return (
<button
key={s.name}
onClick={() => onNavigate(origIdx)}
className={`${sEc.bg} hover:opacity-80 border ${sEc.border} rounded-lg p-3 transition-all text-left`}
data-testid={`button-sign-other-${s.name.toLowerCase()}`}
>
<div className="flex items-center gap-2 mb-1">
<span className="text-xl">{s.symbol}</span>
<span className={`font-medium text-sm ${sEc.text}`}>{s.name}</span>
</div>
<p className="text-[10px] text-muted-foreground">{s.date}</p>
</button>
);
})}
</div>
</div>
</div>
</section>
);
}
export default function HoroscopePage() {
const params = useParams<{ sign?: string }>();
const [selected, setSelected] = useState<number | null>(null);
const detailRef = useRef<HTMLDivElement>(null);
const { data: aiHoroscopes = [], isLoading: aiLoading } = useQuery<AIHoroscope[]>({
queryKey: ["/api/horoscopes/today"],
staleTime: 1000 * 60 * 30,
refetchOnWindowFocus: true,
});
useEffect(() => {
if (params.sign) {
const idx = SIGNS.findIndex((s) => s.name.toLowerCase() === params.sign?.toLowerCase());
if (idx >= 0) setSelected(idx);
}
}, [params.sign]);
const selectedSign = selected !== null ? SIGNS[selected] : null;
const selectedAi = selected !== null ? aiHoroscopes.find(h => h.signIndex === selected) : null;
const today = new Date().toLocaleDateString("de-AT", { day: "numeric", month: "long", year: "numeric" });
const metaTitle = selectedSign
? `${selectedSign.symbol} ${selectedSign.name} Horoskop Tages-, Wochen- & Monatshoroskop`
: "Horoskop Tages-, Wochen- & Monatshoroskop für alle Sternzeichen";
const metaDesc = selectedSign
? `${selectedSign.name} Horoskop für heute, ${today}. Tägliches, wöchentliches und monatliches Horoskop für ${selectedSign.name} (${selectedSign.date}). Liebe, Beruf & Gesundheit bei FOLX TV.`
: `Kostenloses Tageshoroskop, Wochenhoroskop & Monatshoroskop für alle 12 Sternzeichen. Liebe, Beruf, Gesundheit Ihr persönliches Horoskop bei FOLX TV, ${today}.`;
const ogImage = "https://folx.tv/images/horoskop-og.jpg";
usePageMeta(metaTitle, metaDesc, {
ogTitle: metaTitle,
ogDescription: metaDesc,
ogImage,
ogType: "website",
});
const handleSelect = (i: number) => {
setSelected(i);
setTimeout(() => {
detailRef.current?.scrollIntoView({ behavior: "smooth", block: "start" });
}, 100);
};
return (
<div className="min-h-screen bg-background">
<Header />
<InterstitialAd forceShow />
<PageSideAds />
<main className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-4">
<div className="flex items-center gap-3 mb-2">
<Link href="/">
<button className="text-muted-foreground hover:text-foreground transition-colors" data-testid="button-back-home">
<ChevronLeft className="w-5 h-5" />
</button>
</Link>
<h1 className="text-2xl font-bold text-foreground flex items-center gap-2" data-testid="text-horoscope-title">
<Star className="w-6 h-6 text-primary" />
Horoskop
</h1>
</div>
<p className="text-muted-foreground text-sm mb-8 ml-8" data-testid="text-horoscope-subtitle">
Entdecken Sie, was die Sterne für Sie bereithalten Ihr persönliches Tageshoroskop für alle zwölf Sternzeichen.
</p>
{aiLoading && (
<div className="flex items-center gap-2 mb-6 text-sm text-primary bg-primary/10 border border-primary/20 rounded-lg px-4 py-2" data-testid="text-horoscope-loading">
<Loader2 className="w-4 h-4 animate-spin" />
Horoskope werden von den Sternen gelesen...
</div>
)}
<InArticleAd />
<SignGrid onSelect={handleSelect} selectedIndex={selected} aiHoroscopes={aiHoroscopes} />
<div ref={detailRef}>
{selected !== null ? (
<SignDetail signIndex={selected} onNavigate={handleSelect} aiHoroscopes={aiHoroscopes} />
) : (
<div className="text-center py-16 bg-card rounded-xl border border-card-border" data-testid="text-horoscope-prompt">
<Sparkles className="w-16 h-16 text-primary mx-auto mb-4" />
<p className="text-muted-foreground text-lg">Wählen Sie oben Ihr Sternzeichen</p>
<p className="text-muted-foreground/60 text-sm mt-1">für Ihr persönliches Tages-, Wochen- und Monatshoroskop</p>
</div>
)}
</div>
</main>
<Footer />
</div>
);
}