241 lines
12 KiB
TypeScript
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>
|
|
)
|
|
}
|