Files
anouma/components/booking/MonthCalendar.tsx
T
maro 45261a0461 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
2026-08-25 16:40:51 +02:00

133 lines
4.5 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.
"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>
);
}