From 730e92f8c9731027442eddf3915481b57f0e3a0d Mon Sep 17 00:00:00 2001 From: fullsizemalt <106900403+fullsizemalt@users.noreply.github.com> Date: Wed, 24 Dec 2025 13:01:57 -0800 Subject: [PATCH] feat: add Shows, Songs, Venues Admin pages --- frontend/app/admin/layout.tsx | 17 +- frontend/app/admin/shows/page.tsx | 259 +++++++++++++++++++++++++++++ frontend/app/admin/songs/page.tsx | 236 ++++++++++++++++++++++++++ frontend/app/admin/venues/page.tsx | 252 ++++++++++++++++++++++++++++ 4 files changed, 763 insertions(+), 1 deletion(-) create mode 100644 frontend/app/admin/shows/page.tsx create mode 100644 frontend/app/admin/songs/page.tsx create mode 100644 frontend/app/admin/venues/page.tsx diff --git a/frontend/app/admin/layout.tsx b/frontend/app/admin/layout.tsx index d38e6cf..2afa02e 100644 --- a/frontend/app/admin/layout.tsx +++ b/frontend/app/admin/layout.tsx @@ -4,7 +4,7 @@ import Link from "next/link" import { usePathname } from "next/navigation" import { cn } from "@/lib/utils" import { Button } from "@/components/ui/button" -import { LayoutDashboard, MessageSquare, ShieldAlert, Users, Mic2 } from "lucide-react" +import { LayoutDashboard, MessageSquare, ShieldAlert, Users, Mic2, Calendar, Music2, MapPin } from "lucide-react" export default function AdminLayout({ children, @@ -24,6 +24,21 @@ export default function AdminLayout({ href: "/admin/artists", icon: Mic2 }, + { + title: "Shows", + href: "/admin/shows", + icon: Calendar + }, + { + title: "Songs", + href: "/admin/songs", + icon: Music2 + }, + { + title: "Venues", + href: "/admin/venues", + icon: MapPin + }, { title: "Nicknames", href: "/admin/nicknames", diff --git a/frontend/app/admin/shows/page.tsx b/frontend/app/admin/shows/page.tsx new file mode 100644 index 0000000..d840d47 --- /dev/null +++ b/frontend/app/admin/shows/page.tsx @@ -0,0 +1,259 @@ +"use client" + +import { useEffect, useState } from "react" +import { useAuth } from "@/contexts/auth-context" +import { useRouter } from "next/navigation" +import { Card, CardContent } from "@/components/ui/card" +import { Button } from "@/components/ui/button" +import { Input } from "@/components/ui/input" +import { Textarea } from "@/components/ui/textarea" +import { Badge } from "@/components/ui/badge" +import { Search, Edit, Save, X, Calendar, ExternalLink } from "lucide-react" +import { getApiUrl } from "@/lib/api-config" +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, + DialogFooter, +} from "@/components/ui/dialog" +import { Label } from "@/components/ui/label" +import Link from "next/link" + +interface Show { + id: number + date: string + slug: string + venue_name?: string + venue_city?: string + notes: string | null + nugs_link: string | null + bandcamp_link: string | null + youtube_link: string | null +} + +export default function AdminShowsPage() { + const { user, token } = useAuth() + const router = useRouter() + const [shows, setShows] = useState([]) + const [loading, setLoading] = useState(true) + const [search, setSearch] = useState("") + const [editingShow, setEditingShow] = useState(null) + const [saving, setSaving] = useState(false) + + useEffect(() => { + if (!user) { + router.push("/login") + return + } + if (user.role !== "admin") { + router.push("/") + return + } + fetchShows() + }, [user, router]) + + const fetchShows = async () => { + if (!token) return + + try { + const res = await fetch(`${getApiUrl()}/shows?limit=100&sort=date_desc`, { + headers: { Authorization: `Bearer ${token}` } + }) + if (res.ok) { + const data = await res.json() + setShows(data.shows || data) + } + } catch (e) { + console.error("Failed to fetch shows", e) + } finally { + setLoading(false) + } + } + + const updateShow = async () => { + if (!token || !editingShow) return + setSaving(true) + + try { + const res = await fetch(`${getApiUrl()}/admin/shows/${editingShow.id}`, { + method: "PATCH", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${token}` + }, + body: JSON.stringify({ + notes: editingShow.notes, + nugs_link: editingShow.nugs_link, + bandcamp_link: editingShow.bandcamp_link, + youtube_link: editingShow.youtube_link + }) + }) + + if (res.ok) { + fetchShows() + setEditingShow(null) + } + } catch (e) { + console.error("Failed to update show", e) + } finally { + setSaving(false) + } + } + + const formatDate = (dateStr: string) => { + return new Date(dateStr).toLocaleDateString("en-US", { + year: "numeric", + month: "short", + day: "numeric" + }) + } + + const filteredShows = shows.filter(s => + s.venue_name?.toLowerCase().includes(search.toLowerCase()) || + s.venue_city?.toLowerCase().includes(search.toLowerCase()) || + s.date.includes(search) + ) + + if (loading) { + return ( +
+
+
+ {[1, 2, 3].map(i =>
)} +
+
+ ) + } + + return ( +
+
+

+ + Show Management +

+
+ +
+ + setSearch(e.target.value)} + className="pl-9" + /> +
+ + + + + + + + + + + + + + {filteredShows.slice(0, 50).map(show => ( + + + + + + + ))} + +
DateVenueLinksActions
+ + {formatDate(show.date)} + + +

{show.venue_name || "Unknown Venue"}

+

{show.venue_city}

+
+ {show.nugs_link && Nugs} + {show.bandcamp_link && BC} + {show.youtube_link && YT} + {!show.nugs_link && !show.bandcamp_link && !show.youtube_link && ( + No links + )} + + +
+ {filteredShows.length === 0 && ( +
+ No shows found +
+ )} +
+
+ + setEditingShow(null)}> + + + Edit Show: {editingShow && formatDate(editingShow.date)} + + {editingShow && ( +
+
+ +