From 212082050c2d123897feecc0c5b576feec00adf8 Mon Sep 17 00:00:00 2001 From: fullsizemalt <106900403+fullsizemalt@users.noreply.github.com> Date: Mon, 29 Dec 2025 21:49:28 -0800 Subject: [PATCH] feat(bands): add My Bands page with tier management and IGNORED tier --- backend/models.py | 1 + frontend/app/my-bands/page.tsx | 338 ++++++++++++++++++++++++++ frontend/components/layout/navbar.tsx | 3 + 3 files changed, 342 insertions(+) create mode 100644 frontend/app/my-bands/page.tsx diff --git a/backend/models.py b/backend/models.py index 209887d..cbd42dc 100644 --- a/backend/models.py +++ b/backend/models.py @@ -327,6 +327,7 @@ class PreferenceTier(str, Enum): HEADLINER = "headliner" MAIN_STAGE = "main_stage" SUPPORTING = "supporting" + IGNORED = "ignored" # Exclude from feeds, keep attribution mentions class UserVerticalPreference(SQLModel, table=True): """User preferences for which bands to display prominently vs. attribution-only""" diff --git a/frontend/app/my-bands/page.tsx b/frontend/app/my-bands/page.tsx new file mode 100644 index 0000000..655c710 --- /dev/null +++ b/frontend/app/my-bands/page.tsx @@ -0,0 +1,338 @@ +"use client" + +import { useState, useEffect } from "react" +import { useRouter } from "next/navigation" +import { Button } from "@/components/ui/button" +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card" +import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs" +import { useAuth } from "@/contexts/auth-context" +import { getApiUrl } from "@/lib/api-config" +import { Star, Check, Ban, Loader2, Music2 } from "lucide-react" +import Link from "next/link" + +interface Vertical { + id: number + name: string + slug: string + description: string | null + color: string | null + emoji: string | null +} + +interface UserPreference { + vertical_id: number + tier: "headliner" | "main_stage" | "supporting" | "ignored" + priority: number +} + +type TierType = "headliner" | "main_stage" | "supporting" | "ignored" | null + +export default function MyBandsPage() { + const [verticals, setVerticals] = useState([]) + const [preferences, setPreferences] = useState>(new Map()) + const [loading, setLoading] = useState(true) + const [updating, setUpdating] = useState(null) + const { user, token } = useAuth() + const router = useRouter() + + useEffect(() => { + async function fetchData() { + try { + // Fetch all verticals + const verticalsRes = await fetch(`${getApiUrl()}/verticals`) + if (verticalsRes.ok) { + setVerticals(await verticalsRes.json()) + } + + // Fetch user preferences if logged in + if (token) { + const prefsRes = await fetch(`${getApiUrl()}/verticals/preferences`, { + headers: { Authorization: `Bearer ${token}` } + }) + if (prefsRes.ok) { + const prefsData = await prefsRes.json() + const prefsMap = new Map() + prefsData.forEach((p: UserPreference) => { + prefsMap.set(p.vertical_id, p) + }) + setPreferences(prefsMap) + } + } + } catch (error) { + console.error("Failed to fetch data", error) + } finally { + setLoading(false) + } + } + fetchData() + }, [token]) + + const getTier = (verticalId: number): TierType => { + const pref = preferences.get(verticalId) + return pref?.tier || null + } + + const setTier = async (verticalId: number, tier: TierType) => { + if (!token) { + router.push("/login") + return + } + + setUpdating(verticalId) + try { + if (tier === null) { + // Remove preference + await fetch(`${getApiUrl()}/verticals/preferences/${verticalId}`, { + method: "DELETE", + headers: { Authorization: `Bearer ${token}` } + }) + setPreferences(prev => { + const next = new Map(prev) + next.delete(verticalId) + return next + }) + } else { + // Set or update preference + const res = await fetch(`${getApiUrl()}/verticals/preferences/${verticalId}`, { + method: "PUT", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${token}` + }, + body: JSON.stringify({ + tier, + priority: tier === "headliner" ? 100 : tier === "main_stage" ? 50 : 0 + }) + }) + if (res.ok) { + setPreferences(prev => { + const next = new Map(prev) + next.set(verticalId, { vertical_id: verticalId, tier, priority: 0 }) + return next + }) + } + } + } catch (error) { + console.error("Failed to update preference", error) + } finally { + setUpdating(null) + } + } + + const headliners = verticals.filter(v => getTier(v.id) === "headliner") + const following = verticals.filter(v => ["main_stage", "supporting"].includes(getTier(v.id) || "")) + const ignored = verticals.filter(v => getTier(v.id) === "ignored") + const unfollowed = verticals.filter(v => getTier(v.id) === null) + + if (!user) { + return ( +
+ +

Sign in to manage your bands

+

Track your favorite artists and customize your feed.

+ + + +
+ ) + } + + if (loading) { + return ( +
+ +
+ ) + } + + return ( +
+
+
+

My Bands

+

Manage which bands appear in your feed

+
+ + + +
+ + + + + + Headliners + {headliners.length} + + + + Following + {following.length} + + + All Bands + + + + Ignored + {ignored.length} + + + + + {headliners.length === 0 ? ( + + ) : ( + + )} + + + + {following.length === 0 ? ( + + ) : ( + + )} + + + + + + + + {ignored.length === 0 ? ( + + ) : ( + + )} + + +
+ ) +} + +function EmptyState({ title, description }: { title: string; description: string }) { + return ( + + + +

{title}

+

{description}

+
+
+ ) +} + +function BandGrid({ + bands, + getTier, + setTier, + updating +}: { + bands: Vertical[] + getTier: (id: number) => TierType + setTier: (id: number, tier: TierType) => Promise + updating: number | null +}) { + return ( +
+ {bands.map((band) => { + const tier = getTier(band.id) + const isUpdating = updating === band.id + + return ( + + +
+
+ {band.emoji || "🎵"} + {band.name} +
+ {isUpdating && ( + + )} +
+
+ +
+ {/* Headliner */} + + + {/* Follow */} + + + {/* Ignore */} + +
+
+
+ ) + })} +
+ ) +} diff --git a/frontend/components/layout/navbar.tsx b/frontend/components/layout/navbar.tsx index a927744..c60b357 100644 --- a/frontend/components/layout/navbar.tsx +++ b/frontend/components/layout/navbar.tsx @@ -99,6 +99,9 @@ export function Navbar() { Profile + + My Bands + Settings