se sube vistas
This commit is contained in:
1 parent
509272a643
commit
a8c484c1de
16 files changed
+1966
-228
No files matched your search
Binary file not shown.
|
After Width: | Height: | Size: 594 KiB |
+74
-52
@@ -1,22 +1,23 @@
|
|||||||
import { useState } from 'react';
|
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 { mockCourses } from './data/mockCourses';
|
||||||
import { Navbar } from './components/layout/Navbar';
|
import { Navbar } from './components/layout/Navbar';
|
||||||
import { Footer } from './components/layout/Footer';
|
import { Footer } from './components/layout/Footer';
|
||||||
import { HeroSection } from './components/buyer/HeroSection';
|
import { HomeView } from './views/HomeView';
|
||||||
import { CourseGrid } from './components/buyer/CourseGrid';
|
import { BuyerView } from './views/BuyerView';
|
||||||
import { FeaturesBanner } from './components/buyer/FeaturesBanner';
|
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 { CourseModal } from './components/buyer/CourseModal';
|
||||||
import { CartDrawer } from './components/buyer/CartDrawer';
|
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 { CreateCourseModal } from './components/seller/CreateCourseModal';
|
||||||
import { FiCheckCircle } from 'react-icons/fi';
|
import { FiCheckCircle } from 'react-icons/fi';
|
||||||
|
|
||||||
export function App() {
|
export function App() {
|
||||||
const [viewMode, setViewMode] = useState<ViewMode>('buyer');
|
|
||||||
const [courses, setCourses] = useState<Course[]>(mockCourses);
|
const [courses, setCourses] = useState<Course[]>(mockCourses);
|
||||||
const [cartItems, setCartItems] = useState<CartItem[]>([]);
|
const [cartItems, setCartItems] = useState<CartItem[]>([]);
|
||||||
const [searchQuery, setSearchQuery] = useState('');
|
const [searchQuery, setSearchQuery] = useState('');
|
||||||
@@ -72,20 +73,6 @@ export function App() {
|
|||||||
showToast('Curso eliminado correctamente');
|
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);
|
const cartCourseIds = cartItems.map((i) => i.course.id);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -101,8 +88,6 @@ export function App() {
|
|||||||
|
|
||||||
{/* Main Navigation Bar */}
|
{/* Main Navigation Bar */}
|
||||||
<Navbar
|
<Navbar
|
||||||
viewMode={viewMode}
|
|
||||||
onToggleViewMode={setViewMode}
|
|
||||||
cartCount={cartItems.length}
|
cartCount={cartItems.length}
|
||||||
onOpenCart={() => setIsCartOpen(true)}
|
onOpenCart={() => setIsCartOpen(true)}
|
||||||
onOpenCreateCourseModal={() => setIsCreateModalOpen(true)}
|
onOpenCreateCourseModal={() => setIsCreateModalOpen(true)}
|
||||||
@@ -110,20 +95,30 @@ export function App() {
|
|||||||
onSearchChange={setSearchQuery}
|
onSearchChange={setSearchQuery}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Main Content Area */}
|
{/* Main Views Container */}
|
||||||
<main className="flex-1">
|
<main className="flex-1">
|
||||||
{viewMode === 'buyer' ? (
|
<Routes>
|
||||||
/* ==================== MODO ESTUDIANTE (COMPRAR) ==================== */
|
<Route
|
||||||
<>
|
path="/"
|
||||||
<HeroSection
|
element={
|
||||||
onExploreClick={handleScrollToCatalog}
|
<HomeView
|
||||||
|
courses={courses}
|
||||||
|
cartCourseIds={cartCourseIds}
|
||||||
|
searchQuery={searchQuery}
|
||||||
|
onAddToCart={handleAddToCart}
|
||||||
onOpenPreview={(courseId) => {
|
onOpenPreview={(courseId) => {
|
||||||
const target = courses.find((c) => c.id === courseId) || courses[0];
|
const target = courses.find((c) => c.id === courseId) || courses[0];
|
||||||
setSelectedCourseForModal(target);
|
setSelectedCourseForModal(target);
|
||||||
}}
|
}}
|
||||||
|
onOpenDetails={(course) => setSelectedCourseForModal(course)}
|
||||||
|
onClearSearch={() => setSearchQuery('')}
|
||||||
/>
|
/>
|
||||||
|
}
|
||||||
<CourseGrid
|
/>
|
||||||
|
<Route
|
||||||
|
path="/courses"
|
||||||
|
element={
|
||||||
|
<BuyerView
|
||||||
courses={courses}
|
courses={courses}
|
||||||
cartCourseIds={cartCourseIds}
|
cartCourseIds={cartCourseIds}
|
||||||
searchQuery={searchQuery}
|
searchQuery={searchQuery}
|
||||||
@@ -131,31 +126,58 @@ export function App() {
|
|||||||
onOpenDetails={(course) => setSelectedCourseForModal(course)}
|
onOpenDetails={(course) => setSelectedCourseForModal(course)}
|
||||||
onClearSearch={() => setSearchQuery('')}
|
onClearSearch={() => setSearchQuery('')}
|
||||||
/>
|
/>
|
||||||
|
}
|
||||||
<FeaturesBanner />
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
/* ==================== MODO INSTRUCTOR (VENDER) ==================== */
|
|
||||||
<>
|
|
||||||
<SellerHero
|
|
||||||
onOpenCreateCourse={() => setIsCreateModalOpen(true)}
|
|
||||||
onScrollToCalculator={handleScrollToCalculator}
|
|
||||||
/>
|
/>
|
||||||
|
<Route
|
||||||
<SellerDashboard />
|
path="/course/:courseId"
|
||||||
|
element={
|
||||||
<SellerCourseList
|
<CourseDetailView
|
||||||
courses={courses}
|
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)}
|
onPreviewCourse={(course) => setSelectedCourseForModal(course)}
|
||||||
onDeleteCourse={handleDeleteCourse}
|
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>
|
</main>
|
||||||
|
|
||||||
{/* Course Detail Modal */}
|
{/* Course Detail Modal */}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import { useNavigate } from 'react-router';
|
||||||
import {
|
import {
|
||||||
FiStar,
|
FiStar,
|
||||||
FiClock,
|
FiClock,
|
||||||
@@ -14,15 +15,19 @@ interface CourseCardProps {
|
|||||||
course: Course;
|
course: Course;
|
||||||
isInCart: boolean;
|
isInCart: boolean;
|
||||||
onAddToCart: (course: Course) => void;
|
onAddToCart: (course: Course) => void;
|
||||||
onOpenDetails: (course: Course) => void;
|
onOpenDetails?: (course: Course) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const CourseCard: React.FC<CourseCardProps> = ({
|
export const CourseCard: React.FC<CourseCardProps> = ({
|
||||||
course,
|
course,
|
||||||
isInCart,
|
isInCart,
|
||||||
onAddToCart,
|
onAddToCart,
|
||||||
onOpenDetails,
|
|
||||||
}) => {
|
}) => {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
const handleCourseClick = () => {
|
||||||
|
navigate(`/course/${course.id}`);
|
||||||
|
};
|
||||||
const discountPercent = Math.round(
|
const discountPercent = Math.round(
|
||||||
((course.originalPrice - course.price) / course.originalPrice) * 100
|
((course.originalPrice - course.price) / course.originalPrice) * 100
|
||||||
);
|
);
|
||||||
@@ -47,7 +52,7 @@ export const CourseCard: React.FC<CourseCardProps> = ({
|
|||||||
|
|
||||||
{/* Top Image Section */}
|
{/* Top Image Section */}
|
||||||
<div>
|
<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
|
<img
|
||||||
src={course.thumbnail}
|
src={course.thumbnail}
|
||||||
alt={course.title}
|
alt={course.title}
|
||||||
@@ -79,11 +84,11 @@ export const CourseCard: React.FC<CourseCardProps> = ({
|
|||||||
<button
|
<button
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation();
|
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"
|
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>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -112,7 +117,7 @@ export const CourseCard: React.FC<CourseCardProps> = ({
|
|||||||
|
|
||||||
{/* Title */}
|
{/* Title */}
|
||||||
<h3
|
<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"
|
className="text-base font-bold text-white group-hover:text-cyan-300 transition-colors line-clamp-2 cursor-pointer leading-snug"
|
||||||
>
|
>
|
||||||
{course.title}
|
{course.title}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -1,6 +1,5 @@
|
|||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import {
|
import {
|
||||||
FiBookOpen,
|
|
||||||
FiMail,
|
FiMail,
|
||||||
FiCheckCircle,
|
FiCheckCircle,
|
||||||
FiGlobe,
|
FiGlobe,
|
||||||
@@ -10,6 +9,8 @@ import {
|
|||||||
} from 'react-icons/fi';
|
} from 'react-icons/fi';
|
||||||
import { FaTwitter, FaYoutube, FaGithub, FaLinkedin, FaDiscord } from 'react-icons/fa';
|
import { FaTwitter, FaYoutube, FaGithub, FaLinkedin, FaDiscord } from 'react-icons/fa';
|
||||||
|
|
||||||
|
import { BrandLogo } from '../common/BrandLogo';
|
||||||
|
|
||||||
export const Footer: React.FC = () => {
|
export const Footer: React.FC = () => {
|
||||||
const [email, setEmail] = useState('');
|
const [email, setEmail] = useState('');
|
||||||
const [subscribed, setSubscribed] = useState(false);
|
const [subscribed, setSubscribed] = useState(false);
|
||||||
@@ -25,52 +26,6 @@ export const Footer: React.FC = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<footer className="bg-slate-950 border-t border-slate-800 text-slate-400 text-sm">
|
<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 */}
|
{/* Main Footer Links */}
|
||||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-14">
|
<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 */}
|
{/* Brand col */}
|
||||||
<div className="col-span-2 lg:col-span-2">
|
<div className="col-span-2 lg:col-span-2">
|
||||||
<div className="flex items-center gap-3 mb-4">
|
<div className="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]">
|
<BrandLogo size="md" />
|
||||||
<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>
|
</div>
|
||||||
<p className="text-xs text-slate-400 leading-relaxed mb-6 max-w-sm">
|
<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.
|
La plataforma líder para aprender habilidades de alta demanda y convertir tu conocimiento profesional en un negocio digital rentable.
|
||||||
|
|||||||
+138
-113
@@ -1,106 +1,101 @@
|
|||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
|
import { useNavigate, useLocation } from 'react-router';
|
||||||
import {
|
import {
|
||||||
FiSearch,
|
FiSearch,
|
||||||
FiShoppingCart,
|
FiShoppingCart,
|
||||||
FiHeart,
|
|
||||||
FiBell,
|
|
||||||
FiMenu,
|
FiMenu,
|
||||||
FiX,
|
FiX,
|
||||||
FiBookOpen,
|
|
||||||
FiPlusCircle,
|
FiPlusCircle,
|
||||||
FiCompass,
|
FiHome,
|
||||||
|
FiGrid,
|
||||||
|
FiUser,
|
||||||
|
FiShield,
|
||||||
} from 'react-icons/fi';
|
} from 'react-icons/fi';
|
||||||
import { FaGraduationCap, FaChalkboardTeacher } from 'react-icons/fa';
|
import { FaChalkboardTeacher } from 'react-icons/fa';
|
||||||
import type { ViewMode } from '../../types/course';
|
import { BrandLogo } from '../common/BrandLogo';
|
||||||
|
|
||||||
interface NavbarProps {
|
interface NavbarProps {
|
||||||
viewMode: ViewMode;
|
|
||||||
onToggleViewMode: (mode: ViewMode) => void;
|
|
||||||
cartCount: number;
|
cartCount: number;
|
||||||
onOpenCart: () => void;
|
onOpenCart: () => void;
|
||||||
onOpenCreateCourseModal: () => void;
|
onOpenCreateCourseModal?: () => void;
|
||||||
searchQuery: string;
|
searchQuery: string;
|
||||||
onSearchChange: (query: string) => void;
|
onSearchChange: (query: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Navbar: React.FC<NavbarProps> = ({
|
export const Navbar: React.FC<NavbarProps> = ({
|
||||||
viewMode,
|
|
||||||
onToggleViewMode,
|
|
||||||
cartCount,
|
cartCount,
|
||||||
onOpenCart,
|
onOpenCart,
|
||||||
onOpenCreateCourseModal,
|
|
||||||
searchQuery,
|
searchQuery,
|
||||||
onSearchChange,
|
onSearchChange,
|
||||||
}) => {
|
}) => {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const location = useLocation();
|
||||||
const [mobileMenuOpen, setMobileMenuOpen] = useState(false);
|
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 (
|
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="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||||
<div className="flex items-center justify-between h-20 gap-4">
|
<div className="flex items-center justify-between h-20 gap-4">
|
||||||
|
|
||||||
{/* Logo */}
|
{/* 1. Left Brand & Main Links */}
|
||||||
<div className="flex items-center gap-8">
|
<div className="flex items-center gap-6 shrink-0">
|
||||||
<div
|
<div onClick={() => navigate('/')}>
|
||||||
onClick={() => onToggleViewMode('buyer')}
|
<BrandLogo size="md" />
|
||||||
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>
|
</div>
|
||||||
|
|
||||||
{/* Quick Categories dropdown (Desktop) */}
|
{/* Quick Nav Links */}
|
||||||
{viewMode === 'buyer' && (
|
<nav className="hidden lg:flex items-center gap-1">
|
||||||
<div className="relative hidden lg:block">
|
|
||||||
<button
|
<button
|
||||||
onClick={() => setCategoryDropdown(!categoryDropdown)}
|
onClick={() => navigate('/')}
|
||||||
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"
|
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" />
|
<FiHome />
|
||||||
<span>Explorar</span>
|
<span>Inicio</span>
|
||||||
</button>
|
</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
|
<button
|
||||||
key={cat}
|
onClick={() => navigate('/courses')}
|
||||||
onClick={() => {
|
className={`flex items-center gap-1.5 px-3 py-2 rounded-xl text-xs font-semibold transition-all ${
|
||||||
onSearchChange(cat);
|
currentPath === '/courses'
|
||||||
setCategoryDropdown(false);
|
? 'bg-slate-800 text-cyan-400 border border-cyan-500/30'
|
||||||
}}
|
: 'text-slate-300 hover:text-white hover:bg-slate-900'
|
||||||
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}
|
<FiGrid />
|
||||||
|
<span>Catálogo</span>
|
||||||
</button>
|
</button>
|
||||||
))}
|
|
||||||
</div>
|
<button
|
||||||
)}
|
onClick={() => navigate('/admin')}
|
||||||
</div>
|
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>
|
</div>
|
||||||
|
|
||||||
{/* Search bar (Desktop) */}
|
{/* 2. Spacious Center Search Bar */}
|
||||||
{viewMode === 'buyer' && (
|
{!isSellerMode && (
|
||||||
<div className="hidden md:flex flex-1 max-w-md mx-4">
|
<div className="hidden md:flex flex-1 max-w-xl mx-2">
|
||||||
<div className="relative w-full">
|
<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">
|
<div className="absolute inset-y-0 left-0 pl-3.5 flex items-center pointer-events-none text-slate-400">
|
||||||
<FiSearch className="text-base" />
|
<FiSearch className="text-base" />
|
||||||
@@ -108,98 +103,105 @@ export const Navbar: React.FC<NavbarProps> = ({
|
|||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
value={searchQuery}
|
value={searchQuery}
|
||||||
onChange={(e) => onSearchChange(e.target.value)}
|
onChange={handleSearchInputChange}
|
||||||
placeholder="¿Qué habilidad quieres aprender hoy? (React, Python, Figma...)"
|
placeholder="¿Qué 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"
|
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">
|
<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">
|
<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>
|
</kbd>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* View Mode Switcher (Estudiante vs Instructor) & User Actions */}
|
{/* 3. Right Actions & Role Switcher */}
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-2.5 shrink-0">
|
||||||
|
|
||||||
{/* Mode Switcher pill */}
|
{/* Buyer vs Seller Switcher pill */}
|
||||||
<div className="bg-slate-900/90 p-1 rounded-xl border border-slate-800 flex items-center shadow-inner">
|
<div className="bg-slate-900 p-1 rounded-xl border border-slate-800 flex items-center shadow-inner">
|
||||||
<button
|
<button
|
||||||
onClick={() => onToggleViewMode('buyer')}
|
onClick={() => navigate('/courses')}
|
||||||
className={`flex items-center gap-2 px-3 py-1.5 rounded-lg text-xs font-semibold transition-all ${
|
className={`flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-xs font-semibold transition-all ${
|
||||||
viewMode === 'buyer'
|
!isSellerMode
|
||||||
? 'bg-gradient-to-r from-blue-600 to-cyan-500 text-white shadow-md shadow-cyan-500/20'
|
? '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'
|
: 'text-slate-400 hover:text-slate-200'
|
||||||
}`}
|
}`}
|
||||||
title="Modo Estudiante - Explorar y Comprar cursos"
|
title="Modo Estudiante - Explorar y Comprar cursos"
|
||||||
>
|
>
|
||||||
<FaGraduationCap className="text-sm" />
|
<FiGrid className="text-xs" />
|
||||||
<span className="hidden sm:inline">Comprar</span>
|
<span className="hidden sm:inline">Comprar</span>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
onClick={() => onToggleViewMode('seller')}
|
onClick={() => navigate('/seller')}
|
||||||
className={`flex items-center gap-2 px-3 py-1.5 rounded-lg text-xs font-semibold transition-all ${
|
className={`flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-xs font-semibold transition-all ${
|
||||||
viewMode === 'seller'
|
isSellerMode
|
||||||
? 'bg-gradient-to-r from-indigo-600 to-violet-500 text-white shadow-md shadow-violet-500/20'
|
? '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'
|
: 'text-slate-400 hover:text-slate-200'
|
||||||
}`}
|
}`}
|
||||||
title="Modo Instructor - Vender y Gestionar cursos"
|
title="Modo Instructor - Vender y Gestionar cursos"
|
||||||
>
|
>
|
||||||
<FaChalkboardTeacher className="text-sm" />
|
<FaChalkboardTeacher className="text-xs" />
|
||||||
<span className="hidden sm:inline">Vender</span>
|
<span className="hidden sm:inline">Vender</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* If seller mode: Create Course button */}
|
{/* If seller mode: Create Course button */}
|
||||||
{viewMode === 'seller' && (
|
{isSellerMode && (
|
||||||
<button
|
<button
|
||||||
onClick={onOpenCreateCourseModal}
|
onClick={() => navigate('/seller/create')}
|
||||||
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"
|
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>
|
<span>Crear Curso</span>
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Cart button (buyer mode) */}
|
{/* Cart button (Buyer mode) */}
|
||||||
{viewMode === 'buyer' && (
|
{!isSellerMode && (
|
||||||
<button
|
<button
|
||||||
onClick={onOpenCart}
|
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"
|
aria-label="Carrito de compras"
|
||||||
>
|
>
|
||||||
<FiShoppingCart className="text-lg" />
|
<FiShoppingCart className="text-base" />
|
||||||
{cartCount > 0 && (
|
{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}
|
{cartCount}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Wishlist */}
|
{/* Login & Register Buttons */}
|
||||||
{viewMode === 'buyer' && (
|
<div className="hidden xl:flex items-center gap-2 pl-2 border-l border-slate-800">
|
||||||
<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">
|
<button
|
||||||
<FiHeart className="text-lg" />
|
onClick={() => navigate('/login')}
|
||||||
|
className="px-3 py-1.5 text-xs font-semibold text-slate-300 hover:text-white transition"
|
||||||
|
>
|
||||||
|
Iniciar Sesión
|
||||||
</button>
|
</button>
|
||||||
)}
|
<button
|
||||||
|
onClick={() => navigate('/register')}
|
||||||
{/* Notification Bell */}
|
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"
|
||||||
<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" />
|
Registrarse
|
||||||
<span className="absolute top-2 right-2 w-2 h-2 bg-amber-400 rounded-full"></span>
|
|
||||||
</button>
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* User Profile Avatar */}
|
{/* User Profile Avatar */}
|
||||||
<div className="flex items-center gap-2 pl-2 border-l border-slate-800">
|
<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
|
<img
|
||||||
src="https://images.unsplash.com/photo-1534528741775-53994a69daeb?w=100&auto=format&fit=crop&q=80"
|
src="https://images.unsplash.com/photo-1534528741775-53994a69daeb?w=100&auto=format&fit=crop&q=80"
|
||||||
alt="Usuario"
|
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>
|
<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>
|
||||||
@@ -223,34 +225,57 @@ export const Navbar: React.FC<NavbarProps> = ({
|
|||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
value={searchQuery}
|
value={searchQuery}
|
||||||
onChange={(e) => onSearchChange(e.target.value)}
|
onChange={handleSearchInputChange}
|
||||||
placeholder="Buscar cursos..."
|
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"
|
className="w-full pl-10 pr-4 py-2 bg-slate-900 border border-slate-700 rounded-lg text-sm text-white"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid grid-cols-2 gap-2">
|
<div className="grid grid-cols-3 gap-2">
|
||||||
<button
|
<button
|
||||||
onClick={() => { onToggleViewMode('buyer'); setMobileMenuOpen(false); }}
|
onClick={() => { navigate('/'); setMobileMenuOpen(false); }}
|
||||||
className={`flex items-center justify-center gap-2 p-2.5 rounded-lg text-xs font-semibold ${
|
className={`flex items-center justify-center gap-1.5 p-2 rounded-lg text-xs font-semibold ${
|
||||||
viewMode === 'buyer' ? 'bg-blue-600 text-white' : 'bg-slate-800 text-slate-300'
|
currentPath === '/' ? 'bg-blue-600 text-white' : 'bg-slate-800 text-slate-300'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<FaGraduationCap /> Modo Comprador
|
<FiHome /> Inicio
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={() => { onToggleViewMode('seller'); setMobileMenuOpen(false); }}
|
onClick={() => { navigate('/courses'); setMobileMenuOpen(false); }}
|
||||||
className={`flex items-center justify-center gap-2 p-2.5 rounded-lg text-xs font-semibold ${
|
className={`flex items-center justify-center gap-1.5 p-2 rounded-lg text-xs font-semibold ${
|
||||||
viewMode === 'seller' ? 'bg-violet-600 text-white' : 'bg-slate-800 text-slate-300'
|
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>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{viewMode === 'seller' && (
|
<div className="grid grid-cols-2 gap-2 pt-2 border-t border-slate-800">
|
||||||
<button
|
<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"
|
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
|
<FiPlusCircle /> Crear Nuevo Curso
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import {
|
|||||||
FiUsers,
|
FiUsers,
|
||||||
FiStar,
|
FiStar,
|
||||||
FiBookOpen,
|
FiBookOpen,
|
||||||
FiTrendingUp,
|
|
||||||
FiArrowUpRight,
|
FiArrowUpRight,
|
||||||
FiActivity
|
FiActivity
|
||||||
} from 'react-icons/fi';
|
} from 'react-icons/fi';
|
||||||
|
|||||||
@@ -1,10 +1,13 @@
|
|||||||
import { StrictMode } from 'react'
|
import { StrictMode } from 'react'
|
||||||
import { createRoot } from 'react-dom/client'
|
import { createRoot } from 'react-dom/client'
|
||||||
|
import { BrowserRouter } from 'react-router'
|
||||||
import './index.css'
|
import './index.css'
|
||||||
import App from './App.tsx'
|
import App from './App.tsx'
|
||||||
|
|
||||||
createRoot(document.getElementById('root')!).render(
|
createRoot(document.getElementById('root')!).render(
|
||||||
<StrictMode>
|
<StrictMode>
|
||||||
|
<BrowserRouter>
|
||||||
<App />
|
<App />
|
||||||
|
</BrowserRouter>
|
||||||
</StrictMode>,
|
</StrictMode>,
|
||||||
)
|
)
|
||||||
@@ -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>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -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>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -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>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -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>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -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>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -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>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -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>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -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>
|
||||||
|
);
|
||||||
|
};
|
||||||
Reference in new issue
Block a user