- Fix authStore to persist user data, not just isAuthenticated - Fix progressStore handling of undefined API responses - Remove minimax.md documentation file - All progress now properly saves to PostgreSQL - Login flow working correctly
226 lines
6.8 KiB
TypeScript
226 lines
6.8 KiB
TypeScript
import { motion } from 'framer-motion';
|
|
import {
|
|
Footprints,
|
|
BookOpen,
|
|
Scale,
|
|
StretchHorizontal,
|
|
Factory,
|
|
GraduationCap,
|
|
Target,
|
|
Award,
|
|
Lock,
|
|
Unlock,
|
|
Trophy
|
|
} from 'lucide-react';
|
|
import type { Badge } from '../../types';
|
|
|
|
const ICON_MAP: Record<string, React.ComponentType<{ size?: number | string; className?: string }>> = {
|
|
Footprints,
|
|
BookOpen,
|
|
Scale,
|
|
StretchHorizontal,
|
|
Factory,
|
|
GraduationCap,
|
|
Target,
|
|
Award,
|
|
};
|
|
|
|
interface BadgeCardProps {
|
|
badge: Badge;
|
|
size?: 'sm' | 'md' | 'lg';
|
|
}
|
|
|
|
export function BadgeCard({ badge, size = 'md' }: BadgeCardProps) {
|
|
const Icon = ICON_MAP[badge.icono] || Trophy;
|
|
|
|
const sizeClasses = {
|
|
sm: {
|
|
container: 'p-3',
|
|
icon: 20,
|
|
title: 'text-xs',
|
|
desc: 'text-[10px]',
|
|
},
|
|
md: {
|
|
container: 'p-4',
|
|
icon: 28,
|
|
title: 'text-sm',
|
|
desc: 'text-xs',
|
|
},
|
|
lg: {
|
|
container: 'p-5',
|
|
icon: 36,
|
|
title: 'text-base',
|
|
desc: 'text-sm',
|
|
},
|
|
};
|
|
|
|
if (badge.desbloqueado) {
|
|
return (
|
|
<motion.div
|
|
initial={{ scale: 0.8, opacity: 0 }}
|
|
animate={{ scale: 1, opacity: 1 }}
|
|
whileHover={{ scale: 1.05 }}
|
|
className={`${sizeClasses[size].container} bg-gradient-to-br from-yellow-50 to-orange-50 rounded-xl border-2 border-yellow-300 shadow-sm hover:shadow-md transition-all`}
|
|
>
|
|
<div className="flex flex-col items-center text-center">
|
|
<div className="relative">
|
|
<motion.div
|
|
animate={{
|
|
rotate: [0, -5, 5, 0],
|
|
scale: [1, 1.1, 1]
|
|
}}
|
|
transition={{
|
|
duration: 2,
|
|
repeat: Infinity,
|
|
repeatDelay: 3
|
|
}}
|
|
className="w-12 h-12 bg-gradient-to-br from-yellow-400 to-orange-500 rounded-full flex items-center justify-center mb-2"
|
|
>
|
|
<Icon size={sizeClasses[size].icon} className="text-white" />
|
|
</motion.div>
|
|
<motion.div
|
|
initial={{ scale: 0 }}
|
|
animate={{ scale: 1 }}
|
|
transition={{ delay: 0.3, type: "spring" }}
|
|
className="absolute -top-1 -right-1 w-5 h-5 bg-green-500 rounded-full flex items-center justify-center"
|
|
>
|
|
<Unlock size={12} className="text-white" />
|
|
</motion.div>
|
|
</div>
|
|
|
|
<h4 className={`${sizeClasses[size].title} font-bold text-gray-800 mb-1`}>
|
|
{badge.titulo}
|
|
</h4>
|
|
<p className={`${sizeClasses[size].desc} text-gray-600 line-clamp-2`}>
|
|
{badge.descripcion}
|
|
</p>
|
|
|
|
{badge.fechaDesbloqueo && (
|
|
<p className="text-[10px] text-gray-400 mt-2">
|
|
Desbloqueado: {new Date(badge.fechaDesbloqueo).toLocaleDateString()}
|
|
</p>
|
|
)}
|
|
</div>
|
|
</motion.div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div className={`${sizeClasses[size].container} bg-gray-50 rounded-xl border-2 border-gray-200 opacity-70`}>
|
|
<div className="flex flex-col items-center text-center">
|
|
<div className="relative">
|
|
<div className="w-12 h-12 bg-gray-300 rounded-full flex items-center justify-center mb-2">
|
|
<Icon size={sizeClasses[size].icon} className="text-gray-500" />
|
|
</div>
|
|
<div className="absolute -top-1 -right-1 w-5 h-5 bg-gray-400 rounded-full flex items-center justify-center">
|
|
<Lock size={12} className="text-white" />
|
|
</div>
|
|
</div>
|
|
|
|
<h4 className={`${sizeClasses[size].title} font-bold text-gray-500 mb-1`}>
|
|
{badge.titulo}
|
|
</h4>
|
|
|
|
<p className={`${sizeClasses[size].desc} text-gray-400 line-clamp-2`}>
|
|
{badge.descripcion}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
interface BadgesGridProps {
|
|
badges: Badge[];
|
|
columns?: 2 | 3 | 4;
|
|
size?: 'sm' | 'md' | 'lg';
|
|
}
|
|
|
|
export function BadgesGrid({ badges, columns = 4, size = 'md' }: BadgesGridProps) {
|
|
const columnClasses = {
|
|
2: 'grid-cols-2',
|
|
3: 'grid-cols-2 md:grid-cols-3',
|
|
4: 'grid-cols-2 md:grid-cols-3 lg:grid-cols-4',
|
|
};
|
|
|
|
return (
|
|
<div className={`grid ${columnClasses[columns]} gap-4`}>
|
|
{badges.map((badge, index) => (
|
|
<motion.div
|
|
key={badge.id}
|
|
initial={{ opacity: 0, y: 20 }}
|
|
animate={{ opacity: 1, y: 0 }}
|
|
transition={{ delay: index * 0.1 }}
|
|
>
|
|
<BadgeCard badge={badge} size={size} />
|
|
</motion.div>
|
|
))}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
interface BadgesSectionProps {
|
|
badgesDesbloqueados: Badge[];
|
|
badgesBloqueados: Badge[];
|
|
}
|
|
|
|
export function BadgesSection({ badgesDesbloqueados, badgesBloqueados }: BadgesSectionProps) {
|
|
const totalBadges = badgesDesbloqueados.length + badgesBloqueados.length;
|
|
const porcentaje = totalBadges > 0 ? Math.round((badgesDesbloqueados.length / totalBadges) * 100) : 0;
|
|
|
|
return (
|
|
<div className="space-y-6">
|
|
{/* Resumen */}
|
|
<div className="bg-white rounded-xl p-4 border border-gray-200">
|
|
<div className="flex items-center justify-between mb-3">
|
|
<div className="flex items-center gap-3">
|
|
<div className="w-10 h-10 bg-yellow-100 rounded-lg flex items-center justify-center">
|
|
<Trophy size={20} className="text-yellow-600" />
|
|
</div>
|
|
<div>
|
|
<h3 className="font-bold text-gray-900">Logros</h3>
|
|
<p className="text-sm text-gray-500">
|
|
{badgesDesbloqueados.length} de {totalBadges} desbloqueados
|
|
</p>
|
|
</div>
|
|
</div>
|
|
<span className="text-2xl font-bold text-yellow-600">
|
|
{porcentaje}%
|
|
</span>
|
|
</div>
|
|
<div className="w-full bg-gray-100 rounded-full h-3 overflow-hidden">
|
|
<motion.div
|
|
className="h-full bg-gradient-to-r from-yellow-400 to-orange-500 rounded-full"
|
|
initial={{ width: 0 }}
|
|
animate={{ width: `${porcentaje}%` }}
|
|
transition={{ duration: 0.8, ease: "easeOut" }}
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Badges Desbloqueados */}
|
|
{badgesDesbloqueados.length > 0 && (
|
|
<div>
|
|
<h4 className="text-sm font-semibold text-gray-700 mb-3 flex items-center gap-2">
|
|
<Unlock size={16} className="text-green-500" />
|
|
Desbloqueados ({badgesDesbloqueados.length})
|
|
</h4>
|
|
<BadgesGrid badges={badgesDesbloqueados} columns={4} />
|
|
</div>
|
|
)}
|
|
|
|
{/* Badges Bloqueados */}
|
|
{badgesBloqueados.length > 0 && (
|
|
<div>
|
|
<h4 className="text-sm font-semibold text-gray-500 mb-3 flex items-center gap-2">
|
|
<Lock size={16} className="text-gray-400" />
|
|
Por desbloquear ({badgesBloqueados.length})
|
|
</h4>
|
|
<BadgesGrid badges={badgesBloqueados} columns={4} />
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
export default BadgesGrid;
|