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:
Renato
2026-02-12 03:38:33 +01:00
parent d31575a143
commit a2ed69fdb8
68 changed files with 14321 additions and 397 deletions

View 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;