diff --git a/backend/main.py b/backend/main.py index d10c7fc..e5dff19 100644 --- a/backend/main.py +++ b/backend/main.py @@ -1,6 +1,6 @@ from fastapi import FastAPI import os -from routers import auth, shows, venues, songs, social, tours, artists, preferences, reviews, badges, nicknames, moderation, attendance, groups, users, search, performances, notifications, feed, leaderboards, stats, admin, chase, gamification, videos, musicians, sequences +from routers import auth, shows, venues, songs, social, tours, artists, preferences, reviews, badges, nicknames, moderation, attendance, groups, users, search, performances, notifications, feed, leaderboards, stats, admin, chase, gamification, videos, musicians, sequences, verticals from fastapi.middleware.cors import CORSMiddleware @@ -44,6 +44,7 @@ app.include_router(gamification.router) app.include_router(videos.router) app.include_router(musicians.router) app.include_router(sequences.router) +app.include_router(verticals.router) # Optional features - can be disabled via env vars diff --git a/backend/routers/verticals.py b/backend/routers/verticals.py new file mode 100644 index 0000000..551f823 --- /dev/null +++ b/backend/routers/verticals.py @@ -0,0 +1,239 @@ +from fastapi import APIRouter, Depends, HTTPException +from sqlmodel import Session, select +from typing import List +from database import get_session +from models import User, Vertical, UserVerticalPreference +from auth import get_current_user +from pydantic import BaseModel +from datetime import datetime + +router = APIRouter(prefix="/verticals", tags=["verticals"]) + + +class VerticalRead(BaseModel): + id: int + name: str + slug: str + description: str | None = None + + +class UserVerticalPreferenceRead(BaseModel): + vertical_id: int + vertical: VerticalRead + display_mode: str + priority: int + notify_on_show: bool + + +class UserVerticalPreferenceCreate(BaseModel): + vertical_id: int + display_mode: str = "primary" # primary, secondary, attribution_only, hidden + priority: int = 0 + notify_on_show: bool = True + + +class UserVerticalPreferenceUpdate(BaseModel): + display_mode: str | None = None + priority: int | None = None + notify_on_show: bool | None = None + + +class BulkVerticalPreferencesCreate(BaseModel): + """For onboarding - set multiple band preferences at once""" + vertical_ids: List[int] + display_mode: str = "primary" + + +# --- Public endpoints --- + +@router.get("/", response_model=List[VerticalRead]) +def list_verticals(session: Session = Depends(get_session)): + """List all available verticals (bands)""" + verticals = session.exec(select(Vertical)).all() + return verticals + + +@router.get("/{slug}", response_model=VerticalRead) +def get_vertical(slug: str, session: Session = Depends(get_session)): + """Get a specific vertical by slug""" + vertical = session.exec(select(Vertical).where(Vertical.slug == slug)).first() + if not vertical: + raise HTTPException(status_code=404, detail="Vertical not found") + return vertical + + +# --- User preference endpoints --- + +@router.get("/preferences/me", response_model=List[UserVerticalPreferenceRead]) +def get_my_vertical_preferences( + session: Session = Depends(get_session), + current_user: User = Depends(get_current_user) +): + """Get current user's band preferences""" + prefs = session.exec( + select(UserVerticalPreference) + .where(UserVerticalPreference.user_id == current_user.id) + .order_by(UserVerticalPreference.priority) + ).all() + + # Enrich with vertical data + result = [] + for pref in prefs: + vertical = session.get(Vertical, pref.vertical_id) + if vertical: + result.append({ + "vertical_id": pref.vertical_id, + "vertical": vertical, + "display_mode": pref.display_mode, + "priority": pref.priority, + "notify_on_show": pref.notify_on_show + }) + return result + + +@router.post("/preferences", response_model=UserVerticalPreferenceRead) +def add_vertical_preference( + pref: UserVerticalPreferenceCreate, + session: Session = Depends(get_session), + current_user: User = Depends(get_current_user) +): + """Add a band to user's preferences""" + # Check vertical exists + vertical = session.get(Vertical, pref.vertical_id) + if not vertical: + raise HTTPException(status_code=404, detail="Vertical not found") + + # Check if already exists + existing = session.exec( + select(UserVerticalPreference) + .where(UserVerticalPreference.user_id == current_user.id) + .where(UserVerticalPreference.vertical_id == pref.vertical_id) + ).first() + + if existing: + raise HTTPException(status_code=400, detail="Preference already exists") + + db_pref = UserVerticalPreference( + user_id=current_user.id, + vertical_id=pref.vertical_id, + display_mode=pref.display_mode, + priority=pref.priority, + notify_on_show=pref.notify_on_show + ) + session.add(db_pref) + session.commit() + session.refresh(db_pref) + + return { + "vertical_id": db_pref.vertical_id, + "vertical": vertical, + "display_mode": db_pref.display_mode, + "priority": db_pref.priority, + "notify_on_show": db_pref.notify_on_show + } + + +@router.post("/preferences/bulk", response_model=List[UserVerticalPreferenceRead]) +def set_vertical_preferences_bulk( + data: BulkVerticalPreferencesCreate, + session: Session = Depends(get_session), + current_user: User = Depends(get_current_user) +): + """Set multiple band preferences at once (for onboarding)""" + result = [] + + for idx, vid in enumerate(data.vertical_ids): + vertical = session.get(Vertical, vid) + if not vertical: + continue + + # Upsert + existing = session.exec( + select(UserVerticalPreference) + .where(UserVerticalPreference.user_id == current_user.id) + .where(UserVerticalPreference.vertical_id == vid) + ).first() + + if existing: + existing.display_mode = data.display_mode + existing.priority = idx + session.add(existing) + pref = existing + else: + pref = UserVerticalPreference( + user_id=current_user.id, + vertical_id=vid, + display_mode=data.display_mode, + priority=idx, + notify_on_show=True + ) + session.add(pref) + + result.append({ + "vertical_id": vid, + "vertical": vertical, + "display_mode": data.display_mode, + "priority": idx, + "notify_on_show": True + }) + + session.commit() + return result + + +@router.put("/preferences/{vertical_id}", response_model=UserVerticalPreferenceRead) +def update_vertical_preference( + vertical_id: int, + data: UserVerticalPreferenceUpdate, + session: Session = Depends(get_session), + current_user: User = Depends(get_current_user) +): + """Update a specific band preference""" + pref = session.exec( + select(UserVerticalPreference) + .where(UserVerticalPreference.user_id == current_user.id) + .where(UserVerticalPreference.vertical_id == vertical_id) + ).first() + + if not pref: + raise HTTPException(status_code=404, detail="Preference not found") + + vertical = session.get(Vertical, vertical_id) + + update_data = data.model_dump(exclude_unset=True) + for key, value in update_data.items(): + setattr(pref, key, value) + + session.add(pref) + session.commit() + session.refresh(pref) + + return { + "vertical_id": pref.vertical_id, + "vertical": vertical, + "display_mode": pref.display_mode, + "priority": pref.priority, + "notify_on_show": pref.notify_on_show + } + + +@router.delete("/preferences/{vertical_id}") +def delete_vertical_preference( + vertical_id: int, + session: Session = Depends(get_session), + current_user: User = Depends(get_current_user) +): + """Remove a band from user's preferences""" + pref = session.exec( + select(UserVerticalPreference) + .where(UserVerticalPreference.user_id == current_user.id) + .where(UserVerticalPreference.vertical_id == vertical_id) + ).first() + + if not pref: + raise HTTPException(status_code=404, detail="Preference not found") + + session.delete(pref) + session.commit() + + return {"ok": True} diff --git a/frontend/app/onboarding/page.tsx b/frontend/app/onboarding/page.tsx new file mode 100644 index 0000000..84f8c81 --- /dev/null +++ b/frontend/app/onboarding/page.tsx @@ -0,0 +1,14 @@ +import { BandOnboarding } from "@/components/onboarding/band-onboarding" + +export const metadata = { + title: "Pick Your Bands | Fediversion", + description: "Select the bands you want to follow on Fediversion" +} + +export default function OnboardingPage() { + return ( +
+ Select the bands you follow. You can change this anytime. +
+