2026-01-07 09:31:02 +00:00
|
|
|
import React, { useState, useEffect } from 'react';
|
|
|
|
|
import { Button } from '../ui/button';
|
|
|
|
|
import { SearchInput } from '../ui/input';
|
|
|
|
|
import { AchievementCard } from './AchievementCard';
|
|
|
|
|
import { Achievement } from '../../types';
|
|
|
|
|
import { Trophy, Lock, CheckCircle, Loader2 } from 'lucide-react';
|
|
|
|
|
import { gamificationService } from '../../services/gamificationService';
|
|
|
|
|
import { logger } from '@/utils/logger';
|
|
|
|
|
|
|
|
|
|
export const AchievementsView: React.FC = () => {
|
|
|
|
|
const [filter, setFilter] = useState<'all' | 'earned' | 'locked'>('all');
|
|
|
|
|
const [search, setSearch] = useState('');
|
|
|
|
|
const [achievements, setAchievements] = useState<Achievement[]>([]);
|
|
|
|
|
const [loading, setLoading] = useState(true);
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
2026-01-13 18:47:57 +00:00
|
|
|
const fetchData = async () => {
|
|
|
|
|
setLoading(true);
|
|
|
|
|
try {
|
|
|
|
|
const data = await gamificationService.getAchievements('me');
|
|
|
|
|
setAchievements(data);
|
|
|
|
|
} catch (e) {
|
|
|
|
|
logger.error('Error loading achievements', {
|
|
|
|
|
error: e instanceof Error ? e.message : String(e),
|
|
|
|
|
stack: e instanceof Error ? e.stack : undefined,
|
|
|
|
|
});
|
|
|
|
|
} finally {
|
|
|
|
|
setLoading(false);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
fetchData();
|
2026-01-07 09:31:02 +00:00
|
|
|
}, []);
|
|
|
|
|
|
2026-01-13 18:47:57 +00:00
|
|
|
const filtered = achievements.filter((ach) => {
|
|
|
|
|
const matchSearch = ach.name.toLowerCase().includes(search.toLowerCase());
|
|
|
|
|
const isEarned = ach.progress >= ach.maxProgress;
|
|
|
|
|
if (filter === 'earned') return matchSearch && isEarned;
|
|
|
|
|
if (filter === 'locked') return matchSearch && !isEarned;
|
|
|
|
|
return matchSearch;
|
2026-01-07 09:31:02 +00:00
|
|
|
});
|
|
|
|
|
|
2026-01-13 18:47:57 +00:00
|
|
|
const earnedCount = achievements.filter(
|
|
|
|
|
(a) => a.progress >= a.maxProgress,
|
|
|
|
|
).length;
|
2026-01-07 09:31:02 +00:00
|
|
|
|
2026-01-13 18:47:57 +00:00
|
|
|
if (loading)
|
|
|
|
|
return (
|
aesthetic-improvements: align spacing to 8px grid (Action 11.2.1.3)
- Created automated script (scripts/align-8px-grid.py) to align all spacing to 8px grid
- Replaced non-8px-aligned spacing: gap-3/p-3/m-3 (12px) → gap-4/p-4/m-4 (16px), gap-5/p-5/m-5 (20px) → gap-6/p-6/m-6 (24px), gap-10/p-10/m-10 (40px) → gap-12/p-12/m-12 (48px), gap-20/p-20/m-20 (80px) → gap-24/p-24/m-24 (96px)
- Preserved: 4px values (gap-1, p-1, m-1) as they may be intentional fine-tuning, responsive breakpoints (sm:, md:, lg:), test files, documentation
- Modified files across all components to ensure consistent 8px grid alignment
- Action 11.2.1.3: Align all elements to 8px grid - COMPLETE
2026-01-16 10:50:46 +00:00
|
|
|
<div className="flex justify-center py-24">
|
aesthetic-improvements: reduce decorative cyan in marketplace, gamification, commerce, and education (80/20 rule, batch 10)
- Marketplace: LicenceDetailsModal decorative icon and price text, ProductDetailView decorative author text, ReviewProductModal decorative icon, LicenceCard decorative price text (4 instances)
- Gamification: AchievementCard decorative XP reward text, LeaderboardView loading spinner and decorative XP text, ProfileXPView loading spinner and decorative icons, AchievementsView loading spinner (5 instances)
- Commerce: WishlistView decorative price text, PromoCodeModal decorative icon, CartItem decorative license tag icon, OrderSummary decorative total price text (4 instances)
- Education: MyCoursesView decorative icon (1 instance)
- Total: ~14 files, ~14 instances replaced
- Preserved: Functional links (LicenceDetailsModal legal contract link), active/selected states (CourseLearningView active lesson, QuizModal selected answer - already preserved), primary actions, design system variants
- Action 11.3.1.3 in progress (tenth batch: marketplace, gamification, commerce, and education components)
2026-01-16 10:23:25 +00:00
|
|
|
<Loader2 className="w-10 h-10 text-kodo-steel animate-spin" />
|
2026-01-13 18:47:57 +00:00
|
|
|
</div>
|
|
|
|
|
);
|
2026-01-07 09:31:02 +00:00
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<div className="space-y-6 animate-fadeIn pb-20">
|
2026-01-13 18:47:57 +00:00
|
|
|
{/* Header */}
|
ui(tokens): migrate border-kodo-steel to border-border (86 files, 269 instances)
Replace legacy hardcoded border-kodo-steel (RGB 59,69,84, theme-unaware)
with semantic border-border token across 86 user-facing components.
Covers UI primitives (checkbox, badge, modal, table, textarea, alert,
radio-group, avatar), all modals, settings views, social features,
playlist views, inventory, chat, commerce, and cloud file browser.
Only story/test files retain the legacy token.
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-08 23:07:00 +00:00
|
|
|
<div className="flex flex-col md:flex-row justify-between items-end border-b border-border/50 pb-6 gap-4">
|
2026-01-13 18:47:57 +00:00
|
|
|
<div>
|
2026-01-15 22:54:05 +00:00
|
|
|
<h2 className="text-2xl font-display font-bold text-white mb-2">
|
2026-01-13 18:47:57 +00:00
|
|
|
ACHIEVEMENTS
|
|
|
|
|
</h2>
|
2026-02-08 23:04:51 +00:00
|
|
|
<p className="text-muted-foreground font-mono text-sm">
|
2026-01-13 18:47:57 +00:00
|
|
|
Track your milestones and earn rewards.
|
|
|
|
|
</p>
|
2026-01-07 09:31:02 +00:00
|
|
|
</div>
|
ui(tokens): migrate border-kodo-steel to border-border (86 files, 269 instances)
Replace legacy hardcoded border-kodo-steel (RGB 59,69,84, theme-unaware)
with semantic border-border token across 86 user-facing components.
Covers UI primitives (checkbox, badge, modal, table, textarea, alert,
radio-group, avatar), all modals, settings views, social features,
playlist views, inventory, chat, commerce, and cloud file browser.
Only story/test files retain the legacy token.
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-08 23:07:00 +00:00
|
|
|
<div className="bg-kodo-ink px-4 py-2 rounded-lg border border-border flex items-center gap-4">
|
2026-01-13 18:47:57 +00:00
|
|
|
<Trophy className="w-5 h-5 text-kodo-gold" />
|
|
|
|
|
<span className="text-sm font-bold text-white">
|
|
|
|
|
{earnedCount} / {achievements.length} Unlocked
|
|
|
|
|
</span>
|
2026-01-07 09:31:02 +00:00
|
|
|
</div>
|
2026-01-13 18:47:57 +00:00
|
|
|
</div>
|
2026-01-07 09:31:02 +00:00
|
|
|
|
2026-01-13 18:47:57 +00:00
|
|
|
{/* Controls */}
|
ui(tokens): migrate border-kodo-steel to border-border (86 files, 269 instances)
Replace legacy hardcoded border-kodo-steel (RGB 59,69,84, theme-unaware)
with semantic border-border token across 86 user-facing components.
Covers UI primitives (checkbox, badge, modal, table, textarea, alert,
radio-group, avatar), all modals, settings views, social features,
playlist views, inventory, chat, commerce, and cloud file browser.
Only story/test files retain the legacy token.
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-08 23:07:00 +00:00
|
|
|
<div className="flex flex-col md:flex-row gap-4 items-center bg-kodo-ink/50 p-4 rounded-xl border border-border/50">
|
2026-01-13 18:47:57 +00:00
|
|
|
<div className="flex gap-2 w-full md:w-auto">
|
|
|
|
|
<Button
|
|
|
|
|
variant={filter === 'all' ? 'primary' : 'ghost'}
|
|
|
|
|
size="sm"
|
|
|
|
|
onClick={() => setFilter('all')}
|
|
|
|
|
>
|
|
|
|
|
All
|
|
|
|
|
</Button>
|
|
|
|
|
<Button
|
|
|
|
|
variant={filter === 'earned' ? 'primary' : 'ghost'}
|
|
|
|
|
size="sm"
|
|
|
|
|
onClick={() => setFilter('earned')}
|
|
|
|
|
icon={<CheckCircle className="w-3 h-3" />}
|
|
|
|
|
>
|
|
|
|
|
Earned
|
|
|
|
|
</Button>
|
|
|
|
|
<Button
|
|
|
|
|
variant={filter === 'locked' ? 'primary' : 'ghost'}
|
|
|
|
|
size="sm"
|
|
|
|
|
onClick={() => setFilter('locked')}
|
|
|
|
|
icon={<Lock className="w-3 h-3" />}
|
|
|
|
|
>
|
|
|
|
|
Locked
|
|
|
|
|
</Button>
|
2026-01-07 09:31:02 +00:00
|
|
|
</div>
|
2026-01-13 18:47:57 +00:00
|
|
|
<div className="w-full md:w-96">
|
|
|
|
|
<SearchInput
|
|
|
|
|
placeholder="Search achievements..."
|
|
|
|
|
value={search}
|
|
|
|
|
onChange={(e) => setSearch(e.target.value)}
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* Grid */}
|
|
|
|
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
|
|
|
|
{filtered.map((ach) => (
|
|
|
|
|
<AchievementCard key={ach.id} achievement={ach} />
|
|
|
|
|
))}
|
|
|
|
|
</div>
|
2026-01-07 09:31:02 +00:00
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
};
|