refactor: Open Day schedule — hall selector instead of full grid

Replace wide multi-column hall grid with hall selector tabs + single
column time slots. Each hall tab shows class count badge. Scales
better as more halls are added.
This commit is contained in:
2026-03-24 19:34:02 +03:00
parent 2693491fee
commit 353484af2e

View File

@@ -292,25 +292,32 @@ function ScheduleGrid({
styles: string[]; styles: string[];
onClassesChange: () => void; onClassesChange: () => void;
}) { }) {
const [selectedHall, setSelectedHall] = useState(halls[0] ?? "");
const timeSlots = generateTimeSlots(10, 22); const timeSlots = generateTimeSlots(10, 22);
// Build lookup: hall -> time -> class // Build lookup: time -> class for selected hall
const grid = useMemo(() => { const hallClasses = useMemo(() => {
const map: Record<string, Record<string, OpenDayClass>> = {}; const map: Record<string, OpenDayClass> = {};
for (const hall of halls) map[hall] = {};
for (const cls of classes) { for (const cls of classes) {
if (!map[cls.hall]) map[cls.hall] = {}; if (cls.hall === selectedHall) map[cls.startTime] = cls;
map[cls.hall][cls.startTime] = cls;
} }
return map; return map;
}, [classes, selectedHall]);
// Count classes per hall for the tab badges
const hallCounts = useMemo(() => {
const counts: Record<string, number> = {};
for (const hall of halls) counts[hall] = 0;
for (const cls of classes) counts[cls.hall] = (counts[cls.hall] || 0) + 1;
return counts;
}, [classes, halls]); }, [classes, halls]);
async function addClass(hall: string, startTime: string) { async function addClass(startTime: string) {
const endTime = addHour(startTime); const endTime = addHour(startTime);
await adminFetch("/api/admin/open-day/classes", { await adminFetch("/api/admin/open-day/classes", {
method: "POST", method: "POST",
headers: { "Content-Type": "application/json" }, headers: { "Content-Type": "application/json" },
body: JSON.stringify({ eventId, hall, startTime, endTime, trainer: "—", style: "—" }), body: JSON.stringify({ eventId, hall: selectedHall, startTime, endTime, trainer: "—", style: "—" }),
}); });
onClassesChange(); onClassesChange();
} }
@@ -342,52 +349,61 @@ function ScheduleGrid({
{halls.length === 0 ? ( {halls.length === 0 ? (
<p className="text-sm text-neutral-500">Нет залов в расписании. Добавьте локации в разделе «Расписание».</p> <p className="text-sm text-neutral-500">Нет залов в расписании. Добавьте локации в разделе «Расписание».</p>
) : ( ) : (
<div className="overflow-x-auto"> <>
<table className="w-full border-collapse min-w-[500px]"> {/* Hall selector */}
<thead> <div className="flex gap-2 flex-wrap">
<tr> {halls.map((hall) => (
<th className="text-left text-xs text-neutral-500 font-medium pb-2 w-16">Время</th> <button
{halls.map((hall) => ( key={hall}
<th key={hall} className="text-left text-xs text-neutral-400 font-medium pb-2 px-1"> onClick={() => setSelectedHall(hall)}
{hall} className={`rounded-lg px-3 py-1.5 text-xs font-medium transition-all ${
</th> selectedHall === hall
))} ? "bg-gold/20 text-gold border border-gold/40"
</tr> : "bg-neutral-800 text-neutral-400 border border-white/10 hover:text-white"
</thead> }`}
<tbody> >
{timeSlots.map((time) => ( {hall}
<tr key={time} className="border-t border-white/5"> {hallCounts[hall] > 0 && (
<td className="text-xs text-neutral-500 py-1 pr-2 align-top pt-2">{time}</td> <span className={`ml-1.5 ${selectedHall === hall ? "text-gold/60" : "text-neutral-600"}`}>
{halls.map((hall) => { {hallCounts[hall]}
const cls = grid[hall]?.[time]; </span>
return ( )}
<td key={hall} className="py-1 px-1 align-top"> </button>
{cls ? ( ))}
<ClassCell </div>
cls={cls}
minBookings={minBookings} {/* Time slots for selected hall */}
trainers={trainers} <div className="space-y-1">
styles={styles} {timeSlots.map((time) => {
onUpdate={updateClass} const cls = hallClasses[time];
onDelete={deleteClass} return (
onCancel={cancelClass} <div key={time} className="flex items-start gap-3 border-t border-white/5 py-1.5">
/> <span className="text-xs text-neutral-500 w-12 pt-1.5 shrink-0">{time}</span>
) : ( <div className="flex-1">
<button {cls ? (
onClick={() => addClass(hall, time)} <ClassCell
className="w-full rounded-lg border border-dashed border-white/5 p-2 text-neutral-600 hover:text-gold hover:border-gold/20 transition-colors" cls={cls}
> minBookings={minBookings}
<Plus size={12} className="mx-auto" /> trainers={trainers}
</button> styles={styles}
)} onUpdate={updateClass}
</td> onDelete={deleteClass}
); onCancel={cancelClass}
})} />
</tr> ) : (
))} <button
</tbody> onClick={() => addClass(time)}
</table> className="w-full rounded-lg border border-dashed border-white/5 p-2 text-neutral-600 hover:text-gold hover:border-gold/20 transition-colors"
</div> >
<Plus size={12} className="mx-auto" />
</button>
)}
</div>
</div>
);
})}
</div>
</>
)} )}
</div> </div>
); );