Prevent scheduling events in the past + full events-section audit

Audited the whole events flow end-to-end (create, hosting dashboard, discovery
grid, detail, RSVP, sponsors) and confirmed the wiring is coherent:
- Create → parse → display round-trips correctly for sponsors, tour/spot/crew/
  musician links, entry fee, and featured listings
- Sponsor parser safely skips shop-coordinate `sponsor` tags (no name at pos 2),
  so events and tour/shop sponsorships never collide
- Batch RSVP counts feed the grid; hosting dashboard + detail keep per-item counts

Fix surfaced by the audit: CreateEventPage let a host pick a start time in the
past, silently producing a dead event in the Past tab. Added a `min={today}` on
the date pickers and a submit guard (10-min grace) with a clear message.

Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
This commit is contained in:
shakespeare.diy 2026-09-03 15:02:25 +07:00
parent fbb70983cd
commit d577c8e455

View file

@ -1,4 +1,4 @@
import { useState, useEffect } from 'react'; import { useState, useEffect, useMemo } from 'react';
import { useSeoMeta } from '@unhead/react'; import { useSeoMeta } from '@unhead/react';
import { useNavigate, useSearchParams } from 'react-router-dom'; import { useNavigate, useSearchParams } from 'react-router-dom';
import ngeohash from 'ngeohash'; import ngeohash from 'ngeohash';
@ -158,6 +158,14 @@ export default function CreateEventPage() {
return acc; return acc;
}, {} as Record<string, SkateSpot[]>); }, {} as Record<string, SkateSpot[]>);
// Today (local) as YYYY-MM-DD, used as the min for the date pickers so hosts
// can't accidentally schedule an event in the past.
const todayStr = useMemo(() => {
const d = new Date();
const off = d.getTimezoneOffset();
return new Date(d.getTime() - off * 60_000).toISOString().split('T')[0];
}, []);
const toggleType = (type: string) => { const toggleType = (type: string) => {
setSelectedTypes((prev) => setSelectedTypes((prev) =>
prev.includes(type) ? prev.filter((t) => t !== type) : [...prev, type] prev.includes(type) ? prev.filter((t) => t !== type) : [...prev, type]
@ -272,6 +280,17 @@ export default function CreateEventPage() {
return; return;
} }
// Guard against scheduling in the past (a 10-min grace window covers
// "starting right now" without rejecting it).
if (startTimestamp < Math.floor(Date.now() / 1000) - 600) {
toast({
title: 'That start time is in the past',
description: 'Pick a date and time in the future so skaters can RSVP and show up.',
variant: 'destructive',
});
return;
}
// Parse end time if provided // Parse end time if provided
let endTimestamp: number | undefined; let endTimestamp: number | undefined;
if (endDate && endTime) { if (endDate && endTime) {
@ -535,6 +554,7 @@ export default function CreateEventPage() {
<Input <Input
type="date" type="date"
value={startDate} value={startDate}
min={todayStr}
onChange={(e) => setStartDate(e.target.value)} onChange={(e) => setStartDate(e.target.value)}
required required
/> />
@ -554,6 +574,7 @@ export default function CreateEventPage() {
<Input <Input
type="date" type="date"
value={endDate} value={endDate}
min={startDate || todayStr}
onChange={(e) => setEndDate(e.target.value)} onChange={(e) => setEndDate(e.target.value)}
/> />
<Input <Input