Files
anouma/components/booking/MonthCalendar.tsx
T

134 lines
4.6 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";
import { berlinDateKey, formatBerlinDate } from "@/lib/calendar/timezone";
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 berlinDateKey(d);
}
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 = formatBerlinDate(month, { month: "long", year: "numeric" });
const today = dateKey(new Date());
return (
<div className="rounded-3xl bg-white p-4 sm: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-11 w-11 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-11 w-11 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 min-h-11 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 ring-1 ring-inset ring-anouma-mauve-dark/50" : ""}`}
>
<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>
);
}