161 lines
7.1 KiB
TypeScript
161 lines
7.1 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"
|
|
|
|
interface ArtistPageProps {
|
|
params: {
|
|
slug: string
|
|
}
|
|
}
|
|
|
|
async function getArtist(slug: string) {
|
|
const res = await fetch(`${process.env.INTERNAL_API_URL}/artists/${slug}`, {
|
|
next: { revalidate: 60 },
|
|
})
|
|
|
|
if (!res.ok) {
|
|
if (res.status === 404) return null
|
|
throw new Error("Failed to fetch artist")
|
|
}
|
|
|
|
return res.json()
|
|
}
|
|
|
|
export async function generateMetadata({ params }: ArtistPageProps): Promise<Metadata> {
|
|
const data = await getArtist(params.slug)
|
|
if (!data) return { title: "Artist Not Found" }
|
|
|
|
return {
|
|
title: `${data.artist.name} | Fediversion`,
|
|
description: data.artist.bio || `Artist profile for ${data.artist.name} on Fediversion.`,
|
|
}
|
|
}
|
|
|
|
export default async function ArtistPage({ params }: ArtistPageProps) {
|
|
const data = await getArtist(params.slug)
|
|
if (!data) return notFound()
|
|
|
|
const { artist, covers, guest_appearances } = 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">
|
|
{artist.image_url ? (
|
|
<img
|
|
src={artist.image_url}
|
|
alt={artist.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">
|
|
{artist.name[0]}
|
|
</div>
|
|
)}
|
|
<div>
|
|
<h1 className="text-4xl font-bold tracking-tight">{artist.name}</h1>
|
|
{artist.instrument && (
|
|
<p className="text-muted-foreground">{artist.instrument}</p>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
{artist.bio && (
|
|
<p className="max-w-3xl text-lg text-muted-foreground leading-relaxed">
|
|
{artist.bio}
|
|
</p>
|
|
)}
|
|
</div>
|
|
|
|
<Separator />
|
|
|
|
<Tabs defaultValue="covers" className="space-y-6">
|
|
<TabsList>
|
|
<TabsTrigger value="covers">
|
|
Covers
|
|
<Badge variant="secondary" className="ml-2">{covers.length}</Badge>
|
|
</TabsTrigger>
|
|
<TabsTrigger value="guests">
|
|
Guest Appearances
|
|
<Badge variant="secondary" className="ml-2">{guest_appearances.length}</Badge>
|
|
</TabsTrigger>
|
|
</TabsList>
|
|
|
|
<TabsContent value="covers" className="space-y-4">
|
|
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
|
|
{covers.map((song: any) => (
|
|
<Link
|
|
key={song.id}
|
|
href={`/songs/${song.slug}`}
|
|
className="block group"
|
|
>
|
|
<Card className="h-full transition-colors group-hover:bg-accent/50">
|
|
<CardHeader>
|
|
<CardTitle className="group-hover:text-primary transition-colors">
|
|
{song.title}
|
|
</CardTitle>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<p className="text-sm text-muted-foreground">
|
|
Covered by Goose
|
|
</p>
|
|
</CardContent>
|
|
</Card>
|
|
</Link>
|
|
))}
|
|
{covers.length === 0 && (
|
|
<div className="col-span-full py-12 text-center text-muted-foreground">
|
|
No known covers by this artist.
|
|
</div>
|
|
)}
|
|
</div>
|
|
</TabsContent>
|
|
|
|
<TabsContent value="guests" className="space-y-4">
|
|
<div className="border rounded-lg">
|
|
<div className="divide-y">
|
|
{guest_appearances.map((perf: 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>
|
|
<Link
|
|
href={`/shows/${perf.show_slug}`}
|
|
className="font-semibold hover:underline"
|
|
>
|
|
{new Date(perf.date).toLocaleDateString(undefined, {
|
|
year: 'numeric',
|
|
month: 'long',
|
|
day: 'numeric'
|
|
})}
|
|
</Link>
|
|
<p className="text-sm text-muted-foreground">
|
|
{perf.venue} • {perf.city}
|
|
</p>
|
|
</div>
|
|
<div className="flex items-center gap-2">
|
|
<span className="text-sm text-muted-foreground">Sat in on:</span>
|
|
<Link
|
|
href={`/songs/${perf.song_slug}`}
|
|
className="font-medium hover:text-primary transition-colors"
|
|
>
|
|
{perf.song_title}
|
|
</Link>
|
|
</div>
|
|
</div>
|
|
))}
|
|
{guest_appearances.length === 0 && (
|
|
<div className="p-12 text-center text-muted-foreground">
|
|
No recorded guest appearances.
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</TabsContent>
|
|
</Tabs>
|
|
</div>
|
|
)
|
|
}
|