155 lines
7.4 KiB
TypeScript
155 lines
7.4 KiB
TypeScript
|
|
|
||
|
|
import React, { useState, useEffect } from 'react';
|
||
|
|
import { Card } from '../ui/card';
|
||
|
|
import { Button } from '../ui/button';
|
||
|
|
import { XPBar } from './XPBar';
|
||
|
|
import { AchievementCard } from './AchievementCard';
|
||
|
|
import { TrendingUp, Target, Crown, Zap, Loader2 } from 'lucide-react';
|
||
|
|
import { Achievement } from '../../types';
|
||
|
|
import { gamificationService } from '../../services/gamificationService';
|
||
|
|
import { logger } from '@/utils/logger';
|
||
|
|
|
||
|
|
interface ProfileXPViewProps {
|
||
|
|
username: string;
|
||
|
|
}
|
||
|
|
|
||
|
|
export const ProfileXPView: React.FC<ProfileXPViewProps> = ({ username }) => {
|
||
|
|
const [xpData, setXpData] = useState<any>(null);
|
||
|
|
const [recentAchievements, setRecentAchievements] = useState<Achievement[]>([]);
|
||
|
|
const [loading, setLoading] = useState(true);
|
||
|
|
|
||
|
|
useEffect(() => {
|
||
|
|
const fetchData = async () => {
|
||
|
|
setLoading(true);
|
||
|
|
try {
|
||
|
|
const [xp, achievements] = await Promise.all([
|
||
|
|
gamificationService.getUserXP('me'),
|
||
|
|
gamificationService.getAchievements('me')
|
||
|
|
]);
|
||
|
|
setXpData(xp);
|
||
|
|
setRecentAchievements(achievements.slice(0, 3));
|
||
|
|
} catch (e) {
|
||
|
|
logger.error('Error loading profile XP data', {
|
||
|
|
error: e instanceof Error ? e.message : String(e),
|
||
|
|
stack: e instanceof Error ? e.stack : undefined,
|
||
|
|
username,
|
||
|
|
});
|
||
|
|
} finally {
|
||
|
|
setLoading(false);
|
||
|
|
}
|
||
|
|
};
|
||
|
|
fetchData();
|
||
|
|
}, []);
|
||
|
|
|
||
|
|
if (loading) return <div className="flex justify-center py-20"><Loader2 className="w-10 h-10 text-kodo-cyan animate-spin" /></div>;
|
||
|
|
|
||
|
|
return (
|
||
|
|
<div className="space-y-8 animate-fadeIn pb-20">
|
||
|
|
<h2 className="text-3xl font-display font-bold text-white mb-6">LEVEL & PROGRESS</h2>
|
||
|
|
|
||
|
|
{/* Main XP Card */}
|
||
|
|
<Card variant="gaming" className="p-8 relative overflow-hidden border-kodo-gold/30">
|
||
|
|
<div className="relative z-10 flex flex-col md:flex-row items-center gap-8">
|
||
|
|
{/* Level Badge */}
|
||
|
|
<div className="flex flex-col items-center justify-center">
|
||
|
|
<div className="w-24 h-24 bg-gradient-to-b from-kodo-gold to-orange-600 rounded-full flex items-center justify-center shadow-[0_0_30px_rgba(234,179,8,0.4)] border-4 border-black">
|
||
|
|
<div className="text-4xl font-black text-black">{xpData.level}</div>
|
||
|
|
</div>
|
||
|
|
<div className="mt-2 text-kodo-gold font-bold uppercase tracking-widest text-sm">Level</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
{/* Progress */}
|
||
|
|
<div className="flex-1 w-full space-y-4">
|
||
|
|
<div className="flex justify-between items-end">
|
||
|
|
<div>
|
||
|
|
<h3 className="text-2xl font-bold text-white">{username}</h3>
|
||
|
|
<p className="text-gray-400 text-sm">Producer • Rank #{xpData.rank}</p>
|
||
|
|
</div>
|
||
|
|
<div className="text-right">
|
||
|
|
<div className="text-2xl font-mono font-bold text-kodo-gold">{xpData.current} XP</div>
|
||
|
|
<div className="text-xs text-gray-500">Next Level: {xpData.next} XP</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<XPBar currentXP={xpData.current} nextLevelXP={xpData.next} level={xpData.level} size="lg" showLabels={false} />
|
||
|
|
|
||
|
|
<div className="flex gap-4 pt-2">
|
||
|
|
<div className="bg-black/30 px-3 py-1 rounded text-xs text-gray-400">
|
||
|
|
<span className="text-white font-bold">{xpData.totalEarned.toLocaleString()}</span> Total Lifetime XP
|
||
|
|
</div>
|
||
|
|
<div className="bg-black/30 px-3 py-1 rounded text-xs text-gray-400">
|
||
|
|
<span className="text-kodo-lime font-bold">+12%</span> vs Last Week
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</Card>
|
||
|
|
|
||
|
|
{/* Stats Grid */}
|
||
|
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
|
||
|
|
<Card variant="default" className="flex items-center gap-4 p-4">
|
||
|
|
<div className="w-12 h-12 bg-kodo-ink rounded-lg flex items-center justify-center text-kodo-gold">
|
||
|
|
<Crown className="w-6 h-6" />
|
||
|
|
</div>
|
||
|
|
<div>
|
||
|
|
<div className="text-xs text-gray-500 uppercase font-bold">Global Rank</div>
|
||
|
|
<div className="text-xl font-bold text-white">#{xpData.rank}</div>
|
||
|
|
</div>
|
||
|
|
</Card>
|
||
|
|
<Card variant="default" className="flex items-center gap-4 p-4">
|
||
|
|
<div className="w-12 h-12 bg-kodo-ink rounded-lg flex items-center justify-center text-kodo-cyan">
|
||
|
|
<Zap className="w-6 h-6" />
|
||
|
|
</div>
|
||
|
|
<div>
|
||
|
|
<div className="text-xs text-gray-500 uppercase font-bold">Daily Streak</div>
|
||
|
|
<div className="text-xl font-bold text-white">12 Days</div>
|
||
|
|
</div>
|
||
|
|
</Card>
|
||
|
|
<Card variant="default" className="flex items-center gap-4 p-4">
|
||
|
|
<div className="w-12 h-12 bg-kodo-ink rounded-lg flex items-center justify-center text-kodo-magenta">
|
||
|
|
<Target className="w-6 h-6" />
|
||
|
|
</div>
|
||
|
|
<div>
|
||
|
|
<div className="text-xs text-gray-500 uppercase font-bold">Quests Complete</div>
|
||
|
|
<div className="text-xl font-bold text-white">8/10</div>
|
||
|
|
</div>
|
||
|
|
</Card>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
{/* Recent Achievements */}
|
||
|
|
<div>
|
||
|
|
<div className="flex justify-between items-center mb-4">
|
||
|
|
<h3 className="font-bold text-white">Recent Achievements</h3>
|
||
|
|
<Button variant="ghost" size="sm">View All</Button>
|
||
|
|
</div>
|
||
|
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||
|
|
{recentAchievements.map(ach => (
|
||
|
|
<AchievementCard key={ach.id} achievement={ach} />
|
||
|
|
))}
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
{/* XP History Graph (Mock) */}
|
||
|
|
<Card variant="default">
|
||
|
|
<h3 className="font-bold text-white mb-6 flex items-center gap-2">
|
||
|
|
<TrendingUp className="w-5 h-5 text-kodo-cyan" /> XP History
|
||
|
|
</h3>
|
||
|
|
<div className="h-48 flex items-end gap-2 px-2">
|
||
|
|
{Array.from({length: 14}).map((_, i) => (
|
||
|
|
<div key={i} className="flex-1 flex flex-col justify-end gap-1 h-full group relative cursor-pointer">
|
||
|
|
<div className="w-full bg-kodo-gold rounded-t opacity-50 group-hover:opacity-100 transition-opacity" style={{height: `${Math.random() * 60 + 10}%`}}></div>
|
||
|
|
<div className="absolute -top-8 left-1/2 -translate-x-1/2 bg-black text-white text-xs px-2 py-1 rounded opacity-0 group-hover:opacity-100 pointer-events-none whitespace-nowrap">
|
||
|
|
+{Math.floor(Math.random() * 500)} XP
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
))}
|
||
|
|
</div>
|
||
|
|
<div className="flex justify-between text-xs text-gray-500 mt-2">
|
||
|
|
<span>14 Days Ago</span>
|
||
|
|
<span>Today</span>
|
||
|
|
</div>
|
||
|
|
</Card>
|
||
|
|
</div>
|
||
|
|
);
|
||
|
|
};
|