From 7c9bcd81a6f1d65315511a9107a4b7ad4583f1ea Mon Sep 17 00:00:00 2001 From: fullsizemalt <106900403+fullsizemalt@users.noreply.github.com> Date: Mon, 29 Dec 2025 01:18:28 -0800 Subject: [PATCH] feat(frontend): implement date-grouped show list and band filter for All Bands view --- frontend/app/shows/page.tsx | 92 +++++-------- frontend/components/shows/band-filter.tsx | 91 +++++++++++++ .../components/shows/date-grouped-list.tsx | 124 ++++++++++++++++++ 3 files changed, 249 insertions(+), 58 deletions(-) create mode 100644 frontend/components/shows/band-filter.tsx create mode 100644 frontend/components/shows/date-grouped-list.tsx diff --git a/frontend/app/shows/page.tsx b/frontend/app/shows/page.tsx index 66ad59e..7907d9a 100644 --- a/frontend/app/shows/page.tsx +++ b/frontend/app/shows/page.tsx @@ -1,19 +1,24 @@ "use client" -import { useEffect, useState, Suspense } from "react" +import { useEffect, useState, Suspense, useMemo } from "react" import { getApiUrl } from "@/lib/api-config" -import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card" -import Link from "next/link" -import { Calendar, MapPin, Loader2, Youtube } from "lucide-react" +import { Loader2, Calendar } from "lucide-react" import { Skeleton } from "@/components/ui/skeleton" import { useSearchParams } from "next/navigation" import { Button } from "@/components/ui/button" +import Link from "next/link" +import { BandFilter } from "@/components/shows/band-filter" +import { DateGroupedList } from "@/components/shows/date-grouped-list" interface Show { id: number slug?: string date: string youtube_link?: string + vertical?: { + name: string + slug: string + } venue: { id: number name: string @@ -28,6 +33,7 @@ function ShowsContent() { const [shows, setShows] = useState([]) const [loading, setLoading] = useState(true) + const [selectedBands, setSelectedBands] = useState([]) useEffect(() => { const url = `${getApiUrl()}/shows/?limit=2000&status=past${year ? `&year=${year}` : ''}` @@ -44,6 +50,14 @@ function ShowsContent() { .finally(() => setLoading(false)) }, [year]) + // Filter shows locally based on selection + const filteredShows = useMemo(() => { + if (selectedBands.length === 0) return shows + return shows.filter(show => + show.vertical && selectedBands.includes(show.vertical.slug) + ) + }, [shows, selectedBands]) + if (loading) { return (
@@ -54,20 +68,7 @@ function ShowsContent() {
{Array.from({ length: 12 }).map((_, i) => ( - - -
- - -
-
- -
- - -
-
-
+ ))}
@@ -76,55 +77,30 @@ function ShowsContent() { return (
-
-
+
+

Shows

Browse the complete archive of performances.

- - - +
+ + + + +
-
- {shows.map((show) => ( - - - {show.youtube_link && ( -
- -
- )} - - - - {new Date(show.date).toLocaleDateString(undefined, { - weekday: 'long', - year: 'numeric', - month: 'long', - day: 'numeric' - })} - - - -
- - - {show.venue?.name}, {show.venue?.city}, {show.venue?.state} - -
-
-
- - ))} -
+
) } diff --git a/frontend/components/shows/band-filter.tsx b/frontend/components/shows/band-filter.tsx new file mode 100644 index 0000000..3d09ef5 --- /dev/null +++ b/frontend/components/shows/band-filter.tsx @@ -0,0 +1,91 @@ + +"use client" + +import { Button } from "@/components/ui/button" +import { Checkbox } from "@/components/ui/checkbox" +import { + DropdownMenu, + DropdownMenuCheckboxItem, + DropdownMenuContent, + DropdownMenuLabel, + DropdownMenuSeparator, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu" +import { VERTICALS } from "@/config/verticals" +import { Filter } from "lucide-react" + +interface BandFilterProps { + selected: string[] + onChange: (selected: string[]) => void +} + +export function BandFilter({ selected, onChange }: BandFilterProps) { + const toggleVertical = (slug: string) => { + if (selected.includes(slug)) { + onChange(selected.filter(s => s !== slug)) + } else { + onChange([...selected, slug]) + } + } + + const clearFilters = () => { + onChange([]) + } + + return ( +
+ + + + + + Filter by Band + + {VERTICALS.map((vertical) => ( + toggleVertical(vertical.slug)} + > + {vertical.name} + + ))} + {selected.length > 0 && ( + <> + + + Clear Filters + + + )} + + + + {/* Inline filters for desktop - optionally could show these next to dropdown or instead of on large screens */} +
+ {selected.length > 0 && ( + + )} +
+
+ ) +} diff --git a/frontend/components/shows/date-grouped-list.tsx b/frontend/components/shows/date-grouped-list.tsx new file mode 100644 index 0000000..8a526b4 --- /dev/null +++ b/frontend/components/shows/date-grouped-list.tsx @@ -0,0 +1,124 @@ + +"use client" + +import Link from "next/link" +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card" +import { Calendar, MapPin, Youtube, Music2 } from "lucide-react" +import { Button } from "@/components/ui/button" + +interface Show { + id: number + slug?: string + date: string + youtube_link?: string + vertical?: { + name: string + slug: string + } + venue: { + id: number + name: string + city: string + state: string + } +} + +interface DateGroupedListProps { + shows: Show[] +} + +export function DateGroupedList({ shows }: DateGroupedListProps) { + if (shows.length === 0) { + return ( +
+ No shows found matching your filters. +
+ ) + } + + // Group shows by date + const groupedShows: Record = {} + + shows.forEach(show => { + // Use local date string to avoid timezone shifts if possible, or usually just split T + const dateKey = new Date(show.date).toLocaleDateString(undefined, { + weekday: 'long', + year: 'numeric', + month: 'long', + day: 'numeric' + }) + + if (!groupedShows[dateKey]) { + groupedShows[dateKey] = [] + } + groupedShows[dateKey].push(show) + }) + + // Sort dates (descending) - assuming API returned sorted, but safe to re-sort keys if list order isn't guaranteed + // Actually relying on input order is safer if we trust the API to sort by date desc. + // Let's just iterate over the unique keys in the order they appear (which will be desc if input is desc) + const uniqueDates = Array.from(new Set(shows.map(s => + new Date(s.date).toLocaleDateString(undefined, { + weekday: 'long', + year: 'numeric', + month: 'long', + day: 'numeric' + }) + ))) + + return ( +
+ {uniqueDates.map(dateStr => ( +
+
+

+ + {dateStr} +

+
+ +
+ {groupedShows[dateStr].map(show => ( + + + {/* Vertical Stripe/Badge */} + {show.vertical && ( +
+ )} + + {show.youtube_link && ( +
+ +
+ )} + + {/* Added padding-left for stripe */} + + {show.vertical && ( + + + {show.vertical.name} + + )} + + {show.venue?.name} + + + + +
+ + + {show.venue?.city}, {show.venue?.state} + +
+
+ + + ))} +
+
+ ))} +
+ ) +}