fediversion/frontend/app/bands/[slug]/page.tsx
fullsizemalt cf7748a980
Some checks failed
Deploy Fediversion / deploy (push) Failing after 1s
feat: Add band profile and musician profile pages with API endpoints and database support
2025-12-28 23:00:30 -08:00

306 lines
16 KiB
TypeScript

import { Metadata } from "next"
import { notFound } from "next/navigation"
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
import { Badge } from "@/components/ui/badge"
import { Separator } from "@/components/ui/separator"
import Link from "next/link"
import { Music, Calendar, MapPin, Users, ExternalLink, Globe } from "lucide-react"
interface BandPageProps {
params: {
slug: string
}
}
async function getBand(slug: string) {
const res = await fetch(`${process.env.INTERNAL_API_URL}/bands/${slug}`, {
next: { revalidate: 60 },
})
if (!res.ok) {
if (res.status === 404) return null
throw new Error("Failed to fetch band")
}
return res.json()
}
export async function generateMetadata({ params }: BandPageProps): Promise<Metadata> {
const data = await getBand(params.slug)
if (!data) return { title: "Band Not Found" }
return {
title: `${data.band.name} | Fediversion`,
description: data.band.description || `Band profile for ${data.band.name} on Fediversion.`,
}
}
export default async function BandPage({ params }: BandPageProps) {
const data = await getBand(params.slug)
if (!data) return notFound()
const { band, current_members, past_members, stats } = data
// Format origin location
const originParts = [band.origin_city, band.origin_state, band.origin_country].filter(Boolean)
const originLocation = originParts.join(", ")
return (
<div className="container py-8 space-y-8">
{/* Header */}
<div className="flex flex-col gap-4">
<div className="flex items-center gap-4">
{band.logo_url ? (
<img
src={band.logo_url}
alt={band.name}
className="w-24 h-24 rounded-lg object-cover border-2 border-primary/20"
/>
) : (
<div
className="w-24 h-24 rounded-lg flex items-center justify-center text-3xl font-bold text-white"
style={{ backgroundColor: band.accent_color || '#6366f1' }}
>
{band.name[0]}
</div>
)}
<div>
<h1 className="text-4xl font-bold tracking-tight">{band.name}</h1>
<div className="flex flex-wrap gap-3 mt-2 text-sm text-muted-foreground">
{originLocation && (
<span className="flex items-center gap-1">
<MapPin className="h-4 w-4" />
{originLocation}
</span>
)}
{band.formed_year && (
<span className="flex items-center gap-1">
<Calendar className="h-4 w-4" />
Formed {band.formed_year}
</span>
)}
</div>
</div>
</div>
{band.description && (
<p className="max-w-3xl text-lg text-muted-foreground leading-relaxed">
{band.long_description || band.description}
</p>
)}
{/* External Links */}
<div className="flex flex-wrap gap-2">
{band.website_url && (
<Link href={band.website_url} target="_blank" className="inline-flex items-center gap-1 px-3 py-1 rounded-full bg-accent hover:bg-accent/80 text-sm">
<Globe className="h-3 w-3" /> Website
</Link>
)}
{band.nugs_url && (
<Link href={band.nugs_url} target="_blank" className="inline-flex items-center gap-1 px-3 py-1 rounded-full bg-orange-500/20 hover:bg-orange-500/30 text-sm text-orange-600 dark:text-orange-400">
<Music className="h-3 w-3" /> Nugs.net
</Link>
)}
{band.relisten_url && (
<Link href={band.relisten_url} target="_blank" className="inline-flex items-center gap-1 px-3 py-1 rounded-full bg-blue-500/20 hover:bg-blue-500/30 text-sm text-blue-600 dark:text-blue-400">
<Music className="h-3 w-3" /> Relisten
</Link>
)}
{band.spotify_url && (
<Link href={band.spotify_url} target="_blank" className="inline-flex items-center gap-1 px-3 py-1 rounded-full bg-green-500/20 hover:bg-green-500/30 text-sm text-green-600 dark:text-green-400">
<Music className="h-3 w-3" /> Spotify
</Link>
)}
{band.wikipedia_url && (
<Link href={band.wikipedia_url} target="_blank" className="inline-flex items-center gap-1 px-3 py-1 rounded-full bg-gray-500/20 hover:bg-gray-500/30 text-sm">
<ExternalLink className="h-3 w-3" /> Wikipedia
</Link>
)}
</div>
</div>
<Separator />
{/* Stats Grid */}
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
<Card>
<CardContent className="pt-6 text-center">
<div className="text-3xl font-bold">{stats.total_shows.toLocaleString()}</div>
<div className="text-sm text-muted-foreground">Shows</div>
</CardContent>
</Card>
<Card>
<CardContent className="pt-6 text-center">
<div className="text-3xl font-bold">{stats.total_songs.toLocaleString()}</div>
<div className="text-sm text-muted-foreground">Songs</div>
</CardContent>
</Card>
<Card>
<CardContent className="pt-6 text-center">
<div className="text-3xl font-bold">{stats.total_venues.toLocaleString()}</div>
<div className="text-sm text-muted-foreground">Venues</div>
</CardContent>
</Card>
<Card>
<CardContent className="pt-6 text-center">
<div className="text-3xl font-bold">
{stats.first_show && stats.last_show
? new Date(stats.last_show).getFullYear() - new Date(stats.first_show).getFullYear() + 1
: '—'
}
</div>
<div className="text-sm text-muted-foreground">Active Years</div>
</CardContent>
</Card>
</div>
{/* Members Section */}
{(current_members.length > 0 || past_members.length > 0) && (
<>
<h2 className="text-2xl font-bold flex items-center gap-2">
<Users className="h-6 w-6" /> Members
</h2>
<Tabs defaultValue="current" className="space-y-6">
<TabsList>
<TabsTrigger value="current">
Current
<Badge variant="secondary" className="ml-2">{current_members.length}</Badge>
</TabsTrigger>
<TabsTrigger value="past">
Past
<Badge variant="secondary" className="ml-2">{past_members.length}</Badge>
</TabsTrigger>
</TabsList>
<TabsContent value="current" className="space-y-4">
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
{current_members.map((member: any) => (
<Link
key={member.id}
href={`/musicians/${member.slug}`}
className="block group"
>
<Card className="h-full transition-colors group-hover:bg-accent/50">
<CardContent className="pt-6 flex items-center gap-4">
{member.image_url ? (
<img
src={member.image_url}
alt={member.name}
className="w-16 h-16 rounded-full object-cover"
/>
) : (
<div className="w-16 h-16 rounded-full bg-accent flex items-center justify-center text-xl font-bold">
{member.name[0]}
</div>
)}
<div>
<div className="font-semibold group-hover:text-primary transition-colors">
{member.name}
</div>
<div className="text-sm text-muted-foreground">
{member.role || member.primary_instrument}
</div>
{member.start_date && (
<div className="text-xs text-muted-foreground">
Since {new Date(member.start_date).getFullYear()}
</div>
)}
</div>
</CardContent>
</Card>
</Link>
))}
{current_members.length === 0 && (
<div className="col-span-full py-12 text-center text-muted-foreground">
No current members listed.
</div>
)}
</div>
</TabsContent>
<TabsContent value="past" className="space-y-4">
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
{past_members.map((member: any) => (
<Link
key={member.id}
href={`/musicians/${member.slug}`}
className="block group"
>
<Card className="h-full transition-colors group-hover:bg-accent/50">
<CardContent className="pt-6 flex items-center gap-4">
{member.image_url ? (
<img
src={member.image_url}
alt={member.name}
className="w-16 h-16 rounded-full object-cover opacity-75"
/>
) : (
<div className="w-16 h-16 rounded-full bg-accent flex items-center justify-center text-xl font-bold opacity-75">
{member.name[0]}
</div>
)}
<div>
<div className="font-semibold group-hover:text-primary transition-colors">
{member.name}
</div>
<div className="text-sm text-muted-foreground">
{member.role || member.primary_instrument}
</div>
{(member.start_date || member.end_date) && (
<div className="text-xs text-muted-foreground">
{member.start_date ? new Date(member.start_date).getFullYear() : '?'}
{' - '}
{member.end_date ? new Date(member.end_date).getFullYear() : '?'}
</div>
)}
</div>
</CardContent>
</Card>
</Link>
))}
{past_members.length === 0 && (
<div className="col-span-full py-12 text-center text-muted-foreground">
No past members listed.
</div>
)}
</div>
</TabsContent>
</Tabs>
</>
)}
{/* Quick Links */}
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
<Link href={`/${band.slug}/shows`} className="block">
<Card className="hover:bg-accent/50 transition-colors">
<CardContent className="pt-6 text-center">
<span className="font-semibold">All Shows</span>
</CardContent>
</Card>
</Link>
<Link href={`/${band.slug}/songs`} className="block">
<Card className="hover:bg-accent/50 transition-colors">
<CardContent className="pt-6 text-center">
<span className="font-semibold">All Songs</span>
</CardContent>
</Card>
</Link>
<Link href={`/${band.slug}/venues`} className="block">
<Card className="hover:bg-accent/50 transition-colors">
<CardContent className="pt-6 text-center">
<span className="font-semibold">All Venues</span>
</CardContent>
</Card>
</Link>
<Link href={`/${band.slug}/performances`} className="block">
<Card className="hover:bg-accent/50 transition-colors">
<CardContent className="pt-6 text-center">
<span className="font-semibold">Top Performances</span>
</CardContent>
</Card>
</Link>
</div>
</div>
)
}