Backend: - Add XP, level, streak fields to User model - Add tier, category, xp_reward fields to Badge model - Create gamification service with XP, levels, streaks, badge checking - Add gamification router with level progress, leaderboard endpoints - Define 16 badge types across attendance, ratings, social, milestones Frontend: - LevelProgressCard component with XP bar and streak display - XPLeaderboard component showing top users - Integrate level progress into profile page Slug System: - All entities now support slug-based URLs - Performances use songslug-YYYY-MM-DD format
190 lines
4.9 KiB
Python
190 lines
4.9 KiB
Python
"""
|
|
Gamification Router - XP, Levels, Leaderboards
|
|
"""
|
|
from fastapi import APIRouter, Depends, HTTPException
|
|
from sqlmodel import Session, select
|
|
from typing import List
|
|
from pydantic import BaseModel
|
|
from datetime import datetime
|
|
|
|
from database import get_session
|
|
from models import User, Badge, UserBadge
|
|
from routers.auth import get_current_user
|
|
from services.gamification import (
|
|
calculate_level,
|
|
xp_for_next_level,
|
|
update_streak,
|
|
check_and_award_badges,
|
|
get_leaderboard,
|
|
seed_badges,
|
|
LEVEL_NAMES,
|
|
XP_REWARDS,
|
|
)
|
|
|
|
router = APIRouter(prefix="/gamification", tags=["gamification"])
|
|
|
|
|
|
class LevelProgress(BaseModel):
|
|
current_xp: int
|
|
level: int
|
|
level_name: str
|
|
xp_for_next: int
|
|
xp_progress: int
|
|
progress_percent: float
|
|
streak_days: int
|
|
|
|
|
|
class LeaderboardEntry(BaseModel):
|
|
rank: int
|
|
username: str
|
|
xp: int
|
|
level: int
|
|
level_name: str
|
|
streak: int
|
|
|
|
|
|
class BadgeResponse(BaseModel):
|
|
id: int
|
|
name: str
|
|
description: str
|
|
icon: str
|
|
slug: str
|
|
tier: str
|
|
category: str
|
|
awarded_at: datetime | None = None
|
|
|
|
|
|
class XPRewardsInfo(BaseModel):
|
|
rewards: dict
|
|
level_thresholds: List[int]
|
|
level_names: dict
|
|
|
|
|
|
@router.get("/me", response_model=LevelProgress)
|
|
async def get_my_progress(
|
|
current_user: User = Depends(get_current_user),
|
|
session: Session = Depends(get_session)
|
|
):
|
|
"""Get current user's XP and level progress"""
|
|
xp_needed, xp_progress = xp_for_next_level(current_user.xp)
|
|
progress_percent = (xp_progress / xp_needed * 100) if xp_needed > 0 else 100
|
|
|
|
return LevelProgress(
|
|
current_xp=current_user.xp,
|
|
level=current_user.level,
|
|
level_name=LEVEL_NAMES.get(current_user.level, "Unknown"),
|
|
xp_for_next=xp_needed,
|
|
xp_progress=xp_progress,
|
|
progress_percent=round(progress_percent, 1),
|
|
streak_days=current_user.streak_days,
|
|
)
|
|
|
|
|
|
@router.post("/activity")
|
|
async def record_activity(
|
|
current_user: User = Depends(get_current_user),
|
|
session: Session = Depends(get_session)
|
|
):
|
|
"""Record user activity and update streak"""
|
|
streak = update_streak(session, current_user)
|
|
new_badges = check_and_award_badges(session, current_user)
|
|
session.commit()
|
|
|
|
return {
|
|
"streak_days": streak,
|
|
"new_badges": [b.name for b in new_badges],
|
|
"xp": current_user.xp,
|
|
"level": current_user.level,
|
|
}
|
|
|
|
|
|
@router.get("/leaderboard", response_model=List[LeaderboardEntry])
|
|
async def get_xp_leaderboard(
|
|
limit: int = 10,
|
|
session: Session = Depends(get_session)
|
|
):
|
|
"""Get top users by XP"""
|
|
leaders = get_leaderboard(session, limit)
|
|
|
|
return [
|
|
LeaderboardEntry(
|
|
rank=i + 1,
|
|
username=l["email"],
|
|
xp=l["xp"],
|
|
level=l["level"],
|
|
level_name=l["level_name"],
|
|
streak=l["streak"],
|
|
)
|
|
for i, l in enumerate(leaders)
|
|
]
|
|
|
|
|
|
@router.get("/badges", response_model=List[BadgeResponse])
|
|
async def get_all_badges(session: Session = Depends(get_session)):
|
|
"""Get all available badges"""
|
|
badges = session.exec(select(Badge).order_by(Badge.tier, Badge.category)).all()
|
|
return [
|
|
BadgeResponse(
|
|
id=b.id,
|
|
name=b.name,
|
|
description=b.description,
|
|
icon=b.icon,
|
|
slug=b.slug,
|
|
tier=b.tier,
|
|
category=b.category,
|
|
)
|
|
for b in badges
|
|
]
|
|
|
|
|
|
@router.get("/badges/me", response_model=List[BadgeResponse])
|
|
async def get_my_badges(
|
|
current_user: User = Depends(get_current_user),
|
|
session: Session = Depends(get_session)
|
|
):
|
|
"""Get current user's earned badges"""
|
|
user_badges = session.exec(
|
|
select(UserBadge, Badge)
|
|
.join(Badge)
|
|
.where(UserBadge.user_id == current_user.id)
|
|
.order_by(UserBadge.awarded_at.desc())
|
|
).all()
|
|
|
|
return [
|
|
BadgeResponse(
|
|
id=b.id,
|
|
name=b.name,
|
|
description=b.description,
|
|
icon=b.icon,
|
|
slug=b.slug,
|
|
tier=b.tier,
|
|
category=b.category,
|
|
awarded_at=ub.awarded_at,
|
|
)
|
|
for ub, b in user_badges
|
|
]
|
|
|
|
|
|
@router.get("/info", response_model=XPRewardsInfo)
|
|
async def get_xp_info():
|
|
"""Get XP reward values and level thresholds"""
|
|
from services.gamification import LEVEL_THRESHOLDS
|
|
|
|
return XPRewardsInfo(
|
|
rewards=XP_REWARDS,
|
|
level_thresholds=LEVEL_THRESHOLDS,
|
|
level_names=LEVEL_NAMES,
|
|
)
|
|
|
|
|
|
@router.post("/seed-badges")
|
|
async def seed_badge_data(
|
|
current_user: User = Depends(get_current_user),
|
|
session: Session = Depends(get_session)
|
|
):
|
|
"""Seed badge definitions (admin only)"""
|
|
if current_user.role not in ["admin", "moderator"]:
|
|
raise HTTPException(status_code=403, detail="Admin access required")
|
|
|
|
seed_badges(session)
|
|
return {"message": "Badges seeded successfully"}
|