diff --git a/backend/models.py b/backend/models.py index 0fbfb32..79c9d3f 100644 --- a/backend/models.py +++ b/backend/models.py @@ -173,6 +173,8 @@ class User(SQLModel, table=True): role: str = Field(default="user") # user, moderator, admin bio: Optional[str] = Field(default=None) avatar: Optional[str] = Field(default=None) + avatar_bg_color: Optional[str] = Field(default="#3B82F6", description="Hex color for avatar background") + avatar_text: Optional[str] = Field(default=None, description="1-3 character text overlay on avatar") # Gamification xp: int = Field(default=0, description="Experience points") diff --git a/backend/routers/users.py b/backend/routers/users.py index fc35bb7..1486e19 100644 --- a/backend/routers/users.py +++ b/backend/routers/users.py @@ -14,6 +14,24 @@ class UserProfileUpdate(BaseModel): avatar: Optional[str] = None username: Optional[str] = None display_name: Optional[str] = None + avatar_bg_color: Optional[str] = None + avatar_text: Optional[str] = None + +# Preset avatar colors +AVATAR_COLORS = [ + "#3B82F6", # Blue + "#EF4444", # Red + "#10B981", # Green + "#F59E0B", # Amber + "#8B5CF6", # Purple + "#EC4899", # Pink + "#6366F1", # Indigo + "#14B8A6", # Teal +] + +class AvatarUpdate(BaseModel): + bg_color: Optional[str] = None + text: Optional[str] = None # 1-3 alphanumeric chars @router.get("/{user_id}", response_model=UserRead) def get_user_public(user_id: int, session: Session = Depends(get_session)): @@ -34,6 +52,15 @@ def update_my_profile( current_user.bio = update.bio if update.avatar is not None: current_user.avatar = update.avatar + if update.avatar_bg_color is not None: + # Validate it's a valid hex color or preset + if update.avatar_bg_color in AVATAR_COLORS or (update.avatar_bg_color.startswith('#') and len(update.avatar_bg_color) == 7): + current_user.avatar_bg_color = update.avatar_bg_color + if update.avatar_text is not None: + # Validate 1-3 alphanumeric characters + import re + if len(update.avatar_text) <= 3 and re.match(r'^[A-Za-z0-9]*$', update.avatar_text): + current_user.avatar_text = update.avatar_text if update.avatar_text else None if update.username or update.display_name: # Find or create primary profile @@ -74,6 +101,42 @@ def update_my_profile( session.refresh(current_user) return current_user +@router.patch("/me/avatar") +def update_avatar( + update: AvatarUpdate, + current_user: User = Depends(get_current_user), + session: Session = Depends(get_session) +): + """Update avatar customization""" + import re + + if update.bg_color is not None: + if update.bg_color in AVATAR_COLORS or (update.bg_color.startswith('#') and len(update.bg_color) == 7): + current_user.avatar_bg_color = update.bg_color + else: + raise HTTPException(status_code=400, detail="Invalid color. Use a preset color or valid hex.") + + if update.text is not None: + if len(update.text) > 3: + raise HTTPException(status_code=400, detail="Avatar text must be 3 characters or less") + if update.text and not re.match(r'^[A-Za-z0-9]*$', update.text): + raise HTTPException(status_code=400, detail="Avatar text must be alphanumeric") + current_user.avatar_text = update.text.upper() if update.text else None + + session.add(current_user) + session.commit() + session.refresh(current_user) + + return { + "avatar_bg_color": current_user.avatar_bg_color, + "avatar_text": current_user.avatar_text + } + +@router.get("/avatar/colors") +def get_avatar_colors(): + """Get available avatar preset colors""" + return {"colors": AVATAR_COLORS} + @router.patch("/me/preferences", response_model=UserPreferencesUpdate) def update_preferences( prefs: UserPreferencesUpdate, diff --git a/backend/schemas.py b/backend/schemas.py index 2946e92..325ab20 100644 --- a/backend/schemas.py +++ b/backend/schemas.py @@ -12,6 +12,8 @@ class UserRead(SQLModel): email: str is_active: bool is_superuser: bool + avatar_bg_color: Optional[str] = "#3B82F6" + avatar_text: Optional[str] = None class Token(SQLModel): access_token: str diff --git a/frontend/app/profile/page.tsx b/frontend/app/profile/page.tsx index 02d1010..823eec8 100644 --- a/frontend/app/profile/page.tsx +++ b/frontend/app/profile/page.tsx @@ -14,7 +14,7 @@ import { UserGroupsList } from "@/components/profile/user-groups-list" import { ChaseSongsList } from "@/components/profile/chase-songs-list" import { AttendanceSummary } from "@/components/profile/attendance-summary" import { LevelProgressCard } from "@/components/gamification/level-progress" -import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar" +import { UserAvatar } from "@/components/ui/user-avatar" import { motion } from "framer-motion" // Types @@ -23,6 +23,8 @@ interface UserProfile { email: string username: string avatar: string | null + avatar_bg_color: string | null + avatar_text: string | null bio: string | null created_at: string } @@ -125,10 +127,13 @@ export default function ProfilePage() {
+ Leave empty to show first letter of username +
+{error}
+ )} + + +