From 29c5d30ebb9473990c0cbd1c16dad98ca20bda35 Mon Sep 17 00:00:00 2001 From: fullsizemalt <106900403+fullsizemalt@users.noreply.github.com> Date: Sun, 28 Dec 2025 13:56:22 -0800 Subject: [PATCH] feat: Add multi-vertical frontend infrastructure Phase 3 - Frontend Multi-Vertical Support: - Add VerticalContext for band state management - Add BandSelector dropdown component - Create dynamic [vertical] routes for shows, songs, venues - Update navbar to use band selector and vertical-aware links - Update api-config.ts for Fediversion domain - Rebrand from Elmeg to Fediversion --- frontend/app/[vertical]/page.tsx | 60 +++++++++++ frontend/app/[vertical]/shows/page.tsx | 76 ++++++++++++++ frontend/app/[vertical]/songs/page.tsx | 75 +++++++++++++ frontend/app/[vertical]/venues/page.tsx | 75 +++++++++++++ frontend/app/layout.tsx | 21 ++-- frontend/components/layout/band-selector.tsx | 55 ++++++++++ frontend/components/layout/navbar.tsx | 16 +-- frontend/contexts/vertical-context.tsx | 104 +++++++++++++++++++ frontend/lib/api-config.ts | 9 +- 9 files changed, 475 insertions(+), 16 deletions(-) create mode 100644 frontend/app/[vertical]/page.tsx create mode 100644 frontend/app/[vertical]/shows/page.tsx create mode 100644 frontend/app/[vertical]/songs/page.tsx create mode 100644 frontend/app/[vertical]/venues/page.tsx create mode 100644 frontend/components/layout/band-selector.tsx create mode 100644 frontend/contexts/vertical-context.tsx diff --git a/frontend/app/[vertical]/page.tsx b/frontend/app/[vertical]/page.tsx new file mode 100644 index 0000000..65216a2 --- /dev/null +++ b/frontend/app/[vertical]/page.tsx @@ -0,0 +1,60 @@ +import { notFound } from "next/navigation" +import { VERTICALS } from "@/contexts/vertical-context" + +interface Props { + params: { vertical: string } +} + +export function generateStaticParams() { + return VERTICALS.map((v) => ({ + vertical: v.slug, + })) +} + +export default function VerticalPage({ params }: Props) { + const vertical = VERTICALS.find((v) => v.slug === params.vertical) + + if (!vertical) { + notFound() + } + + return ( +
+
+

+ {vertical.emoji} + {vertical.name} +

+

+ Explore setlists, rate performances, and connect with the {vertical.name} community. +

+
+ +
+ +

Shows

+

Browse all concerts and setlists

+
+ + +

Songs

+

Explore the catalog and stats

+
+ + +

Venues

+

See where they've played

+
+
+
+ ) +} diff --git a/frontend/app/[vertical]/shows/page.tsx b/frontend/app/[vertical]/shows/page.tsx new file mode 100644 index 0000000..0ef11b5 --- /dev/null +++ b/frontend/app/[vertical]/shows/page.tsx @@ -0,0 +1,76 @@ +import { VERTICALS } from "@/contexts/vertical-context" +import { notFound } from "next/navigation" +import { getApiUrl } from "@/lib/api-config" + +interface Props { + params: { vertical: string } +} + +export function generateStaticParams() { + return VERTICALS.map((v) => ({ + vertical: v.slug, + })) +} + +async function getShows(verticalSlug: string) { + try { + const res = await fetch(`${getApiUrl()}/shows?vertical=${verticalSlug}`, { + next: { revalidate: 60 } + }) + if (!res.ok) return [] + return res.json() + } catch { + return [] + } +} + +export default async function ShowsPage({ params }: Props) { + const vertical = VERTICALS.find((v) => v.slug === params.vertical) + + if (!vertical) { + notFound() + } + + const shows = await getShows(vertical.slug) + + return ( +
+
+

+ {vertical.emoji} + {vertical.name} Shows +

+
+ + {shows.length === 0 ? ( +
+

No shows imported yet for {vertical.name}.

+

Run the data importer to populate shows.

+
+ ) : ( +
+ {shows.map((show: any) => ( + +
+
+
{show.venue?.name || "Unknown Venue"}
+
+ {show.venue?.city}, {show.venue?.state || show.venue?.country} +
+
+
+
{new Date(show.date).toLocaleDateString()}
+
{show.tour?.name}
+
+
+
+ ))} +
+ )} +
+ ) +} diff --git a/frontend/app/[vertical]/songs/page.tsx b/frontend/app/[vertical]/songs/page.tsx new file mode 100644 index 0000000..2c8b699 --- /dev/null +++ b/frontend/app/[vertical]/songs/page.tsx @@ -0,0 +1,75 @@ +import { VERTICALS } from "@/contexts/vertical-context" +import { notFound } from "next/navigation" +import { getApiUrl } from "@/lib/api-config" + +interface Props { + params: { vertical: string } +} + +export function generateStaticParams() { + return VERTICALS.map((v) => ({ + vertical: v.slug, + })) +} + +async function getSongs(verticalSlug: string) { + try { + const res = await fetch(`${getApiUrl()}/songs?vertical=${verticalSlug}`, { + next: { revalidate: 60 } + }) + if (!res.ok) return [] + return res.json() + } catch { + return [] + } +} + +export default async function SongsPage({ params }: Props) { + const vertical = VERTICALS.find((v) => v.slug === params.vertical) + + if (!vertical) { + notFound() + } + + const songs = await getSongs(vertical.slug) + + return ( +
+
+

+ {vertical.emoji} + {vertical.name} Songs +

+
+ + {songs.length === 0 ? ( +
+

No songs imported yet for {vertical.name}.

+

Run the data importer to populate songs.

+
+ ) : ( +
+ {songs.map((song: any) => ( + +
+
{song.title}
+ {song.original_artist && ( +
+ Cover of {song.original_artist} +
+ )} +
+
+ {song.times_played || 0} plays +
+
+ ))} +
+ )} +
+ ) +} diff --git a/frontend/app/[vertical]/venues/page.tsx b/frontend/app/[vertical]/venues/page.tsx new file mode 100644 index 0000000..0e67a9e --- /dev/null +++ b/frontend/app/[vertical]/venues/page.tsx @@ -0,0 +1,75 @@ +import { VERTICALS } from "@/contexts/vertical-context" +import { notFound } from "next/navigation" +import { getApiUrl } from "@/lib/api-config" + +interface Props { + params: { vertical: string } +} + +export function generateStaticParams() { + return VERTICALS.map((v) => ({ + vertical: v.slug, + })) +} + +async function getVenues(verticalSlug: string) { + try { + const res = await fetch(`${getApiUrl()}/venues?vertical=${verticalSlug}`, { + next: { revalidate: 60 } + }) + if (!res.ok) return [] + return res.json() + } catch { + return [] + } +} + +export default async function VenuesPage({ params }: Props) { + const vertical = VERTICALS.find((v) => v.slug === params.vertical) + + if (!vertical) { + notFound() + } + + const venues = await getVenues(vertical.slug) + + return ( +
+
+

+ {vertical.emoji} + {vertical.name} Venues +

+
+ + {venues.length === 0 ? ( +
+

No venues imported yet for {vertical.name}.

+

Run the data importer to populate venues.

+
+ ) : ( +
+ {venues.map((venue: any) => ( + +
+
{venue.name}
+
+ {venue.city}, {venue.state || venue.country} +
+
+ {venue.capacity && ( +
+ Capacity: {venue.capacity.toLocaleString()} +
+ )} +
+ ))} +
+ )} +
+ ) +} diff --git a/frontend/app/layout.tsx b/frontend/app/layout.tsx index 790feca..873952d 100644 --- a/frontend/app/layout.tsx +++ b/frontend/app/layout.tsx @@ -5,6 +5,7 @@ import { Navbar } from "@/components/layout/navbar"; import { cn } from "@/lib/utils"; import { PreferencesProvider } from "@/contexts/preferences-context"; import { AuthProvider } from "@/contexts/auth-context"; +import { VerticalProvider } from "@/contexts/vertical-context"; import { ThemeProvider } from "@/components/theme-provider"; import { Footer } from "@/components/layout/footer"; import Script from "next/script"; @@ -20,8 +21,8 @@ const jetbrainsMono = JetBrains_Mono({ }); export const metadata: Metadata = { - title: "Elmeg", - description: "A Place to talk Goose", + title: "Fediversion", + description: "The ultimate HeadyVersion platform for all jam bands", }; export default function RootLayout({ @@ -48,13 +49,15 @@ export default function RootLayout({ disableTransitionOnChange > - - -
- {children} -
-