"use client"; import { useMemo, useState } from "react"; import Link from "next/link"; import { type Stop, type StopStatus, formatMonthYear, formatTime12, getStopDate, getStopStatus, isSameDay, } from "./types"; type Props = { stops: Stop[]; }; const WEEKDAY_LABELS = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"]; const STATUS_STYLES: Record = { active: { bg: "bg-[var(--admin-accent-light)]", text: "text-[var(--admin-accent-text)]", dot: "bg-[var(--admin-accent-dot)]", border: "border-[var(--admin-accent)]/40", }, draft: { bg: "bg-amber-50", text: "text-amber-800", dot: "bg-amber-500", border: "border-amber-300", }, inactive: { bg: "bg-stone-100", text: "text-stone-500", dot: "bg-stone-400", border: "border-stone-300", }, }; function buildMonthGrid(viewYear: number, viewMonth: number) { // Sunday-first 6-row grid const first = new Date(viewYear, viewMonth, 1); const startWeekday = first.getDay(); const daysInMonth = new Date(viewYear, viewMonth + 1, 0).getDate(); const cells: { date: Date; inMonth: boolean }[] = []; // Leading days from previous month for (let i = startWeekday - 1; i >= 0; i--) { const d = new Date(viewYear, viewMonth, -i); cells.push({ date: d, inMonth: false }); } for (let d = 1; d <= daysInMonth; d++) { cells.push({ date: new Date(viewYear, viewMonth, d), inMonth: true }); } // Trailing days to fill 6 rows = 42 cells while (cells.length < 42) { const last = cells[cells.length - 1].date; const next = new Date(last); next.setDate(last.getDate() + 1); cells.push({ date: next, inMonth: false }); } return cells; } function ymd(d: Date): string { const y = d.getFullYear(); const m = String(d.getMonth() + 1).padStart(2, "0"); const day = String(d.getDate()).padStart(2, "0"); return `${y}-${m}-${day}`; } export default function StopsCalendar({ stops }: Props) { const today = useMemo(() => { const d = new Date(); d.setHours(0, 0, 0, 0); return d; }, []); const [view, setView] = useState({ year: today.getFullYear(), month: today.getMonth() }); const [selectedDate, setSelectedDate] = useState(ymd(today)); const cells = useMemo(() => buildMonthGrid(view.year, view.month), [view]); const stopsByDate = useMemo(() => { const map = new Map(); for (const s of stops) { const d = getStopDate(s); if (!d) continue; const k = ymd(d); const arr = map.get(k) ?? []; arr.push(s); map.set(k, arr); } // sort each bucket by time for (const arr of map.values()) { arr.sort((a, b) => (a.time || "").localeCompare(b.time || "")); } return map; }, [stops]); const visibleStopsByDate = useMemo(() => { const map = new Map(); for (const [k, arr] of stopsByDate.entries()) { map.set( k, arr.filter((s) => getStopStatus(s) !== "inactive" || arr.length <= 6) ); } return map; }, [stopsByDate]); const monthStopsCount = useMemo(() => { let count = 0; for (const cell of cells) { if (!cell.inMonth) continue; count += stopsByDate.get(ymd(cell.date))?.length ?? 0; } return count; }, [cells, stopsByDate]); const upcomingWeek = useMemo(() => { const end = new Date(today); end.setDate(end.getDate() + 7); let count = 0; for (const s of stops) { const d = getStopDate(s); if (!d) continue; if (d >= today && d < end && getStopStatus(s) !== "inactive") count++; } return count; }, [stops, today]); const goPrev = () => { setView((v) => { const m = v.month - 1; if (m < 0) return { year: v.year - 1, month: 11 }; return { ...v, month: m }; }); setSelectedDate(null); }; const goNext = () => { setView((v) => { const m = v.month + 1; if (m > 11) return { year: v.year + 1, month: 0 }; return { ...v, month: m }; }); setSelectedDate(null); }; const goToday = () => { setView({ year: today.getFullYear(), month: today.getMonth() }); setSelectedDate(ymd(today)); }; const monthLabel = formatMonthYear(new Date(view.year, view.month, 1)); const isCurrentMonth = view.year === today.getFullYear() && view.month === today.getMonth(); const selectedDayStops = selectedDate ? stopsByDate.get(selectedDate) ?? [] : []; return (
{/* Calendar card */}
{/* Decorative paper texture */}
\")", }} /> {/* Header */}

{monthLabel.split(" ")[0]} {" "} {monthLabel.split(" ")[1]}

{monthStopsCount} stop{monthStopsCount === 1 ? "" : "s"} scheduled
{/* Weekday header */}
{WEEKDAY_LABELS.map((d, i) => (
{d}
))}
{/* Grid */}
{cells.map((cell, idx) => { const k = ymd(cell.date); const dayStops = stopsByDate.get(k) ?? []; const visibleStops = visibleStopsByDate.get(k) ?? []; const isToday = isSameDay(cell.date, today); const isSelected = k === selectedDate; const isWeekend = cell.date.getDay() === 0 || cell.date.getDay() === 6; const overflow = dayStops.length - visibleStops.length; return ( ); })}
{/* Legend */}
{(["active", "draft", "inactive"] as StopStatus[]).map((s) => { const st = STATUS_STYLES[s]; return (
{s}
); })}
{upcomingWeek} {" "} upcoming in 7d
{/* Day detail panel */}
); }