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 ({