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
5 changes: 4 additions & 1 deletion src/pages/EditionView/tabs/ScheduleTab/DayFilterSelect.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,10 @@ export function DayFilterSelect({
<label className="text-sm font-medium text-purple-200">Day</label>
</div>
<Select value={selectedDay} onValueChange={onDayChange}>
<SelectTrigger className="bg-white/10 border-purple-400/30 text-purple-100">
<SelectTrigger
data-testid="day-filter-trigger"
className="bg-white/10 border-purple-400/30 text-purple-100"
>
<SelectValue />
</SelectTrigger>
<SelectContent className="bg-gray-800 border-purple-400/30">
Expand Down
132 changes: 132 additions & 0 deletions src/pages/EditionView/tabs/ScheduleTab/ScheduleFilterSheet.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
import { useState } from "react";
import { Filter } from "lucide-react";
import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge";
import {
Sheet,
SheetClose,
SheetContent,
SheetDescription,
SheetFooter,
SheetHeader,
SheetTitle,
SheetTrigger,
} from "@/components/ui/sheet";
import { DayFilterSelect } from "./DayFilterSelect";
import { TimeFilterSelect } from "./TimeFilterSelect";
import { StageFilterButtons } from "./StageFilterButtons";
import { useTimelineUrlState } from "@/hooks/useTimelineUrlState";

interface ScheduleFilterSheetProps {
tab: "timeline" | "list";
}

/**
* Shared day / time-of-day / stage filter UI for both Schedule views
* (Timeline and List). Renders as a trigger button (with an active-filter
* count badge) plus a bottom sheet - the same component instance handles
* both the trigger and the sheet content so each view only has to render
* one thing in its filter row.
*
* Filter state is the shared URL state from `useTimelineUrlState`, so
* setting a filter on one view keeps it visible (and clearable) on the
* other. Opening, applying, or clearing filters here never scrolls either
* view - only day-jump navigation does that.
*
* Active-filter count: `day` and `time` each count 1 when away from "all",
* and every selected stage counts 1 of its own (so picking two stages adds
* 2). Vote-type chips will fold into this same count in a later ticket.
*/
export function ScheduleFilterSheet({ tab }: ScheduleFilterSheetProps) {
const [open, setOpen] = useState(false);
const {
day,
time,
stages,
updateDay,
updateTime,
updateStages,
clearFilters,
} = useTimelineUrlState(tab);

const activeFilterCount =
(day !== "all" ? 1 : 0) + (time !== "all" ? 1 : 0) + stages.length;
const hasActiveFilters = activeFilterCount > 0;

return (
<Sheet open={open} onOpenChange={setOpen}>
<SheetTrigger asChild>
<Button
type="button"
variant="ghost"
size="sm"
data-testid="schedule-filters-trigger"
className={
hasActiveFilters
? "flex items-center gap-2 bg-purple-600/50 text-purple-100 hover:bg-purple-600/60"
: "flex items-center gap-2 text-purple-300 hover:text-purple-100"
}
>
<Filter className="h-4 w-4" />
<span className="hidden md:inline">Filters</span>
{hasActiveFilters && (
<Badge
variant="secondary"
data-testid="schedule-filters-badge"
className="bg-purple-800/50 text-purple-100"
>
{activeFilterCount}
</Badge>
)}
</Button>
</SheetTrigger>
<SheetContent
side="bottom"
data-testid="schedule-filter-sheet"
className="bg-gray-900 border-purple-400/30 max-h-[85vh] overflow-y-auto"
>
<SheetHeader>
<SheetTitle className="text-purple-100">Filter schedule</SheetTitle>
<SheetDescription className="text-purple-300">
Narrow the schedule by day, time of day, and stage.
</SheetDescription>
</SheetHeader>

<div className="mt-4 grid grid-cols-1 gap-4 md:grid-cols-3">
<DayFilterSelect selectedDay={day} onDayChange={updateDay} />
<TimeFilterSelect selectedTime={time} onTimeChange={updateTime} />
<StageFilterButtons
selectedStages={stages}
onStageToggle={handleStageToggle}
/>
</div>

<SheetFooter className="mt-6">
{hasActiveFilters && (
<Button
type="button"
variant="ghost"
onClick={clearFilters}
data-testid="schedule-filters-clear"
className="text-red-400 hover:text-red-300 hover:bg-red-400/10"
>
Clear all
</Button>
)}
<SheetClose asChild>
<Button type="button" className="bg-purple-600 hover:bg-purple-700">
Done
</Button>
</SheetClose>
</SheetFooter>
</SheetContent>
</Sheet>
);

function handleStageToggle(stageId: string) {
const newStages = stages.includes(stageId)
? stages.filter((id) => id !== stageId)
: [...stages, stageId];
updateStages(newStages);
}
}
5 changes: 4 additions & 1 deletion src/pages/EditionView/tabs/ScheduleTab/TimeFilterSelect.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,10 @@ export function TimeFilterSelect({
<label className="text-sm font-medium text-purple-200">Time</label>
</div>
<Select value={selectedTime} onValueChange={onTimeChange}>
<SelectTrigger className="bg-white/10 border-purple-400/30 text-purple-100">
<SelectTrigger
data-testid="time-filter-trigger"
className="bg-white/10 border-purple-400/30 text-purple-100"
>
<SelectValue />
</SelectTrigger>
<SelectContent className="bg-gray-800 border-purple-400/30">
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { DayJumpButtons } from "./DayJumpButtons";
import { ScheduleFilterSheet } from "../ScheduleFilterSheet";
import type { ScheduleDay } from "@/hooks/useScheduleData";

interface TimelineToolbarProps {
Expand All @@ -21,18 +22,23 @@ export function TimelineToolbar({
? days
: days.filter((day) => day.date === selectedDay);

if (visibleDays.length === 0) return null;

return (
<div
data-testid="timeline-day-toolbar"
className="sticky top-0 z-40 mb-4 flex gap-2 overflow-x-auto rounded-lg border border-purple-400/20 bg-gray-900/95 p-2 backdrop-blur-md"
className="sticky top-0 z-40 mb-4 flex items-center gap-2 overflow-x-auto rounded-lg border border-purple-400/20 bg-gray-900/95 p-2 backdrop-blur-md"
>
<DayJumpButtons
days={visibleDays}
timezone={timezone}
onJumpToDay={onJumpToDay}
/>
{visibleDays.length > 0 && (
<div data-testid="timeline-day-buttons" className="flex gap-2">
<DayJumpButtons
days={visibleDays}
timezone={timezone}
onJumpToDay={onJumpToDay}
/>
</div>
)}
<div className="ml-auto shrink-0">
<ScheduleFilterSheet tab="timeline" />
</div>
</div>
);
}
63 changes: 0 additions & 63 deletions src/pages/EditionView/tabs/ScheduleTab/list/ListFilters.tsx

This file was deleted.

7 changes: 2 additions & 5 deletions src/pages/EditionView/tabs/ScheduleTab/list/ListSchedule.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -134,16 +134,13 @@ export function ListSchedule() {
}

return (
<div className="space-y-6">
<div className="space-y-6" data-testid="list-schedule">
{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 (
<TimeSlotGroup
Expand Down
11 changes: 9 additions & 2 deletions src/pages/EditionView/tabs/ScheduleTab/list/ListTab.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,20 @@
import { FestivalTimeBadge } from "../FestivalTimeBadge";
import { ListSchedule } from "./ListSchedule";
import { ListFilters } from "./ListFilters";
import { ScheduleFilterSheet } from "../ScheduleFilterSheet";
import { FilterContainer } from "@/components/filters/FilterContainer";

export function ScheduleTabList() {
return (
<>
<FestivalTimeBadge />

<ListFilters />
<FilterContainer>
<div className="flex items-center gap-2 flex-wrap">
<h3 className="text-purple-100 font-medium">Filters</h3>
<div className="ml-auto" />
<ScheduleFilterSheet tab="list" />
</div>
</FilterContainer>
<ListSchedule />
</>
);
Expand Down
Loading