306 lines
16 KiB
TypeScript
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>
|
|
)
|
|
}
|