Show single-month availability calendar with nav controls
This commit is contained in:
parent
7c48d0e086
commit
a33470435a
2 changed files with 44 additions and 45 deletions
|
|
@ -5,12 +5,12 @@ import { expandBlockedDates, getCalendarRanges } from '../../../../../lib/calend
|
||||||
export async function GET(_: Request, { params }: { params: { id: string } }) {
|
export async function GET(_: Request, { params }: { params: { id: string } }) {
|
||||||
const monthParam = Number(new URL(_.url).searchParams.get('month') ?? new Date().getUTCMonth());
|
const monthParam = Number(new URL(_.url).searchParams.get('month') ?? new Date().getUTCMonth());
|
||||||
const yearParam = Number(new URL(_.url).searchParams.get('year') ?? new Date().getUTCFullYear());
|
const yearParam = Number(new URL(_.url).searchParams.get('year') ?? new Date().getUTCFullYear());
|
||||||
const monthsParam = Math.min(Number(new URL(_.url).searchParams.get('months') ?? 2), 12);
|
const monthsParam = Math.min(Number(new URL(_.url).searchParams.get('months') ?? 1), 12);
|
||||||
const forceRefresh = new URL(_.url).searchParams.get('refresh') === '1';
|
const forceRefresh = new URL(_.url).searchParams.get('refresh') === '1';
|
||||||
|
|
||||||
const month = Number.isFinite(monthParam) ? monthParam : new Date().getUTCMonth();
|
const month = Number.isFinite(monthParam) ? monthParam : new Date().getUTCMonth();
|
||||||
const year = Number.isFinite(yearParam) ? yearParam : new Date().getUTCFullYear();
|
const year = Number.isFinite(yearParam) ? yearParam : new Date().getUTCFullYear();
|
||||||
const months = Number.isFinite(monthsParam) && monthsParam > 0 ? monthsParam : 2;
|
const months = Number.isFinite(monthsParam) && monthsParam > 0 ? monthsParam : 1;
|
||||||
|
|
||||||
const listing = await prisma.listing.findUnique({ where: { id: params.id }, select: { calendarUrls: true } });
|
const listing = await prisma.listing.findUnique({ where: { id: params.id }, select: { calendarUrls: true } });
|
||||||
if (!listing) {
|
if (!listing) {
|
||||||
|
|
|
||||||
|
|
@ -41,7 +41,7 @@ function buildMonths(monthCount: number, blocked: Set<string>, startYear: number
|
||||||
|
|
||||||
type AvailabilityResponse = { blockedDates: string[] };
|
type AvailabilityResponse = { blockedDates: string[] };
|
||||||
|
|
||||||
export default function AvailabilityCalendar({ listingId, hasCalendar, months = 2 }: { listingId: string; hasCalendar: boolean; months?: number }) {
|
export default function AvailabilityCalendar({ listingId, hasCalendar }: { listingId: string; hasCalendar: boolean }) {
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
const today = useMemo(() => new Date(), []);
|
const today = useMemo(() => new Date(), []);
|
||||||
const [month, setMonth] = useState<number>(today.getUTCMonth());
|
const [month, setMonth] = useState<number>(today.getUTCMonth());
|
||||||
|
|
@ -49,8 +49,9 @@ export default function AvailabilityCalendar({ listingId, hasCalendar, months =
|
||||||
const [blockedDates, setBlockedDates] = useState<string[]>([]);
|
const [blockedDates, setBlockedDates] = useState<string[]>([]);
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
const monthCount = 1;
|
||||||
const blockedSet = useMemo(() => new Set(blockedDates), [blockedDates]);
|
const blockedSet = useMemo(() => new Set(blockedDates), [blockedDates]);
|
||||||
const monthViews = useMemo(() => buildMonths(months, blockedSet, year, month), [months, blockedSet, year, month]);
|
const monthViews = useMemo(() => buildMonths(monthCount, blockedSet, year, month), [monthCount, blockedSet, year, month]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!hasCalendar) return;
|
if (!hasCalendar) return;
|
||||||
|
|
@ -60,7 +61,7 @@ export default function AvailabilityCalendar({ listingId, hasCalendar, months =
|
||||||
const params = new URLSearchParams({
|
const params = new URLSearchParams({
|
||||||
month: String(month),
|
month: String(month),
|
||||||
year: String(year),
|
year: String(year),
|
||||||
months: String(months),
|
months: String(monthCount),
|
||||||
refresh: '1',
|
refresh: '1',
|
||||||
});
|
});
|
||||||
fetch(`/api/listings/${listingId}/availability?${params.toString()}`, { cache: 'no-store', signal: controller.signal })
|
fetch(`/api/listings/${listingId}/availability?${params.toString()}`, { cache: 'no-store', signal: controller.signal })
|
||||||
|
|
@ -79,7 +80,7 @@ export default function AvailabilityCalendar({ listingId, hasCalendar, months =
|
||||||
.finally(() => setLoading(false));
|
.finally(() => setLoading(false));
|
||||||
|
|
||||||
return () => controller.abort();
|
return () => controller.abort();
|
||||||
}, [listingId, hasCalendar, month, year, months]);
|
}, [listingId, hasCalendar, month, year, monthCount]);
|
||||||
|
|
||||||
function shiftMonth(delta: number) {
|
function shiftMonth(delta: number) {
|
||||||
const next = new Date(Date.UTC(year, month, 1));
|
const next = new Date(Date.UTC(year, month, 1));
|
||||||
|
|
@ -108,7 +109,7 @@ export default function AvailabilityCalendar({ listingId, hasCalendar, months =
|
||||||
<span style={{ fontWeight: 700 }}>{t('availabilityTitle')}</span>
|
<span style={{ fontWeight: 700 }}>{t('availabilityTitle')}</span>
|
||||||
<span className="badge secondary">{t('availabilityLegendBooked')}</span>
|
<span className="badge secondary">{t('availabilityLegendBooked')}</span>
|
||||||
</div>
|
</div>
|
||||||
<div style={{ marginLeft: 'auto', display: 'flex', alignItems: 'center', gap: 6, flexWrap: 'wrap' }}>
|
<div style={{ marginLeft: 'auto', display: 'flex', alignItems: 'center', gap: 8, flexWrap: 'wrap' }}>
|
||||||
<button type="button" className="button secondary" onClick={() => shiftMonth(-1)} disabled={!hasCalendar || loading} style={{ padding: '6px 10px' }}>
|
<button type="button" className="button secondary" onClick={() => shiftMonth(-1)} disabled={!hasCalendar || loading} style={{ padding: '6px 10px' }}>
|
||||||
←
|
←
|
||||||
</button>
|
</button>
|
||||||
|
|
@ -132,45 +133,43 @@ export default function AvailabilityCalendar({ listingId, hasCalendar, months =
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{error ? <div style={{ color: '#f87171', fontSize: 13 }}>{error}</div> : null}
|
{error ? <div style={{ color: '#f87171', fontSize: 13 }}>{error}</div> : null}
|
||||||
<div style={{ display: 'grid', gap: 12, gridTemplateColumns: `repeat(${months}, minmax(180px, 1fr))` }}>
|
{monthViews.map((monthView) => (
|
||||||
{monthViews.map((month) => (
|
<div key={monthView.label} className="panel" style={{ padding: 12 }}>
|
||||||
<div key={month.label} className="panel" style={{ padding: 12 }}>
|
<div style={{ fontWeight: 600, marginBottom: 8 }}>{monthView.label}</div>
|
||||||
<div style={{ fontWeight: 600, marginBottom: 8 }}>{month.label}</div>
|
<div
|
||||||
<div
|
style={{
|
||||||
style={{
|
display: 'grid',
|
||||||
display: 'grid',
|
gridTemplateColumns: 'repeat(7, 1fr)',
|
||||||
gridTemplateColumns: 'repeat(7, 1fr)',
|
gap: 6,
|
||||||
gap: 6,
|
fontSize: 12,
|
||||||
fontSize: 12,
|
textAlign: 'center',
|
||||||
textAlign: 'center',
|
}}
|
||||||
}}
|
>
|
||||||
>
|
{['S', 'M', 'T', 'W', 'T', 'F', 'S'].map((d) => (
|
||||||
{['S', 'M', 'T', 'W', 'T', 'F', 'S'].map((d) => (
|
<div key={d} style={{ color: '#94a3b8', fontWeight: 600 }}>
|
||||||
<div key={d} style={{ color: '#94a3b8', fontWeight: 600 }}>
|
{d}
|
||||||
{d}
|
</div>
|
||||||
</div>
|
))}
|
||||||
))}
|
{monthView.days.map((day, idx) => (
|
||||||
{month.days.map((day, idx) => (
|
<div
|
||||||
<div
|
key={`${monthView.label}-${idx}`}
|
||||||
key={`${month.label}-${idx}`}
|
style={{
|
||||||
style={{
|
height: 32,
|
||||||
height: 32,
|
borderRadius: 8,
|
||||||
borderRadius: 8,
|
background: day.isFiller ? 'transparent' : day.blocked ? 'rgba(248,113,113,0.2)' : 'rgba(148,163,184,0.1)',
|
||||||
background: day.isFiller ? 'transparent' : day.blocked ? 'rgba(248,113,113,0.2)' : 'rgba(148,163,184,0.1)',
|
color: day.isFiller ? 'transparent' : day.blocked ? '#ef4444' : '#e2e8f0',
|
||||||
color: day.isFiller ? 'transparent' : day.blocked ? '#ef4444' : '#e2e8f0',
|
display: 'flex',
|
||||||
display: 'flex',
|
alignItems: 'center',
|
||||||
alignItems: 'center',
|
justifyContent: 'center',
|
||||||
justifyContent: 'center',
|
}}
|
||||||
}}
|
aria-label={day.date ? `${day.date}${day.blocked ? ' (booked)' : ''}` : undefined}
|
||||||
aria-label={day.date ? `${day.date}${day.blocked ? ' (booked)' : ''}` : undefined}
|
>
|
||||||
>
|
{day.label}
|
||||||
{day.label}
|
</div>
|
||||||
</div>
|
))}
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
))}
|
</div>
|
||||||
</div>
|
))}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue