Files
task-manager/client/src/pages/AchievementsPage.tsx
T
paul 9819d8db0b
continuous-integration/drone/push Build is failing
feat: Notifications page, Sidebar layout fixes, and Achievements enhancements
- implemented /notifications page with overdue/upcoming alerts
- Fixed sidebar scrolling in collapsed mode
- Moved notification button to sidebar footer
- Enhanced Achievements page with streak stats and tooltips
- Improved XP history to show task titles
- Added missing translations (en/de)
- Removed top bar header
- Fixed Docker environment routing
2025-12-17 10:06:36 +01:00

743 lines
51 KiB
TypeScript

import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { useTranslation } from 'react-i18next';
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { Progress } from "@/components/ui/progress";
import { Input } from "@/components/ui/input";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { BarChart, Bar, XAxis, YAxis, Tooltip, ResponsiveContainer, Cell } from 'recharts';
import { Trophy, Target, TrendingUp, Plus, CheckCircle2, Circle, Flame, Scroll, BookOpen, Hammer, Award, Medal, Star, Crown, Zap, Sparkles, Sun, Timer } from 'lucide-react';
import { useState, useCallback } from 'react';
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from '@/components/ui/dialog';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
import { Goal, Reward, User } from '@shared/schema';
import { getLevelFromXP, getRankKey, LEVEL_THRESHOLDS } from '@/lib/gamification';
import { cn } from "@/lib/utils";
import { RewardCard } from '@/components/gamification/RewardCard';
import { useToast } from "@/hooks/use-toast";
import { Tooltip as UITooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip";
export default function AchievementsPage({ user }: { user: User }) {
const { t } = useTranslation();
const queryClient = useQueryClient();
const { toast } = useToast();
const [isGoalModalOpen, setIsGoalModalOpen] = useState(false);
// Mock User Data (Replace with context later)
// const user = { xp: 350, streak: 5, bestStreak: 12 };
const level = getLevelFromXP(user.xp);
const rankKey = getRankKey(level);
// Fetch Analytics Data
const { data: weeklyData } = useQuery({
queryKey: ['/api/analytics/weekly'],
queryFn: async () => {
const res = await fetch('/api/analytics/weekly');
return res.json();
}
});
const { data: yearlyData } = useQuery({
queryKey: ['/api/analytics/yearly'],
queryFn: async () => {
const res = await fetch('/api/analytics/yearly');
return res.json();
}
});
const { data: monthlyData } = useQuery({
queryKey: ['/api/analytics/monthly'],
queryFn: async () => {
const res = await fetch('/api/analytics/monthly');
return res.json();
}
});
// Fetch Goals
const { data: goals = [] } = useQuery<Goal[]>({
queryKey: ['/api/goals'],
});
// Create Goal Mutation
const createGoalMutation = useMutation({
mutationFn: async (newGoal: any) => {
const res = await fetch('/api/goals', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(newGoal)
});
return res.json();
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['/api/goals'] });
setIsGoalModalOpen(false);
}
});
// Rewards
const { data: rewards = [] } = useQuery<Reward[]>({
queryKey: ['/api/rewards', user.id],
queryFn: async () => {
const res = await fetch(`/api/rewards?userId=${user.id}`);
return res.json();
}
});
const { data: history = [] } = useQuery<any[]>({
queryKey: ['/api/user/history'],
});
const { data: inventory = [] } = useQuery<any[]>({
queryKey: ['/api/user/inventory'],
});
const handleCreateGoal = (e: React.FormEvent) => {
e.preventDefault();
const formData = new FormData(e.target as HTMLFormElement);
createGoalMutation.mutate({
title: formData.get('title'),
target: parseInt(formData.get('target') as string),
type: formData.get('type'),
userId: user.id
});
};
const CustomTooltip = ({ active, payload, label }: any) => {
if (active && payload && payload.length) {
return (
<div className="rounded-lg border bg-background/95 p-3 shadow-xl backdrop-blur-sm">
<div className="mb-1 text-xs font-medium text-muted-foreground">
{t(`analytics.${label}`) === `analytics.${label}` ?
(label && !isNaN(label) ? `${t('analytics.cw')} ${label}` : label)
: t(`analytics.${label}`)}
</div>
<div className="flex items-center gap-2">
<div className="h-2 w-2 rounded-full bg-primary" />
<span className="text-sm font-bold">
{payload[0].value} XP
</span>
</div>
</div>
);
}
return null;
};
// Create Reward Mutation
const createRewardMutation = useMutation({
mutationFn: async (newReward: { title: string, description: string, cost: number }) => {
const res = await fetch('/api/rewards', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
...newReward,
icon: "gift", // Default icon for now
type: "virtual"
})
});
if (!res.ok) throw new Error("Failed to create reward");
return res.json();
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['/api/rewards'] });
toast({ title: t('rewards.created', 'Reward created!') });
},
onError: () => {
toast({ title: t('rewards.createError', 'Failed to create reward'), variant: "destructive" });
}
});
const handleCreateReward = (e: React.FormEvent) => {
e.preventDefault();
const formData = new FormData(e.target as HTMLFormElement);
createRewardMutation.mutate({
title: formData.get('title') as string,
description: formData.get('description') as string,
cost: parseInt(formData.get('cost') as string)
});
};
const [activeTab, setActiveTab] = useState("overview");
return (
<div className="space-y-6 pb-20 md:pb-0">
<div className="flex items-center justify-between">
<div>
<h1 className="text-3xl font-bold tracking-tight mb-1">{t('achievements.title')}</h1>
<p className="text-muted-foreground">{t('achievements.subtitle')}</p>
</div>
</div>
<div className="space-y-4">
<div className="md:inline-flex h-10 items-center justify-center rounded-md bg-muted p-1 text-muted-foreground w-full md:w-auto grid grid-cols-2 md:grid-cols-none gap-1 md:gap-0 h-auto md:h-10">
{['overview', 'rewards', 'inventory', 'history', 'rules'].map((tab) => (
<button
key={tab}
onClick={() => setActiveTab(tab)}
className={cn(
"inline-flex items-center justify-center whitespace-nowrap rounded-sm px-3 py-1.5 text-sm font-medium ring-offset-background transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
activeTab === tab ? "bg-background text-foreground shadow-sm" : "hover:bg-background/50"
)}
>
{tab === 'rewards' ? t('rewards.shopTitle') :
tab === 'rules' ? t('gamification.rules.title') :
t(`achievements.${tab}`)}
</button>
))}
</div>
{activeTab === 'overview' && (
<div className="space-y-4">
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-4">
{/* Level Card */}
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">{t('gamification.level', { level })}</CardTitle>
<Trophy className="h-4 w-4 text-yellow-500" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold text-foreground">{t(`ranks.${rankKey}`)}</div>
<p className="text-xs text-muted-foreground flex items-center mt-1">
<span className="text-green-500 flex items-center mr-1">
<TrendingUp className="h-3 w-3 mr-1" /> +15%
</span>
{t('achievements.fromLastWeek')}
</p>
</CardContent>
</Card>
{/* Streak Card */}
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<div className="flex items-center gap-2">
<CardTitle className="text-sm font-medium">{t('achievements.currentStreak')}</CardTitle>
<UITooltip>
<TooltipTrigger>
<div className="h-4 w-4 rounded-full border border-muted-foreground/30 flex items-center justify-center text-[10px] text-muted-foreground">?</div>
</TooltipTrigger>
<TooltipContent>
<p>{t('gamification.rules.streakTooltip')}</p>
</TooltipContent>
</UITooltip>
</div>
<Flame className="h-4 w-4 text-orange-500" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">{t('achievements.days', { count: user.currentStreak })}</div>
<p className="text-xs text-muted-foreground mt-1 mb-3">
{t('achievements.bestStreak', { count: user.currentStreak })}
</p>
<div className="grid grid-cols-2 gap-2 pt-2 border-t">
<div className="space-y-1">
<div className="flex items-center gap-1">
<span className="text-[10px] text-muted-foreground uppercase">{t('achievements.weeklyStreak')}</span>
<UITooltip>
<TooltipTrigger>
<div className="h-3 w-3 rounded-full border border-muted-foreground/30 flex items-center justify-center text-[8px] text-muted-foreground">?</div>
</TooltipTrigger>
<TooltipContent>
<p>{t('gamification.rules.streakBonus')}</p>
</TooltipContent>
</UITooltip>
</div>
<div className="font-bold text-sm">0</div>
</div>
<div className="space-y-1">
<div className="flex items-center gap-1">
<span className="text-[10px] text-muted-foreground uppercase">{t('achievements.monthlyStreak')}</span>
<UITooltip>
<TooltipTrigger>
<div className="h-3 w-3 rounded-full border border-muted-foreground/30 flex items-center justify-center text-[8px] text-muted-foreground">?</div>
</TooltipTrigger>
<TooltipContent>
<p>{t('gamification.rules.streakBonus')}</p>
</TooltipContent>
</UITooltip>
</div>
<div className="font-bold text-sm">0</div>
</div>
</div>
</CardContent>
</Card>
{/* More stats placeholder */}
</div>
<div className="grid gap-4 md:grid-cols-7">
<Card className="col-span-4">
<CardHeader>
<CardTitle>{t('achievements.xpActivity')}</CardTitle>
<CardDescription>{t('achievements.xpDescription')}</CardDescription>
</CardHeader>
<CardContent className="pl-2">
<Tabs defaultValue="weekly" className="space-y-4">
<div className="flex items-center justify-end px-4">
<TabsList>
<TabsTrigger value="weekly">{t('achievements.weekly')}</TabsTrigger>
<TabsTrigger value="monthly">{t('achievements.monthly')}</TabsTrigger>
<TabsTrigger value="yearly">{t('achievements.yearly')}</TabsTrigger>
</TabsList>
</div>
<TabsContent value="monthly" className="h-[300px]">
<ResponsiveContainer width="100%" height="100%">
<BarChart data={monthlyData}>
<XAxis
dataKey="labelKey"
stroke="#888888"
fontSize={12}
tickLine={false}
axisLine={false}
tickFormatter={(val) => {
const translationKey = `analytics.${val}`;
const translated = t(translationKey);
return translated !== translationKey ? translated : `${t('analytics.cw')} ${val}`;
}}
/>
<YAxis
stroke="#888888"
fontSize={12}
tickLine={false}
axisLine={false}
tickFormatter={(value) => `${value}`}
/>
<Tooltip content={<CustomTooltip />} cursor={{ fill: 'rgba(255,255,255,0.05)' }} />
<Bar dataKey="xp" radius={[4, 4, 0, 0]}>
{monthlyData?.map((entry: any, index: number) => (
<Cell key={`cell-${index}`} fill={entry.xp > 300 ? 'hsl(var(--primary))' : 'hsl(var(--muted-foreground))'} opacity={0.8} />
))}
</Bar>
</BarChart>
</ResponsiveContainer>
</TabsContent>
<TabsContent value="weekly" className="h-[300px]">
<ResponsiveContainer width="100%" height="100%">
<BarChart data={weeklyData}>
<XAxis
dataKey="labelKey"
stroke="#888888"
fontSize={12}
tickLine={false}
axisLine={false}
tickFormatter={(val) => t(`analytics.${val}`)}
/>
<YAxis
stroke="#888888"
fontSize={12}
tickLine={false}
axisLine={false}
tickFormatter={(value) => `${value}`}
/>
<Tooltip content={<CustomTooltip />} cursor={{ fill: 'rgba(255,255,255,0.05)' }} />
<Bar dataKey="xp" radius={[4, 4, 0, 0]}>
{weeklyData?.map((entry: any, index: number) => (
<Cell key={`cell-${index}`} fill={entry.xp > 300 ? 'hsl(var(--primary))' : 'hsl(var(--muted-foreground))'} opacity={0.8} />
))}
</Bar>
</BarChart>
</ResponsiveContainer>
</TabsContent>
<TabsContent value="yearly" className="h-[300px]">
<ResponsiveContainer width="100%" height="100%">
<BarChart data={yearlyData}>
<XAxis
dataKey="labelKey"
stroke="#888888"
fontSize={12}
tickLine={false}
axisLine={false}
tickFormatter={(val) => t(`analytics.${val}`)}
/>
<YAxis stroke="#888888" fontSize={12} tickLine={false} axisLine={false} />
<Tooltip content={<CustomTooltip />} cursor={{ fill: 'rgba(255,255,255,0.05)' }} />
<Bar dataKey="xp" fill="hsl(var(--primary))" radius={[4, 4, 0, 0]} opacity={0.8} />
</BarChart>
</ResponsiveContainer>
</TabsContent>
</Tabs>
</CardContent>
</Card>
<Card className="col-span-3">
<CardHeader className="flex flex-row items-center justify-between">
<div>
<CardTitle>{t('achievements.goals')}</CardTitle>
<CardDescription>{t('achievements.goalsDescription')}</CardDescription>
</div>
<Dialog open={isGoalModalOpen} onOpenChange={setIsGoalModalOpen}>
<DialogTrigger asChild>
<Button size="sm" variant="outline">
<Plus className="h-4 w-4 mr-2" />
{t('achievements.addGoal')}
</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>{t('achievements.createGoal')}</DialogTitle>
</DialogHeader>
<form onSubmit={handleCreateGoal} className="space-y-4 py-4">
<div className="space-y-2">
<label className="text-sm font-medium">{t('achievements.goalTitle')}</label>
<Input name="title" placeholder={t('achievements.goalTitlePlaceholder')} required />
</div>
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<label className="text-sm font-medium">{t('achievements.targetValue')}</label>
<Input name="target" type="number" placeholder="50" required />
</div>
<div className="space-y-2">
<label className="text-sm font-medium">{t('achievements.type')}</label>
<Select name="type" defaultValue="weekly_tasks">
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="weekly_tasks">{t('achievements.types.weekly_tasks')}</SelectItem>
<SelectItem value="total_xp">{t('achievements.types.total_xp')}</SelectItem>
<SelectItem value="streak">{t('achievements.types.streak')}</SelectItem>
</SelectContent>
</Select>
</div>
</div>
<Button type="submit" className="w-full">{t('achievements.createGoal')}</Button>
</form>
</DialogContent>
</Dialog>
</CardHeader>
<CardContent>
<div className="space-y-6">
{goals.length === 0 ? (
<div className="text-center text-muted-foreground py-8">
<Target className="h-8 w-8 mx-auto mb-2 opacity-20" />
<p className="text-sm">{t('achievements.noGoals')}</p>
</div>
) : (
goals.map((goal) => (
<div key={goal.id} className="space-y-2">
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
{goal.completed ? (
<CheckCircle2 className="h-4 w-4 text-green-500" />
) : (
<Circle className="h-4 w-4 text-muted-foreground" />
)}
<span className={`text-sm font-medium ${goal.completed ? 'line-through text-muted-foreground' : ''}`}>
{goal.title}
</span>
</div>
<span className="text-xs text-muted-foreground">
{goal.current} / {goal.target}
</span>
</div>
<Progress value={(goal.current / goal.target) * 100} className="h-2" />
</div>
))
)}
</div>
</CardContent>
</Card>
</div>
</div>
)}
{activeTab === 'rewards' && (
<div className="space-y-4">
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-4">
<Card className="col-span-full mb-4 bg-gradient-to-r from-primary/10 via-primary/5 to-transparent border-primary/20">
<CardHeader className="flex flex-row items-center justify-between">
<div className="flex flex-col gap-1">
<div className="flex items-center gap-2">
<Trophy className="h-5 w-5 text-primary" />
<CardTitle>{t('rewards.shopTitle')}</CardTitle>
</div>
<CardDescription>{t('achievements.rewardsDescription', { xp: user.xp })}</CardDescription>
</div>
<Dialog>
<DialogTrigger asChild>
<Button size="sm">
<Plus className="h-4 w-4 mr-2" />
{t('achievements.addReward')}
</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>{t('achievements.createCustomReward')}</DialogTitle>
</DialogHeader>
<form onSubmit={handleCreateReward} className="space-y-4 py-4">
<div className="space-y-2">
<label className="text-sm font-medium">{t('achievements.rewardTitle')}</label>
<Input name="title" required />
</div>
<div className="space-y-2">
<label className="text-sm font-medium">{t('achievements.rewardDescription')}</label>
<Input name="description" />
</div>
<div className="space-y-2">
<label className="text-sm font-medium">{t('achievements.rewardCost')}</label>
<Input name="cost" type="number" required />
</div>
<Button type="submit" className="w-full">{t('achievements.createRewardBtn')}</Button>
</form>
</DialogContent>
</Dialog>
</CardHeader>
</Card>
{rewards.map(reward => (
<div key={reward.id} className="h-full">
<RewardCard
reward={reward}
userXp={user.xp}
userId={user.id}
/>
</div>
))}
</div>
</div>
)}
{activeTab === 'inventory' && (
<div className="space-y-4">
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-4">
{inventory.length === 0 ? (
<div className="col-span-full text-center py-12 text-muted-foreground">
<Trophy className="h-12 w-12 mx-auto mb-3 opacity-20" />
<p>{t('achievements.noInventory')}</p>
<Button variant="link" onClick={() => document.querySelector('[value="rewards"]')?.dispatchEvent(new MouseEvent('click', { bubbles: true }))}>
{t('achievements.goToShop')}
</Button>
</div>
) : (
inventory.map((item) => (
<Card key={item.id} className="overflow-hidden">
<div className="h-32 bg-muted flex items-center justify-center text-4xl">
{/* Quick icon mapping or default */}
{item.reward.icon === 'coffee' ? '☕' :
item.reward.icon === 'gamepad-2' ? '🎮' :
item.reward.icon === 'palette' ? '🎨' : '🎁'}
</div>
<CardHeader className="pb-2">
<CardTitle className="text-base">{item.reward.title.startsWith('rewards.') ? t(item.reward.title) : item.reward.title}</CardTitle>
</CardHeader>
<CardContent>
<p className="text-xs text-muted-foreground mb-2">
{item.reward.description?.startsWith('rewards.') ? t(item.reward.description) : item.reward.description}
</p>
<p className="text-xs text-muted-foreground">
{t('achievements.purchasedAt', { date: new Date(item.purchasedAt).toLocaleDateString() })}
</p>
</CardContent>
</Card>
))
)}
</div>
</div>
)}
{activeTab === 'history' && (
<div className="space-y-4">
<Card>
<CardHeader>
<CardTitle>{t('achievements.xpHistory')}</CardTitle>
</CardHeader>
<CardContent>
<div className="space-y-1">
{history.length === 0 ? (
<p className="text-center py-8 text-muted-foreground">{t('achievements.noHistory')}</p>
) : (
history.map((event) => (
<div key={event.id} className="flex items-center justify-between py-3 border-b last:border-0 hover:bg-muted/50 px-2 rounded-md transition-colors">
<div className="flex items-center gap-3">
<div className={`p-2 rounded-full ${event.source === 'complete_task_late' ? 'bg-red-100 text-red-600 dark:bg-red-900/30 dark:text-red-400' :
(event.source === 'task_completion' || event.source === 'complete_task') ? 'bg-green-100 text-green-600 dark:bg-green-900/30 dark:text-green-400' :
event.source === 'daily_streak' ? 'bg-orange-100 text-orange-600 dark:bg-orange-900/30 dark:text-orange-400' :
event.source === 'ai_action' ? 'bg-purple-100 text-purple-600 dark:bg-purple-900/30 dark:text-purple-400' :
'bg-blue-100 text-blue-600 dark:bg-blue-900/30 dark:text-blue-400'
}`}>
{event.source === 'complete_task_late' ? <Timer className="h-4 w-4" /> :
event.source.includes('complete') ? <CheckCircle2 className="h-4 w-4" /> :
event.source === 'daily_streak' ? <Flame className="h-4 w-4" /> :
event.source === 'ai_action' ? <Sparkles className="h-4 w-4" /> :
<Trophy className="h-4 w-4" />}
</div>
<div>
<p className="font-medium text-sm">
{event.details?.taskTitle ? event.details.taskTitle : (t(`gamification.source.${event.source}`, { defaultValue: event.source }) as string)}
{event.source === 'complete_task_late' && <span className="ml-2 text-xs text-red-500 font-normal">({t('gamification.late', 'Late')})</span>}
{(event.source === 'complete_task' || event.source === 'task_completion') && <span className="ml-2 text-xs text-green-500 font-normal">({t('gamification.onTime', 'On Time')})</span>}
</p>
<div className="text-xs text-muted-foreground flex items-center gap-1">
<span>{new Date(event.createdAt).toLocaleString()}</span>
{event.details?.taskTitle && (
<>
<span></span>
<span>{t(`gamification.source.${event.source}`, { defaultValue: event.source })}</span>
</>
)}
</div>
</div>
</div>
<span className="font-bold text-green-600 dark:text-green-400">
+{event.amount} XP
</span>
</div>
))
)}
</div>
</CardContent>
</Card>
</div>
)}
{activeTab === 'rules' && (
<div className="space-y-4">
<div className="grid gap-6 md:grid-cols-2">
{/* Level Requirements */}
<Card className="border-primary/10 shadow-md">
<CardHeader className="bg-muted/30 pb-4">
<div className="flex items-center gap-2">
<Trophy className="h-5 w-5 text-primary" />
<div>
<CardTitle>{t('gamification.rules.levelRequirements')}</CardTitle>
<CardDescription>{t('gamification.rules.xpSystem')}</CardDescription>
</div>
</div>
</CardHeader>
<CardContent className="p-0">
<div className="divide-y divide-border">
{LEVEL_THRESHOLDS.slice(0, 20).map((threshold, index) => {
const level = index + 1;
const rankKey = getRankKey(level);
const isCurrentLevel = getLevelFromXP(user.xp) === level;
// Visual Configuration for Ranks
const getRankStyle = (l: number) => {
if (l >= 20) return { icon: Sparkles, color: "text-rose-500", bg: "bg-rose-500/10", border: "border-rose-500/20" };
if (l >= 19) return { icon: Sun, color: "text-amber-500", bg: "bg-amber-500/10", border: "border-amber-500/20" };
if (l >= 18) return { icon: Crown, color: "text-yellow-600", bg: "bg-yellow-600/10", border: "border-yellow-600/20" };
if (l >= 17) return { icon: Zap, color: "text-violet-500", bg: "bg-violet-500/10", border: "border-violet-500/20" };
if (l >= 16) return { icon: Star, color: "text-cyan-500", bg: "bg-cyan-500/10", border: "border-cyan-500/20" };
if (l >= 15) return { icon: Award, color: "text-blue-500", bg: "bg-blue-500/10", border: "border-blue-500/20" };
if (l >= 14) return { icon: BookOpen, color: "text-indigo-500", bg: "bg-indigo-500/10", border: "border-indigo-500/20" };
if (l >= 13) return { icon: Scroll, color: "text-emerald-500", bg: "bg-emerald-500/10", border: "border-emerald-500/20" };
if (l >= 12) return { icon: Hammer, color: "text-slate-500", bg: "bg-slate-500/10", border: "border-slate-500/20" };
if (l >= 11) return { icon: Medal, color: "text-orange-500", bg: "bg-orange-500/10", border: "border-orange-500/20" };
// 1-10
if (l >= 10) return { icon: Sun, color: "text-rose-500", bg: "bg-rose-500/10", border: "border-rose-500/20" };
if (l >= 9) return { icon: Sparkles, color: "text-purple-500", bg: "bg-purple-500/10", border: "border-purple-500/20" };
if (l >= 8) return { icon: Zap, color: "text-violet-500", bg: "bg-violet-500/10", border: "border-violet-500/20" };
if (l >= 7) return { icon: Crown, color: "text-yellow-600", bg: "bg-yellow-600/10", border: "border-yellow-600/20" };
if (l >= 6) return { icon: Star, color: "text-yellow-500", bg: "bg-yellow-500/10", border: "border-yellow-500/20" };
if (l >= 5) return { icon: Medal, color: "text-orange-500", bg: "bg-orange-500/10", border: "border-orange-500/20" };
if (l >= 4) return { icon: Award, color: "text-blue-500", bg: "bg-blue-500/10", border: "border-blue-500/20" };
if (l >= 3) return { icon: Hammer, color: "text-cyan-500", bg: "bg-cyan-500/10", border: "border-cyan-500/20" };
if (l >= 2) return { icon: BookOpen, color: "text-green-500", bg: "bg-green-500/10", border: "border-green-500/20" };
return { icon: Scroll, color: "text-slate-500", bg: "bg-slate-500/10", border: "border-slate-500/20" };
};
const style = getRankStyle(level);
const RankIcon = style.icon;
return (
<div
key={index}
className={`flex items-center justify-between p-4 transition-all hover:bg-muted/50 ${isCurrentLevel ? 'bg-primary/5 ring-1 ring-inset ring-primary/20' : ''}`}
>
<div className="flex items-center gap-4">
<div className={`h-10 w-10 rounded-lg ${style.bg} ${style.color} flex items-center justify-center border ${style.border}`}>
<RankIcon className="h-5 w-5" />
</div>
<div>
<div className={`font-semibold ${isCurrentLevel ? 'text-primary' : ''}`}>
{t(`ranks.${rankKey}`)}
{isCurrentLevel && <span className="ml-2 text-xs bg-primary text-primary-foreground px-2 py-0.5 rounded-full">{t('gamification.level', { level })}</span>}
</div>
<div className="text-xs text-muted-foreground flex items-center gap-1">
{t('gamification.rules.level', { level })}
</div>
</div>
</div>
<div className="text-right">
<div className="font-mono font-medium text-sm">
{t('gamification.rules.xp', { xp: threshold })}
</div>
{isCurrentLevel && (
<div className="text-[10px] text-primary font-medium mt-0.5">
Current
</div>
)}
</div>
</div>
);
})}
</div>
</CardContent>
</Card>
{/* XP Rewards */}
<Card className="border-primary/10 shadow-md h-fit">
<CardHeader className="bg-muted/30 pb-4">
<div className="flex items-center gap-2">
<Target className="h-5 w-5 text-green-500" />
<div>
<CardTitle>{t('gamification.rules.actions')}</CardTitle>
<CardDescription>{t('gamification.rules.xpSystem')}</CardDescription>
</div>
</div>
</CardHeader>
<CardContent className="p-0">
<div className="divide-y divide-border">
{[
{ action: 'createTask', points: 10, icon: Plus, color: 'text-blue-500', bg: 'bg-blue-500/10' },
{ action: 'createSubtask', points: 5, icon: Plus, color: 'text-cyan-500', bg: 'bg-cyan-500/10' },
{ action: 'updateTask', points: 2, icon: CheckCircle2, color: 'text-slate-500', bg: 'bg-slate-500/10' },
{ action: 'completeTask', points: 50, icon: CheckCircle2, color: 'text-green-500', bg: 'bg-green-500/10' },
{ action: 'completeTaskLate', points: 20, icon: CheckCircle2, color: 'text-yellow-500', bg: 'bg-yellow-500/10' },
{ action: 'aiAction', points: 5, icon: Sparkles, color: 'text-purple-500', bg: 'bg-purple-500/10' },
{ action: 'dailyStreak', points: 100, icon: Flame, color: 'text-orange-500', bg: 'bg-orange-500/10', tooltip: 'streakTooltip' },
{ action: 'weeklyStreak', points: 300, icon: Flame, color: 'text-orange-600', bg: 'bg-orange-600/10', tooltip: 'streakBonus' },
{ action: 'monthlyStreak', points: 1000, icon: Flame, color: 'text-red-500', bg: 'bg-red-500/10', tooltip: 'streakBonus' },
].map((item, index) => {
const ActionIcon = item.icon;
const content = (
<div key={index} className="flex items-center justify-between p-4 hover:bg-muted/50 transition-colors cursor-help">
<div className="flex items-center gap-3">
<div className={`p-2 rounded-md ${item.bg} ${item.color}`}>
<ActionIcon className="h-4 w-4" />
</div>
<span className="font-medium text-sm">{t(`gamification.rules.${item.action}`)}</span>
</div>
<div className="font-bold text-green-600 dark:text-green-400 bg-green-100 dark:bg-green-900/30 px-2 py-1 rounded text-xs">
+{item.points} XP
</div>
</div>
);
if (item.tooltip) {
return (
<UITooltip key={index}>
<TooltipTrigger asChild>
{content}
</TooltipTrigger>
<TooltipContent>
<p className="max-w-[200px]">{t(`gamification.rules.${item.tooltip}`)}</p>
</TooltipContent>
</UITooltip>
);
}
return content;
})}
</div>
</CardContent>
</Card>
</div>
</div>
)}
</div>
</div>
);
}