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.
+
+
+
+
+
+ )
+}
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.
+
+ ) : (
+
+ )}
+
+ )
+}
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.
+
+ ) : (
+
+ )}
+
+ )
+}
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.
+
+ ) : (
+
+ )}
+
+ )
+}
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}
-
-
-
+
+
+
+
+ {children}
+
+
+
+