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
147 lines
5.9 KiB
TypeScript
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>
|
|
)
|
|
}
|