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[];
onClassesChange: () => void;
}) {
const [selectedHall, setSelectedHall] = useState(halls[0] ?? "");
const timeSlots = generateTimeSlots(10, 22);
// Build lookup: hall -> time -> class
const grid = useMemo(() => {
const map: Record<string, Record<string, OpenDayClass>> = {};
for (const hall of halls) map[hall] = {};
// Build lookup: time -> class for selected hall
const hallClasses = useMemo(() => {
const map: Record<string, OpenDayClass> = {};
for (const cls of classes) {
if (!map[cls.hall]) map[cls.hall] = {};
map[cls.hall][cls.startTime] = cls;
if (cls.hall === selectedHall) map[cls.startTime] = cls;
}
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]);
async function addClass(hall: string, startTime: string) {
async function addClass(startTime: string) {
const endTime = addHour(startTime);
await adminFetch("/api/admin/open-day/classes", {
method: "POST",
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();
}
@@ -342,26 +349,37 @@ function ScheduleGrid({
{halls.length === 0 ? (
<p className="text-sm text-neutral-500">Нет залов в расписании. Добавьте локации в разделе «Расписание».</p>
) : (
<div className="overflow-x-auto">
<table className="w-full border-collapse min-w-[500px]">
<thead>
<tr>
<th className="text-left text-xs text-neutral-500 font-medium pb-2 w-16">Время</th>
<>
{/* Hall selector */}
<div className="flex gap-2 flex-wrap">
{halls.map((hall) => (
<th key={hall} className="text-left text-xs text-neutral-400 font-medium pb-2 px-1">
<button
key={hall}
onClick={() => setSelectedHall(hall)}
className={`rounded-lg px-3 py-1.5 text-xs font-medium transition-all ${
selectedHall === hall
? "bg-gold/20 text-gold border border-gold/40"
: "bg-neutral-800 text-neutral-400 border border-white/10 hover:text-white"
}`}
>
{hall}
</th>
{hallCounts[hall] > 0 && (
<span className={`ml-1.5 ${selectedHall === hall ? "text-gold/60" : "text-neutral-600"}`}>
{hallCounts[hall]}
</span>
)}
</button>
))}
</tr>
</thead>
<tbody>
{timeSlots.map((time) => (
<tr key={time} className="border-t border-white/5">
<td className="text-xs text-neutral-500 py-1 pr-2 align-top pt-2">{time}</td>
{halls.map((hall) => {
const cls = grid[hall]?.[time];
</div>
{/* Time slots for selected hall */}
<div className="space-y-1">
{timeSlots.map((time) => {
const cls = hallClasses[time];
return (
<td key={hall} className="py-1 px-1 align-top">
<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">
{cls ? (
<ClassCell
cls={cls}
@@ -374,20 +392,18 @@ function ScheduleGrid({
/>
) : (
<button
onClick={() => addClass(hall, time)}
onClick={() => addClass(time)}
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"
>
<Plus size={12} className="mx-auto" />
</button>
)}
</td>
</div>
</div>
);
})}
</tr>
))}
</tbody>
</table>
</div>
</>
)}
</div>
);