fediversion/frontend/app/musicians/[slug]/page.tsx
fullsizemalt d4f6f60df6
Some checks failed
Deploy Fediversion / deploy (push) Failing after 1s
fix: update dynamic routes for Next.js 16 async params API
2025-12-29 22:16:11 -08:00

241 lines
12 KiB
TypeScript

import { Metadata } from "next"
import { notFound } from "next/navigation"
import { Card, CardContent } 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, Instagram } from "lucide-react"
interface MusicianPageProps {
params: Promise<{
slug: string
}>
}
async function getMusician(slug: string) {
const res = await fetch(`${process.env.INTERNAL_API_URL}/musicians/${slug}`, {
next: { revalidate: 60 },
})
if (!res.ok) {
if (res.status === 404) return null
throw new Error("Failed to fetch musician")
}
return res.json()
}
export async function generateMetadata({ params }: MusicianPageProps): Promise<Metadata> {
const { slug } = await params
const data = await getMusician(slug)
if (!data) return { title: "Musician Not Found" }
return {
title: `${data.musician.name} | Fediversion`,
description: data.musician.bio || `Musician profile for ${data.musician.name} on Fediversion.`,
}
}
export default async function MusicianPage({ params }: MusicianPageProps) {
const { slug } = await params
const data = await getMusician(slug)
if (!data) return notFound()
const { musician, bands, guest_appearances, sit_in_summary, stats } = data
return (
<div className="container py-8 space-y-8">
{/* Header */}
<div className="flex flex-col gap-4">
<div className="flex items-center gap-4">
{musician.image_url ? (
<img
src={musician.image_url}
alt={musician.name}
className="w-24 h-24 rounded-full object-cover border-2 border-primary/20"
/>
) : (
<div className="w-24 h-24 rounded-full bg-accent flex items-center justify-center text-3xl font-bold text-muted-foreground">
{musician.name[0]}
</div>
)}
<div>
<h1 className="text-4xl font-bold tracking-tight">{musician.name}</h1>
{musician.primary_instrument && (
<p className="text-muted-foreground flex items-center gap-1">
<Music className="h-4 w-4" />
{musician.primary_instrument}
</p>
)}
</div>
</div>
{musician.bio && (
<p className="max-w-3xl text-lg text-muted-foreground leading-relaxed">
{musician.bio}
</p>
)}
{/* External Links */}
<div className="flex flex-wrap gap-2">
{musician.website_url && (
<Link href={musician.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>
)}
{musician.instagram_url && (
<Link href={musician.instagram_url} target="_blank" className="inline-flex items-center gap-1 px-3 py-1 rounded-full bg-pink-500/20 hover:bg-pink-500/30 text-sm text-pink-600 dark:text-pink-400">
<Instagram className="h-3 w-3" /> Instagram
</Link>
)}
{musician.wikipedia_url && (
<Link href={musician.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 */}
<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_bands || 0}</div>
<div className="text-sm text-muted-foreground">Bands</div>
</CardContent>
</Card>
<Card>
<CardContent className="pt-6 text-center">
<div className="text-3xl font-bold">{stats?.current_bands || 0}</div>
<div className="text-sm text-muted-foreground">Current</div>
</CardContent>
</Card>
<Card>
<CardContent className="pt-6 text-center">
<div className="text-3xl font-bold">{stats?.total_sit_ins || 0}</div>
<div className="text-sm text-muted-foreground">Sit-Ins</div>
</CardContent>
</Card>
<Card>
<CardContent className="pt-6 text-center">
<div className="text-3xl font-bold">{stats?.bands_sat_in_with || 0}</div>
<div className="text-sm text-muted-foreground">Bands Sat In With</div>
</CardContent>
</Card>
</div>
{/* Band History */}
{bands && bands.length > 0 && (
<>
<h2 className="text-2xl font-bold flex items-center gap-2">
<Users className="h-6 w-6" /> Band History
</h2>
<div className="grid gap-4 md:grid-cols-2">
{bands.map((band: any, i: number) => (
<Link
key={i}
href={`/bands/${band.artist_slug || band.band_slug}`}
className="block group"
>
<Card className={`h-full transition-colors group-hover:bg-accent/50 ${band.is_current ? 'border-primary/50' : ''}`}>
<CardContent className="pt-6 flex items-center justify-between">
<div>
<div className="font-semibold group-hover:text-primary transition-colors flex items-center gap-2">
{band.artist_name}
{band.is_current && (
<Badge variant="default" className="text-xs">Current</Badge>
)}
</div>
<div className="text-sm text-muted-foreground">
{band.role}
</div>
</div>
<div className="text-sm text-muted-foreground text-right">
{band.start_date?.split('-')[0] || '?'}
{' - '}
{band.is_current ? 'Present' : (band.end_date?.split('-')[0] || '?')}
</div>
</CardContent>
</Card>
</Link>
))}
</div>
</>
)}
{/* Sit-In Summary */}
{sit_in_summary && sit_in_summary.length > 0 && (
<>
<h2 className="text-2xl font-bold flex items-center gap-2">
<Music className="h-6 w-6" /> Sit-In Summary
</h2>
<div className="grid gap-4 md:grid-cols-3 lg:grid-cols-4">
{sit_in_summary.map((band: any, i: number) => (
<Link
key={i}
href={`/${band.vertical_slug}`}
className="block group"
>
<Card className="h-full transition-colors group-hover:bg-accent/50">
<CardContent className="pt-6 text-center">
<div className="font-semibold group-hover:text-primary transition-colors">
{band.vertical_name}
</div>
<div className="text-2xl font-bold text-primary">
{band.count}
</div>
<div className="text-xs text-muted-foreground">
sit-in{band.count !== 1 ? 's' : ''}
</div>
</CardContent>
</Card>
</Link>
))}
</div>
</>
)}
{/* Recent Guest Appearances */}
{guest_appearances && guest_appearances.length > 0 && (
<>
<h2 className="text-2xl font-bold">Recent Guest Appearances</h2>
<div className="border rounded-lg">
<div className="divide-y">
{guest_appearances.slice(0, 20).map((appearance: any, i: number) => (
<div key={i} className="p-4 flex flex-col sm:flex-row sm:items-center justify-between gap-4 hover:bg-accent/50 transition-colors">
<div>
<div className="flex items-center gap-2">
<Badge variant="outline">{appearance.vertical_name}</Badge>
<Link
href={`/shows/${appearance.performance_slug?.split('-').slice(0, 4).join('-') || '#'}`}
className="font-semibold hover:underline"
>
{appearance.show_date}
</Link>
</div>
{appearance.instrument && (
<p className="text-xs text-muted-foreground mt-1">
Playing: {appearance.instrument}
</p>
)}
</div>
<div className="text-sm">
<span className="text-muted-foreground">Sat in on: </span>
<span className="font-medium">{appearance.song_title}</span>
</div>
</div>
))}
</div>
</div>
{guest_appearances.length > 20 && (
<p className="text-sm text-muted-foreground text-center">
Showing 20 of {guest_appearances.length} appearances
</p>
)}
</>
)}
</div>
)
}