Restored to '7aa09373aacdcc1083018c718e608e30b6be2b33'
Replit-Restored-To: 7aa09373aa
This commit is contained in:
@@ -17,27 +17,9 @@ export default function Footer() {
|
||||
<ul className="space-y-2 text-sm">
|
||||
<li>
|
||||
<Link href="/category/News">
|
||||
<span className="text-muted-foreground cursor-pointer hover:text-primary transition-colors" data-testid="link-footer-news">News</span>
|
||||
</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link href="/videos">
|
||||
<span className="text-muted-foreground cursor-pointer hover:text-primary transition-colors" data-testid="link-footer-video">Video</span>
|
||||
</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link href="/gallery">
|
||||
<span className="text-muted-foreground cursor-pointer hover:text-primary transition-colors" data-testid="link-footer-gallery">Fotogalerie</span>
|
||||
</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link href="/horoskop">
|
||||
<span className="text-muted-foreground cursor-pointer hover:text-primary transition-colors" data-testid="link-footer-horoskop">Horoskop</span>
|
||||
</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link href="/rezepte">
|
||||
<span className="text-muted-foreground cursor-pointer hover:text-primary transition-colors" data-testid="link-footer-rezepte">Rezepte</span>
|
||||
<span className="text-muted-foreground cursor-pointer hover:text-primary transition-colors" data-testid="link-footer-news">
|
||||
News
|
||||
</span>
|
||||
</Link>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
@@ -8,9 +8,6 @@ const navItems = [
|
||||
{ label: "Start", href: "/" },
|
||||
{ label: "News", href: "/category/News" },
|
||||
{ label: "Video", href: "/videos" },
|
||||
{ label: "Galerie", href: "/gallery" },
|
||||
{ label: "Horoskop", href: "/horoskop" },
|
||||
{ label: "Rezepte", href: "/rezepte" },
|
||||
];
|
||||
|
||||
export default function Header() {
|
||||
|
||||
@@ -1,108 +0,0 @@
|
||||
import { useState, useEffect, useCallback } from "react";
|
||||
import { useLocation } from "wouter";
|
||||
import { Star, Heart, Briefcase, TrendingUp, ChevronLeft, ChevronRight } from "lucide-react";
|
||||
import { SIGNS, ELEMENT_COLORS, getHoroscope, getRating } from "@/lib/horoscope-data";
|
||||
|
||||
function MiniStars({ count, max = 5 }: { count: number; max?: number }) {
|
||||
return (
|
||||
<div className="flex gap-px">
|
||||
{Array.from({ length: max }).map((_, i) => (
|
||||
<div key={i} className={`w-1.5 h-1.5 rounded-full ${i < count ? "bg-amber-400" : "bg-white/10"}`} />
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function HoroscopeWidget() {
|
||||
const [, navigate] = useLocation();
|
||||
const [index, setIndex] = useState(0);
|
||||
const [paused, setPaused] = useState(false);
|
||||
|
||||
const next = useCallback(() => setIndex((i) => (i + 1) % SIGNS.length), []);
|
||||
const prev = useCallback(() => setIndex((i) => (i - 1 + SIGNS.length) % SIGNS.length), []);
|
||||
|
||||
useEffect(() => {
|
||||
if (paused) return;
|
||||
const timer = setInterval(next, 6000);
|
||||
return () => clearInterval(timer);
|
||||
}, [paused, next]);
|
||||
|
||||
const sign = SIGNS[index];
|
||||
const horoscope = getHoroscope(index);
|
||||
|
||||
return (
|
||||
<div
|
||||
className="rounded-lg border border-card-border overflow-hidden h-full w-full cursor-pointer group hover:border-primary/50 transition-colors flex flex-col"
|
||||
style={{ background: "linear-gradient(135deg, hsl(250 30% 14%), hsl(270 25% 10%))" }}
|
||||
onMouseEnter={() => setPaused(true)}
|
||||
onMouseLeave={() => setPaused(false)}
|
||||
onClick={() => navigate("/horoskop")}
|
||||
data-testid="widget-horoscope"
|
||||
>
|
||||
<div className="p-3 flex items-center justify-between border-b border-white/10 flex-shrink-0">
|
||||
<div className="flex items-center gap-2">
|
||||
<Star className="w-4 h-4 text-amber-400" />
|
||||
<h3 className="font-bold text-white text-sm">Horoskop</h3>
|
||||
</div>
|
||||
<div className="flex gap-1">
|
||||
<button
|
||||
onClick={(e) => { e.stopPropagation(); prev(); }}
|
||||
className="w-5 h-5 rounded-full bg-white/10 hover:bg-white/20 flex items-center justify-center transition-colors"
|
||||
data-testid="button-horoscope-prev"
|
||||
>
|
||||
<ChevronLeft className="w-3 h-3 text-white/70" />
|
||||
</button>
|
||||
<button
|
||||
onClick={(e) => { e.stopPropagation(); next(); }}
|
||||
className="w-5 h-5 rounded-full bg-white/10 hover:bg-white/20 flex items-center justify-center transition-colors"
|
||||
data-testid="button-horoscope-next"
|
||||
>
|
||||
<ChevronRight className="w-3 h-3 text-white/70" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex-1 flex flex-col">
|
||||
<div className="p-4 flex items-center gap-3">
|
||||
<div className="w-14 h-14 rounded-xl bg-white/5 border border-white/10 flex items-center justify-center flex-shrink-0">
|
||||
<span className="text-3xl">{sign.symbol}</span>
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className="font-bold text-lg text-white">{sign.name}</p>
|
||||
<p className="text-[10px] text-white/50">{sign.date}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-3 gap-1 px-3 py-1.5 border-t border-b border-white/5">
|
||||
<div className="flex flex-col items-center gap-0.5">
|
||||
<Heart className="w-3 h-3 text-rose-400" />
|
||||
<MiniStars count={getRating(index, "love")} />
|
||||
</div>
|
||||
<div className="flex flex-col items-center gap-0.5">
|
||||
<Briefcase className="w-3 h-3 text-amber-400" />
|
||||
<MiniStars count={getRating(index, "career")} />
|
||||
</div>
|
||||
<div className="flex flex-col items-center gap-0.5">
|
||||
<TrendingUp className="w-3 h-3 text-emerald-400" />
|
||||
<MiniStars count={getRating(index, "health")} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="px-3 pb-3 pt-2 flex-1 flex flex-col justify-between">
|
||||
<p className="text-xs text-white/60 leading-relaxed line-clamp-5">{horoscope.general}</p>
|
||||
<p className="text-[10px] text-amber-400 mt-2 group-hover:underline">Mehr lesen</p>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-center gap-1 pb-3">
|
||||
{SIGNS.map((_, i) => (
|
||||
<button
|
||||
key={i}
|
||||
onClick={(e) => { e.stopPropagation(); setIndex(i); }}
|
||||
className={`w-1.5 h-1.5 rounded-full transition-all ${i === index ? "bg-amber-400 w-3" : "bg-white/15 hover:bg-white/30"}`}
|
||||
data-testid={`button-horoscope-dot-${i}`}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,95 +0,0 @@
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { useState, useEffect, useCallback } from "react";
|
||||
import { Newspaper, ExternalLink } from "lucide-react";
|
||||
|
||||
interface NewsItem {
|
||||
title: string;
|
||||
link: string;
|
||||
source: string;
|
||||
pubDate: string;
|
||||
}
|
||||
|
||||
const VISIBLE_COUNT = 5;
|
||||
|
||||
export function NewsWidget() {
|
||||
const { data: news, isLoading } = useQuery<NewsItem[]>({
|
||||
queryKey: ["/api/news-feed"],
|
||||
});
|
||||
|
||||
const [offset, setOffset] = useState(0);
|
||||
const [paused, setPaused] = useState(false);
|
||||
|
||||
const items = news || [];
|
||||
const total = items.length;
|
||||
|
||||
const advance = useCallback(() => {
|
||||
if (total <= VISIBLE_COUNT) return;
|
||||
setOffset((o) => (o + 1) % total);
|
||||
}, [total]);
|
||||
|
||||
useEffect(() => {
|
||||
if (paused || total <= VISIBLE_COUNT) return;
|
||||
const timer = setInterval(advance, 5000);
|
||||
return () => clearInterval(timer);
|
||||
}, [paused, advance, total]);
|
||||
|
||||
const visible: NewsItem[] = [];
|
||||
for (let i = 0; i < Math.min(VISIBLE_COUNT, total); i++) {
|
||||
visible.push(items[(offset + i) % total]);
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className="bg-card rounded-lg border border-card-border overflow-hidden h-full flex flex-col"
|
||||
onMouseEnter={() => setPaused(true)}
|
||||
onMouseLeave={() => setPaused(false)}
|
||||
data-testid="widget-news"
|
||||
>
|
||||
<div className="p-3 flex items-center gap-2 border-b border-card-border flex-shrink-0">
|
||||
<Newspaper className="w-4 h-4 text-primary" />
|
||||
<h3 className="font-bold text-card-foreground text-sm">Musiknachrichten</h3>
|
||||
</div>
|
||||
<div className="p-3 flex-1 flex flex-col justify-between">
|
||||
{visible.map((item, i) => (
|
||||
<a
|
||||
key={`${offset}-${i}`}
|
||||
href={item.link}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="block group cursor-pointer"
|
||||
data-testid={`link-news-${i}`}
|
||||
>
|
||||
<div className="flex items-start gap-2">
|
||||
<div className="flex-1 min-w-0">
|
||||
<h4 className="text-xs font-medium text-card-foreground line-clamp-2 group-hover:text-primary transition-colors leading-snug">
|
||||
{item.title}
|
||||
</h4>
|
||||
<div className="flex items-center gap-1.5 mt-1">
|
||||
<span className="text-[10px] text-primary font-medium">{item.source}</span>
|
||||
<span className="text-[10px] text-muted-foreground">{item.pubDate}</span>
|
||||
</div>
|
||||
</div>
|
||||
<ExternalLink className="w-3 h-3 text-muted-foreground flex-shrink-0 mt-0.5 opacity-0 group-hover:opacity-100 transition-opacity" />
|
||||
</div>
|
||||
{i < VISIBLE_COUNT - 1 && <div className="border-b border-card-border mt-2 mb-2" />}
|
||||
</a>
|
||||
))}
|
||||
{isLoading && (
|
||||
<div className="space-y-3 flex-1">
|
||||
{Array.from({ length: VISIBLE_COUNT }).map((_, i) => (
|
||||
<div key={i} className="space-y-1.5">
|
||||
<div className="h-3 bg-muted animate-pulse rounded w-full" />
|
||||
<div className="h-3 bg-muted animate-pulse rounded w-3/4" />
|
||||
<div className="h-2 bg-muted animate-pulse rounded w-1/3 mt-1" />
|
||||
{i < VISIBLE_COUNT - 1 && <div className="border-b border-card-border mt-3" />}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
{!isLoading && total === 0 && (
|
||||
<p className="text-xs text-muted-foreground text-center py-2">Keine Nachrichten verfügbar</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,255 +0,0 @@
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { useState, useEffect, useCallback } from "react";
|
||||
import { ChevronLeft, ChevronRight, X, Images, Maximize2 } from "lucide-react";
|
||||
|
||||
interface GalleryImage {
|
||||
folder: string;
|
||||
fileName: string;
|
||||
thumb: string;
|
||||
large: string;
|
||||
}
|
||||
|
||||
function Lightbox({
|
||||
images,
|
||||
startIndex,
|
||||
onClose,
|
||||
}: {
|
||||
images: GalleryImage[];
|
||||
startIndex: number;
|
||||
onClose: () => void;
|
||||
}) {
|
||||
const [index, setIndex] = useState(startIndex);
|
||||
|
||||
const prev = useCallback(() => setIndex((i) => (i - 1 + images.length) % images.length), [images.length]);
|
||||
const next = useCallback(() => setIndex((i) => (i + 1) % images.length), [images.length]);
|
||||
|
||||
useEffect(() => {
|
||||
const handler = (e: KeyboardEvent) => {
|
||||
if (e.key === "Escape") onClose();
|
||||
if (e.key === "ArrowLeft") prev();
|
||||
if (e.key === "ArrowRight") next();
|
||||
};
|
||||
window.addEventListener("keydown", handler);
|
||||
return () => window.removeEventListener("keydown", handler);
|
||||
}, [onClose, prev, next]);
|
||||
|
||||
return (
|
||||
<div
|
||||
className="fixed inset-0 z-50 bg-black/95 flex items-center justify-center"
|
||||
onClick={onClose}
|
||||
data-testid="lightbox-overlay"
|
||||
>
|
||||
<button
|
||||
onClick={(e) => { e.stopPropagation(); onClose(); }}
|
||||
className="absolute top-4 right-4 text-white/70 hover:text-white z-50 p-2"
|
||||
data-testid="button-lightbox-close"
|
||||
>
|
||||
<X className="w-8 h-8" />
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={(e) => { e.stopPropagation(); prev(); }}
|
||||
className="absolute left-2 md:left-6 text-white/70 hover:text-white z-50 p-2"
|
||||
data-testid="button-lightbox-prev"
|
||||
>
|
||||
<ChevronLeft className="w-10 h-10" />
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={(e) => { e.stopPropagation(); next(); }}
|
||||
className="absolute right-2 md:right-6 text-white/70 hover:text-white z-50 p-2"
|
||||
data-testid="button-lightbox-next"
|
||||
>
|
||||
<ChevronRight className="w-10 h-10" />
|
||||
</button>
|
||||
|
||||
<div className="max-w-[90vw] max-h-[85vh] flex items-center justify-center" onClick={(e) => e.stopPropagation()}>
|
||||
<img
|
||||
src={images[index].large}
|
||||
alt={images[index].fileName}
|
||||
className="max-w-full max-h-[85vh] object-contain rounded-lg"
|
||||
data-testid="img-lightbox"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="absolute bottom-4 text-white/60 text-sm" data-testid="text-lightbox-counter">
|
||||
{index + 1} / {images.length}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function SingleImageCarousel({
|
||||
images,
|
||||
autoPlay = true,
|
||||
interval = 5000,
|
||||
onExpand,
|
||||
}: {
|
||||
images: GalleryImage[];
|
||||
autoPlay?: boolean;
|
||||
interval?: number;
|
||||
onExpand?: (index: number) => void;
|
||||
}) {
|
||||
const [index, setIndex] = useState(0);
|
||||
const [paused, setPaused] = useState(false);
|
||||
|
||||
const prev = useCallback(() => setIndex((i) => (i - 1 + images.length) % images.length), [images.length]);
|
||||
const next = useCallback(() => setIndex((i) => (i + 1) % images.length), [images.length]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!autoPlay || paused || images.length <= 1) return;
|
||||
const timer = setInterval(next, interval);
|
||||
return () => clearInterval(timer);
|
||||
}, [autoPlay, paused, next, interval, images.length]);
|
||||
|
||||
if (images.length === 0) return null;
|
||||
|
||||
return (
|
||||
<div
|
||||
className="relative w-full h-full"
|
||||
onMouseEnter={() => setPaused(true)}
|
||||
onMouseLeave={() => setPaused(false)}
|
||||
>
|
||||
<div className="relative w-full aspect-[4/5] overflow-hidden rounded-b-lg">
|
||||
<img
|
||||
src={images[index].large || images[index].thumb}
|
||||
alt={images[index].fileName}
|
||||
className="w-full h-full object-cover transition-opacity duration-500"
|
||||
style={{ objectPosition: "center 25%" }}
|
||||
loading="lazy"
|
||||
data-testid="img-gallery-current"
|
||||
/>
|
||||
|
||||
<div className="absolute inset-0 bg-gradient-to-t from-black/40 via-transparent to-transparent" />
|
||||
|
||||
<button
|
||||
onClick={prev}
|
||||
className="absolute left-1.5 top-1/2 -translate-y-1/2 w-7 h-7 rounded-full bg-black/50 hover:bg-black/70 flex items-center justify-center text-white/80 hover:text-white transition-all"
|
||||
data-testid="button-gallery-prev"
|
||||
>
|
||||
<ChevronLeft className="w-4 h-4" />
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={next}
|
||||
className="absolute right-1.5 top-1/2 -translate-y-1/2 w-7 h-7 rounded-full bg-black/50 hover:bg-black/70 flex items-center justify-center text-white/80 hover:text-white transition-all"
|
||||
data-testid="button-gallery-next"
|
||||
>
|
||||
<ChevronRight className="w-4 h-4" />
|
||||
</button>
|
||||
|
||||
{onExpand && (
|
||||
<button
|
||||
onClick={() => onExpand(index)}
|
||||
className="absolute top-2 right-2 w-7 h-7 rounded-full bg-black/50 hover:bg-black/70 flex items-center justify-center text-white/80 hover:text-white transition-all"
|
||||
data-testid="button-gallery-expand"
|
||||
>
|
||||
<Maximize2 className="w-3.5 h-3.5" />
|
||||
</button>
|
||||
)}
|
||||
|
||||
<div className="absolute bottom-2 left-0 right-0 flex justify-center gap-1">
|
||||
{images.slice(0, Math.min(images.length, 20)).map((_, i) => (
|
||||
<button
|
||||
key={i}
|
||||
onClick={() => setIndex(i)}
|
||||
className={`w-1.5 h-1.5 rounded-full transition-all ${i === index ? "bg-white w-3" : "bg-white/40 hover:bg-white/60"}`}
|
||||
data-testid={`button-gallery-dot-${i}`}
|
||||
/>
|
||||
))}
|
||||
{images.length > 20 && (
|
||||
<span className="text-white/50 text-[9px] ml-1">+{images.length - 20}</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function PhotoGalleryWidget() {
|
||||
const { data: images, isLoading } = useQuery<GalleryImage[]>({
|
||||
queryKey: ["/api/gallery"],
|
||||
});
|
||||
|
||||
const [lightboxIndex, setLightboxIndex] = useState<number | null>(null);
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="bg-card rounded-lg border border-card-border overflow-hidden h-full" data-testid="widget-gallery-loading">
|
||||
<div className="p-3 flex items-center gap-2 border-b border-card-border">
|
||||
<Images className="w-4 h-4 text-primary" />
|
||||
<h3 className="font-bold text-card-foreground text-sm">Fotogalerie</h3>
|
||||
</div>
|
||||
<div className="aspect-[4/5] bg-muted animate-pulse" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!images || images.length === 0) return null;
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="bg-card rounded-lg border border-card-border overflow-hidden h-full" data-testid="widget-gallery">
|
||||
<div className="p-3 flex items-center gap-2 border-b border-card-border">
|
||||
<Images className="w-4 h-4 text-primary" />
|
||||
<h3 className="font-bold text-card-foreground text-sm">Fotogalerie</h3>
|
||||
</div>
|
||||
<SingleImageCarousel
|
||||
images={images}
|
||||
autoPlay={true}
|
||||
interval={10000}
|
||||
onExpand={(i) => setLightboxIndex(i)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{lightboxIndex !== null && (
|
||||
<Lightbox
|
||||
images={images}
|
||||
startIndex={lightboxIndex}
|
||||
onClose={() => setLightboxIndex(null)}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default function GalleryPage() {
|
||||
const { data: images, isLoading } = useQuery<GalleryImage[]>({
|
||||
queryKey: ["/api/gallery"],
|
||||
});
|
||||
|
||||
const [lightboxIndex, setLightboxIndex] = useState<number | null>(null);
|
||||
|
||||
return (
|
||||
<div data-testid="page-gallery">
|
||||
{isLoading ? (
|
||||
<div className="max-w-3xl mx-auto">
|
||||
<div className="aspect-[4/3] bg-card rounded-lg animate-pulse" />
|
||||
</div>
|
||||
) : images && images.length > 0 ? (
|
||||
<div className="max-w-3xl mx-auto">
|
||||
<div className="bg-card rounded-lg border border-card-border overflow-hidden">
|
||||
<SingleImageCarousel
|
||||
images={images}
|
||||
autoPlay={false}
|
||||
onExpand={(i) => setLightboxIndex(i)}
|
||||
/>
|
||||
</div>
|
||||
<p className="text-center text-muted-foreground text-sm mt-3" data-testid="text-gallery-count">
|
||||
{images.length} Fotos
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<p className="text-center text-muted-foreground">Keine Fotos vorhanden</p>
|
||||
)}
|
||||
|
||||
{lightboxIndex !== null && images && (
|
||||
<Lightbox
|
||||
images={images}
|
||||
startIndex={lightboxIndex}
|
||||
onClose={() => setLightboxIndex(null)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,57 +0,0 @@
|
||||
import { useState } from "react";
|
||||
import { useLocation } from "wouter";
|
||||
import { ChefHat, Clock, Users } from "lucide-react";
|
||||
|
||||
interface WidgetRecipe {
|
||||
title: string;
|
||||
image: string;
|
||||
time: string;
|
||||
servings: string;
|
||||
}
|
||||
|
||||
const WIDGET_RECIPES: WidgetRecipe[] = [
|
||||
{ title: "Kaiserschmarrn", image: "/uploads/recipe-kaiserschmarrn.png", time: "25 Min.", servings: "2" },
|
||||
{ title: "Wiener Schnitzel", image: "/uploads/recipe-wiener-schnitzel.png", time: "30 Min.", servings: "4" },
|
||||
{ title: "Apfelstrudel", image: "/uploads/recipe-apfelstrudel.png", time: "60 Min.", servings: "6" },
|
||||
{ title: "Schweinshaxe", image: "/uploads/recipe-schweinshaxe.png", time: "180 Min.", servings: "4" },
|
||||
{ title: "Käsespätzle", image: "/uploads/recipe-kaesespaetzle.png", time: "45 Min.", servings: "4" },
|
||||
{ title: "Sachertorte", image: "/uploads/recipe-sachertorte.png", time: "90 Min.", servings: "8" },
|
||||
{ title: "Maultaschen", image: "/uploads/recipe-maultaschen.png", time: "90 Min.", servings: "4" },
|
||||
{ title: "Schlutzkrapfen", image: "/uploads/recipe-schlutzkrapfen.png", time: "60 Min.", servings: "4" },
|
||||
];
|
||||
|
||||
export function RecipeWidget() {
|
||||
const [, navigate] = useLocation();
|
||||
const dayIndex = Math.floor(Date.now() / 86400000) % WIDGET_RECIPES.length;
|
||||
const recipe = WIDGET_RECIPES[dayIndex];
|
||||
|
||||
return (
|
||||
<button
|
||||
onClick={() => navigate("/rezepte")}
|
||||
className="bg-card rounded-lg border border-card-border overflow-hidden h-full w-full text-left cursor-pointer group hover:border-primary/50 transition-colors flex flex-col"
|
||||
data-testid="widget-recipe"
|
||||
>
|
||||
<div className="p-3 flex items-center gap-2 border-b border-card-border flex-shrink-0">
|
||||
<ChefHat className="w-4 h-4 text-primary" />
|
||||
<h3 className="font-bold text-card-foreground text-sm">Rezept des Tages</h3>
|
||||
</div>
|
||||
<div className="relative flex-1">
|
||||
<img
|
||||
src={recipe.image}
|
||||
alt={recipe.title}
|
||||
className="w-full h-full object-cover transition-transform duration-500 group-hover:scale-105"
|
||||
loading="lazy"
|
||||
/>
|
||||
<div className="absolute inset-0 bg-gradient-to-t from-black/80 via-black/20 to-transparent" />
|
||||
<div className="absolute bottom-0 left-0 right-0 p-3">
|
||||
<h4 className="text-white font-bold text-base">{recipe.title}</h4>
|
||||
<p className="text-white/60 text-xs mt-0.5 flex items-center gap-2">
|
||||
<span className="flex items-center gap-1"><Clock className="w-3 h-3" />{recipe.time}</span>
|
||||
<span className="flex items-center gap-1"><Users className="w-3 h-3" />{recipe.servings}</span>
|
||||
</p>
|
||||
<p className="text-[10px] text-primary mt-1 group-hover:underline">Alle Rezepte ansehen</p>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user