199 lines
7.2 KiB
TypeScript
199 lines
7.2 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>
|
|
);
|
|
};
|