elmeg-demo/frontend/components/gamification/level-progress.tsx
fullsizemalt 5ffb428bb8 feat: Add gamification system
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
2025-12-21 18:58:42 -08:00

147 lines
5.9 KiB
TypeScript

"use client"
import { useState, useEffect } from "react"
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
import { Badge } from "@/components/ui/badge"
import { Progress } from "@/components/ui/progress"
import { Flame, Star, Trophy, Zap, TrendingUp } from "lucide-react"
import { getApiUrl } from "@/lib/api-config"
import { motion } from "framer-motion"
interface LevelProgress {
current_xp: number
level: number
level_name: string
xp_for_next: number
xp_progress: number
progress_percent: number
streak_days: number
}
const TIER_COLORS = {
bronze: "bg-amber-700/20 text-amber-600 border-amber-600/30",
silver: "bg-slate-400/20 text-slate-300 border-slate-400/30",
gold: "bg-yellow-500/20 text-yellow-400 border-yellow-500/30",
platinum: "bg-cyan-400/20 text-cyan-300 border-cyan-400/30",
diamond: "bg-purple-500/20 text-purple-300 border-purple-500/30",
}
export function LevelProgressCard() {
const [progress, setProgress] = useState<LevelProgress | null>(null)
const [loading, setLoading] = useState(true)
useEffect(() => {
fetchProgress()
}, [])
const fetchProgress = async () => {
const token = localStorage.getItem("token")
if (!token) {
setLoading(false)
return
}
try {
const res = await fetch(`${getApiUrl()}/gamification/me`, {
headers: { Authorization: `Bearer ${token}` }
})
if (res.ok) {
const data = await res.json()
setProgress(data)
}
} catch (err) {
console.error("Failed to fetch level progress", err)
} finally {
setLoading(false)
}
}
if (loading) {
return <div className="text-muted-foreground">Loading...</div>
}
if (!progress) {
return null
}
return (
<Card className="overflow-hidden">
<CardHeader className="bg-gradient-to-r from-primary/10 via-purple-500/10 to-pink-500/10">
<CardTitle className="flex items-center justify-between">
<div className="flex items-center gap-2">
<TrendingUp className="h-5 w-5 text-primary" />
Level Progress
</div>
{progress.streak_days > 0 && (
<Badge variant="outline" className="gap-1 bg-orange-500/10 text-orange-400 border-orange-500/30">
<Flame className="h-3 w-3" />
{progress.streak_days} day streak
</Badge>
)}
</CardTitle>
</CardHeader>
<CardContent className="pt-6 space-y-4">
{/* Level Badge */}
<motion.div
initial={{ scale: 0.9, opacity: 0 }}
animate={{ scale: 1, opacity: 1 }}
className="flex items-center gap-4"
>
<div className="relative">
<div className="w-16 h-16 rounded-full bg-gradient-to-br from-primary to-purple-600 flex items-center justify-center text-2xl font-bold text-white shadow-lg">
{progress.level}
</div>
<div className="absolute -bottom-1 -right-1 bg-background rounded-full p-1">
<Star className="h-4 w-4 text-yellow-500" />
</div>
</div>
<div>
<h3 className="text-xl font-bold">{progress.level_name}</h3>
<p className="text-sm text-muted-foreground">
{progress.current_xp.toLocaleString()} XP total
</p>
</div>
</motion.div>
{/* Progress Bar */}
<div className="space-y-2">
<div className="flex justify-between text-sm">
<span className="text-muted-foreground">
Level {progress.level + 1}
</span>
<span className="font-mono">
{progress.xp_progress} / {progress.xp_for_next} XP
</span>
</div>
<Progress value={progress.progress_percent} className="h-3" />
<p className="text-xs text-muted-foreground text-center">
{Math.round(progress.xp_for_next - progress.xp_progress)} XP until next level
</p>
</div>
{/* XP Tips */}
<div className="pt-4 border-t">
<p className="text-xs text-muted-foreground mb-2 font-medium">Earn XP by:</p>
<div className="grid grid-cols-2 gap-2 text-xs">
<div className="flex items-center gap-2">
<Trophy className="h-3 w-3 text-primary" />
<span>Rating performances</span>
</div>
<div className="flex items-center gap-2">
<Zap className="h-3 w-3 text-yellow-500" />
<span>Writing reviews</span>
</div>
<div className="flex items-center gap-2">
<Star className="h-3 w-3 text-purple-500" />
<span>Marking attendance</span>
</div>
<div className="flex items-center gap-2">
<Flame className="h-3 w-3 text-orange-500" />
<span>Daily streaks</span>
</div>
</div>
</div>
</CardContent>
</Card>
)
}