"use client"; import { useEffect, useMemo, useState } from "react"; import { useI18n } from "./I18nProvider"; type MonthView = { label: string; days: { label: string; date: string; blocked: boolean; isFiller: boolean }[]; }; function buildMonths( monthCount: number, blocked: Set, startYear: number, startMonth: number, ): MonthView[] { const months: MonthView[] = []; const base = new Date(Date.UTC(startYear, startMonth, 1)); for (let i = 0; i < monthCount; i += 1) { const monthDate = new Date(base); monthDate.setUTCMonth(base.getUTCMonth() + i); const year = monthDate.getUTCFullYear(); const month = monthDate.getUTCMonth(); const label = monthDate.toLocaleDateString(undefined, { month: "long", year: "numeric", }); const firstDay = new Date(Date.UTC(year, month, 1)); const startWeekday = firstDay.getUTCDay(); // 0=Sun const daysInMonth = new Date(Date.UTC(year, month + 1, 0)).getUTCDate(); const days: MonthView["days"] = []; for (let f = 0; f < startWeekday; f += 1) { days.push({ label: "", date: "", blocked: false, isFiller: true }); } for (let d = 1; d <= daysInMonth; d += 1) { const date = new Date(Date.UTC(year, month, d)); const iso = date.toISOString().slice(0, 10); days.push({ label: String(d), date: iso, blocked: blocked.has(iso), isFiller: false, }); } months.push({ label, days }); } return months; } type AvailabilityResponse = { blockedDates: string[] }; export default function AvailabilityCalendar({ listingId, hasCalendar, }: { listingId: string; hasCalendar: boolean; }) { const { t, locale } = useI18n(); const today = useMemo(() => new Date(), []); const [month, setMonth] = useState(today.getUTCMonth()); const [year, setYear] = useState(today.getUTCFullYear()); const [blockedDates, setBlockedDates] = useState([]); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); const monthCount = 1; const blockedSet = useMemo(() => new Set(blockedDates), [blockedDates]); const monthViews = useMemo( () => buildMonths(monthCount, blockedSet, year, month), [monthCount, blockedSet, year, month], ); useEffect(() => { if (!hasCalendar) return; setLoading(true); setError(null); const controller = new AbortController(); const params = new URLSearchParams({ month: String(month), year: String(year), months: String(monthCount), refresh: "1", }); fetch(`/api/listings/${listingId}/availability?${params.toString()}`, { cache: "no-store", signal: controller.signal, }) .then(async (res) => { const data: AvailabilityResponse = await res.json(); if (!res.ok) throw new Error( (data as any)?.error || "Failed to load availability", ); return data; }) .then((data) => { setBlockedDates( Array.isArray(data.blockedDates) ? data.blockedDates : [], ); }) .catch((err) => { if (err.name === "AbortError") return; setError(err.message || "Failed to load availability"); }) .finally(() => setLoading(false)); return () => controller.abort(); }, [listingId, hasCalendar, month, year, monthCount]); function shiftMonth(delta: number) { const next = new Date(Date.UTC(year, month, 1)); next.setUTCMonth(next.getUTCMonth() + delta); setMonth(next.getUTCMonth()); setYear(next.getUTCFullYear()); } const monthOptions = useMemo( () => Array.from({ length: 12 }, (_, m) => ({ value: m, label: new Date(Date.UTC(2020, m, 1)).toLocaleString(locale, { month: "long", }), })), [locale], ); const yearOptions = useMemo(() => { const current = today.getUTCFullYear(); return Array.from({ length: 5 }, (_, i) => current - 1 + i); }, [today]); return (
{t("availabilityTitle")}
{t("availabilityLegendBooked")}
{error ? (
{error}
) : null} {monthViews.map((monthView) => (
{["S", "M", "T", "W", "T", "F", "S"].map((d) => (
{d}
))} {monthView.days.map((day, idx) => (
{day.label}
))}
))}
); }