feat(frontend): implement date-grouped show list and band filter for All Bands view
Some checks failed
Deploy Fediversion / deploy (push) Failing after 1s

This commit is contained in:
fullsizemalt 2025-12-29 01:18:28 -08:00
parent af9fcd4060
commit 7c9bcd81a6
3 changed files with 249 additions and 58 deletions

View file

@ -1,19 +1,24 @@
"use client" "use client"
import { useEffect, useState, Suspense } from "react" import { useEffect, useState, Suspense, useMemo } from "react"
import { getApiUrl } from "@/lib/api-config" import { getApiUrl } from "@/lib/api-config"
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card" import { Loader2, Calendar } from "lucide-react"
import Link from "next/link"
import { Calendar, MapPin, Loader2, Youtube } from "lucide-react"
import { Skeleton } from "@/components/ui/skeleton" import { Skeleton } from "@/components/ui/skeleton"
import { useSearchParams } from "next/navigation" import { useSearchParams } from "next/navigation"
import { Button } from "@/components/ui/button" 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 { interface Show {
id: number id: number
slug?: string slug?: string
date: string date: string
youtube_link?: string youtube_link?: string
vertical?: {
name: string
slug: string
}
venue: { venue: {
id: number id: number
name: string name: string
@ -28,6 +33,7 @@ function ShowsContent() {
const [shows, setShows] = useState<Show[]>([]) const [shows, setShows] = useState<Show[]>([])
const [loading, setLoading] = useState(true) const [loading, setLoading] = useState(true)
const [selectedBands, setSelectedBands] = useState<string[]>([])
useEffect(() => { useEffect(() => {
const url = `${getApiUrl()}/shows/?limit=2000&status=past${year ? `&year=${year}` : ''}` const url = `${getApiUrl()}/shows/?limit=2000&status=past${year ? `&year=${year}` : ''}`
@ -44,6 +50,14 @@ function ShowsContent() {
.finally(() => setLoading(false)) .finally(() => setLoading(false))
}, [year]) }, [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) { if (loading) {
return ( return (
<div className="container py-10 space-y-8"> <div className="container py-10 space-y-8">
@ -54,20 +68,7 @@ function ShowsContent() {
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3"> <div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
{Array.from({ length: 12 }).map((_, i) => ( {Array.from({ length: 12 }).map((_, i) => (
<Card key={i} className="h-full border-muted/40"> <Skeleton key={i} className="h-32 w-full rounded-lg" />
<CardHeader>
<div className="flex items-center gap-2">
<Skeleton className="h-5 w-5 rounded-full" />
<Skeleton className="h-5 w-3/4" />
</div>
</CardHeader>
<CardContent>
<div className="flex items-center gap-2">
<Skeleton className="h-4 w-4 rounded-full" />
<Skeleton className="h-4 w-1/2" />
</div>
</CardContent>
</Card>
))} ))}
</div> </div>
</div> </div>
@ -76,55 +77,30 @@ function ShowsContent() {
return ( return (
<div className="container py-10 space-y-8 animate-in fade-in duration-700"> <div className="container py-10 space-y-8 animate-in fade-in duration-700">
<div className="flex flex-col gap-2"> <div className="flex flex-col gap-4">
<div className="flex justify-between items-start"> <div className="flex flex-col md:flex-row md:items-center justify-between gap-4">
<div> <div>
<h1 className="text-3xl font-bold tracking-tight">Shows</h1> <h1 className="text-3xl font-bold tracking-tight">Shows</h1>
<p className="text-muted-foreground"> <p className="text-muted-foreground">
Browse the complete archive of performances. Browse the complete archive of performances.
</p> </p>
</div> </div>
<Link href="/shows/upcoming"> <div className="flex items-center gap-2">
<Button variant="outline" className="gap-2"> <BandFilter
<Calendar className="h-4 w-4" /> selected={selectedBands}
Upcoming Shows onChange={setSelectedBands}
</Button> />
</Link> <Link href="/shows/upcoming">
<Button variant="outline" className="gap-2">
<Calendar className="h-4 w-4" />
Upcoming
</Button>
</Link>
</div>
</div> </div>
</div> </div>
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3"> <DateGroupedList shows={filteredShows} />
{shows.map((show) => (
<Link key={show.id} href={`/shows/${show.slug}`} className="block group">
<Card className="h-full transition-all duration-300 hover:scale-[1.02] hover:shadow-lg group-hover:border-primary/50 relative">
{show.youtube_link && (
<div className="absolute top-2 right-2 bg-red-500/10 text-red-500 p-1.5 rounded-full" title="Full show video available">
<Youtube className="h-4 w-4" />
</div>
)}
<CardHeader>
<CardTitle className="flex items-center gap-2 group-hover:text-primary transition-colors">
<Calendar className="h-5 w-5 text-muted-foreground group-hover:text-primary/70 transition-colors" />
{new Date(show.date).toLocaleDateString(undefined, {
weekday: 'long',
year: 'numeric',
month: 'long',
day: 'numeric'
})}
</CardTitle>
</CardHeader>
<CardContent>
<div className="flex items-center gap-2 text-muted-foreground group-hover:text-foreground transition-colors">
<MapPin className="h-4 w-4" />
<span>
{show.venue?.name}, {show.venue?.city}, {show.venue?.state}
</span>
</div>
</CardContent>
</Card>
</Link>
))}
</div>
</div> </div>
) )
} }

View file

@ -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 (
<div className="flex items-center gap-2">
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="outline" size="sm" className="h-8 gap-2">
<Filter className="h-3.5 w-3.5" />
Bands
{selected.length > 0 && (
<span className="ml-1 rounded-full bg-primary/10 px-1.5 py-0.5 text-xs font-semibold text-primary">
{selected.length}
</span>
)}
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-56">
<DropdownMenuLabel>Filter by Band</DropdownMenuLabel>
<DropdownMenuSeparator />
{VERTICALS.map((vertical) => (
<DropdownMenuCheckboxItem
key={vertical.slug}
checked={selected.includes(vertical.slug)}
onCheckedChange={() => toggleVertical(vertical.slug)}
>
{vertical.name}
</DropdownMenuCheckboxItem>
))}
{selected.length > 0 && (
<>
<DropdownMenuSeparator />
<DropdownMenuCheckboxItem
checked={false}
onCheckedChange={clearFilters}
className="text-red-500 focus:text-red-500"
>
Clear Filters
</DropdownMenuCheckboxItem>
</>
)}
</DropdownMenuContent>
</DropdownMenu>
{/* Inline filters for desktop - optionally could show these next to dropdown or instead of on large screens */}
<div className="hidden lg:flex items-center gap-2 ml-2">
{selected.length > 0 && (
<Button
variant="ghost"
size="sm"
onClick={clearFilters}
className="h-8 text-muted-foreground hover:text-foreground"
>
Reset
</Button>
)}
</div>
</div>
)
}

View file

@ -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 (
<div className="text-center py-12 text-muted-foreground">
No shows found matching your filters.
</div>
)
}
// Group shows by date
const groupedShows: Record<string, Show[]> = {}
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 (
<div className="space-y-8">
{uniqueDates.map(dateStr => (
<div key={dateStr} className="space-y-4">
<div className="sticky top-16 z-10 bg-background/95 backdrop-blur py-2 border-b">
<h2 className="text-xl font-bold flex items-center gap-2">
<Calendar className="h-5 w-5 text-primary" />
{dateStr}
</h2>
</div>
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
{groupedShows[dateStr].map(show => (
<Link key={show.id} href={`/shows/${show.slug}`} className="block group">
<Card className="h-full transition-all duration-300 hover:scale-[1.02] hover:shadow-lg group-hover:border-primary/50 relative overflow-hidden">
{/* Vertical Stripe/Badge */}
{show.vertical && (
<div className="absolute top-0 left-0 w-1 h-full bg-primary/50 group-hover:bg-primary transition-colors" />
)}
{show.youtube_link && (
<div className="absolute top-2 right-2 bg-red-500/10 text-red-500 p-1.5 rounded-full" title="Full show video available">
<Youtube className="h-4 w-4" />
</div>
)}
<CardHeader className="pl-5 pb-2"> {/* Added padding-left for stripe */}
<CardTitle className="text-lg flex flex-col gap-1">
{show.vertical && (
<span className="text-xs font-medium text-primary uppercase tracking-wider flex items-center gap-1">
<Music2 className="h-3 w-3" />
{show.vertical.name}
</span>
)}
<span className="group-hover:text-primary transition-colors">
{show.venue?.name}
</span>
</CardTitle>
</CardHeader>
<CardContent className="pl-5 pt-0">
<div className="flex items-center gap-2 text-sm text-muted-foreground group-hover:text-foreground transition-colors">
<MapPin className="h-3.5 w-3.5" />
<span>
{show.venue?.city}, {show.venue?.state}
</span>
</div>
</CardContent>
</Card>
</Link>
))}
</div>
</div>
))}
</div>
)
}