initial proyect
This commit is contained in:
commit
509272a643
31 files changed
+7170
No files matched your search
@@ -0,0 +1,241 @@
|
||||
import React, { useState } from 'react';
|
||||
import {
|
||||
FiX,
|
||||
FiTrash2,
|
||||
FiShoppingCart,
|
||||
FiTag,
|
||||
FiCheckCircle,
|
||||
FiArrowRight,
|
||||
FiLock,
|
||||
FiShield
|
||||
} from 'react-icons/fi';
|
||||
import type { CartItem } from '../../types/course';
|
||||
|
||||
interface CartDrawerProps {
|
||||
isOpen: boolean;
|
||||
cartItems: CartItem[];
|
||||
onClose: () => void;
|
||||
onRemoveItem: (courseId: string) => void;
|
||||
onClearCart: () => void;
|
||||
}
|
||||
|
||||
export const CartDrawer: React.FC<CartDrawerProps> = ({
|
||||
isOpen,
|
||||
cartItems,
|
||||
onClose,
|
||||
onRemoveItem,
|
||||
onClearCart,
|
||||
}) => {
|
||||
const [couponCode, setCouponCode] = useState('');
|
||||
const [appliedDiscount, setAppliedDiscount] = useState<number>(0);
|
||||
const [couponMessage, setCouponMessage] = useState<string | null>(null);
|
||||
const [isCheckingOut, setIsCheckingOut] = useState(false);
|
||||
const [checkoutSuccess, setCheckoutSuccess] = useState(false);
|
||||
|
||||
if (!isOpen) return null;
|
||||
|
||||
const rawSubtotal = cartItems.reduce((acc, item) => acc + item.course.price, 0);
|
||||
const discountAmount = (rawSubtotal * appliedDiscount) / 100;
|
||||
const finalTotal = Math.max(0, rawSubtotal - discountAmount);
|
||||
|
||||
const handleApplyCoupon = (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
const code = couponCode.trim().toUpperCase();
|
||||
if (code === 'DESCUENTO20' || code === 'PROMO20') {
|
||||
setAppliedDiscount(20);
|
||||
setCouponMessage('¡Cupón del 20% aplicado con éxito!');
|
||||
} else if (code === 'PROMO50' || code === 'RYNEXT50') {
|
||||
setAppliedDiscount(50);
|
||||
setCouponMessage('¡Cupón especial del 50% de descuento aplicado!');
|
||||
} else if (code.length > 0) {
|
||||
setCouponMessage('Cupón no válido. Prueba con "DESCUENTO20" o "RYNEXT50"');
|
||||
}
|
||||
};
|
||||
|
||||
const handleCheckout = () => {
|
||||
setIsCheckingOut(true);
|
||||
setTimeout(() => {
|
||||
setIsCheckingOut(false);
|
||||
setCheckoutSuccess(true);
|
||||
setTimeout(() => {
|
||||
setCheckoutSuccess(false);
|
||||
onClearCart();
|
||||
onClose();
|
||||
}, 3000);
|
||||
}, 1500);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 z-50 overflow-hidden bg-slate-950/70 backdrop-blur-sm animate-in fade-in duration-200">
|
||||
<div className="absolute inset-y-0 right-0 max-w-full flex pl-10">
|
||||
|
||||
{/* Slide panel */}
|
||||
<div className="w-screen max-w-md bg-slate-900 border-l border-slate-800 shadow-2xl flex flex-col justify-between">
|
||||
|
||||
{/* Top Header */}
|
||||
<div className="p-6 border-b border-slate-800 flex items-center justify-between bg-slate-950/50">
|
||||
<div className="flex items-center gap-2.5">
|
||||
<div className="p-2 rounded-lg bg-cyan-500/10 text-cyan-400">
|
||||
<FiShoppingCart className="text-lg" />
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="text-base font-bold text-white">Carrito de Compras</h3>
|
||||
<p className="text-xs text-slate-400">
|
||||
{cartItems.length} {cartItems.length === 1 ? 'curso seleccionado' : 'cursos seleccionados'}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="p-2 rounded-xl text-slate-400 hover:text-white hover:bg-slate-800 transition"
|
||||
aria-label="Cerrar carrito"
|
||||
>
|
||||
<FiX className="text-xl" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Cart Items List */}
|
||||
<div className="flex-1 overflow-y-auto p-6 space-y-4">
|
||||
{checkoutSuccess ? (
|
||||
<div className="text-center py-12 space-y-4 animate-in zoom-in-95 duration-300">
|
||||
<div className="w-16 h-16 rounded-full bg-emerald-500/20 text-emerald-400 flex items-center justify-center mx-auto text-3xl">
|
||||
<FiCheckCircle />
|
||||
</div>
|
||||
<h4 className="text-xl font-bold text-white">¡Compra Realizada con Éxito!</h4>
|
||||
<p className="text-xs text-slate-300 max-w-xs mx-auto">
|
||||
Hemos enviado los accesos a tu correo. Tus cursos ya están disponibles en tu biblioteca de aprendizaje.
|
||||
</p>
|
||||
<div className="p-3 bg-slate-950 rounded-xl border border-slate-800 text-xs text-cyan-400 font-mono">
|
||||
Orden #RY-{Math.floor(100000 + Math.random() * 900000)}
|
||||
</div>
|
||||
</div>
|
||||
) : cartItems.length > 0 ? (
|
||||
cartItems.map((item) => (
|
||||
<div
|
||||
key={item.course.id}
|
||||
className="flex gap-3.5 p-3 rounded-xl bg-slate-950/60 border border-slate-800/80 hover:border-slate-700 transition"
|
||||
>
|
||||
<img
|
||||
src={item.course.thumbnail}
|
||||
alt={item.course.title}
|
||||
className="w-20 h-14 object-cover rounded-lg bg-slate-800 shrink-0"
|
||||
/>
|
||||
|
||||
<div className="flex-1 min-w-0 flex flex-col justify-between">
|
||||
<div>
|
||||
<h4 className="text-xs font-bold text-white line-clamp-1">
|
||||
{item.course.title}
|
||||
</h4>
|
||||
<p className="text-[11px] text-slate-400 truncate">
|
||||
Por {item.course.instructor.name}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between pt-1">
|
||||
<span className="text-sm font-bold text-white font-mono">
|
||||
${item.course.price.toFixed(2)}
|
||||
</span>
|
||||
|
||||
<button
|
||||
onClick={() => onRemoveItem(item.course.id)}
|
||||
className="text-slate-500 hover:text-rose-400 text-xs flex items-center gap-1 transition"
|
||||
title="Eliminar curso"
|
||||
>
|
||||
<FiTrash2 />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
) : (
|
||||
<div className="text-center py-16 space-y-4">
|
||||
<div className="w-16 h-16 rounded-full bg-slate-800 text-slate-500 flex items-center justify-center mx-auto text-2xl">
|
||||
<FiShoppingCart />
|
||||
</div>
|
||||
<h4 className="text-sm font-bold text-white">Tu carrito está vacío</h4>
|
||||
<p className="text-xs text-slate-400 max-w-xs mx-auto">
|
||||
Explora nuestro catálogo de cursos en desarrollo, IA y diseño para comenzar.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Bottom Summary & Checkout Form */}
|
||||
{cartItems.length > 0 && !checkoutSuccess && (
|
||||
<div className="p-6 border-t border-slate-800 bg-slate-950/90 space-y-4">
|
||||
|
||||
{/* Coupon Form */}
|
||||
<form onSubmit={handleApplyCoupon} className="space-y-1.5">
|
||||
<div className="flex gap-2">
|
||||
<div className="relative flex-1">
|
||||
<FiTag className="absolute left-3 top-3 text-slate-500 text-xs" />
|
||||
<input
|
||||
type="text"
|
||||
value={couponCode}
|
||||
onChange={(e) => setCouponCode(e.target.value)}
|
||||
placeholder="Cupón (ej. DESCUENTO20)"
|
||||
className="w-full pl-8 pr-3 py-2 bg-slate-900 border border-slate-700 rounded-lg text-xs text-white placeholder-slate-500 focus:outline-none focus:border-cyan-500 uppercase"
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
type="submit"
|
||||
className="px-3 py-2 bg-slate-800 hover:bg-slate-700 text-slate-200 font-semibold text-xs rounded-lg transition"
|
||||
>
|
||||
Aplicar
|
||||
</button>
|
||||
</div>
|
||||
{couponMessage && (
|
||||
<p className={`text-[11px] ${appliedDiscount > 0 ? 'text-emerald-400' : 'text-amber-400'}`}>
|
||||
{couponMessage}
|
||||
</p>
|
||||
)}
|
||||
</form>
|
||||
|
||||
{/* Price Breakdown */}
|
||||
<div className="space-y-1.5 text-xs text-slate-400 pt-2 border-t border-slate-800">
|
||||
<div className="flex justify-between">
|
||||
<span>Subtotal</span>
|
||||
<span className="font-mono text-slate-200">${rawSubtotal.toFixed(2)}</span>
|
||||
</div>
|
||||
{appliedDiscount > 0 && (
|
||||
<div className="flex justify-between text-emerald-400">
|
||||
<span>Descuento ({appliedDiscount}%)</span>
|
||||
<span className="font-mono">-${discountAmount.toFixed(2)}</span>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex justify-between text-sm font-bold text-white pt-2 border-t border-slate-800">
|
||||
<span>Total a pagar</span>
|
||||
<span className="text-xl text-cyan-400 font-mono">${finalTotal.toFixed(2)}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Checkout Button */}
|
||||
<button
|
||||
onClick={handleCheckout}
|
||||
disabled={isCheckingOut}
|
||||
className="w-full py-3.5 bg-gradient-to-r from-blue-600 via-cyan-600 to-teal-500 hover:from-blue-500 hover:to-cyan-400 text-white font-bold rounded-xl text-sm shadow-xl shadow-cyan-500/25 transition-all flex items-center justify-center gap-2 cursor-pointer disabled:opacity-50"
|
||||
>
|
||||
{isCheckingOut ? (
|
||||
<span>Procesando pago seguro...</span>
|
||||
) : (
|
||||
<>
|
||||
<FiLock className="text-sm" />
|
||||
<span>Completar Pago Seguro</span>
|
||||
<FiArrowRight className="text-sm" />
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
|
||||
<div className="flex items-center justify-center gap-2 text-[11px] text-slate-500">
|
||||
<FiShield className="text-emerald-400" />
|
||||
<span>Garantía total de devolución de 30 días</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,79 @@
|
||||
import React from 'react';
|
||||
import {
|
||||
FiGrid,
|
||||
FiCode,
|
||||
FiCpu,
|
||||
FiLayout,
|
||||
FiShield,
|
||||
FiTrendingUp,
|
||||
FiDollarSign
|
||||
} from 'react-icons/fi';
|
||||
import { mockCategories } from '../../data/mockCourses';
|
||||
|
||||
interface CategoryPillsProps {
|
||||
selectedCategory: string;
|
||||
onSelectCategory: (categoryName: string) => void;
|
||||
}
|
||||
|
||||
export const CategoryPills: React.FC<CategoryPillsProps> = ({
|
||||
selectedCategory,
|
||||
onSelectCategory,
|
||||
}) => {
|
||||
const getCategoryIcon = (iconName: string) => {
|
||||
switch (iconName) {
|
||||
case 'FiCode':
|
||||
return <FiCode className="text-base" />;
|
||||
case 'FiCpu':
|
||||
return <FiCpu className="text-base" />;
|
||||
case 'FiLayout':
|
||||
return <FiLayout className="text-base" />;
|
||||
case 'FiShield':
|
||||
return <FiShield className="text-base" />;
|
||||
case 'FiTrendingUp':
|
||||
return <FiTrendingUp className="text-base" />;
|
||||
case 'FiDollarSign':
|
||||
return <FiDollarSign className="text-base" />;
|
||||
default:
|
||||
return <FiGrid className="text-base" />;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="w-full overflow-x-auto pb-3 pt-2 no-scrollbar">
|
||||
<div className="flex items-center gap-2.5 min-w-max">
|
||||
{mockCategories.map((cat) => {
|
||||
const isSelected =
|
||||
(cat.id === 'all' && selectedCategory === 'Todos') ||
|
||||
cat.name === selectedCategory ||
|
||||
(cat.id === 'all' && selectedCategory === '');
|
||||
|
||||
return (
|
||||
<button
|
||||
key={cat.id}
|
||||
onClick={() => onSelectCategory(cat.id === 'all' ? 'Todos' : cat.name)}
|
||||
className={`flex items-center gap-2.5 px-4 py-2.5 rounded-xl text-xs font-semibold transition-all cursor-pointer select-none ${
|
||||
isSelected
|
||||
? 'bg-gradient-to-r from-blue-600 to-cyan-500 text-white shadow-lg shadow-cyan-500/20 scale-[1.02]'
|
||||
: 'bg-slate-900/80 hover:bg-slate-800 text-slate-300 hover:text-white border border-slate-800 hover:border-slate-700'
|
||||
}`}
|
||||
>
|
||||
<span className={isSelected ? 'text-white' : 'text-cyan-400'}>
|
||||
{getCategoryIcon(cat.icon)}
|
||||
</span>
|
||||
<span>{cat.name}</span>
|
||||
<span
|
||||
className={`text-[10px] px-1.5 py-0.5 rounded-full font-mono ${
|
||||
isSelected
|
||||
? 'bg-white/20 text-white'
|
||||
: 'bg-slate-800 text-slate-400'
|
||||
}`}
|
||||
>
|
||||
{cat.count}
|
||||
</span>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,209 @@
|
||||
import React from 'react';
|
||||
import {
|
||||
FiStar,
|
||||
FiClock,
|
||||
FiBook,
|
||||
FiShoppingCart,
|
||||
FiCheck,
|
||||
FiEye,
|
||||
FiZap
|
||||
} from 'react-icons/fi';
|
||||
import type { Course } from '../../types/course';
|
||||
|
||||
interface CourseCardProps {
|
||||
course: Course;
|
||||
isInCart: boolean;
|
||||
onAddToCart: (course: Course) => void;
|
||||
onOpenDetails: (course: Course) => void;
|
||||
}
|
||||
|
||||
export const CourseCard: React.FC<CourseCardProps> = ({
|
||||
course,
|
||||
isInCart,
|
||||
onAddToCart,
|
||||
onOpenDetails,
|
||||
}) => {
|
||||
const discountPercent = Math.round(
|
||||
((course.originalPrice - course.price) / course.originalPrice) * 100
|
||||
);
|
||||
|
||||
const getBadgeStyle = (badge?: string) => {
|
||||
switch (badge) {
|
||||
case 'Bestseller':
|
||||
return 'bg-amber-500 text-slate-950';
|
||||
case 'Nuevo':
|
||||
return 'bg-emerald-500 text-white';
|
||||
case 'Oferta':
|
||||
return 'bg-rose-500 text-white';
|
||||
case 'Destacado':
|
||||
return 'bg-indigo-500 text-white';
|
||||
default:
|
||||
return 'bg-blue-500 text-white';
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="group relative flex flex-col justify-between rounded-2xl bg-slate-900/90 border border-slate-800 hover:border-slate-700 transition-all duration-300 hover:shadow-2xl hover:shadow-cyan-500/10 hover:-translate-y-1 overflow-hidden">
|
||||
|
||||
{/* Top Image Section */}
|
||||
<div>
|
||||
<div className="relative aspect-video w-full overflow-hidden bg-slate-800 cursor-pointer" onClick={() => onOpenDetails(course)}>
|
||||
<img
|
||||
src={course.thumbnail}
|
||||
alt={course.title}
|
||||
className="w-full h-full object-cover group-hover:scale-105 transition-transform duration-500"
|
||||
/>
|
||||
<div className="absolute inset-0 bg-gradient-to-t from-slate-950/80 via-transparent to-black/30" />
|
||||
|
||||
{/* Badge */}
|
||||
{course.badge && (
|
||||
<span
|
||||
className={`absolute top-3 left-3 px-2.5 py-1 rounded-md text-[10px] font-black uppercase tracking-wider shadow-md ${getBadgeStyle(
|
||||
course.badge
|
||||
)}`}
|
||||
>
|
||||
{course.badge === 'Bestseller' && '🔥 '}
|
||||
{course.badge}
|
||||
</span>
|
||||
)}
|
||||
|
||||
{/* Discount Tag */}
|
||||
{discountPercent > 0 && (
|
||||
<span className="absolute top-3 right-3 bg-rose-600/95 text-white px-2 py-0.5 rounded text-[10px] font-black tracking-wide shadow-md">
|
||||
-{discountPercent}% OFF
|
||||
</span>
|
||||
)}
|
||||
|
||||
{/* Quick Preview Hover Overlay */}
|
||||
<div className="absolute inset-0 bg-slate-950/60 opacity-0 group-hover:opacity-100 transition-opacity flex items-center justify-center gap-3 backdrop-blur-[2px]">
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onOpenDetails(course);
|
||||
}}
|
||||
className="px-3.5 py-2 bg-white/90 hover:bg-white text-slate-950 rounded-xl text-xs font-bold flex items-center gap-1.5 shadow-lg transform translate-y-2 group-hover:translate-y-0 transition-transform"
|
||||
>
|
||||
<FiEye /> Vista Rápida
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Duration & Lessons Overlay */}
|
||||
<div className="absolute bottom-2 left-3 right-3 flex items-center justify-between text-[11px] text-slate-300 font-medium">
|
||||
<span className="flex items-center gap-1 bg-slate-950/80 backdrop-blur-md px-2 py-0.5 rounded">
|
||||
<FiClock className="text-cyan-400" /> {course.duration}
|
||||
</span>
|
||||
<span className="flex items-center gap-1 bg-slate-950/80 backdrop-blur-md px-2 py-0.5 rounded">
|
||||
<FiBook className="text-cyan-400" /> {course.lessonsCount} lecciones
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Content Section */}
|
||||
<div className="p-5 space-y-3">
|
||||
{/* Category & Level */}
|
||||
<div className="flex items-center justify-between text-[11px]">
|
||||
<span className="font-semibold text-cyan-400 uppercase tracking-wider">
|
||||
{course.category}
|
||||
</span>
|
||||
<span className="px-2 py-0.5 rounded bg-slate-800 text-slate-400 border border-slate-700/50">
|
||||
{course.level}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Title */}
|
||||
<h3
|
||||
onClick={() => onOpenDetails(course)}
|
||||
className="text-base font-bold text-white group-hover:text-cyan-300 transition-colors line-clamp-2 cursor-pointer leading-snug"
|
||||
>
|
||||
{course.title}
|
||||
</h3>
|
||||
|
||||
{/* Subtitle */}
|
||||
<p className="text-xs text-slate-400 line-clamp-2 leading-relaxed">
|
||||
{course.subtitle}
|
||||
</p>
|
||||
|
||||
{/* Instructor info */}
|
||||
<div className="flex items-center gap-2 pt-1">
|
||||
<img
|
||||
src={course.instructor.avatar}
|
||||
alt={course.instructor.name}
|
||||
className="w-6 h-6 rounded-full object-cover ring-1 ring-cyan-500/30"
|
||||
/>
|
||||
<span className="text-xs text-slate-300 font-medium truncate">
|
||||
{course.instructor.name}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Rating and Reviews */}
|
||||
<div className="flex items-center gap-2 pt-1 text-xs">
|
||||
<div className="flex items-center gap-1 font-bold text-amber-400">
|
||||
<span>{course.rating.toFixed(1)}</span>
|
||||
<div className="flex text-amber-400 text-xs">
|
||||
{[...Array(5)].map((_, i) => (
|
||||
<FiStar
|
||||
key={i}
|
||||
className={`inline ${
|
||||
i < Math.floor(course.rating)
|
||||
? 'fill-amber-400 text-amber-400'
|
||||
: 'text-slate-600'
|
||||
}`}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<span className="text-slate-500 font-mono text-[11px]">
|
||||
({course.reviewsCount.toLocaleString()})
|
||||
</span>
|
||||
<span className="text-slate-600">•</span>
|
||||
<span className="text-slate-400 text-[11px]">
|
||||
{course.studentsCount.toLocaleString()} alumnos
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Pricing & Cart Action Bottom */}
|
||||
<div className="p-5 pt-3 border-t border-slate-800/80 bg-slate-950/40 flex items-center justify-between gap-3">
|
||||
<div>
|
||||
<div className="flex items-baseline gap-1.5">
|
||||
<span className="text-xl font-black text-white font-mono">
|
||||
${course.price.toFixed(2)}
|
||||
</span>
|
||||
{course.originalPrice > course.price && (
|
||||
<span className="text-xs text-slate-500 line-through font-mono">
|
||||
${course.originalPrice.toFixed(2)}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<span className="text-[10px] text-emerald-400 flex items-center gap-0.5">
|
||||
<FiZap /> Acceso total + Certificado
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<button
|
||||
onClick={() => onAddToCart(course)}
|
||||
disabled={isInCart}
|
||||
className={`px-3.5 py-2.5 rounded-xl text-xs font-bold flex items-center gap-1.5 transition-all shadow-md ${
|
||||
isInCart
|
||||
? 'bg-emerald-500/20 text-emerald-300 border border-emerald-500/30 cursor-default'
|
||||
: 'bg-gradient-to-r from-blue-600 to-cyan-500 hover:from-blue-500 hover:to-cyan-400 text-white shadow-cyan-500/20 hover:scale-105 active:scale-95'
|
||||
}`}
|
||||
>
|
||||
{isInCart ? (
|
||||
<>
|
||||
<FiCheck className="text-sm" />
|
||||
<span>En Carrito</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<FiShoppingCart className="text-sm" />
|
||||
<span>Añadir</span>
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,259 @@
|
||||
import { useState, useMemo } from 'react';
|
||||
import {
|
||||
FiFilter,
|
||||
FiFlag,
|
||||
FiStar,
|
||||
FiPercent,
|
||||
FiSpeaker,
|
||||
FiLayers,
|
||||
FiRefreshCw
|
||||
} from 'react-icons/fi';
|
||||
import type { Course, ActiveTabFilter } from '../../types/course';
|
||||
import { CourseCard } from './CourseCard';
|
||||
import { CategoryPills } from './CategoryPills';
|
||||
|
||||
interface CourseGridProps {
|
||||
courses: Course[];
|
||||
cartCourseIds: string[];
|
||||
searchQuery: string;
|
||||
onAddToCart: (course: Course) => void;
|
||||
onOpenDetails: (course: Course) => void;
|
||||
onClearSearch: () => void;
|
||||
}
|
||||
|
||||
export const CourseGrid: React.FC<CourseGridProps> = ({
|
||||
courses,
|
||||
cartCourseIds,
|
||||
searchQuery,
|
||||
onAddToCart,
|
||||
onOpenDetails,
|
||||
onClearSearch,
|
||||
}) => {
|
||||
const [activeTab, setActiveTab] = useState<ActiveTabFilter>('all');
|
||||
const [selectedCategory, setSelectedCategory] = useState<string>('Todos');
|
||||
const [selectedLevel, setSelectedLevel] = useState<string>('Todos');
|
||||
const [sortBy, setSortBy] = useState<'rating' | 'price-asc' | 'price-desc' | 'students'>('students');
|
||||
|
||||
// Filtered & Sorted courses computation
|
||||
const filteredCourses = useMemo(() => {
|
||||
return courses
|
||||
.filter((course) => {
|
||||
// Search query filter
|
||||
if (searchQuery.trim()) {
|
||||
const q = searchQuery.toLowerCase();
|
||||
const matchTitle = course.title.toLowerCase().includes(q);
|
||||
const matchSub = course.subtitle.toLowerCase().includes(q);
|
||||
const matchCat = course.category.toLowerCase().includes(q);
|
||||
const matchInst = course.instructor.name.toLowerCase().includes(q);
|
||||
if (!matchTitle && !matchSub && !matchCat && !matchInst) return false;
|
||||
}
|
||||
|
||||
// Category filter
|
||||
if (selectedCategory !== 'Todos') {
|
||||
if (course.category !== selectedCategory) return false;
|
||||
}
|
||||
|
||||
// Level filter
|
||||
if (selectedLevel !== 'Todos') {
|
||||
if (course.level !== selectedLevel && course.level !== 'Todos los niveles') return false;
|
||||
}
|
||||
|
||||
// Tab filter
|
||||
if (activeTab === 'popular' && course.studentsCount < 6000) return false;
|
||||
if (activeTab === 'top-rated' && course.rating < 4.9) return false;
|
||||
if (activeTab === 'offers' && !course.badge?.includes('Oferta') && course.originalPrice <= course.price) return false;
|
||||
if (activeTab === 'new' && course.badge !== 'Nuevo') return false;
|
||||
|
||||
return true;
|
||||
})
|
||||
.sort((a, b) => {
|
||||
if (sortBy === 'rating') return b.rating - a.rating;
|
||||
if (sortBy === 'price-asc') return a.price - b.price;
|
||||
if (sortBy === 'price-desc') return b.price - a.price;
|
||||
return b.studentsCount - a.studentsCount; // default: popular
|
||||
});
|
||||
}, [courses, searchQuery, selectedCategory, selectedLevel, activeTab, sortBy]);
|
||||
|
||||
const resetAllFilters = () => {
|
||||
setActiveTab('all');
|
||||
setSelectedCategory('Todos');
|
||||
setSelectedLevel('Todos');
|
||||
setSortBy('students');
|
||||
onClearSearch();
|
||||
};
|
||||
|
||||
return (
|
||||
<section id="catalog-section" className="py-12 bg-slate-950/60">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
|
||||
{/* Section Header */}
|
||||
<div className="flex flex-col md:flex-row md:items-end justify-between mb-8 gap-4">
|
||||
<div>
|
||||
<div className="flex items-center gap-2 text-cyan-400 text-xs font-bold uppercase tracking-wider mb-2">
|
||||
<FiLayers />
|
||||
<span>Catálogo Completo</span>
|
||||
</div>
|
||||
<h2 className="text-2xl sm:text-3xl font-extrabold text-white tracking-tight">
|
||||
Explora Cursos Destacados
|
||||
</h2>
|
||||
<p className="text-sm text-slate-400 mt-1">
|
||||
Filtra por tu área de interés, nivel o aprovecha promociones de temporada.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Quick Tabs */}
|
||||
<div className="flex flex-wrap items-center gap-1.5 p-1 bg-slate-900 border border-slate-800 rounded-xl">
|
||||
<button
|
||||
onClick={() => setActiveTab('all')}
|
||||
className={`px-3 py-1.5 rounded-lg text-xs font-semibold transition-all ${
|
||||
activeTab === 'all'
|
||||
? 'bg-cyan-500/20 text-cyan-300 border border-cyan-500/40'
|
||||
: 'text-slate-400 hover:text-white'
|
||||
}`}
|
||||
>
|
||||
Todos ({courses.length})
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={() => setActiveTab('popular')}
|
||||
className={`flex items-center gap-1 px-3 py-1.5 rounded-lg text-xs font-semibold transition-all ${
|
||||
activeTab === 'popular'
|
||||
? 'bg-amber-500/20 text-amber-300 border border-amber-500/40'
|
||||
: 'text-slate-400 hover:text-white'
|
||||
}`}
|
||||
>
|
||||
<FiFlag className="text-amber-400" /> Populares
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={() => setActiveTab('top-rated')}
|
||||
className={`flex items-center gap-1 px-3 py-1.5 rounded-lg text-xs font-semibold transition-all ${
|
||||
activeTab === 'top-rated'
|
||||
? 'bg-blue-500/20 text-blue-300 border border-blue-500/40'
|
||||
: 'text-slate-400 hover:text-white'
|
||||
}`}
|
||||
>
|
||||
<FiStar className="text-blue-400" /> Top Calificados
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={() => setActiveTab('offers')}
|
||||
className={`flex items-center gap-1 px-3 py-1.5 rounded-lg text-xs font-semibold transition-all ${
|
||||
activeTab === 'offers'
|
||||
? 'bg-rose-500/20 text-rose-300 border border-rose-500/40'
|
||||
: 'text-slate-400 hover:text-white'
|
||||
}`}
|
||||
>
|
||||
<FiPercent className="text-rose-400" /> Ofertas
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={() => setActiveTab('new')}
|
||||
className={`flex items-center gap-1 px-3 py-1.5 rounded-lg text-xs font-semibold transition-all ${
|
||||
activeTab === 'new'
|
||||
? 'bg-emerald-500/20 text-emerald-300 border border-emerald-500/40'
|
||||
: 'text-slate-400 hover:text-white'
|
||||
}`}
|
||||
>
|
||||
<FiSpeaker className="text-emerald-400" /> Nuevos
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Categories Bar */}
|
||||
<div className="mb-6">
|
||||
<CategoryPills
|
||||
selectedCategory={selectedCategory}
|
||||
onSelectCategory={setSelectedCategory}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Secondary Filter & Sorting Controls */}
|
||||
<div className="flex flex-wrap items-center justify-between gap-4 p-4 rounded-xl bg-slate-900/50 border border-slate-800/80 mb-8">
|
||||
|
||||
<div className="flex flex-wrap items-center gap-3">
|
||||
<span className="flex items-center gap-1 text-xs font-semibold text-slate-400">
|
||||
<FiFilter className="text-cyan-400" /> Filtros:
|
||||
</span>
|
||||
|
||||
{/* Level Filter */}
|
||||
<select
|
||||
value={selectedLevel}
|
||||
onChange={(e) => setSelectedLevel(e.target.value)}
|
||||
className="bg-slate-800 text-slate-200 border border-slate-700 rounded-lg px-3 py-1.5 text-xs font-medium focus:outline-none focus:border-cyan-500"
|
||||
>
|
||||
<option value="Todos">Nivel: Todos</option>
|
||||
<option value="Principiante">Principiante</option>
|
||||
<option value="Intermedio">Intermedio</option>
|
||||
<option value="Avanzado">Avanzado</option>
|
||||
</select>
|
||||
|
||||
{/* Clear Filters if active */}
|
||||
{(selectedCategory !== 'Todos' || selectedLevel !== 'Todos' || activeTab !== 'all' || searchQuery.trim()) && (
|
||||
<button
|
||||
onClick={resetAllFilters}
|
||||
className="flex items-center gap-1 text-xs text-rose-400 hover:text-rose-300 font-medium px-2 py-1 rounded bg-rose-500/10 border border-rose-500/20 transition"
|
||||
>
|
||||
<FiRefreshCw className="text-[10px]" /> Limpiar filtros
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Sort By Dropdown & Results Counter */}
|
||||
<div className="flex items-center gap-3">
|
||||
<span className="text-xs text-slate-400">
|
||||
Mostrando <strong className="text-white font-mono">{filteredCourses.length}</strong> cursos
|
||||
</span>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-xs text-slate-400">Ordenar por:</span>
|
||||
<select
|
||||
value={sortBy}
|
||||
onChange={(e) => setSortBy(e.target.value as any)}
|
||||
className="bg-slate-800 text-slate-200 border border-slate-700 rounded-lg px-3 py-1.5 text-xs font-medium focus:outline-none focus:border-cyan-500"
|
||||
>
|
||||
<option value="students">Más populares (Alumnos)</option>
|
||||
<option value="rating">Mejor valorados</option>
|
||||
<option value="price-asc">Precio: Menor a Mayor</option>
|
||||
<option value="price-desc">Precio: Mayor a Menor</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
{/* Courses Grid */}
|
||||
{filteredCourses.length > 0 ? (
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6 lg:gap-8">
|
||||
{filteredCourses.map((course) => (
|
||||
<CourseCard
|
||||
key={course.id}
|
||||
course={course}
|
||||
isInCart={cartCourseIds.includes(course.id)}
|
||||
onAddToCart={onAddToCart}
|
||||
onOpenDetails={onOpenDetails}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-center py-20 bg-slate-900/40 rounded-2xl border border-slate-800 p-8 space-y-4">
|
||||
<div className="w-16 h-16 rounded-full bg-slate-800 text-slate-400 flex items-center justify-center mx-auto text-2xl">
|
||||
<FiFilter />
|
||||
</div>
|
||||
<h3 className="text-lg font-bold text-white">No se encontraron cursos con estos filtros</h3>
|
||||
<p className="text-xs text-slate-400 max-w-md mx-auto">
|
||||
Intenta cambiar los términos de búsqueda, la categoría o restablecer los filtros para ver todos los cursos disponibles.
|
||||
</p>
|
||||
<button
|
||||
onClick={resetAllFilters}
|
||||
className="px-4 py-2 bg-gradient-to-r from-blue-600 to-cyan-500 text-white text-xs font-bold rounded-xl shadow-lg shadow-cyan-500/20"
|
||||
>
|
||||
Restablecer todos los filtros
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,388 @@
|
||||
import { useState } from 'react';
|
||||
import {
|
||||
FiX,
|
||||
FiCheckCircle,
|
||||
FiPlay,
|
||||
FiClock,
|
||||
FiBook,
|
||||
FiStar,
|
||||
FiGlobe,
|
||||
FiAward,
|
||||
FiShield,
|
||||
FiChevronDown,
|
||||
FiChevronUp,
|
||||
FiShoppingCart
|
||||
} from 'react-icons/fi';
|
||||
import type { Course } from '../../types/course';
|
||||
|
||||
interface CourseModalProps {
|
||||
course: Course | null;
|
||||
isOpen: boolean;
|
||||
isInCart: boolean;
|
||||
onClose: () => void;
|
||||
onAddToCart: (course: Course) => void;
|
||||
onBuyNow: (course: Course) => void;
|
||||
}
|
||||
|
||||
export const CourseModal: React.FC<CourseModalProps> = ({
|
||||
course,
|
||||
isOpen,
|
||||
isInCart,
|
||||
onClose,
|
||||
onAddToCart,
|
||||
onBuyNow,
|
||||
}) => {
|
||||
const [expandedModules, setExpandedModules] = useState<Record<string, boolean>>({
|
||||
m1: true,
|
||||
m2: true,
|
||||
m3: true
|
||||
});
|
||||
const [activeTab, setActiveTab] = useState<'curriculum' | 'instructor' | 'reviews'>('curriculum');
|
||||
const [isPlayingVideo, setIsPlayingVideo] = useState(false);
|
||||
|
||||
if (!isOpen || !course) return null;
|
||||
|
||||
const toggleModule = (modId: string) => {
|
||||
setExpandedModules((prev) => ({ ...prev, [modId]: !prev[modId] }));
|
||||
};
|
||||
|
||||
const discountPercent = Math.round(
|
||||
((course.originalPrice - course.price) / course.originalPrice) * 100
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 z-50 overflow-y-auto bg-slate-950/80 backdrop-blur-md flex items-center justify-center p-2 sm:p-4 animate-in fade-in duration-200">
|
||||
|
||||
{/* Modal Container */}
|
||||
<div
|
||||
className="relative w-full max-w-5xl bg-slate-900 border border-slate-800 rounded-2xl shadow-2xl overflow-hidden my-8 max-h-[92vh] flex flex-col"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
|
||||
{/* Top Header Bar */}
|
||||
<div className="flex items-center justify-between px-6 py-4 border-b border-slate-800 bg-slate-950/60 sticky top-0 z-20">
|
||||
<div className="flex items-center gap-3">
|
||||
<span className="text-xs font-semibold px-2.5 py-1 rounded bg-cyan-500/10 text-cyan-400 border border-cyan-500/20">
|
||||
{course.category}
|
||||
</span>
|
||||
<span className="text-xs text-slate-400">• Nivel: {course.level}</span>
|
||||
</div>
|
||||
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="p-2 rounded-xl text-slate-400 hover:text-white hover:bg-slate-800 transition"
|
||||
aria-label="Cerrar modal"
|
||||
>
|
||||
<FiX className="text-xl" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Modal Body Scrollable */}
|
||||
<div className="overflow-y-auto flex-1 p-6 space-y-8">
|
||||
|
||||
{/* Top Hero & Video Preview Section */}
|
||||
<div className="grid grid-cols-1 lg:grid-cols-12 gap-8">
|
||||
|
||||
{/* Main Info */}
|
||||
<div className="lg:col-span-7 space-y-4">
|
||||
<h2 className="text-2xl sm:text-3xl font-extrabold text-white leading-tight">
|
||||
{course.title}
|
||||
</h2>
|
||||
|
||||
<p className="text-slate-300 text-sm leading-relaxed">
|
||||
{course.subtitle}
|
||||
</p>
|
||||
|
||||
{/* Meta stats */}
|
||||
<div className="flex flex-wrap items-center gap-4 text-xs pt-2 text-slate-400 border-t border-slate-800/80">
|
||||
<div className="flex items-center gap-1.5 text-amber-400 font-bold">
|
||||
<FiStar className="fill-amber-400" />
|
||||
<span>{course.rating.toFixed(1)}</span>
|
||||
<span className="text-slate-400 font-normal">({course.reviewsCount} reseñas)</span>
|
||||
</div>
|
||||
<div>👥 {course.studentsCount.toLocaleString()} estudiantes</div>
|
||||
<div className="flex items-center gap-1"><FiGlobe /> {course.language}</div>
|
||||
<div className="flex items-center gap-1"><FiClock /> Actualizado: {course.updatedAt}</div>
|
||||
</div>
|
||||
|
||||
{/* What You Will Learn Card */}
|
||||
<div className="p-5 rounded-xl bg-slate-950/60 border border-slate-800 space-y-3">
|
||||
<h4 className="text-sm font-bold text-white flex items-center gap-2">
|
||||
<FiAward className="text-cyan-400" /> Lo que aprenderás en este curso
|
||||
</h4>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-2.5 pt-2">
|
||||
{course.whatYouWillLearn.map((item, idx) => (
|
||||
<div key={idx} className="flex items-start gap-2 text-xs text-slate-300">
|
||||
<FiCheckCircle className="text-emerald-400 text-sm mt-0.5 shrink-0" />
|
||||
<span>{item}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Video Preview & Buy Box */}
|
||||
<div className="lg:col-span-5 space-y-4">
|
||||
<div className="rounded-xl overflow-hidden bg-slate-950 border border-slate-800 shadow-xl relative aspect-video group">
|
||||
{!isPlayingVideo ? (
|
||||
<>
|
||||
<img
|
||||
src={course.thumbnail}
|
||||
alt={course.title}
|
||||
className="w-full h-full object-cover"
|
||||
/>
|
||||
<div className="absolute inset-0 bg-slate-950/40 flex flex-col items-center justify-center gap-2">
|
||||
<button
|
||||
onClick={() => setIsPlayingVideo(true)}
|
||||
className="w-14 h-14 rounded-full bg-cyan-500 hover:bg-cyan-400 text-slate-950 flex items-center justify-center text-xl shadow-lg shadow-cyan-500/50 hover:scale-110 transition-transform"
|
||||
>
|
||||
<FiPlay className="ml-1" />
|
||||
</button>
|
||||
<span className="text-xs font-bold text-white bg-slate-900/80 px-2.5 py-1 rounded-md border border-slate-700">
|
||||
Vista previa gratuita de lección
|
||||
</span>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<div className="w-full h-full flex flex-col items-center justify-center bg-slate-950 p-4 text-center space-y-3">
|
||||
<div className="w-12 h-12 rounded-full bg-cyan-500/20 text-cyan-400 flex items-center justify-center text-xl animate-pulse">
|
||||
<FiPlay />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-xs font-bold text-white">Reproduciendo Clase Muestra (HD)</p>
|
||||
<p className="text-[11px] text-slate-400">Lección 1: Novedades y Configuración Inicial</p>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => setIsPlayingVideo(false)}
|
||||
className="text-xs text-rose-400 hover:underline"
|
||||
>
|
||||
Pausar y volver a portada
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Price & Checkout Card */}
|
||||
<div className="p-5 rounded-xl bg-slate-950/80 border border-slate-800 space-y-4 shadow-xl">
|
||||
<div className="flex items-baseline justify-between">
|
||||
<div className="flex items-baseline gap-2">
|
||||
<span className="text-3xl font-black text-white font-mono">
|
||||
${course.price.toFixed(2)}
|
||||
</span>
|
||||
{course.originalPrice > course.price && (
|
||||
<span className="text-sm text-slate-500 line-through font-mono">
|
||||
${course.originalPrice.toFixed(2)}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
{discountPercent > 0 && (
|
||||
<span className="px-2 py-1 rounded bg-rose-500/20 border border-rose-500/30 text-rose-400 text-xs font-bold">
|
||||
{discountPercent}% Descuento
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<button
|
||||
onClick={() => {
|
||||
onBuyNow(course);
|
||||
onClose();
|
||||
}}
|
||||
className="w-full py-3 bg-gradient-to-r from-emerald-600 to-teal-500 hover:from-emerald-500 hover:to-teal-400 text-white font-bold rounded-xl text-sm shadow-lg shadow-emerald-500/20 transition-all hover:scale-[1.01]"
|
||||
>
|
||||
Comprar Ahora
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={() => onAddToCart(course)}
|
||||
disabled={isInCart}
|
||||
className={`w-full py-2.5 rounded-xl text-sm font-bold flex items-center justify-center gap-2 transition-all ${
|
||||
isInCart
|
||||
? 'bg-slate-800 text-slate-400 border border-slate-700 cursor-default'
|
||||
: 'bg-slate-800 hover:bg-slate-700 text-white border border-slate-700'
|
||||
}`}
|
||||
>
|
||||
<FiShoppingCart />
|
||||
<span>{isInCart ? 'Ya está en el carrito' : 'Añadir al Carrito'}</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1.5 text-[11px] text-slate-400 pt-2 border-t border-slate-800">
|
||||
<div className="flex items-center gap-2">
|
||||
<FiClock className="text-cyan-400" /> {course.duration} de video bajo demanda
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<FiBook className="text-cyan-400" /> {course.lessonsCount} lecciones descargables
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<FiShield className="text-emerald-400" /> Garantía de 30 días o te devolvemos el dinero
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<FiAward className="text-cyan-400" /> Certificado de acreditación oficial
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
{/* Nav Tabs inside modal */}
|
||||
<div className="border-b border-slate-800 flex gap-6 text-sm font-semibold">
|
||||
<button
|
||||
onClick={() => setActiveTab('curriculum')}
|
||||
className={`pb-3 transition relative ${
|
||||
activeTab === 'curriculum'
|
||||
? 'text-cyan-400 border-b-2 border-cyan-400'
|
||||
: 'text-slate-400 hover:text-slate-200'
|
||||
}`}
|
||||
>
|
||||
Contenido del Curso ({course.modules.length} Módulos)
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setActiveTab('instructor')}
|
||||
className={`pb-3 transition relative ${
|
||||
activeTab === 'instructor'
|
||||
? 'text-cyan-400 border-b-2 border-cyan-400'
|
||||
: 'text-slate-400 hover:text-slate-200'
|
||||
}`}
|
||||
>
|
||||
Instructor
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setActiveTab('reviews')}
|
||||
className={`pb-3 transition relative ${
|
||||
activeTab === 'reviews'
|
||||
? 'text-cyan-400 border-b-2 border-cyan-400'
|
||||
: 'text-slate-400 hover:text-slate-200'
|
||||
}`}
|
||||
>
|
||||
Valoraciones ({course.reviews.length})
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Tab Content 1: Curriculum */}
|
||||
{activeTab === 'curriculum' && (
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center justify-between text-xs text-slate-400">
|
||||
<span>{course.modules.length} módulos • {course.lessonsCount} lecciones • {course.duration} duración total</span>
|
||||
</div>
|
||||
|
||||
<div className="space-y-3">
|
||||
{course.modules.map((mod) => {
|
||||
const isExpanded = !!expandedModules[mod.id];
|
||||
return (
|
||||
<div key={mod.id} className="rounded-xl border border-slate-800 overflow-hidden bg-slate-950/40">
|
||||
<button
|
||||
onClick={() => toggleModule(mod.id)}
|
||||
className="w-full flex items-center justify-between p-4 bg-slate-900/60 hover:bg-slate-900 transition text-left"
|
||||
>
|
||||
<div className="font-bold text-sm text-slate-200 flex items-center gap-2">
|
||||
{isExpanded ? <FiChevronUp className="text-cyan-400" /> : <FiChevronDown className="text-slate-500" />}
|
||||
<span>{mod.title}</span>
|
||||
</div>
|
||||
<span className="text-xs text-slate-400 font-mono">
|
||||
{mod.lessons.length} lecciones
|
||||
</span>
|
||||
</button>
|
||||
|
||||
{isExpanded && (
|
||||
<div className="divide-y divide-slate-850 p-2 space-y-1">
|
||||
{mod.lessons.map((lesson) => (
|
||||
<div
|
||||
key={lesson.id}
|
||||
className="flex items-center justify-between p-2.5 rounded-lg hover:bg-slate-900/80 text-xs transition"
|
||||
>
|
||||
<div className="flex items-center gap-2.5 text-slate-300">
|
||||
<div className="w-5 h-5 rounded-full bg-slate-800 text-slate-400 flex items-center justify-center text-[10px]">
|
||||
<FiPlay />
|
||||
</div>
|
||||
<span>{lesson.title}</span>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-3">
|
||||
{lesson.isFreePreview && (
|
||||
<span className="px-2 py-0.5 rounded bg-cyan-500/20 text-cyan-300 border border-cyan-500/30 text-[10px] font-bold">
|
||||
Vista Previa
|
||||
</span>
|
||||
)}
|
||||
<span className="text-slate-400 font-mono">{lesson.duration}</span>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Tab Content 2: Instructor */}
|
||||
{activeTab === 'instructor' && (
|
||||
<div className="p-6 rounded-xl bg-slate-950/60 border border-slate-800 space-y-4">
|
||||
<div className="flex flex-col sm:flex-row items-start sm:items-center gap-4">
|
||||
<img
|
||||
src={course.instructor.avatar}
|
||||
alt={course.instructor.name}
|
||||
className="w-16 h-16 rounded-full object-cover ring-2 ring-cyan-500/50"
|
||||
/>
|
||||
<div>
|
||||
<h3 className="text-lg font-bold text-white">{course.instructor.name}</h3>
|
||||
<p className="text-xs text-cyan-400 font-medium">{course.instructor.role}</p>
|
||||
|
||||
<div className="flex items-center gap-4 text-xs text-slate-400 mt-2">
|
||||
<span className="flex items-center gap-1 text-amber-400 font-bold">
|
||||
<FiStar className="fill-amber-400" /> {course.instructor.rating} Calificación
|
||||
</span>
|
||||
<span>👨🎓 {course.instructor.totalStudents.toLocaleString()} Estudiantes</span>
|
||||
<span>📚 {course.instructor.totalCourses} Cursos creados</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p className="text-xs text-slate-300 leading-relaxed border-t border-slate-800 pt-4">
|
||||
{course.instructor.bio}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Tab Content 3: Reviews */}
|
||||
{activeTab === 'reviews' && (
|
||||
<div className="space-y-4">
|
||||
{course.reviews.map((rev) => (
|
||||
<div key={rev.id} className="p-4 rounded-xl bg-slate-950/60 border border-slate-800 space-y-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2.5">
|
||||
<img
|
||||
src={rev.userAvatar}
|
||||
alt={rev.userName}
|
||||
className="w-8 h-8 rounded-full object-cover"
|
||||
/>
|
||||
<div>
|
||||
<h4 className="text-xs font-bold text-white">{rev.userName}</h4>
|
||||
<span className="text-[10px] text-slate-500">{rev.date}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex text-amber-400 text-xs">
|
||||
{[...Array(rev.rating)].map((_, i) => (
|
||||
<FiStar key={i} className="fill-amber-400" />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p className="text-xs text-slate-300 leading-relaxed pl-10">
|
||||
"{rev.comment}"
|
||||
</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,62 @@
|
||||
import React from 'react';
|
||||
import { FiAward, FiClock, FiUsers, FiShield } from 'react-icons/fi';
|
||||
|
||||
export const FeaturesBanner: React.FC = () => {
|
||||
const features = [
|
||||
{
|
||||
icon: <FiAward className="text-2xl text-cyan-400" />,
|
||||
title: 'Certificados Verificables',
|
||||
description: 'Comparte tus logros con enlaces verificables en LinkedIn y tu CV profesional.'
|
||||
},
|
||||
{
|
||||
icon: <FiClock className="text-2xl text-blue-400" />,
|
||||
title: 'Acceso Ilimitado de por Vida',
|
||||
description: 'Aprende a tu ritmo. Vuelve a consultar el material y las actualizaciones cuando quieras.'
|
||||
},
|
||||
{
|
||||
icon: <FiUsers className="text-2xl text-indigo-400" />,
|
||||
title: 'Comunidad Exclusiva en Discord',
|
||||
description: 'Resuelve dudas directamente con profesores y conecta con otros estudiantes.'
|
||||
},
|
||||
{
|
||||
icon: <FiShield className="text-2xl text-emerald-400" />,
|
||||
title: 'Garantía 100% de 30 Días',
|
||||
description: 'Si el curso no cumple con tus expectativas, te devolvemos el dinero sin preguntas.'
|
||||
}
|
||||
];
|
||||
|
||||
return (
|
||||
<section className="py-16 bg-slate-950/80 border-t border-slate-800/80">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
|
||||
<div className="text-center max-w-3xl mx-auto mb-12 space-y-3">
|
||||
<span className="text-xs font-bold uppercase tracking-wider text-cyan-400 bg-cyan-500/10 px-3 py-1 rounded-full border border-cyan-500/20">
|
||||
¿Por qué elegir RyCourses?
|
||||
</span>
|
||||
<h2 className="text-2xl sm:text-4xl font-extrabold text-white">
|
||||
Diseñado para acelerar tu crecimiento profesional
|
||||
</h2>
|
||||
<p className="text-sm text-slate-400">
|
||||
No somos solo videos; somos una plataforma integral de educación moderna para la era de la Inteligencia Artificial.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-6">
|
||||
{features.map((f, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className="p-6 rounded-2xl bg-slate-900/60 border border-slate-800 hover:border-slate-700 transition-all hover:-translate-y-1 hover:shadow-xl hover:shadow-cyan-500/5 group"
|
||||
>
|
||||
<div className="w-12 h-12 rounded-xl bg-slate-800 flex items-center justify-center mb-4 group-hover:scale-110 transition-transform">
|
||||
{f.icon}
|
||||
</div>
|
||||
<h3 className="text-base font-bold text-white mb-2">{f.title}</h3>
|
||||
<p className="text-xs text-slate-400 leading-relaxed">{f.description}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,198 @@
|
||||
import React from 'react';
|
||||
import { FiArrowRight, FiPlay, FiStar, FiUsers, FiAward, FiCheckCircle } from 'react-icons/fi';
|
||||
import { FaGraduationCap } from 'react-icons/fa';
|
||||
|
||||
interface HeroSectionProps {
|
||||
onExploreClick: () => void;
|
||||
onOpenPreview: (courseId: string) => void;
|
||||
}
|
||||
|
||||
export const HeroSection: React.FC<HeroSectionProps> = ({ onExploreClick, onOpenPreview }) => {
|
||||
return (
|
||||
<div className="relative overflow-hidden bg-slate-950 pt-10 pb-16 lg:pt-16 lg:pb-24 border-b border-slate-800/60">
|
||||
|
||||
{/* Background glow effects */}
|
||||
<div className="absolute -top-40 -left-40 w-96 h-96 bg-blue-600/20 rounded-full blur-3xl pointer-events-none" />
|
||||
<div className="absolute top-1/3 -right-40 w-96 h-96 bg-cyan-500/15 rounded-full blur-3xl pointer-events-none" />
|
||||
<div className="absolute -bottom-20 left-1/3 w-80 h-80 bg-indigo-600/15 rounded-full blur-3xl pointer-events-none" />
|
||||
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 relative z-10">
|
||||
<div className="grid grid-cols-1 lg:grid-cols-12 gap-12 items-center">
|
||||
|
||||
{/* Left Column: Copy & CTAs */}
|
||||
<div className="lg:col-span-7 space-y-6 text-center lg:text-left">
|
||||
|
||||
{/* Promo Pill */}
|
||||
<div className="inline-flex items-center gap-2 px-3.5 py-1.5 rounded-full text-xs font-semibold bg-gradient-to-r from-blue-500/15 to-cyan-500/15 border border-cyan-500/30 text-cyan-300">
|
||||
<span className="flex h-2 w-2 rounded-full bg-cyan-400 animate-ping" />
|
||||
<FaGraduationCap className="text-cyan-400 text-sm" />
|
||||
<span>Nuevos cursos actualizados para 2026</span>
|
||||
</div>
|
||||
|
||||
{/* Main Headline */}
|
||||
<h1 className="text-3xl sm:text-5xl lg:text-6xl font-black text-white tracking-tight leading-[1.15]">
|
||||
Aprende las habilidades{' '}
|
||||
<span className="text-transparent bg-clip-text bg-gradient-to-r from-blue-400 via-cyan-400 to-teal-300">
|
||||
más demandadas
|
||||
</span>{' '}
|
||||
con expertos reales.
|
||||
</h1>
|
||||
|
||||
{/* Subtitle */}
|
||||
<p className="text-base sm:text-lg text-slate-300 max-w-2xl mx-auto lg:mx-0 font-normal leading-relaxed">
|
||||
Cursos prácticos basados en proyectos reales. Desde <span className="text-cyan-300 font-medium">Desarrollo Web y Apps</span> hasta <span className="text-cyan-300 font-medium">Inteligencia Artificial y Ciberseguridad</span>. Aprende a tu propio ritmo y obtén certificaciones verificadas.
|
||||
</p>
|
||||
|
||||
{/* Actions & Buttons */}
|
||||
<div className="flex flex-col sm:flex-row items-center justify-center lg:justify-start gap-4 pt-2">
|
||||
<button
|
||||
onClick={onExploreClick}
|
||||
className="w-full sm:w-auto px-8 py-4 bg-gradient-to-r from-blue-600 via-cyan-600 to-teal-500 hover:from-blue-500 hover:to-cyan-400 text-white font-bold rounded-xl text-base shadow-xl shadow-cyan-500/25 hover:shadow-cyan-500/40 hover:scale-[1.02] active:scale-[0.98] transition-all flex items-center justify-center gap-2"
|
||||
>
|
||||
<span>Explorar Catálogo</span>
|
||||
<FiArrowRight className="text-lg" />
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={() => onOpenPreview('course-1')}
|
||||
className="w-full sm:w-auto px-6 py-4 bg-slate-900/80 hover:bg-slate-800 text-slate-200 hover:text-white font-semibold rounded-xl text-base border border-slate-700 hover:border-slate-600 transition-all flex items-center justify-center gap-2"
|
||||
>
|
||||
<div className="w-6 h-6 rounded-full bg-cyan-500/20 text-cyan-400 flex items-center justify-center text-xs">
|
||||
<FiPlay className="ml-0.5" />
|
||||
</div>
|
||||
<span>Ver Clase Muestra Gratis</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Trust checkmarks */}
|
||||
<div className="pt-3 flex flex-wrap items-center justify-center lg:justify-start gap-x-6 gap-y-2 text-xs text-slate-400">
|
||||
<span className="flex items-center gap-1.5">
|
||||
<FiCheckCircle className="text-emerald-400" /> Acceso de por vida
|
||||
</span>
|
||||
<span className="flex items-center gap-1.5">
|
||||
<FiCheckCircle className="text-emerald-400" /> Garantía de reembolso 30 días
|
||||
</span>
|
||||
<span className="flex items-center gap-1.5">
|
||||
<FiCheckCircle className="text-emerald-400" /> Certificado de finalización
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Right Column: Hero Visual Card */}
|
||||
<div className="lg:col-span-5 relative">
|
||||
<div className="relative mx-auto max-w-md lg:max-w-none">
|
||||
|
||||
{/* Main Course Highlight Card */}
|
||||
<div className="rounded-2xl bg-gradient-to-b from-slate-800/80 to-slate-900/90 border border-slate-700/80 p-5 shadow-2xl backdrop-blur-md relative overflow-hidden group">
|
||||
|
||||
{/* Thumbnail image */}
|
||||
<div className="relative rounded-xl overflow-hidden aspect-video mb-4 bg-slate-800">
|
||||
<img
|
||||
src="https://images.unsplash.com/photo-1633356122544-f134324a6cee?w=800&auto=format&fit=crop&q=80"
|
||||
alt="React 19 Preview"
|
||||
className="w-full h-full object-cover group-hover:scale-105 transition-transform duration-500"
|
||||
/>
|
||||
<div className="absolute inset-0 bg-gradient-to-t from-slate-950/90 via-slate-950/20 to-transparent" />
|
||||
|
||||
{/* Play badge */}
|
||||
<button
|
||||
onClick={() => onOpenPreview('course-1')}
|
||||
className="absolute inset-0 m-auto w-14 h-14 rounded-full bg-gradient-to-r from-blue-600 to-cyan-500 text-white flex items-center justify-center shadow-lg shadow-cyan-500/50 hover:scale-110 active:scale-95 transition-transform"
|
||||
aria-label="Reproducir video"
|
||||
>
|
||||
<FiPlay className="text-xl ml-1" />
|
||||
</button>
|
||||
|
||||
<span className="absolute top-3 left-3 bg-amber-500/90 text-slate-950 text-[11px] font-black px-2.5 py-1 rounded-md uppercase tracking-wider shadow">
|
||||
🔥 Curso Más Vendido
|
||||
</span>
|
||||
|
||||
<span className="absolute bottom-3 right-3 bg-slate-950/80 backdrop-blur-md text-white text-xs px-2 py-0.5 rounded font-mono">
|
||||
38h 45m
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Course Meta in card */}
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center justify-between text-xs text-slate-400">
|
||||
<span className="font-semibold text-cyan-400">Desarrollo Web & Next.js</span>
|
||||
<div className="flex items-center gap-1 text-amber-400">
|
||||
<FiStar className="fill-amber-400" />
|
||||
<span className="font-bold text-white">4.9</span>
|
||||
<span>(1.4k reseñas)</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h3 className="text-lg font-bold text-white leading-snug">
|
||||
Master en React 19, Next.js 15 y TypeScript Profesional
|
||||
</h3>
|
||||
|
||||
<div className="flex items-center justify-between pt-2 border-t border-slate-800">
|
||||
<div className="flex items-center gap-2">
|
||||
<img
|
||||
src="https://images.unsplash.com/photo-1534528741775-53994a69daeb?w=100&auto=format&fit=crop&q=80"
|
||||
alt="Instructor"
|
||||
className="w-7 h-7 rounded-full object-cover ring-1 ring-cyan-400/50"
|
||||
/>
|
||||
<span className="text-xs text-slate-300 font-medium">Ing. Carlos Mendoza</span>
|
||||
</div>
|
||||
|
||||
<div className="flex items-baseline gap-2">
|
||||
<span className="text-xs text-slate-500 line-through font-mono">$89.99</span>
|
||||
<span className="text-xl font-black text-emerald-400 font-mono">$24.99</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Floating Stat Badge: Students Active */}
|
||||
<div className="absolute -bottom-6 -left-6 bg-slate-900/90 border border-slate-700/80 rounded-xl p-3.5 shadow-2xl backdrop-blur-md flex items-center gap-3 hidden sm:flex animate-bounce duration-1000">
|
||||
<div className="w-10 h-10 rounded-lg bg-blue-500/20 text-blue-400 flex items-center justify-center text-lg">
|
||||
<FiUsers />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-xs font-bold text-white">50,000+ Estudiantes</p>
|
||||
<p className="text-[11px] text-slate-400">Aprendiendo activamente</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Floating Stat Badge: Top Rated */}
|
||||
<div className="absolute -top-6 -right-6 bg-slate-900/90 border border-slate-700/80 rounded-xl p-3.5 shadow-2xl backdrop-blur-md flex items-center gap-3 hidden sm:flex">
|
||||
<div className="w-10 h-10 rounded-lg bg-amber-500/20 text-amber-400 flex items-center justify-center text-lg">
|
||||
<FiAward />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-xs font-bold text-white">Calidad Top 1%</p>
|
||||
<p className="text-[11px] text-slate-400">Instructores validados</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
{/* Bottom stats banner */}
|
||||
<div className="mt-16 pt-10 border-t border-slate-800/80 grid grid-cols-2 md:grid-cols-4 gap-6 text-center">
|
||||
<div className="p-4 rounded-xl bg-slate-900/40 border border-slate-800/60">
|
||||
<div className="text-2xl sm:text-3xl font-black text-white font-mono">1,200+</div>
|
||||
<div className="text-xs text-slate-400 mt-1">Cursos en Alta Definición</div>
|
||||
</div>
|
||||
<div className="p-4 rounded-xl bg-slate-900/40 border border-slate-800/60">
|
||||
<div className="text-2xl sm:text-3xl font-black text-cyan-400 font-mono">50,000+</div>
|
||||
<div className="text-xs text-slate-400 mt-1">Alumnos Graduados</div>
|
||||
</div>
|
||||
<div className="p-4 rounded-xl bg-slate-900/40 border border-slate-800/60">
|
||||
<div className="text-2xl sm:text-3xl font-black text-amber-400 font-mono">4.9 / 5.0</div>
|
||||
<div className="text-xs text-slate-400 mt-1">Puntuación Media</div>
|
||||
</div>
|
||||
<div className="p-4 rounded-xl bg-slate-900/40 border border-slate-800/60">
|
||||
<div className="text-2xl sm:text-3xl font-black text-emerald-400 font-mono">98%</div>
|
||||
<div className="text-xs text-slate-400 mt-1">Tasa de Contratación</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
Reference in new issue
Block a user