Initial commit: ANOUMA website with Payload CMS, WebRTC meetings, and booking system

- 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
This commit is contained in:
maro
2026-08-25 16:40:51 +02:00
commit 45261a0461
138 changed files with 23263 additions and 0 deletions
+114
View File
@@ -0,0 +1,114 @@
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>
);
}