feat: Implement core monetization features including course pricing, payment preference creation, and transaction management with Mercado Pago integration.

This commit is contained in:
2026-02-15 13:40:48 -03:00
parent f613f34a96
commit 34e72ae985
13 changed files with 895 additions and 194 deletions
+97 -31
View File
@@ -16,6 +16,7 @@ export default function CourseOutlinePage({ params }: { params: { id: string } }
const [recommendations, setRecommendations] = useState<Recommendation[]>([]);
const [loadingAI, setLoadingAI] = useState(false);
const [userGrades, setUserGrades] = useState<UserGrade[]>([]);
const [isEnrolled, setIsEnrolled] = useState(false);
useEffect(() => {
const fetchData = async () => {
@@ -27,6 +28,9 @@ export default function CourseOutlinePage({ params }: { params: { id: string } }
if (user) {
const grades = await lmsApi.getUserGrades(user.id, params.id);
setUserGrades(grades);
const enrollmentData = await lmsApi.getEnrollments(user.id);
setIsEnrolled(enrollmentData.some(e => e.course_id === params.id));
}
} catch (err) {
console.error(err);
@@ -44,6 +48,30 @@ export default function CourseOutlinePage({ params }: { params: { id: string } }
.finally(() => setLoadingAI(false));
}, [params.id, user]);
const handleEnrollOrBuy = async () => {
if (!user) {
window.location.href = "/auth/login";
return;
}
try {
await lmsApi.enroll(params.id, user.id);
setIsEnrolled(true);
} catch (err: any) {
if (err.message.includes("Payment Required")) {
try {
const { init_point } = await lmsApi.createPaymentPreference(params.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-4xl mx-auto px-6 py-20 animate-pulse">
@@ -131,6 +159,23 @@ export default function CourseOutlinePage({ params }: { params: { id: string } }
</div>
<div className="flex gap-2">
{!isEnrolled && (
<button
onClick={handleEnrollOrBuy}
className="btn-premium px-8 py-3 !bg-blue-600 !text-white shadow-lg shadow-blue-500/20 active:scale-95 flex items-center gap-2"
>
{courseData.price > 0 ? (
<>
<span className="font-black">{courseData.currency} {courseData.price.toFixed(0)}</span>
Comprar Ahora
</>
) : (
<>
<CheckCircle2 size={16} /> Inscribirse Gratis
</>
)}
</button>
)}
<Link href={`/courses/${params.id}/calendar`}>
<button className="px-6 py-3 glass hover:border-blue-500/50 transition-all font-bold text-xs uppercase tracking-widest flex items-center gap-3 active:scale-95">
<Calendar size={16} /> Cronología
@@ -217,41 +262,62 @@ export default function CourseOutlinePage({ params }: { params: { id: string } }
<div className="grid gap-3 pl-14">
{module.lessons.map((lesson) => (
<Link key={lesson.id} href={`/courses/${params.id}/lessons/${lesson.id}`}>
<div className="glass-card !p-4 group hover:bg-white/10 border-white/5 active:scale-[0.99] transition-all">
<div className="flex items-center justify-between">
<div className="flex items-center gap-4">
<div className="w-10 h-10 rounded-lg bg-white/5 flex items-center justify-center group-hover:bg-blue-500/20 transition-colors">
{lesson.content_type === 'video' ? (
<PlayCircle size={18} className="text-gray-400 group-hover:text-blue-400" />
) : (
<BookOpen size={18} className="text-gray-400 group-hover:text-blue-400" />
)}
</div>
<div>
<h3 className="text-sm font-bold text-gray-200 group-hover:text-white transition-colors">{lesson.title}</h3>
<span className="text-[10px] font-bold uppercase tracking-widest text-gray-500">
{lesson.content_type === 'activity' ? 'Actividad Interactiva' : 'Lección en Video'}
</span>
</div>
</div>
<div className="flex items-center gap-6">
{getStatusIcon(lesson.id, lesson.is_graded, lesson.allow_retry)}
{lesson.due_date && (
<div className="text-right hidden sm:block">
<div className="text-[9px] font-black uppercase tracking-widest text-gray-600">Vencimiento</div>
<div className={`text-[10px] font-bold ${new Date(lesson.due_date) < new Date() ? 'text-red-400' : 'text-blue-400'}`}>
{new Date(lesson.due_date).toLocaleDateString()}
</div>
isEnrolled ? (
<Link key={lesson.id} href={`/courses/${params.id}/lessons/${lesson.id}`}>
<div className="glass-card !p-4 group hover:bg-white/10 border-white/5 active:scale-[0.99] transition-all">
<div className="flex items-center justify-between">
<div className="flex items-center gap-4">
<div className="w-10 h-10 rounded-lg bg-white/5 flex items-center justify-center group-hover:bg-blue-500/20 transition-colors">
{lesson.content_type === 'video' ? (
<PlayCircle size={18} className="text-gray-400 group-hover:text-blue-400" />
) : (
<BookOpen size={18} className="text-gray-400 group-hover:text-blue-400" />
)}
</div>
<div>
<h3 className="text-sm font-bold text-gray-200 group-hover:text-white transition-colors">{lesson.title}</h3>
<span className="text-[10px] font-bold uppercase tracking-widest text-gray-500">
{lesson.content_type === 'activity' ? 'Actividad Interactiva' : 'Lección en Video'}
</span>
</div>
</div>
<div className="flex items-center gap-6">
{getStatusIcon(lesson.id, lesson.is_graded, lesson.allow_retry)}
{lesson.due_date && (
<div className="text-right hidden sm:block">
<div className="text-[9px] font-black uppercase tracking-widest text-gray-600">Vencimiento</div>
<div className={`text-[10px] font-bold ${new Date(lesson.due_date) < new Date() ? 'text-red-400' : 'text-blue-400'}`}>
{new Date(lesson.due_date).toLocaleDateString()}
</div>
</div>
)}
<div className="opacity-0 group-hover:opacity-100 transition-opacity">
<ChevronRight size={18} className="text-blue-500" />
</div>
)}
<div className="opacity-0 group-hover:opacity-100 transition-opacity">
<ChevronRight size={18} className="text-blue-500" />
</div>
</div>
</div>
</Link>
) : (
<div key={lesson.id} onClick={handleEnrollOrBuy} className="glass-card !p-4 group border-white/5 opacity-60 cursor-pointer hover:bg-white/5 transition-all">
<div className="flex items-center justify-between">
<div className="flex items-center gap-4">
<div className="w-10 h-10 rounded-lg bg-white/5 flex items-center justify-center">
<Clock size={18} className="text-gray-600" />
</div>
<div>
<h3 className="text-sm font-bold text-gray-500">{lesson.title}</h3>
<span className="text-[10px] font-bold uppercase tracking-widest text-gray-600 flex items-center gap-1">
Contenido Protegido
</span>
</div>
</div>
<div className="flex items-center gap-2 text-gray-600 font-bold text-[10px] uppercase tracking-widest">
<span>Bloqueado</span>
</div>
</div>
</div>
</Link>
)
))}
</div>
</div>
@@ -262,6 +328,6 @@ export default function CourseOutlinePage({ params }: { params: { id: string } }
<div className="mt-20">
<DiscussionBoard courseId={params.id} />
</div>
</div>
</div >
);
}