fediversion/frontend/app/artists/[slug]/page.tsx
fullsizemalt b4cddf41ea feat: Initialize Fediversion multi-band platform
- Fork elmeg-demo codebase for multi-band support
- Add data importer infrastructure with base class
- Create band-specific importers:
  - phish.py: Phish.net API v5
  - grateful_dead.py: Grateful Stats API
  - setlistfm.py: Dead & Company, Billy Strings (Setlist.fm)
- Add spec-kit configuration for Gemini
- Update README with supported bands and architecture
2025-12-28 12:39:28 -08:00

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} | Elmeg`,
description: data.artist.bio || `Artist profile for ${data.artist.name} on Elmeg.`,
}
}
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>
)
}