feat: frontend artist page and song linking
Some checks are pending
Deploy Elmeg / deploy (push) Waiting to run
Some checks are pending
Deploy Elmeg / deploy (push) Waiting to run
This commit is contained in:
parent
61715a119c
commit
b67d4929a4
3 changed files with 168 additions and 3 deletions
|
|
@ -7,7 +7,7 @@ Migration script to refactor Artists and link Songs.
|
|||
from sqlmodel import Session, select, text
|
||||
from database import engine
|
||||
from models import Artist, Song
|
||||
from slugify import slugify
|
||||
from slugify import generate_slug as slugify
|
||||
|
||||
def migrate_artists():
|
||||
with Session(engine) as session:
|
||||
|
|
|
|||
161
frontend/app/artists/[slug]/page.tsx
Normal file
161
frontend/app/artists/[slug]/page.tsx
Normal file
|
|
@ -0,0 +1,161 @@
|
|||
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>
|
||||
)
|
||||
}
|
||||
|
|
@ -57,9 +57,13 @@ export default async function SongDetailPage({ params }: { params: Promise<{ slu
|
|||
<div>
|
||||
<div className="flex items-baseline gap-3">
|
||||
<h1 className="text-3xl font-bold tracking-tight">{song.title}</h1>
|
||||
{song.original_artist && (
|
||||
{song.artist ? (
|
||||
<Link href={`/artists/${song.artist.slug}`} className="text-lg text-muted-foreground font-medium hover:text-primary transition-colors">
|
||||
({song.artist.name})
|
||||
</Link>
|
||||
) : song.original_artist ? (
|
||||
<span className="text-lg text-muted-foreground font-medium">({song.original_artist})</span>
|
||||
)}
|
||||
) : null}
|
||||
</div>
|
||||
{song.tags && song.tags.length > 0 && (
|
||||
<div className="flex flex-wrap gap-2 mt-2">
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue