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