fediversion/frontend/components/home/tiered-band-list.tsx
fullsizemalt 0c7df04b92
Some checks failed
Deploy Fediversion / deploy (push) Failing after 1s
feat(bands): filter ignored bands from home feed
2025-12-29 21:51:20 -08:00

158 lines
6.9 KiB
TypeScript

"use client"
import { useEffect, useState } from "react"
import Link from "next/link"
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
import { useAuth } from "@/contexts/auth-context"
import { getApiUrl } from "@/lib/api-config"
interface Vertical {
id: number
name: string
slug: string
description: string | null
}
interface Preference {
vertical_id: number
tier: string
}
export function TieredBandList({ initialVerticals }: { initialVerticals: Vertical[] }) {
const [verticals, setVerticals] = useState<Vertical[]>(initialVerticals)
const [preferences, setPreferences] = useState<Preference[]>([])
const { token, isAuthenticated } = useAuth()
const [loading, setLoading] = useState(false)
// Default headliners for guests
const defaultHeadliners = ["phish", "grateful-dead", "dead-and-company", "billy-strings", "goose"]
useEffect(() => {
if (isAuthenticated && token) {
setLoading(true)
fetch(`${process.env.NEXT_PUBLIC_API_URL || "https://api.fediversion.org"}/verticals/preferences/me`, {
headers: { Authorization: `Bearer ${token}` }
})
.then(res => {
if (res.ok) return res.json()
return []
})
.then((data: any[]) => {
// Map response to preference format
const prefs = data.map(p => ({
vertical_id: p.vertical_id,
tier: p.tier || "main_stage"
}))
setPreferences(prefs)
})
.catch(err => console.error("Failed to fetch preferences", err))
.finally(() => setLoading(false))
}
}, [isAuthenticated, token])
// Determine tiers
let headliners: Vertical[] = []
let others: Vertical[] = []
if (isAuthenticated && preferences.length > 0) {
// User has preferences
const headlinerIds = preferences.filter(p => p.tier === "headliner").map(p => p.vertical_id)
const ignoredIds = preferences.filter(p => p.tier === "ignored").map(p => p.vertical_id)
// Filter out ignored bands from display
const visibleVerticals = verticals.filter(v => !ignoredIds.includes(v.id))
headliners = visibleVerticals.filter(v => headlinerIds.includes(v.id))
others = visibleVerticals.filter(v => !headlinerIds.includes(v.id))
// If user has NO headliners set but HAS preferences, maybe show their top priority?
// Or just defaults.
if (headliners.length === 0) {
// Fallback to top 3 priority?
// For now, let's mix in defaults if empty? No, respect user choice.
// If they selected bands but no headliners, all are "others" (Touring Acts).
}
// Optionally filter "others" to only show subscribed bands?
// The requirement says "Main Stage: Standard following".
// "Supporting: Hidden unless relevant".
// Let's show subscribed bands as "Your Bands" and others as "Discover".
// But for simple "Tiered Band Preferences" UI, let's keep it simple:
// Headliners = Tier 'headliner'
// Touring Acts = Everyone else (excluding ignored)
} else {
// Guest or no prefs
headliners = verticals.filter(v => defaultHeadliners.includes(v.slug))
others = verticals.filter(v => !defaultHeadliners.includes(v.slug))
}
return (
<section className="space-y-12 max-w-6xl mx-auto px-4">
{/* Headliners */}
{headliners.length > 0 && (
<div className="space-y-6">
<div className="flex items-center gap-4">
<div className="h-px flex-1 bg-border" />
<h2 className="text-2xl font-bold tracking-tight text-muted-foreground uppercase text-sm">
{isAuthenticated && preferences.length > 0 ? "Your Headliners" : "Headliners"}
</h2>
<div className="h-px flex-1 bg-border" />
</div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{headliners.map((vertical) => (
<VerticalCard key={vertical.slug} vertical={vertical} featured />
))}
</div>
</div>
)}
{/* Other Acts */}
{others.length > 0 && (
<div className="space-y-6">
<div className="flex items-center gap-4">
<div className="h-px flex-1 bg-border" />
<h2 className="text-2xl font-bold tracking-tight text-muted-foreground uppercase text-sm">Touring Acts</h2>
<div className="h-px flex-1 bg-border" />
</div>
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4">
{others.map((vertical) => (
<VerticalCard key={vertical.slug} vertical={vertical} />
))}
</div>
</div>
)}
</section>
)
}
function VerticalCard({ vertical, featured = false }: { vertical: Vertical, featured?: boolean }) {
return (
<Link href={`/${vertical.slug}`} className="group block h-full">
<Card className={`h-full transition-all duration-300 hover:scale-[1.02] hover:shadow-xl border-muted/60 ${featured ? 'bg-card' : 'bg-muted/30 hover:bg-card'}`}>
<CardHeader>
<CardTitle className={`flex items-center justify-between ${featured ? 'text-2xl' : 'text-lg'}`}>
<span className="capitalize">{vertical.name}</span>
{featured && (
<span className="text-muted-foreground opacity-0 group-hover:opacity-100 transition-opacity"></span>
)}
</CardTitle>
{featured && vertical.description && (
<CardDescription className="line-clamp-2 mt-2">
{vertical.description}
</CardDescription>
)}
</CardHeader>
{featured && (
<CardContent>
<div className="flex flex-wrap gap-2">
<span className="text-xs font-medium px-2 py-1 rounded-full bg-primary/10 text-primary">Shows</span>
<span className="text-xs font-medium px-2 py-1 rounded-full bg-primary/10 text-primary">Setlists</span>
<span className="text-xs font-medium px-2 py-1 rounded-full bg-primary/10 text-primary">Stats</span>
</div>
</CardContent>
)}
</Card>
</Link>
)
}