diff --git a/src/pages/EditionView/tabs/ScheduleTab/list/ListDayGroup.tsx b/src/pages/EditionView/tabs/ScheduleTab/list/ListDayGroup.tsx index 08332a4b..8e6235fa 100644 --- a/src/pages/EditionView/tabs/ScheduleTab/list/ListDayGroup.tsx +++ b/src/pages/EditionView/tabs/ScheduleTab/list/ListDayGroup.tsx @@ -44,15 +44,21 @@ export function ListDayGroup({ dayKey, slots, timezone }: ListDayGroupProps) { -
- {slots.map((slot) => ( - - ))} -
+ {slots.length === 0 ? ( +

+ No sets match your filters. +

+ ) : ( +
+ {slots.map((slot) => ( + + ))} +
+ )} ); } diff --git a/src/routes/festivals/$festivalSlug/editions/$editionSlug/schedule/list.tsx b/src/routes/festivals/$festivalSlug/editions/$editionSlug/schedule/list.tsx index 423cc53d..5bb85e22 100644 --- a/src/routes/festivals/$festivalSlug/editions/$editionSlug/schedule/list.tsx +++ b/src/routes/festivals/$festivalSlug/editions/$editionSlug/schedule/list.tsx @@ -3,7 +3,6 @@ import { useMemo } from "react"; import { useSuspenseQuery } from "@tanstack/react-query"; import { useScheduleData } from "@/hooks/useScheduleData"; import { useSetsByEditionQuery as useEditionSetsQuery } from "@/api/sets/useSetsByEdition"; -import { getFestivalDayKey } from "@/lib/timeUtils"; import { filterScheduleDays } from "@/lib/scheduleFilter"; import { ListDayGroup } from "@/pages/EditionView/tabs/ScheduleTab/list/ListDayGroup"; import { ScheduleFilterSheet } from "@/pages/EditionView/tabs/ScheduleTab/ScheduleFilterSheet"; @@ -81,21 +80,30 @@ function ListSchedule() { festival.timezone, ); - // Flatten filtered days/stages into a single list, enriching each set - // with the stage name/color the group view needs. Sets without a - // startTime can't be placed into a time slot, so they're dropped here. - const allSets: (ScheduleSet & { - stageName: string; - stageColor?: string | undefined; - })[] = []; + // The day filter narrows to the days the user picked; set-level filters + // (type/vote/time/stage) never drop a day, they just empty its stages + // (see filterScheduleDays' contract) so its header stays visible with an + // empty state instead of disappearing. + const visibleDays = + selectedDay === "all" + ? filteredScheduleDays + : filteredScheduleDays.filter((day) => day.date === selectedDay); + + return visibleDays.map((day): DayGroup => { + // Enrich each set with the stage name/color the group view needs. + // Sets without a startTime can't be placed into a time slot, so + // they're dropped here. + const daySets: (ScheduleSet & { + stageName: string; + stageColor?: string | undefined; + })[] = []; - filteredScheduleDays.forEach((day) => { day.stages.forEach((stage) => { const stageData = stages.find((s) => s.id === stage.id); stage.sets.forEach((set) => { if (set.startTime) { - allSets.push({ + daySets.push({ ...set, stageName: stage.name, stageColor: stageData?.color || undefined, @@ -103,48 +111,30 @@ function ListSchedule() { } }); }); - }); - - // Group sets by start time - const timeGroups = new Map< - string, - (ScheduleSet & { stageName: string; stageColor?: string | undefined })[] - >(); - allSets.forEach((set) => { - if (!set.startTime) return; + // Group sets by start time + const timeGroups = new Map(); - const timeKey = set.startTime.toISOString(); - if (!timeGroups.has(timeKey)) { - timeGroups.set(timeKey, []); - } - timeGroups.get(timeKey)!.push(set); - }); + daySets.forEach((set) => { + if (!set.startTime) return; - // Convert to sorted array - const slots: TimeSlot[] = Array.from(timeGroups.entries()) - .map(([timeKey, sets]) => ({ - time: new Date(timeKey), - sets: sets, - })) - .sort((a, b) => a.time.getTime() - b.time.getTime()); - - const groups = new Map(); - slots.forEach((slot) => { - const dayKey = getFestivalDayKey( - slot.time.toISOString(), - festival.timezone, - ); - if (!dayKey) return; - if (!groups.has(dayKey)) groups.set(dayKey, []); - groups.get(dayKey)!.push(slot); - }); + const timeKey = set.startTime.toISOString(); + if (!timeGroups.has(timeKey)) { + timeGroups.set(timeKey, []); + } + timeGroups.get(timeKey)!.push(set); + }); - const sortedDayGroups: DayGroup[] = Array.from(groups.entries()) - .map(([dayKey, daySlots]) => ({ dayKey, slots: daySlots })) - .sort((a, b) => a.dayKey.localeCompare(b.dayKey)); + // Convert to sorted array + const slots: TimeSlot[] = Array.from(timeGroups.entries()) + .map(([timeKey, sets]) => ({ + time: new Date(timeKey), + sets: sets, + })) + .sort((a, b) => a.time.getTime() - b.time.getTime()); - return sortedDayGroups; + return { dayKey: day.date, slots }; + }); }, [ scheduleDays, selectedDay, diff --git a/tests/e2e/schedule-type-filter.spec.ts b/tests/e2e/schedule-type-filter.spec.ts index 5a309c6b..ff1800a8 100644 --- a/tests/e2e/schedule-type-filter.spec.ts +++ b/tests/e2e/schedule-type-filter.spec.ts @@ -21,10 +21,10 @@ test.describe("Schedule set-type filter", { tag: "@smoke" }, () => { await expect(listSchedule(page)).toBeVisible(); await expect(page.getByText(MUSIC_SET_NAME).first()).toBeVisible(); - // Open the sheet from the workshop's own day group (Jul 13): filtering - // unmounts day groups left without sets, and the sheet unmounts with its - // host header. - const dayGroup = listSchedule(page).getByRole("region", { name: /Jul 13/ }); + // Open the sheet from a day group the workshop filter empties (Jul 12): + // emptied day groups stay mounted with an empty state, so the sheet that + // opened it must stay open too. + const dayGroup = listSchedule(page).getByRole("region", { name: /Jul 12/ }); await dayGroup.getByRole("button", { name: /Filters/ }).click(); const sheet = page.getByRole("dialog"); await expect(sheet).toBeVisible(); @@ -39,6 +39,9 @@ test.describe("Schedule set-type filter", { tag: "@smoke" }, () => { await expect( dayGroup.getByRole("button", { name: "Filters (1 active)" }), ).toBeVisible(); + await expect( + dayGroup.getByText("No sets match your filters."), + ).toBeVisible(); }); test("a shared ?types= link filters the timeline view too", async ({