initial proyect

This commit is contained in:
Miguel Ortega committed 2026-08-21 22:53:15 -05:00
commit 509272a643
31 files changed
+7170

No files matched your search

+241
View File
@@ -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>
);
};
+79
View File
@@ -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>
);
};
+209
View File
@@ -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>
);
};
+259
View File
@@ -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>
);
};
+388
View File
@@ -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>
);
};
+62
View File
@@ -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>
);
};
+198
View File
@@ -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>
);
};
+171
View File
@@ -0,0 +1,171 @@
import React, { useState } from 'react';
import {
FiBookOpen,
FiMail,
FiCheckCircle,
FiGlobe,
FiShield,
FiAward,
FiHeart
} from 'react-icons/fi';
import { FaTwitter, FaYoutube, FaGithub, FaLinkedin, FaDiscord } from 'react-icons/fa';
export const Footer: React.FC = () => {
const [email, setEmail] = useState('');
const [subscribed, setSubscribed] = useState(false);
const handleSubscribe = (e: React.FormEvent) => {
e.preventDefault();
if (email.trim()) {
setSubscribed(true);
setTimeout(() => setSubscribed(false), 4000);
setEmail('');
}
};
return (
<footer className="bg-slate-950 border-t border-slate-800 text-slate-400 text-sm">
{/* Top Banner / Newsletter */}
<div className="border-b border-slate-800/80 bg-gradient-to-r from-blue-950/40 via-indigo-950/40 to-slate-950/80">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-12">
<div className="flex flex-col lg:flex-row items-center justify-between gap-8">
<div className="max-w-xl text-center lg:text-left">
<span className="inline-flex items-center gap-2 px-3 py-1 rounded-full text-xs font-semibold bg-cyan-500/10 text-cyan-400 border border-cyan-500/20 mb-3">
<FiAward /> Actualizaciones semanales de cursos
</span>
<h3 className="text-2xl font-bold text-white">
Suscríbete para recibir descuentos exclusivos y nuevos lanzamientos
</h3>
<p className="text-slate-400 text-xs sm:text-sm mt-2">
Únete a más de 50,000 profesionales que potencian su carrera cada semana. Sin spam.
</p>
</div>
{/* Newsletter form */}
<form onSubmit={handleSubscribe} className="w-full max-w-md">
<div className="flex flex-col sm:flex-row gap-2">
<div className="relative flex-1">
<FiMail className="absolute left-3.5 top-3.5 text-slate-500" />
<input
type="email"
required
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder="[email protected]"
className="w-full pl-10 pr-4 py-3 bg-slate-900 border border-slate-700 rounded-xl text-sm text-white placeholder-slate-500 focus:outline-none focus:ring-2 focus:ring-cyan-500"
/>
</div>
<button
type="submit"
className="px-6 py-3 bg-gradient-to-r from-blue-600 to-cyan-500 hover:from-blue-500 hover:to-cyan-400 text-white font-semibold rounded-xl text-sm transition-all shadow-lg shadow-cyan-500/20 whitespace-nowrap"
>
{subscribed ? '¡Suscrito!' : 'Suscribirme'}
</button>
</div>
{subscribed && (
<p className="flex items-center gap-1.5 text-emerald-400 text-xs mt-2 animate-fadeIn">
<FiCheckCircle /> ¡Gracias por suscribirte! Revisa tu bandeja de entrada pronto.
</p>
)}
</form>
</div>
</div>
</div>
{/* Main Footer Links */}
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-14">
<div className="grid grid-cols-2 md:grid-cols-4 lg:grid-cols-5 gap-8 lg:gap-12">
{/* Brand col */}
<div className="col-span-2 lg:col-span-2">
<div className="flex items-center gap-3 mb-4">
<div className="w-10 h-10 rounded-xl bg-gradient-to-tr from-indigo-600 via-blue-500 to-cyan-400 p-[2px]">
<div className="w-full h-full bg-slate-950 rounded-[10px] flex items-center justify-center">
<FiBookOpen className="text-cyan-400 text-lg" />
</div>
</div>
<span className="text-xl font-bold text-white tracking-tight">
Ry<span className="text-cyan-400">Courses</span>
</span>
</div>
<p className="text-xs text-slate-400 leading-relaxed mb-6 max-w-sm">
La plataforma líder para aprender habilidades de alta demanda y convertir tu conocimiento profesional en un negocio digital rentable.
</p>
<div className="flex items-center gap-3">
<a href="#" className="w-9 h-9 rounded-lg bg-slate-900 border border-slate-800 flex items-center justify-center text-slate-400 hover:text-cyan-400 hover:border-cyan-500/40 transition">
<FaTwitter />
</a>
<a href="#" className="w-9 h-9 rounded-lg bg-slate-900 border border-slate-800 flex items-center justify-center text-slate-400 hover:text-red-400 hover:border-red-500/40 transition">
<FaYoutube />
</a>
<a href="#" className="w-9 h-9 rounded-lg bg-slate-900 border border-slate-800 flex items-center justify-center text-slate-400 hover:text-white hover:border-slate-600 transition">
<FaGithub />
</a>
<a href="#" className="w-9 h-9 rounded-lg bg-slate-900 border border-slate-800 flex items-center justify-center text-slate-400 hover:text-blue-400 hover:border-blue-500/40 transition">
<FaLinkedin />
</a>
<a href="#" className="w-9 h-9 rounded-lg bg-slate-900 border border-slate-800 flex items-center justify-center text-slate-400 hover:text-indigo-400 hover:border-indigo-500/40 transition">
<FaDiscord />
</a>
</div>
</div>
{/* Column 1: Estudiantes */}
<div>
<h4 className="text-xs font-bold uppercase tracking-wider text-slate-200 mb-4">Para Estudiantes</h4>
<ul className="space-y-2.5 text-xs">
<li><a href="#" className="hover:text-cyan-300 transition">Explorar Cursos</a></li>
<li><a href="#" className="hover:text-cyan-300 transition">Rutas de Especialización</a></li>
<li><a href="#" className="hover:text-cyan-300 transition">Certificados Oficiales</a></li>
<li><a href="#" className="hover:text-cyan-300 transition">Becas y Descuentos</a></li>
<li><a href="#" className="hover:text-cyan-300 transition">Preguntas Frecuentes</a></li>
</ul>
</div>
{/* Column 2: Instructores */}
<div>
<h4 className="text-xs font-bold uppercase tracking-wider text-slate-200 mb-4">Para Instructores</h4>
<ul className="space-y-2.5 text-xs">
<li><a href="#" className="hover:text-cyan-300 transition">Enseñar en RyCourses</a></li>
<li><a href="#" className="hover:text-cyan-300 transition">Calculadora de Ganancias</a></li>
<li><a href="#" className="hover:text-cyan-300 transition">Guía del Creador</a></li>
<li><a href="#" className="hover:text-cyan-300 transition">Políticas de Contenido</a></li>
<li><a href="#" className="hover:text-cyan-300 transition">Comunidad de Profesores</a></li>
</ul>
</div>
{/* Column 3: Empresa & Legal */}
<div>
<h4 className="text-xs font-bold uppercase tracking-wider text-slate-200 mb-4">Empresa & Soporte</h4>
<ul className="space-y-2.5 text-xs">
<li><a href="#" className="hover:text-cyan-300 transition">Sobre Nosotros</a></li>
<li><a href="#" className="hover:text-cyan-300 transition">RyCourses para Empresas</a></li>
<li><a href="#" className="hover:text-cyan-300 transition">Términos de Servicio</a></li>
<li><a href="#" className="hover:text-cyan-300 transition">Política de Privacidad</a></li>
<li><a href="#" className="hover:text-cyan-300 transition">Centro de Ayuda</a></li>
</ul>
</div>
</div>
{/* Bottom bar */}
<div className="mt-12 pt-8 border-t border-slate-800/80 flex flex-col md:flex-row items-center justify-between gap-4 text-xs">
<div className="flex items-center gap-4">
<span className="flex items-center gap-1.5 text-slate-400">
<FiGlobe className="text-cyan-400" /> Español (América Latina)
</span>
<span className="flex items-center gap-1.5 text-slate-400">
<FiShield className="text-emerald-400" /> Pagos cifrados SSL 256-bit
</span>
</div>
<div className="flex items-center gap-1 text-slate-500">
<span>© 2026 RyCourses Inc. Hecho con</span>
<FiHeart className="text-rose-500 inline text-xs" />
<span>para creadores y estudiantes.</span>
</div>
</div>
</div>
</footer>
);
};
+264
View File
@@ -0,0 +1,264 @@
import React, { useState } from 'react';
import {
FiSearch,
FiShoppingCart,
FiHeart,
FiBell,
FiMenu,
FiX,
FiBookOpen,
FiPlusCircle,
FiCompass,
} from 'react-icons/fi';
import { FaGraduationCap, FaChalkboardTeacher } from 'react-icons/fa';
import type { ViewMode } from '../../types/course';
interface NavbarProps {
viewMode: ViewMode;
onToggleViewMode: (mode: ViewMode) => void;
cartCount: number;
onOpenCart: () => void;
onOpenCreateCourseModal: () => void;
searchQuery: string;
onSearchChange: (query: string) => void;
}
export const Navbar: React.FC<NavbarProps> = ({
viewMode,
onToggleViewMode,
cartCount,
onOpenCart,
onOpenCreateCourseModal,
searchQuery,
onSearchChange,
}) => {
const [mobileMenuOpen, setMobileMenuOpen] = useState(false);
const [categoryDropdown, setCategoryDropdown] = useState(false);
return (
<header className="sticky top-0 z-40 w-full border-b border-slate-800/80 bg-slate-950/80 backdrop-blur-md transition-all">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="flex items-center justify-between h-20 gap-4">
{/* Logo */}
<div className="flex items-center gap-8">
<div
onClick={() => onToggleViewMode('buyer')}
className="flex items-center gap-3 cursor-pointer group select-none"
>
<div className="w-11 h-11 rounded-xl bg-gradient-to-tr from-indigo-600 via-blue-500 to-cyan-400 p-[2px] shadow-lg shadow-indigo-500/20 group-hover:scale-105 transition-transform">
<div className="w-full h-full bg-slate-950 rounded-[10px] flex items-center justify-center">
<FiBookOpen className="text-cyan-400 text-xl" />
</div>
</div>
<div>
<div className="flex items-center gap-1.5">
<span className="text-xl font-black tracking-tight text-white group-hover:text-cyan-300 transition-colors">
Ry<span className="text-transparent bg-clip-text bg-gradient-to-r from-blue-400 to-cyan-400">Courses</span>
</span>
<span className="text-[10px] font-bold px-1.5 py-0.5 rounded bg-indigo-500/20 text-indigo-300 border border-indigo-500/30">
PRO
</span>
</div>
<p className="text-[11px] text-slate-400 font-medium tracking-wide">Plataforma Educativa</p>
</div>
</div>
{/* Quick Categories dropdown (Desktop) */}
{viewMode === 'buyer' && (
<div className="relative hidden lg:block">
<button
onClick={() => setCategoryDropdown(!categoryDropdown)}
className="flex items-center gap-2 px-3.5 py-2 rounded-lg text-sm font-medium text-slate-300 hover:text-white hover:bg-slate-800/60 border border-slate-700/60 transition"
>
<FiCompass className="text-cyan-400" />
<span>Explorar</span>
</button>
{categoryDropdown && (
<div
onMouseLeave={() => setCategoryDropdown(false)}
className="absolute top-full left-0 mt-2 w-56 rounded-xl bg-slate-900 border border-slate-800 shadow-2xl p-2 z-50 animate-in fade-in slide-in-from-top-2 duration-200"
>
{['Desarrollo Web', 'Inteligencia Artificial', 'Diseño UI/UX', 'Ciberseguridad', 'Marketing Digital', 'Negocios'].map((cat) => (
<button
key={cat}
onClick={() => {
onSearchChange(cat);
setCategoryDropdown(false);
}}
className="w-full text-left px-3 py-2 text-xs font-medium text-slate-300 hover:text-cyan-300 hover:bg-slate-800/80 rounded-lg transition"
>
{cat}
</button>
))}
</div>
)}
</div>
)}
</div>
{/* Search bar (Desktop) */}
{viewMode === 'buyer' && (
<div className="hidden md:flex flex-1 max-w-md mx-4">
<div className="relative w-full">
<div className="absolute inset-y-0 left-0 pl-3.5 flex items-center pointer-events-none text-slate-400">
<FiSearch className="text-base" />
</div>
<input
type="text"
value={searchQuery}
onChange={(e) => onSearchChange(e.target.value)}
placeholder="¿Qué habilidad quieres aprender hoy? (React, Python, Figma...)"
className="w-full pl-10 pr-16 py-2.5 bg-slate-900/90 border border-slate-700/70 rounded-xl text-sm text-slate-100 placeholder-slate-400 focus:outline-none focus:ring-2 focus:ring-cyan-500/50 focus:border-cyan-500 transition-all shadow-inner"
/>
<div className="absolute inset-y-0 right-0 pr-3 flex items-center pointer-events-none">
<kbd className="hidden xl:inline-block px-1.5 py-0.5 text-[10px] font-semibold text-slate-400 bg-slate-800 border border-slate-700 rounded">
Ctrl + K
</kbd>
</div>
</div>
</div>
)}
{/* View Mode Switcher (Estudiante vs Instructor) & User Actions */}
<div className="flex items-center gap-3">
{/* Mode Switcher pill */}
<div className="bg-slate-900/90 p-1 rounded-xl border border-slate-800 flex items-center shadow-inner">
<button
onClick={() => onToggleViewMode('buyer')}
className={`flex items-center gap-2 px-3 py-1.5 rounded-lg text-xs font-semibold transition-all ${
viewMode === 'buyer'
? 'bg-gradient-to-r from-blue-600 to-cyan-500 text-white shadow-md shadow-cyan-500/20'
: 'text-slate-400 hover:text-slate-200'
}`}
title="Modo Estudiante - Explorar y Comprar cursos"
>
<FaGraduationCap className="text-sm" />
<span className="hidden sm:inline">Comprar</span>
</button>
<button
onClick={() => onToggleViewMode('seller')}
className={`flex items-center gap-2 px-3 py-1.5 rounded-lg text-xs font-semibold transition-all ${
viewMode === 'seller'
? 'bg-gradient-to-r from-indigo-600 to-violet-500 text-white shadow-md shadow-violet-500/20'
: 'text-slate-400 hover:text-slate-200'
}`}
title="Modo Instructor - Vender y Gestionar cursos"
>
<FaChalkboardTeacher className="text-sm" />
<span className="hidden sm:inline">Vender</span>
</button>
</div>
{/* If seller mode: Create Course button */}
{viewMode === 'seller' && (
<button
onClick={onOpenCreateCourseModal}
className="hidden sm:flex items-center gap-2 px-4 py-2 rounded-xl bg-gradient-to-r from-violet-600 to-indigo-600 hover:from-violet-500 hover:to-indigo-500 text-white font-medium text-xs shadow-lg shadow-violet-600/30 hover:scale-[1.02] transition-all"
>
<FiPlusCircle className="text-base" />
<span>Crear Curso</span>
</button>
)}
{/* Cart button (buyer mode) */}
{viewMode === 'buyer' && (
<button
onClick={onOpenCart}
className="relative p-2.5 text-slate-300 hover:text-white bg-slate-900/80 hover:bg-slate-800 border border-slate-700/80 rounded-xl transition"
aria-label="Carrito de compras"
>
<FiShoppingCart className="text-lg" />
{cartCount > 0 && (
<span className="absolute -top-1.5 -right-1.5 w-5 h-5 bg-gradient-to-tr from-rose-500 to-pink-500 text-white text-[11px] font-bold rounded-full flex items-center justify-center animate-bounce shadow-md">
{cartCount}
</span>
)}
</button>
)}
{/* Wishlist */}
{viewMode === 'buyer' && (
<button className="hidden sm:flex p-2.5 text-slate-400 hover:text-rose-400 bg-slate-900/60 hover:bg-slate-800/80 border border-slate-800 rounded-xl transition">
<FiHeart className="text-lg" />
</button>
)}
{/* Notification Bell */}
<button className="relative hidden sm:flex p-2.5 text-slate-400 hover:text-amber-400 bg-slate-900/60 hover:bg-slate-800/80 border border-slate-800 rounded-xl transition">
<FiBell className="text-lg" />
<span className="absolute top-2 right-2 w-2 h-2 bg-amber-400 rounded-full"></span>
</button>
{/* User Profile Avatar */}
<div className="flex items-center gap-2 pl-2 border-l border-slate-800">
<div className="relative cursor-pointer">
<img
src="https://images.unsplash.com/photo-1534528741775-53994a69daeb?w=100&auto=format&fit=crop&q=80"
alt="Usuario"
className="w-9 h-9 rounded-full object-cover ring-2 ring-indigo-500/50 hover:ring-cyan-400 transition"
/>
<span className="absolute bottom-0 right-0 w-2.5 h-2.5 bg-emerald-500 ring-2 ring-slate-950 rounded-full"></span>
</div>
</div>
{/* Mobile menu trigger */}
<button
onClick={() => setMobileMenuOpen(!mobileMenuOpen)}
className="p-2 text-slate-400 hover:text-white md:hidden"
>
{mobileMenuOpen ? <FiX className="text-2xl" /> : <FiMenu className="text-2xl" />}
</button>
</div>
</div>
{/* Mobile search & menu expanded */}
{mobileMenuOpen && (
<div className="md:hidden py-4 border-t border-slate-800 space-y-4">
<div className="relative">
<FiSearch className="absolute left-3.5 top-3 text-slate-400" />
<input
type="text"
value={searchQuery}
onChange={(e) => onSearchChange(e.target.value)}
placeholder="Buscar cursos..."
className="w-full pl-10 pr-4 py-2 bg-slate-900 border border-slate-700 rounded-lg text-sm text-white"
/>
</div>
<div className="grid grid-cols-2 gap-2">
<button
onClick={() => { onToggleViewMode('buyer'); setMobileMenuOpen(false); }}
className={`flex items-center justify-center gap-2 p-2.5 rounded-lg text-xs font-semibold ${
viewMode === 'buyer' ? 'bg-blue-600 text-white' : 'bg-slate-800 text-slate-300'
}`}
>
<FaGraduationCap /> Modo Comprador
</button>
<button
onClick={() => { onToggleViewMode('seller'); setMobileMenuOpen(false); }}
className={`flex items-center justify-center gap-2 p-2.5 rounded-lg text-xs font-semibold ${
viewMode === 'seller' ? 'bg-violet-600 text-white' : 'bg-slate-800 text-slate-300'
}`}
>
<FaChalkboardTeacher /> Modo Vendedor
</button>
</div>
{viewMode === 'seller' && (
<button
onClick={() => { onOpenCreateCourseModal(); setMobileMenuOpen(false); }}
className="w-full py-2.5 bg-gradient-to-r from-violet-600 to-indigo-600 text-white font-semibold rounded-lg text-xs flex items-center justify-center gap-2"
>
<FiPlusCircle /> Crear Nuevo Curso
</button>
)}
</div>
)}
</div>
</header>
);
};
+390
View File
@@ -0,0 +1,390 @@
import React, { useState } from 'react';
import {
FiX,
FiCheck,
FiArrowRight,
FiArrowLeft,
FiBookOpen,
FiDollarSign,
FiPlus,
FiCheckCircle
} from 'react-icons/fi';
import type { Course, CourseLevel } from '../../types/course';
import { mockInstructors } from '../../data/mockCourses';
interface CreateCourseModalProps {
isOpen: boolean;
onClose: () => void;
onCreateCourse: (newCourse: Course) => void;
}
export const CreateCourseModal: React.FC<CreateCourseModalProps> = ({
isOpen,
onClose,
onCreateCourse,
}) => {
const [step, setStep] = useState<1 | 2 | 3>(1);
// Form State
const [title, setTitle] = useState('');
const [subtitle, setSubtitle] = useState('');
const [category, setCategory] = useState('Desarrollo Web');
const [level, setLevel] = useState<CourseLevel>('Todos los niveles');
const [price, setPrice] = useState('29.99');
const [originalPrice, setOriginalPrice] = useState('99.99');
const [thumbnail, setThumbnail] = useState(
'https://images.unsplash.com/photo-1516321318423-f06f85e504b3?w=800&auto=format&fit=crop&q=80'
);
const [duration, setDuration] = useState('20h 30min');
const [whatYouWillLearn, setWhatYouWillLearn] = useState([
'Aprenderás conceptos clave y buenas prácticas de la industria',
'Construirás proyectos prácticos de portafolio para conseguir empleo'
]);
const [newOutcome, setNewOutcome] = useState('');
if (!isOpen) return null;
const handleAddOutcome = () => {
if (newOutcome.trim()) {
setWhatYouWillLearn([...whatYouWillLearn, newOutcome.trim()]);
setNewOutcome('');
}
};
const handleRemoveOutcome = (index: number) => {
setWhatYouWillLearn(whatYouWillLearn.filter((_, i) => i !== index));
};
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
const createdCourse: Course = {
id: `course-${Date.now()}`,
title: title || 'Nuevo Curso Especializado',
subtitle: subtitle || 'Aprende los fundamentos y técnicas avanzadas en esta guía paso a paso.',
category,
level,
price: parseFloat(price) || 19.99,
originalPrice: parseFloat(originalPrice) || 59.99,
rating: 5.0,
reviewsCount: 1,
studentsCount: 0,
thumbnail: thumbnail || 'https://images.unsplash.com/photo-1516321318423-f06f85e504b3?w=800&auto=format&fit=crop&q=80',
duration: duration || '15h 00min',
lessonsCount: 45,
badge: 'Nuevo',
instructor: mockInstructors.diego,
updatedAt: 'Febrero 2026',
language: 'Español',
whatYouWillLearn: whatYouWillLearn.length > 0 ? whatYouWillLearn : ['Dominar los temas principales del curso'],
requirements: ['Ganas de aprender y una computadora con acceso a internet'],
modules: [
{
id: 'm1',
title: 'Módulo 1: Introducción y Fundamentos',
lessons: [
{ id: 'l1', title: 'Bienvenida al curso y objetivos', duration: '10:00', isFreePreview: true },
{ id: 'l2', title: 'Configuración del entorno de trabajo', duration: '15:30', isFreePreview: true }
]
},
{
id: 'm2',
title: 'Módulo 2: Casos Prácticos y Proyecto Final',
lessons: [
{ id: 'l3', title: 'Construcción del proyecto paso a paso', duration: '30:00' },
{ id: 'l4', title: 'Entrega y certificación', duration: '12:00' }
]
}
],
reviews: [],
isPublished: true,
salesCount: 0,
totalRevenue: 0
};
onCreateCourse(createdCourse);
onClose();
};
return (
<div className="fixed inset-0 z-50 overflow-y-auto bg-slate-950/80 backdrop-blur-md flex items-center justify-center p-3 sm:p-6 animate-in fade-in duration-200">
<div
className="relative w-full max-w-2xl bg-slate-900 border border-slate-800 rounded-2xl shadow-2xl overflow-hidden my-6"
onClick={(e) => e.stopPropagation()}
>
{/* Header */}
<div className="p-6 border-b border-slate-800 bg-slate-950/60 flex items-center justify-between">
<div className="flex items-center gap-3">
<div className="w-10 h-10 rounded-xl bg-violet-500/20 text-violet-400 flex items-center justify-center text-lg">
<FiBookOpen />
</div>
<div>
<h3 className="text-base font-bold text-white">Publicar Nuevo Curso</h3>
<p className="text-xs text-slate-400">Paso {step} de 3</p>
</div>
</div>
<button
onClick={onClose}
className="p-2 rounded-xl text-slate-400 hover:text-white hover:bg-slate-800 transition"
>
<FiX className="text-xl" />
</button>
</div>
{/* Step Progress Bar */}
<div className="w-full bg-slate-800 h-1.5">
<div
className="bg-gradient-to-r from-violet-600 to-indigo-500 h-full transition-all duration-300"
style={{ width: `${(step / 3) * 100}%` }}
/>
</div>
{/* Form Body */}
<form onSubmit={handleSubmit} className="p-6 space-y-5">
{/* Step 1: Basic Information */}
{step === 1 && (
<div className="space-y-4 animate-in fade-in">
<div>
<label className="block text-xs font-bold text-slate-300 mb-1.5">
Título del Curso *
</label>
<input
type="text"
required
value={title}
onChange={(e) => setTitle(e.target.value)}
placeholder="Ej. Master en Desarrollo de Software Full Stack"
className="w-full px-4 py-2.5 bg-slate-950 border border-slate-700 rounded-xl text-sm text-white focus:outline-none focus:border-violet-500"
/>
</div>
<div>
<label className="block text-xs font-bold text-slate-300 mb-1.5">
Subtítulo / Breve Descripción *
</label>
<textarea
rows={2}
required
value={subtitle}
onChange={(e) => setSubtitle(e.target.value)}
placeholder="Explica en 2 frases lo que el estudiante logrará al finalizar este curso."
className="w-full px-4 py-2.5 bg-slate-950 border border-slate-700 rounded-xl text-sm text-white focus:outline-none focus:border-violet-500"
/>
</div>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
<div>
<label className="block text-xs font-bold text-slate-300 mb-1.5">
Categoría
</label>
<select
value={category}
onChange={(e) => setCategory(e.target.value)}
className="w-full px-3 py-2.5 bg-slate-950 border border-slate-700 rounded-xl text-xs text-white focus:outline-none focus:border-violet-500"
>
<option value="Desarrollo Web">Desarrollo Web</option>
<option value="Inteligencia Artificial">Inteligencia Artificial</option>
<option value="Diseño UI/UX">Diseño UI/UX</option>
<option value="Ciberseguridad">Ciberseguridad</option>
<option value="Marketing Digital">Marketing Digital</option>
<option value="Negocios & Finanzas">Negocios & Finanzas</option>
</select>
</div>
<div>
<label className="block text-xs font-bold text-slate-300 mb-1.5">
Nivel del Curso
</label>
<select
value={level}
onChange={(e) => setLevel(e.target.value as CourseLevel)}
className="w-full px-3 py-2.5 bg-slate-950 border border-slate-700 rounded-xl text-xs text-white focus:outline-none focus:border-violet-500"
>
<option value="Principiante">Principiante</option>
<option value="Intermedio">Intermedio</option>
<option value="Avanzado">Avanzado</option>
<option value="Todos los niveles">Todos los niveles</option>
</select>
</div>
</div>
</div>
)}
{/* Step 2: Pricing & Duration */}
{step === 2 && (
<div className="space-y-4 animate-in fade-in">
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
<div>
<label className="block text-xs font-bold text-slate-300 mb-1.5">
Precio de Venta ($ USD) *
</label>
<div className="relative">
<FiDollarSign className="absolute left-3 top-3 text-slate-400" />
<input
type="number"
step="0.01"
required
value={price}
onChange={(e) => setPrice(e.target.value)}
placeholder="29.99"
className="w-full pl-8 pr-4 py-2.5 bg-slate-950 border border-slate-700 rounded-xl text-sm text-white focus:outline-none focus:border-violet-500 font-mono"
/>
</div>
</div>
<div>
<label className="block text-xs font-bold text-slate-300 mb-1.5">
Precio Original Tachado ($ USD)
</label>
<div className="relative">
<FiDollarSign className="absolute left-3 top-3 text-slate-400" />
<input
type="number"
step="0.01"
value={originalPrice}
onChange={(e) => setOriginalPrice(e.target.value)}
placeholder="89.99"
className="w-full pl-8 pr-4 py-2.5 bg-slate-950 border border-slate-700 rounded-xl text-sm text-white focus:outline-none focus:border-violet-500 font-mono"
/>
</div>
</div>
</div>
<div>
<label className="block text-xs font-bold text-slate-300 mb-1.5">
Duración Total Estimada de Video
</label>
<input
type="text"
value={duration}
onChange={(e) => setDuration(e.target.value)}
placeholder="Ej. 24h 45min"
className="w-full px-4 py-2.5 bg-slate-950 border border-slate-700 rounded-xl text-sm text-white focus:outline-none focus:border-violet-500"
/>
</div>
<div className="p-4 rounded-xl bg-violet-950/30 border border-violet-500/20 text-xs text-slate-300 space-y-1">
<p className="font-bold text-violet-300">💡 Consejo de fijación de precios:</p>
<p>Los cursos con precios entre $19.99 y $34.99 tienen una tasa de conversión 45% más alta en nuestra plataforma.</p>
</div>
</div>
)}
{/* Step 3: Media & What you'll learn */}
{step === 3 && (
<div className="space-y-4 animate-in fade-in">
<div>
<label className="block text-xs font-bold text-slate-300 mb-1.5">
URL de Imagen de Portada (Thumbnail)
</label>
<div className="flex gap-2">
<input
type="url"
value={thumbnail}
onChange={(e) => setThumbnail(e.target.value)}
placeholder="https://images.unsplash.com/..."
className="flex-1 px-4 py-2.5 bg-slate-950 border border-slate-700 rounded-xl text-xs text-white focus:outline-none focus:border-violet-500"
/>
</div>
{thumbnail && (
<img
src={thumbnail}
alt="Preview"
className="mt-2 h-24 w-40 object-cover rounded-lg border border-slate-700"
/>
)}
</div>
<div>
<label className="block text-xs font-bold text-slate-300 mb-1.5">
Lo que aprenderá el alumno (Puntos clave)
</label>
<div className="flex gap-2 mb-2">
<input
type="text"
value={newOutcome}
onChange={(e) => setNewOutcome(e.target.value)}
placeholder="Ej. Crear APIs RESTful y GraphQL con Node.js"
className="flex-1 px-3 py-2 bg-slate-950 border border-slate-700 rounded-lg text-xs text-white"
/>
<button
type="button"
onClick={handleAddOutcome}
className="px-3 py-2 bg-slate-800 hover:bg-slate-700 text-white rounded-lg text-xs flex items-center gap-1"
>
<FiPlus /> Añadir
</button>
</div>
<div className="space-y-1.5 max-h-32 overflow-y-auto">
{whatYouWillLearn.map((item, idx) => (
<div
key={idx}
className="flex items-center justify-between p-2 rounded-lg bg-slate-950 border border-slate-800 text-xs text-slate-300"
>
<span className="flex items-center gap-1.5 truncate">
<FiCheckCircle className="text-emerald-400 shrink-0" /> {item}
</span>
<button
type="button"
onClick={() => handleRemoveOutcome(idx)}
className="text-slate-500 hover:text-rose-400 pl-2"
>
<FiX />
</button>
</div>
))}
</div>
</div>
</div>
)}
{/* Bottom Actions */}
<div className="pt-4 border-t border-slate-800 flex items-center justify-between">
{step > 1 ? (
<button
type="button"
onClick={() => setStep((s) => (s - 1) as any)}
className="px-4 py-2 bg-slate-800 hover:bg-slate-700 text-slate-300 text-xs font-semibold rounded-xl flex items-center gap-1.5 transition"
>
<FiArrowLeft /> Anterior
</button>
) : (
<div />
)}
{step < 3 ? (
<button
type="button"
onClick={() => {
if (step === 1 && !title.trim()) {
setTitle('Nuevo Curso Especializado');
}
setStep((s) => (s + 1) as any);
}}
className="px-5 py-2 bg-violet-600 hover:bg-violet-500 text-white text-xs font-bold rounded-xl flex items-center gap-1.5 transition shadow-lg shadow-violet-600/30"
>
<span>Siguiente</span>
<FiArrowRight />
</button>
) : (
<button
type="submit"
className="px-6 py-2.5 bg-gradient-to-r from-emerald-600 to-teal-500 hover:from-emerald-500 hover:to-teal-400 text-white text-xs font-bold rounded-xl flex items-center gap-2 transition shadow-lg shadow-emerald-500/30"
>
<FiCheck className="text-base" />
<span>Publicar Curso Ahora</span>
</button>
)}
</div>
</form>
</div>
</div>
);
};
+148
View File
@@ -0,0 +1,148 @@
import { useState } from 'react';
import { FiDollarSign, FiUsers, FiCheckCircle } from 'react-icons/fi';
interface SellerCalculatorProps {
onStartCourse: () => void;
}
export const SellerCalculator: React.FC<SellerCalculatorProps> = ({ onStartCourse }) => {
const [coursePrice, setCoursePrice] = useState<number>(29);
const [monthlyStudents, setMonthlyStudents] = useState<number>(150);
const instructorRevenueShare = 0.90; // 90% revenue
const monthlyRevenue = coursePrice * monthlyStudents * instructorRevenueShare;
const annualRevenue = monthlyRevenue * 12;
return (
<section id="seller-calculator" className="py-16 bg-slate-950/90 border-b 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-violet-400 bg-violet-500/10 px-3 py-1 rounded-full border border-violet-500/20">
Simulador Interactivo
</span>
<h2 className="text-2xl sm:text-4xl font-extrabold text-white">
Calcula tu potencial de ingresos como instructor
</h2>
<p className="text-sm text-slate-400">
Ajusta el precio de tu curso y la cantidad de alumnos estimados al mes para ver cuánto podrías ganar.
</p>
</div>
<div className="grid grid-cols-1 lg:grid-cols-12 gap-8 items-center bg-slate-900/60 border border-slate-800 rounded-3xl p-6 sm:p-10 shadow-2xl backdrop-blur-sm">
{/* Controls column */}
<div className="lg:col-span-6 space-y-8">
{/* Price Slider */}
<div className="space-y-3">
<div className="flex justify-between items-center">
<label className="text-sm font-bold text-white flex items-center gap-2">
<FiDollarSign className="text-violet-400 text-base" /> Precio por curso
</label>
<span className="text-xl font-black text-cyan-400 font-mono">
${coursePrice} USD
</span>
</div>
<input
type="range"
min="9"
max="199"
step="5"
value={coursePrice}
onChange={(e) => setCoursePrice(Number(e.target.value))}
className="w-full h-2.5 bg-slate-800 rounded-lg appearance-none cursor-pointer accent-violet-500"
/>
<div className="flex justify-between text-[11px] text-slate-500 font-mono">
<span>$9 (Básico)</span>
<span>$99 (Especialización)</span>
<span>$199 (Master/Bootcamp)</span>
</div>
</div>
{/* Students Slider */}
<div className="space-y-3">
<div className="flex justify-between items-center">
<label className="text-sm font-bold text-white flex items-center gap-2">
<FiUsers className="text-violet-400 text-base" /> Alumnos nuevos por mes
</label>
<span className="text-xl font-black text-violet-400 font-mono">
{monthlyStudents} alumnos
</span>
</div>
<input
type="range"
min="10"
max="1000"
step="10"
value={monthlyStudents}
onChange={(e) => setMonthlyStudents(Number(e.target.value))}
className="w-full h-2.5 bg-slate-800 rounded-lg appearance-none cursor-pointer accent-violet-500"
/>
<div className="flex justify-between text-[11px] text-slate-500 font-mono">
<span>10 alumnos</span>
<span>500 alumnos</span>
<span>1,000+ alumnos</span>
</div>
</div>
{/* Features checkmarks */}
<div className="space-y-2 pt-2 text-xs text-slate-400">
<div className="flex items-center gap-2">
<FiCheckCircle className="text-emerald-400" /> Cobras el 90% del valor de cada venta
</div>
<div className="flex items-center gap-2">
<FiCheckCircle className="text-emerald-400" /> Sin cuotas mensuales ni cobros por alojamiento de video
</div>
<div className="flex items-center gap-2">
<FiCheckCircle className="text-emerald-400" /> Tus derechos de autor y propiedad intelectual 100% protegidos
</div>
</div>
</div>
{/* Results Display Column */}
<div className="lg:col-span-6 bg-gradient-to-br from-violet-950/60 to-slate-950 p-8 rounded-2xl border border-violet-500/30 text-center space-y-6 shadow-xl">
<span className="text-xs font-semibold text-violet-300 uppercase tracking-wider">
Ingresos Estimados Netos
</span>
{/* Monthly Estimate */}
<div>
<div className="text-4xl sm:text-5xl font-black text-transparent bg-clip-text bg-gradient-to-r from-violet-400 via-purple-300 to-cyan-300 font-mono">
${monthlyRevenue.toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}
</div>
<p className="text-xs text-slate-400 mt-1 font-medium">al mes proyectado</p>
</div>
{/* Annual Estimate */}
<div className="p-4 rounded-xl bg-slate-900/80 border border-slate-800 flex items-center justify-around">
<div>
<span className="text-[11px] text-slate-400">Proyección Anual</span>
<p className="text-xl font-bold text-white font-mono">
${annualRevenue.toLocaleString('en-US', { minimumFractionDigits: 0, maximumFractionDigits: 0 })} USD
</p>
</div>
<div className="h-8 w-[1px] bg-slate-800" />
<div>
<span className="text-[11px] text-slate-400">Comisión RyCourses</span>
<p className="text-xl font-bold text-emerald-400 font-mono">Solo 10%</p>
</div>
</div>
<button
onClick={onStartCourse}
className="w-full py-3.5 bg-gradient-to-r from-violet-600 to-indigo-600 hover:from-violet-500 hover:to-indigo-500 text-white font-bold rounded-xl text-sm shadow-xl shadow-violet-600/30 transition-all hover:scale-[1.02] cursor-pointer"
>
Comenzar a Crear mi Curso Ahora
</button>
</div>
</div>
</div>
</section>
);
};
+201
View File
@@ -0,0 +1,201 @@
import React, { useState } from 'react';
import {
FiPlusCircle,
FiEdit,
FiEye,
FiTrash2,
FiStar,
FiUsers,
FiSearch
} from 'react-icons/fi';
import type { Course } from '../../types/course';
interface SellerCourseListProps {
courses: Course[];
onOpenCreateModal: () => void;
onPreviewCourse: (course: Course) => void;
onDeleteCourse: (courseId: string) => void;
}
export const SellerCourseList: React.FC<SellerCourseListProps> = ({
courses,
onOpenCreateModal,
onPreviewCourse,
onDeleteCourse,
}) => {
const [filterQuery, setFilterQuery] = useState('');
const [statusFilter, setStatusFilter] = useState<'all' | 'published' | 'draft'>('all');
const filtered = courses.filter((c) => {
if (filterQuery.trim()) {
const match = c.title.toLowerCase().includes(filterQuery.toLowerCase());
if (!match) return false;
}
if (statusFilter === 'published' && !c.isPublished) return false;
if (statusFilter === 'draft' && c.isPublished) return false;
return true;
});
return (
<section className="py-12 bg-slate-950/70 border-t border-slate-800">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 space-y-6">
{/* Header & New Course trigger */}
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-4">
<div>
<h2 className="text-2xl font-bold text-white">Mis Cursos Gestionados</h2>
<p className="text-xs text-slate-400">
Administra tus contenidos, actualiza precios y supervisa las ventas de tus cursos.
</p>
</div>
<button
onClick={onOpenCreateModal}
className="px-5 py-2.5 bg-gradient-to-r from-violet-600 to-indigo-600 hover:from-violet-500 hover:to-indigo-500 text-white text-xs font-bold rounded-xl shadow-lg shadow-violet-600/30 flex items-center gap-2 self-start sm:self-auto cursor-pointer"
>
<FiPlusCircle className="text-base" />
<span>Crear Nuevo Curso</span>
</button>
</div>
{/* Filter Toolbar */}
<div className="flex flex-col sm:flex-row items-center justify-between gap-4 p-4 rounded-xl bg-slate-900/60 border border-slate-800">
<div className="relative w-full sm:w-72">
<FiSearch className="absolute left-3 top-3 text-slate-400 text-xs" />
<input
type="text"
value={filterQuery}
onChange={(e) => setFilterQuery(e.target.value)}
placeholder="Buscar en mis cursos..."
className="w-full pl-8 pr-3 py-2 bg-slate-800 border border-slate-700 rounded-lg text-xs text-white placeholder-slate-400 focus:outline-none focus:border-violet-500"
/>
</div>
<div className="flex items-center gap-2 self-start sm:self-auto">
<button
onClick={() => setStatusFilter('all')}
className={`px-3 py-1.5 rounded-lg text-xs font-semibold ${
statusFilter === 'all'
? 'bg-violet-600 text-white'
: 'bg-slate-800 text-slate-400 hover:text-white'
}`}
>
Todos ({courses.length})
</button>
<button
onClick={() => setStatusFilter('published')}
className={`px-3 py-1.5 rounded-lg text-xs font-semibold ${
statusFilter === 'published'
? 'bg-emerald-600 text-white'
: 'bg-slate-800 text-slate-400 hover:text-white'
}`}
>
Publicados
</button>
<button
onClick={() => setStatusFilter('draft')}
className={`px-3 py-1.5 rounded-lg text-xs font-semibold ${
statusFilter === 'draft'
? 'bg-amber-600 text-white'
: 'bg-slate-800 text-slate-400 hover:text-white'
}`}
>
Borradores
</button>
</div>
</div>
{/* Course Cards / Table list */}
<div className="space-y-3">
{filtered.map((course) => (
<div
key={course.id}
className="p-4 sm:p-5 rounded-2xl bg-slate-900/80 border border-slate-800/90 hover:border-slate-700 transition flex flex-col md:flex-row items-start md:items-center justify-between gap-4"
>
{/* Left info */}
<div className="flex items-center gap-4 min-w-0">
<img
src={course.thumbnail}
alt={course.title}
className="w-24 h-16 object-cover rounded-xl bg-slate-800 shrink-0"
/>
<div className="space-y-1 min-w-0">
<div className="flex items-center gap-2 flex-wrap">
<span className="text-[11px] font-bold text-violet-400 uppercase">
{course.category}
</span>
<span className={`text-[10px] font-bold px-2 py-0.5 rounded-full ${
course.isPublished !== false
? 'bg-emerald-500/20 text-emerald-300 border border-emerald-500/30'
: 'bg-amber-500/20 text-amber-300 border border-amber-500/30'
}`}>
{course.isPublished !== false ? '● Publicado' : '● Borrador'}
</span>
</div>
<h3 className="text-sm font-bold text-white truncate max-w-lg">
{course.title}
</h3>
<div className="flex items-center gap-4 text-xs text-slate-400">
<span className="flex items-center gap-1 text-amber-400 font-bold">
<FiStar className="fill-amber-400" /> {course.rating.toFixed(1)}
</span>
<span className="flex items-center gap-1">
<FiUsers /> {course.studentsCount.toLocaleString()} alumnos
</span>
<span>{course.duration}</span>
</div>
</div>
</div>
{/* Right financials & actions */}
<div className="flex items-center justify-between md:justify-end gap-6 w-full md:w-auto pt-3 md:pt-0 border-t md:border-t-0 border-slate-800">
<div className="text-left md:text-right">
<span className="text-[11px] text-slate-400">Ventas Generadas</span>
<div className="text-base font-black text-emerald-400 font-mono">
${(course.totalRevenue || course.price * (course.salesCount || 100)).toLocaleString('en-US', { minimumFractionDigits: 2 })}
</div>
<span className="text-[10px] text-slate-500">
Precio: ${course.price.toFixed(2)} USD
</span>
</div>
<div className="flex items-center gap-2">
<button
onClick={() => onPreviewCourse(course)}
className="p-2.5 rounded-xl bg-slate-800 hover:bg-slate-700 text-slate-300 hover:text-white transition"
title="Vista previa del curso"
>
<FiEye />
</button>
<button
className="p-2.5 rounded-xl bg-slate-800 hover:bg-slate-700 text-slate-300 hover:text-white transition"
title="Editar contenido"
>
<FiEdit />
</button>
<button
onClick={() => onDeleteCourse(course.id)}
className="p-2.5 rounded-xl bg-rose-500/10 hover:bg-rose-500/20 text-rose-400 border border-rose-500/20 transition"
title="Eliminar curso"
>
<FiTrash2 />
</button>
</div>
</div>
</div>
))}
</div>
</div>
</section>
);
};
+166
View File
@@ -0,0 +1,166 @@
import React from 'react';
import {
FiDollarSign,
FiUsers,
FiStar,
FiBookOpen,
FiTrendingUp,
FiArrowUpRight,
FiActivity
} from 'react-icons/fi';
export const SellerDashboard: React.FC = () => {
const salesHistory = [
{ month: 'Sep', amount: 3200, height: '45%' },
{ month: 'Oct', amount: 4100, height: '60%' },
{ month: 'Nov', amount: 3800, height: '55%' },
{ month: 'Dic', amount: 5600, height: '80%' },
{ month: 'Ene', amount: 6200, height: '90%' },
{ month: 'Feb', amount: 7400, height: '100%' },
];
const recentEnrollments = [
{ student: 'Marcos R.', course: 'Master en React 19', date: 'Hace 5 min', amount: '$24.99', country: '🇲🇽' },
{ student: 'Ana Paula L.', course: 'IA & Machine Learning', date: 'Hace 18 min', amount: '$29.99', country: '🇨🇴' },
{ student: 'Gabriel V.', course: 'Diseño UX/UI en Figma', date: 'Hace 42 min', amount: '$19.99', country: '🇦🇷' },
{ student: 'Elena T.', course: 'DevOps con AWS', date: 'Hace 1 hora', amount: '$32.99', country: '🇪🇸' },
];
return (
<section className="py-12 bg-slate-950">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 space-y-8">
{/* Section Header */}
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-4">
<div>
<div className="flex items-center gap-2 text-xs font-bold text-violet-400 uppercase tracking-wider mb-1">
<FiActivity />
<span>Resumen Ejecutivo</span>
</div>
<h2 className="text-2xl sm:text-3xl font-extrabold text-white">
Panel de Rendimiento del Creador
</h2>
</div>
<span className="text-xs text-slate-400 bg-slate-900 border border-slate-800 px-3 py-1.5 rounded-lg w-fit">
Últimos 30 días
</span>
</div>
{/* 4 Metric Cards */}
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-5">
<div className="p-5 rounded-2xl bg-slate-900/80 border border-slate-800 space-y-2">
<div className="flex items-center justify-between">
<span className="text-xs text-slate-400 font-medium">Ingresos Totales</span>
<div className="w-8 h-8 rounded-lg bg-emerald-500/10 text-emerald-400 flex items-center justify-center text-base">
<FiDollarSign />
</div>
</div>
<div className="text-2xl font-black text-white font-mono">$110,480.90</div>
<div className="flex items-center gap-1 text-[11px] text-emerald-400 font-semibold">
<FiArrowUpRight /> +18.4% este mes
</div>
</div>
<div className="p-5 rounded-2xl bg-slate-900/80 border border-slate-800 space-y-2">
<div className="flex items-center justify-between">
<span className="text-xs text-slate-400 font-medium">Alumnos Totales</span>
<div className="w-8 h-8 rounded-lg bg-cyan-500/10 text-cyan-400 flex items-center justify-center text-base">
<FiUsers />
</div>
</div>
<div className="text-2xl font-black text-white font-mono">34,500</div>
<div className="flex items-center gap-1 text-[11px] text-cyan-400 font-semibold">
<FiArrowUpRight /> +420 nuevos esta semana
</div>
</div>
<div className="p-5 rounded-2xl bg-slate-900/80 border border-slate-800 space-y-2">
<div className="flex items-center justify-between">
<span className="text-xs text-slate-400 font-medium">Calificación Media</span>
<div className="w-8 h-8 rounded-lg bg-amber-500/10 text-amber-400 flex items-center justify-center text-base">
<FiStar />
</div>
</div>
<div className="text-2xl font-black text-white font-mono">4.93 / 5.0</div>
<div className="text-[11px] text-slate-400">Basado en 4,210 reseñas</div>
</div>
<div className="p-5 rounded-2xl bg-slate-900/80 border border-slate-800 space-y-2">
<div className="flex items-center justify-between">
<span className="text-xs text-slate-400 font-medium">Cursos Publicados</span>
<div className="w-8 h-8 rounded-lg bg-violet-500/10 text-violet-400 flex items-center justify-center text-base">
<FiBookOpen />
</div>
</div>
<div className="text-2xl font-black text-white font-mono">6 Activos</div>
<div className="text-[11px] text-violet-300 font-medium">1 curso en borrador</div>
</div>
</div>
{/* Analytics Chart & Recent Activity Grid */}
<div className="grid grid-cols-1 lg:grid-cols-12 gap-8">
{/* Visual Sales Growth Chart */}
<div className="lg:col-span-7 p-6 rounded-2xl bg-slate-900/60 border border-slate-800 space-y-6">
<div className="flex items-center justify-between">
<div>
<h3 className="text-sm font-bold text-white">Evolución de Ventas Mensuales</h3>
<p className="text-xs text-slate-400">Ingresos brutos acumulados en USD</p>
</div>
<span className="text-xs font-bold text-emerald-400 bg-emerald-500/10 px-2.5 py-1 rounded-md border border-emerald-500/20">
+130% Crecimiento Semestral
</span>
</div>
{/* Bar chart mockup */}
<div className="h-48 flex items-end justify-between gap-3 pt-6 pb-2 border-b border-slate-800">
{salesHistory.map((item, idx) => (
<div key={idx} className="flex-1 flex flex-col items-center gap-2 group">
<span className="text-[10px] font-mono text-slate-400 opacity-0 group-hover:opacity-100 transition-opacity">
${item.amount}
</span>
<div
style={{ height: item.height }}
className="w-full max-w-[42px] bg-gradient-to-t from-violet-600 to-indigo-400 rounded-t-lg group-hover:from-cyan-500 group-hover:to-teal-300 transition-all duration-300 shadow-md shadow-violet-500/10"
/>
<span className="text-xs font-medium text-slate-400">{item.month}</span>
</div>
))}
</div>
</div>
{/* Real-time Sales Feed */}
<div className="lg:col-span-5 p-6 rounded-2xl bg-slate-900/60 border border-slate-800 space-y-4">
<div className="flex items-center justify-between">
<h3 className="text-sm font-bold text-white">Inscripciones Recientes</h3>
<span className="flex h-2 w-2 rounded-full bg-emerald-400 animate-pulse" />
</div>
<div className="divide-y divide-slate-800/80">
{recentEnrollments.map((enr, i) => (
<div key={i} className="py-3 flex items-center justify-between text-xs">
<div className="flex items-center gap-2.5">
<span className="text-base">{enr.country}</span>
<div>
<h4 className="font-bold text-white">{enr.student}</h4>
<p className="text-[11px] text-slate-400">{enr.course}</p>
</div>
</div>
<div className="text-right">
<span className="font-mono font-bold text-emerald-400">{enr.amount}</span>
<p className="text-[10px] text-slate-500">{enr.date}</p>
</div>
</div>
))}
</div>
</div>
</div>
</div>
</section>
);
};
+141
View File
@@ -0,0 +1,141 @@
import React from 'react';
import {
FiPlusCircle,
FiDollarSign,
FiTrendingUp,
} from 'react-icons/fi';
import { FaChalkboardTeacher } from 'react-icons/fa';
interface SellerHeroProps {
onOpenCreateCourse: () => void;
onScrollToCalculator: () => void;
}
export const SellerHero: React.FC<SellerHeroProps> = ({
onOpenCreateCourse,
onScrollToCalculator,
}) => {
return (
<div className="relative overflow-hidden bg-slate-950 pt-12 pb-16 lg:pt-20 lg:pb-24 border-b border-slate-800">
{/* Background ambient lighting */}
<div className="absolute top-0 right-1/4 w-96 h-96 bg-violet-600/20 rounded-full blur-3xl pointer-events-none" />
<div className="absolute bottom-0 left-1/4 w-96 h-96 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: Heading & Value props */}
<div className="lg:col-span-7 space-y-6 text-center lg:text-left">
<div className="inline-flex items-center gap-2 px-3.5 py-1.5 rounded-full text-xs font-semibold bg-violet-500/15 border border-violet-500/30 text-violet-300">
<FaChalkboardTeacher className="text-violet-400" />
<span>RyCourses Creator Studio</span>
</div>
<h1 className="text-3xl sm:text-5xl lg:text-6xl font-black text-white tracking-tight leading-[1.15]">
Convierte tu conocimiento en{' '}
<span className="text-transparent bg-clip-text bg-gradient-to-r from-violet-400 via-purple-300 to-indigo-300">
ingresos recurrentes.
</span>
</h1>
<p className="text-base sm:text-lg text-slate-300 max-w-2xl mx-auto lg:mx-0 leading-relaxed font-normal">
Únete a miles de instructores y profesionales que comparten su pasión, forman a la próxima generación de talento y generan ingresos mensuales vendiendo cursos de alta calidad.
</p>
{/* CTAs */}
<div className="flex flex-col sm:flex-row items-center justify-center lg:justify-start gap-4 pt-2">
<button
onClick={onOpenCreateCourse}
className="w-full sm:w-auto px-8 py-4 bg-gradient-to-r from-violet-600 to-indigo-600 hover:from-violet-500 hover:to-indigo-500 text-white font-bold rounded-xl text-base shadow-xl shadow-violet-600/30 hover:scale-[1.02] active:scale-[0.98] transition-all flex items-center justify-center gap-2 cursor-pointer"
>
<FiPlusCircle className="text-xl" />
<span>Crear y Publicar Curso</span>
</button>
<button
onClick={onScrollToCalculator}
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"
>
<FiDollarSign className="text-violet-400" />
<span>Calcular Mis Ganancias</span>
</button>
</div>
{/* Creator Perks */}
<div className="pt-4 grid grid-cols-2 sm:grid-cols-3 gap-4 text-left">
<div className="p-3 rounded-xl bg-slate-900/60 border border-slate-800">
<div className="text-lg font-bold text-violet-400 font-mono">Hasta 95%</div>
<div className="text-xs text-slate-400">Ganancias por venta</div>
</div>
<div className="p-3 rounded-xl bg-slate-900/60 border border-slate-800">
<div className="text-lg font-bold text-cyan-400 font-mono">$0 Costo</div>
<div className="text-xs text-slate-400">Para publicar cursos</div>
</div>
<div className="p-3 rounded-xl bg-slate-900/60 border border-slate-800 col-span-2 sm:col-span-1">
<div className="text-lg font-bold text-emerald-400 font-mono">Pagos Mensuales</div>
<div className="text-xs text-slate-400">Vía Stripe y PayPal</div>
</div>
</div>
</div>
{/* Right Column: Instructor Success Preview Card */}
<div className="lg:col-span-5">
<div className="rounded-2xl bg-gradient-to-b from-slate-900 to-slate-950 border border-violet-500/30 p-6 shadow-2xl space-y-6">
<div className="flex items-center justify-between pb-4 border-b border-slate-800">
<div className="flex items-center gap-3">
<div className="w-12 h-12 rounded-full bg-violet-500/20 text-violet-400 flex items-center justify-center text-xl">
<FiTrendingUp />
</div>
<div>
<h3 className="text-sm font-bold text-white">Panel de Ventas en Vivo</h3>
<p className="text-[11px] text-emerald-400 font-medium">● Actualizado hace 2 min</p>
</div>
</div>
<span className="text-xs px-2.5 py-1 rounded bg-violet-500/20 text-violet-300 font-bold">
Top Creador
</span>
</div>
{/* Stats highlights */}
<div className="grid grid-cols-2 gap-4">
<div className="p-4 rounded-xl bg-slate-900 border border-slate-800">
<span className="text-xs text-slate-400">Ventas Este Mes</span>
<p className="text-2xl font-black text-white font-mono mt-1">$4,850.00</p>
<span className="text-[10px] text-emerald-400 font-medium">↑ +24% vs mes anterior</span>
</div>
<div className="p-4 rounded-xl bg-slate-900 border border-slate-800">
<span className="text-xs text-slate-400">Nuevos Alumnos</span>
<p className="text-2xl font-black text-cyan-400 font-mono mt-1">+184</p>
<span className="text-[10px] text-slate-400 font-medium">12 países diferentes</span>
</div>
</div>
{/* Testimonial highlight */}
<div className="p-4 rounded-xl bg-slate-950 border border-slate-800/80 space-y-2">
<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-8 h-8 rounded-full object-cover"
/>
<div>
<h4 className="text-xs font-bold text-white">Ing. Carlos Mendoza</h4>
<p className="text-[10px] text-slate-400">Instructor de Desarrollo Web</p>
</div>
</div>
<p className="text-xs text-slate-300 italic">
"Publicar mi curso en RyCourses me permitió alcanzar a más de 30,000 alumnos en toda Latinoamérica y generar ingresos pasivos recurrentes."
</p>
</div>
</div>
</div>
</div>
</div>
</div>
);
};