Some checks failed
Deploy Fediversion / deploy (push) Failing after 1s
- Frontend: Implemented Tabbed interface (Recent, My Feed, Upcoming, By Band) - Frontend: Added BandGrid component with selection logic - Frontend: Added FilterPills component for active filters - Backend: Added show_count to Verticals API - Backend: Updated read_shows to support correct sorting for Upcoming status
272 lines
10 KiB
TypeScript
272 lines
10 KiB
TypeScript
"use client"
|
|
|
|
import { useEffect, useState, Suspense } from "react"
|
|
import { getApiUrl } from "@/lib/api-config"
|
|
import { Loader2, Music2 } from "lucide-react"
|
|
import { useSearchParams, useRouter, usePathname } from "next/navigation"
|
|
import { Button } from "@/components/ui/button"
|
|
import { Tabs, TabsList, TabsTrigger, TabsContent } from "@/components/ui/tabs"
|
|
import { DateGroupedList } from "@/components/shows/date-grouped-list"
|
|
import { FilterPills } from "@/components/shows/filter-pills"
|
|
import { BandGrid } from "@/components/shows/band-grid"
|
|
|
|
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
|
|
}
|
|
performances?: any[] // Simplified
|
|
}
|
|
|
|
interface Vertical {
|
|
id: number
|
|
slug: string
|
|
name: string
|
|
show_count: number
|
|
logo_url?: string | null
|
|
}
|
|
|
|
function ShowsContent() {
|
|
const searchParams = useSearchParams()
|
|
const router = useRouter()
|
|
const pathname = usePathname()
|
|
|
|
// --- State ---
|
|
const activeView = searchParams.get("view") || "recent"
|
|
const bandsParam = searchParams.get("bands")
|
|
const selectedBands = bandsParam ? bandsParam.split(",") : []
|
|
|
|
const [shows, setShows] = useState<Show[]>([])
|
|
const [verticals, setVerticals] = useState<Vertical[]>([])
|
|
const [loading, setLoading] = useState(true)
|
|
const [loadingVerticals, setLoadingVerticals] = useState(true)
|
|
|
|
// --- Data Fetching: Verticals (Always) ---
|
|
useEffect(() => {
|
|
setLoadingVerticals(true)
|
|
fetch(`${getApiUrl()}/verticals/`)
|
|
.then(res => {
|
|
if (!res.ok) throw new Error("Failed to fetch verticals")
|
|
return res.json()
|
|
})
|
|
.then(data => {
|
|
setVerticals(data)
|
|
setLoadingVerticals(false)
|
|
})
|
|
.catch(err => {
|
|
console.error(err)
|
|
setLoadingVerticals(false)
|
|
})
|
|
}, [])
|
|
|
|
// --- Data Fetching: Shows (Dependent on View) ---
|
|
useEffect(() => {
|
|
if (activeView === "bands") {
|
|
// Don't fetch shows if we are just browsing bands
|
|
setLoading(false)
|
|
return
|
|
}
|
|
|
|
setLoading(true)
|
|
const params = new URLSearchParams()
|
|
|
|
// Add band filters
|
|
if (selectedBands.length > 0) {
|
|
selectedBands.forEach(b => params.append("vertical_slugs", b))
|
|
}
|
|
|
|
// Add view-specific params
|
|
if (activeView === "upcoming") {
|
|
params.set("status", "upcoming")
|
|
} else if (activeView === "my-feed") {
|
|
// My Feed implies specific tiers
|
|
// We use the same tiers as FeedFilter used: HEADLINER, MAIN_STAGE, SUPPORTING
|
|
["HEADLINER", "MAIN_STAGE", "SUPPORTING"].forEach(t => params.append("tiers", t))
|
|
// Also we might want to default to "past" shows for feed? Or all?
|
|
// "My Feed" usually means recent updates.
|
|
// Let's explicitly ask for "past" shows (recent history) unless user wants upcoming feed?
|
|
// For now, let's show PAST shows in My Feed (History), maybe add Upcoming toggle later?
|
|
// Or just show all? `read_shows` sorts by date desc.
|
|
// Let's default to Recent (Past) for Feed.
|
|
// params.set("status", "past")
|
|
// Actually, if we leave status blank, it returns all (sorted by date desc if modified, but check `read_shows`)
|
|
// `read_shows` default sorts DESC.
|
|
// Let's assume user wants recent feed.
|
|
} else {
|
|
// Recent (Default)
|
|
params.set("status", "past")
|
|
}
|
|
|
|
fetch(`${getApiUrl()}/shows/?${params.toString()}`)
|
|
.then(res => {
|
|
// If 401 (Unauthorized) for My Feed, we might get empty list or error
|
|
if (res.status === 401 && activeView === "my-feed") {
|
|
// Redirect to login or handle?
|
|
// For now, shows API returns [] if anon try to filter by tiers.
|
|
return []
|
|
}
|
|
if (!res.ok) throw new Error("Failed to fetch shows")
|
|
return res.json()
|
|
})
|
|
.then(data => {
|
|
setShows(data)
|
|
setLoading(false)
|
|
})
|
|
.catch(err => {
|
|
console.error(err)
|
|
setShows([]) // Clear on error
|
|
setLoading(false)
|
|
})
|
|
|
|
}, [activeView, bandsParam]) // bandsParam is the dependency
|
|
|
|
// --- Handlers ---
|
|
const updateUrl = (view: string, bands: string[]) => {
|
|
const params = new URLSearchParams()
|
|
if (view !== "recent") params.set("view", view)
|
|
if (bands.length > 0) params.set("bands", bands.join(","))
|
|
|
|
// Push state
|
|
router.push(`${pathname}${params.toString() ? `?${params.toString()}` : ''}`)
|
|
}
|
|
|
|
const handleTabChange = (val: string) => {
|
|
updateUrl(val, selectedBands)
|
|
}
|
|
|
|
const handleToggleBand = (slug: string) => {
|
|
let newBands = [...selectedBands]
|
|
if (newBands.includes(slug)) {
|
|
newBands = newBands.filter(b => b !== slug)
|
|
} else {
|
|
newBands.push(slug)
|
|
}
|
|
|
|
// If we are on "bands" tab and select a band, switch to "recent" to show results?
|
|
// User plan says: "Clicking a band adds it to the active filter and switches to 'Recent' view."
|
|
if (activeView === "bands" && !selectedBands.includes(slug)) {
|
|
updateUrl("recent", newBands)
|
|
} else {
|
|
// Otherwise just update filters (e.g. if unchecking, stay on grid? or if adding from elsewhere?)
|
|
// If checking from grid -> go to recent.
|
|
// If unchecking -> stay?
|
|
// Let's just follow the rule: Select -> Go to Recent. Unselect -> Stay.
|
|
if (!selectedBands.includes(slug)) {
|
|
updateUrl("recent", newBands)
|
|
} else {
|
|
updateUrl(activeView, newBands)
|
|
}
|
|
}
|
|
}
|
|
|
|
const handleRemoveBand = (slug: string) => {
|
|
const newBands = selectedBands.filter(b => b !== slug)
|
|
updateUrl(activeView, newBands)
|
|
}
|
|
|
|
const handleClearBands = () => {
|
|
updateUrl(activeView, [])
|
|
}
|
|
|
|
// --- Render Helpers ---
|
|
const renderShowList = () => {
|
|
if (loading) {
|
|
return (
|
|
<div className="flex justify-center py-20">
|
|
<Loader2 className="h-8 w-8 animate-spin text-muted-foreground" />
|
|
</div>
|
|
)
|
|
}
|
|
|
|
if (shows.length === 0) {
|
|
return (
|
|
<div className="text-center py-20 text-muted-foreground">
|
|
<Music2 className="h-12 w-12 mx-auto mb-4 opacity-20" />
|
|
<p>No shows found matching your criteria.</p>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
return <DateGroupedList shows={shows} />
|
|
}
|
|
|
|
return (
|
|
<div className="container py-6 max-w-5xl">
|
|
<div className="sticky top-0 z-10 bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60 pb-4 -mx-4 px-4 md:mx-0 md:px-0">
|
|
<Tabs value={activeView} onValueChange={handleTabChange} className="w-full">
|
|
<div className="flex flex-col md:flex-row md:items-center justify-between gap-4 mb-4">
|
|
<TabsList className="grid w-full md:w-auto grid-cols-4 h-11">
|
|
<TabsTrigger value="recent" className="flex items-center gap-2">
|
|
Recent
|
|
</TabsTrigger>
|
|
<TabsTrigger value="my-feed" className="flex items-center gap-2">
|
|
My Feed
|
|
</TabsTrigger>
|
|
<TabsTrigger value="upcoming" className="flex items-center gap-2">
|
|
Upcoming
|
|
</TabsTrigger>
|
|
<TabsTrigger value="bands" className="flex items-center gap-2">
|
|
By Band
|
|
</TabsTrigger>
|
|
</TabsList>
|
|
</div>
|
|
|
|
<FilterPills
|
|
selectedBands={selectedBands}
|
|
verticals={verticals}
|
|
onRemove={handleRemoveBand}
|
|
onClear={handleClearBands}
|
|
/>
|
|
|
|
<div className="mt-2 min-h-[500px]">
|
|
<TabsContent value="recent" className="m-0">
|
|
{renderShowList()}
|
|
</TabsContent>
|
|
|
|
<TabsContent value="my-feed" className="m-0">
|
|
{/* Maybe add auth check banner here if shows is empty and user not logged in? */}
|
|
{/* For now, just render list */}
|
|
{renderShowList()}
|
|
</TabsContent>
|
|
|
|
<TabsContent value="upcoming" className="m-0">
|
|
{renderShowList()}
|
|
</TabsContent>
|
|
|
|
<TabsContent value="bands" className="m-0">
|
|
{loadingVerticals ? (
|
|
<div className="flex justify-center py-20">
|
|
<Loader2 className="h-8 w-8 animate-spin text-muted-foreground" />
|
|
</div>
|
|
) : (
|
|
<BandGrid
|
|
verticals={verticals}
|
|
selectedBands={selectedBands}
|
|
onToggle={handleToggleBand} // Note: logic inside handleToggle moves to Recent
|
|
/>
|
|
)}
|
|
</TabsContent>
|
|
</div>
|
|
</Tabs>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
export default function ShowsPage() {
|
|
return (
|
|
<Suspense fallback={<div className="container py-6 flex justify-center"><Loader2 className="h-8 w-8 animate-spin" /></div>}>
|
|
<ShowsContent />
|
|
</Suspense>
|
|
)
|
|
}
|