Replaces em dashes with en dashes across all hardcoded copy, emails, and seed content, then reworks the site's visual language so it reads as deliberately designed rather than assembled from generic components: - New ArrowLink component replaces ad hoc "Mehr erfahren ->" links and ghost buttons; one shared FormField (TextField/TextAreaField) replaces 7 near-duplicate field styles across every form - Navbar loses its glassmorphism blur for a solid warm background, gets a subtle active-link underline and a softer, warmer mega-menu - PageHeader supports per-page blob tone/position and a compact mode instead of an identical band on every interior page - Homepage: the core offer is featured large instead of four equal cards, the philosophy section gets a single calmer accent blob, blog teasers now show their cover image like the real /aktuelles page does - Booking calendar and widget: 44px touch targets throughout (month nav, day cells, time slots, cancel action), a visible "today" ring, and responsive padding so the calendar isn't cramped on small screens - Quieter, warmer empty states on /termine and /aktuelles No functional changes: all interaction logic (nav menus, forms, booking, CMS data fetching) is untouched, only markup and styling.
133 lines
4.6 KiB
TypeScript
133 lines
4.6 KiB
TypeScript
"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-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>
|
||
);
|
||
}
|