From b67d4929a448bce064257e4d283672bc58ab7fce Mon Sep 17 00:00:00 2001 From: fullsizemalt <106900403+fullsizemalt@users.noreply.github.com> Date: Wed, 24 Dec 2025 12:22:36 -0800 Subject: [PATCH] feat: frontend artist page and song linking --- backend/migrations/migrate_artists.py | 2 +- frontend/app/artists/[slug]/page.tsx | 161 ++++++++++++++++++++++++++ frontend/app/songs/[slug]/page.tsx | 8 +- 3 files changed, 168 insertions(+), 3 deletions(-) create mode 100644 frontend/app/artists/[slug]/page.tsx diff --git a/backend/migrations/migrate_artists.py b/backend/migrations/migrate_artists.py index 1c86319..f75dec1 100644 --- a/backend/migrations/migrate_artists.py +++ b/backend/migrations/migrate_artists.py @@ -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: diff --git a/frontend/app/artists/[slug]/page.tsx b/frontend/app/artists/[slug]/page.tsx new file mode 100644 index 0000000..214ae5b --- /dev/null +++ b/frontend/app/artists/[slug]/page.tsx @@ -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 { + 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 ( +
+ {/* Header */} +
+
+ {artist.image_url ? ( + {artist.name} + ) : ( +
+ {artist.name[0]} +
+ )} +
+

{artist.name}

+ {artist.instrument && ( +

{artist.instrument}

+ )} +
+
+ + {artist.bio && ( +

+ {artist.bio} +

+ )} +
+ + + + + + + Covers + {covers.length} + + + Guest Appearances + {guest_appearances.length} + + + + +
+ {covers.map((song: any) => ( + + + + + {song.title} + + + +

+ Covered by Goose +

+
+
+ + ))} + {covers.length === 0 && ( +
+ No known covers by this artist. +
+ )} +
+
+ + +
+
+ {guest_appearances.map((perf: any, i: number) => ( +
+
+ + {new Date(perf.date).toLocaleDateString(undefined, { + year: 'numeric', + month: 'long', + day: 'numeric' + })} + +

+ {perf.venue} • {perf.city} +

+
+
+ Sat in on: + + {perf.song_title} + +
+
+ ))} + {guest_appearances.length === 0 && ( +
+ No recorded guest appearances. +
+ )} +
+
+
+
+
+ ) +} diff --git a/frontend/app/songs/[slug]/page.tsx b/frontend/app/songs/[slug]/page.tsx index 37edfb1..76f4f45 100644 --- a/frontend/app/songs/[slug]/page.tsx +++ b/frontend/app/songs/[slug]/page.tsx @@ -57,9 +57,13 @@ export default async function SongDetailPage({ params }: { params: Promise<{ slu

{song.title}

- {song.original_artist && ( + {song.artist ? ( + + ({song.artist.name}) + + ) : song.original_artist ? ( ({song.original_artist}) - )} + ) : null}
{song.tags && song.tags.length > 0 && (