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:
@@ -0,0 +1,132 @@
|
||||
"use client";
|
||||
|
||||
import { useMemo, useState } from "react";
|
||||
|
||||
export type CalendarMarker = {
|
||||
date: Date;
|
||||
status: "pending" | "confirmed" | "rejected" | "cancelled" | "past" | "public" | "private";
|
||||
};
|
||||
|
||||
const WEEKDAY_LABELS = ["Mo", "Di", "Mi", "Do", "Fr", "Sa", "So"];
|
||||
|
||||
const markerColor: Record<CalendarMarker["status"], string> = {
|
||||
pending: "bg-anouma-sand",
|
||||
confirmed: "bg-anouma-sage",
|
||||
rejected: "bg-red-400",
|
||||
cancelled: "bg-anouma-taupe",
|
||||
past: "bg-anouma-taupe/50",
|
||||
public: "bg-anouma-rose",
|
||||
private: "bg-anouma-mauve-dark",
|
||||
};
|
||||
|
||||
function startOfMonth(d: Date) {
|
||||
return new Date(d.getFullYear(), d.getMonth(), 1);
|
||||
}
|
||||
function dateKey(d: Date) {
|
||||
return d.toISOString().slice(0, 10);
|
||||
}
|
||||
|
||||
export function MonthCalendar({
|
||||
markers,
|
||||
onSelectDay,
|
||||
selectedDay,
|
||||
}: {
|
||||
markers: CalendarMarker[];
|
||||
onSelectDay?: (day: Date) => void;
|
||||
selectedDay?: Date | null;
|
||||
}) {
|
||||
const [month, setMonth] = useState(() => startOfMonth(new Date()));
|
||||
|
||||
const markersByDay = useMemo(() => {
|
||||
const map = new Map<string, CalendarMarker[]>();
|
||||
for (const marker of markers) {
|
||||
const key = dateKey(marker.date);
|
||||
map.set(key, [...(map.get(key) ?? []), marker]);
|
||||
}
|
||||
return map;
|
||||
}, [markers]);
|
||||
|
||||
const weeks = useMemo(() => {
|
||||
const first = startOfMonth(month);
|
||||
const firstWeekday = (first.getDay() + 6) % 7; // Monday = 0
|
||||
const daysInMonth = new Date(month.getFullYear(), month.getMonth() + 1, 0).getDate();
|
||||
|
||||
const cells: (Date | null)[] = Array(firstWeekday).fill(null);
|
||||
for (let d = 1; d <= daysInMonth; d++) {
|
||||
cells.push(new Date(month.getFullYear(), month.getMonth(), d));
|
||||
}
|
||||
while (cells.length % 7 !== 0) cells.push(null);
|
||||
|
||||
const result: (Date | null)[][] = [];
|
||||
for (let i = 0; i < cells.length; i += 7) result.push(cells.slice(i, i + 7));
|
||||
return result;
|
||||
}, [month]);
|
||||
|
||||
const monthLabel = month.toLocaleDateString("de-DE", { month: "long", year: "numeric" });
|
||||
const today = dateKey(new Date());
|
||||
|
||||
return (
|
||||
<div className="rounded-3xl bg-white p-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setMonth(new Date(month.getFullYear(), month.getMonth() - 1, 1))}
|
||||
aria-label="Vorheriger Monat"
|
||||
className="flex h-9 w-9 items-center justify-center rounded-full text-anouma-plum hover:bg-anouma-cream-light"
|
||||
>
|
||||
‹
|
||||
</button>
|
||||
<p className="font-serif text-lg font-medium capitalize text-anouma-plum">{monthLabel}</p>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setMonth(new Date(month.getFullYear(), month.getMonth() + 1, 1))}
|
||||
aria-label="Nächster Monat"
|
||||
className="flex h-9 w-9 items-center justify-center rounded-full text-anouma-plum hover:bg-anouma-cream-light"
|
||||
>
|
||||
›
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="mt-4 grid grid-cols-7 gap-1 text-center text-xs font-medium uppercase tracking-wide text-anouma-plum/50">
|
||||
{WEEKDAY_LABELS.map((d) => (
|
||||
<div key={d}>{d}</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="mt-1 space-y-1">
|
||||
{weeks.map((week, i) => (
|
||||
<div key={i} className="grid grid-cols-7 gap-1">
|
||||
{week.map((day, j) => {
|
||||
if (!day) return <div key={j} />;
|
||||
const key = dateKey(day);
|
||||
const dayMarkers = markersByDay.get(key) ?? [];
|
||||
const isSelected = selectedDay && dateKey(selectedDay) === key;
|
||||
return (
|
||||
<button
|
||||
key={j}
|
||||
type="button"
|
||||
onClick={() => onSelectDay?.(day)}
|
||||
className={`flex aspect-square flex-col items-center justify-center rounded-xl text-sm transition-colors ${
|
||||
isSelected ? "bg-anouma-mauve-dark text-white" : "text-anouma-plum hover:bg-anouma-cream-light"
|
||||
} ${key === today && !isSelected ? "font-semibold" : ""}`}
|
||||
>
|
||||
<span>{day.getDate()}</span>
|
||||
{dayMarkers.length > 0 && (
|
||||
<span className="mt-0.5 flex gap-0.5">
|
||||
{dayMarkers.slice(0, 3).map((m, k) => (
|
||||
<span
|
||||
key={k}
|
||||
className={`h-1.5 w-1.5 rounded-full ${isSelected ? "bg-white" : markerColor[m.status]}`}
|
||||
/>
|
||||
))}
|
||||
</span>
|
||||
)}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user