- Next.js 16 App Router site with the ANOUMA design system - Payload CMS (PostgreSQL) for offers, events, posts and page content - WebRTC video-call system with custom signaling server - SMTP email reminders and booking-request notifications - Customer accounts, calendar-based availability, and booking workflow
115 lines
3.6 KiB
TypeScript
115 lines
3.6 KiB
TypeScript
import Image from "next/image";
|
|
import type { ImageMood } from "@/lib/angebote";
|
|
|
|
/**
|
|
* Renders a photo when `src` is provided, otherwise falls back to a warm,
|
|
* organic gradient placeholder in the given mood. Pages should always pass
|
|
* `src` once real photography exists — no other prop changes are needed.
|
|
*/
|
|
|
|
const moodGradients: Record<ImageMood, string> = {
|
|
plum: "linear-gradient(135deg, #a97070 0%, #66505f 55%, #453238 100%)",
|
|
dustyrose: "linear-gradient(135deg, #ecc8be 0%, #a97070 55%, #8b616d 100%)",
|
|
moss: "linear-gradient(135deg, #d4c1a5 0%, #89937c 55%, #4f5b45 100%)",
|
|
caramel: "linear-gradient(135deg, #efd5c8 0%, #d4c1a5 55%, #a47c60 100%)",
|
|
mauve: "linear-gradient(135deg, #e5c4bc 0%, #8b6f7d 55%, #66505f 100%)",
|
|
sand: "linear-gradient(135deg, #f2e2d6 0%, #e8dcc8 55%, #a89580 100%)",
|
|
rose: "linear-gradient(135deg, #f0e0d3 0%, #e0b4b1 55%, #d19ca1 100%)",
|
|
peach: "linear-gradient(135deg, #f2e2d6 0%, #ecc8be 55%, #e0b4b1 100%)",
|
|
};
|
|
|
|
const moodIcons: Record<ImageMood, React.ReactNode> = {
|
|
plum: (
|
|
<path d="M32 4c8 6 12 14 12 22 0 9-6.5 16-12 16S20 35 20 26c0-8 4-16 12-22Zm0 60V38" />
|
|
),
|
|
dustyrose: <path d="M32 8c9 8 18 18 18 30a18 18 0 1 1-36 0c0-12 9-22 18-30Z" />,
|
|
moss: (
|
|
<path d="M32 60V24M32 24c-10 0-18-8-18-18 10 0 18 8 18 18Zm0 0c0-10 8-18 18-18 0 10-8 18-18 18Z" />
|
|
),
|
|
caramel: (
|
|
<path d="M32 6c6 10 14 20 14 30a14 14 0 1 1-28 0c0-10 8-20 14-30Z" />
|
|
),
|
|
mauve: <path d="M8 32c8-14 16-20 24-20s16 6 24 20c-8 14-16 20-24 20S16 46 8 32Z" />,
|
|
sand: (
|
|
<path d="M6 24c8-6 12-6 20 0s12 6 20 0M6 40c8-6 12-6 20 0s12 6 20 0" />
|
|
),
|
|
rose: (
|
|
<path d="M32 34a10 10 0 1 0 0-20 10 10 0 0 0 0 20Zm0 0a10 10 0 1 1 0 20 10 10 0 0 1 0-20Zm-14-10a10 10 0 1 1 14 10 10 10 0 0 1-14-10Zm28 0a10 10 0 1 0-14 10 10 10 0 0 0 14-10Z" />
|
|
),
|
|
peach: <path d="M8 40c6-20 18-32 24-32s18 12 24 32c-8 8-16 12-24 12s-16-4-24-12Z" />,
|
|
};
|
|
|
|
type ImagePlaceholderProps = {
|
|
mood: ImageMood;
|
|
label: string;
|
|
src?: string;
|
|
alt?: string;
|
|
className?: string;
|
|
shape?: "blob" | "soft";
|
|
priority?: boolean;
|
|
};
|
|
|
|
export function ImagePlaceholder({
|
|
mood,
|
|
label,
|
|
src,
|
|
alt,
|
|
className = "",
|
|
shape = "blob",
|
|
priority = false,
|
|
}: ImagePlaceholderProps) {
|
|
const radius =
|
|
shape === "blob"
|
|
? "rounded-[62%_38%_53%_47%/45%_42%_58%_55%]"
|
|
: "rounded-[2.5rem]";
|
|
|
|
if (src) {
|
|
return (
|
|
<div
|
|
className={`relative overflow-hidden ${radius} ${className}`}
|
|
aria-hidden={alt ? undefined : true}
|
|
>
|
|
<Image
|
|
src={src}
|
|
alt={alt ?? ""}
|
|
fill
|
|
priority={priority}
|
|
className="object-cover"
|
|
sizes="(min-width: 1024px) 50vw, 100vw"
|
|
/>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div
|
|
className={`relative overflow-hidden ${radius} ${className}`}
|
|
style={{ background: moodGradients[mood] }}
|
|
role="img"
|
|
aria-label={label}
|
|
>
|
|
<svg
|
|
className="absolute inset-0 h-full w-full opacity-[0.14] mix-blend-overlay"
|
|
aria-hidden="true"
|
|
>
|
|
<filter id={`grain-${mood}`}>
|
|
<feTurbulence type="fractalNoise" baseFrequency="0.85" numOctaves="2" stitchTiles="stitch" />
|
|
</filter>
|
|
<rect width="100%" height="100%" filter={`url(#grain-${mood})`} />
|
|
</svg>
|
|
<svg
|
|
viewBox="0 0 64 64"
|
|
className="absolute left-1/2 top-1/2 h-16 w-16 -translate-x-1/2 -translate-y-1/2 text-white/40 sm:h-20 sm:w-20"
|
|
fill="none"
|
|
stroke="currentColor"
|
|
strokeWidth="1.25"
|
|
strokeLinecap="round"
|
|
strokeLinejoin="round"
|
|
aria-hidden="true"
|
|
>
|
|
{moodIcons[mood]}
|
|
</svg>
|
|
</div>
|
|
);
|
|
}
|