283 lines
14 KiB
TypeScript
283 lines
14 KiB
TypeScript
"use client";
|
|
|
|
import { useEffect, useState } from "react";
|
|
import { lmsApi, Course, Lesson } from "@/lib/api";
|
|
import Link from "next/link";
|
|
import { useAuth } from "@/context/AuthContext";
|
|
import { useRouter } from "next/navigation";
|
|
import { Rocket, CheckCircle2, ArrowRight, Star, Calendar, Clock, AlertCircle, Zap, TrendingUp } from "lucide-react";
|
|
import Leaderboard from "@/components/Leaderboard";
|
|
|
|
export default function CatalogPage() {
|
|
const [courses, setCourses] = useState<Course[]>([]);
|
|
const [enrollments, setEnrollments] = useState<string[]>([]);
|
|
const [loading, setLoading] = useState(true);
|
|
const [gamification, setGamification] = useState<{ points: number, level: number, badges: any[] } | null>(null);
|
|
const [upcomingDeadlines, setUpcomingDeadlines] = useState<{ lesson: Lesson, courseTitle: string, courseId: string }[]>([]);
|
|
|
|
const { user } = useAuth();
|
|
const router = useRouter();
|
|
|
|
useEffect(() => {
|
|
const fetchData = async () => {
|
|
try {
|
|
const coursesData = await lmsApi.getCatalog(user?.organization_id, user?.id);
|
|
setCourses(coursesData);
|
|
|
|
if (user) {
|
|
const enrollmentData = await lmsApi.getEnrollments(user.id);
|
|
setEnrollments(enrollmentData.map(e => e.course_id));
|
|
|
|
const gamificationData = await lmsApi.getGamification(user.id);
|
|
setGamification(gamificationData);
|
|
|
|
// Fetch deadlines for enrolled courses
|
|
const deadlines: { lesson: Lesson, courseTitle: string, courseId: string }[] = [];
|
|
for (const enrollment of enrollmentData) {
|
|
try {
|
|
const { course, modules } = await lmsApi.getCourseOutline(enrollment.course_id);
|
|
modules.forEach(mod => {
|
|
mod.lessons.forEach(l => {
|
|
if (l.due_date && new Date(l.due_date) >= new Date()) {
|
|
deadlines.push({ lesson: l, courseTitle: course.title, courseId: enrollment.course_id });
|
|
}
|
|
});
|
|
});
|
|
} catch (err) {
|
|
console.error(`No se pudo cargar el esquema del curso ${enrollment.course_id}`, err);
|
|
}
|
|
}
|
|
setUpcomingDeadlines(deadlines.sort((a, b) => new Date(a.lesson.due_date!).getTime() - new Date(b.lesson.due_date!).getTime()).slice(0, 3));
|
|
}
|
|
} catch (err) {
|
|
console.error(err);
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
fetchData();
|
|
}, [user]);
|
|
|
|
const handleEnrollOrBuy = async (course: Course) => {
|
|
if (!user) {
|
|
router.push("/auth/login");
|
|
return;
|
|
}
|
|
|
|
try {
|
|
await lmsApi.enroll(course.id, user.id);
|
|
setEnrollments(prev => [...prev, course.id]);
|
|
} catch (err: any) {
|
|
// Check for 402 Payment Required
|
|
if (err.message.includes("Payment Required")) {
|
|
try {
|
|
const { init_point } = await lmsApi.createPaymentPreference(course.id);
|
|
window.location.href = init_point;
|
|
} catch (pErr) {
|
|
console.error("Falló la creación de preferencia de pago", pErr);
|
|
alert("No se pudo iniciar el proceso de pago.");
|
|
}
|
|
} else {
|
|
console.error("Falló la inscripción", err);
|
|
}
|
|
}
|
|
};
|
|
|
|
if (loading) {
|
|
return (
|
|
<div className="max-w-7xl mx-auto px-6 py-20">
|
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
|
|
{[1, 2, 3].map(i => (
|
|
<div key={i} className="h-80 glass-card animate-pulse bg-white/5 border-white/5 rounded-3xl"></div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div className="max-w-7xl mx-auto px-4 sm:px-6 py-10 md:py-20">
|
|
<div className="mb-12 md:mb-20 flex flex-col md:flex-row md:items-end justify-between gap-8 text-center md:text-left">
|
|
<div className="space-y-4">
|
|
<div className="flex items-center justify-center md:justify-start gap-2 text-[10px] font-black uppercase tracking-[0.3em] text-blue-500">
|
|
<Star size={14} className="fill-blue-500" />
|
|
<span>Currículo Premier</span>
|
|
</div>
|
|
<h1 className="text-4xl md:text-6xl font-black tracking-tighter leading-tight md:leading-none">
|
|
Explorar <span className="block sm:inline text-transparent bg-clip-text bg-gradient-to-r from-blue-400 to-indigo-600">Cursos</span>
|
|
</h1>
|
|
<p className="text-gray-500 font-medium max-w-xl text-base md:text-lg mx-auto md:mx-0">
|
|
Domina las habilidades del futuro con nuestro contenido educativo de alta fidelidad.
|
|
</p>
|
|
</div>
|
|
{!user && (
|
|
<Link href="/auth/register" className="btn-premium !bg-white !text-black shadow-none !px-8 w-full sm:w-auto">
|
|
Comienza Gratis
|
|
</Link>
|
|
)}
|
|
</div>
|
|
|
|
{user && gamification && (
|
|
<div className="mb-16 grid grid-cols-1 lg:grid-cols-3 gap-8 animate-in fade-in slide-in-from-top-6 duration-700">
|
|
<div className="lg:col-span-2 space-y-8">
|
|
<div className="glass-card p-10 bg-gradient-to-br from-blue-600/20 via-indigo-700/10 to-transparent border-blue-500/20 rounded-3xl relative overflow-hidden group">
|
|
<div className="relative z-10 flex flex-col md:flex-row md:items-center gap-10">
|
|
<div className="flex-shrink-0 relative">
|
|
<div className="w-24 h-24 rounded-3xl bg-blue-600 flex items-center justify-center shadow-2xl shadow-blue-500/40 rotate-3 group-hover:rotate-0 transition-transform duration-500">
|
|
<Zap className="text-white fill-white/20" size={48} />
|
|
</div>
|
|
<div className="absolute -bottom-2 -right-2 w-10 h-10 rounded-full bg-white text-black flex items-center justify-center font-black text-xs border-4 border-[#050505]">
|
|
{gamification.level}
|
|
</div>
|
|
</div>
|
|
|
|
<div className="flex-1 space-y-4">
|
|
<div>
|
|
<div className="text-[10px] font-black uppercase tracking-[0.3em] text-blue-400 mb-1">Posición Actual</div>
|
|
<h2 className="text-3xl font-black text-white">Nivel {gamification.level} Pionero</h2>
|
|
</div>
|
|
|
|
<div className="space-y-2">
|
|
<div className="flex justify-between items-end">
|
|
<div className="text-[10px] font-bold text-gray-500 uppercase tracking-widest">
|
|
{gamification.points} / {Math.pow(gamification.level, 2) * 100} XP
|
|
</div>
|
|
<div className="text-[10px] font-black text-blue-400 uppercase tracking-widest">
|
|
{Math.floor(((gamification.points - Math.pow(gamification.level - 1, 2) * 100) / (Math.pow(gamification.level, 2) * 100 - Math.pow(gamification.level - 1, 2) * 100)) * 100)}% para el Nivel {gamification.level + 1}
|
|
</div>
|
|
</div>
|
|
<div className="h-2 w-full bg-white/5 rounded-full overflow-hidden border border-white/5">
|
|
<div
|
|
className="h-full bg-gradient-to-r from-blue-500 to-indigo-500 shadow-[0_0_20px_rgba(59,130,246,0.5)] transition-all duration-1000"
|
|
style={{ width: `${Math.min(100, Math.max(0, ((gamification.points - Math.pow(gamification.level - 1, 2) * 100) / (Math.pow(gamification.level, 2) * 100 - Math.pow(gamification.level - 1, 2) * 100)) * 100))}%` }}
|
|
></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Background Flair */}
|
|
<div className="absolute -bottom-20 -right-20 w-64 h-64 bg-blue-500/10 blur-[120px] rounded-full pointer-events-none group-hover:bg-blue-500/20 transition-colors duration-500"></div>
|
|
</div>
|
|
|
|
<div className="glass-card p-8 bg-white/[0.01] border-white/5 rounded-3xl">
|
|
<h3 className="text-xs font-black uppercase tracking-widest text-gray-500 mb-6 flex items-center gap-2">
|
|
<CheckCircle2 size={14} /> Mis Insignias
|
|
</h3>
|
|
<div className="flex flex-wrap gap-4">
|
|
{gamification.badges.length === 0 ? (
|
|
<p className="text-sm text-gray-600 italic">Aún no has ganado insignias. ¡Comienza a aprender para desbloquear logros!</p>
|
|
) : (
|
|
gamification.badges.map(badge => (
|
|
<div key={badge.id} className="group/badge relative">
|
|
<div className="w-14 h-14 rounded-2xl bg-gradient-to-tr from-amber-400/10 to-orange-500/10 border border-amber-500/20 flex items-center justify-center shadow-lg transition-all hover:scale-110 hover:bg-amber-500/20 cursor-help" title={badge.description}>
|
|
<span className="text-2xl">🏆</span>
|
|
</div>
|
|
<div className="absolute -bottom-1 -right-1 w-5 h-5 bg-green-500 rounded-full border-2 border-[#050505] flex items-center justify-center text-[8px] font-bold">✓</div>
|
|
</div>
|
|
))
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="lg:col-span-1">
|
|
<Leaderboard />
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{user && upcomingDeadlines.length > 0 && (
|
|
<div className="mb-16 animate-in fade-in slide-in-from-top-4 duration-700 delay-200">
|
|
<h3 className="text-xs font-black uppercase tracking-[0.3em] text-gray-500 mb-6 flex items-center gap-2">
|
|
<Calendar size={14} /> Próximos Vencimientos
|
|
</h3>
|
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
|
|
{upcomingDeadlines.map(({ lesson, courseTitle, courseId }) => (
|
|
<Link key={lesson.id} href={`/courses/${courseId}/lessons/${lesson.id}`} className="block group">
|
|
<div className="glass-card p-6 border-blue-500/10 bg-blue-500/2 rounded-3xl hover:border-blue-500/30 transition-all">
|
|
<div className="flex justify-between items-start mb-4">
|
|
<div className="text-[10px] font-black uppercase tracking-widest text-blue-400 group-hover:text-blue-300 transition-colors">
|
|
{lesson.important_date_type || 'Actividad'}
|
|
</div>
|
|
<div className="text-right">
|
|
<div className="text-xs font-black text-white">{new Date(lesson.due_date!).toLocaleDateString()}</div>
|
|
<div className="text-[8px] font-bold text-gray-600 uppercase tracking-widest">Vencimiento</div>
|
|
</div>
|
|
</div>
|
|
<h4 className="font-bold text-sm text-gray-200 mb-1 group-hover:text-white transition-colors line-clamp-1">{lesson.title}</h4>
|
|
<p className="text-[10px] text-gray-500 font-bold uppercase tracking-widest">{courseTitle}</p>
|
|
</div>
|
|
</Link>
|
|
))}
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{courses.length === 0 ? (
|
|
<div className="py-20 text-center glass-card border-dashed border-white/10 rounded-3xl bg-white/[0.01]">
|
|
<p className="text-gray-500 font-bold uppercase tracking-widest">Aún no se han publicado cursos.</p>
|
|
</div>
|
|
) : (
|
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
|
|
{courses.map((course) => {
|
|
const isEnrolled = enrollments.includes(course.id);
|
|
|
|
return (
|
|
<div key={course.id} className="glass-card group relative overflow-hidden h-full flex flex-col p-8 border-white/5 bg-white/[0.02] hover:bg-white/[0.04] transition-all duration-500 rounded-3xl">
|
|
<div className="mb-8 flex items-start justify-between">
|
|
<div className="w-14 h-14 rounded-2xl bg-gradient-to-br from-blue-600 to-indigo-700 flex items-center justify-center shadow-2xl shadow-blue-500/20 group-hover:scale-110 transition-transform duration-500">
|
|
<Rocket size={24} className="text-white fill-white/10" />
|
|
</div>
|
|
{isEnrolled && (
|
|
<span className="text-[10px] font-black uppercase tracking-widest px-3 py-1 rounded-full bg-green-500/10 text-green-400 border border-green-500/20">
|
|
Inscrito
|
|
</span>
|
|
)}
|
|
</div>
|
|
|
|
<h2 className="text-2xl font-black text-white mb-4 leading-tight group-hover:text-blue-400 transition-colors">
|
|
{course.title}
|
|
</h2>
|
|
|
|
<div className="flex-1">
|
|
<p className="text-gray-500 text-sm font-medium line-clamp-3 mb-10 leading-relaxed">
|
|
{course.description || "Currículo detallado que cubre desde los principios fundamentales hasta el dominio avanzado, elaborado por veteranos de la industria."}
|
|
</p>
|
|
</div>
|
|
|
|
<div className="pt-8 border-t border-white/5 flex items-center justify-between mt-auto">
|
|
{isEnrolled ? (
|
|
<Link href={`/courses/${course.id}`} className="btn-premium w-full !bg-blue-600/10 !text-blue-400 border border-blue-500/20 hover:!bg-blue-600/20 !shadow-none gap-2">
|
|
Continuar Aprendiendo <ArrowRight size={16} />
|
|
</Link>
|
|
) : (
|
|
<button
|
|
onClick={() => handleEnrollOrBuy(course)}
|
|
className="btn-premium w-full group-hover:scale-[1.02] transition-transform flex items-center justify-center gap-2 group/btn"
|
|
>
|
|
{course.price > 0 ? (
|
|
<>
|
|
<span className="font-black text-lg mr-2">
|
|
{course.currency} {course.price.toFixed(0).replace(/\B(?=(\d{3})+(?!\d))/g, ".")}
|
|
</span>
|
|
Comprar Ahora
|
|
</>
|
|
) : (
|
|
<>
|
|
<CheckCircle2 size={18} className="text-white/50 group-hover/btn:text-white transition-colors" />
|
|
Inscribirse Gratis
|
|
</>
|
|
)}
|
|
</button>
|
|
)}
|
|
</div>
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|