diff --git a/src/components/ui/sheet.tsx b/src/components/ui/sheet.tsx index c13f278b..2330f6cb 100644 --- a/src/components/ui/sheet.tsx +++ b/src/components/ui/sheet.tsx @@ -63,7 +63,7 @@ const SheetContent = React.forwardRef< {...props} > {children} - + Close diff --git a/src/pages/EditionView/tabs/ScheduleTab/DayFilterSelect.tsx b/src/pages/EditionView/tabs/ScheduleTab/DayFilterSelect.tsx index 91a72aa9..38e7100a 100644 --- a/src/pages/EditionView/tabs/ScheduleTab/DayFilterSelect.tsx +++ b/src/pages/EditionView/tabs/ScheduleTab/DayFilterSelect.tsx @@ -49,7 +49,10 @@ export function DayFilterSelect({ - + diff --git a/src/pages/EditionView/tabs/ScheduleTab/horizontal/TimelineToolbar.tsx b/src/pages/EditionView/tabs/ScheduleTab/horizontal/TimelineToolbar.tsx index a5bd3d95..99fba8c5 100644 --- a/src/pages/EditionView/tabs/ScheduleTab/horizontal/TimelineToolbar.tsx +++ b/src/pages/EditionView/tabs/ScheduleTab/horizontal/TimelineToolbar.tsx @@ -3,6 +3,7 @@ import { Map } from "lucide-react"; import { DayJumpButtons } from "./DayJumpButtons"; import { NowButton } from "./NowButton"; import { Button } from "@/components/ui/button"; +import { ScheduleFilterSheet } from "../ScheduleFilterSheet"; import type { ScheduleDay } from "@/hooks/useScheduleData"; import { useScrollEdgeFade } from "./useScrollEdgeFade"; @@ -22,7 +23,9 @@ interface TimelineToolbarProps { const SCROLL_FADE_PX = 24; // Sticky nav toolbar above the Timeline strip. Navigation scrolls, it never -// filters; with a day filter active only that day's button shows. +// filters; with a day filter active only that day's button shows. Hosts the +// Filters trigger (bottom sheet) inline in the same row - it never renders +// on its own line. export function TimelineToolbar({ days, selectedDay, @@ -89,6 +92,7 @@ export function TimelineToolbar({ {isOverviewExpanded ? "Hide overview" : "Show overview"} + ); diff --git a/src/pages/EditionView/tabs/ScheduleTab/list/ListFilters.tsx b/src/pages/EditionView/tabs/ScheduleTab/list/ListFilters.tsx deleted file mode 100644 index 17b106de..00000000 --- a/src/pages/EditionView/tabs/ScheduleTab/list/ListFilters.tsx +++ /dev/null @@ -1,63 +0,0 @@ -import { useState } from "react"; -import { DayFilterSelect } from "../DayFilterSelect"; -import { TimeFilterSelect } from "../TimeFilterSelect"; -import { StageFilterButtons } from "../StageFilterButtons"; -import { useTimelineUrlState } from "@/hooks/useTimelineUrlState"; -import { FilterToggle } from "@/components/filters/FilterToggle"; -import { FilterContainer } from "@/components/filters/FilterContainer"; - -export function ListFilters() { - const [isExpanded, setIsExpanded] = useState(false); - const { - day, - time, - stages, - updateDay, - updateTime, - updateStages, - clearFilters, - } = useTimelineUrlState("list"); - - function handleStageToggle(stageId: string) { - const newStages = stages.includes(stageId) - ? stages.filter((id) => id !== stageId) - : [...stages, stageId]; - updateStages(newStages); - } - - const activeFilterCount = - (day !== "all" ? 1 : 0) + (time !== "all" ? 1 : 0) + stages.length; - const hasActiveFilters = activeFilterCount > 0; - const shouldShowFilters = isExpanded; - - return ( - -
-

Filters

-
- - setIsExpanded(!isExpanded)} - hasActiveFilters={hasActiveFilters} - activeFilterCount={activeFilterCount} - label="Filters" - onClearFilters={hasActiveFilters ? clearFilters : undefined} - /> -
- - {shouldShowFilters && ( -
-
- - - -
-
- )} - - ); -} diff --git a/src/pages/EditionView/tabs/ScheduleTab/list/ListSchedule.tsx b/src/pages/EditionView/tabs/ScheduleTab/list/ListSchedule.tsx index 7b4b1511..78bba4bf 100644 --- a/src/pages/EditionView/tabs/ScheduleTab/list/ListSchedule.tsx +++ b/src/pages/EditionView/tabs/ScheduleTab/list/ListSchedule.tsx @@ -134,16 +134,13 @@ export function ListSchedule() { } return ( -
+
{timeSlots.map((slot, index) => { const prevSlot = index > 0 ? timeSlots[index - 1] : null; const showDateHeader = !prevSlot || getFestivalDayKey(slot.time.toISOString(), festival.timezone) !== - getFestivalDayKey( - prevSlot.time.toISOString(), - festival.timezone, - ); + getFestivalDayKey(prevSlot.time.toISOString(), festival.timezone); return ( - + +
+

Filters

+
+ +
+ ); diff --git a/tests/e2e/schedule-filter-sheet.spec.ts b/tests/e2e/schedule-filter-sheet.spec.ts new file mode 100644 index 00000000..8915393b --- /dev/null +++ b/tests/e2e/schedule-filter-sheet.spec.ts @@ -0,0 +1,120 @@ +import { test, expect } from "@playwright/test"; + +// Seeded in supabase/seed.sql: festival slug "test", edition slug "2025", +// three festival days (Jul 12-14, 2025), stages "Main Stage" and "Club Stage". +const TIMELINE_PATH = "/festivals/test/editions/2025/schedule/timeline"; +const LIST_PATH = "/festivals/test/editions/2025/schedule/list"; +const MAIN_STAGE_ID = "11111111-1111-1111-1111-11111111111a"; + +async function openSheet(page: import("@playwright/test").Page) { + await page.getByTestId("schedule-filters-trigger").click(); + await expect(page.getByTestId("schedule-filter-sheet")).toBeVisible(); +} + +test.describe("Schedule filter sheet", () => { + test("opens from the Timeline toolbar with title and description", async ({ + page, + }) => { + await page.goto(TIMELINE_PATH); + + const scrollContainer = page.getByTestId("timeline-scroll-container"); + if (!(await scrollContainer.isVisible().catch(() => false))) { + test.skip(true, "Schedule not revealed in this environment"); + } + + // The Filters trigger sits inline in the toolbar row, alongside the + // day-jump buttons - never alone on its own line. + const toolbar = page.getByTestId("timeline-day-toolbar"); + const trigger = toolbar.getByTestId("schedule-filters-trigger"); + await expect(trigger).toBeVisible(); + + await trigger.click(); + + const sheet = page.getByTestId("schedule-filter-sheet"); + await expect(sheet).toBeVisible(); + await expect(sheet.getByText("Filter schedule")).toBeVisible(); + await expect( + sheet.getByText("Narrow the schedule by day, time of day, and stage."), + ).toBeVisible(); + }); + + test("opens from the List view's filter row into the same sheet", async ({ + page, + }) => { + await page.goto(LIST_PATH); + + const listSchedule = page.getByTestId("list-schedule"); + if (!(await listSchedule.isVisible().catch(() => false))) { + test.skip(true, "Schedule not revealed in this environment"); + } + + await openSheet(page); + await expect( + page.getByTestId("schedule-filter-sheet").getByText("Filter schedule"), + ).toBeVisible(); + }); + + test("selecting a day filter updates the badge, both views, and never scrolls the Timeline", async ({ + page, + }) => { + await page.goto(TIMELINE_PATH); + + const scrollContainer = page.getByTestId("timeline-scroll-container"); + if (!(await scrollContainer.isVisible().catch(() => false))) { + test.skip(true, "Schedule not revealed in this environment"); + } + + await expect(page.getByTestId("schedule-filters-badge")).toHaveCount(0); + + const scrollLeftBefore = await scrollContainer.evaluate( + (el) => el.scrollLeft, + ); + + await openSheet(page); + await page.getByTestId("day-filter-trigger").click(); + await page.getByRole("option", { name: /^Saturday$/ }).click(); + await page.getByTestId("schedule-filter-sheet").getByText("Done").click(); + + await expect(page).toHaveURL(/day=2025-07-12/); + await expect(page.getByTestId("schedule-filters-badge")).toHaveText("1"); + + // Day filter collapses the Timeline strip to a single day, but applying + // it must never scroll the viewport. + const toolbar = page.getByTestId("timeline-day-toolbar"); + await expect( + toolbar.getByTestId("timeline-day-buttons").getByRole("button"), + ).toHaveCount(1); + + const scrollLeftAfter = await scrollContainer.evaluate( + (el) => el.scrollLeft, + ); + expect(scrollLeftAfter).toBe(scrollLeftBefore); + + // Shared URL state: the List view sees and can clear the same filter. + await page.goto(`${LIST_PATH}?day=2025-07-12`); + await expect(page.getByTestId("schedule-filters-badge")).toHaveText("1"); + }); + + test("clearing filters from the sheet restores both views and removes the badge", async ({ + page, + }) => { + await page.goto(`${TIMELINE_PATH}?day=2025-07-12&stages=${MAIN_STAGE_ID}`); + + const scrollContainer = page.getByTestId("timeline-scroll-container"); + if (!(await scrollContainer.isVisible().catch(() => false))) { + test.skip(true, "Schedule not revealed in this environment"); + } + + await expect(page.getByTestId("schedule-filters-badge")).toHaveText("2"); + + await openSheet(page); + await page.getByTestId("schedule-filters-clear").click(); + + await expect(page.getByTestId("schedule-filters-badge")).toHaveCount(0); + await expect(page).not.toHaveURL(/day=/); + await expect(page).not.toHaveURL(/stages=/); + + await page.goto(LIST_PATH); + await expect(page.getByTestId("schedule-filters-badge")).toHaveCount(0); + }); +});