Merge pull request #1 from Rynext/feat-home

se sube vistas
This commit is contained in:
Frank Rojas authored and GitHub committed 2026-08-24 21:11:28 -05:00
commit 2ae7091a3a
16 files changed
+1967 -229

No files matched your search

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 594 KiB

+75 -53
View File
@@ -1,22 +1,23 @@
import { useState } from 'react';
import type { Course, CartItem, ViewMode } from './types/course';
import { Routes, Route } from 'react-router';
import type { Course, CartItem } from './types/course';
import { mockCourses } from './data/mockCourses';
import { Navbar } from './components/layout/Navbar';
import { Footer } from './components/layout/Footer';
import { HeroSection } from './components/buyer/HeroSection';
import { CourseGrid } from './components/buyer/CourseGrid';
import { FeaturesBanner } from './components/buyer/FeaturesBanner';
import { HomeView } from './views/HomeView';
import { BuyerView } from './views/BuyerView';
import { SellerView } from './views/SellerView';
import { CourseDetailView } from './views/CourseDetailView';
import { CreateCourseView } from './views/CreateCourseView';
import { AdminDashboardView } from './views/AdminDashboardView';
import { LoginView } from './views/LoginView';
import { RegisterView } from './views/RegisterView';
import { CourseModal } from './components/buyer/CourseModal';
import { CartDrawer } from './components/buyer/CartDrawer';
import { SellerHero } from './components/seller/SellerHero';
import { SellerDashboard } from './components/seller/SellerDashboard';
import { SellerCalculator } from './components/seller/SellerCalculator';
import { SellerCourseList } from './components/seller/SellerCourseList';
import { CreateCourseModal } from './components/seller/CreateCourseModal';
import { FiCheckCircle} from 'react-icons/fi';
import { FiCheckCircle } from 'react-icons/fi';
export function App() {
const [viewMode, setViewMode] = useState<ViewMode>('buyer');
const [courses, setCourses] = useState<Course[]>(mockCourses);
const [cartItems, setCartItems] = useState<CartItem[]>([]);
const [searchQuery, setSearchQuery] = useState('');
@@ -72,20 +73,6 @@ export function App() {
showToast('Curso eliminado correctamente');
};
const handleScrollToCatalog = () => {
const el = document.getElementById('catalog-section');
if (el) {
el.scrollIntoView({ behavior: 'smooth' });
}
};
const handleScrollToCalculator = () => {
const el = document.getElementById('seller-calculator');
if (el) {
el.scrollIntoView({ behavior: 'smooth' });
}
};
const cartCourseIds = cartItems.map((i) => i.course.id);
return (
@@ -101,8 +88,6 @@ export function App() {
{/* Main Navigation Bar */}
<Navbar
viewMode={viewMode}
onToggleViewMode={setViewMode}
cartCount={cartItems.length}
onOpenCart={() => setIsCartOpen(true)}
onOpenCreateCourseModal={() => setIsCreateModalOpen(true)}
@@ -110,20 +95,30 @@ export function App() {
onSearchChange={setSearchQuery}
/>
{/* Main Content Area */}
{/* Main Views Container */}
<main className="flex-1">
{viewMode === 'buyer' ? (
/* ==================== MODO ESTUDIANTE (COMPRAR) ==================== */
<>
<HeroSection
onExploreClick={handleScrollToCatalog}
<Routes>
<Route
path="/"
element={
<HomeView
courses={courses}
cartCourseIds={cartCourseIds}
searchQuery={searchQuery}
onAddToCart={handleAddToCart}
onOpenPreview={(courseId) => {
const target = courses.find((c) => c.id === courseId) || courses[0];
setSelectedCourseForModal(target);
}}
onOpenDetails={(course) => setSelectedCourseForModal(course)}
onClearSearch={() => setSearchQuery('')}
/>
<CourseGrid
}
/>
<Route
path="/courses"
element={
<BuyerView
courses={courses}
cartCourseIds={cartCourseIds}
searchQuery={searchQuery}
@@ -131,31 +126,58 @@ export function App() {
onOpenDetails={(course) => setSelectedCourseForModal(course)}
onClearSearch={() => setSearchQuery('')}
/>
<FeaturesBanner />
</>
) : (
/* ==================== MODO INSTRUCTOR (VENDER) ==================== */
<>
<SellerHero
onOpenCreateCourse={() => setIsCreateModalOpen(true)}
onScrollToCalculator={handleScrollToCalculator}
}
/>
<SellerDashboard />
<SellerCourseList
<Route
path="/course/:courseId"
element={
<CourseDetailView
courses={courses}
onOpenCreateModal={() => setIsCreateModalOpen(true)}
cartCourseIds={cartCourseIds}
onAddToCart={handleAddToCart}
onBuyNow={handleBuyNow}
/>
}
/>
<Route
path="/admin"
element={
<AdminDashboardView
courses={courses}
onDeleteCourse={handleDeleteCourse}
onShowToast={showToast}
/>
}
/>
<Route
path="/seller"
element={
<SellerView
courses={courses}
onOpenCreateCourse={() => setIsCreateModalOpen(true)}
onPreviewCourse={(course) => setSelectedCourseForModal(course)}
onDeleteCourse={handleDeleteCourse}
/>
<SellerCalculator
onStartCourse={() => setIsCreateModalOpen(true)}
}
/>
</>
)}
<Route
path="/seller/create"
element={
<CreateCourseView
onCreateCourse={handleCreateCourse}
onShowToast={showToast}
/>
}
/>
<Route
path="/login"
element={<LoginView onShowToast={showToast} />}
/>
<Route
path="/register"
element={<RegisterView onShowToast={showToast} />}
/>
</Routes>
</main>
{/* Course Detail Modal */}
+11 -6
View File
@@ -1,4 +1,5 @@
import React from 'react';
import { useNavigate } from 'react-router';
import {
FiStar,
FiClock,
@@ -14,15 +15,19 @@ interface CourseCardProps {
course: Course;
isInCart: boolean;
onAddToCart: (course: Course) => void;
onOpenDetails: (course: Course) => void;
onOpenDetails?: (course: Course) => void;
}
export const CourseCard: React.FC<CourseCardProps> = ({
course,
isInCart,
onAddToCart,
onOpenDetails,
}) => {
const navigate = useNavigate();
const handleCourseClick = () => {
navigate(`/course/${course.id}`);
};
const discountPercent = Math.round(
((course.originalPrice - course.price) / course.originalPrice) * 100
);
@@ -47,7 +52,7 @@ export const CourseCard: React.FC<CourseCardProps> = ({
{/* Top Image Section */}
<div>
<div className="relative aspect-video w-full overflow-hidden bg-slate-800 cursor-pointer" onClick={() => onOpenDetails(course)}>
<div className="relative aspect-video w-full overflow-hidden bg-slate-800 cursor-pointer" onClick={handleCourseClick}>
<img
src={course.thumbnail}
alt={course.title}
@@ -79,11 +84,11 @@ export const CourseCard: React.FC<CourseCardProps> = ({
<button
onClick={(e) => {
e.stopPropagation();
onOpenDetails(course);
handleCourseClick();
}}
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
<FiEye /> Ver Curso Completo
</button>
</div>
@@ -112,7 +117,7 @@ export const CourseCard: React.FC<CourseCardProps> = ({
{/* Title */}
<h3
onClick={() => onOpenDetails(course)}
onClick={handleCourseClick}
className="text-base font-bold text-white group-hover:text-cyan-300 transition-colors line-clamp-2 cursor-pointer leading-snug"
>
{course.title}
+45
View File
@@ -0,0 +1,45 @@
import React from 'react';
interface BrandLogoProps {
size?: 'sm' | 'md' | 'lg';
showSubtitle?: boolean;
}
export const BrandLogo: React.FC<BrandLogoProps> = ({ size = 'md', showSubtitle = true }) => {
const iconSizes = {
sm: 'w-8 h-8',
md: 'w-10 h-10',
lg: 'w-12 h-12',
};
const textSizes = {
sm: 'text-lg',
md: 'text-xl',
lg: 'text-2xl',
};
return (
<div className="flex items-center gap-3 group select-none cursor-pointer">
{/* Official PNG Logo Image with sleek container styling */}
<div className={`relative ${iconSizes[size]} p-0.5 rounded-xl bg-slate-900 border border-slate-700/80 shadow-md shadow-cyan-950/40 group-hover:border-cyan-500/60 group-hover:scale-105 transition-all duration-300 overflow-hidden`}>
<img
src="/logo.png"
alt="RyCourses Emblem"
className="w-full h-full object-cover rounded-[10px]"
/>
</div>
{/* Typography */}
<div>
<div className="flex items-center gap-2">
<span className={`${textSizes[size]} font-extrabold tracking-tight text-white group-hover:text-cyan-300 transition-colors`}>
Ry<span className="text-transparent bg-clip-text bg-gradient-to-r from-cyan-400 to-blue-400">Courses</span>
</span>
</div>
{showSubtitle && (
<p className="text-[11px] text-slate-400 font-medium tracking-wide">Plataforma Educativa</p>
)}
</div>
</div>
);
};
+4 -56
View File
@@ -1,6 +1,5 @@
import React, { useState } from 'react';
import {
FiBookOpen,
FiMail,
FiCheckCircle,
FiGlobe,
@@ -10,6 +9,8 @@ import {
} from 'react-icons/fi';
import { FaTwitter, FaYoutube, FaGithub, FaLinkedin, FaDiscord } from 'react-icons/fa';
import { BrandLogo } from '../common/BrandLogo';
export const Footer: React.FC = () => {
const [email, setEmail] = useState('');
const [subscribed, setSubscribed] = useState(false);
@@ -25,52 +26,6 @@ export const Footer: React.FC = () => {
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">
@@ -78,15 +33,8 @@ export const Footer: React.FC = () => {
{/* 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 className="mb-4">
<BrandLogo size="md" />
</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.
+138 -113
View File
@@ -1,106 +1,101 @@
import React, { useState } from 'react';
import { useNavigate, useLocation } from 'react-router';
import {
FiSearch,
FiShoppingCart,
FiHeart,
FiBell,
FiMenu,
FiX,
FiBookOpen,
FiPlusCircle,
FiCompass,
FiHome,
FiGrid,
FiUser,
FiShield,
} from 'react-icons/fi';
import { FaGraduationCap, FaChalkboardTeacher } from 'react-icons/fa';
import type { ViewMode } from '../../types/course';
import { FaChalkboardTeacher } from 'react-icons/fa';
import { BrandLogo } from '../common/BrandLogo';
interface NavbarProps {
viewMode: ViewMode;
onToggleViewMode: (mode: ViewMode) => void;
cartCount: number;
onOpenCart: () => void;
onOpenCreateCourseModal: () => void;
onOpenCreateCourseModal?: () => void;
searchQuery: string;
onSearchChange: (query: string) => void;
}
export const Navbar: React.FC<NavbarProps> = ({
viewMode,
onToggleViewMode,
cartCount,
onOpenCart,
onOpenCreateCourseModal,
searchQuery,
onSearchChange,
}) => {
const navigate = useNavigate();
const location = useLocation();
const [mobileMenuOpen, setMobileMenuOpen] = useState(false);
const [categoryDropdown, setCategoryDropdown] = useState(false);
const currentPath = location.pathname;
const isSellerMode = currentPath === '/seller' || currentPath.startsWith('/seller/');
const handleSearchInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
onSearchChange(e.target.value);
if (currentPath !== '/courses' && e.target.value.trim() !== '') {
navigate('/courses');
}
};
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">
<header className="sticky top-0 z-40 w-full border-b border-slate-800/80 bg-slate-950/90 backdrop-blur-xl 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>
{/* 1. Left Brand & Main Links */}
<div className="flex items-center gap-6 shrink-0">
<div onClick={() => navigate('/')}>
<BrandLogo size="md" />
</div>
{/* Quick Categories dropdown (Desktop) */}
{viewMode === 'buyer' && (
<div className="relative hidden lg:block">
{/* Quick Nav Links */}
<nav className="hidden lg:flex items-center gap-1">
<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"
onClick={() => navigate('/')}
className={`flex items-center gap-1.5 px-3 py-2 rounded-xl text-xs font-semibold transition-all ${
currentPath === '/'
? 'bg-slate-800 text-cyan-400 border border-cyan-500/30'
: 'text-slate-300 hover:text-white hover:bg-slate-900'
}`}
>
<FiCompass className="text-cyan-400" />
<span>Explorar</span>
<FiHome />
<span>Inicio</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"
onClick={() => navigate('/courses')}
className={`flex items-center gap-1.5 px-3 py-2 rounded-xl text-xs font-semibold transition-all ${
currentPath === '/courses'
? 'bg-slate-800 text-cyan-400 border border-cyan-500/30'
: 'text-slate-300 hover:text-white hover:bg-slate-900'
}`}
>
{cat}
<FiGrid />
<span>Catálogo</span>
</button>
))}
</div>
)}
</div>
)}
<button
onClick={() => navigate('/admin')}
className={`flex items-center gap-1.5 px-3 py-2 rounded-xl text-xs font-semibold transition-all ${
currentPath === '/admin'
? 'bg-slate-800 text-cyan-400 border border-cyan-500/30'
: 'text-slate-300 hover:text-white hover:bg-slate-900'
}`}
>
<FiShield />
<span>Admin</span>
</button>
</nav>
</div>
{/* Search bar (Desktop) */}
{viewMode === 'buyer' && (
<div className="hidden md:flex flex-1 max-w-md mx-4">
{/* 2. Spacious Center Search Bar */}
{!isSellerMode && (
<div className="hidden md:flex flex-1 max-w-xl mx-2">
<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" />
@@ -108,98 +103,105 @@ export const Navbar: React.FC<NavbarProps> = ({
<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"
onChange={handleSearchInputChange}
placeholder="¿Qué quieres aprender hoy? (React, Python, Figma...)"
className="w-full pl-10 pr-12 py-2.5 bg-slate-900/90 border border-slate-700/80 rounded-xl text-xs 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
⌘K
</kbd>
</div>
</div>
</div>
)}
{/* View Mode Switcher (Estudiante vs Instructor) & User Actions */}
<div className="flex items-center gap-3">
{/* 3. Right Actions & Role Switcher */}
<div className="flex items-center gap-2.5 shrink-0">
{/* Mode Switcher pill */}
<div className="bg-slate-900/90 p-1 rounded-xl border border-slate-800 flex items-center shadow-inner">
{/* Buyer vs Seller Switcher pill */}
<div className="bg-slate-900 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'
onClick={() => navigate('/courses')}
className={`flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-xs font-semibold transition-all ${
!isSellerMode
? '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" />
<FiGrid className="text-xs" />
<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'
onClick={() => navigate('/seller')}
className={`flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-xs font-semibold transition-all ${
isSellerMode
? '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" />
<FaChalkboardTeacher className="text-xs" />
<span className="hidden sm:inline">Vender</span>
</button>
</div>
{/* If seller mode: Create Course button */}
{viewMode === 'seller' && (
{isSellerMode && (
<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"
onClick={() => navigate('/seller/create')}
className="hidden sm:flex items-center gap-1.5 px-3.5 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 transition-all"
>
<FiPlusCircle className="text-base" />
<FiPlusCircle className="text-sm" />
<span>Crear Curso</span>
</button>
)}
{/* Cart button (buyer mode) */}
{viewMode === 'buyer' && (
{/* Cart button (Buyer mode) */}
{!isSellerMode && (
<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"
className="relative p-2.5 text-slate-300 hover:text-white bg-slate-900 hover:bg-slate-800 border border-slate-700/80 rounded-xl transition"
aria-label="Carrito de compras"
>
<FiShoppingCart className="text-lg" />
<FiShoppingCart className="text-base" />
{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">
<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 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" />
{/* Login & Register Buttons */}
<div className="hidden xl:flex items-center gap-2 pl-2 border-l border-slate-800">
<button
onClick={() => navigate('/login')}
className="px-3 py-1.5 text-xs font-semibold text-slate-300 hover:text-white transition"
>
Iniciar Sesión
</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
onClick={() => navigate('/register')}
className="px-3.5 py-1.5 rounded-xl bg-cyan-500/10 hover:bg-cyan-500/20 text-cyan-400 font-semibold text-xs border border-cyan-500/30 transition"
>
Registrarse
</button>
</div>
{/* User Profile Avatar */}
<div className="flex items-center gap-2 pl-2 border-l border-slate-800">
<div className="relative cursor-pointer">
<div
onClick={() => navigate('/login')}
className="relative cursor-pointer group"
title="Mi Cuenta"
>
<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"
className="w-9 h-9 rounded-full object-cover ring-2 ring-indigo-500/50 group-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>
@@ -223,34 +225,57 @@ export const Navbar: React.FC<NavbarProps> = ({
<input
type="text"
value={searchQuery}
onChange={(e) => onSearchChange(e.target.value)}
onChange={handleSearchInputChange}
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">
<div className="grid grid-cols-3 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'
onClick={() => { navigate('/'); setMobileMenuOpen(false); }}
className={`flex items-center justify-center gap-1.5 p-2 rounded-lg text-xs font-semibold ${
currentPath === '/' ? 'bg-blue-600 text-white' : 'bg-slate-800 text-slate-300'
}`}
>
<FaGraduationCap /> Modo Comprador
<FiHome /> Inicio
</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'
onClick={() => { navigate('/courses'); setMobileMenuOpen(false); }}
className={`flex items-center justify-center gap-1.5 p-2 rounded-lg text-xs font-semibold ${
currentPath === '/courses' ? 'bg-cyan-600 text-white' : 'bg-slate-800 text-slate-300'
}`}
>
<FaChalkboardTeacher /> Modo Vendedor
<FiGrid /> Catálogo
</button>
<button
onClick={() => { navigate('/seller'); setMobileMenuOpen(false); }}
className={`flex items-center justify-center gap-1.5 p-2 rounded-lg text-xs font-semibold ${
currentPath === '/seller' ? 'bg-violet-600 text-white' : 'bg-slate-800 text-slate-300'
}`}
>
<FaChalkboardTeacher /> Vender
</button>
</div>
{viewMode === 'seller' && (
<div className="grid grid-cols-2 gap-2 pt-2 border-t border-slate-800">
<button
onClick={() => { onOpenCreateCourseModal(); setMobileMenuOpen(false); }}
onClick={() => { navigate('/login'); setMobileMenuOpen(false); }}
className="w-full py-2.5 bg-slate-800 text-white font-semibold rounded-lg text-xs flex items-center justify-center gap-2"
>
<FiUser /> Iniciar Sesión
</button>
<button
onClick={() => { navigate('/register'); setMobileMenuOpen(false); }}
className="w-full py-2.5 bg-cyan-600 text-white font-semibold rounded-lg text-xs flex items-center justify-center gap-2"
>
Registrarse
</button>
</div>
{isSellerMode && (
<button
onClick={() => { navigate('/seller/create'); 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
@@ -4,7 +4,6 @@ import {
FiUsers,
FiStar,
FiBookOpen,
FiTrendingUp,
FiArrowUpRight,
FiActivity
} from 'react-icons/fi';
+3
View File
@@ -1,10 +1,13 @@
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import { BrowserRouter } from 'react-router'
import './index.css'
import App from './App.tsx'
createRoot(document.getElementById('root')!).render(
<StrictMode>
<BrowserRouter>
<App />
</BrowserRouter>
</StrictMode>,
)
+441
View File
@@ -0,0 +1,441 @@
import React, { useState } from 'react';
import type { Course } from '../types/course';
import {
FiDollarSign,
FiUsers,
FiBookOpen,
FiTrendingUp,
FiCheckCircle,
FiTrash2,
FiShield,
FiSearch,
FiFilter,
FiActivity,
FiPieChart,
FiSettings
} from 'react-icons/fi';
interface AdminDashboardViewProps {
courses: Course[];
onDeleteCourse: (courseId: string) => void;
onShowToast: (msg: string) => void;
}
export const AdminDashboardView: React.FC<AdminDashboardViewProps> = ({
courses,
onDeleteCourse,
onShowToast,
}) => {
const [activeTab, setActiveTab] = useState<'courses' | 'users' | 'finance' | 'settings'>('courses');
const [statusFilter, setStatusFilter] = useState<'all' | 'published' | 'pending'>('all');
const [searchTerm, setSearchTerm] = useState('');
// Course status state management for demonstration
const [courseStatuses, setCourseStatuses] = useState<Record<string, 'published' | 'pending' | 'rejected'>>({});
const getStatus = (courseId: string) => {
return courseStatuses[courseId] || 'published';
};
const toggleCourseStatus = (courseId: string, title: string) => {
const current = getStatus(courseId);
const nextStatus = current === 'published' ? 'pending' : 'published';
setCourseStatuses({ ...courseStatuses, [courseId]: nextStatus });
onShowToast(`Estado del curso "${title.slice(0, 25)}..." cambiado a ${nextStatus === 'published' ? 'Publicado' : 'Pendiente'}`);
};
// Mock platform users
const [users] = useState([
{ id: 'u-1', name: 'Ing. Carlos Mendoza', email: '[email protected]', role: 'Instructor', courses: 4, joined: '12 Ene 2026', status: 'Activo' },
{ id: 'u-2', name: 'Dra. Elena Torres', email: '[email protected]', role: 'Instructor', courses: 2, joined: '04 Feb 2026', status: 'Activo' },
{ id: 'u-3', name: 'Marcos Rodriguez', email: '[email protected]', role: 'Estudiante', courses: 5, joined: '18 Feb 2026', status: 'Activo' },
{ id: 'u-4', name: 'Sofía Martinez', email: '[email protected]', role: 'Estudiante', courses: 2, joined: '20 Feb 2026', status: 'Activo' },
{ id: 'u-5', name: 'Admin Principal', email: '[email protected]', role: 'Administrador', courses: 0, joined: '01 Ene 2026', status: 'Activo' },
]);
const filteredCourses = courses.filter((c) => {
const matchesSearch = c.title.toLowerCase().includes(searchTerm.toLowerCase()) || c.instructor.name.toLowerCase().includes(searchTerm.toLowerCase());
const currentStatus = getStatus(c.id);
const matchesStatus = statusFilter === 'all' || currentStatus === statusFilter;
return matchesSearch && matchesStatus;
});
return (
<div className="min-h-screen bg-slate-950 text-slate-100 pb-20">
{/* Top Banner */}
<div className="bg-gradient-to-r from-slate-900 via-indigo-950 to-slate-900 border-b border-slate-800 py-8">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="flex flex-col md:flex-row md:items-center justify-between gap-4">
<div>
<div className="inline-flex items-center gap-2 px-3 py-1 rounded-full text-xs font-bold bg-violet-500/20 text-violet-300 border border-violet-500/30 mb-2">
<FiShield /> Panel General de Administración
</div>
<h1 className="text-2xl sm:text-4xl font-extrabold text-white">
Control & Moderación RyCourses
</h1>
<p className="text-xs sm:text-sm text-slate-400 mt-1">
Gestión centralizada de catálogo, usuarios, instructores y métricas financieras.
</p>
</div>
<div className="flex items-center gap-3">
<span className="text-xs text-emerald-400 bg-emerald-500/10 border border-emerald-500/20 px-3 py-1.5 rounded-xl font-semibold flex items-center gap-1.5">
<span className="w-2 h-2 rounded-full bg-emerald-400 animate-pulse" /> Servidores Normales (99.9% Uptime)
</span>
</div>
</div>
</div>
</div>
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8 space-y-8">
{/* KPI Platform Stat 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 shadow-xl">
<div className="flex items-center justify-between">
<span className="text-xs text-slate-400 font-medium">Ingresos Totales Plataforma</span>
<div className="w-9 h-9 rounded-xl bg-emerald-500/10 text-emerald-400 flex items-center justify-center text-lg">
<FiDollarSign />
</div>
</div>
<div className="text-2xl font-black text-white font-mono">$184,920.00</div>
<div className="text-[11px] text-emerald-400 font-semibold flex items-center gap-1">
<FiTrendingUp /> +24.8% en ventas este mes
</div>
</div>
<div className="p-5 rounded-2xl bg-slate-900/80 border border-slate-800 space-y-2 shadow-xl">
<div className="flex items-center justify-between">
<span className="text-xs text-slate-400 font-medium">Usuarios Registrados</span>
<div className="w-9 h-9 rounded-xl bg-cyan-500/10 text-cyan-400 flex items-center justify-center text-lg">
<FiUsers />
</div>
</div>
<div className="text-2xl font-black text-white font-mono">54,320</div>
<div className="text-[11px] text-slate-400">
<strong className="text-cyan-400">1,240</strong> Instructores activos
</div>
</div>
<div className="p-5 rounded-2xl bg-slate-900/80 border border-slate-800 space-y-2 shadow-xl">
<div className="flex items-center justify-between">
<span className="text-xs text-slate-400 font-medium">Cursos en Catálogo</span>
<div className="w-9 h-9 rounded-xl bg-violet-500/10 text-violet-400 flex items-center justify-center text-lg">
<FiBookOpen />
</div>
</div>
<div className="text-2xl font-black text-white font-mono">{courses.length} Publicados</div>
<div className="text-[11px] text-amber-400 font-medium">2 pendientes de aprobación</div>
</div>
<div className="p-5 rounded-2xl bg-slate-900/80 border border-slate-800 space-y-2 shadow-xl">
<div className="flex items-center justify-between">
<span className="text-xs text-slate-400 font-medium">Comisión Neta (20%)</span>
<div className="w-9 h-9 rounded-xl bg-amber-500/10 text-amber-400 flex items-center justify-center text-lg">
<FiPieChart />
</div>
</div>
<div className="text-2xl font-black text-white font-mono">$36,984.00</div>
<div className="text-[11px] text-slate-400">Generado para la plataforma</div>
</div>
</div>
{/* Tab Navigation */}
<div className="flex border-b border-slate-800 space-x-4 text-xs font-bold">
<button
onClick={() => setActiveTab('courses')}
className={`pb-3.5 px-2 flex items-center gap-2 transition-colors border-b-2 ${
activeTab === 'courses'
? 'text-cyan-400 border-cyan-400'
: 'text-slate-400 border-transparent hover:text-white'
}`}
>
<FiBookOpen /> Gestor de Cursos ({courses.length})
</button>
<button
onClick={() => setActiveTab('users')}
className={`pb-3.5 px-2 flex items-center gap-2 transition-colors border-b-2 ${
activeTab === 'users'
? 'text-cyan-400 border-cyan-400'
: 'text-slate-400 border-transparent hover:text-white'
}`}
>
<FiUsers /> Usuarios e Instructores ({users.length})
</button>
<button
onClick={() => setActiveTab('finance')}
className={`pb-3.5 px-2 flex items-center gap-2 transition-colors border-b-2 ${
activeTab === 'finance'
? 'text-cyan-400 border-cyan-400'
: 'text-slate-400 border-transparent hover:text-white'
}`}
>
<FiDollarSign /> Finanzas & Pagos
</button>
<button
onClick={() => setActiveTab('settings')}
className={`pb-3.5 px-2 flex items-center gap-2 transition-colors border-b-2 ${
activeTab === 'settings'
? 'text-cyan-400 border-cyan-400'
: 'text-slate-400 border-transparent hover:text-white'
}`}
>
<FiSettings /> Ajustes Globales
</button>
</div>
{/* TAB 1: Gestor de Cursos */}
{activeTab === 'courses' && (
<div className="space-y-6">
{/* Filter and Search Bar */}
<div className="flex flex-col sm:flex-row items-center justify-between gap-4 bg-slate-900 p-4 rounded-2xl border border-slate-800">
<div className="relative flex-1 w-full max-w-md">
<FiSearch className="absolute left-3.5 top-3 text-slate-500" />
<input
type="text"
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
placeholder="Buscar curso por título o instructor..."
className="w-full pl-10 pr-4 py-2 bg-slate-950 border border-slate-800 rounded-xl text-xs text-white placeholder-slate-500 focus:outline-none focus:ring-2 focus:ring-cyan-500"
/>
</div>
<div className="flex items-center gap-2 w-full sm:w-auto">
<FiFilter className="text-slate-400 text-sm" />
<select
value={statusFilter}
onChange={(e) => setStatusFilter(e.target.value as any)}
className="bg-slate-950 border border-slate-800 rounded-xl px-3 py-2 text-xs text-slate-200 focus:outline-none focus:ring-2 focus:ring-cyan-500"
>
<option value="all">Todos los estados</option>
<option value="published">Publicados</option>
<option value="pending">Pendientes de revisión</option>
</select>
</div>
</div>
{/* Courses Management Table */}
<div className="bg-slate-900 border border-slate-800 rounded-2xl overflow-hidden shadow-xl">
<div className="overflow-x-auto">
<table className="w-full text-left text-xs text-slate-300">
<thead className="bg-slate-950 text-slate-400 uppercase tracking-wider font-semibold border-b border-slate-800">
<tr>
<th className="p-4">Curso</th>
<th className="p-4">Categoría</th>
<th className="p-4">Instructor</th>
<th className="p-4">Precio</th>
<th className="p-4">Estudiantes</th>
<th className="p-4">Estado</th>
<th className="p-4 text-right">Acciones Admin</th>
</tr>
</thead>
<tbody className="divide-y divide-slate-800/80">
{filteredCourses.map((c) => {
const status = getStatus(c.id);
return (
<tr key={c.id} className="hover:bg-slate-800/40 transition">
<td className="p-4">
<div className="flex items-center gap-3">
<img
src={c.thumbnail}
alt={c.title}
className="w-12 h-8 rounded object-cover border border-slate-700 shrink-0"
/>
<div>
<h4 className="font-bold text-white max-w-xs line-clamp-1">{c.title}</h4>
<span className="text-[10px] text-slate-500">ID: {c.id}</span>
</div>
</div>
</td>
<td className="p-4">
<span className="px-2 py-0.5 rounded bg-slate-800 text-cyan-400 font-medium">
{c.category}
</span>
</td>
<td className="p-4">
<div className="flex items-center gap-2">
<img
src={c.instructor.avatar}
alt={c.instructor.name}
className="w-6 h-6 rounded-full object-cover"
/>
<span className="font-medium text-slate-200">{c.instructor.name}</span>
</div>
</td>
<td className="p-4 font-mono font-bold text-white">
${c.price.toFixed(2)}
</td>
<td className="p-4 font-mono text-slate-300">
{c.studentsCount.toLocaleString()}
</td>
<td className="p-4">
{status === 'published' ? (
<span className="px-2.5 py-1 rounded-full bg-emerald-500/10 text-emerald-400 border border-emerald-500/20 text-[10px] font-bold inline-flex items-center gap-1">
<FiCheckCircle /> Publicado
</span>
) : (
<span className="px-2.5 py-1 rounded-full bg-amber-500/10 text-amber-400 border border-amber-500/20 text-[10px] font-bold inline-flex items-center gap-1">
<FiActivity /> En Revisión
</span>
)}
</td>
<td className="p-4 text-right">
<div className="flex items-center justify-end gap-2">
<button
onClick={() => toggleCourseStatus(c.id, c.title)}
className={`px-2.5 py-1.5 rounded-lg text-[11px] font-bold border transition ${
status === 'published'
? 'bg-slate-800 hover:bg-slate-700 text-amber-400 border-amber-500/30'
: 'bg-emerald-600 hover:bg-emerald-500 text-white border-emerald-400'
}`}
>
{status === 'published' ? 'Pausar' : 'Aprobar'}
</button>
<button
onClick={() => {
onDeleteCourse(c.id);
}}
className="p-2 rounded-lg bg-slate-800 hover:bg-rose-950 text-slate-400 hover:text-rose-400 border border-slate-700 transition"
title="Eliminar curso de la plataforma"
>
<FiTrash2 />
</button>
</div>
</td>
</tr>
);
})}
</tbody>
</table>
</div>
</div>
</div>
)}
{/* TAB 2: Usuarios & Instructores */}
{activeTab === 'users' && (
<div className="bg-slate-900 border border-slate-800 rounded-2xl overflow-hidden shadow-xl">
<div className="p-4 border-b border-slate-800 flex items-center justify-between">
<h3 className="font-bold text-white text-sm">Directorio General de Cuentas</h3>
<span className="text-xs text-slate-400">Total: {users.length} registrados</span>
</div>
<table className="w-full text-left text-xs text-slate-300">
<thead className="bg-slate-950 text-slate-400 uppercase tracking-wider font-semibold border-b border-slate-800">
<tr>
<th className="p-4">Usuario</th>
<th className="p-4">Correo</th>
<th className="p-4">Rol</th>
<th className="p-4">Cursos Asoc.</th>
<th className="p-4">Registro</th>
<th className="p-4">Estado</th>
</tr>
</thead>
<tbody className="divide-y divide-slate-800/80">
{users.map((u) => (
<tr key={u.id} className="hover:bg-slate-800/40 transition">
<td className="p-4 font-bold text-white">{u.name}</td>
<td className="p-4 text-slate-400 font-mono">{u.email}</td>
<td className="p-4">
<span className={`px-2.5 py-1 rounded-md text-[10px] font-bold ${
u.role === 'Administrador'
? 'bg-rose-500/20 text-rose-300 border border-rose-500/30'
: u.role === 'Instructor'
? 'bg-violet-500/20 text-violet-300 border border-violet-500/30'
: 'bg-blue-500/20 text-blue-300 border border-blue-500/30'
}`}>
{u.role}
</span>
</td>
<td className="p-4 font-mono">{u.courses}</td>
<td className="p-4 text-slate-400">{u.joined}</td>
<td className="p-4">
<span className="text-emerald-400 font-semibold">{u.status}</span>
</td>
</tr>
))}
</tbody>
</table>
</div>
)}
{/* TAB 3: Finanzas */}
{activeTab === 'finance' && (
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div className="bg-slate-900 border border-slate-800 rounded-2xl p-6 space-y-4">
<h3 className="font-bold text-white text-base">Desglose de Repartición Financiera</h3>
<div className="space-y-3 text-xs">
<div className="flex justify-between p-3 bg-slate-950 rounded-xl border border-slate-800">
<span className="text-slate-400">Ventas Brutas Acumuladas:</span>
<span className="font-mono font-bold text-white">$184,920.00</span>
</div>
<div className="flex justify-between p-3 bg-slate-950 rounded-xl border border-slate-800">
<span className="text-slate-400">Pago a Instructores (80%):</span>
<span className="font-mono font-bold text-emerald-400">$147,936.00</span>
</div>
<div className="flex justify-between p-3 bg-slate-950 rounded-xl border border-slate-800">
<span className="text-slate-400">Comisión Neta Plataforma (20%):</span>
<span className="font-mono font-bold text-cyan-400">$36,984.00</span>
</div>
</div>
</div>
<div className="bg-slate-900 border border-slate-800 rounded-2xl p-6 space-y-4">
<h3 className="font-bold text-white text-base">Métodos de Pago Integrados</h3>
<p className="text-xs text-slate-400">
Stripe & PayPal operando de forma continua para procesamiento automático de cobros y giros a instructores.
</p>
<div className="p-4 rounded-xl bg-emerald-500/10 border border-emerald-500/20 text-emerald-400 text-xs font-semibold flex items-center gap-2">
<FiCheckCircle /> Pagos automáticos semanales habilitados.
</div>
</div>
</div>
)}
{/* TAB 4: Ajustes Globales */}
{activeTab === 'settings' && (
<div className="bg-slate-900 border border-slate-800 rounded-2xl p-6 space-y-6 max-w-2xl">
<h3 className="font-bold text-white text-base">Configuración de Plataforma</h3>
<div className="space-y-4 text-xs">
<div>
<label className="block text-slate-300 font-semibold mb-1">Nombre de la Aplicación</label>
<input type="text" defaultValue="RyCourses" className="w-full px-4 py-2.5 bg-slate-950 border border-slate-800 rounded-xl text-white font-bold" />
</div>
<div>
<label className="block text-slate-300 font-semibold mb-1">Porcentaje de Comisión Base (%)</label>
<input type="number" defaultValue={20} className="w-full px-4 py-2.5 bg-slate-950 border border-slate-800 rounded-xl text-white font-mono" />
</div>
<div className="pt-2">
<button
type="button"
onClick={() => onShowToast('Ajustes guardados correctamente.')}
className="px-6 py-3 bg-gradient-to-r from-blue-600 to-cyan-500 text-white font-bold rounded-xl text-xs shadow-lg shadow-cyan-500/20"
>
Guardar Cambios
</button>
</div>
</div>
</div>
)}
</div>
</div>
);
};
+51
View File
@@ -0,0 +1,51 @@
import React from 'react';
import type { Course } from '../types/course';
import { CourseGrid } from '../components/buyer/CourseGrid';
import { FeaturesBanner } from '../components/buyer/FeaturesBanner';
interface BuyerViewProps {
courses: Course[];
cartCourseIds: string[];
searchQuery: string;
onAddToCart: (course: Course) => void;
onOpenDetails: (course: Course) => void;
onClearSearch: () => void;
}
export const BuyerView: React.FC<BuyerViewProps> = ({
courses,
cartCourseIds,
searchQuery,
onAddToCart,
onOpenDetails,
onClearSearch,
}) => {
return (
<div className="py-6 space-y-12">
{/* Header section for Catalog View */}
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="bg-gradient-to-r from-slate-900 via-indigo-950/60 to-slate-900 border border-slate-800/80 rounded-2xl p-6 sm:p-8 shadow-xl">
<h1 className="text-2xl sm:text-4xl font-extrabold text-white">
Catálogo de Cursos
</h1>
<p className="text-slate-400 text-xs sm:text-sm mt-2 max-w-2xl">
Explora nuestra biblioteca de cursos diseñados para ayudarte a dominar las mejores tecnologías y potenciar tu carrera profesional.
</p>
</div>
</div>
{/* Main Course Grid & Filtering */}
<CourseGrid
courses={courses}
cartCourseIds={cartCourseIds}
searchQuery={searchQuery}
onAddToCart={onAddToCart}
onOpenDetails={onOpenDetails}
onClearSearch={onClearSearch}
/>
{/* Features Banner */}
<FeaturesBanner />
</div>
);
};
+346
View File
@@ -0,0 +1,346 @@
import React, { useState } from 'react';
import { useParams, useNavigate } from 'react-router';
import type { Course } from '../types/course';
import {
FiStar,
FiClock,
FiBook,
FiGlobe,
FiCheck,
FiShoppingCart,
FiPlay,
FiChevronDown,
FiChevronUp,
FiAward,
FiShield,
FiArrowLeft
} from 'react-icons/fi';
interface CourseDetailViewProps {
courses: Course[];
cartCourseIds: string[];
onAddToCart: (course: Course) => void;
onBuyNow: (course: Course) => void;
}
export const CourseDetailView: React.FC<CourseDetailViewProps> = ({
courses,
cartCourseIds,
onAddToCart,
onBuyNow,
}) => {
const { courseId } = useParams<{ courseId: string }>();
const navigate = useNavigate();
const course = courses.find((c) => c.id === courseId) || courses[0];
const isInCart = cartCourseIds.includes(course.id);
const [activeTab, setActiveTab] = useState<'overview' | 'curriculum' | 'instructor' | 'reviews'>('overview');
const [expandedModules, setExpandedModules] = useState<string[]>([course.modules[0]?.id || 'm-1']);
const toggleModule = (id: string) => {
if (expandedModules.includes(id)) {
setExpandedModules(expandedModules.filter((mId) => mId !== id));
} else {
setExpandedModules([...expandedModules, id]);
}
};
return (
<div className="min-h-screen bg-slate-950 pb-20">
{/* Top Breadcrumb & Back Navigation */}
<div className="bg-slate-900/60 border-b border-slate-800 py-3">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<button
onClick={() => navigate('/courses')}
className="inline-flex items-center gap-2 text-xs font-semibold text-slate-400 hover:text-cyan-400 transition"
>
<FiArrowLeft /> Regresar al Catálogo
</button>
</div>
</div>
{/* Main Course Hero & Details */}
<div className="bg-gradient-to-b from-slate-900 via-slate-950 to-slate-950 border-b border-slate-800 py-10 lg:py-14">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="grid grid-cols-1 lg:grid-cols-12 gap-10">
{/* Left Info Column */}
<div className="lg:col-span-8 space-y-6">
<div className="flex flex-wrap items-center gap-2 text-xs">
<span className="px-3 py-1 rounded-full font-bold bg-cyan-500/10 text-cyan-400 border border-cyan-500/20">
{course.category}
</span>
<span className="px-2.5 py-1 rounded bg-slate-800 text-slate-300 font-medium">
Nivel: {course.level}
</span>
{course.badge && (
<span className="px-2.5 py-1 rounded bg-amber-500 text-slate-950 font-black">
🔥 {course.badge}
</span>
)}
</div>
<h1 className="text-3xl sm:text-4xl font-extrabold text-white leading-tight">
{course.title}
</h1>
<p className="text-slate-300 text-base sm:text-lg leading-relaxed">
{course.subtitle}
</p>
{/* Rating & Instructor summary */}
<div className="flex flex-wrap items-center gap-4 text-xs text-slate-300 pt-1">
<div className="flex items-center gap-1.5 font-bold text-amber-400">
<span>{course.rating.toFixed(1)}</span>
<div className="flex">
{[...Array(5)].map((_, i) => (
<FiStar key={i} className="fill-amber-400 text-amber-400 text-sm" />
))}
</div>
<span className="text-slate-400 font-normal">({course.reviewsCount} reseñas)</span>
</div>
<span className="text-slate-600">•</span>
<span className="text-slate-300">{course.studentsCount.toLocaleString()} alumnos inscritos</span>
</div>
<div className="flex flex-wrap items-center gap-6 text-xs text-slate-400 pt-2 border-t border-slate-800/80">
<div className="flex items-center gap-2">
<img
src={course.instructor.avatar}
alt={course.instructor.name}
className="w-8 h-8 rounded-full object-cover ring-1 ring-cyan-400"
/>
<span>Creado por <strong className="text-white font-medium">{course.instructor.name}</strong></span>
</div>
<div className="flex items-center gap-1.5">
<FiGlobe className="text-cyan-400" />
<span>{course.language}</span>
</div>
<div className="flex items-center gap-1.5">
<FiClock className="text-cyan-400" />
<span>Última actualización: {course.updatedAt}</span>
</div>
</div>
</div>
{/* Right Sticky Purchase Card */}
<div className="lg:col-span-4">
<div className="bg-slate-900 border border-slate-700/80 rounded-3xl p-6 shadow-2xl space-y-6 sticky top-24">
{/* Thumbnail Preview */}
<div className="relative rounded-2xl overflow-hidden aspect-video bg-slate-800 group">
<img
src={course.thumbnail}
alt={course.title}
className="w-full h-full object-cover"
/>
<div className="absolute inset-0 bg-slate-950/40 flex items-center justify-center">
<div className="w-14 h-14 rounded-full bg-cyan-500 text-slate-950 flex items-center justify-center text-xl shadow-xl shadow-cyan-500/40 group-hover:scale-110 transition">
<FiPlay className="ml-1" />
</div>
</div>
</div>
{/* Price section */}
<div className="space-y-1">
<div className="flex items-baseline gap-3">
<span className="text-3xl font-black text-white font-mono">
${course.price.toFixed(2)}
</span>
<span className="text-sm text-slate-500 line-through font-mono">
${course.originalPrice.toFixed(2)}
</span>
</div>
<p className="text-xs text-emerald-400 font-semibold">
¡Ahorra un {Math.round(((course.originalPrice - course.price) / course.originalPrice) * 100)}%!
</p>
</div>
{/* Buttons */}
<div className="space-y-3">
<button
onClick={() => onBuyNow(course)}
className="w-full py-3.5 bg-gradient-to-r from-blue-600 to-cyan-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 text-center"
>
Comprar Ahora
</button>
<button
onClick={() => onAddToCart(course)}
className={`w-full py-3.5 border rounded-xl text-sm font-bold transition-all flex items-center justify-center gap-2 ${
isInCart
? 'bg-emerald-500/20 border-emerald-500/40 text-emerald-300'
: 'bg-slate-800 hover:bg-slate-700 border-slate-700 text-white'
}`}
>
{isInCart ? <FiCheck /> : <FiShoppingCart />}
<span>{isInCart ? 'En el Carrito' : 'Añadir al Carrito'}</span>
</button>
</div>
{/* Benefits checklist */}
<div className="space-y-2.5 pt-4 border-t border-slate-800 text-xs text-slate-300">
<h4 className="font-bold text-white uppercase tracking-wider text-[11px]">Este curso incluye:</h4>
<div className="flex items-center gap-2">
<FiClock className="text-cyan-400 shrink-0" />
<span>{course.duration} de video bajo demanda</span>
</div>
<div className="flex items-center gap-2">
<FiBook className="text-cyan-400 shrink-0" />
<span>{course.lessonsCount} lecciones descargables</span>
</div>
<div className="flex items-center gap-2">
<FiAward className="text-cyan-400 shrink-0" />
<span>Certificado de finalización</span>
</div>
<div className="flex items-center gap-2">
<FiShield className="text-emerald-400 shrink-0" />
<span>Garantía de reembolso de 30 días</span>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
{/* Main Content Tabs & Curriculum */}
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-12">
<div className="lg:w-8/12 space-y-10">
{/* Navigation Tabs */}
<div className="flex border-b border-slate-800 space-x-6 text-sm font-semibold">
<button
onClick={() => setActiveTab('overview')}
className={`pb-3 transition-colors ${
activeTab === 'overview'
? 'text-cyan-400 border-b-2 border-cyan-400'
: 'text-slate-400 hover:text-white'
}`}
>
Descripción
</button>
<button
onClick={() => setActiveTab('curriculum')}
className={`pb-3 transition-colors ${
activeTab === 'curriculum'
? 'text-cyan-400 border-b-2 border-cyan-400'
: 'text-slate-400 hover:text-white'
}`}
>
Temario del Curso ({course.modules.length} módulos)
</button>
<button
onClick={() => setActiveTab('instructor')}
className={`pb-3 transition-colors ${
activeTab === 'instructor'
? 'text-cyan-400 border-b-2 border-cyan-400'
: 'text-slate-400 hover:text-white'
}`}
>
Instructor
</button>
</div>
{/* Tab 1: What you will learn */}
{activeTab === 'overview' && (
<div className="space-y-8">
<div className="bg-slate-900/80 border border-slate-800 rounded-2xl p-6 space-y-4">
<h3 className="text-lg font-bold text-white">Lo que aprenderás</h3>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3 text-xs text-slate-300">
{course.whatYouWillLearn.map((item, idx) => (
<div key={idx} className="flex items-start gap-2">
<FiCheck className="text-cyan-400 shrink-0 mt-0.5" />
<span>{item}</span>
</div>
))}
</div>
</div>
<div className="space-y-3">
<h3 className="text-lg font-bold text-white">Requisitos previas</h3>
<ul className="list-disc list-inside space-y-1.5 text-xs text-slate-300">
{course.requirements.map((req, idx) => (
<li key={idx}>{req}</li>
))}
</ul>
</div>
</div>
)}
{/* Tab 2: Curriculum Accordion */}
{activeTab === 'curriculum' && (
<div className="space-y-4">
<h3 className="text-lg font-bold text-white">Contenido del Programa</h3>
<div className="divide-y divide-slate-800 border border-slate-800 rounded-2xl bg-slate-900/60 overflow-hidden">
{course.modules.map((mod) => (
<div key={mod.id} className="bg-slate-900/90">
<button
onClick={() => toggleModule(mod.id)}
className="w-full px-5 py-4 flex items-center justify-between text-left hover:bg-slate-800/60 transition"
>
<div className="flex items-center gap-3">
{expandedModules.includes(mod.id) ? (
<FiChevronUp className="text-cyan-400 text-lg" />
) : (
<FiChevronDown className="text-slate-500 text-lg" />
)}
<span className="text-sm font-bold text-white">{mod.title}</span>
</div>
<span className="text-xs text-slate-400 font-mono">{mod.lessons.length} lecciones</span>
</button>
{expandedModules.includes(mod.id) && (
<div className="px-5 pb-4 space-y-2 bg-slate-950/40 divide-y divide-slate-800/40">
{mod.lessons.map((les) => (
<div key={les.id} className="pt-2 flex items-center justify-between text-xs text-slate-300">
<div className="flex items-center gap-2">
<FiPlay className="text-cyan-400 text-xs" />
<span>{les.title}</span>
</div>
<span className="text-slate-500 font-mono text-[11px]">{les.duration}</span>
</div>
))}
</div>
)}
</div>
))}
</div>
</div>
)}
{/* Tab 3: Instructor */}
{activeTab === 'instructor' && (
<div className="bg-slate-900/80 border border-slate-800 rounded-2xl p-6 space-y-4">
<div className="flex 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-400"
/>
<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>
</div>
<p className="text-xs text-slate-300 leading-relaxed">
{course.instructor.bio}
</p>
</div>
)}
</div>
</div>
</div>
);
};
+312
View File
@@ -0,0 +1,312 @@
import React, { useState } from 'react';
import { useNavigate } from 'react-router';
import type { Course } from '../types/course';
import { FiArrowLeft, FiPlus, FiTrash2, FiUploadCloud } from 'react-icons/fi';
interface CreateCourseViewProps {
onCreateCourse: (course: Course) => void;
onShowToast: (msg: string) => void;
}
export const CreateCourseView: React.FC<CreateCourseViewProps> = ({
onCreateCourse,
onShowToast,
}) => {
const navigate = useNavigate();
const [title, setTitle] = useState('');
const [subtitle, setSubtitle] = useState('');
const [category, setCategory] = useState('Desarrollo Web');
const [level, setLevel] = useState<'Principiante' | 'Intermedio' | 'Avanzado' | 'Todos los niveles'>('Principiante');
const [price, setPrice] = useState(29.99);
const [originalPrice, setOriginalPrice] = useState(89.99);
const [thumbnail, setThumbnail] = useState('https://images.unsplash.com/photo-1517694712202-14dd9538aa97?w=800&auto=format&fit=crop&q=80');
const [modules, setModules] = useState<{ title: string; lessonsCount: number }[]>([
{ title: 'Módulo 1: Introducción y Fundamentos', lessonsCount: 4 },
{ title: 'Módulo 2: Proyecto Práctico Principal', lessonsCount: 6 },
]);
const [newModuleTitle, setNewModuleTitle] = useState('');
const handleAddModule = () => {
if (newModuleTitle.trim()) {
setModules([...modules, { title: newModuleTitle, lessonsCount: 3 }]);
setNewModuleTitle('');
}
};
const handleRemoveModule = (idx: number) => {
setModules(modules.filter((_, i) => i !== idx));
};
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
if (!title.trim()) return;
const newCourse: Course = {
id: `course-${Date.now()}`,
title,
subtitle: subtitle || 'Aprende paso a paso con proyectos prácticos.',
category,
level,
price: Number(price),
originalPrice: Number(originalPrice),
rating: 5.0,
reviewsCount: 1,
studentsCount: 1,
thumbnail,
duration: '12h 30m',
lessonsCount: modules.reduce((sum, m) => sum + m.lessonsCount, 0),
badge: 'Nuevo',
instructor: {
id: 'inst-1',
name: 'Tu Nombre (Instructor)',
role: 'Instructor Experto',
avatar: 'https://images.unsplash.com/photo-1534528741775-53994a69daeb?w=100&auto=format&fit=crop&q=80',
rating: 5.0,
totalStudents: 1,
totalCourses: 1,
bio: 'Creador de contenido y profesional en tecnología.',
},
updatedAt: 'Reciente',
language: 'Español',
whatYouWillLearn: [
'Dominio completo de la tecnología aprendida.',
'Construcción de proyectos prácticos listos para producción.',
'Código fuente completo e instructivos.',
],
requirements: ['Computadora con conexión a internet.'],
modules: modules.map((m, idx) => ({
id: `mod-${idx}`,
title: m.title,
lessons: [
{ id: `l-${idx}-1`, title: 'Lección 1: Conceptos Clave', duration: '15:00', isFreePreview: true },
{ id: `l-${idx}-2`, title: 'Lección 2: Implementación', duration: '22:30' },
],
})),
reviews: [],
isPublished: true,
salesCount: 0,
totalRevenue: 0,
};
onCreateCourse(newCourse);
onShowToast('¡Tu curso ha sido publicado exitosamente en el Marketplace!');
navigate('/seller');
};
return (
<div className="min-h-screen bg-slate-950 py-10">
<div className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 space-y-8">
{/* Header */}
<div className="flex items-center justify-between border-b border-slate-800 pb-5">
<div className="space-y-1">
<button
onClick={() => navigate('/seller')}
className="inline-flex items-center gap-2 text-xs font-semibold text-slate-400 hover:text-cyan-400 mb-2 transition"
>
<FiArrowLeft /> Regresar a Panel de Instructor
</button>
<h1 className="text-2xl sm:text-3xl font-extrabold text-white">
Publicar Nuevo Curso
</h1>
<p className="text-xs text-slate-400">
Completa los detalles de tu curso para lanzarlo a la comunidad de estudiantes
</p>
</div>
</div>
{/* Form Container */}
<form onSubmit={handleSubmit} className="space-y-8">
{/* Card 1: Informacion Basica */}
<div className="bg-slate-900 border border-slate-800 rounded-3xl p-6 sm:p-8 space-y-6 shadow-xl">
<h3 className="text-sm font-bold text-cyan-400 uppercase tracking-wider">
1. Información Básica
</h3>
<div className="space-y-4">
<div>
<label className="block text-xs font-semibold text-slate-300 mb-1">
Título del Curso *
</label>
<input
type="text"
required
value={title}
onChange={(e) => setTitle(e.target.value)}
placeholder="Ej: Master en React 19 y Next.js 15 desde Cero"
className="w-full px-4 py-3 bg-slate-950 border border-slate-700/80 rounded-xl text-xs text-white placeholder-slate-500 focus:outline-none focus:ring-2 focus:ring-cyan-500"
/>
</div>
<div>
<label className="block text-xs font-semibold text-slate-300 mb-1">
Subtítulo Resumen
</label>
<input
type="text"
value={subtitle}
onChange={(e) => setSubtitle(e.target.value)}
placeholder="Aprende a construir aplicaciones fullstack de alto rendimiento..."
className="w-full px-4 py-3 bg-slate-950 border border-slate-700/80 rounded-xl text-xs text-white placeholder-slate-500 focus:outline-none focus:ring-2 focus:ring-cyan-500"
/>
</div>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
<div>
<label className="block text-xs font-semibold text-slate-300 mb-1">
Categoría
</label>
<select
value={category}
onChange={(e) => setCategory(e.target.value)}
className="w-full px-4 py-3 bg-slate-950 border border-slate-700/80 rounded-xl text-xs text-white focus:outline-none focus:ring-2 focus:ring-cyan-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">Negocios</option>
</select>
</div>
<div>
<label className="block text-xs font-semibold text-slate-300 mb-1">
Nivel del Curso
</label>
<select
value={level}
onChange={(e) => setLevel(e.target.value as any)}
className="w-full px-4 py-3 bg-slate-950 border border-slate-700/80 rounded-xl text-xs text-white focus:outline-none focus:ring-2 focus:ring-cyan-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>
</div>
{/* Card 2: Precio e Imagen */}
<div className="bg-slate-900 border border-slate-800 rounded-3xl p-6 sm:p-8 space-y-6 shadow-xl">
<h3 className="text-sm font-bold text-cyan-400 uppercase tracking-wider">
2. Precio e Imagen de Portada
</h3>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
<div>
<label className="block text-xs font-semibold text-slate-300 mb-1">
Precio de Venta ($ USD)
</label>
<input
type="number"
step="0.01"
required
value={price}
onChange={(e) => setPrice(Number(e.target.value))}
className="w-full px-4 py-3 bg-slate-950 border border-slate-700/80 rounded-xl text-xs text-white font-mono focus:outline-none focus:ring-2 focus:ring-cyan-500"
/>
</div>
<div>
<label className="block text-xs font-semibold text-slate-300 mb-1">
Precio Original Táchado ($ USD)
</label>
<input
type="number"
step="0.01"
required
value={originalPrice}
onChange={(e) => setOriginalPrice(Number(e.target.value))}
className="w-full px-4 py-3 bg-slate-950 border border-slate-700/80 rounded-xl text-xs text-white font-mono focus:outline-none focus:ring-2 focus:ring-cyan-500"
/>
</div>
</div>
<div>
<label className="block text-xs font-semibold text-slate-300 mb-1">
URL de la Imagen de Portada
</label>
<div className="flex gap-2">
<input
type="url"
value={thumbnail}
onChange={(e) => setThumbnail(e.target.value)}
className="flex-1 px-4 py-3 bg-slate-950 border border-slate-700/80 rounded-xl text-xs text-white placeholder-slate-500 focus:outline-none focus:ring-2 focus:ring-cyan-500"
/>
</div>
<div className="mt-3 aspect-video max-w-sm rounded-xl overflow-hidden bg-slate-950 border border-slate-800">
<img src={thumbnail} alt="Vista previa" className="w-full h-full object-cover" />
</div>
</div>
</div>
{/* Card 3: Estructura del Temario */}
<div className="bg-slate-900 border border-slate-800 rounded-3xl p-6 sm:p-8 space-y-6 shadow-xl">
<h3 className="text-sm font-bold text-cyan-400 uppercase tracking-wider">
3. Módulos y Temario
</h3>
<div className="space-y-3">
{modules.map((m, idx) => (
<div key={idx} className="flex items-center justify-between p-4 bg-slate-950 border border-slate-800 rounded-xl text-xs">
<span className="font-semibold text-white">{m.title}</span>
<button
type="button"
onClick={() => handleRemoveModule(idx)}
className="p-2 text-slate-500 hover:text-rose-400 transition"
>
<FiTrash2 />
</button>
</div>
))}
<div className="flex gap-2 pt-2">
<input
type="text"
value={newModuleTitle}
onChange={(e) => setNewModuleTitle(e.target.value)}
placeholder="Agregar nuevo título de módulo..."
className="flex-1 px-4 py-2.5 bg-slate-950 border border-slate-700/80 rounded-xl text-xs text-white focus:outline-none focus:ring-2 focus:ring-cyan-500"
/>
<button
type="button"
onClick={handleAddModule}
className="px-4 py-2.5 bg-slate-800 hover:bg-slate-700 text-cyan-400 font-semibold rounded-xl text-xs flex items-center gap-1.5 transition"
>
<FiPlus /> Módulo
</button>
</div>
</div>
</div>
{/* Submit Actions */}
<div className="flex items-center justify-end gap-4 pt-4">
<button
type="button"
onClick={() => navigate('/seller')}
className="px-6 py-3.5 bg-slate-800 hover:bg-slate-700 text-slate-300 font-semibold rounded-xl text-xs transition"
>
Cancelar
</button>
<button
type="submit"
className="px-8 py-3.5 bg-gradient-to-r from-blue-600 to-cyan-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 gap-2"
>
<FiUploadCloud />
<span>Publicar Curso en RyCourses</span>
</button>
</div>
</form>
</div>
</div>
);
};
+185
View File
@@ -0,0 +1,185 @@
import React from 'react';
import type { Course } from '../types/course';
import { CourseGrid } from '../components/buyer/CourseGrid';
import {
FiPlay,
FiStar,
FiCheckCircle,
FiAward
} from 'react-icons/fi';
interface HomeViewProps {
courses: Course[];
cartCourseIds: string[];
searchQuery: string;
onAddToCart: (course: Course) => void;
onOpenPreview: (courseId: string) => void;
onOpenDetails: (course: Course) => void;
onClearSearch: () => void;
}
export const HomeView: React.FC<HomeViewProps> = ({
courses,
cartCourseIds,
searchQuery,
onAddToCart,
onOpenDetails,
onClearSearch,
}) => {
// Most watched course spotlight (top featured course)
const topCourse = courses[0];
return (
<div className="space-y-8 pb-16">
{/* 1. Welcome & Top Trending Video Hero Section */}
<section className="relative overflow-hidden bg-gradient-to-b from-slate-900 via-slate-950 to-slate-950 border-b border-slate-800/80 pt-8 pb-12">
{/* Glow background accents */}
<div className="absolute top-0 left-1/4 w-96 h-96 bg-cyan-500/10 rounded-full blur-3xl pointer-events-none" />
<div className="absolute bottom-0 right-1/4 w-96 h-96 bg-blue-600/10 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-8 items-center">
{/* Left Column: Welcome & Quick Metrics */}
<div className="lg:col-span-7 space-y-5">
<div className="inline-flex items-center gap-2 px-3.5 py-1.5 rounded-full text-xs font-semibold bg-cyan-500/10 text-cyan-400 border border-cyan-500/20">
<FiAward />
<span>¡Bienvenido a RyCourses Pro!</span>
</div>
<h1 className="text-3xl sm:text-4xl lg:text-5xl font-black text-white tracking-tight leading-tight">
Bienvenido a{' '}
<span className="text-transparent bg-clip-text bg-gradient-to-r from-cyan-400 via-blue-400 to-teal-300">
RyCourses Pro!
</span>
</h1>
<p className="text-sm sm:text-base text-slate-300 max-w-xl leading-relaxed font-normal">
Explora cursos prácticos, proyectos guiados y contenido creado por instructores expertos en Desarrollo Web, IA, Diseño y Tecnología.
</p>
{/* Trust Checkmarks */}
<div className="flex flex-wrap items-center gap-x-5 gap-y-2 text-xs text-slate-300 pt-1">
<span className="flex items-center gap-1.5">
<FiCheckCircle className="text-emerald-400 text-sm" /> Acceso de por vida
</span>
<span className="flex items-center gap-1.5">
<FiCheckCircle className="text-emerald-400 text-sm" /> Garantía de 30 días
</span>
<span className="flex items-center gap-1.5">
<FiCheckCircle className="text-emerald-400 text-sm" /> Certificados al finalizar
</span>
</div>
{/* Stats summary */}
<div className="pt-3 grid grid-cols-3 gap-4 max-w-md border-t border-slate-800/80">
<div>
<div className="text-lg font-black text-white font-mono">{courses.length}+</div>
<div className="text-[11px] text-slate-400">Cursos disponibles</div>
</div>
<div>
<div className="text-lg font-black text-cyan-400 font-mono">50k+</div>
<div className="text-[11px] text-slate-400">Estudiantes</div>
</div>
<div>
<div className="text-lg font-black text-amber-400 font-mono">4.9 ★</div>
<div className="text-[11px] text-slate-400">Valoración media</div>
</div>
</div>
</div>
{/* Right Column: Top Trending Video Spotlight Card */}
{topCourse && (
<div className="lg:col-span-5">
<div className="bg-slate-900/90 border border-slate-800 rounded-3xl p-5 shadow-2xl backdrop-blur-md space-y-4 hover:border-cyan-500/40 transition">
<div className="flex items-center justify-between">
<span className="inline-flex items-center gap-1.5 px-3 py-1 rounded-md text-[11px] font-black bg-amber-500 text-slate-950 uppercase tracking-wider shadow">
<FiAward /> Curso Más Visto de la Semana
</span>
<span className="text-[11px] text-slate-400 font-mono">38h 45m</span>
</div>
{/* Video Thumbnail with Play Button */}
<div
onClick={() => onOpenDetails(topCourse)}
className="relative aspect-video rounded-2xl overflow-hidden bg-slate-800 cursor-pointer group"
>
<img
src={topCourse.thumbnail}
alt={topCourse.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-slate-950/20 to-transparent" />
<div className="absolute inset-0 flex items-center justify-center">
<div className="w-14 h-14 rounded-full bg-cyan-500 text-slate-950 flex items-center justify-center text-xl shadow-xl shadow-cyan-500/40 group-hover:scale-110 active:scale-95 transition-all">
<FiPlay className="ml-1" />
</div>
</div>
<div className="absolute bottom-3 left-3 right-3 flex items-center justify-between text-xs text-white">
<span className="font-semibold px-2 py-0.5 rounded bg-slate-950/80 backdrop-blur-md">
Ver Clase Muestra
</span>
<span className="flex items-center gap-1 font-bold text-amber-400 bg-slate-950/80 backdrop-blur-md px-2 py-0.5 rounded">
<FiStar className="fill-amber-400" /> {topCourse.rating}
</span>
</div>
</div>
{/* Title & Price */}
<div className="space-y-2">
<h3
onClick={() => onOpenDetails(topCourse)}
className="text-base font-bold text-white hover:text-cyan-300 cursor-pointer line-clamp-1 transition-colors"
>
{topCourse.title}
</h3>
<div className="flex items-center justify-between pt-1 border-t border-slate-800">
<div className="flex items-center gap-2">
<img
src={topCourse.instructor.avatar}
alt={topCourse.instructor.name}
className="w-6 h-6 rounded-full object-cover"
/>
<span className="text-xs text-slate-300 font-medium">{topCourse.instructor.name}</span>
</div>
<div className="flex items-baseline gap-2 font-mono">
<span className="text-xs text-slate-500 line-through">${topCourse.originalPrice}</span>
<span className="text-lg font-black text-emerald-400">${topCourse.price}</span>
</div>
</div>
</div>
</div>
</div>
)}
</div>
</div>
</section>
{/* 2. Direct Catalog Header & Full Interactive Course Grid */}
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 space-y-4">
{/* Full Interactive Grid (Categories, Search & Cards) */}
<CourseGrid
courses={courses}
cartCourseIds={cartCourseIds}
searchQuery={searchQuery}
onAddToCart={onAddToCart}
onOpenDetails={onOpenDetails}
onClearSearch={onClearSearch}
/>
</div>
</div>
);
};
+155
View File
@@ -0,0 +1,155 @@
import React, { useState } from 'react';
import { useNavigate, Link } from 'react-router';
import { BrandLogo } from '../components/common/BrandLogo';
import { FiMail, FiLock, FiArrowRight } from 'react-icons/fi';
import { FaGoogle, FaGithub } from 'react-icons/fa';
interface LoginViewProps {
onShowToast: (msg: string) => void;
}
export const LoginView: React.FC<LoginViewProps> = ({ onShowToast }) => {
const navigate = useNavigate();
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [rememberMe, setRememberMe] = useState(true);
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
if (email && password) {
onShowToast('¡Bienvenido de nuevo a RyCourses!');
navigate('/');
}
};
return (
<div className="min-h-[85vh] flex items-center justify-center py-12 px-4 sm:px-6 lg:px-8 relative bg-slate-950">
{/* Glow background accents */}
<div className="absolute top-1/4 left-1/4 w-72 h-72 bg-blue-600/10 rounded-full blur-3xl pointer-events-none" />
<div className="absolute bottom-1/4 right-1/4 w-72 h-72 bg-cyan-500/10 rounded-full blur-3xl pointer-events-none" />
<div className="max-w-md w-full space-y-8 bg-slate-900/90 border border-slate-800 p-8 sm:p-10 rounded-3xl shadow-2xl backdrop-blur-md relative z-10">
{/* Header */}
<div className="text-center space-y-3">
<div className="flex justify-center">
<BrandLogo size="lg" showSubtitle={false} />
</div>
<h1 className="text-2xl font-bold text-white tracking-tight">
Iniciar Sesión
</h1>
<p className="text-xs text-slate-400">
Ingresa tus credenciales para acceder a tu panel y cursos
</p>
</div>
{/* Social logins */}
<div className="grid grid-cols-2 gap-3 pt-2">
<button
type="button"
onClick={() => {
onShowToast('Iniciando sesión con Google...');
navigate('/');
}}
className="flex items-center justify-center gap-2 py-2.5 px-4 rounded-xl bg-slate-800 hover:bg-slate-700/80 border border-slate-700 text-xs font-semibold text-slate-200 transition"
>
<FaGoogle className="text-red-400" />
<span>Google</span>
</button>
<button
type="button"
onClick={() => {
onShowToast('Iniciando sesión con GitHub...');
navigate('/');
}}
className="flex items-center justify-center gap-2 py-2.5 px-4 rounded-xl bg-slate-800 hover:bg-slate-700/80 border border-slate-700 text-xs font-semibold text-slate-200 transition"
>
<FaGithub className="text-white" />
<span>GitHub</span>
</button>
</div>
<div className="relative my-4">
<div className="absolute inset-0 flex items-center">
<div className="w-full border-t border-slate-800" />
</div>
<div className="relative flex justify-center text-xs">
<span className="px-3 bg-slate-900 text-slate-500 font-medium">o continúa con correo</span>
</div>
</div>
{/* Form */}
<form onSubmit={handleSubmit} className="space-y-4">
<div>
<label className="block text-xs font-semibold text-slate-300 mb-1.5">
Correo Electrónico
</label>
<div className="relative">
<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-950 border border-slate-700/80 rounded-xl text-xs text-white placeholder-slate-500 focus:outline-none focus:ring-2 focus:ring-cyan-500"
/>
</div>
</div>
<div>
<div className="flex items-center justify-between mb-1.5">
<label className="block text-xs font-semibold text-slate-300">
Contraseña
</label>
<a href="#" className="text-[11px] text-cyan-400 hover:underline">
¿Olvidaste tu contraseña?
</a>
</div>
<div className="relative">
<FiLock className="absolute left-3.5 top-3.5 text-slate-500" />
<input
type="password"
required
value={password}
onChange={(e) => setPassword(e.target.value)}
placeholder="••••••••"
className="w-full pl-10 pr-4 py-3 bg-slate-950 border border-slate-700/80 rounded-xl text-xs text-white placeholder-slate-500 focus:outline-none focus:ring-2 focus:ring-cyan-500"
/>
</div>
</div>
<div className="flex items-center justify-between pt-1">
<label className="flex items-center gap-2 cursor-pointer text-xs text-slate-400">
<input
type="checkbox"
checked={rememberMe}
onChange={(e) => setRememberMe(e.target.checked)}
className="rounded border-slate-700 bg-slate-950 text-cyan-500 focus:ring-cyan-500"
/>
<span>Recordar sesión</span>
</label>
</div>
<button
type="submit"
className="w-full py-3.5 px-4 bg-gradient-to-r from-blue-600 to-cyan-500 hover:from-blue-500 hover:to-cyan-400 text-white font-bold rounded-xl text-sm shadow-lg shadow-cyan-500/25 transition-all flex items-center justify-center gap-2"
>
<span>Iniciar Sesión</span>
<FiArrowRight />
</button>
</form>
{/* Footer link */}
<p className="text-center text-xs text-slate-400 pt-2">
¿No tienes una cuenta aún?{' '}
<Link to="/register" className="text-cyan-400 font-bold hover:underline">
Regístrate gratis
</Link>
</p>
</div>
</div>
);
};
+148
View File
@@ -0,0 +1,148 @@
import React, { useState } from 'react';
import { useNavigate, Link } from 'react-router';
import { BrandLogo } from '../components/common/BrandLogo';
import { FiMail, FiLock, FiUser, FiArrowRight, FiCheckCircle } from 'react-icons/fi';
import { FaGraduationCap, FaChalkboardTeacher } from 'react-icons/fa';
interface RegisterViewProps {
onShowToast: (msg: string) => void;
}
export const RegisterView: React.FC<RegisterViewProps> = ({ onShowToast }) => {
const navigate = useNavigate();
const [name, setName] = useState('');
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [accountType, setAccountType] = useState<'buyer' | 'seller'>('buyer');
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
if (name && email && password) {
onShowToast(`¡Cuenta de ${accountType === 'buyer' ? 'Estudiante' : 'Instructor'} creada con éxito!`);
navigate(accountType === 'seller' ? '/seller' : '/courses');
}
};
return (
<div className="min-h-[85vh] flex items-center justify-center py-12 px-4 sm:px-6 lg:px-8 relative bg-slate-950">
<div className="max-w-md w-full space-y-8 bg-slate-900/90 border border-slate-800 p-8 sm:p-10 rounded-3xl shadow-2xl backdrop-blur-md relative z-10">
{/* Header */}
<div className="text-center space-y-3">
<div className="flex justify-center">
<BrandLogo size="lg" showSubtitle={false} />
</div>
<h1 className="text-2xl font-bold text-white tracking-tight">
Crear Cuenta
</h1>
<p className="text-xs text-slate-400">
Únete a la comunidad de RyCourses y empieza hoy mismo
</p>
</div>
{/* Account type selector */}
<div className="grid grid-cols-2 gap-2 p-1 bg-slate-950 rounded-2xl border border-slate-800">
<button
type="button"
onClick={() => setAccountType('buyer')}
className={`flex items-center justify-center gap-2 py-2.5 rounded-xl text-xs font-semibold transition-all ${
accountType === 'buyer'
? 'bg-blue-600 text-white shadow-md'
: 'text-slate-400 hover:text-white'
}`}
>
<FaGraduationCap /> Estudiante
</button>
<button
type="button"
onClick={() => setAccountType('seller')}
className={`flex items-center justify-center gap-2 py-2.5 rounded-xl text-xs font-semibold transition-all ${
accountType === 'seller'
? 'bg-violet-600 text-white shadow-md'
: 'text-slate-400 hover:text-white'
}`}
>
<FaChalkboardTeacher /> Instructor
</button>
</div>
{/* Form */}
<form onSubmit={handleSubmit} className="space-y-4">
<div>
<label className="block text-xs font-semibold text-slate-300 mb-1.5">
Nombre Completo
</label>
<div className="relative">
<FiUser className="absolute left-3.5 top-3.5 text-slate-500" />
<input
type="text"
required
value={name}
onChange={(e) => setName(e.target.value)}
placeholder="Carlos Mendoza"
className="w-full pl-10 pr-4 py-3 bg-slate-950 border border-slate-700/80 rounded-xl text-xs text-white placeholder-slate-500 focus:outline-none focus:ring-2 focus:ring-cyan-500"
/>
</div>
</div>
<div>
<label className="block text-xs font-semibold text-slate-300 mb-1.5">
Correo Electrónico
</label>
<div className="relative">
<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-950 border border-slate-700/80 rounded-xl text-xs text-white placeholder-slate-500 focus:outline-none focus:ring-2 focus:ring-cyan-500"
/>
</div>
</div>
<div>
<label className="block text-xs font-semibold text-slate-300 mb-1.5">
Contraseña
</label>
<div className="relative">
<FiLock className="absolute left-3.5 top-3.5 text-slate-500" />
<input
type="password"
required
value={password}
onChange={(e) => setPassword(e.target.value)}
placeholder="Mínimo 8 caracteres"
className="w-full pl-10 pr-4 py-3 bg-slate-950 border border-slate-700/80 rounded-xl text-xs text-white placeholder-slate-500 focus:outline-none focus:ring-2 focus:ring-cyan-500"
/>
</div>
</div>
<div className="flex items-center gap-2 pt-1 text-xs text-slate-400">
<FiCheckCircle className="text-emerald-400 shrink-0" />
<span>Acepto los términos de servicio y políticas de privacidad.</span>
</div>
<button
type="submit"
className="w-full py-3.5 px-4 bg-gradient-to-r from-blue-600 to-cyan-500 hover:from-blue-500 hover:to-cyan-400 text-white font-bold rounded-xl text-sm shadow-lg shadow-cyan-500/25 transition-all flex items-center justify-center gap-2"
>
<span>Crear Cuenta Gratis</span>
<FiArrowRight />
</button>
</form>
{/* Footer link */}
<p className="text-center text-xs text-slate-400 pt-2">
¿Ya tienes una cuenta?{' '}
<Link to="/login" className="text-cyan-400 font-bold hover:underline">
Inicia sesión
</Link>
</p>
</div>
</div>
);
};
+53
View File
@@ -0,0 +1,53 @@
import React from 'react';
import type { Course } from '../types/course';
import { SellerHero } from '../components/seller/SellerHero';
import { SellerDashboard } from '../components/seller/SellerDashboard';
import { SellerCourseList } from '../components/seller/SellerCourseList';
import { SellerCalculator } from '../components/seller/SellerCalculator';
interface SellerViewProps {
courses: Course[];
onOpenCreateCourse: () => void;
onPreviewCourse: (course: Course) => void;
onDeleteCourse: (courseId: string) => void;
}
export const SellerView: React.FC<SellerViewProps> = ({
courses,
onOpenCreateCourse,
onPreviewCourse,
onDeleteCourse,
}) => {
const handleScrollToCalculator = () => {
const el = document.getElementById('seller-calculator');
if (el) {
el.scrollIntoView({ behavior: 'smooth' });
}
};
return (
<div className="space-y-0">
{/* Seller Hero Section */}
<SellerHero
onOpenCreateCourse={onOpenCreateCourse}
onScrollToCalculator={handleScrollToCalculator}
/>
{/* Instructor Dashboard Stats */}
<SellerDashboard />
{/* Published Courses Management List */}
<SellerCourseList
courses={courses}
onOpenCreateModal={onOpenCreateCourse}
onPreviewCourse={onPreviewCourse}
onDeleteCourse={onDeleteCourse}
/>
{/* Revenue Calculator */}
<SellerCalculator
onStartCourse={onOpenCreateCourse}
/>
</div>
);
};