Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 15 additions & 9 deletions src/pages/EditionView/tabs/ScheduleTab/list/ListDayGroup.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -44,15 +44,21 @@ export function ListDayGroup({ dayKey, slots, timezone }: ListDayGroupProps) {
</div>
</header>

<div className="space-y-6">
{slots.map((slot) => (
<TimeSlotGroup
key={slot.time.toISOString()}
timeSlot={slot}
timezone={timezone}
/>
))}
</div>
{slots.length === 0 ? (
<p className="text-sm text-muted-foreground">
No sets match your filters.
</p>
) : (
<div className="space-y-6">
{slots.map((slot) => (
<TimeSlotGroup
key={slot.time.toISOString()}
timeSlot={slot}
timezone={timezone}
/>
))}
</div>
)}
</section>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -81,70 +80,61 @@ 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,
});
}
});
});
});

// 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<string, typeof daySets>();

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<string, TimeSlot[]>();
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,
Expand Down
11 changes: 7 additions & 4 deletions tests/e2e/schedule-type-filter.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand All @@ -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 ({
Expand Down
Loading