Fix login blank screen and progress persistence
- 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
This commit is contained in:
225
frontend/src/components/progress/Badges.tsx
Normal file
225
frontend/src/components/progress/Badges.tsx
Normal file
@@ -0,0 +1,225 @@
|
||||
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;
|
||||
Reference in New Issue
Block a user