diff --git a/src/pages/EditionView/tabs/ScheduleTab/TimelineTab.tsx b/src/pages/EditionView/tabs/ScheduleTab/TimelineTab.tsx index 922c529a..4d474236 100644 --- a/src/pages/EditionView/tabs/ScheduleTab/TimelineTab.tsx +++ b/src/pages/EditionView/tabs/ScheduleTab/TimelineTab.tsx @@ -1,11 +1,15 @@ import { FestivalTimeBadge } from "./FestivalTimeBadge"; import { Timeline } from "./horizontal/Timeline"; +import { ScheduleLineupView } from "./lineup/ScheduleLineupView"; +import { useScheduleReveal } from "@/hooks/useScheduleReveal"; export function ScheduleTabTimeline() { + const { canShowTime } = useScheduleReveal(); + return ( <> - - + {canShowTime && } + {canShowTime ? : } ); } diff --git a/src/pages/EditionView/tabs/ScheduleTab/horizontal/Timeline.tsx b/src/pages/EditionView/tabs/ScheduleTab/horizontal/Timeline.tsx index 5cd0c0f4..827709a3 100644 --- a/src/pages/EditionView/tabs/ScheduleTab/horizontal/Timeline.tsx +++ b/src/pages/EditionView/tabs/ScheduleTab/horizontal/Timeline.tsx @@ -10,15 +10,12 @@ import { useSetsByEditionQuery as useEditionSetsQuery } from "@/api/sets/useSets import { useTimelineUrlState } from "@/hooks/useTimelineUrlState"; import { getFestivalHour } from "@/lib/timeUtils"; import { stagesByEditionQuery } from "@/api/stages/useStagesByEdition"; -import { useScheduleReveal } from "@/hooks/useScheduleReveal"; -import { ScheduleNotRevealedPlaceholder } from "../ScheduleNotRevealedPlaceholder"; export function Timeline() { const { festival } = useFestivalEdition(); const { edition } = useRouteContext({ from: "/festivals/$festivalSlug/editions/$editionSlug/schedule/timeline", }); - const { canShowTime } = useScheduleReveal(); const { data: editionSets = [], isLoading: setsLoading } = useEditionSetsQuery(edition.id); const { data: stages } = useSuspenseQuery(stagesByEditionQuery(edition.id)); @@ -123,10 +120,6 @@ export function Timeline() { ); } - if (!canShowTime) { - return ; - } - return (
diff --git a/src/pages/EditionView/tabs/ScheduleTab/lineup/DaysLineupView.tsx b/src/pages/EditionView/tabs/ScheduleTab/lineup/DaysLineupView.tsx new file mode 100644 index 00000000..06f13293 --- /dev/null +++ b/src/pages/EditionView/tabs/ScheduleTab/lineup/DaysLineupView.tsx @@ -0,0 +1,42 @@ +import { LineupSetItem } from "./LineupSetItem"; +import type { ScheduleDay } from "@/hooks/useScheduleData"; + +interface DaysLineupViewProps { + scheduleDays: ScheduleDay[]; +} + +export function DaysLineupView({ scheduleDays }: DaysLineupViewProps) { + const daysWithSets = scheduleDays + .map((day) => ({ + ...day, + sets: day.stages + .flatMap((stage) => stage.sets) + .sort((a, b) => a.name.localeCompare(b.name)), + })) + .filter((day) => day.sets.length > 0); + + if (!daysWithSets.length) { + return ( +
+

No scheduled sets found.

+
+ ); + } + + return ( +
+ {daysWithSets.map((day) => ( +
+

+ {day.displayDate} +

+
+ {day.sets.map((set) => ( + + ))} +
+
+ ))} +
+ ); +} diff --git a/src/pages/EditionView/tabs/ScheduleTab/lineup/LineupFilters.tsx b/src/pages/EditionView/tabs/ScheduleTab/lineup/LineupFilters.tsx new file mode 100644 index 00000000..a59ff022 --- /dev/null +++ b/src/pages/EditionView/tabs/ScheduleTab/lineup/LineupFilters.tsx @@ -0,0 +1,67 @@ +import { useState } from "react"; +import { DayFilterSelect } from "../DayFilterSelect"; +import { StageFilterButtons } from "../StageFilterButtons"; +import { useTimelineUrlState } from "@/hooks/useTimelineUrlState"; +import { useScheduleReveal } from "@/hooks/useScheduleReveal"; +import { FilterToggle } from "@/components/filters/FilterToggle"; +import { FilterContainer } from "@/components/filters/FilterContainer"; +import { cn } from "@/lib/utils"; + +interface LineupFiltersProps { + tab: "timeline" | "list"; +} + +export function LineupFilters({ tab }: LineupFiltersProps) { + const [isExpanded, setIsExpanded] = useState(false); + const { canShowStage } = useScheduleReveal(); + const { day, stages, updateDay, updateStages, clearFilters } = + useTimelineUrlState(tab); + + function handleStageToggle(stageId: string) { + const newStages = stages.includes(stageId) + ? stages.filter((id) => id !== stageId) + : [...stages, stageId]; + updateStages(newStages); + } + + const activeFilterCount = + (day !== "all" ? 1 : 0) + (canShowStage ? stages.length : 0); + const hasActiveFilters = activeFilterCount > 0; + + return ( + +
+

Filters

+
+ + setIsExpanded(!isExpanded)} + hasActiveFilters={hasActiveFilters} + activeFilterCount={activeFilterCount} + label="Filters" + onClearFilters={hasActiveFilters ? clearFilters : undefined} + /> +
+ + {isExpanded && ( +
+
+ + {canShowStage && ( + + )} +
+
+ )} + + ); +} diff --git a/src/pages/EditionView/tabs/ScheduleTab/lineup/LineupSetItem.tsx b/src/pages/EditionView/tabs/ScheduleTab/lineup/LineupSetItem.tsx new file mode 100644 index 00000000..c7d2501c --- /dev/null +++ b/src/pages/EditionView/tabs/ScheduleTab/lineup/LineupSetItem.tsx @@ -0,0 +1,29 @@ +import { Link, useParams } from "@tanstack/react-router"; +import { Card, CardContent } from "@/components/ui/card"; +import { VoteButtons } from "../VoteButtons"; +import type { ScheduleSet } from "@/hooks/useScheduleData"; + +interface LineupSetItemProps { + set: ScheduleSet; +} + +export function LineupSetItem({ set }: LineupSetItemProps) { + const { festivalSlug, editionSlug } = useParams({ + from: "/festivals/$festivalSlug/editions/$editionSlug", + }); + + return ( + + + + {set.name} + + + + + ); +} diff --git a/src/pages/EditionView/tabs/ScheduleTab/lineup/ScheduleLineupView.tsx b/src/pages/EditionView/tabs/ScheduleTab/lineup/ScheduleLineupView.tsx new file mode 100644 index 00000000..b34a3715 --- /dev/null +++ b/src/pages/EditionView/tabs/ScheduleTab/lineup/ScheduleLineupView.tsx @@ -0,0 +1,72 @@ +import { useMemo } from "react"; +import { useSuspenseQuery } from "@tanstack/react-query"; +import { useRouteContext } from "@tanstack/react-router"; +import { useFestivalEdition } from "@/contexts/FestivalEditionContext"; +import { useSetsByEditionQuery as useEditionSetsQuery } from "@/api/sets/useSetsByEdition"; +import { stagesByEditionQuery } from "@/api/stages/useStagesByEdition"; +import { useScheduleData } from "@/hooks/useScheduleData"; +import { useScheduleReveal } from "@/hooks/useScheduleReveal"; +import { useTimelineUrlState } from "@/hooks/useTimelineUrlState"; +import { ScheduleNotRevealedPlaceholder } from "../ScheduleNotRevealedPlaceholder"; +import { LineupFilters } from "./LineupFilters"; +import { DaysLineupView } from "./DaysLineupView"; +import { StagesLineupGrid } from "./StagesLineupGrid"; + +interface ScheduleLineupViewProps { + tab: "timeline" | "list"; +} + +export function ScheduleLineupView({ tab }: ScheduleLineupViewProps) { + const { festival } = useFestivalEdition(); + const { canShowDay, canShowStage } = useScheduleReveal(); + const route = + `/festivals/$festivalSlug/editions/$editionSlug/schedule/${tab}` as const; + const { edition } = useRouteContext({ from: route }); + const { data: editionSets = [], isLoading: setsLoading } = + useEditionSetsQuery(edition.id); + const { data: stages } = useSuspenseQuery(stagesByEditionQuery(edition.id)); + const { scheduleDays, loading } = useScheduleData({ + sets: editionSets, + stages, + timezone: festival.timezone, + }); + const { day: selectedDay, stages: selectedStages } = + useTimelineUrlState(tab); + + const filteredScheduleDays = useMemo(() => { + return scheduleDays + .filter((day) => selectedDay === "all" || day.date === selectedDay) + .map((day) => ({ + ...day, + stages: day.stages.filter( + (stage) => + !canShowStage || + selectedStages.length === 0 || + selectedStages.includes(stage.id), + ), + })); + }, [scheduleDays, selectedDay, selectedStages, canShowStage]); + + if (loading || setsLoading) { + return ( +
+

Loading schedule...

+
+ ); + } + + if (!canShowDay) { + return ; + } + + return ( +
+ + {canShowStage ? ( + + ) : ( + + )} +
+ ); +} diff --git a/src/pages/EditionView/tabs/ScheduleTab/lineup/StagesLineupGrid.tsx b/src/pages/EditionView/tabs/ScheduleTab/lineup/StagesLineupGrid.tsx new file mode 100644 index 00000000..f2898ff7 --- /dev/null +++ b/src/pages/EditionView/tabs/ScheduleTab/lineup/StagesLineupGrid.tsx @@ -0,0 +1,47 @@ +import { LineupSetItem } from "./LineupSetItem"; +import type { ScheduleDay } from "@/hooks/useScheduleData"; + +interface StagesLineupGridProps { + scheduleDays: ScheduleDay[]; +} + +export function StagesLineupGrid({ scheduleDays }: StagesLineupGridProps) { + const daysWithStages = scheduleDays.filter((day) => day.stages.length > 0); + + if (!daysWithStages.length) { + return ( +
+

No scheduled sets found.

+
+ ); + } + + return ( +
+ {daysWithStages.map((day) => ( +
+

+ {day.displayDate} +

+
+ {day.stages.map((stage) => ( +
+

+ {stage.name} +

+
+ {stage.sets + .slice() + .sort((a, b) => a.name.localeCompare(b.name)) + .map((set) => ( + + ))} +
+
+ ))} +
+
+ ))} +
+ ); +} diff --git a/src/pages/EditionView/tabs/ScheduleTab/list/ListSchedule.tsx b/src/pages/EditionView/tabs/ScheduleTab/list/ListSchedule.tsx index 5667bee9..dc16b729 100644 --- a/src/pages/EditionView/tabs/ScheduleTab/list/ListSchedule.tsx +++ b/src/pages/EditionView/tabs/ScheduleTab/list/ListSchedule.tsx @@ -9,8 +9,6 @@ import { TimeSlotGroup } from "./TimeSlotGroup"; import type { ScheduleSet } from "@/hooks/useScheduleData"; import { useTimelineUrlState } from "@/hooks/useTimelineUrlState"; import { stagesByEditionQuery } from "@/api/stages/useStagesByEdition"; -import { useScheduleReveal } from "@/hooks/useScheduleReveal"; -import { ScheduleNotRevealedPlaceholder } from "../ScheduleNotRevealedPlaceholder"; interface TimeSlot { time: Date; @@ -22,7 +20,6 @@ export function ListSchedule() { const { edition } = useRouteContext({ from: "/festivals/$festivalSlug/editions/$editionSlug/schedule/list", }); - const { canShowTime } = useScheduleReveal(); const { data: editionSets = [], isLoading: setsLoading } = useEditionSetsQuery(edition.id); const { data: stages } = useSuspenseQuery(stagesByEditionQuery(edition.id)); @@ -158,10 +155,6 @@ export function ListSchedule() { ); } - if (!canShowTime) { - return ; - } - if (!timeSlots.length) { return (
diff --git a/src/pages/EditionView/tabs/ScheduleTab/list/ListTab.tsx b/src/pages/EditionView/tabs/ScheduleTab/list/ListTab.tsx index fe3146ca..ecb92c06 100644 --- a/src/pages/EditionView/tabs/ScheduleTab/list/ListTab.tsx +++ b/src/pages/EditionView/tabs/ScheduleTab/list/ListTab.tsx @@ -1,14 +1,23 @@ import { FestivalTimeBadge } from "../FestivalTimeBadge"; import { ListSchedule } from "./ListSchedule"; import { ListFilters } from "./ListFilters"; +import { ScheduleLineupView } from "../lineup/ScheduleLineupView"; +import { useScheduleReveal } from "@/hooks/useScheduleReveal"; export function ScheduleTabList() { + const { canShowTime } = useScheduleReveal(); + return ( <> - - - - + {canShowTime && } + {canShowTime ? ( + <> + + + + ) : ( + + )} ); }