XP System:
- XP now awarded for attendance (+25), ratings (+10), reviews (+50)
- First-time bonuses for first rating (+25) and first review (+50)
- Streak bonuses (+10 per day, capped at 7x)
- Badge awards automatically grant XP
User Titles & Flair System (Tracker-style):
- Level-based free titles: Rookie → Immortal
- Purchasable titles with XP: Jam Connoisseur, Setlist Savant, etc.
- Username colors purchasable with XP (6 colors + Rainbow)
- Emoji flairs purchasable with XP
- Early adopter perks: exclusive titles, colors, 10% XP bonus
New Fields on User:
- custom_title, title_color, flair
- is_early_adopter, is_supporter
- joined_at
Shop API Endpoints:
- GET /gamification/shop/titles
- POST /gamification/shop/titles/purchase
- GET/POST for colors and flairs
- GET /gamification/user/{id}/display
- GET /gamification/early-adopter-perks
Frontend:
- XP Leaderboard added to home page
- LevelProgressCard shows on profile
228 lines
7.6 KiB
Python
228 lines
7.6 KiB
Python
import re
|
|
from typing import List, Optional
|
|
from fastapi import APIRouter, Depends, HTTPException, Query
|
|
from sqlmodel import Session, select, func
|
|
from database import get_session
|
|
from models import Comment, Rating, User, Profile, Reaction
|
|
from schemas import CommentCreate, CommentRead, RatingCreate, RatingRead, ReactionCreate, ReactionRead
|
|
from auth import get_current_user
|
|
from helpers import create_notification
|
|
from services.gamification import award_xp, check_and_award_badges, update_streak, XP_REWARDS
|
|
|
|
router = APIRouter(prefix="/social", tags=["social"])
|
|
|
|
# --- Comments ---
|
|
|
|
@router.post("/comments", response_model=CommentRead)
|
|
def create_comment(
|
|
comment: CommentCreate,
|
|
session: Session = Depends(get_session),
|
|
current_user: User = Depends(get_current_user)
|
|
):
|
|
db_comment = Comment.model_validate(comment)
|
|
db_comment.user_id = current_user.id
|
|
session.add(db_comment)
|
|
session.commit()
|
|
session.refresh(db_comment)
|
|
|
|
# Notify parent author if reply
|
|
if db_comment.parent_id:
|
|
parent_comment = session.get(Comment, db_comment.parent_id)
|
|
if parent_comment and parent_comment.user_id != current_user.id:
|
|
create_notification(
|
|
session,
|
|
user_id=parent_comment.user_id,
|
|
title="New Reply",
|
|
message=f"Someone replied to your comment.",
|
|
type="reply",
|
|
link=f"/activity"
|
|
)
|
|
|
|
# Handle Mentions
|
|
mention_pattern = r"@(\w+)"
|
|
mentions = re.findall(mention_pattern, db_comment.content)
|
|
if mentions:
|
|
# Find users with these profile usernames
|
|
mentioned_profiles = session.exec(select(Profile).where(Profile.username.in_(mentions))).all()
|
|
for profile in mentioned_profiles:
|
|
if profile.user_id != current_user.id:
|
|
create_notification(
|
|
session,
|
|
user_id=profile.user_id,
|
|
title="You were mentioned!",
|
|
message=f"Someone mentioned you in a comment.",
|
|
type="mention",
|
|
link=f"/activity" # Generic link for now
|
|
)
|
|
|
|
return db_comment
|
|
|
|
@router.get("/comments", response_model=List[CommentRead])
|
|
def read_comments(
|
|
show_id: Optional[int] = None,
|
|
venue_id: Optional[int] = None,
|
|
song_id: Optional[int] = None,
|
|
offset: int = 0,
|
|
limit: int = Query(default=50, le=100),
|
|
session: Session = Depends(get_session)
|
|
):
|
|
query = select(Comment)
|
|
if show_id:
|
|
query = query.where(Comment.show_id == show_id)
|
|
if venue_id:
|
|
query = query.where(Comment.venue_id == venue_id)
|
|
if song_id:
|
|
query = query.where(Comment.song_id == song_id)
|
|
|
|
query = query.order_by(Comment.created_at.desc()).offset(offset).limit(limit)
|
|
comments = session.exec(query).all()
|
|
return comments
|
|
|
|
# --- Ratings ---
|
|
|
|
@router.post("/ratings", response_model=RatingRead)
|
|
def create_rating(
|
|
rating: RatingCreate,
|
|
session: Session = Depends(get_session),
|
|
current_user: User = Depends(get_current_user)
|
|
):
|
|
# Check if user already rated this entity
|
|
query = select(Rating).where(Rating.user_id == current_user.id)
|
|
if rating.show_id:
|
|
query = query.where(Rating.show_id == rating.show_id)
|
|
elif rating.song_id:
|
|
query = query.where(Rating.song_id == rating.song_id)
|
|
elif rating.performance_id:
|
|
query = query.where(Rating.performance_id == rating.performance_id)
|
|
elif rating.venue_id:
|
|
query = query.where(Rating.venue_id == rating.venue_id)
|
|
elif rating.tour_id:
|
|
query = query.where(Rating.tour_id == rating.tour_id)
|
|
else:
|
|
raise HTTPException(status_code=400, detail="Must rate a show, song, performance, venue, or tour")
|
|
|
|
existing_rating = session.exec(query).first()
|
|
if existing_rating:
|
|
# Update existing (no XP for updating)
|
|
existing_rating.score = rating.score
|
|
session.add(existing_rating)
|
|
session.commit()
|
|
session.refresh(existing_rating)
|
|
return existing_rating
|
|
|
|
db_rating = Rating.model_validate(rating)
|
|
db_rating.user_id = current_user.id
|
|
session.add(db_rating)
|
|
|
|
# Award XP for new rating
|
|
# Check if first rating for bonus
|
|
rating_count = session.exec(
|
|
select(func.count(Rating.id)).where(Rating.user_id == current_user.id)
|
|
).one() or 0
|
|
|
|
xp_amount = XP_REWARDS["rating_submit"]
|
|
if rating_count == 0:
|
|
xp_amount += XP_REWARDS["first_rating"] # Bonus for first rating
|
|
|
|
award_xp(session, current_user, xp_amount, "rating")
|
|
update_streak(session, current_user)
|
|
check_and_award_badges(session, current_user)
|
|
|
|
session.commit()
|
|
session.refresh(db_rating)
|
|
return db_rating
|
|
|
|
@router.get("/ratings/average", response_model=float)
|
|
def get_average_rating(
|
|
show_id: Optional[int] = None,
|
|
song_id: Optional[int] = None,
|
|
performance_id: Optional[int] = None,
|
|
venue_id: Optional[int] = None,
|
|
tour_id: Optional[int] = None,
|
|
session: Session = Depends(get_session)
|
|
):
|
|
query = select(func.avg(Rating.score))
|
|
if show_id:
|
|
query = query.where(Rating.show_id == show_id)
|
|
elif song_id:
|
|
query = query.where(Rating.song_id == song_id)
|
|
elif performance_id:
|
|
query = query.where(Rating.performance_id == performance_id)
|
|
elif venue_id:
|
|
query = query.where(Rating.venue_id == venue_id)
|
|
elif tour_id:
|
|
query = query.where(Rating.tour_id == tour_id)
|
|
else:
|
|
# Return 0 if no entity specified instead of error (graceful degradation)
|
|
return 0.0
|
|
|
|
avg = session.exec(query).first()
|
|
return float(avg) if avg else 0.0
|
|
|
|
# --- Reactions ---
|
|
|
|
@router.post("/reactions", response_model=ReactionRead)
|
|
def toggle_reaction(
|
|
reaction: ReactionCreate,
|
|
session: Session = Depends(get_session),
|
|
current_user: User = Depends(get_current_user)
|
|
):
|
|
query = select(Reaction).where(
|
|
Reaction.user_id == current_user.id,
|
|
Reaction.entity_type == reaction.entity_type,
|
|
Reaction.entity_id == reaction.entity_id
|
|
)
|
|
existing = session.exec(query).first()
|
|
|
|
if existing:
|
|
if existing.emoji == reaction.emoji:
|
|
# Toggle off
|
|
session.delete(existing)
|
|
session.commit()
|
|
return existing
|
|
else:
|
|
# Change emoji
|
|
existing.emoji = reaction.emoji
|
|
session.add(existing)
|
|
session.commit()
|
|
session.refresh(existing)
|
|
return existing
|
|
|
|
# Create new
|
|
db_reaction = Reaction.model_validate(reaction)
|
|
db_reaction.user_id = current_user.id
|
|
session.add(db_reaction)
|
|
session.commit()
|
|
session.refresh(db_reaction)
|
|
|
|
return db_reaction
|
|
|
|
@router.get("/reactions/counts")
|
|
def get_reaction_counts(
|
|
entity_type: str,
|
|
entity_id: int,
|
|
session: Session = Depends(get_session)
|
|
):
|
|
# Group by emoji and count
|
|
query = select(Reaction.emoji, func.count(Reaction.id)).where(
|
|
Reaction.entity_type == entity_type,
|
|
Reaction.entity_id == entity_id
|
|
).group_by(Reaction.emoji)
|
|
|
|
results = session.exec(query).all()
|
|
# returns list of (emoji, count)
|
|
return {emoji: count for emoji, count in results}
|
|
|
|
@router.get("/reactions/me")
|
|
def get_my_reaction(
|
|
entity_type: str,
|
|
entity_id: int,
|
|
current_user: User = Depends(get_current_user),
|
|
session: Session = Depends(get_session)
|
|
):
|
|
query = select(Reaction).where(
|
|
Reaction.user_id == current_user.id,
|
|
Reaction.entity_type == entity_type,
|
|
Reaction.entity_id == entity_id
|
|
)
|
|
return session.exec(query).first()
|