'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}
))}
))}
); }