veza/apps/web/src/components/gamification/ProfileXPView.tsx
senke 39b2b642d2 feat(web): UI premium Discord/Spotify-like — tokens, shadows, focus, layout
Plan UI premium 6–8 semaines (design system, shell, Storybook, a11y):

- Design system: DESIGN_TOKENS.md, APP_SHELL.md, FULL_LAYOUT_PAGE.md. Single source
  for layout/shell (index.css), shadows (design-system.css), durations/easing.
- Tokens: shadow-cover-depth, shadow-gold-glow, shadow-fab-glow; layout max-height
  (max-h-layout-drawer, max-h-layout-panel, max-h-layout-list). All duration-200/300/500
  replaced by --duration-fast/normal/slow. Arbitrary shadows replaced by token classes.
- Shell & player: Sidebar, Header, GlobalPlayer, MiniPlayer, PlayerQueue, PlayerControls,
  AudioPlayer use tokens; focus-visible on Sidebar, PlayerQueue, DropdownMenuTrigger/Item,
  TabsTrigger. Typography: text-[10px]/[9px] → text-xs where applicable.
- ESLint: no-restricted-syntax (warn) for w-/h-/rounded-/shadow-/text-/spacing arbitrary.
- Scripts: report-arbitrary-values.mjs, capture/compare/generate visual; visual-complete.spec.ts.
- Stories full layout: Dashboard, Playlists, Library, Settings, Profile in DashboardLayout.stories.
- .cursorrules + README: DESIGN_TOKENS, APP_SHELL, visual commands, no arbitrary without justification.
- apps/web/.gitignore: e2e test artifacts (test-results-visual, playwright-report-visual).

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-08 17:15:58 +01:00

199 lines
7.3 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-24">
<Loader2 className="w-10 h-10 text-kodo-steel animate-spin" />
</div>
);
return (
<div className="space-y-8 animate-fadeIn pb-20">
<h2 className="text-2xl font-display font-bold text-white mb-6">
LEVEL & PROGRESS
</h2>
{/* Main XP Card */}
<Card
variant="glass"
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-gold-glow 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-kodo-content-dim 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-kodo-content-dim">
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-4 py-1 rounded text-xs text-kodo-content-dim">
<span className="text-white font-bold">
{xpData.totalEarned.toLocaleString()}
</span>{' '}
Total Lifetime XP
</div>
<div className="bg-black/30 px-4 py-1 rounded text-xs text-kodo-content-dim">
<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-kodo-content-dim 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-steel">
<Zap className="w-6 h-6" />
</div>
<div>
<div className="text-xs text-kodo-content-dim 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-kodo-content-dim 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-steel" /> 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-kodo-content-dim mt-2">
<span>14 Days Ago</span>
<span>Today</span>
</div>
</Card>
</div>
);
};