26 files changed
+3913
-1213
No files matched your search
+51
-13
@@ -7,11 +7,13 @@ import { Footer } from './components/layout/Footer';
|
||||
import { HomeView } from './views/HomeView';
|
||||
import { BuyerView } from './views/BuyerView';
|
||||
import { SellerView } from './views/SellerView';
|
||||
import { TeachersView } from './views/TeachersView';
|
||||
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 { TermsView } from './views/TermsView';
|
||||
import { CourseModal } from './components/buyer/CourseModal';
|
||||
import { CartDrawer } from './components/buyer/CartDrawer';
|
||||
import { CreateCourseModal } from './components/seller/CreateCourseModal';
|
||||
@@ -19,7 +21,9 @@ import { FiCheckCircle } from 'react-icons/fi';
|
||||
|
||||
export function App() {
|
||||
const location = useLocation();
|
||||
const isAuthPage = location.pathname === '/login' || location.pathname === '/register';
|
||||
const isAuthPage = location.pathname === '/login' || location.pathname === '/register' || location.pathname === '/terms';
|
||||
const isDashboardPage = location.pathname === '/admin' || location.pathname === '/teachers';
|
||||
const hideNavbarFooter = isAuthPage || isDashboardPage;
|
||||
|
||||
const [courses, setCourses] = useState<Course[]>(mockCourses);
|
||||
const [cartItems, setCartItems] = useState<CartItem[]>([]);
|
||||
@@ -27,6 +31,7 @@ export function App() {
|
||||
|
||||
// Modals & Drawers state
|
||||
const [selectedCourseForModal, setSelectedCourseForModal] = useState<Course | null>(null);
|
||||
const [isTeacherPreviewModal, setIsTeacherPreviewModal] = useState(false);
|
||||
const [isCartOpen, setIsCartOpen] = useState(false);
|
||||
const [isCreateModalOpen, setIsCreateModalOpen] = useState(false);
|
||||
|
||||
@@ -95,18 +100,18 @@ export function App() {
|
||||
const cartCourseIds = cartItems.map((i) => i.course.id);
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-slate-950 text-slate-100 flex flex-col selection:bg-cyan-500 selection:text-slate-950">
|
||||
<div className="min-h-screen flex flex-col transition-colors duration-300 bg-slate-950 text-slate-100 dark:bg-slate-950 dark:text-slate-100 selection:bg-cyan-500">
|
||||
|
||||
{/* Toast notification banner */}
|
||||
{toastMessage && (
|
||||
<div className="fixed bottom-6 right-6 z-50 flex items-center gap-3 px-5 py-3.5 rounded-2xl bg-slate-900/95 border border-cyan-500/40 text-white shadow-2xl shadow-cyan-500/20 backdrop-blur-md animate-in slide-in-from-bottom-5 duration-300">
|
||||
<FiCheckCircle className="text-cyan-400 text-lg shrink-0" />
|
||||
<div className="fixed bottom-6 right-6 z-50 flex items-center gap-3 px-5 py-3.5 rounded-2xl border shadow-2xl backdrop-blur-md animate-in slide-in-from-bottom-5 duration-300 bg-slate-900/95 border-cyan-500/40 text-white shadow-cyan-500/20">
|
||||
<FiCheckCircle className="text-cyan-500 text-lg shrink-0" />
|
||||
<span className="text-xs font-semibold">{toastMessage}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Main Navigation Bar (hidden on Auth pages) */}
|
||||
{!isAuthPage && (
|
||||
{/* Main Navigation Bar (hidden on Auth & Dashboard pages) */}
|
||||
{!hideNavbarFooter && (
|
||||
<Navbar
|
||||
currentUser={currentUser}
|
||||
onLogout={handleLogout}
|
||||
@@ -132,8 +137,12 @@ export function App() {
|
||||
onOpenPreview={(courseId) => {
|
||||
const target = courses.find((c) => c.id === courseId) || courses[0];
|
||||
setSelectedCourseForModal(target);
|
||||
setIsTeacherPreviewModal(false);
|
||||
}}
|
||||
onOpenDetails={(course) => {
|
||||
setSelectedCourseForModal(course);
|
||||
setIsTeacherPreviewModal(false);
|
||||
}}
|
||||
onOpenDetails={(course) => setSelectedCourseForModal(course)}
|
||||
onClearSearch={() => setSearchQuery('')}
|
||||
/>
|
||||
}
|
||||
@@ -146,7 +155,10 @@ export function App() {
|
||||
cartCourseIds={cartCourseIds}
|
||||
searchQuery={searchQuery}
|
||||
onAddToCart={handleAddToCart}
|
||||
onOpenDetails={(course) => setSelectedCourseForModal(course)}
|
||||
onOpenDetails={(course) => {
|
||||
setSelectedCourseForModal(course);
|
||||
setIsTeacherPreviewModal(false);
|
||||
}}
|
||||
onClearSearch={() => setSearchQuery('')}
|
||||
/>
|
||||
}
|
||||
@@ -172,13 +184,31 @@ export function App() {
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="/teachers"
|
||||
element={
|
||||
<TeachersView
|
||||
courses={courses}
|
||||
onOpenCreateCourse={() => setIsCreateModalOpen(true)}
|
||||
onPreviewCourse={(course) => {
|
||||
setSelectedCourseForModal(course);
|
||||
setIsTeacherPreviewModal(true);
|
||||
}}
|
||||
onDeleteCourse={handleDeleteCourse}
|
||||
onShowToast={showToast}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="/seller"
|
||||
element={
|
||||
<SellerView
|
||||
currentUser={currentUser}
|
||||
courses={courses}
|
||||
onOpenCreateCourse={() => setIsCreateModalOpen(true)}
|
||||
onPreviewCourse={(course) => setSelectedCourseForModal(course)}
|
||||
onPreviewCourse={(course) => {
|
||||
setSelectedCourseForModal(course);
|
||||
setIsTeacherPreviewModal(true);
|
||||
}}
|
||||
onDeleteCourse={handleDeleteCourse}
|
||||
/>
|
||||
}
|
||||
@@ -192,6 +222,10 @@ export function App() {
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="/terms"
|
||||
element={<TermsView />}
|
||||
/>
|
||||
<Route
|
||||
path="/login"
|
||||
element={<LoginView onShowToast={showToast} onLogin={handleLogin} />}
|
||||
@@ -208,7 +242,11 @@ export function App() {
|
||||
course={selectedCourseForModal}
|
||||
isOpen={!!selectedCourseForModal}
|
||||
isInCart={selectedCourseForModal ? cartCourseIds.includes(selectedCourseForModal.id) : false}
|
||||
onClose={() => setSelectedCourseForModal(null)}
|
||||
isTeacherPreview={isTeacherPreviewModal}
|
||||
onClose={() => {
|
||||
setSelectedCourseForModal(null);
|
||||
setIsTeacherPreviewModal(false);
|
||||
}}
|
||||
onAddToCart={handleAddToCart}
|
||||
onBuyNow={handleBuyNow}
|
||||
/>
|
||||
@@ -229,8 +267,8 @@ export function App() {
|
||||
onCreateCourse={handleCreateCourse}
|
||||
/>
|
||||
|
||||
{/* Global Footer (hidden on Auth pages) */}
|
||||
{!isAuthPage && <Footer />}
|
||||
{/* Global Footer (hidden on Auth & Dashboard pages) */}
|
||||
{!hideNavbarFooter && <Footer />}
|
||||
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -11,7 +11,8 @@ import {
|
||||
FiShield,
|
||||
FiChevronDown,
|
||||
FiChevronUp,
|
||||
FiShoppingCart
|
||||
FiShoppingCart,
|
||||
FiLock
|
||||
} from 'react-icons/fi';
|
||||
import type { Course } from '../../types/course';
|
||||
|
||||
@@ -19,6 +20,7 @@ interface CourseModalProps {
|
||||
course: Course | null;
|
||||
isOpen: boolean;
|
||||
isInCart: boolean;
|
||||
isTeacherPreview?: boolean;
|
||||
onClose: () => void;
|
||||
onAddToCart: (course: Course) => void;
|
||||
onBuyNow: (course: Course) => void;
|
||||
@@ -28,6 +30,7 @@ export const CourseModal: React.FC<CourseModalProps> = ({
|
||||
course,
|
||||
isOpen,
|
||||
isInCart,
|
||||
isTeacherPreview = false,
|
||||
onClose,
|
||||
onAddToCart,
|
||||
onBuyNow,
|
||||
@@ -53,24 +56,29 @@ export const CourseModal: React.FC<CourseModalProps> = ({
|
||||
return (
|
||||
<div className="fixed inset-0 z-50 overflow-y-auto bg-slate-950/80 backdrop-blur-md flex items-center justify-center p-2 sm:p-4 animate-in fade-in duration-200">
|
||||
|
||||
{/* Modal Container */}
|
||||
{/* Modal Container with full Light / Dark Mode support */}
|
||||
<div
|
||||
className="relative w-full max-w-5xl bg-slate-900 border border-slate-800 rounded-2xl shadow-2xl overflow-hidden my-8 max-h-[92vh] flex flex-col"
|
||||
className="relative w-full max-w-5xl transition-colors duration-300 bg-white dark:bg-slate-900 border border-slate-200 dark:border-slate-800 text-slate-900 dark:text-slate-100 rounded-2xl shadow-2xl overflow-hidden my-8 max-h-[92vh] flex flex-col"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
|
||||
{/* Top Header Bar */}
|
||||
<div className="flex items-center justify-between px-6 py-4 border-b border-slate-800 bg-slate-950/60 sticky top-0 z-20">
|
||||
<div className="flex items-center justify-between px-6 py-4 border-b border-slate-200 dark:border-slate-800 bg-slate-50 dark:bg-slate-950/60 sticky top-0 z-20">
|
||||
<div className="flex items-center gap-3">
|
||||
<span className="text-xs font-semibold px-2.5 py-1 rounded bg-cyan-500/10 text-cyan-400 border border-cyan-500/20">
|
||||
<span className="text-xs font-semibold px-2.5 py-1 rounded bg-cyan-500/10 text-cyan-600 dark:text-cyan-400 border border-cyan-500/20">
|
||||
{course.category}
|
||||
</span>
|
||||
<span className="text-xs text-slate-400">• Nivel: {course.level}</span>
|
||||
<span className="text-xs text-slate-500 dark:text-slate-400">• Nivel: {course.level}</span>
|
||||
{isTeacherPreview && (
|
||||
<span className="text-xs font-bold px-2.5 py-1 rounded bg-amber-500/10 text-amber-600 dark:text-amber-400 border border-amber-500/20 flex items-center gap-1">
|
||||
<FiShield /> Vista Previa de Docente
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="p-2 rounded-xl text-slate-400 hover:text-white hover:bg-slate-800 transition"
|
||||
className="p-2 rounded-xl text-slate-400 hover:text-slate-900 dark:hover:text-white hover:bg-slate-200 dark:hover:bg-slate-800 transition"
|
||||
aria-label="Cerrar modal"
|
||||
>
|
||||
<FiX className="text-xl" />
|
||||
@@ -85,20 +93,20 @@ export const CourseModal: React.FC<CourseModalProps> = ({
|
||||
|
||||
{/* Main Info */}
|
||||
<div className="lg:col-span-7 space-y-4">
|
||||
<h2 className="text-2xl sm:text-3xl font-extrabold text-white leading-tight">
|
||||
<h2 className="text-2xl sm:text-3xl font-extrabold text-slate-900 dark:text-white leading-tight">
|
||||
{course.title}
|
||||
</h2>
|
||||
|
||||
<p className="text-slate-300 text-sm leading-relaxed">
|
||||
<p className="text-slate-600 dark:text-slate-300 text-sm leading-relaxed">
|
||||
{course.subtitle}
|
||||
</p>
|
||||
|
||||
{/* Meta stats */}
|
||||
<div className="flex flex-wrap items-center gap-4 text-xs pt-2 text-slate-400 border-t border-slate-800/80">
|
||||
<div className="flex items-center gap-1.5 text-amber-400 font-bold">
|
||||
<div className="flex flex-wrap items-center gap-4 text-xs pt-2 text-slate-500 dark:text-slate-400 border-t border-slate-200 dark:border-slate-800/80">
|
||||
<div className="flex items-center gap-1.5 text-amber-500 dark:text-amber-400 font-bold">
|
||||
<FiStar className="fill-amber-400" />
|
||||
<span>{course.rating.toFixed(1)}</span>
|
||||
<span className="text-slate-400 font-normal">({course.reviewsCount} reseñas)</span>
|
||||
<span className="text-slate-500 dark:text-slate-400 font-normal">({course.reviewsCount} reseñas)</span>
|
||||
</div>
|
||||
<div>👥 {course.studentsCount.toLocaleString()} estudiantes</div>
|
||||
<div className="flex items-center gap-1"><FiGlobe /> {course.language}</div>
|
||||
@@ -106,14 +114,14 @@ export const CourseModal: React.FC<CourseModalProps> = ({
|
||||
</div>
|
||||
|
||||
{/* What You Will Learn Card */}
|
||||
<div className="p-5 rounded-xl bg-slate-950/60 border border-slate-800 space-y-3">
|
||||
<h4 className="text-sm font-bold text-white flex items-center gap-2">
|
||||
<FiAward className="text-cyan-400" /> Lo que aprenderás en este curso
|
||||
<div className="p-5 rounded-xl bg-slate-50 dark:bg-slate-950/60 border border-slate-200 dark:border-slate-800 space-y-3">
|
||||
<h4 className="text-sm font-bold text-slate-900 dark:text-white flex items-center gap-2">
|
||||
<FiAward className="text-cyan-500 dark:text-cyan-400" /> Lo que aprenderás en este curso
|
||||
</h4>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-2.5 pt-2">
|
||||
{course.whatYouWillLearn.map((item, idx) => (
|
||||
<div key={idx} className="flex items-start gap-2 text-xs text-slate-300">
|
||||
<FiCheckCircle className="text-emerald-400 text-sm mt-0.5 shrink-0" />
|
||||
<div key={idx} className="flex items-start gap-2 text-xs text-slate-700 dark:text-slate-300">
|
||||
<FiCheckCircle className="text-emerald-500 dark:text-emerald-400 text-sm mt-0.5 shrink-0" />
|
||||
<span>{item}</span>
|
||||
</div>
|
||||
))}
|
||||
@@ -163,62 +171,82 @@ export const CourseModal: React.FC<CourseModalProps> = ({
|
||||
</div>
|
||||
|
||||
{/* Price & Checkout Card */}
|
||||
<div className="p-5 rounded-xl bg-slate-950/80 border border-slate-800 space-y-4 shadow-xl">
|
||||
<div className="p-5 rounded-xl bg-slate-50 dark:bg-slate-950/80 border border-slate-200 dark:border-slate-800 space-y-4 shadow-xl">
|
||||
<div className="flex items-baseline justify-between">
|
||||
<div className="flex items-baseline gap-2">
|
||||
<span className="text-3xl font-black text-white font-mono">
|
||||
<span className="text-3xl font-black text-slate-900 dark:text-white font-mono">
|
||||
${course.price.toFixed(2)}
|
||||
</span>
|
||||
{course.originalPrice > course.price && (
|
||||
<span className="text-sm text-slate-500 line-through font-mono">
|
||||
<span className="text-sm text-slate-400 line-through font-mono">
|
||||
${course.originalPrice.toFixed(2)}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
{discountPercent > 0 && (
|
||||
<span className="px-2 py-1 rounded bg-rose-500/20 border border-rose-500/30 text-rose-400 text-xs font-bold">
|
||||
<span className="px-2 py-1 rounded bg-rose-500/20 border border-rose-500/30 text-rose-500 dark:text-rose-400 text-xs font-bold">
|
||||
{discountPercent}% Descuento
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<button
|
||||
onClick={() => {
|
||||
onBuyNow(course);
|
||||
onClose();
|
||||
}}
|
||||
className="w-full py-3 bg-gradient-to-r from-emerald-600 to-teal-500 hover:from-emerald-500 hover:to-teal-400 text-white font-bold rounded-xl text-sm shadow-lg shadow-emerald-500/20 transition-all hover:scale-[1.01]"
|
||||
>
|
||||
Comprar Ahora
|
||||
</button>
|
||||
{/* If isTeacherPreview: Disable Buy & Cart buttons */}
|
||||
{isTeacherPreview ? (
|
||||
<div className="space-y-2 pt-1">
|
||||
<button
|
||||
disabled
|
||||
className="w-full py-3 bg-slate-200 dark:bg-slate-800 text-slate-500 dark:text-slate-400 font-bold rounded-xl text-xs border border-slate-300 dark:border-slate-700 cursor-not-allowed flex items-center justify-center gap-2 select-none"
|
||||
title="Este curso pertenece a tu propia cuenta de instructor"
|
||||
>
|
||||
<FiLock className="text-sm" /> No puedes comprar tu propio curso
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={() => onAddToCart(course)}
|
||||
disabled={isInCart}
|
||||
className={`w-full py-2.5 rounded-xl text-sm font-bold flex items-center justify-center gap-2 transition-all ${
|
||||
isInCart
|
||||
? 'bg-slate-800 text-slate-400 border border-slate-700 cursor-default'
|
||||
: 'bg-slate-800 hover:bg-slate-700 text-white border border-slate-700'
|
||||
}`}
|
||||
>
|
||||
<FiShoppingCart />
|
||||
<span>{isInCart ? 'Ya está en el carrito' : 'Añadir al Carrito'}</span>
|
||||
</button>
|
||||
</div>
|
||||
<button
|
||||
disabled
|
||||
className="w-full py-2.5 bg-slate-200/60 dark:bg-slate-800/60 text-slate-400 dark:text-slate-500 font-semibold rounded-xl text-xs border border-slate-300 dark:border-slate-800 cursor-not-allowed flex items-center justify-center gap-2 select-none"
|
||||
>
|
||||
<FiShoppingCart className="text-sm" /> Añadir al Carrito (Deshabilitado)
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-2">
|
||||
<button
|
||||
onClick={() => {
|
||||
onBuyNow(course);
|
||||
onClose();
|
||||
}}
|
||||
className="w-full py-3 bg-gradient-to-r from-emerald-600 to-teal-500 hover:from-emerald-500 hover:to-teal-400 text-white font-bold rounded-xl text-sm shadow-lg shadow-emerald-500/20 transition-all hover:scale-[1.01]"
|
||||
>
|
||||
Comprar Ahora
|
||||
</button>
|
||||
|
||||
<div className="space-y-1.5 text-[11px] text-slate-400 pt-2 border-t border-slate-800">
|
||||
<button
|
||||
onClick={() => onAddToCart(course)}
|
||||
disabled={isInCart}
|
||||
className={`w-full py-2.5 rounded-xl text-sm font-bold flex items-center justify-center gap-2 transition-all ${
|
||||
isInCart
|
||||
? 'bg-slate-200 dark:bg-slate-800 text-slate-400 dark:text-slate-400 border border-slate-300 dark:border-slate-700 cursor-default'
|
||||
: 'bg-slate-900 dark:bg-slate-800 hover:bg-slate-800 dark:hover:bg-slate-700 text-white border border-slate-800 dark:border-slate-700'
|
||||
}`}
|
||||
>
|
||||
<FiShoppingCart />
|
||||
<span>{isInCart ? 'Ya está en el carrito' : 'Añadir al Carrito'}</span>
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="space-y-1.5 text-[11px] text-slate-500 dark:text-slate-400 pt-2 border-t border-slate-200 dark:border-slate-800">
|
||||
<div className="flex items-center gap-2">
|
||||
<FiClock className="text-cyan-400" /> {course.duration} de video bajo demanda
|
||||
<FiClock className="text-cyan-500 dark:text-cyan-400" /> {course.duration} de video bajo demanda
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<FiBook className="text-cyan-400" /> {course.lessonsCount} lecciones descargables
|
||||
<FiBook className="text-cyan-500 dark:text-cyan-400" /> {course.lessonsCount} lecciones descargables
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<FiShield className="text-emerald-400" /> Garantía de 30 días o te devolvemos el dinero
|
||||
<FiShield className="text-emerald-500 dark:text-emerald-400" /> Garantía de 30 días o te devolvemos el dinero
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<FiAward className="text-cyan-400" /> Certificado de acreditación oficial
|
||||
<FiAward className="text-cyan-500 dark:text-cyan-400" /> Certificado de acreditación oficial
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -227,13 +255,13 @@ export const CourseModal: React.FC<CourseModalProps> = ({
|
||||
</div>
|
||||
|
||||
{/* Nav Tabs inside modal */}
|
||||
<div className="border-b border-slate-800 flex gap-6 text-sm font-semibold">
|
||||
<div className="border-b border-slate-200 dark:border-slate-800 flex gap-6 text-sm font-semibold">
|
||||
<button
|
||||
onClick={() => setActiveTab('curriculum')}
|
||||
className={`pb-3 transition relative ${
|
||||
activeTab === 'curriculum'
|
||||
? 'text-cyan-400 border-b-2 border-cyan-400'
|
||||
: 'text-slate-400 hover:text-slate-200'
|
||||
? 'text-cyan-600 dark:text-cyan-400 border-b-2 border-cyan-500'
|
||||
: 'text-slate-500 dark:text-slate-400 hover:text-slate-900 dark:hover:text-slate-200'
|
||||
}`}
|
||||
>
|
||||
Contenido del Curso ({course.modules.length} Módulos)
|
||||
@@ -242,8 +270,8 @@ export const CourseModal: React.FC<CourseModalProps> = ({
|
||||
onClick={() => setActiveTab('instructor')}
|
||||
className={`pb-3 transition relative ${
|
||||
activeTab === 'instructor'
|
||||
? 'text-cyan-400 border-b-2 border-cyan-400'
|
||||
: 'text-slate-400 hover:text-slate-200'
|
||||
? 'text-cyan-600 dark:text-cyan-400 border-b-2 border-cyan-500'
|
||||
: 'text-slate-500 dark:text-slate-400 hover:text-slate-900 dark:hover:text-slate-200'
|
||||
}`}
|
||||
>
|
||||
Instructor
|
||||
@@ -252,8 +280,8 @@ export const CourseModal: React.FC<CourseModalProps> = ({
|
||||
onClick={() => setActiveTab('reviews')}
|
||||
className={`pb-3 transition relative ${
|
||||
activeTab === 'reviews'
|
||||
? 'text-cyan-400 border-b-2 border-cyan-400'
|
||||
: 'text-slate-400 hover:text-slate-200'
|
||||
? 'text-cyan-600 dark:text-cyan-400 border-b-2 border-cyan-500'
|
||||
: 'text-slate-500 dark:text-slate-400 hover:text-slate-900 dark:hover:text-slate-200'
|
||||
}`}
|
||||
>
|
||||
Valoraciones ({course.reviews.length})
|
||||
@@ -263,7 +291,7 @@ export const CourseModal: React.FC<CourseModalProps> = ({
|
||||
{/* Tab Content 1: Curriculum */}
|
||||
{activeTab === 'curriculum' && (
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center justify-between text-xs text-slate-400">
|
||||
<div className="flex items-center justify-between text-xs text-slate-500 dark:text-slate-400">
|
||||
<span>{course.modules.length} módulos • {course.lessonsCount} lecciones • {course.duration} duración total</span>
|
||||
</div>
|
||||
|
||||
@@ -271,29 +299,29 @@ export const CourseModal: React.FC<CourseModalProps> = ({
|
||||
{course.modules.map((mod) => {
|
||||
const isExpanded = !!expandedModules[mod.id];
|
||||
return (
|
||||
<div key={mod.id} className="rounded-xl border border-slate-800 overflow-hidden bg-slate-950/40">
|
||||
<div key={mod.id} className="rounded-xl border border-slate-200 dark:border-slate-800 overflow-hidden bg-slate-50 dark:bg-slate-950/40">
|
||||
<button
|
||||
onClick={() => toggleModule(mod.id)}
|
||||
className="w-full flex items-center justify-between p-4 bg-slate-900/60 hover:bg-slate-900 transition text-left"
|
||||
className="w-full flex items-center justify-between p-4 bg-slate-100 dark:bg-slate-900/60 hover:bg-slate-200 dark:hover:bg-slate-900 transition text-left"
|
||||
>
|
||||
<div className="font-bold text-sm text-slate-200 flex items-center gap-2">
|
||||
{isExpanded ? <FiChevronUp className="text-cyan-400" /> : <FiChevronDown className="text-slate-500" />}
|
||||
<div className="font-bold text-sm text-slate-800 dark:text-slate-200 flex items-center gap-2">
|
||||
{isExpanded ? <FiChevronUp className="text-cyan-500 dark:text-cyan-400" /> : <FiChevronDown className="text-slate-400 dark:text-slate-500" />}
|
||||
<span>{mod.title}</span>
|
||||
</div>
|
||||
<span className="text-xs text-slate-400 font-mono">
|
||||
<span className="text-xs text-slate-500 dark:text-slate-400 font-mono">
|
||||
{mod.lessons.length} lecciones
|
||||
</span>
|
||||
</button>
|
||||
|
||||
{isExpanded && (
|
||||
<div className="divide-y divide-slate-850 p-2 space-y-1">
|
||||
<div className="divide-y divide-slate-200 dark:divide-slate-800 p-2 space-y-1">
|
||||
{mod.lessons.map((lesson) => (
|
||||
<div
|
||||
key={lesson.id}
|
||||
className="flex items-center justify-between p-2.5 rounded-lg hover:bg-slate-900/80 text-xs transition"
|
||||
className="flex items-center justify-between p-2.5 rounded-lg hover:bg-slate-100 dark:hover:bg-slate-900/80 text-xs transition"
|
||||
>
|
||||
<div className="flex items-center gap-2.5 text-slate-300">
|
||||
<div className="w-5 h-5 rounded-full bg-slate-800 text-slate-400 flex items-center justify-center text-[10px]">
|
||||
<div className="flex items-center gap-2.5 text-slate-700 dark:text-slate-300">
|
||||
<div className="w-5 h-5 rounded-full bg-slate-200 dark:bg-slate-800 text-slate-500 dark:text-slate-400 flex items-center justify-center text-[10px]">
|
||||
<FiPlay />
|
||||
</div>
|
||||
<span>{lesson.title}</span>
|
||||
@@ -301,11 +329,11 @@ export const CourseModal: React.FC<CourseModalProps> = ({
|
||||
|
||||
<div className="flex items-center gap-3">
|
||||
{lesson.isFreePreview && (
|
||||
<span className="px-2 py-0.5 rounded bg-cyan-500/20 text-cyan-300 border border-cyan-500/30 text-[10px] font-bold">
|
||||
<span className="px-2 py-0.5 rounded bg-cyan-500/20 text-cyan-700 dark:text-cyan-300 border border-cyan-500/30 text-[10px] font-bold">
|
||||
Vista Previa
|
||||
</span>
|
||||
)}
|
||||
<span className="text-slate-400 font-mono">{lesson.duration}</span>
|
||||
<span className="text-slate-500 dark:text-slate-400 font-mono">{lesson.duration}</span>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
@@ -320,7 +348,7 @@ export const CourseModal: React.FC<CourseModalProps> = ({
|
||||
|
||||
{/* Tab Content 2: Instructor */}
|
||||
{activeTab === 'instructor' && (
|
||||
<div className="p-6 rounded-xl bg-slate-950/60 border border-slate-800 space-y-4">
|
||||
<div className="p-6 rounded-xl bg-slate-50 dark:bg-slate-950/60 border border-slate-200 dark:border-slate-800 space-y-4">
|
||||
<div className="flex flex-col sm:flex-row items-start sm:items-center gap-4">
|
||||
<img
|
||||
src={course.instructor.avatar}
|
||||
@@ -328,11 +356,11 @@ export const CourseModal: React.FC<CourseModalProps> = ({
|
||||
className="w-16 h-16 rounded-full object-cover ring-2 ring-cyan-500/50"
|
||||
/>
|
||||
<div>
|
||||
<h3 className="text-lg font-bold text-white">{course.instructor.name}</h3>
|
||||
<p className="text-xs text-cyan-400 font-medium">{course.instructor.role}</p>
|
||||
<h3 className="text-lg font-bold text-slate-900 dark:text-white">{course.instructor.name}</h3>
|
||||
<p className="text-xs text-cyan-600 dark:text-cyan-400 font-medium">{course.instructor.role}</p>
|
||||
|
||||
<div className="flex items-center gap-4 text-xs text-slate-400 mt-2">
|
||||
<span className="flex items-center gap-1 text-amber-400 font-bold">
|
||||
<div className="flex items-center gap-4 text-xs text-slate-500 dark:text-slate-400 mt-2">
|
||||
<span className="flex items-center gap-1 text-amber-500 dark:text-amber-400 font-bold">
|
||||
<FiStar className="fill-amber-400" /> {course.instructor.rating} Calificación
|
||||
</span>
|
||||
<span>👨🎓 {course.instructor.totalStudents.toLocaleString()} Estudiantes</span>
|
||||
@@ -341,7 +369,7 @@ export const CourseModal: React.FC<CourseModalProps> = ({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p className="text-xs text-slate-300 leading-relaxed border-t border-slate-800 pt-4">
|
||||
<p className="text-xs text-slate-700 dark:text-slate-300 leading-relaxed border-t border-slate-200 dark:border-slate-800 pt-4">
|
||||
{course.instructor.bio}
|
||||
</p>
|
||||
</div>
|
||||
@@ -351,7 +379,7 @@ export const CourseModal: React.FC<CourseModalProps> = ({
|
||||
{activeTab === 'reviews' && (
|
||||
<div className="space-y-4">
|
||||
{course.reviews.map((rev) => (
|
||||
<div key={rev.id} className="p-4 rounded-xl bg-slate-950/60 border border-slate-800 space-y-2">
|
||||
<div key={rev.id} className="p-4 rounded-xl bg-slate-50 dark:bg-slate-950/60 border border-slate-200 dark:border-slate-800 space-y-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2.5">
|
||||
<img
|
||||
@@ -360,7 +388,7 @@ export const CourseModal: React.FC<CourseModalProps> = ({
|
||||
className="w-8 h-8 rounded-full object-cover"
|
||||
/>
|
||||
<div>
|
||||
<h4 className="text-xs font-bold text-white">{rev.userName}</h4>
|
||||
<h4 className="text-xs font-bold text-slate-900 dark:text-white">{rev.userName}</h4>
|
||||
<span className="text-[10px] text-slate-500">{rev.date}</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -372,7 +400,7 @@ export const CourseModal: React.FC<CourseModalProps> = ({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p className="text-xs text-slate-300 leading-relaxed pl-10">
|
||||
<p className="text-xs text-slate-700 dark:text-slate-300 leading-relaxed pl-10">
|
||||
"{rev.comment}"
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -21,7 +21,7 @@ export const BrandLogo: React.FC<BrandLogoProps> = ({ size = 'md', showSubtitle
|
||||
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`}>
|
||||
<div className={`relative ${iconSizes[size]} p-0.5 rounded-xl bg-slate-100 dark:bg-slate-900 border border-slate-200 dark:border-slate-700/80 shadow-md group-hover:border-cyan-500/60 group-hover:scale-105 transition-all duration-300 overflow-hidden`}>
|
||||
<img
|
||||
src="/logo.png"
|
||||
alt="RyCourses Emblem"
|
||||
@@ -32,12 +32,12 @@ export const BrandLogo: React.FC<BrandLogoProps> = ({ size = 'md', showSubtitle
|
||||
{/* 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 className={`${textSizes[size]} font-extrabold tracking-tight text-slate-900 dark:text-white group-hover:text-cyan-600 dark:group-hover:text-cyan-300 transition-colors`}>
|
||||
Ry<span className="text-transparent bg-clip-text bg-gradient-to-r from-cyan-500 via-sky-500 to-blue-500 dark:from-cyan-400 dark:to-blue-400">Courses</span>
|
||||
</span>
|
||||
</div>
|
||||
{showSubtitle && (
|
||||
<p className="text-[11px] text-slate-400 font-medium tracking-wide">Plataforma Educativa</p>
|
||||
<p className="text-[11px] text-slate-500 dark:text-slate-400 font-medium tracking-wide">Plataforma Educativa</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import React from 'react';
|
||||
import { Link } from 'react-router';
|
||||
import {
|
||||
FiGlobe,
|
||||
FiShield,
|
||||
@@ -74,8 +75,8 @@ export const Footer: React.FC = () => {
|
||||
<ul className="space-y-2.5 text-xs">
|
||||
<li><a href="#" className="hover:text-cyan-300 transition">Sobre Nosotros</a></li>
|
||||
<li><a href="#" className="hover:text-cyan-300 transition">RyCourses para Empresas</a></li>
|
||||
<li><a href="#" className="hover:text-cyan-300 transition">Términos de Servicio</a></li>
|
||||
<li><a href="#" className="hover:text-cyan-300 transition">Política de Privacidad</a></li>
|
||||
<li><Link to="/terms" className="hover:text-cyan-300 transition">Términos de Servicio</Link></li>
|
||||
<li><Link to="/terms" className="hover:text-cyan-300 transition">Política de Privacidad</Link></li>
|
||||
<li><a href="#" className="hover:text-cyan-300 transition">Centro de Ayuda</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
@@ -1,19 +1,16 @@
|
||||
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { useNavigate, useLocation } from 'react-router';
|
||||
import {
|
||||
FiSearch,
|
||||
FiShoppingCart,
|
||||
FiMenu,
|
||||
FiX,
|
||||
FiPlusCircle,
|
||||
FiHome,
|
||||
FiGrid,
|
||||
FiUser,
|
||||
FiShield,
|
||||
FiUser
|
||||
} from 'react-icons/fi';
|
||||
import { FaChalkboardTeacher, FaSignOutAlt } from 'react-icons/fa';
|
||||
import { BrandLogo } from '../common/BrandLogo';
|
||||
import { ThemeToggle, Button } from '../ui';
|
||||
import type { User } from '../../types/course';
|
||||
|
||||
interface NavbarProps {
|
||||
@@ -31,15 +28,13 @@ export const Navbar: React.FC<NavbarProps> = ({
|
||||
onLogout,
|
||||
cartCount,
|
||||
onOpenCart,
|
||||
searchQuery,
|
||||
onSearchChange,
|
||||
}) => {
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
const [mobileMenuOpen, setMobileMenuOpen] = useState(false);
|
||||
const [cartHovered, setCartHovered] = useState(false);
|
||||
|
||||
// Inject keyframe animation styles inline (no external CSS needed)
|
||||
// Inject keyframe animation styles inline
|
||||
useEffect(() => {
|
||||
const styleId = 'cart-run-keyframes';
|
||||
if (!document.getElementById(styleId)) {
|
||||
@@ -64,17 +59,9 @@ export const Navbar: React.FC<NavbarProps> = ({
|
||||
}, []);
|
||||
|
||||
const currentPath = location.pathname;
|
||||
const isSellerMode = currentPath === '/seller' || currentPath.startsWith('/seller/');
|
||||
|
||||
const handleSearchInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
onSearchChange(e.target.value);
|
||||
if (currentPath !== '/courses' && e.target.value.trim() !== '') {
|
||||
navigate('/courses');
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<header className="sticky top-0 z-40 w-full border-b border-slate-800/80 bg-slate-950/90 backdrop-blur-xl transition-all">
|
||||
<header className="sticky top-0 z-40 w-full border-b backdrop-blur-xl transition-all border-slate-200 dark:border-slate-800/80 bg-white/90 dark:bg-slate-950/90 text-slate-900 dark:text-slate-100">
|
||||
<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">
|
||||
|
||||
@@ -84,14 +71,15 @@ export const Navbar: React.FC<NavbarProps> = ({
|
||||
<BrandLogo size="md" />
|
||||
</div>
|
||||
|
||||
{/* Quick Nav Links */}
|
||||
{/* Main Nav Links */}
|
||||
<nav className="hidden lg:flex items-center gap-1">
|
||||
<button
|
||||
onClick={() => navigate('/')}
|
||||
className={`flex items-center gap-1.5 px-3 py-2 rounded-xl text-xs font-semibold transition-all ${currentPath === '/'
|
||||
? 'bg-slate-800 text-cyan-400 border border-cyan-500/30'
|
||||
: 'text-slate-300 hover:text-white hover:bg-slate-900'
|
||||
}`}
|
||||
className={`flex items-center gap-1.5 px-3.5 py-2 rounded-xl text-xs font-semibold transition-all ${
|
||||
currentPath === '/'
|
||||
? 'bg-slate-100 dark:bg-slate-800 text-cyan-600 dark:text-cyan-400 border border-slate-200 dark:border-cyan-500/30'
|
||||
: 'text-slate-600 dark:text-slate-300 hover:text-slate-900 dark:hover:text-white hover:bg-slate-100 dark:hover:bg-slate-900'
|
||||
}`}
|
||||
>
|
||||
<FiHome />
|
||||
<span>Inicio</span>
|
||||
@@ -99,91 +87,62 @@ export const Navbar: React.FC<NavbarProps> = ({
|
||||
|
||||
<button
|
||||
onClick={() => navigate('/courses')}
|
||||
className={`flex items-center gap-1.5 px-3 py-2 rounded-xl text-xs font-semibold transition-all ${currentPath === '/courses'
|
||||
? 'bg-slate-800 text-cyan-400 border border-cyan-500/30'
|
||||
: 'text-slate-300 hover:text-white hover:bg-slate-900'
|
||||
}`}
|
||||
className={`flex items-center gap-1.5 px-3.5 py-2 rounded-xl text-xs font-semibold transition-all ${
|
||||
currentPath === '/courses'
|
||||
? 'bg-slate-100 dark:bg-slate-800 text-cyan-600 dark:text-cyan-400 border border-slate-200 dark:border-cyan-500/30'
|
||||
: 'text-slate-600 dark:text-slate-300 hover:text-slate-900 dark:hover:text-white hover:bg-slate-100 dark:hover:bg-slate-900'
|
||||
}`}
|
||||
>
|
||||
<FiGrid />
|
||||
<span>Catálogo</span>
|
||||
</button>
|
||||
|
||||
{/* Landing link for instructor sales */}
|
||||
<button
|
||||
onClick={() => navigate('/admin')}
|
||||
className={`flex items-center gap-1.5 px-3 py-2 rounded-xl text-xs font-semibold transition-all ${currentPath === '/admin'
|
||||
? 'bg-slate-800 text-cyan-400 border border-cyan-500/30'
|
||||
: 'text-slate-300 hover:text-white hover:bg-slate-900'
|
||||
}`}
|
||||
onClick={() => navigate('/seller')}
|
||||
className={`flex items-center gap-1.5 px-3.5 py-2 rounded-xl text-xs font-semibold transition-all ${
|
||||
currentPath === '/seller'
|
||||
? 'bg-slate-100 dark:bg-slate-800 text-violet-600 dark:text-violet-400 border border-slate-200 dark:border-violet-500/30'
|
||||
: 'text-slate-600 dark:text-slate-300 hover:text-slate-900 dark:hover:text-white hover:bg-slate-100 dark:hover:bg-slate-900'
|
||||
}`}
|
||||
>
|
||||
<FiShield />
|
||||
<span>Admin</span>
|
||||
<FaChalkboardTeacher />
|
||||
<span>Vender Cursos</span>
|
||||
</button>
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
{/* 3. Right Actions & Role Switcher */}
|
||||
<div className="flex items-center gap-2.5 shrink-0">
|
||||
|
||||
{/* Buyer vs Seller Switcher pill */}
|
||||
<div className="bg-slate-900 p-1 rounded-xl border border-slate-800 flex items-center shadow-inner">
|
||||
<button
|
||||
onClick={() => navigate('/courses')}
|
||||
className={`flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-xs font-semibold transition-all ${!isSellerMode
|
||||
? 'bg-gradient-to-r from-blue-600 to-cyan-500 text-white shadow-md shadow-cyan-500/20'
|
||||
: 'text-slate-400 hover:text-slate-200'
|
||||
}`}
|
||||
title="Modo Estudiante - Explorar y Comprar cursos"
|
||||
>
|
||||
<FiGrid className="text-xs" />
|
||||
<span className="hidden sm:inline">Comprar</span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={() => navigate('/seller')}
|
||||
className={`flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-xs font-semibold transition-all ${isSellerMode
|
||||
? 'bg-gradient-to-r from-indigo-600 to-violet-500 text-white shadow-md shadow-violet-500/20'
|
||||
: 'text-slate-400 hover:text-slate-200'
|
||||
}`}
|
||||
title="Modo Instructor - Vender y Gestionar cursos"
|
||||
>
|
||||
<FaChalkboardTeacher className="text-xs" />
|
||||
<span className="hidden sm:inline">Vender</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* If seller mode: Create Course button */}
|
||||
{isSellerMode && (
|
||||
<button
|
||||
onClick={() => navigate('/seller/create')}
|
||||
className="hidden sm:flex items-center gap-1.5 px-3.5 py-2 rounded-xl bg-gradient-to-r from-violet-600 to-indigo-600 hover:from-violet-500 hover:to-indigo-500 text-white font-medium text-xs shadow-lg shadow-violet-600/30 transition-all"
|
||||
>
|
||||
<FiPlusCircle className="text-sm" />
|
||||
<span>Crear Curso</span>
|
||||
</button>
|
||||
)}
|
||||
{/* 2. Right Actions & ThemeToggle Beside Cart */}
|
||||
<div className="flex items-center gap-3 shrink-0">
|
||||
|
||||
{/* Login & Register Buttons OR User Profile */}
|
||||
{!currentUser ? (
|
||||
<div className="hidden xl:flex items-center gap-2 pl-2 border-l border-slate-800">
|
||||
<button
|
||||
<div className="flex items-center gap-2">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
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>
|
||||
<Button
|
||||
variant="gradient"
|
||||
size="sm"
|
||||
onClick={() => navigate('/register')}
|
||||
className="px-3.5 py-1.5 rounded-xl bg-cyan-500/10 hover:bg-cyan-500/20 text-cyan-400 font-semibold text-xs border border-cyan-500/30 transition"
|
||||
>
|
||||
Registrarse
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex items-center gap-2 pl-2 border-l border-slate-800 relative group">
|
||||
<div className="flex items-center gap-2 pl-2 border-l border-slate-200 dark:border-slate-800 relative group">
|
||||
<div className="relative cursor-pointer flex items-center gap-2" title="Mi Cuenta">
|
||||
<div className="hidden sm:block text-right">
|
||||
<p className="text-[11px] font-bold text-slate-200 leading-none">{currentUser.name}</p>
|
||||
<p className="text-[9px] text-cyan-400 font-medium capitalize mt-0.5">{currentUser.role === 'buyer' ? 'Estudiante' : 'Instructor'}</p>
|
||||
<p className="text-[11px] font-bold leading-none text-slate-800 dark:text-slate-200">
|
||||
{currentUser.name}
|
||||
</p>
|
||||
<p className="text-[9px] text-cyan-600 dark:text-cyan-400 font-medium capitalize mt-0.5">
|
||||
{currentUser.role === 'buyer' ? 'Estudiante' : 'Instructor'}
|
||||
</p>
|
||||
</div>
|
||||
<div className="relative">
|
||||
{currentUser.avatar ? (
|
||||
@@ -193,18 +152,18 @@ export const Navbar: React.FC<NavbarProps> = ({
|
||||
className="w-9 h-9 rounded-full object-cover ring-2 ring-indigo-500/50 group-hover:ring-cyan-400 transition"
|
||||
/>
|
||||
) : (
|
||||
<div className="w-9 h-9 rounded-full bg-slate-800 flex items-center justify-center text-sm font-bold text-slate-300 ring-2 ring-indigo-500/50 group-hover:ring-cyan-400 transition">
|
||||
<div className="w-9 h-9 rounded-full flex items-center justify-center text-sm font-bold ring-2 ring-indigo-500/50 group-hover:ring-cyan-400 transition bg-slate-200 dark:bg-slate-800 text-slate-800 dark:text-slate-300">
|
||||
{currentUser.name.charAt(0).toUpperCase()}
|
||||
</div>
|
||||
)}
|
||||
<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-white dark:ring-slate-950 rounded-full"></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Dropdown Menu */}
|
||||
<div className="absolute top-full right-0 mt-2 w-48 bg-slate-900 border border-slate-800 rounded-xl shadow-2xl opacity-0 invisible group-hover:opacity-100 group-hover:visible transition-all duration-200 transform origin-top-right scale-95 group-hover:scale-100 z-50">
|
||||
<div className="absolute top-full right-0 mt-2 w-48 border rounded-xl shadow-2xl opacity-0 invisible group-hover:opacity-100 group-hover:visible transition-all duration-200 transform origin-top-right scale-95 group-hover:scale-100 z-50 bg-white dark:bg-slate-900 border-slate-200 dark:border-slate-800">
|
||||
<div className="p-2 space-y-1">
|
||||
<button onClick={onLogout} className="w-full flex items-center gap-2 px-3 py-2 text-xs font-semibold text-rose-400 hover:text-rose-300 hover:bg-rose-500/10 rounded-lg transition">
|
||||
<button onClick={onLogout} className="w-full flex items-center gap-2 px-3 py-2 text-xs font-semibold text-rose-500 hover:text-rose-600 hover:bg-rose-500/10 rounded-lg transition">
|
||||
<FaSignOutAlt />
|
||||
<span>Cerrar Sesión</span>
|
||||
</button>
|
||||
@@ -213,16 +172,17 @@ export const Navbar: React.FC<NavbarProps> = ({
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Cart button (Buyer mode) - Fixed Far Right, inline animation */}
|
||||
{!isSellerMode && (
|
||||
{/* ThemeToggle and Cart Button Grouped Together */}
|
||||
<div className="flex items-center gap-2.5 pl-2 border-l border-slate-200 dark:border-slate-800 relative">
|
||||
<ThemeToggle variant="icon" size="md" />
|
||||
|
||||
<button
|
||||
onClick={onOpenCart}
|
||||
onMouseEnter={() => setCartHovered(true)}
|
||||
onMouseLeave={() => setCartHovered(false)}
|
||||
className="group fixed top-4 right-4 z-50 p-3 text-slate-300 hover:text-white hover:bg-slate-800 rounded-xl transition-all shadow-lg shadow-black/30 backdrop-blur-sm overflow-hidden"
|
||||
className="group relative p-2.5 rounded-xl transition-all shadow-md backdrop-blur-sm text-slate-700 dark:text-slate-300 hover:text-slate-900 dark:hover:text-white bg-white dark:bg-slate-900 border border-slate-200 dark:border-slate-800 hover:border-cyan-500/50"
|
||||
aria-label="Carrito de compras"
|
||||
>
|
||||
{/* Cart icon with inline running animation */}
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
@@ -232,9 +192,9 @@ export const Navbar: React.FC<NavbarProps> = ({
|
||||
animation: cartHovered ? 'cart-run 0.45s ease-in-out infinite' : 'none',
|
||||
}}
|
||||
>
|
||||
<FiShoppingCart style={{ fontSize: '1.2rem' }} />
|
||||
<FiShoppingCart className="text-lg" />
|
||||
|
||||
{/* Speed streaks — subtle, contained inside icon area */}
|
||||
{/* Speed streaks */}
|
||||
<span style={{
|
||||
position: 'absolute',
|
||||
top: '25%',
|
||||
@@ -270,89 +230,61 @@ export const Navbar: React.FC<NavbarProps> = ({
|
||||
}} />
|
||||
</div>
|
||||
|
||||
{/* Badge counter */}
|
||||
{/* Badge counter: Perfectly placed floating badge */}
|
||||
{cartCount > 0 && (
|
||||
<span
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: '-8px',
|
||||
right: '-8px',
|
||||
width: '20px',
|
||||
height: '20px',
|
||||
background: 'linear-gradient(135deg, #f43f5e, #ec4899)',
|
||||
color: 'white',
|
||||
fontSize: '11px',
|
||||
fontWeight: 700,
|
||||
borderRadius: '9999px',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
boxShadow: '0 2px 8px rgba(244,63,94,0.5)',
|
||||
zIndex: 20,
|
||||
transform: cartHovered ? 'scale(1.2)' : 'scale(1)',
|
||||
transition: 'transform 0.15s ease',
|
||||
}}
|
||||
>
|
||||
<span className="absolute -top-1.5 -right-1.5 min-w-[20px] h-[20px] px-1 bg-gradient-to-r from-rose-500 to-pink-500 text-white text-[11px] font-bold rounded-full flex items-center justify-center shadow-lg shadow-rose-500/40 ring-2 ring-white dark:ring-slate-950 z-30 transition-transform duration-200 group-hover:scale-110">
|
||||
{cartCount}
|
||||
</span>
|
||||
)}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Mobile menu trigger */}
|
||||
<button
|
||||
onClick={() => setMobileMenuOpen(!mobileMenuOpen)}
|
||||
className="p-2 text-slate-400 hover:text-white md:hidden"
|
||||
className="p-2 md:hidden text-slate-600 dark:text-slate-400 hover:text-slate-900 dark:hover:text-white"
|
||||
>
|
||||
{mobileMenuOpen ? <FiX className="text-2xl" /> : <FiMenu className="text-2xl" />}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Mobile search & menu expanded */}
|
||||
{/* Mobile menu expanded */}
|
||||
{mobileMenuOpen && (
|
||||
<div className="md:hidden py-4 border-t border-slate-800 space-y-4">
|
||||
<div className="relative">
|
||||
<FiSearch className="absolute left-3.5 top-3 text-slate-400" />
|
||||
<input
|
||||
type="text"
|
||||
value={searchQuery}
|
||||
onChange={handleSearchInputChange}
|
||||
placeholder="Buscar cursos..."
|
||||
className="w-full pl-10 pr-4 py-2 bg-slate-900 border border-slate-700 rounded-lg text-sm text-white"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="md:hidden py-4 border-t space-y-4 border-slate-200 dark:border-slate-800">
|
||||
<div className="grid grid-cols-3 gap-2">
|
||||
<button
|
||||
onClick={() => { navigate('/'); setMobileMenuOpen(false); }}
|
||||
className={`flex items-center justify-center gap-1.5 p-2 rounded-lg text-xs font-semibold ${currentPath === '/' ? 'bg-blue-600 text-white' : 'bg-slate-800 text-slate-300'
|
||||
}`}
|
||||
className={`flex items-center justify-center gap-1.5 p-2 rounded-lg text-xs font-semibold ${
|
||||
currentPath === '/' ? 'bg-blue-600 text-white' : 'bg-slate-100 dark:bg-slate-800 text-slate-700 dark:text-slate-300'
|
||||
}`}
|
||||
>
|
||||
<FiHome /> Inicio
|
||||
</button>
|
||||
<button
|
||||
onClick={() => { navigate('/courses'); setMobileMenuOpen(false); }}
|
||||
className={`flex items-center justify-center gap-1.5 p-2 rounded-lg text-xs font-semibold ${currentPath === '/courses' ? 'bg-cyan-600 text-white' : 'bg-slate-800 text-slate-300'
|
||||
}`}
|
||||
className={`flex items-center justify-center gap-1.5 p-2 rounded-lg text-xs font-semibold ${
|
||||
currentPath === '/courses' ? 'bg-cyan-600 text-white' : 'bg-slate-100 dark:bg-slate-800 text-slate-700 dark:text-slate-300'
|
||||
}`}
|
||||
>
|
||||
<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'
|
||||
}`}
|
||||
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-100 dark:bg-slate-800 text-slate-700 dark:text-slate-300'
|
||||
}`}
|
||||
>
|
||||
<FaChalkboardTeacher /> Vender
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-2 pt-2 border-t border-slate-800">
|
||||
<div className="grid grid-cols-2 gap-2 pt-2 border-t border-slate-200 dark:border-slate-800">
|
||||
{!currentUser ? (
|
||||
<>
|
||||
<button
|
||||
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"
|
||||
className="w-full py-2.5 font-semibold rounded-lg text-xs flex items-center justify-center gap-2 bg-slate-100 dark:bg-slate-800 text-slate-900 dark:text-white"
|
||||
>
|
||||
<FiUser /> Iniciar Sesión
|
||||
</button>
|
||||
@@ -365,36 +297,27 @@ export const Navbar: React.FC<NavbarProps> = ({
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<div className="w-full py-2.5 px-3 bg-slate-800 rounded-lg flex items-center gap-3">
|
||||
<div className="w-full py-2.5 px-3 rounded-lg flex items-center gap-3 bg-slate-100 dark:bg-slate-800">
|
||||
{currentUser.avatar ? (
|
||||
<img src={currentUser.avatar} alt="Avatar" className="w-6 h-6 rounded-full object-cover" />
|
||||
) : (
|
||||
<div className="w-6 h-6 rounded-full bg-slate-700 flex items-center justify-center text-[10px] font-bold text-white">
|
||||
<div className="w-6 h-6 rounded-full bg-slate-200 dark:bg-slate-700 flex items-center justify-center text-[10px] font-bold text-slate-800 dark:text-white">
|
||||
{currentUser.name.charAt(0).toUpperCase()}
|
||||
</div>
|
||||
)}
|
||||
<div className="text-left">
|
||||
<p className="text-[10px] font-bold text-white truncate max-w-[100px]">{currentUser.name}</p>
|
||||
<p className="text-[10px] font-bold truncate max-w-[100px] text-slate-900 dark:text-white">{currentUser.name}</p>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => { onLogout(); setMobileMenuOpen(false); }}
|
||||
className="w-full py-2.5 bg-rose-500/10 text-rose-400 font-semibold rounded-lg text-xs flex items-center justify-center gap-2 border border-rose-500/20"
|
||||
className="w-full py-2.5 bg-rose-500/10 text-rose-500 font-semibold rounded-lg text-xs flex items-center justify-center gap-2 border border-rose-500/20"
|
||||
>
|
||||
<FaSignOutAlt /> Cerrar Sesión
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{isSellerMode && (
|
||||
<button
|
||||
onClick={() => { navigate('/seller/create'); setMobileMenuOpen(false); }}
|
||||
className="w-full py-2.5 bg-gradient-to-r from-violet-600 to-indigo-600 text-white font-semibold rounded-lg text-xs flex items-center justify-center gap-2"
|
||||
>
|
||||
<FiPlusCircle /> Crear Nuevo Curso
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useState } from 'react';
|
||||
import React, { useState, useRef } from 'react';
|
||||
import {
|
||||
FiX,
|
||||
FiCheck,
|
||||
@@ -7,7 +7,12 @@ import {
|
||||
FiBookOpen,
|
||||
FiDollarSign,
|
||||
FiPlus,
|
||||
FiCheckCircle
|
||||
FiCheckCircle,
|
||||
FiUploadCloud,
|
||||
FiVideo,
|
||||
FiFilm,
|
||||
FiLink,
|
||||
FiImage
|
||||
} from 'react-icons/fi';
|
||||
import type { Course, CourseLevel } from '../../types/course';
|
||||
import { mockInstructors } from '../../data/mockCourses';
|
||||
@@ -24,6 +29,8 @@ export const CreateCourseModal: React.FC<CreateCourseModalProps> = ({
|
||||
onCreateCourse,
|
||||
}) => {
|
||||
const [step, setStep] = useState<1 | 2 | 3>(1);
|
||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||
const imageInputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
// Form State
|
||||
const [title, setTitle] = useState('');
|
||||
@@ -32,10 +39,22 @@ export const CreateCourseModal: React.FC<CreateCourseModalProps> = ({
|
||||
const [level, setLevel] = useState<CourseLevel>('Todos los niveles');
|
||||
const [price, setPrice] = useState('29.99');
|
||||
const [originalPrice, setOriginalPrice] = useState('99.99');
|
||||
|
||||
// Thumbnail Image Drag & Drop State
|
||||
const [thumbnailFile, setThumbnailFile] = useState<File | null>(null);
|
||||
const [thumbnail, setThumbnail] = useState(
|
||||
'https://images.unsplash.com/photo-1516321318423-f06f85e504b3?w=800&auto=format&fit=crop&q=80'
|
||||
);
|
||||
|
||||
const [duration, setDuration] = useState('20h 30min');
|
||||
|
||||
// Video Upload States
|
||||
const [videoFile, setVideoFile] = useState<File | null>(null);
|
||||
const [videoPreviewUrl, setVideoPreviewUrl] = useState<string>('');
|
||||
const [videoEmbedUrl, setVideoEmbedUrl] = useState<string>('');
|
||||
const [isUploading, setIsUploading] = useState(false);
|
||||
const [uploadProgress, setUploadProgress] = useState(0);
|
||||
|
||||
const [whatYouWillLearn, setWhatYouWillLearn] = useState([
|
||||
'Aprenderás conceptos clave y buenas prácticas de la industria',
|
||||
'Construirás proyectos prácticos de portafolio para conseguir empleo'
|
||||
@@ -44,6 +63,77 @@ export const CreateCourseModal: React.FC<CreateCourseModalProps> = ({
|
||||
|
||||
if (!isOpen) return null;
|
||||
|
||||
const handleCloseModal = () => {
|
||||
setStep(1);
|
||||
setTitle('');
|
||||
setSubtitle('');
|
||||
setVideoFile(null);
|
||||
setVideoPreviewUrl('');
|
||||
setVideoEmbedUrl('');
|
||||
setThumbnailFile(null);
|
||||
setUploadProgress(0);
|
||||
setIsUploading(false);
|
||||
onClose();
|
||||
};
|
||||
|
||||
const handleNextStep = () => {
|
||||
if (step === 1 && !title.trim()) {
|
||||
setTitle('Nuevo Curso Especializado');
|
||||
}
|
||||
if (step < 3) {
|
||||
setStep((s) => (s + 1) as 1 | 2 | 3);
|
||||
}
|
||||
};
|
||||
|
||||
const handlePrevStep = () => {
|
||||
if (step > 1) {
|
||||
setStep((s) => (s - 1) as 1 | 2 | 3);
|
||||
}
|
||||
};
|
||||
|
||||
const handleImageFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const file = e.target.files?.[0];
|
||||
if (!file) return;
|
||||
|
||||
setThumbnailFile(file);
|
||||
const objectUrl = URL.createObjectURL(file);
|
||||
setThumbnail(objectUrl);
|
||||
};
|
||||
|
||||
const handleRemoveThumbnail = () => {
|
||||
setThumbnailFile(null);
|
||||
setThumbnail('https://images.unsplash.com/photo-1516321318423-f06f85e504b3?w=800&auto=format&fit=crop&q=80');
|
||||
if (imageInputRef.current) imageInputRef.current.value = '';
|
||||
};
|
||||
|
||||
const handleVideoFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const file = e.target.files?.[0];
|
||||
if (!file) return;
|
||||
|
||||
setVideoFile(file);
|
||||
setIsUploading(true);
|
||||
setUploadProgress(20);
|
||||
|
||||
const interval = setInterval(() => {
|
||||
setUploadProgress((prev) => {
|
||||
if (prev >= 100) {
|
||||
clearInterval(interval);
|
||||
setIsUploading(false);
|
||||
setVideoPreviewUrl(URL.createObjectURL(file));
|
||||
return 100;
|
||||
}
|
||||
return prev + 25;
|
||||
});
|
||||
}, 200);
|
||||
};
|
||||
|
||||
const handleRemoveVideo = () => {
|
||||
setVideoFile(null);
|
||||
setVideoPreviewUrl('');
|
||||
setUploadProgress(0);
|
||||
if (fileInputRef.current) fileInputRef.current.value = '';
|
||||
};
|
||||
|
||||
const handleAddOutcome = () => {
|
||||
if (newOutcome.trim()) {
|
||||
setWhatYouWillLearn([...whatYouWillLearn, newOutcome.trim()]);
|
||||
@@ -55,9 +145,7 @@ export const CreateCourseModal: React.FC<CreateCourseModalProps> = ({
|
||||
setWhatYouWillLearn(whatYouWillLearn.filter((_, i) => i !== index));
|
||||
};
|
||||
|
||||
const handleSubmit = (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
|
||||
const handleFinalPublish = () => {
|
||||
const createdCourse: Course = {
|
||||
id: `course-${Date.now()}`,
|
||||
title: title || 'Nuevo Curso Especializado',
|
||||
@@ -103,88 +191,88 @@ export const CreateCourseModal: React.FC<CreateCourseModalProps> = ({
|
||||
};
|
||||
|
||||
onCreateCourse(createdCourse);
|
||||
onClose();
|
||||
handleCloseModal();
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 z-50 overflow-y-auto bg-slate-950/80 backdrop-blur-md flex items-center justify-center p-3 sm:p-6 animate-in fade-in duration-200">
|
||||
|
||||
<div
|
||||
className="relative w-full max-w-2xl bg-slate-900 border border-slate-800 rounded-2xl shadow-2xl overflow-hidden my-6"
|
||||
className="relative w-full max-w-2xl transition-colors duration-300 bg-white dark:bg-slate-900 border border-slate-200 dark:border-slate-800 text-slate-900 dark:text-slate-100 rounded-2xl shadow-2xl overflow-hidden my-6"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
|
||||
{/* Header */}
|
||||
<div className="p-6 border-b border-slate-800 bg-slate-950/60 flex items-center justify-between">
|
||||
<div className="p-6 border-b border-slate-200 dark:border-slate-800 bg-slate-50 dark:bg-slate-950/60 flex items-center justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-10 h-10 rounded-xl bg-violet-500/20 text-violet-400 flex items-center justify-center text-lg">
|
||||
<div className="w-10 h-10 rounded-xl bg-violet-500/20 text-violet-600 dark:text-violet-400 flex items-center justify-center text-lg">
|
||||
<FiBookOpen />
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="text-base font-bold text-white">Publicar Nuevo Curso</h3>
|
||||
<p className="text-xs text-slate-400">Paso {step} de 3</p>
|
||||
<h3 className="text-base font-bold text-slate-900 dark:text-white">Publicar Nuevo Curso & Cargar Videos</h3>
|
||||
<p className="text-xs text-slate-500 dark:text-slate-400">Paso {step} de 3</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="p-2 rounded-xl text-slate-400 hover:text-white hover:bg-slate-800 transition"
|
||||
type="button"
|
||||
onClick={handleCloseModal}
|
||||
className="p-2 rounded-xl text-slate-400 hover:text-slate-900 dark:hover:text-white hover:bg-slate-200 dark:hover:bg-slate-800 transition"
|
||||
>
|
||||
<FiX className="text-xl" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Step Progress Bar */}
|
||||
<div className="w-full bg-slate-800 h-1.5">
|
||||
<div className="w-full bg-slate-200 dark:bg-slate-800 h-1.5">
|
||||
<div
|
||||
className="bg-gradient-to-r from-violet-600 to-indigo-500 h-full transition-all duration-300"
|
||||
style={{ width: `${(step / 3) * 100}%` }}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Form Body */}
|
||||
<form onSubmit={handleSubmit} className="p-6 space-y-5">
|
||||
{/* Modal Container Body */}
|
||||
<div className="p-6 space-y-5">
|
||||
|
||||
{/* Step 1: Basic Information */}
|
||||
{step === 1 && (
|
||||
<div className="space-y-4 animate-in fade-in">
|
||||
<div>
|
||||
<label className="block text-xs font-bold text-slate-300 mb-1.5">
|
||||
<label className="block text-xs font-bold text-slate-700 dark:text-slate-300 mb-1.5">
|
||||
Título del Curso *
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
required
|
||||
value={title}
|
||||
onChange={(e) => setTitle(e.target.value)}
|
||||
onKeyDown={(e) => e.key === 'Enter' && handleNextStep()}
|
||||
placeholder="Ej. Master en Desarrollo de Software Full Stack"
|
||||
className="w-full px-4 py-2.5 bg-slate-950 border border-slate-700 rounded-xl text-sm text-white focus:outline-none focus:border-violet-500"
|
||||
className="w-full px-4 py-2.5 bg-slate-50 dark:bg-slate-950 border border-slate-300 dark:border-slate-700 rounded-xl text-sm text-slate-900 dark:text-white focus:outline-none focus:border-violet-500"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-xs font-bold text-slate-300 mb-1.5">
|
||||
<label className="block text-xs font-bold text-slate-700 dark:text-slate-300 mb-1.5">
|
||||
Subtítulo / Breve Descripción *
|
||||
</label>
|
||||
<textarea
|
||||
rows={2}
|
||||
required
|
||||
value={subtitle}
|
||||
onChange={(e) => setSubtitle(e.target.value)}
|
||||
placeholder="Explica en 2 frases lo que el estudiante logrará al finalizar este curso."
|
||||
className="w-full px-4 py-2.5 bg-slate-950 border border-slate-700 rounded-xl text-sm text-white focus:outline-none focus:border-violet-500"
|
||||
className="w-full px-4 py-2.5 bg-slate-50 dark:bg-slate-950 border border-slate-300 dark:border-slate-700 rounded-xl text-sm text-slate-900 dark:text-white focus:outline-none focus:border-violet-500"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="block text-xs font-bold text-slate-300 mb-1.5">
|
||||
<label className="block text-xs font-bold text-slate-700 dark:text-slate-300 mb-1.5">
|
||||
Categoría
|
||||
</label>
|
||||
<select
|
||||
value={category}
|
||||
onChange={(e) => setCategory(e.target.value)}
|
||||
className="w-full px-3 py-2.5 bg-slate-950 border border-slate-700 rounded-xl text-xs text-white focus:outline-none focus:border-violet-500"
|
||||
className="w-full px-3 py-2.5 bg-slate-50 dark:bg-slate-950 border border-slate-300 dark:border-slate-700 rounded-xl text-xs text-slate-900 dark:text-white focus:outline-none focus:border-violet-500"
|
||||
>
|
||||
<option value="Desarrollo Web">Desarrollo Web</option>
|
||||
<option value="Inteligencia Artificial">Inteligencia Artificial</option>
|
||||
@@ -196,13 +284,13 @@ export const CreateCourseModal: React.FC<CreateCourseModalProps> = ({
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-xs font-bold text-slate-300 mb-1.5">
|
||||
<label className="block text-xs font-bold text-slate-700 dark:text-slate-300 mb-1.5">
|
||||
Nivel del Curso
|
||||
</label>
|
||||
<select
|
||||
value={level}
|
||||
onChange={(e) => setLevel(e.target.value as CourseLevel)}
|
||||
className="w-full px-3 py-2.5 bg-slate-950 border border-slate-700 rounded-xl text-xs text-white focus:outline-none focus:border-violet-500"
|
||||
className="w-full px-3 py-2.5 bg-slate-50 dark:bg-slate-950 border border-slate-300 dark:border-slate-700 rounded-xl text-xs text-slate-900 dark:text-white focus:outline-none focus:border-violet-500"
|
||||
>
|
||||
<option value="Principiante">Principiante</option>
|
||||
<option value="Intermedio">Intermedio</option>
|
||||
@@ -214,12 +302,82 @@ export const CreateCourseModal: React.FC<CreateCourseModalProps> = ({
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Step 2: Pricing & Duration */}
|
||||
{/* Step 2: Video Upload & Pricing */}
|
||||
{step === 2 && (
|
||||
<div className="space-y-4 animate-in fade-in">
|
||||
<div className="p-4 rounded-xl bg-slate-50 dark:bg-slate-950 border border-slate-200 dark:border-slate-800 space-y-3">
|
||||
<label className="block text-xs font-bold text-slate-900 dark:text-white flex items-center gap-2">
|
||||
<FiVideo className="text-cyan-600 dark:text-cyan-400" /> Subir Video de Clase Promocional (.mp4, .mov)
|
||||
</label>
|
||||
|
||||
<input
|
||||
ref={fileInputRef}
|
||||
type="file"
|
||||
accept="video/*"
|
||||
onChange={handleVideoFileChange}
|
||||
className="hidden"
|
||||
id="modal-video-upload"
|
||||
/>
|
||||
|
||||
{!videoFile && !videoPreviewUrl ? (
|
||||
<label
|
||||
htmlFor="modal-video-upload"
|
||||
className="flex flex-col items-center justify-center p-6 border-2 border-dashed border-slate-300 dark:border-slate-700 hover:border-cyan-500 rounded-xl bg-white dark:bg-slate-900 cursor-pointer group text-center space-y-2 transition"
|
||||
>
|
||||
<FiUploadCloud className="text-3xl text-cyan-500 group-hover:scale-110 transition-transform" />
|
||||
<p className="text-xs font-bold text-slate-900 dark:text-white">Haz clic para seleccionar el video de tu lección</p>
|
||||
<p className="text-[10px] text-slate-500 dark:text-slate-400">MP4, MOV o WEBM hasta 2GB</p>
|
||||
</label>
|
||||
) : (
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center justify-between p-3 rounded-lg bg-white dark:bg-slate-900 border border-slate-200 dark:border-slate-700 text-xs">
|
||||
<div className="flex items-center gap-2">
|
||||
<FiFilm className="text-emerald-500 text-lg" />
|
||||
<span className="font-bold text-slate-900 dark:text-white truncate max-w-xs">{videoFile?.name || 'clase_muestra.mp4'}</span>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleRemoveVideo}
|
||||
className="text-rose-500 hover:text-rose-600"
|
||||
>
|
||||
<FiX />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{isUploading && (
|
||||
<div className="space-y-1">
|
||||
<div className="w-full bg-slate-200 dark:bg-slate-800 h-2 rounded-full overflow-hidden">
|
||||
<div className="bg-cyan-500 h-full transition-all duration-300" style={{ width: `${uploadProgress}%` }} />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{videoPreviewUrl && (
|
||||
<div className="aspect-video w-full rounded-lg overflow-hidden bg-black">
|
||||
<video src={videoPreviewUrl} controls className="w-full h-full object-contain" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="pt-2">
|
||||
<label className="block text-[11px] font-semibold text-slate-600 dark:text-slate-400 mb-1 flex items-center gap-1">
|
||||
<FiLink /> O pega la URL directa del video (Vimeo, YouTube)
|
||||
</label>
|
||||
<input
|
||||
type="url"
|
||||
value={videoEmbedUrl}
|
||||
onChange={(e) => setVideoEmbedUrl(e.target.value)}
|
||||
onKeyDown={(e) => e.key === 'Enter' && handleNextStep()}
|
||||
placeholder="https://vimeo.com/..."
|
||||
className="w-full px-3 py-2 bg-white dark:bg-slate-900 border border-slate-300 dark:border-slate-700 rounded-lg text-xs text-slate-900 dark:text-white font-mono"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="block text-xs font-bold text-slate-300 mb-1.5">
|
||||
<label className="block text-xs font-bold text-slate-700 dark:text-slate-300 mb-1.5">
|
||||
Precio de Venta ($ USD) *
|
||||
</label>
|
||||
<div className="relative">
|
||||
@@ -227,17 +385,33 @@ export const CreateCourseModal: React.FC<CreateCourseModalProps> = ({
|
||||
<input
|
||||
type="number"
|
||||
step="0.01"
|
||||
required
|
||||
value={price}
|
||||
onChange={(e) => setPrice(e.target.value)}
|
||||
onKeyDown={(e) => e.key === 'Enter' && handleNextStep()}
|
||||
placeholder="29.99"
|
||||
className="w-full pl-8 pr-4 py-2.5 bg-slate-950 border border-slate-700 rounded-xl text-sm text-white focus:outline-none focus:border-violet-500 font-mono"
|
||||
className="w-full pl-8 pr-4 py-2.5 bg-slate-50 dark:bg-slate-950 border border-slate-300 dark:border-slate-700 rounded-xl text-sm text-slate-900 dark:text-white focus:outline-none focus:border-violet-500 font-mono"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-xs font-bold text-slate-300 mb-1.5">
|
||||
<label className="block text-xs font-bold text-slate-700 dark:text-slate-300 mb-1.5">
|
||||
Duración Estimada de Video (Horas)
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={duration}
|
||||
onChange={(e) => setDuration(e.target.value)}
|
||||
onKeyDown={(e) => e.key === 'Enter' && handleNextStep()}
|
||||
placeholder="Ej. 18h 45min"
|
||||
className="w-full px-4 py-2.5 bg-slate-50 dark:bg-slate-950 border border-slate-300 dark:border-slate-700 rounded-xl text-sm text-slate-900 dark:text-white focus:outline-none focus:border-violet-500"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="block text-xs font-bold text-slate-700 dark:text-slate-300 mb-1.5">
|
||||
Precio Original Tachado ($ USD)
|
||||
</label>
|
||||
<div className="relative">
|
||||
@@ -247,60 +421,86 @@ export const CreateCourseModal: React.FC<CreateCourseModalProps> = ({
|
||||
step="0.01"
|
||||
value={originalPrice}
|
||||
onChange={(e) => setOriginalPrice(e.target.value)}
|
||||
onKeyDown={(e) => e.key === 'Enter' && handleNextStep()}
|
||||
placeholder="89.99"
|
||||
className="w-full pl-8 pr-4 py-2.5 bg-slate-950 border border-slate-700 rounded-xl text-sm text-white focus:outline-none focus:border-violet-500 font-mono"
|
||||
className="w-full pl-8 pr-4 py-2.5 bg-slate-50 dark:bg-slate-950 border border-slate-300 dark:border-slate-700 rounded-xl text-sm text-slate-900 dark:text-white focus:outline-none focus:border-violet-500 font-mono"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-xs font-bold text-slate-300 mb-1.5">
|
||||
Duración Total Estimada de Video
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={duration}
|
||||
onChange={(e) => setDuration(e.target.value)}
|
||||
placeholder="Ej. 24h 45min"
|
||||
className="w-full px-4 py-2.5 bg-slate-950 border border-slate-700 rounded-xl text-sm text-white focus:outline-none focus:border-violet-500"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="p-4 rounded-xl bg-violet-950/30 border border-violet-500/20 text-xs text-slate-300 space-y-1">
|
||||
<p className="font-bold text-violet-300">💡 Consejo de fijación de precios:</p>
|
||||
<p>Los cursos con precios entre $19.99 y $34.99 tienen una tasa de conversión 45% más alta en nuestra plataforma.</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Step 3: Media & What you'll learn */}
|
||||
{/* Step 3: Thumbnail Drag & Drop & Learning Outcomes */}
|
||||
{step === 3 && (
|
||||
<div className="space-y-4 animate-in fade-in">
|
||||
<div>
|
||||
<label className="block text-xs font-bold text-slate-300 mb-1.5">
|
||||
URL de Imagen de Portada (Thumbnail)
|
||||
|
||||
{/* Thumbnail Image Drag & Drop */}
|
||||
<div className="p-4 rounded-xl bg-slate-50 dark:bg-slate-950 border border-slate-200 dark:border-slate-800 space-y-3">
|
||||
<label className="block text-xs font-bold text-slate-900 dark:text-white flex items-center gap-2">
|
||||
<FiImage className="text-cyan-600 dark:text-cyan-400" /> Imagen de Portada del Curso (Thumbnail)
|
||||
</label>
|
||||
<div className="flex gap-2">
|
||||
|
||||
<input
|
||||
ref={imageInputRef}
|
||||
type="file"
|
||||
accept="image/*"
|
||||
onChange={handleImageFileChange}
|
||||
className="hidden"
|
||||
id="modal-image-upload"
|
||||
/>
|
||||
|
||||
{!thumbnailFile ? (
|
||||
<label
|
||||
htmlFor="modal-image-upload"
|
||||
className="flex flex-col items-center justify-center p-6 border-2 border-dashed border-slate-300 dark:border-slate-700 hover:border-cyan-500 rounded-xl bg-white dark:bg-slate-900 cursor-pointer group text-center space-y-2 transition"
|
||||
>
|
||||
<FiUploadCloud className="text-3xl text-cyan-500 group-hover:scale-110 transition-transform" />
|
||||
<p className="text-xs font-bold text-slate-900 dark:text-white">Haz clic o arrastra tu imagen de portada aquí</p>
|
||||
<p className="text-[10px] text-slate-500 dark:text-slate-400">PNG, JPG, WEBP o SVG (Recomendado: 1280x720)</p>
|
||||
</label>
|
||||
) : (
|
||||
<div className="flex items-center justify-between p-3 rounded-lg bg-white dark:bg-slate-900 border border-slate-200 dark:border-slate-700 text-xs">
|
||||
<div className="flex items-center gap-2">
|
||||
<FiCheckCircle className="text-emerald-500 text-lg" />
|
||||
<span className="font-bold text-slate-900 dark:text-white truncate max-w-xs">{thumbnailFile.name}</span>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleRemoveThumbnail}
|
||||
className="text-rose-500 hover:text-rose-600"
|
||||
>
|
||||
<FiX />
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Live Thumbnail Preview */}
|
||||
{thumbnail && (
|
||||
<div className="aspect-video max-w-xs rounded-xl overflow-hidden bg-slate-950 border border-slate-300 dark:border-slate-700 shadow-md">
|
||||
<img src={thumbnail} alt="Vista previa de portada" className="w-full h-full object-cover" />
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="pt-2">
|
||||
<label className="block text-[11px] font-semibold text-slate-600 dark:text-slate-400 mb-1 flex items-center gap-1">
|
||||
<FiLink /> O pega la URL directa de la imagen
|
||||
</label>
|
||||
<input
|
||||
type="url"
|
||||
value={thumbnail}
|
||||
onChange={(e) => setThumbnail(e.target.value)}
|
||||
onChange={(e) => {
|
||||
setThumbnailFile(null);
|
||||
setThumbnail(e.target.value);
|
||||
}}
|
||||
placeholder="https://images.unsplash.com/..."
|
||||
className="flex-1 px-4 py-2.5 bg-slate-950 border border-slate-700 rounded-xl text-xs text-white focus:outline-none focus:border-violet-500"
|
||||
className="w-full px-3 py-2 bg-white dark:bg-slate-900 border border-slate-300 dark:border-slate-700 rounded-lg text-xs text-slate-900 dark:text-white font-mono"
|
||||
/>
|
||||
</div>
|
||||
{thumbnail && (
|
||||
<img
|
||||
src={thumbnail}
|
||||
alt="Preview"
|
||||
className="mt-2 h-24 w-40 object-cover rounded-lg border border-slate-700"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-xs font-bold text-slate-300 mb-1.5">
|
||||
<label className="block text-xs font-bold text-slate-700 dark:text-slate-300 mb-1.5">
|
||||
Lo que aprenderá el alumno (Puntos clave)
|
||||
</label>
|
||||
<div className="flex gap-2 mb-2">
|
||||
@@ -308,8 +508,9 @@ export const CreateCourseModal: React.FC<CreateCourseModalProps> = ({
|
||||
type="text"
|
||||
value={newOutcome}
|
||||
onChange={(e) => setNewOutcome(e.target.value)}
|
||||
onKeyDown={(e) => e.key === 'Enter' && handleAddOutcome()}
|
||||
placeholder="Ej. Crear APIs RESTful y GraphQL con Node.js"
|
||||
className="flex-1 px-3 py-2 bg-slate-950 border border-slate-700 rounded-lg text-xs text-white"
|
||||
className="flex-1 px-3 py-2 bg-slate-50 dark:bg-slate-950 border border-slate-300 dark:border-slate-700 rounded-lg text-xs text-slate-900 dark:text-white"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
@@ -324,15 +525,15 @@ export const CreateCourseModal: React.FC<CreateCourseModalProps> = ({
|
||||
{whatYouWillLearn.map((item, idx) => (
|
||||
<div
|
||||
key={idx}
|
||||
className="flex items-center justify-between p-2 rounded-lg bg-slate-950 border border-slate-800 text-xs text-slate-300"
|
||||
className="flex items-center justify-between p-2 rounded-lg bg-slate-50 dark:bg-slate-950 border border-slate-200 dark:border-slate-800 text-xs text-slate-700 dark:text-slate-300"
|
||||
>
|
||||
<span className="flex items-center gap-1.5 truncate">
|
||||
<FiCheckCircle className="text-emerald-400 shrink-0" /> {item}
|
||||
<FiCheckCircle className="text-emerald-500 dark:text-emerald-400 shrink-0" /> {item}
|
||||
</span>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleRemoveOutcome(idx)}
|
||||
className="text-slate-500 hover:text-rose-400 pl-2"
|
||||
className="text-slate-400 hover:text-rose-500 pl-2"
|
||||
>
|
||||
<FiX />
|
||||
</button>
|
||||
@@ -344,12 +545,12 @@ export const CreateCourseModal: React.FC<CreateCourseModalProps> = ({
|
||||
)}
|
||||
|
||||
{/* Bottom Actions */}
|
||||
<div className="pt-4 border-t border-slate-800 flex items-center justify-between">
|
||||
<div className="pt-4 border-t border-slate-200 dark:border-slate-800 flex items-center justify-between">
|
||||
{step > 1 ? (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setStep((s) => (s - 1) as any)}
|
||||
className="px-4 py-2 bg-slate-800 hover:bg-slate-700 text-slate-300 text-xs font-semibold rounded-xl flex items-center gap-1.5 transition"
|
||||
onClick={handlePrevStep}
|
||||
className="px-4 py-2 bg-slate-200 dark:bg-slate-800 hover:bg-slate-300 dark:hover:bg-slate-700 text-slate-800 dark:text-slate-300 text-xs font-semibold rounded-xl flex items-center gap-1.5 transition"
|
||||
>
|
||||
<FiArrowLeft /> Anterior
|
||||
</button>
|
||||
@@ -360,12 +561,7 @@ export const CreateCourseModal: React.FC<CreateCourseModalProps> = ({
|
||||
{step < 3 ? (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
if (step === 1 && !title.trim()) {
|
||||
setTitle('Nuevo Curso Especializado');
|
||||
}
|
||||
setStep((s) => (s + 1) as any);
|
||||
}}
|
||||
onClick={handleNextStep}
|
||||
className="px-5 py-2 bg-violet-600 hover:bg-violet-500 text-white text-xs font-bold rounded-xl flex items-center gap-1.5 transition shadow-lg shadow-violet-600/30"
|
||||
>
|
||||
<span>Siguiente</span>
|
||||
@@ -373,16 +569,17 @@ export const CreateCourseModal: React.FC<CreateCourseModalProps> = ({
|
||||
</button>
|
||||
) : (
|
||||
<button
|
||||
type="submit"
|
||||
type="button"
|
||||
onClick={handleFinalPublish}
|
||||
className="px-6 py-2.5 bg-gradient-to-r from-emerald-600 to-teal-500 hover:from-emerald-500 hover:to-teal-400 text-white text-xs font-bold rounded-xl flex items-center gap-2 transition shadow-lg shadow-emerald-500/30"
|
||||
>
|
||||
<FiCheck className="text-base" />
|
||||
<span>Publicar Curso Ahora</span>
|
||||
<span>Publicar Curso y Videos</span>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
</form>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,121 @@
|
||||
import React from 'react';
|
||||
import { FiX } from 'react-icons/fi';
|
||||
|
||||
export interface BadgeProps {
|
||||
children: React.ReactNode;
|
||||
variant?:
|
||||
| 'default'
|
||||
| 'primary'
|
||||
| 'secondary'
|
||||
| 'success'
|
||||
| 'warning'
|
||||
| 'danger'
|
||||
| 'info'
|
||||
| 'outline';
|
||||
size?: 'sm' | 'md' | 'lg';
|
||||
dot?: boolean;
|
||||
dotPulse?: boolean;
|
||||
icon?: React.ReactNode;
|
||||
removable?: boolean;
|
||||
onRemove?: () => void;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const variantStyles: Record<NonNullable<BadgeProps['variant']>, { badge: string; dot: string }> = {
|
||||
default: {
|
||||
badge: 'bg-slate-100 text-slate-800 border-slate-200 dark:bg-slate-800/80 dark:text-slate-200 dark:border-slate-700',
|
||||
dot: 'bg-slate-500',
|
||||
},
|
||||
primary: {
|
||||
badge: 'bg-cyan-50 text-cyan-700 border-cyan-200 dark:bg-cyan-950/60 dark:text-cyan-300 dark:border-cyan-500/30',
|
||||
dot: 'bg-cyan-500',
|
||||
},
|
||||
secondary: {
|
||||
badge: 'bg-purple-50 text-purple-700 border-purple-200 dark:bg-purple-950/60 dark:text-purple-300 dark:border-purple-500/30',
|
||||
dot: 'bg-purple-500',
|
||||
},
|
||||
success: {
|
||||
badge: 'bg-emerald-50 text-emerald-700 border-emerald-200 dark:bg-emerald-950/60 dark:text-emerald-300 dark:border-emerald-500/30',
|
||||
dot: 'bg-emerald-500',
|
||||
},
|
||||
warning: {
|
||||
badge: 'bg-amber-50 text-amber-700 border-amber-200 dark:bg-amber-950/60 dark:text-amber-300 dark:border-amber-500/30',
|
||||
dot: 'bg-amber-500',
|
||||
},
|
||||
danger: {
|
||||
badge: 'bg-rose-50 text-rose-700 border-rose-200 dark:bg-rose-950/60 dark:text-rose-300 dark:border-rose-500/30',
|
||||
dot: 'bg-rose-500',
|
||||
},
|
||||
info: {
|
||||
badge: 'bg-sky-50 text-sky-700 border-sky-200 dark:bg-sky-950/60 dark:text-sky-300 dark:border-sky-500/30',
|
||||
dot: 'bg-sky-500',
|
||||
},
|
||||
outline: {
|
||||
badge: 'bg-transparent text-slate-700 border-slate-300 dark:text-slate-300 dark:border-slate-700',
|
||||
dot: 'bg-slate-400',
|
||||
},
|
||||
};
|
||||
|
||||
const sizeStyles = {
|
||||
sm: 'text-[10px] px-2 py-0.5 gap-1 rounded-md font-semibold',
|
||||
md: 'text-xs px-2.5 py-1 gap-1.5 rounded-lg font-medium',
|
||||
lg: 'text-sm px-3 py-1.5 gap-2 rounded-xl font-medium',
|
||||
};
|
||||
|
||||
export const Badge: React.FC<BadgeProps> = ({
|
||||
children,
|
||||
variant = 'default',
|
||||
size = 'md',
|
||||
dot = false,
|
||||
dotPulse = false,
|
||||
icon,
|
||||
removable = false,
|
||||
onRemove,
|
||||
className = '',
|
||||
}) => {
|
||||
const currentVariant = variantStyles[variant];
|
||||
|
||||
return (
|
||||
<span
|
||||
className={`
|
||||
inline-flex items-center select-none border transition-colors
|
||||
${currentVariant.badge}
|
||||
${sizeStyles[size]}
|
||||
${className}
|
||||
`}
|
||||
>
|
||||
{/* Optional Dot indicator */}
|
||||
{dot && (
|
||||
<span className="relative flex h-1.5 w-1.5 shrink-0">
|
||||
{dotPulse && (
|
||||
<span
|
||||
className={`animate-ping absolute inline-flex h-full w-full rounded-full opacity-75 ${currentVariant.dot}`}
|
||||
/>
|
||||
)}
|
||||
<span className={`relative inline-flex rounded-full h-1.5 w-1.5 ${currentVariant.dot}`} />
|
||||
</span>
|
||||
)}
|
||||
|
||||
{/* Optional Icon */}
|
||||
{icon && <span className="shrink-0">{icon}</span>}
|
||||
|
||||
{/* Content */}
|
||||
<span>{children}</span>
|
||||
|
||||
{/* Optional Remove Button */}
|
||||
{removable && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onRemove?.();
|
||||
}}
|
||||
className="ml-0.5 p-0.5 rounded-full hover:bg-black/10 dark:hover:bg-white/10 transition-colors"
|
||||
aria-label="Remover etiqueta"
|
||||
>
|
||||
<FiX className="text-xs" />
|
||||
</button>
|
||||
)}
|
||||
</span>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,96 @@
|
||||
import React from 'react';
|
||||
import { Spinner } from './Spinner';
|
||||
|
||||
export interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
|
||||
variant?: 'primary' | 'secondary' | 'outline' | 'ghost' | 'danger' | 'success' | 'gradient';
|
||||
size?: 'xs' | 'sm' | 'md' | 'lg' | 'icon';
|
||||
isLoading?: boolean;
|
||||
loadingText?: string;
|
||||
leftIcon?: React.ReactNode;
|
||||
rightIcon?: React.ReactNode;
|
||||
fullWidth?: boolean;
|
||||
}
|
||||
|
||||
const variantStyles: Record<NonNullable<ButtonProps['variant']>, string> = {
|
||||
primary:
|
||||
'bg-cyan-500 hover:bg-cyan-400 text-slate-950 font-semibold shadow-md shadow-cyan-500/20 hover:shadow-cyan-500/30 focus-visible:ring-cyan-500 dark:bg-cyan-500 dark:hover:bg-cyan-400 dark:text-slate-950 dark:shadow-cyan-500/30',
|
||||
secondary:
|
||||
'bg-slate-100 hover:bg-slate-200 text-slate-800 border border-slate-200 shadow-sm focus-visible:ring-slate-400 dark:bg-slate-800 dark:hover:bg-slate-700 dark:text-slate-100 dark:border-slate-700 dark:shadow-none dark:focus-visible:ring-slate-500',
|
||||
outline:
|
||||
'bg-transparent border border-slate-300 text-slate-700 hover:bg-slate-100 hover:border-slate-400 focus-visible:ring-slate-400 dark:border-slate-700 dark:text-slate-200 dark:hover:bg-slate-800/80 dark:hover:border-slate-600 dark:focus-visible:ring-slate-500',
|
||||
ghost:
|
||||
'bg-transparent text-slate-600 hover:bg-slate-100 hover:text-slate-900 focus-visible:ring-slate-400 dark:text-slate-300 dark:hover:bg-slate-800/70 dark:hover:text-white dark:focus-visible:ring-slate-600',
|
||||
danger:
|
||||
'bg-rose-600 hover:bg-rose-500 text-white font-medium shadow-md shadow-rose-600/25 focus-visible:ring-rose-500 dark:bg-rose-600 dark:hover:bg-rose-500 dark:text-white',
|
||||
success:
|
||||
'bg-emerald-600 hover:bg-emerald-500 text-white font-medium shadow-md shadow-emerald-600/25 focus-visible:ring-emerald-500 dark:bg-emerald-500 dark:hover:bg-emerald-400 dark:text-slate-950',
|
||||
gradient:
|
||||
'bg-gradient-to-r from-cyan-500 via-sky-500 to-blue-600 hover:from-cyan-400 hover:via-sky-400 hover:to-blue-500 text-white font-semibold shadow-lg shadow-cyan-500/25 hover:shadow-cyan-500/40 focus-visible:ring-cyan-500 dark:from-cyan-500 dark:to-blue-600',
|
||||
};
|
||||
|
||||
const sizeStyles: Record<NonNullable<ButtonProps['size']>, string> = {
|
||||
xs: 'text-[11px] px-2.5 py-1 rounded-lg gap-1.5',
|
||||
sm: 'text-xs px-3 py-1.5 rounded-xl gap-2',
|
||||
md: 'text-sm px-4 py-2.5 rounded-xl gap-2',
|
||||
lg: 'text-base px-6 py-3.5 rounded-2xl gap-2.5',
|
||||
icon: 'p-2.5 rounded-xl justify-center items-center',
|
||||
};
|
||||
|
||||
export const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
||||
(
|
||||
{
|
||||
variant = 'primary',
|
||||
size = 'md',
|
||||
isLoading = false,
|
||||
loadingText,
|
||||
leftIcon,
|
||||
rightIcon,
|
||||
fullWidth = false,
|
||||
disabled,
|
||||
className = '',
|
||||
children,
|
||||
...props
|
||||
},
|
||||
ref
|
||||
) => {
|
||||
const isInteractiveDisabled = disabled || isLoading;
|
||||
|
||||
return (
|
||||
<button
|
||||
ref={ref}
|
||||
disabled={isInteractiveDisabled}
|
||||
className={`
|
||||
relative inline-flex items-center justify-center font-medium select-none
|
||||
transition-all duration-200 ease-out
|
||||
focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2
|
||||
focus-visible:ring-offset-white dark:focus-visible:ring-offset-slate-950
|
||||
active:scale-[0.98]
|
||||
disabled:opacity-50 disabled:cursor-not-allowed disabled:pointer-events-none disabled:active:scale-100
|
||||
${fullWidth ? 'w-full' : ''}
|
||||
${variantStyles[variant]}
|
||||
${sizeStyles[size]}
|
||||
${className}
|
||||
`}
|
||||
{...props}
|
||||
>
|
||||
{isLoading ? (
|
||||
<>
|
||||
<Spinner
|
||||
size={size === 'xs' || size === 'sm' ? 'xs' : 'sm'}
|
||||
color={variant === 'primary' || variant === 'success' ? 'slate' : 'current'}
|
||||
/>
|
||||
{loadingText ? <span>{loadingText}</span> : children}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
{leftIcon && <span className="shrink-0">{leftIcon}</span>}
|
||||
{children && <span>{children}</span>}
|
||||
{rightIcon && <span className="shrink-0">{rightIcon}</span>}
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
Button.displayName = 'Button';
|
||||
@@ -0,0 +1,146 @@
|
||||
import React from 'react';
|
||||
|
||||
export interface CardProps extends React.HTMLAttributes<HTMLDivElement> {
|
||||
variant?: 'default' | 'elevated' | 'glass' | 'outline' | 'interactive';
|
||||
padding?: 'none' | 'sm' | 'md' | 'lg';
|
||||
}
|
||||
|
||||
const variantStyles: Record<NonNullable<CardProps['variant']>, string> = {
|
||||
default:
|
||||
'bg-white dark:bg-slate-900/90 border border-slate-200 dark:border-slate-800/80 shadow-sm',
|
||||
elevated:
|
||||
'bg-white dark:bg-slate-900 border border-slate-100 dark:border-slate-800 shadow-xl shadow-slate-200/50 dark:shadow-black/40',
|
||||
glass:
|
||||
'bg-white/80 dark:bg-slate-900/60 backdrop-blur-xl border border-slate-200/70 dark:border-slate-800/70 shadow-lg',
|
||||
outline:
|
||||
'bg-transparent border border-slate-300 dark:border-slate-800 shadow-none',
|
||||
interactive:
|
||||
'bg-white dark:bg-slate-900/90 border border-slate-200 dark:border-slate-800 shadow-sm hover:shadow-xl hover:shadow-cyan-500/10 hover:border-cyan-500/50 dark:hover:border-cyan-500/40 cursor-pointer transform hover:-translate-y-1 transition-all duration-300 ease-out',
|
||||
};
|
||||
|
||||
const paddingStyles: Record<NonNullable<CardProps['padding']>, string> = {
|
||||
none: 'p-0',
|
||||
sm: 'p-4',
|
||||
md: 'p-6',
|
||||
lg: 'p-8',
|
||||
};
|
||||
|
||||
export const Card = React.forwardRef<HTMLDivElement, CardProps>(
|
||||
(
|
||||
{
|
||||
variant = 'default',
|
||||
padding = 'none',
|
||||
className = '',
|
||||
children,
|
||||
...props
|
||||
},
|
||||
ref
|
||||
) => {
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
className={`
|
||||
rounded-2xl overflow-hidden transition-colors
|
||||
${variantStyles[variant]}
|
||||
${paddingStyles[padding]}
|
||||
${className}
|
||||
`}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
Card.displayName = 'Card';
|
||||
|
||||
export interface CardHeaderProps extends React.HTMLAttributes<HTMLDivElement> {
|
||||
bordered?: boolean;
|
||||
}
|
||||
|
||||
export const CardHeader: React.FC<CardHeaderProps> = ({
|
||||
bordered = false,
|
||||
className = '',
|
||||
children,
|
||||
...props
|
||||
}) => {
|
||||
return (
|
||||
<div
|
||||
className={`
|
||||
p-5 sm:p-6 flex flex-col space-y-1.5
|
||||
${bordered ? 'border-b border-slate-100 dark:border-slate-800/80' : ''}
|
||||
${className}
|
||||
`}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const CardTitle: React.FC<React.HTMLAttributes<HTMLHeadingElement>> = ({
|
||||
className = '',
|
||||
children,
|
||||
...props
|
||||
}) => {
|
||||
return (
|
||||
<h3
|
||||
className={`text-lg sm:text-xl font-bold tracking-tight text-slate-900 dark:text-white transition-colors ${className}`}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</h3>
|
||||
);
|
||||
};
|
||||
|
||||
export const CardDescription: React.FC<React.HTMLAttributes<HTMLParagraphElement>> = ({
|
||||
className = '',
|
||||
children,
|
||||
...props
|
||||
}) => {
|
||||
return (
|
||||
<p
|
||||
className={`text-xs sm:text-sm text-slate-500 dark:text-slate-400 leading-relaxed ${className}`}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</p>
|
||||
);
|
||||
};
|
||||
|
||||
export const CardContent: React.FC<React.HTMLAttributes<HTMLDivElement>> = ({
|
||||
className = '',
|
||||
children,
|
||||
...props
|
||||
}) => {
|
||||
return (
|
||||
<div className={`p-5 sm:p-6 ${className}`} {...props}>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export interface CardFooterProps extends React.HTMLAttributes<HTMLDivElement> {
|
||||
bordered?: boolean;
|
||||
}
|
||||
|
||||
export const CardFooter: React.FC<CardFooterProps> = ({
|
||||
bordered = false,
|
||||
className = '',
|
||||
children,
|
||||
...props
|
||||
}) => {
|
||||
return (
|
||||
<div
|
||||
className={`
|
||||
p-5 sm:p-6 flex items-center justify-between
|
||||
${bordered ? 'border-t border-slate-100 dark:border-slate-800/80' : ''}
|
||||
${className}
|
||||
`}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,111 @@
|
||||
import React, { useEffect, useRef } from 'react';
|
||||
import { FiCheck, FiMinus } from 'react-icons/fi';
|
||||
|
||||
export interface CheckboxProps extends Omit<React.InputHTMLAttributes<HTMLInputElement>, 'type'> {
|
||||
label?: React.ReactNode;
|
||||
description?: string;
|
||||
errorMessage?: string;
|
||||
indeterminate?: boolean;
|
||||
}
|
||||
|
||||
export const Checkbox = React.forwardRef<HTMLInputElement, CheckboxProps>(
|
||||
(
|
||||
{
|
||||
label,
|
||||
description,
|
||||
errorMessage,
|
||||
indeterminate = false,
|
||||
checked,
|
||||
disabled,
|
||||
className = '',
|
||||
id,
|
||||
name,
|
||||
...props
|
||||
},
|
||||
forwardedRef
|
||||
) => {
|
||||
const internalRef = useRef<HTMLInputElement>(null);
|
||||
const resolvedRef = (forwardedRef as React.RefObject<HTMLInputElement>) || internalRef;
|
||||
const checkboxId = id || name || (typeof label === 'string' ? label.toLowerCase().replace(/\s+/g, '-') : undefined);
|
||||
|
||||
useEffect(() => {
|
||||
if (resolvedRef.current) {
|
||||
resolvedRef.current.indeterminate = indeterminate;
|
||||
}
|
||||
}, [indeterminate, resolvedRef]);
|
||||
|
||||
const isChecked = Boolean(checked);
|
||||
|
||||
return (
|
||||
<div className={`flex flex-col space-y-1 ${className}`}>
|
||||
<label
|
||||
htmlFor={checkboxId}
|
||||
className={`inline-flex items-start gap-2.5 select-none cursor-pointer group ${
|
||||
disabled ? 'opacity-50 !cursor-not-allowed' : ''
|
||||
}`}
|
||||
>
|
||||
{/* Custom Styled Box */}
|
||||
<div className="relative flex items-center justify-center mt-0.5 shrink-0">
|
||||
<input
|
||||
ref={resolvedRef}
|
||||
id={checkboxId}
|
||||
name={name}
|
||||
type="checkbox"
|
||||
checked={checked}
|
||||
disabled={disabled}
|
||||
className="peer sr-only"
|
||||
{...props}
|
||||
/>
|
||||
|
||||
<div
|
||||
className={`
|
||||
w-4 h-4 rounded-md flex items-center justify-center transition-all duration-200
|
||||
border
|
||||
peer-focus-visible:ring-2 peer-focus-visible:ring-offset-2
|
||||
peer-focus-visible:ring-cyan-500 peer-focus-visible:ring-offset-white dark:peer-focus-visible:ring-offset-slate-950
|
||||
${
|
||||
isChecked || indeterminate
|
||||
? 'bg-cyan-500 border-cyan-500 text-slate-950 shadow-sm shadow-cyan-500/20'
|
||||
: 'bg-white dark:bg-slate-900 border-slate-300 dark:border-slate-700 text-transparent group-hover:border-slate-400 dark:group-hover:border-slate-500'
|
||||
}
|
||||
${
|
||||
errorMessage
|
||||
? '!border-rose-500 !bg-rose-50 dark:!bg-rose-950/30'
|
||||
: ''
|
||||
}
|
||||
`}
|
||||
>
|
||||
{indeterminate ? (
|
||||
<FiMinus className="text-xs stroke-3" />
|
||||
) : isChecked ? (
|
||||
<FiCheck className="text-xs stroke-3" />
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Label and Sub-description */}
|
||||
{(label || description) && (
|
||||
<div className="flex flex-col text-left">
|
||||
{label && (
|
||||
<span className="text-xs sm:text-sm font-medium text-slate-800 dark:text-slate-200 group-hover:text-slate-950 dark:group-hover:text-white transition-colors">
|
||||
{label}
|
||||
</span>
|
||||
)}
|
||||
{description && (
|
||||
<span className="text-xs text-slate-500 dark:text-slate-400">
|
||||
{description}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</label>
|
||||
|
||||
{errorMessage && (
|
||||
<p className="text-xs text-rose-600 dark:text-rose-400 pl-6.5">{errorMessage}</p>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
Checkbox.displayName = 'Checkbox';
|
||||
@@ -0,0 +1,188 @@
|
||||
import React, { useState } from 'react';
|
||||
import { FiEye, FiEyeOff, FiX, FiAlertCircle } from 'react-icons/fi';
|
||||
|
||||
export interface InputProps extends Omit<React.InputHTMLAttributes<HTMLInputElement>, 'size'> {
|
||||
label?: string;
|
||||
helperText?: string;
|
||||
errorMessage?: string;
|
||||
leftIcon?: React.ReactNode;
|
||||
rightIcon?: React.ReactNode;
|
||||
isClearable?: boolean;
|
||||
onClear?: () => void;
|
||||
sizeVariant?: 'sm' | 'md' | 'lg';
|
||||
fullWidth?: boolean;
|
||||
}
|
||||
|
||||
const sizeStyles = {
|
||||
sm: {
|
||||
input: 'py-2 text-xs rounded-lg',
|
||||
icon: 'text-sm',
|
||||
leftPad: 'pl-8',
|
||||
rightPad: 'pr-8',
|
||||
},
|
||||
md: {
|
||||
input: 'py-2.5 text-sm rounded-xl',
|
||||
icon: 'text-base',
|
||||
leftPad: 'pl-10',
|
||||
rightPad: 'pr-10',
|
||||
},
|
||||
lg: {
|
||||
input: 'py-3.5 text-base rounded-2xl',
|
||||
icon: 'text-lg',
|
||||
leftPad: 'pl-12',
|
||||
rightPad: 'pr-12',
|
||||
},
|
||||
};
|
||||
|
||||
export const Input = React.forwardRef<HTMLInputElement, InputProps>(
|
||||
(
|
||||
{
|
||||
label,
|
||||
helperText,
|
||||
errorMessage,
|
||||
leftIcon,
|
||||
rightIcon,
|
||||
isClearable = false,
|
||||
onClear,
|
||||
sizeVariant = 'md',
|
||||
fullWidth = true,
|
||||
type = 'text',
|
||||
id,
|
||||
name,
|
||||
disabled,
|
||||
required,
|
||||
className = '',
|
||||
value,
|
||||
...props
|
||||
},
|
||||
ref
|
||||
) => {
|
||||
const [showPassword, setShowPassword] = useState(false);
|
||||
const inputId = id || name || (label ? label.toLowerCase().replace(/\s+/g, '-') : undefined);
|
||||
|
||||
const isPasswordType = type === 'password';
|
||||
const computedType = isPasswordType ? (showPassword ? 'text' : 'password') : type;
|
||||
const hasError = Boolean(errorMessage);
|
||||
const hasValue = value !== undefined && value !== '' && value !== null;
|
||||
|
||||
const currentSize = sizeStyles[sizeVariant];
|
||||
|
||||
return (
|
||||
<div className={`${fullWidth ? 'w-full' : 'inline-block'} space-y-1.5`}>
|
||||
{/* Label */}
|
||||
{label && (
|
||||
<div className="flex items-center justify-between">
|
||||
<label
|
||||
htmlFor={inputId}
|
||||
className={`block text-xs font-semibold select-none transition-colors ${
|
||||
hasError
|
||||
? 'text-rose-600 dark:text-rose-400'
|
||||
: 'text-slate-700 dark:text-slate-300'
|
||||
}`}
|
||||
>
|
||||
{label}
|
||||
{required && <span className="ml-1 text-rose-500 font-bold">*</span>}
|
||||
</label>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Input Wrapper */}
|
||||
<div className="relative flex items-center">
|
||||
{/* Left Icon */}
|
||||
{leftIcon && (
|
||||
<div
|
||||
className={`absolute left-3.5 pointer-events-none flex items-center justify-center transition-colors ${
|
||||
hasError
|
||||
? 'text-rose-500'
|
||||
: 'text-slate-400 dark:text-slate-500'
|
||||
} ${currentSize.icon}`}
|
||||
>
|
||||
{leftIcon}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Core Input Element */}
|
||||
<input
|
||||
ref={ref}
|
||||
id={inputId}
|
||||
name={name}
|
||||
type={computedType}
|
||||
value={value}
|
||||
disabled={disabled}
|
||||
required={required}
|
||||
className={`
|
||||
w-full px-3.5 font-normal transition-all duration-200 outline-none
|
||||
bg-white text-slate-900 placeholder:text-slate-400
|
||||
border border-slate-300
|
||||
hover:border-slate-400
|
||||
focus:border-cyan-500 focus:ring-4 focus:ring-cyan-500/15
|
||||
dark:bg-slate-900/90 dark:text-slate-100 dark:placeholder:text-slate-500
|
||||
dark:border-slate-800 dark:hover:border-slate-700
|
||||
dark:focus:border-cyan-500 dark:focus:ring-cyan-500/25
|
||||
disabled:opacity-60 disabled:bg-slate-100 disabled:dark:bg-slate-950 disabled:cursor-not-allowed
|
||||
${leftIcon ? currentSize.leftPad : ''}
|
||||
${isPasswordType || isClearable || rightIcon ? currentSize.rightPad : ''}
|
||||
${
|
||||
hasError
|
||||
? '!border-rose-500 !ring-rose-500/20 text-rose-900 dark:text-rose-200 focus:!border-rose-500 focus:!ring-rose-500/20'
|
||||
: ''
|
||||
}
|
||||
${currentSize.input}
|
||||
${className}
|
||||
`}
|
||||
{...props}
|
||||
/>
|
||||
|
||||
{/* Right Action Icons (Clear, Password Toggle, Custom RightIcon) */}
|
||||
<div className="absolute right-3 flex items-center gap-1.5 text-slate-400 dark:text-slate-500">
|
||||
{isClearable && hasValue && !disabled && (
|
||||
<button
|
||||
type="button"
|
||||
tabIndex={-1}
|
||||
onClick={onClear}
|
||||
aria-label="Limpiar campo"
|
||||
className="p-1 rounded-md hover:bg-slate-100 hover:text-slate-700 dark:hover:bg-slate-800 dark:hover:text-slate-200 transition-colors"
|
||||
>
|
||||
<FiX className="text-sm" />
|
||||
</button>
|
||||
)}
|
||||
|
||||
{isPasswordType && !disabled && (
|
||||
<button
|
||||
type="button"
|
||||
tabIndex={-1}
|
||||
onClick={() => setShowPassword((prev) => !prev)}
|
||||
aria-label={showPassword ? 'Ocultar contraseña' : 'Ver contraseña'}
|
||||
className="p-1 rounded-md hover:bg-slate-100 hover:text-slate-700 dark:hover:bg-slate-800 dark:hover:text-slate-200 transition-colors"
|
||||
>
|
||||
{showPassword ? (
|
||||
<FiEyeOff className={currentSize.icon} />
|
||||
) : (
|
||||
<FiEye className={currentSize.icon} />
|
||||
)}
|
||||
</button>
|
||||
)}
|
||||
|
||||
{!isPasswordType && rightIcon && (
|
||||
<div className={`pointer-events-none flex items-center justify-center ${currentSize.icon}`}>
|
||||
{rightIcon}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Error or Helper Message */}
|
||||
{hasError ? (
|
||||
<p className="flex items-center gap-1.5 text-xs text-rose-600 dark:text-rose-400 animate-fadeIn">
|
||||
<FiAlertCircle className="shrink-0 text-xs" />
|
||||
<span>{errorMessage}</span>
|
||||
</p>
|
||||
) : helperText ? (
|
||||
<p className="text-xs text-slate-500 dark:text-slate-400">{helperText}</p>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
Input.displayName = 'Input';
|
||||
@@ -0,0 +1,128 @@
|
||||
import React, { useEffect } from 'react';
|
||||
import { FiX } from 'react-icons/fi';
|
||||
|
||||
export interface ModalProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
title?: React.ReactNode;
|
||||
description?: React.ReactNode;
|
||||
size?: 'sm' | 'md' | 'lg' | 'xl' | '2xl' | 'full';
|
||||
children: React.ReactNode;
|
||||
footer?: React.ReactNode;
|
||||
closeOnOverlayClick?: boolean;
|
||||
showCloseButton?: boolean;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const sizeStyles = {
|
||||
sm: 'max-w-md',
|
||||
md: 'max-w-lg',
|
||||
lg: 'max-w-2xl',
|
||||
xl: 'max-w-4xl',
|
||||
'2xl': 'max-w-6xl',
|
||||
full: 'max-w-[95vw] h-[90vh]',
|
||||
};
|
||||
|
||||
export const Modal: React.FC<ModalProps> = ({
|
||||
isOpen,
|
||||
onClose,
|
||||
title,
|
||||
description,
|
||||
size = 'md',
|
||||
children,
|
||||
footer,
|
||||
closeOnOverlayClick = true,
|
||||
showCloseButton = true,
|
||||
className = '',
|
||||
}) => {
|
||||
// ESC key handler and body scroll lock
|
||||
useEffect(() => {
|
||||
if (!isOpen) return;
|
||||
|
||||
const handleKeyDown = (e: KeyboardEvent) => {
|
||||
if (e.key === 'Escape') {
|
||||
onClose();
|
||||
}
|
||||
};
|
||||
|
||||
const originalOverflow = document.body.style.overflow;
|
||||
document.body.style.overflow = 'hidden';
|
||||
window.addEventListener('keydown', handleKeyDown);
|
||||
|
||||
return () => {
|
||||
document.body.style.overflow = originalOverflow;
|
||||
window.removeEventListener('keydown', handleKeyDown);
|
||||
};
|
||||
}, [isOpen, onClose]);
|
||||
|
||||
if (!isOpen) return null;
|
||||
|
||||
return (
|
||||
<div
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
className="fixed inset-0 z-50 flex items-center justify-center p-4 sm:p-6 overflow-y-auto"
|
||||
>
|
||||
{/* Backdrop */}
|
||||
<div
|
||||
onClick={() => {
|
||||
if (closeOnOverlayClick) onClose();
|
||||
}}
|
||||
className="fixed inset-0 bg-slate-950/70 backdrop-blur-md transition-opacity animate-fadeIn"
|
||||
/>
|
||||
|
||||
{/* Modal Dialog Card */}
|
||||
<div
|
||||
className={`
|
||||
relative w-full rounded-3xl overflow-hidden shadow-2xl z-10
|
||||
bg-white text-slate-900 border border-slate-200
|
||||
dark:bg-slate-900/95 dark:text-slate-100 dark:border-slate-800
|
||||
transition-all transform animate-scaleUp
|
||||
${sizeStyles[size]}
|
||||
${className}
|
||||
`}
|
||||
>
|
||||
{/* Header */}
|
||||
{(title || description || showCloseButton) && (
|
||||
<div className="flex items-start justify-between p-6 pb-4 border-b border-slate-100 dark:border-slate-800/80">
|
||||
<div className="space-y-1 pr-6">
|
||||
{title && (
|
||||
<h2 className="text-xl font-bold text-slate-900 dark:text-white tracking-tight">
|
||||
{title}
|
||||
</h2>
|
||||
)}
|
||||
{description && (
|
||||
<p className="text-xs sm:text-sm text-slate-500 dark:text-slate-400">
|
||||
{description}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{showCloseButton && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClose}
|
||||
aria-label="Cerrar modal"
|
||||
className="p-2 rounded-xl text-slate-400 hover:text-slate-700 hover:bg-slate-100 dark:hover:text-slate-200 dark:hover:bg-slate-800 transition-colors"
|
||||
>
|
||||
<FiX className="text-lg" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Body Content */}
|
||||
<div className="p-6 overflow-y-auto max-h-[calc(85vh-160px)]">
|
||||
{children}
|
||||
</div>
|
||||
|
||||
{/* Footer */}
|
||||
{footer && (
|
||||
<div className="p-6 pt-4 border-t border-slate-100 dark:border-slate-800/80 bg-slate-50/50 dark:bg-slate-950/40 flex items-center justify-end gap-3">
|
||||
{footer}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,164 @@
|
||||
import React from 'react';
|
||||
import { FiChevronDown, FiAlertCircle } from 'react-icons/fi';
|
||||
|
||||
export interface SelectOption {
|
||||
value: string | number;
|
||||
label: string;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
export interface SelectProps extends Omit<React.SelectHTMLAttributes<HTMLSelectElement>, 'size'> {
|
||||
label?: string;
|
||||
helperText?: string;
|
||||
errorMessage?: string;
|
||||
leftIcon?: React.ReactNode;
|
||||
options?: SelectOption[];
|
||||
sizeVariant?: 'sm' | 'md' | 'lg';
|
||||
fullWidth?: boolean;
|
||||
}
|
||||
|
||||
const sizeStyles = {
|
||||
sm: {
|
||||
select: 'py-2 text-xs rounded-lg',
|
||||
icon: 'text-sm',
|
||||
leftPad: 'pl-8',
|
||||
rightPad: 'pr-8',
|
||||
},
|
||||
md: {
|
||||
select: 'py-2.5 text-sm rounded-xl',
|
||||
icon: 'text-base',
|
||||
leftPad: 'pl-10',
|
||||
rightPad: 'pr-10',
|
||||
},
|
||||
lg: {
|
||||
select: 'py-3.5 text-base rounded-2xl',
|
||||
icon: 'text-lg',
|
||||
leftPad: 'pl-12',
|
||||
rightPad: 'pr-12',
|
||||
},
|
||||
};
|
||||
|
||||
export const Select = React.forwardRef<HTMLSelectElement, SelectProps>(
|
||||
(
|
||||
{
|
||||
label,
|
||||
helperText,
|
||||
errorMessage,
|
||||
leftIcon,
|
||||
options,
|
||||
sizeVariant = 'md',
|
||||
fullWidth = true,
|
||||
id,
|
||||
name,
|
||||
disabled,
|
||||
required,
|
||||
className = '',
|
||||
children,
|
||||
...props
|
||||
},
|
||||
ref
|
||||
) => {
|
||||
const selectId = id || name || (label ? label.toLowerCase().replace(/\s+/g, '-') : undefined);
|
||||
const hasError = Boolean(errorMessage);
|
||||
const currentSize = sizeStyles[sizeVariant];
|
||||
|
||||
return (
|
||||
<div className={`${fullWidth ? 'w-full' : 'inline-block'} space-y-1.5`}>
|
||||
{/* Label */}
|
||||
{label && (
|
||||
<div className="flex items-center justify-between">
|
||||
<label
|
||||
htmlFor={selectId}
|
||||
className={`block text-xs font-semibold select-none transition-colors ${
|
||||
hasError
|
||||
? 'text-rose-600 dark:text-rose-400'
|
||||
: 'text-slate-700 dark:text-slate-300'
|
||||
}`}
|
||||
>
|
||||
{label}
|
||||
{required && <span className="ml-1 text-rose-500 font-bold">*</span>}
|
||||
</label>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Select Wrapper */}
|
||||
<div className="relative flex items-center">
|
||||
{/* Left Icon */}
|
||||
{leftIcon && (
|
||||
<div
|
||||
className={`absolute left-3.5 pointer-events-none flex items-center justify-center transition-colors ${
|
||||
hasError
|
||||
? 'text-rose-500'
|
||||
: 'text-slate-400 dark:text-slate-500'
|
||||
} ${currentSize.icon}`}
|
||||
>
|
||||
{leftIcon}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Core Select Element */}
|
||||
<select
|
||||
ref={ref}
|
||||
id={selectId}
|
||||
name={name}
|
||||
disabled={disabled}
|
||||
required={required}
|
||||
className={`
|
||||
w-full px-3.5 font-normal transition-all duration-200 outline-none appearance-none cursor-pointer
|
||||
bg-white text-slate-900
|
||||
border border-slate-300
|
||||
hover:border-slate-400
|
||||
focus:border-cyan-500 focus:ring-4 focus:ring-cyan-500/15
|
||||
dark:bg-slate-900/90 dark:text-slate-100
|
||||
dark:border-slate-800 dark:hover:border-slate-700
|
||||
dark:focus:border-cyan-500 dark:focus:ring-cyan-500/25
|
||||
disabled:opacity-60 disabled:bg-slate-100 disabled:dark:bg-slate-950 disabled:cursor-not-allowed
|
||||
${leftIcon ? currentSize.leftPad : ''}
|
||||
${currentSize.rightPad}
|
||||
${
|
||||
hasError
|
||||
? '!border-rose-500 !ring-rose-500/20 text-rose-900 dark:text-rose-200 focus:!border-rose-500 focus:!ring-rose-500/20'
|
||||
: ''
|
||||
}
|
||||
${currentSize.select}
|
||||
${className}
|
||||
`}
|
||||
{...props}
|
||||
>
|
||||
{options
|
||||
? options.map((opt) => (
|
||||
<option
|
||||
key={opt.value}
|
||||
value={opt.value}
|
||||
disabled={opt.disabled}
|
||||
className="bg-white text-slate-900 dark:bg-slate-900 dark:text-slate-100"
|
||||
>
|
||||
{opt.label}
|
||||
</option>
|
||||
))
|
||||
: children}
|
||||
</select>
|
||||
|
||||
{/* Right Chevron Icon */}
|
||||
<div
|
||||
className={`absolute right-3.5 pointer-events-none flex items-center justify-center text-slate-400 dark:text-slate-500 ${currentSize.icon}`}
|
||||
>
|
||||
<FiChevronDown />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Error or Helper Message */}
|
||||
{hasError ? (
|
||||
<p className="flex items-center gap-1.5 text-xs text-rose-600 dark:text-rose-400 animate-fadeIn">
|
||||
<FiAlertCircle className="shrink-0 text-xs" />
|
||||
<span>{errorMessage}</span>
|
||||
</p>
|
||||
) : helperText ? (
|
||||
<p className="text-xs text-slate-500 dark:text-slate-400">{helperText}</p>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
Select.displayName = 'Select';
|
||||
@@ -0,0 +1,44 @@
|
||||
import React from 'react';
|
||||
|
||||
export interface SpinnerProps {
|
||||
size?: 'xs' | 'sm' | 'md' | 'lg' | 'xl';
|
||||
color?: 'cyan' | 'blue' | 'white' | 'slate' | 'current';
|
||||
className?: string;
|
||||
label?: string;
|
||||
}
|
||||
|
||||
const sizeClasses = {
|
||||
xs: 'w-3.5 h-3.5 border-2',
|
||||
sm: 'w-4 h-4 border-2',
|
||||
md: 'w-6 h-6 border-2',
|
||||
lg: 'w-8 h-8 border-3',
|
||||
xl: 'w-12 h-12 border-4',
|
||||
};
|
||||
|
||||
const colorClasses = {
|
||||
cyan: 'border-cyan-500/20 border-t-cyan-500 dark:border-cyan-400/20 dark:border-t-cyan-400',
|
||||
blue: 'border-blue-500/20 border-t-blue-500 dark:border-blue-400/20 dark:border-t-blue-400',
|
||||
white: 'border-white/30 border-t-white',
|
||||
slate: 'border-slate-300 border-t-slate-700 dark:border-slate-700 dark:border-t-slate-200',
|
||||
current: 'border-current/20 border-t-current',
|
||||
};
|
||||
|
||||
export const Spinner: React.FC<SpinnerProps> = ({
|
||||
size = 'md',
|
||||
color = 'cyan',
|
||||
className = '',
|
||||
label = 'Cargando...',
|
||||
}) => {
|
||||
return (
|
||||
<div
|
||||
role="status"
|
||||
aria-label={label}
|
||||
className={`inline-flex items-center justify-center ${className}`}
|
||||
>
|
||||
<div
|
||||
className={`rounded-full animate-spin ${sizeClasses[size]} ${colorClasses[color]}`}
|
||||
/>
|
||||
<span className="sr-only">{label}</span>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,122 @@
|
||||
import React from 'react';
|
||||
|
||||
export interface SwitchProps {
|
||||
label?: React.ReactNode;
|
||||
description?: string;
|
||||
checked?: boolean;
|
||||
onChange?: (checked: boolean) => void;
|
||||
disabled?: boolean;
|
||||
size?: 'sm' | 'md' | 'lg';
|
||||
checkedIcon?: React.ReactNode;
|
||||
uncheckedIcon?: React.ReactNode;
|
||||
id?: string;
|
||||
name?: string;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const trackSizes = {
|
||||
sm: 'w-8 h-4.5',
|
||||
md: 'w-11 h-6',
|
||||
lg: 'w-14 h-7.5',
|
||||
};
|
||||
|
||||
const thumbSizes = {
|
||||
sm: 'w-3.5 h-3.5',
|
||||
md: 'w-5 h-5',
|
||||
lg: 'w-6.5 h-6.5',
|
||||
};
|
||||
|
||||
const translateSizes = {
|
||||
sm: 'translate-x-3.5',
|
||||
md: 'translate-x-5',
|
||||
lg: 'translate-x-6.5',
|
||||
};
|
||||
|
||||
export const Switch: React.FC<SwitchProps> = ({
|
||||
label,
|
||||
description,
|
||||
checked = false,
|
||||
onChange,
|
||||
disabled = false,
|
||||
size = 'md',
|
||||
checkedIcon,
|
||||
uncheckedIcon,
|
||||
id,
|
||||
name,
|
||||
className = '',
|
||||
}) => {
|
||||
const switchId = id || name || (typeof label === 'string' ? label.toLowerCase().replace(/\s+/g, '-') : undefined);
|
||||
|
||||
const handleClick = () => {
|
||||
if (!disabled && onChange) {
|
||||
onChange(!checked);
|
||||
}
|
||||
};
|
||||
|
||||
const handleKeyDown = (e: React.KeyboardEvent) => {
|
||||
if (e.key === ' ' || e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
handleClick();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={`inline-flex items-start gap-3 select-none ${className}`}>
|
||||
{/* Switch Button */}
|
||||
<button
|
||||
id={switchId}
|
||||
type="button"
|
||||
role="switch"
|
||||
aria-checked={checked}
|
||||
disabled={disabled}
|
||||
onClick={handleClick}
|
||||
onKeyDown={handleKeyDown}
|
||||
className={`
|
||||
relative shrink-0 inline-flex items-center rounded-full p-0.5 cursor-pointer transition-colors duration-200 ease-in-out
|
||||
focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2
|
||||
focus-visible:ring-cyan-500 focus-visible:ring-offset-white dark:focus-visible:ring-offset-slate-950
|
||||
disabled:opacity-50 disabled:cursor-not-allowed
|
||||
${trackSizes[size]}
|
||||
${
|
||||
checked
|
||||
? 'bg-cyan-500 dark:bg-cyan-500'
|
||||
: 'bg-slate-300 dark:bg-slate-700 hover:bg-slate-400 dark:hover:bg-slate-600'
|
||||
}
|
||||
`}
|
||||
>
|
||||
{/* Thumb */}
|
||||
<span
|
||||
className={`
|
||||
pointer-events-none flex items-center justify-center rounded-full bg-white shadow-md transform transition-transform duration-200 ease-in-out text-slate-800
|
||||
${thumbSizes[size]}
|
||||
${checked ? translateSizes[size] : 'translate-x-0'}
|
||||
`}
|
||||
>
|
||||
{checked ? checkedIcon : uncheckedIcon}
|
||||
</span>
|
||||
</button>
|
||||
|
||||
{/* Label and description */}
|
||||
{(label || description) && (
|
||||
<div className="flex flex-col text-left cursor-pointer" onClick={handleClick}>
|
||||
{label && (
|
||||
<span
|
||||
className={`text-xs sm:text-sm font-medium transition-colors ${
|
||||
disabled
|
||||
? 'text-slate-400 dark:text-slate-600'
|
||||
: 'text-slate-800 dark:text-slate-200 hover:text-slate-950 dark:hover:text-white'
|
||||
}`}
|
||||
>
|
||||
{label}
|
||||
</span>
|
||||
)}
|
||||
{description && (
|
||||
<span className="text-xs text-slate-500 dark:text-slate-400">
|
||||
{description}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,117 @@
|
||||
import React from 'react';
|
||||
import { FiAlertCircle } from 'react-icons/fi';
|
||||
|
||||
export interface TextareaProps extends React.TextareaHTMLAttributes<HTMLTextAreaElement> {
|
||||
label?: string;
|
||||
helperText?: string;
|
||||
errorMessage?: string;
|
||||
showCharCount?: boolean;
|
||||
fullWidth?: boolean;
|
||||
}
|
||||
|
||||
export const Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>(
|
||||
(
|
||||
{
|
||||
label,
|
||||
helperText,
|
||||
errorMessage,
|
||||
showCharCount = false,
|
||||
fullWidth = true,
|
||||
id,
|
||||
name,
|
||||
disabled,
|
||||
required,
|
||||
maxLength,
|
||||
className = '',
|
||||
value,
|
||||
rows = 4,
|
||||
...props
|
||||
},
|
||||
ref
|
||||
) => {
|
||||
const textareaId = id || name || (label ? label.toLowerCase().replace(/\s+/g, '-') : undefined);
|
||||
const hasError = Boolean(errorMessage);
|
||||
const currentLength = typeof value === 'string' ? value.length : 0;
|
||||
|
||||
return (
|
||||
<div className={`${fullWidth ? 'w-full' : 'inline-block'} space-y-1.5`}>
|
||||
{/* Header (Label & Character Count) */}
|
||||
{(label || (showCharCount && maxLength)) && (
|
||||
<div className="flex items-center justify-between">
|
||||
{label && (
|
||||
<label
|
||||
htmlFor={textareaId}
|
||||
className={`block text-xs font-semibold select-none transition-colors ${
|
||||
hasError
|
||||
? 'text-rose-600 dark:text-rose-400'
|
||||
: 'text-slate-700 dark:text-slate-300'
|
||||
}`}
|
||||
>
|
||||
{label}
|
||||
{required && <span className="ml-1 text-rose-500 font-bold">*</span>}
|
||||
</label>
|
||||
)}
|
||||
|
||||
{showCharCount && maxLength && (
|
||||
<span
|
||||
className={`text-[11px] font-medium transition-colors ${
|
||||
currentLength >= maxLength
|
||||
? 'text-rose-500 font-bold'
|
||||
: currentLength >= maxLength * 0.85
|
||||
? 'text-amber-500'
|
||||
: 'text-slate-400 dark:text-slate-500'
|
||||
}`}
|
||||
>
|
||||
{currentLength}/{maxLength}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Core Textarea */}
|
||||
<div className="relative">
|
||||
<textarea
|
||||
ref={ref}
|
||||
id={textareaId}
|
||||
name={name}
|
||||
value={value}
|
||||
disabled={disabled}
|
||||
required={required}
|
||||
maxLength={maxLength}
|
||||
rows={rows}
|
||||
className={`
|
||||
w-full p-3.5 text-sm font-normal rounded-xl transition-all duration-200 outline-none resize-y
|
||||
bg-white text-slate-900 placeholder:text-slate-400
|
||||
border border-slate-300
|
||||
hover:border-slate-400
|
||||
focus:border-cyan-500 focus:ring-4 focus:ring-cyan-500/15
|
||||
dark:bg-slate-900/90 dark:text-slate-100 dark:placeholder:text-slate-500
|
||||
dark:border-slate-800 dark:hover:border-slate-700
|
||||
dark:focus:border-cyan-500 dark:focus:ring-cyan-500/25
|
||||
disabled:opacity-60 disabled:bg-slate-100 disabled:dark:bg-slate-950 disabled:cursor-not-allowed
|
||||
${
|
||||
hasError
|
||||
? '!border-rose-500 !ring-rose-500/20 text-rose-900 dark:text-rose-200 focus:!border-rose-500 focus:!ring-rose-500/20'
|
||||
: ''
|
||||
}
|
||||
${className}
|
||||
`}
|
||||
{...props}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Error or Helper Message */}
|
||||
{hasError ? (
|
||||
<p className="flex items-center gap-1.5 text-xs text-rose-600 dark:text-rose-400 animate-fadeIn">
|
||||
<FiAlertCircle className="shrink-0 text-xs" />
|
||||
<span>{errorMessage}</span>
|
||||
</p>
|
||||
) : helperText ? (
|
||||
<p className="text-xs text-slate-500 dark:text-slate-400">{helperText}</p>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
Textarea.displayName = 'Textarea';
|
||||
@@ -0,0 +1,133 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { FiSun, FiMoon } from 'react-icons/fi';
|
||||
|
||||
export interface ThemeToggleProps {
|
||||
variant?: 'icon' | 'pill' | 'switch';
|
||||
size?: 'sm' | 'md' | 'lg';
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export const ThemeToggle: React.FC<ThemeToggleProps> = ({
|
||||
variant = 'icon',
|
||||
size = 'md',
|
||||
className = '',
|
||||
}) => {
|
||||
const [isDark, setIsDark] = useState<boolean>(() => {
|
||||
if (typeof window === 'undefined') return true;
|
||||
const stored = localStorage.getItem('theme');
|
||||
if (stored) return stored === 'dark';
|
||||
return (
|
||||
document.documentElement.classList.contains('dark') ||
|
||||
window.matchMedia('(prefers-color-scheme: dark)').matches
|
||||
);
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
const root = document.documentElement;
|
||||
if (isDark) {
|
||||
root.classList.add('dark');
|
||||
localStorage.setItem('theme', 'dark');
|
||||
} else {
|
||||
root.classList.remove('dark');
|
||||
localStorage.setItem('theme', 'light');
|
||||
}
|
||||
}, [isDark]);
|
||||
|
||||
const toggleTheme = () => {
|
||||
setIsDark((prev) => !prev);
|
||||
};
|
||||
|
||||
const iconSizes = {
|
||||
sm: 'text-sm',
|
||||
md: 'text-base',
|
||||
lg: 'text-lg',
|
||||
};
|
||||
|
||||
const paddingSizes = {
|
||||
sm: 'p-1.5',
|
||||
md: 'p-2.5',
|
||||
lg: 'p-3',
|
||||
};
|
||||
|
||||
if (variant === 'pill') {
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
onClick={toggleTheme}
|
||||
aria-label={isDark ? 'Cambiar a modo claro' : 'Cambiar a modo oscuro'}
|
||||
className={`
|
||||
inline-flex items-center gap-2 px-3 py-1.5 rounded-full text-xs font-semibold
|
||||
bg-slate-100 dark:bg-slate-800 text-slate-700 dark:text-slate-200
|
||||
border border-slate-200 dark:border-slate-700
|
||||
hover:border-cyan-500/50 dark:hover:border-cyan-500/50
|
||||
transition-all duration-200 shadow-sm ${className}
|
||||
`}
|
||||
>
|
||||
{isDark ? (
|
||||
<>
|
||||
<FiMoon className="text-cyan-400" />
|
||||
<span>Modo Oscuro</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<FiSun className="text-amber-500" />
|
||||
<span>Modo Claro</span>
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
if (variant === 'switch') {
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
role="switch"
|
||||
aria-checked={isDark}
|
||||
onClick={toggleTheme}
|
||||
aria-label="Alternar tema de color"
|
||||
className={`
|
||||
relative inline-flex items-center w-14 h-8 rounded-full p-1 transition-colors duration-300
|
||||
${isDark ? 'bg-slate-800 border border-slate-700' : 'bg-slate-200 border border-slate-300'}
|
||||
${className}
|
||||
`}
|
||||
>
|
||||
<span
|
||||
className={`
|
||||
flex items-center justify-center w-6 h-6 rounded-full bg-white dark:bg-slate-900 shadow-md transform transition-transform duration-300
|
||||
${isDark ? 'translate-x-6 text-cyan-400' : 'translate-x-0 text-amber-500'}
|
||||
`}
|
||||
>
|
||||
{isDark ? <FiMoon className="text-xs" /> : <FiSun className="text-xs" />}
|
||||
</span>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
// Default 'icon' variant
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
onClick={toggleTheme}
|
||||
aria-label={isDark ? 'Cambiar a modo claro' : 'Cambiar a modo oscuro'}
|
||||
title={isDark ? 'Cambiar a modo claro' : 'Cambiar a modo oscuro'}
|
||||
className={`
|
||||
relative inline-flex items-center justify-center rounded-xl transition-all duration-200
|
||||
bg-slate-100 dark:bg-slate-800/80 text-slate-600 dark:text-slate-300
|
||||
border border-slate-200 dark:border-slate-700/80
|
||||
hover:text-cyan-600 dark:hover:text-cyan-400
|
||||
hover:border-cyan-500/40 dark:hover:border-cyan-500/40
|
||||
hover:bg-slate-200/60 dark:hover:bg-slate-800
|
||||
focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-cyan-500
|
||||
${paddingSizes[size]}
|
||||
${className}
|
||||
`}
|
||||
>
|
||||
{isDark ? (
|
||||
<FiSun className={`${iconSizes[size]} text-amber-400 transition-transform hover:rotate-45`} />
|
||||
) : (
|
||||
<FiMoon className={`${iconSizes[size]} text-slate-700 transition-transform hover:-rotate-12`} />
|
||||
)}
|
||||
</button>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,48 @@
|
||||
// Button & Actions
|
||||
export { Button } from './Button';
|
||||
export type { ButtonProps } from './Button';
|
||||
|
||||
// Form Fields & Inputs
|
||||
export { Input } from './Input';
|
||||
export type { InputProps } from './Input';
|
||||
|
||||
export { Textarea } from './Textarea';
|
||||
export type { TextareaProps } from './Textarea';
|
||||
|
||||
export { Select } from './Select';
|
||||
export type { SelectProps, SelectOption } from './Select';
|
||||
|
||||
export { Checkbox } from './Checkbox';
|
||||
export type { CheckboxProps } from './Checkbox';
|
||||
|
||||
export { Switch } from './Switch';
|
||||
export type { SwitchProps } from './Switch';
|
||||
|
||||
// Feedback & Data Display
|
||||
export { Badge } from './Badge';
|
||||
export type { BadgeProps } from './Badge';
|
||||
|
||||
export { Spinner } from './Spinner';
|
||||
export type { SpinnerProps } from './Spinner';
|
||||
|
||||
// Layout & Overlays
|
||||
export {
|
||||
Card,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
CardDescription,
|
||||
CardContent,
|
||||
CardFooter,
|
||||
} from './Card';
|
||||
export type {
|
||||
CardProps,
|
||||
CardHeaderProps,
|
||||
CardFooterProps,
|
||||
} from './Card';
|
||||
|
||||
export { Modal } from './Modal';
|
||||
export type { ModalProps } from './Modal';
|
||||
|
||||
// Theme Control
|
||||
export { ThemeToggle } from './ThemeToggle';
|
||||
export type { ThemeToggleProps } from './ThemeToggle';
|
||||
+77
-17
@@ -1,5 +1,7 @@
|
||||
@import "tailwindcss";
|
||||
|
||||
@custom-variant dark (&:where(.dark, .dark *));
|
||||
|
||||
@layer base {
|
||||
html {
|
||||
scroll-behavior: smooth;
|
||||
@@ -7,6 +9,34 @@
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes scaleUp {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: scale(0.95) translateY(8px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: scale(1) translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.animate-fadeIn {
|
||||
animation: fadeIn 0.2s ease-out forwards;
|
||||
}
|
||||
|
||||
.animate-scaleUp {
|
||||
animation: scaleUp 0.25s cubic-bezier(0.16, 1, 0.3, 1) forwards;
|
||||
}
|
||||
|
||||
/* Custom modern scrollbar */
|
||||
::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
@@ -26,30 +56,39 @@
|
||||
background: #475569;
|
||||
}
|
||||
|
||||
/* Cart Running Animation */
|
||||
@keyframes cart-run {
|
||||
/* Full-Screen Crossing Flying Spaceship Animation */
|
||||
@keyframes float-spaceship {
|
||||
0% {
|
||||
transform: translateX(0) rotate(0deg) translateY(0);
|
||||
transform: translate(-10vw, 68vh) rotate(23deg) scale(0.65);
|
||||
opacity: 0.1;
|
||||
}
|
||||
20% {
|
||||
transform: translateX(3px) rotate(-10deg) translateY(-2px);
|
||||
|
||||
15% {
|
||||
opacity: 0.85;
|
||||
}
|
||||
40% {
|
||||
transform: translateX(1px) rotate(-5deg) translateY(1px);
|
||||
|
||||
35% {
|
||||
transform: translate(30vw, 45vh) rotate(25deg) scale(0.85);
|
||||
opacity: 0.95;
|
||||
}
|
||||
60% {
|
||||
transform: translateX(4px) rotate(-12deg) translateY(-2px);
|
||||
|
||||
65% {
|
||||
transform: translate(65vw, 25vh) rotate(22deg) scale(1);
|
||||
opacity: 0.95;
|
||||
}
|
||||
80% {
|
||||
transform: translateX(2px) rotate(-8deg) translateY(0px);
|
||||
|
||||
85% {
|
||||
opacity: 0.85;
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: translateX(0) rotate(0deg) translateY(0);
|
||||
transform: translate(110vw, 5vh) rotate(24deg) scale(0.7);
|
||||
opacity: 0.1;
|
||||
}
|
||||
}
|
||||
|
||||
.animate-cart-run {
|
||||
animation: cart-run 0.5s ease-in-out infinite;
|
||||
.animate-spaceship {
|
||||
animation: float-spaceship 20s cubic-bezier(0.35, 0, 0.25, 1) infinite;
|
||||
}
|
||||
|
||||
/* Speed Streaks for Cart */
|
||||
@@ -58,10 +97,12 @@
|
||||
opacity: 0;
|
||||
transform: translateX(0) scaleX(0.2);
|
||||
}
|
||||
|
||||
50% {
|
||||
opacity: 0.8;
|
||||
transform: translateX(-8px) scaleX(1);
|
||||
}
|
||||
|
||||
100% {
|
||||
opacity: 0;
|
||||
transform: translateX(-16px) scaleX(0);
|
||||
@@ -77,23 +118,42 @@
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.group-hover\\:animate-cart-run:hover .speed-line-1 {
|
||||
.group:hover .speed-line-1 {
|
||||
animation: speed-streak 0.6s infinite linear;
|
||||
top: 30%;
|
||||
right: 110%;
|
||||
width: 12px;
|
||||
}
|
||||
|
||||
.group-hover\\:animate-cart-run:hover .speed-line-2 {
|
||||
.group:hover .speed-line-2 {
|
||||
animation: speed-streak 0.5s infinite linear 0.2s;
|
||||
top: 60%;
|
||||
right: 100%;
|
||||
width: 8px;
|
||||
}
|
||||
|
||||
.group-hover\\:animate-cart-run:hover .speed-line-3 {
|
||||
.group:hover .speed-line-3 {
|
||||
animation: speed-streak 0.7s infinite linear 0.4s;
|
||||
bottom: 10%;
|
||||
right: 120%;
|
||||
width: 14px;
|
||||
}
|
||||
|
||||
/* Twinkling Star Particles Animation */
|
||||
@keyframes star-twinkle {
|
||||
|
||||
0%,
|
||||
100% {
|
||||
opacity: 0.15;
|
||||
transform: scale(0.65);
|
||||
}
|
||||
|
||||
50% {
|
||||
opacity: 0.95;
|
||||
transform: scale(1.3);
|
||||
}
|
||||
}
|
||||
|
||||
.animate-twinkle {
|
||||
animation: star-twinkle 3s ease-in-out infinite alternate;
|
||||
}
|
||||
+538
-347
@@ -1,5 +1,15 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Link } from 'react-router';
|
||||
import type { Course } from '../types/course';
|
||||
import { BrandLogo } from '../components/common/BrandLogo';
|
||||
import {
|
||||
Card,
|
||||
Button,
|
||||
Badge,
|
||||
Input,
|
||||
Select,
|
||||
ThemeToggle
|
||||
} from '../components/ui';
|
||||
import {
|
||||
FiDollarSign,
|
||||
FiUsers,
|
||||
@@ -9,10 +19,12 @@ import {
|
||||
FiTrash2,
|
||||
FiShield,
|
||||
FiSearch,
|
||||
FiFilter,
|
||||
FiActivity,
|
||||
FiPieChart,
|
||||
FiSettings
|
||||
FiSettings,
|
||||
FiMenu,
|
||||
FiX,
|
||||
FiArrowLeft
|
||||
} from 'react-icons/fi';
|
||||
|
||||
interface AdminDashboardViewProps {
|
||||
@@ -26,13 +38,28 @@ export const AdminDashboardView: React.FC<AdminDashboardViewProps> = ({
|
||||
onDeleteCourse,
|
||||
onShowToast,
|
||||
}) => {
|
||||
const [activeTab, setActiveTab] = useState<'courses' | 'users' | 'finance' | 'settings'>('courses');
|
||||
const [activeTab, setActiveTab] = useState<'overview' | 'courses' | 'users' | 'finance' | 'settings'>('overview');
|
||||
const [sidebarOpen, setSidebarOpen] = useState(false);
|
||||
const [statusFilter, setStatusFilter] = useState<'all' | 'published' | 'pending'>('all');
|
||||
const [searchTerm, setSearchTerm] = useState('');
|
||||
const [userSearchTerm, setUserSearchTerm] = useState('');
|
||||
|
||||
// Course status state management for demonstration
|
||||
const [courseStatuses, setCourseStatuses] = useState<Record<string, 'published' | 'pending' | 'rejected'>>({});
|
||||
|
||||
// Mock platform users
|
||||
const [users, setUsers] = 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' },
|
||||
]);
|
||||
|
||||
// Platform Commission Setting State
|
||||
const [commissionRate, setCommissionRate] = useState(20);
|
||||
const [siteName, setSiteName] = useState('RyCourses');
|
||||
|
||||
const getStatus = (courseId: string) => {
|
||||
return courseStatuses[courseId] || 'published';
|
||||
};
|
||||
@@ -41,17 +68,19 @@ export const AdminDashboardView: React.FC<AdminDashboardViewProps> = ({
|
||||
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'}`);
|
||||
onShowToast(`Estado del curso "${title.slice(0, 22)}..." 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 toggleUserStatus = (userId: string, userName: string) => {
|
||||
setUsers(users.map((u) => {
|
||||
if (u.id === userId) {
|
||||
const next = u.status === 'Activo' ? 'Suspendido' : 'Activo';
|
||||
onShowToast(`Estado de ${userName} cambiado a ${next}`);
|
||||
return { ...u, status: next };
|
||||
}
|
||||
return u;
|
||||
}));
|
||||
};
|
||||
|
||||
const filteredCourses = courses.filter((c) => {
|
||||
const matchesSearch = c.title.toLowerCase().includes(searchTerm.toLowerCase()) || c.instructor.name.toLowerCase().includes(searchTerm.toLowerCase());
|
||||
@@ -60,382 +89,544 @@ export const AdminDashboardView: React.FC<AdminDashboardViewProps> = ({
|
||||
return matchesSearch && matchesStatus;
|
||||
});
|
||||
|
||||
const filteredUsers = users.filter((u) =>
|
||||
u.name.toLowerCase().includes(userSearchTerm.toLowerCase()) || u.email.toLowerCase().includes(userSearchTerm.toLowerCase()) || u.role.toLowerCase().includes(userSearchTerm.toLowerCase())
|
||||
);
|
||||
|
||||
const totalGrossRevenue = 184920.00;
|
||||
const netCommission = totalGrossRevenue * (commissionRate / 100);
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-slate-950 text-slate-100 pb-20">
|
||||
<div className="min-h-screen transition-colors duration-300 bg-slate-50 dark:bg-slate-950 text-slate-900 dark:text-slate-100 flex flex-col md:flex-row font-sans selection:bg-cyan-500">
|
||||
|
||||
{/* 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">
|
||||
{/* Mobile Sidebar Overlay */}
|
||||
{sidebarOpen && (
|
||||
<div
|
||||
className="fixed inset-0 bg-slate-950/80 backdrop-blur-sm z-40 md:hidden"
|
||||
onClick={() => setSidebarOpen(false)}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* 1. Left Sidebar Navigation */}
|
||||
<aside className={`
|
||||
fixed md:sticky top-0 left-0 h-screen w-72 bg-white dark:bg-slate-900 border-r border-slate-200 dark:border-slate-800 z-50 flex flex-col justify-between p-5 transition-transform duration-300 shadow-xl
|
||||
${sidebarOpen ? 'translate-x-0' : '-translate-x-full md:translate-x-0'}
|
||||
`}>
|
||||
<div className="space-y-6">
|
||||
{/* Sidebar Brand Logo & Close button */}
|
||||
<div className="flex items-center justify-between pb-3 border-b border-slate-200 dark:border-slate-800">
|
||||
<Link to="/" className="hover:opacity-90 transition">
|
||||
<BrandLogo size="sm" showSubtitle={false} />
|
||||
</Link>
|
||||
|
||||
<button
|
||||
onClick={() => setSidebarOpen(false)}
|
||||
className="md:hidden p-1.5 rounded-lg text-slate-500 hover:text-slate-900 dark:text-slate-400 dark:hover:text-white"
|
||||
>
|
||||
<FiX className="text-xl" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Admin Pill Badge */}
|
||||
<div className="px-3 py-2 rounded-xl bg-violet-500/10 border border-violet-500/25 flex items-center gap-2.5">
|
||||
<div className="w-7 h-7 rounded-lg bg-violet-500/20 text-violet-600 dark:text-violet-400 flex items-center justify-center text-sm font-bold shrink-0">
|
||||
<FiShield />
|
||||
</div>
|
||||
<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
|
||||
<p className="text-xs font-bold text-slate-900 dark:text-white leading-none">Admin Suite</p>
|
||||
<p className="text-[10px] text-violet-600 dark:text-violet-400 font-medium mt-0.5">Control Central</p>
|
||||
</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>
|
||||
{/* Navigation Links */}
|
||||
<nav className="space-y-1.5">
|
||||
<button
|
||||
onClick={() => { setActiveTab('overview'); setSidebarOpen(false); }}
|
||||
className={`w-full flex items-center gap-3 px-3.5 py-3 rounded-xl text-xs font-semibold transition-all ${
|
||||
activeTab === 'overview'
|
||||
? 'bg-slate-100 dark:bg-slate-800 text-cyan-600 dark:text-cyan-400 border border-slate-200 dark:border-cyan-500/30 shadow-sm'
|
||||
: 'text-slate-600 dark:text-slate-400 hover:text-slate-900 dark:hover:text-white hover:bg-slate-100 dark:hover:bg-slate-800/60'
|
||||
}`}
|
||||
>
|
||||
<FiActivity className="text-base" />
|
||||
<span>Dashboard & Métricas</span>
|
||||
</button>
|
||||
|
||||
<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 />
|
||||
<button
|
||||
onClick={() => { setActiveTab('courses'); setSidebarOpen(false); }}
|
||||
className={`w-full flex items-center justify-between px-3.5 py-3 rounded-xl text-xs font-semibold transition-all ${
|
||||
activeTab === 'courses'
|
||||
? 'bg-slate-100 dark:bg-slate-800 text-cyan-600 dark:text-cyan-400 border border-slate-200 dark:border-cyan-500/30 shadow-sm'
|
||||
: 'text-slate-600 dark:text-slate-400 hover:text-slate-900 dark:hover:text-white hover:bg-slate-100 dark:hover:bg-slate-800/60'
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
<FiBookOpen className="text-base" />
|
||||
<span>Gestor de Cursos</span>
|
||||
</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>
|
||||
<Badge variant="primary" size="sm">{courses.length}</Badge>
|
||||
</button>
|
||||
|
||||
<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 />
|
||||
<button
|
||||
onClick={() => { setActiveTab('users'); setSidebarOpen(false); }}
|
||||
className={`w-full flex items-center justify-between px-3.5 py-3 rounded-xl text-xs font-semibold transition-all ${
|
||||
activeTab === 'users'
|
||||
? 'bg-slate-100 dark:bg-slate-800 text-cyan-600 dark:text-cyan-400 border border-slate-200 dark:border-cyan-500/30 shadow-sm'
|
||||
: 'text-slate-600 dark:text-slate-400 hover:text-slate-900 dark:hover:text-white hover:bg-slate-100 dark:hover:bg-slate-800/60'
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
<FiUsers className="text-base" />
|
||||
<span>Usuarios & Roles</span>
|
||||
</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>
|
||||
<Badge variant="secondary" size="sm">{users.length}</Badge>
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={() => { setActiveTab('finance'); setSidebarOpen(false); }}
|
||||
className={`w-full flex items-center gap-3 px-3.5 py-3 rounded-xl text-xs font-semibold transition-all ${
|
||||
activeTab === 'finance'
|
||||
? 'bg-slate-100 dark:bg-slate-800 text-cyan-600 dark:text-cyan-400 border border-slate-200 dark:border-cyan-500/30 shadow-sm'
|
||||
: 'text-slate-600 dark:text-slate-400 hover:text-slate-900 dark:hover:text-white hover:bg-slate-100 dark:hover:bg-slate-800/60'
|
||||
}`}
|
||||
>
|
||||
<FiDollarSign className="text-base" />
|
||||
<span>Finanzas & Ventas</span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={() => { setActiveTab('settings'); setSidebarOpen(false); }}
|
||||
className={`w-full flex items-center gap-3 px-3.5 py-3 rounded-xl text-xs font-semibold transition-all ${
|
||||
activeTab === 'settings'
|
||||
? 'bg-slate-100 dark:bg-slate-800 text-cyan-600 dark:text-cyan-400 border border-slate-200 dark:border-cyan-500/30 shadow-sm'
|
||||
: 'text-slate-600 dark:text-slate-400 hover:text-slate-900 dark:hover:text-white hover:bg-slate-100 dark:hover:bg-slate-800/60'
|
||||
}`}
|
||||
>
|
||||
<FiSettings className="text-base" />
|
||||
<span>Configuración Global</span>
|
||||
</button>
|
||||
</nav>
|
||||
</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>
|
||||
{/* Sidebar Footer */}
|
||||
<div className="pt-4 border-t border-slate-200 dark:border-slate-800 space-y-3">
|
||||
<div className="flex items-center justify-between px-2">
|
||||
<span className="text-xs text-slate-600 dark:text-slate-400 font-medium">Modo</span>
|
||||
<ThemeToggle variant="pill" size="sm" />
|
||||
</div>
|
||||
|
||||
<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>
|
||||
<Link to="/">
|
||||
<Button variant="outline" size="sm" fullWidth leftIcon={<FiArrowLeft />}>
|
||||
Volver al inicio
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
{/* TAB 1: Gestor de Cursos */}
|
||||
{activeTab === 'courses' && (
|
||||
<div className="space-y-6">
|
||||
{/* 2. Main Content Area */}
|
||||
<div className="flex-1 flex flex-col min-w-0">
|
||||
|
||||
{/* 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"
|
||||
/>
|
||||
{/* Top Header Bar */}
|
||||
<header className="sticky top-0 z-30 h-16 bg-white/90 dark:bg-slate-900/90 border-b border-slate-200 dark:border-slate-800 backdrop-blur-xl px-4 sm:px-8 flex items-center justify-between gap-4">
|
||||
<div className="flex items-center gap-3">
|
||||
<button
|
||||
onClick={() => setSidebarOpen(true)}
|
||||
className="md:hidden text-slate-600 dark:text-slate-400 hover:text-slate-900 dark:hover:text-white p-2"
|
||||
>
|
||||
<FiMenu className="text-xl" />
|
||||
</button>
|
||||
<h1 className="text-base sm:text-lg font-bold text-slate-900 dark:text-white tracking-tight">
|
||||
{activeTab === 'overview' && 'Panel General de Administración'}
|
||||
{activeTab === 'courses' && 'Catálogo & Moderación de Cursos'}
|
||||
{activeTab === 'users' && 'Directorio de Usuarios & Permisos'}
|
||||
{activeTab === 'finance' && 'Finanzas, Comisiones & Repartición'}
|
||||
{activeTab === 'settings' && 'Ajustes del Sistema RyCourses'}
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-3">
|
||||
<Badge variant="success" size="md">
|
||||
<span className="w-2 h-2 rounded-full bg-emerald-400 animate-pulse mr-1" />
|
||||
Servidor Producción
|
||||
</Badge>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{/* Dynamic Content Views */}
|
||||
<main className="p-4 sm:p-8 space-y-8 flex-1">
|
||||
|
||||
{/* TAB 0: OVERVIEW */}
|
||||
{activeTab === 'overview' && (
|
||||
<div className="space-y-8">
|
||||
|
||||
{/* KPI Cards Grid */}
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-5">
|
||||
<Card variant="elevated" padding="md" className="space-y-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-xs text-slate-500 dark:text-slate-400 font-medium">Ventas Brutas Totales</span>
|
||||
<div className="w-9 h-9 rounded-xl bg-emerald-500/10 text-emerald-600 dark:text-emerald-400 flex items-center justify-center text-lg">
|
||||
<FiDollarSign />
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-2xl font-black text-slate-900 dark:text-white font-mono">${totalGrossRevenue.toLocaleString()}</div>
|
||||
<div className="text-[11px] text-emerald-600 dark:text-emerald-400 font-semibold flex items-center gap-1">
|
||||
<FiTrendingUp /> +24.8% en crecimiento
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Card variant="elevated" padding="md" className="space-y-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-xs text-slate-500 dark:text-slate-400 font-medium">Usuarios Registrados</span>
|
||||
<div className="w-9 h-9 rounded-xl bg-cyan-500/10 text-cyan-600 dark:text-cyan-400 flex items-center justify-center text-lg">
|
||||
<FiUsers />
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-2xl font-black text-slate-900 dark:text-white font-mono">54,320</div>
|
||||
<div className="text-[11px] text-slate-500 dark:text-slate-400">
|
||||
<strong className="text-cyan-600 dark:text-cyan-400">1,240</strong> Instructores activos
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Card variant="elevated" padding="md" className="space-y-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-xs text-slate-500 dark:text-slate-400 font-medium">Cursos Publicados</span>
|
||||
<div className="w-9 h-9 rounded-xl bg-violet-500/10 text-violet-600 dark:text-violet-400 flex items-center justify-center text-lg">
|
||||
<FiBookOpen />
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-2xl font-black text-slate-900 dark:text-white font-mono">{courses.length} Cursos</div>
|
||||
<div className="text-[11px] text-amber-600 dark:text-amber-400 font-medium">2 pendientes de revisión</div>
|
||||
</Card>
|
||||
|
||||
<Card variant="elevated" padding="md" className="space-y-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-xs text-slate-500 dark:text-slate-400 font-medium">Comisión Neta Plataforma ({commissionRate}%)</span>
|
||||
<div className="w-9 h-9 rounded-xl bg-amber-500/10 text-amber-600 dark:text-amber-400 flex items-center justify-center text-lg">
|
||||
<FiPieChart />
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-2xl font-black text-slate-900 dark:text-white font-mono">${netCommission.toLocaleString()}</div>
|
||||
<div className="text-[11px] text-slate-500 dark:text-slate-400">Ingreso neto RyCourses</div>
|
||||
</Card>
|
||||
</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>
|
||||
{/* Quick Summary Grid */}
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||
|
||||
{/* Recent Courses Moderation Preview */}
|
||||
<Card variant="elevated" padding="md" className="space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<h3 className="font-bold text-sm text-slate-900 dark:text-white">Últimos Cursos Registrados</h3>
|
||||
<Button variant="ghost" size="xs" onClick={() => setActiveTab('courses')}>
|
||||
Ver catálogo completo
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="space-y-3">
|
||||
{courses.slice(0, 4).map((c) => (
|
||||
<div key={c.id} className="flex items-center justify-between p-3 rounded-xl bg-slate-100/80 dark:bg-slate-950/60 border border-slate-200 dark:border-slate-800">
|
||||
<div className="flex items-center gap-3 min-w-0">
|
||||
<img src={c.thumbnail} alt={c.title} className="w-10 h-8 rounded object-cover border border-slate-300 dark:border-slate-700 shrink-0" />
|
||||
<div className="min-w-0">
|
||||
<h4 className="font-bold text-xs text-slate-900 dark:text-white truncate">{c.title}</h4>
|
||||
<span className="text-[10px] text-slate-500 dark:text-slate-400">{c.instructor.name}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Badge variant="primary" size="sm">${c.price}</Badge>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* System Status & Server Logs */}
|
||||
<Card variant="elevated" padding="md" className="space-y-4">
|
||||
<h3 className="font-bold text-sm text-slate-900 dark:text-white">Estado de Infraestructura & Logs</h3>
|
||||
<div className="space-y-3 text-xs">
|
||||
<div className="flex justify-between p-3 bg-slate-100/80 dark:bg-slate-950 rounded-xl border border-slate-200 dark:border-slate-800">
|
||||
<span className="text-slate-600 dark:text-slate-400">Base de Datos PostgreSQL (Supabase):</span>
|
||||
<span className="font-mono text-emerald-600 dark:text-emerald-400 font-bold">Conectado (4ms)</span>
|
||||
</div>
|
||||
<div className="flex justify-between p-3 bg-slate-100/80 dark:bg-slate-950 rounded-xl border border-slate-200 dark:border-slate-800">
|
||||
<span className="text-slate-600 dark:text-slate-400">Pasarela Stripe / PayPal Webhooks:</span>
|
||||
<span className="font-mono text-emerald-600 dark:text-emerald-400 font-bold">Operativo 100%</span>
|
||||
</div>
|
||||
<div className="flex justify-between p-3 bg-slate-100/80 dark:bg-slate-950 rounded-xl border border-slate-200 dark:border-slate-800">
|
||||
<span className="text-slate-600 dark:text-slate-400">Almacenamiento de Video CDN:</span>
|
||||
<span className="font-mono text-cyan-600 dark:text-cyan-400 font-bold">Cloudflare Stream Active</span>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
</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">
|
||||
{/* TAB 1: COURSES MANAGEMENT */}
|
||||
{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-white dark:bg-slate-900 p-4 rounded-2xl border border-slate-200 dark:border-slate-800 shadow-sm">
|
||||
<div className="w-full sm:max-w-md">
|
||||
<Input
|
||||
placeholder="Buscar por título de curso o nombre del instructor..."
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
leftIcon={<FiSearch className="text-slate-400" />}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-3 w-full sm:w-auto">
|
||||
<Select
|
||||
value={statusFilter}
|
||||
onChange={(e) => setStatusFilter(e.target.value as any)}
|
||||
options={[
|
||||
{ value: 'all', label: 'Todos los estados' },
|
||||
{ value: 'published', label: 'Publicados' },
|
||||
{ value: 'pending', label: 'En Revisión' },
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Courses Management Table */}
|
||||
<Card variant="elevated" padding="none" className="overflow-hidden">
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full text-left text-xs text-slate-700 dark:text-slate-300">
|
||||
<thead className="bg-slate-100 dark:bg-slate-950 text-slate-600 dark:text-slate-400 uppercase tracking-wider font-semibold border-b border-slate-200 dark: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">Alumnos</th>
|
||||
<th className="p-4">Estado</th>
|
||||
<th className="p-4 text-right">Acciones Admin</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-slate-200 dark:divide-slate-800/80">
|
||||
{filteredCourses.map((c) => {
|
||||
const status = getStatus(c.id);
|
||||
return (
|
||||
<tr key={c.id} className="hover:bg-slate-100/60 dark: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-300 dark:border-slate-700 shrink-0"
|
||||
/>
|
||||
<div>
|
||||
<h4 className="font-bold text-slate-900 dark:text-white max-w-xs line-clamp-1">{c.title}</h4>
|
||||
<span className="text-[10px] text-slate-500 dark:text-slate-500">ID: {c.id}</span>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
|
||||
<td className="p-4">
|
||||
<Badge variant="primary" size="sm">{c.category}</Badge>
|
||||
</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-800 dark:text-slate-200">{c.instructor.name}</span>
|
||||
</div>
|
||||
</td>
|
||||
|
||||
<td className="p-4 font-mono font-bold text-slate-900 dark:text-white">
|
||||
${c.price.toFixed(2)}
|
||||
</td>
|
||||
|
||||
<td className="p-4 font-mono text-slate-700 dark:text-slate-300">
|
||||
{c.studentsCount.toLocaleString()}
|
||||
</td>
|
||||
|
||||
<td className="p-4">
|
||||
{status === 'published' ? (
|
||||
<Badge variant="success" size="sm"><FiCheckCircle /> Publicado</Badge>
|
||||
) : (
|
||||
<Badge variant="warning" size="sm"><FiActivity /> En Revisión</Badge>
|
||||
)}
|
||||
</td>
|
||||
|
||||
<td className="p-4 text-right">
|
||||
<div className="flex items-center justify-end gap-2">
|
||||
<Button
|
||||
variant={status === 'published' ? 'outline' : 'success'}
|
||||
size="xs"
|
||||
onClick={() => toggleCourseStatus(c.id, c.title)}
|
||||
>
|
||||
{status === 'published' ? 'Pausar' : 'Aprobar'}
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
variant="danger"
|
||||
size="xs"
|
||||
onClick={() => onDeleteCourse(c.id)}
|
||||
>
|
||||
<FiTrash2 />
|
||||
</Button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* TAB 2: USERS MANAGEMENT */}
|
||||
{activeTab === 'users' && (
|
||||
<div className="space-y-6">
|
||||
|
||||
<div className="flex flex-col sm:flex-row items-center justify-between gap-4 bg-white dark:bg-slate-900 p-4 rounded-2xl border border-slate-200 dark:border-slate-800 shadow-sm">
|
||||
<div className="w-full sm:max-w-md">
|
||||
<Input
|
||||
placeholder="Buscar usuario por nombre, email o rol..."
|
||||
value={userSearchTerm}
|
||||
onChange={(e) => setUserSearchTerm(e.target.value)}
|
||||
leftIcon={<FiSearch className="text-slate-400" />}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Badge variant="secondary" size="md">
|
||||
Total Registrados: {users.length}
|
||||
</Badge>
|
||||
</div>
|
||||
|
||||
<Card variant="elevated" padding="none" className="overflow-hidden">
|
||||
<table className="w-full text-left text-xs text-slate-700 dark:text-slate-300">
|
||||
<thead className="bg-slate-100 dark:bg-slate-950 text-slate-600 dark:text-slate-400 uppercase tracking-wider font-semibold border-b border-slate-200 dark: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">Usuario</th>
|
||||
<th className="p-4">Correo</th>
|
||||
<th className="p-4">Rol</th>
|
||||
<th className="p-4">Cursos</th>
|
||||
<th className="p-4">Registro</th>
|
||||
<th className="p-4">Estado</th>
|
||||
<th className="p-4 text-right">Acciones Admin</th>
|
||||
<th className="p-4 text-right">Acción</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 className="divide-y divide-slate-200 dark:divide-slate-800/80">
|
||||
{filteredUsers.map((u) => (
|
||||
<tr key={u.id} className="hover:bg-slate-100/60 dark:hover:bg-slate-800/40 transition">
|
||||
<td className="p-4 font-bold text-slate-900 dark:text-white">{u.name}</td>
|
||||
<td className="p-4 text-slate-500 dark:text-slate-400 font-mono">{u.email}</td>
|
||||
<td className="p-4">
|
||||
<Badge
|
||||
variant={u.role === 'Administrador' ? 'danger' : u.role === 'Instructor' ? 'secondary' : 'primary'}
|
||||
size="sm"
|
||||
>
|
||||
{u.role}
|
||||
</Badge>
|
||||
</td>
|
||||
<td className="p-4 font-mono">{u.courses}</td>
|
||||
<td className="p-4 text-slate-500 dark:text-slate-400">{u.joined}</td>
|
||||
<td className="p-4">
|
||||
<Badge variant={u.status === 'Activo' ? 'success' : 'danger'} size="sm">
|
||||
{u.status}
|
||||
</Badge>
|
||||
</td>
|
||||
<td className="p-4 text-right">
|
||||
<Button
|
||||
variant={u.status === 'Activo' ? 'outline' : 'success'}
|
||||
size="xs"
|
||||
onClick={() => toggleUserStatus(u.id, u.name)}
|
||||
>
|
||||
{u.status === 'Activo' ? 'Suspender' : 'Activar'}
|
||||
</Button>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
</div>
|
||||
)}
|
||||
|
||||
</div>
|
||||
)}
|
||||
{/* TAB 3: FINANCIALS */}
|
||||
{activeTab === 'finance' && (
|
||||
<div className="space-y-6">
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<Card variant="elevated" padding="lg" className="space-y-4">
|
||||
<h3 className="font-bold text-slate-900 dark:text-white text-base">Desglose de Repartición Financiera</h3>
|
||||
<div className="space-y-3 text-xs">
|
||||
<div className="flex justify-between p-3.5 bg-slate-100/80 dark:bg-slate-950 rounded-xl border border-slate-200 dark:border-slate-800">
|
||||
<span className="text-slate-600 dark:text-slate-400">Ventas Brutas Acumuladas:</span>
|
||||
<span className="font-mono font-bold text-slate-900 dark:text-white">${totalGrossRevenue.toLocaleString()}</span>
|
||||
</div>
|
||||
<div className="flex justify-between p-3.5 bg-slate-100/80 dark:bg-slate-950 rounded-xl border border-slate-200 dark:border-slate-800">
|
||||
<span className="text-slate-600 dark:text-slate-400">Pago a Instructores ({100 - commissionRate}%):</span>
|
||||
<span className="font-mono font-bold text-emerald-600 dark:text-emerald-400">${(totalGrossRevenue * ((100 - commissionRate) / 100)).toLocaleString()}</span>
|
||||
</div>
|
||||
<div className="flex justify-between p-3.5 bg-slate-100/80 dark:bg-slate-950 rounded-xl border border-slate-200 dark:border-slate-800">
|
||||
<span className="text-slate-600 dark:text-slate-400">Comisión Neta RyCourses ({commissionRate}%):</span>
|
||||
<span className="font-mono font-bold text-cyan-600 dark:text-cyan-400">${netCommission.toLocaleString()}</span>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Card variant="elevated" padding="lg" className="space-y-4">
|
||||
<h3 className="font-bold text-slate-900 dark:text-white text-base">Métodos de Pago & Webhooks</h3>
|
||||
<p className="text-xs text-slate-600 dark:text-slate-400 leading-relaxed">
|
||||
Integración automatizada con Stripe Connect y PayPal Payouts API para la liquidación semanal a instructores.
|
||||
</p>
|
||||
<div className="p-4 rounded-xl bg-emerald-500/10 border border-emerald-500/20 text-emerald-600 dark:text-emerald-400 text-xs font-semibold flex items-center gap-2">
|
||||
<FiCheckCircle /> Transacciones de pago en vivo cifradas a 256-bit SSL.
|
||||
</div>
|
||||
</Card>
|
||||
</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 4: GLOBAL SETTINGS */}
|
||||
{activeTab === 'settings' && (
|
||||
<Card variant="elevated" padding="lg" className="max-w-xl space-y-6">
|
||||
<h3 className="font-bold text-slate-900 dark:text-white text-base">Configuración de Plataforma</h3>
|
||||
|
||||
{/* 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="space-y-4">
|
||||
<Input
|
||||
label="Nombre de la Plataforma"
|
||||
value={siteName}
|
||||
onChange={(e) => setSiteName(e.target.value)}
|
||||
/>
|
||||
|
||||
<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>
|
||||
)}
|
||||
<Input
|
||||
label="Porcentaje de Comisión Base Plataforma (%)"
|
||||
type="number"
|
||||
value={commissionRate}
|
||||
onChange={(e) => setCommissionRate(Number(e.target.value))}
|
||||
/>
|
||||
|
||||
{/* 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"
|
||||
<Button
|
||||
variant="gradient"
|
||||
fullWidth
|
||||
size="md"
|
||||
onClick={() => onShowToast('Ajustes globales guardados con éxito')}
|
||||
>
|
||||
Guardar Cambios
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Card>
|
||||
)}
|
||||
|
||||
</main>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
);
|
||||
};
|
||||
+274
-32
@@ -1,7 +1,19 @@
|
||||
import React, { useState } from 'react';
|
||||
import React, { useState, useRef } from 'react';
|
||||
import { useNavigate } from 'react-router';
|
||||
import type { Course } from '../types/course';
|
||||
import { FiArrowLeft, FiPlus, FiTrash2, FiUploadCloud } from 'react-icons/fi';
|
||||
import {
|
||||
FiArrowLeft,
|
||||
FiPlus,
|
||||
FiTrash2,
|
||||
FiUploadCloud,
|
||||
FiVideo,
|
||||
FiCheckCircle,
|
||||
FiPlay,
|
||||
FiX,
|
||||
FiFilm,
|
||||
FiLink,
|
||||
FiImage
|
||||
} from 'react-icons/fi';
|
||||
|
||||
interface CreateCourseViewProps {
|
||||
onCreateCourse: (course: Course) => void;
|
||||
@@ -13,6 +25,8 @@ export const CreateCourseView: React.FC<CreateCourseViewProps> = ({
|
||||
onShowToast,
|
||||
}) => {
|
||||
const navigate = useNavigate();
|
||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||
const imageInputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
const [title, setTitle] = useState('');
|
||||
const [subtitle, setSubtitle] = useState('');
|
||||
@@ -20,18 +34,76 @@ export const CreateCourseView: React.FC<CreateCourseViewProps> = ({
|
||||
const [level, setLevel] = useState<'Principiante' | 'Intermedio' | 'Avanzado' | 'Todos los niveles'>('Principiante');
|
||||
const [price, setPrice] = useState(29.99);
|
||||
const [originalPrice, setOriginalPrice] = useState(89.99);
|
||||
|
||||
// Thumbnail Image Drag & Drop State
|
||||
const [thumbnailFile, setThumbnailFile] = useState<File | null>(null);
|
||||
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 },
|
||||
// Video Upload States
|
||||
const [videoFile, setVideoFile] = useState<File | null>(null);
|
||||
const [videoPreviewUrl, setVideoPreviewUrl] = useState<string>('');
|
||||
const [videoEmbedUrl, setVideoEmbedUrl] = useState<string>('');
|
||||
const [isUploading, setIsUploading] = useState(false);
|
||||
const [uploadProgress, setUploadProgress] = useState(0);
|
||||
|
||||
const [modules, setModules] = useState<{ title: string; lessonsCount: number; videoName?: string }[]>([
|
||||
{ title: 'Módulo 1: Introducción y Fundamentos', lessonsCount: 4, videoName: '01_introduccion.mp4' },
|
||||
{ title: 'Módulo 2: Proyecto Práctico Principal', lessonsCount: 6, videoName: '02_proyecto_demo.mp4' },
|
||||
]);
|
||||
|
||||
const [newModuleTitle, setNewModuleTitle] = useState('');
|
||||
|
||||
const handleImageFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const file = e.target.files?.[0];
|
||||
if (!file) return;
|
||||
|
||||
setThumbnailFile(file);
|
||||
const objectUrl = URL.createObjectURL(file);
|
||||
setThumbnail(objectUrl);
|
||||
onShowToast(`Imagen de portada "${file.name}" cargada correctamente`);
|
||||
};
|
||||
|
||||
const handleRemoveThumbnail = () => {
|
||||
setThumbnailFile(null);
|
||||
setThumbnail('https://images.unsplash.com/photo-1517694712202-14dd9538aa97?w=800&auto=format&fit=crop&q=80');
|
||||
if (imageInputRef.current) imageInputRef.current.value = '';
|
||||
};
|
||||
|
||||
// Handle Video File Upload Simulation
|
||||
const handleVideoFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const file = e.target.files?.[0];
|
||||
if (!file) return;
|
||||
|
||||
setVideoFile(file);
|
||||
setIsUploading(true);
|
||||
setUploadProgress(15);
|
||||
|
||||
// Simulate progress bar upload to 100%
|
||||
const interval = setInterval(() => {
|
||||
setUploadProgress((prev) => {
|
||||
if (prev >= 100) {
|
||||
clearInterval(interval);
|
||||
setIsUploading(false);
|
||||
const previewUrl = URL.createObjectURL(file);
|
||||
setVideoPreviewUrl(previewUrl);
|
||||
onShowToast(`¡Video "${file.name}" cargado exitosamente!`);
|
||||
return 100;
|
||||
}
|
||||
return prev + 25;
|
||||
});
|
||||
}, 200);
|
||||
};
|
||||
|
||||
const handleRemoveVideo = () => {
|
||||
setVideoFile(null);
|
||||
setVideoPreviewUrl('');
|
||||
setUploadProgress(0);
|
||||
if (fileInputRef.current) fileInputRef.current.value = '';
|
||||
};
|
||||
|
||||
const handleAddModule = () => {
|
||||
if (newModuleTitle.trim()) {
|
||||
setModules([...modules, { title: newModuleTitle, lessonsCount: 3 }]);
|
||||
setModules([...modules, { title: newModuleTitle, lessonsCount: 3, videoName: 'clase_muestra.mp4' }]);
|
||||
setNewModuleTitle('');
|
||||
}
|
||||
};
|
||||
@@ -63,7 +135,7 @@ export const CreateCourseView: React.FC<CreateCourseViewProps> = ({
|
||||
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',
|
||||
avatar: 'https://images.unsplash.com/photo-1534528741775-53994a69daeb?w=800&auto=format&fit=crop&q=80',
|
||||
rating: 5.0,
|
||||
totalStudents: 1,
|
||||
totalCourses: 1,
|
||||
@@ -92,28 +164,28 @@ export const CreateCourseView: React.FC<CreateCourseViewProps> = ({
|
||||
};
|
||||
|
||||
onCreateCourse(newCourse);
|
||||
onShowToast('¡Tu curso ha sido publicado exitosamente en el Marketplace!');
|
||||
navigate('/seller');
|
||||
onShowToast('¡Tu curso, portada y videos han sido publicados exitosamente!');
|
||||
navigate('/teachers');
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-slate-950 py-10">
|
||||
<div className="min-h-screen transition-colors duration-300 bg-slate-950 text-slate-100 py-10 selection:bg-cyan-500 font-sans">
|
||||
<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')}
|
||||
onClick={() => navigate('/teachers')}
|
||||
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
|
||||
Publicar Nuevo Curso & Cargar Contenido
|
||||
</h1>
|
||||
<p className="text-xs text-slate-400">
|
||||
Completa los detalles de tu curso para lanzarlo a la comunidad de estudiantes
|
||||
Sube la portada y los videos de tus clases para lanzar tu contenido a la plataforma.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -193,10 +265,117 @@ export const CreateCourseView: React.FC<CreateCourseViewProps> = ({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Card 2: Precio e Imagen */}
|
||||
{/* Card 2: Subir Video de Muestra / Lección Promocional */}
|
||||
<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
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h3 className="text-sm font-bold text-cyan-400 uppercase tracking-wider flex items-center gap-2">
|
||||
<FiVideo /> 2. Subir Video Promocional / Clase de Muestra
|
||||
</h3>
|
||||
<p className="text-xs text-slate-400 mt-1">
|
||||
Sube el archivo de video (.mp4, .mov, .webm) o pega la URL directa de tu lección para la vista previa del curso.
|
||||
</p>
|
||||
</div>
|
||||
<span className="px-2.5 py-1 rounded bg-violet-500/10 text-violet-300 border border-violet-500/20 text-[11px] font-bold">
|
||||
HD 1080p
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Video Upload Dropzone */}
|
||||
<div className="space-y-4">
|
||||
<input
|
||||
ref={fileInputRef}
|
||||
type="file"
|
||||
accept="video/*"
|
||||
onChange={handleVideoFileChange}
|
||||
className="hidden"
|
||||
id="video-upload-input"
|
||||
/>
|
||||
|
||||
{!videoFile && !videoPreviewUrl ? (
|
||||
<label
|
||||
htmlFor="video-upload-input"
|
||||
className="flex flex-col items-center justify-center p-8 border-2 border-dashed border-slate-700 hover:border-cyan-500/60 rounded-2xl bg-slate-950/60 hover:bg-slate-950 transition cursor-pointer group text-center space-y-3"
|
||||
>
|
||||
<div className="w-14 h-14 rounded-2xl bg-cyan-500/10 text-cyan-400 flex items-center justify-center text-2xl group-hover:scale-110 transition-transform">
|
||||
<FiUploadCloud />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm font-bold text-white">Haz clic o arrastra tu archivo de video aquí</p>
|
||||
<p className="text-xs text-slate-400 mt-0.5">Soporta MP4, MOV, WEBM hasta 2GB por clase</p>
|
||||
</div>
|
||||
<span className="px-4 py-2 bg-slate-800 hover:bg-slate-700 text-cyan-400 font-semibold rounded-xl text-xs transition border border-slate-700">
|
||||
Seleccionar Archivo de Video
|
||||
</span>
|
||||
</label>
|
||||
) : (
|
||||
<div className="p-4 rounded-2xl bg-slate-950 border border-slate-800 space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-10 h-10 rounded-xl bg-emerald-500/10 text-emerald-400 flex items-center justify-center text-xl">
|
||||
<FiFilm />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-xs font-bold text-white truncate max-w-xs">{videoFile?.name || 'video_promocional.mp4'}</p>
|
||||
<p className="text-[11px] text-emerald-400 font-semibold flex items-center gap-1">
|
||||
<FiCheckCircle /> Video procesado correctamente ({videoFile ? `${(videoFile.size / (1024 * 1024)).toFixed(1)} MB` : '142 MB'})
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleRemoveVideo}
|
||||
className="p-2 rounded-lg bg-slate-800 hover:bg-rose-950 text-slate-400 hover:text-rose-400 transition"
|
||||
title="Eliminar video"
|
||||
>
|
||||
<FiX />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{isUploading && (
|
||||
<div className="space-y-1">
|
||||
<div className="flex justify-between text-[11px] text-slate-400 font-mono">
|
||||
<span>Cargando video al servidor de la plataforma...</span>
|
||||
<span>{uploadProgress}%</span>
|
||||
</div>
|
||||
<div className="w-full bg-slate-800 h-2 rounded-full overflow-hidden">
|
||||
<div className="bg-cyan-500 h-full transition-all duration-300" style={{ width: `${uploadProgress}%` }} />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{videoPreviewUrl && (
|
||||
<div className="aspect-video w-full rounded-xl overflow-hidden bg-black border border-slate-800 relative">
|
||||
<video
|
||||
src={videoPreviewUrl}
|
||||
controls
|
||||
className="w-full h-full object-contain"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="pt-2">
|
||||
<label className="block text-xs font-semibold text-slate-300 mb-1 flex items-center gap-1.5">
|
||||
<FiLink className="text-slate-400" /> O pega el enlace directo de tu video (Vimeo, YouTube, Cloudflare)
|
||||
</label>
|
||||
<input
|
||||
type="url"
|
||||
value={videoEmbedUrl}
|
||||
onChange={(e) => setVideoEmbedUrl(e.target.value)}
|
||||
placeholder="https://vimeo.com/123456789 o https://youtube.com/watch?v=..."
|
||||
className="w-full px-4 py-2.5 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 font-mono"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Card 3: Precio e Imagen de Portada (Drag & Drop + URL) */}
|
||||
<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 flex items-center gap-2">
|
||||
<FiImage /> 3. Precio e Imagen de Portada
|
||||
</h3>
|
||||
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||
@@ -216,7 +395,7 @@ export const CreateCourseView: React.FC<CreateCourseViewProps> = ({
|
||||
|
||||
<div>
|
||||
<label className="block text-xs font-semibold text-slate-300 mb-1">
|
||||
Precio Original Táchado ($ USD)
|
||||
Precio Original Tachado ($ USD)
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
@@ -229,34 +408,97 @@ export const CreateCourseView: React.FC<CreateCourseViewProps> = ({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-xs font-semibold text-slate-300 mb-1">
|
||||
URL de la Imagen de Portada
|
||||
{/* Thumbnail Drag and Drop Section */}
|
||||
<div className="space-y-4 pt-2">
|
||||
<label className="block text-xs font-semibold text-slate-300">
|
||||
Imagen de Portada (Thumbnail del Curso)
|
||||
</label>
|
||||
<div className="flex gap-2">
|
||||
|
||||
<input
|
||||
ref={imageInputRef}
|
||||
type="file"
|
||||
accept="image/*"
|
||||
onChange={handleImageFileChange}
|
||||
className="hidden"
|
||||
id="create-view-image-upload"
|
||||
/>
|
||||
|
||||
{!thumbnailFile ? (
|
||||
<label
|
||||
htmlFor="create-view-image-upload"
|
||||
className="flex flex-col items-center justify-center p-8 border-2 border-dashed border-slate-700 hover:border-cyan-500/60 rounded-2xl bg-slate-950/60 hover:bg-slate-950 transition cursor-pointer group text-center space-y-3"
|
||||
>
|
||||
<div className="w-12 h-12 rounded-2xl bg-cyan-500/10 text-cyan-400 flex items-center justify-center text-xl group-hover:scale-110 transition-transform">
|
||||
<FiUploadCloud />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-xs font-bold text-white">Haz clic o arrastra tu imagen de portada aquí</p>
|
||||
<p className="text-[11px] text-slate-400 mt-0.5">Soporta PNG, JPG, WEBP o SVG (Recomendado: 1280x720)</p>
|
||||
</div>
|
||||
<span className="px-3.5 py-1.5 bg-slate-800 hover:bg-slate-700 text-cyan-400 font-semibold rounded-xl text-xs transition border border-slate-700">
|
||||
Seleccionar Imagen
|
||||
</span>
|
||||
</label>
|
||||
) : (
|
||||
<div className="p-3 rounded-xl bg-slate-950 border border-slate-800 flex items-center justify-between">
|
||||
<div className="flex items-center gap-2.5">
|
||||
<FiCheckCircle className="text-emerald-400 text-lg" />
|
||||
<span className="text-xs font-bold text-white truncate max-w-xs">{thumbnailFile.name}</span>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleRemoveThumbnail}
|
||||
className="p-1.5 rounded-lg bg-slate-800 text-slate-400 hover:text-rose-400 transition"
|
||||
>
|
||||
<FiX />
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Live Thumbnail Preview */}
|
||||
{thumbnail && (
|
||||
<div className="aspect-video max-w-sm rounded-xl overflow-hidden bg-slate-950 border border-slate-800 shadow-md">
|
||||
<img src={thumbnail} alt="Vista previa de portada" className="w-full h-full object-cover" />
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div>
|
||||
<label className="block text-xs font-semibold text-slate-400 mb-1 flex items-center gap-1.5">
|
||||
<FiLink /> O pega la URL directa de la imagen de portada
|
||||
</label>
|
||||
<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"
|
||||
onChange={(e) => {
|
||||
setThumbnailFile(null);
|
||||
setThumbnail(e.target.value);
|
||||
}}
|
||||
placeholder="https://images.unsplash.com/..."
|
||||
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 font-mono"
|
||||
/>
|
||||
</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 */}
|
||||
{/* Card 4: Estructura del Temario & Asignación de Videos por Módulo */}
|
||||
<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
|
||||
4. Módulos y Videos de Clases
|
||||
</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>
|
||||
<div key={idx} className="p-4 bg-slate-950 border border-slate-800 rounded-xl text-xs flex items-center justify-between gap-3">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-8 h-8 rounded-lg bg-cyan-500/10 text-cyan-400 flex items-center justify-center text-sm">
|
||||
<FiPlay />
|
||||
</div>
|
||||
<div>
|
||||
<span className="font-semibold text-white block">{m.title}</span>
|
||||
<span className="text-[10px] text-slate-400">Video adjunto: {m.videoName || 'leccion.mp4'}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleRemoveModule(idx)}
|
||||
@@ -290,7 +532,7 @@ export const CreateCourseView: React.FC<CreateCourseViewProps> = ({
|
||||
<div className="flex items-center justify-end gap-4 pt-4">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => navigate('/seller')}
|
||||
onClick={() => navigate('/teachers')}
|
||||
className="px-6 py-3.5 bg-slate-800 hover:bg-slate-700 text-slate-300 font-semibold rounded-xl text-xs transition"
|
||||
>
|
||||
Cancelar
|
||||
@@ -300,7 +542,7 @@ export const CreateCourseView: React.FC<CreateCourseViewProps> = ({
|
||||
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>
|
||||
<span>Publicar Curso y Videos</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
|
||||
+164
-254
@@ -1,18 +1,19 @@
|
||||
import React, { useState } from 'react';
|
||||
import { useNavigate, Link } from 'react-router';
|
||||
import { BrandLogo } from '../components/common/BrandLogo';
|
||||
import {
|
||||
Button,
|
||||
Input,
|
||||
Checkbox,
|
||||
ThemeToggle,
|
||||
Card
|
||||
} from '../components/ui';
|
||||
import {
|
||||
FiMail,
|
||||
FiLock,
|
||||
FiEye,
|
||||
FiEyeOff,
|
||||
FiArrowRight,
|
||||
FiArrowLeft,
|
||||
FiStar,
|
||||
FiCheckCircle,
|
||||
FiBookOpen,
|
||||
FiAward,
|
||||
FiUsers
|
||||
FiCheckCircle
|
||||
} from 'react-icons/fi';
|
||||
import type { User } from '../types/course';
|
||||
|
||||
@@ -23,303 +24,212 @@ interface LoginViewProps {
|
||||
|
||||
export const LoginView: React.FC<LoginViewProps> = ({ onShowToast, onLogin }) => {
|
||||
const navigate = useNavigate();
|
||||
|
||||
const [email, setEmail] = useState('');
|
||||
const [password, setPassword] = useState('');
|
||||
const [showPassword, setShowPassword] = useState(false);
|
||||
const [rememberMe, setRememberMe] = useState(true);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
const handleMockLogin = (name: string, role: 'buyer' | 'seller', avatarUrl?: string) => {
|
||||
const mockUser: User = {
|
||||
id: Math.random().toString(36).substr(2, 9),
|
||||
name,
|
||||
email: email || `${role}@rycourses.com`,
|
||||
role,
|
||||
avatar: avatarUrl || 'https://images.unsplash.com/photo-1534528741775-53994a69daeb?w=100&auto=format&fit=crop&q=80',
|
||||
};
|
||||
onLogin(mockUser);
|
||||
onShowToast(`¡Bienvenido de nuevo, ${name}!`);
|
||||
navigate(role === 'seller' ? '/seller' : '/courses');
|
||||
};
|
||||
const isValidEmail = email.includes('@') && email.includes('.');
|
||||
|
||||
const handleSubmit = (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
if (email && password) {
|
||||
handleMockLogin('Usuario Estudiante', 'buyer');
|
||||
}
|
||||
if (!email || !password) return;
|
||||
|
||||
setIsLoading(true);
|
||||
|
||||
setTimeout(() => {
|
||||
let derivedName = 'Usuario RyCourses';
|
||||
if (email.includes('@')) {
|
||||
const rawName = email.split('@')[0];
|
||||
derivedName = rawName
|
||||
.replace(/[._-]/g, ' ')
|
||||
.replace(/\b\w/g, (char) => char.toUpperCase());
|
||||
}
|
||||
|
||||
const isSeller = email.toLowerCase().includes('instructor') || email.toLowerCase().includes('seller');
|
||||
const role: 'buyer' | 'seller' = isSeller ? 'seller' : 'buyer';
|
||||
|
||||
const userObj: User = {
|
||||
id: Math.random().toString(36).substring(2, 11),
|
||||
name: derivedName,
|
||||
email,
|
||||
role,
|
||||
avatar: role === 'seller'
|
||||
? 'https://images.unsplash.com/photo-1573496359142-b8d87734a5a2?w=100&auto=format&fit=crop&q=80'
|
||||
: 'https://images.unsplash.com/photo-1534528741775-53994a69daeb?w=100&auto=format&fit=crop&q=80',
|
||||
};
|
||||
|
||||
onLogin(userObj);
|
||||
onShowToast(`¡Bienvenido de nuevo, ${derivedName}!`);
|
||||
setIsLoading(false);
|
||||
navigate(role === 'seller' ? '/seller' : '/courses');
|
||||
}, 450);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen grid grid-cols-1 lg:grid-cols-12 bg-slate-950 text-slate-100 font-sans selection:bg-cyan-500 selection:text-slate-950 relative overflow-hidden">
|
||||
<div className="min-h-screen flex flex-col justify-between font-sans selection:bg-cyan-500 relative overflow-hidden p-6 sm:p-10 lg:p-12 transition-colors duration-300 bg-slate-50 dark:bg-slate-950 text-slate-900 dark:text-slate-100">
|
||||
|
||||
{/* Ambient background glow */}
|
||||
<div className="absolute -top-40 -left-40 w-96 h-96 bg-cyan-600/15 rounded-full blur-[120px] pointer-events-none" />
|
||||
<div className="absolute top-1/2 right-1/3 w-80 h-80 bg-blue-600/10 rounded-full blur-[140px] pointer-events-none" />
|
||||
{/* Top-Right Background Radial Gradient Glow */}
|
||||
<div
|
||||
className="absolute -top-32 -right-32 w-[600px] sm:w-[850px] h-[600px] sm:h-[850px] rounded-full pointer-events-none opacity-30 dark:opacity-35 blur-[150px]"
|
||||
style={{
|
||||
background: 'radial-gradient(circle at center, rgba(6, 182, 212, 0.45) 0%, rgba(37, 99, 235, 0.3) 45%, transparent 70%)'
|
||||
}}
|
||||
/>
|
||||
<div className="absolute bottom-0 left-0 w-[450px] h-[450px] rounded-full pointer-events-none opacity-15 dark:opacity-20 blur-[160px] bg-blue-900/30" />
|
||||
<div className="absolute inset-0 bg-[radial-gradient(#94a3b8_1px,transparent_1px)] dark:bg-[radial-gradient(#334155_1px,transparent_1px)] [background-size:24px_24px] opacity-20 dark:opacity-10 pointer-events-none" />
|
||||
|
||||
{/* ============================================================== */}
|
||||
{/* LEFT COLUMN: BRANDING & GRAPHIC BANNER */}
|
||||
{/* ============================================================== */}
|
||||
<div className="hidden lg:flex lg:col-span-6 xl:col-span-7 relative flex-col justify-between p-10 xl:p-14 overflow-hidden bg-slate-900/40 border-r border-slate-800/60">
|
||||
{/* Interactive Background Atmosphere: Twinkling Star Particles & Flying Space Shuttle */}
|
||||
<div className="absolute inset-0 pointer-events-none overflow-hidden z-0">
|
||||
<div className="absolute top-12 left-1/6 w-1.5 h-1.5 rounded-full bg-cyan-400 animate-twinkle opacity-75" style={{ animationDelay: '0s' }} />
|
||||
<div className="absolute top-1/4 right-1/5 w-2 h-2 rounded-full bg-blue-400 animate-twinkle opacity-85" style={{ animationDelay: '1.4s' }} />
|
||||
<div className="absolute top-1/3 left-1/4 w-1 h-1 rounded-full bg-teal-400 animate-twinkle opacity-70" style={{ animationDelay: '2.1s' }} />
|
||||
<div className="absolute top-1/2 right-1/3 w-1.5 h-1.5 rounded-full bg-cyan-400 animate-twinkle opacity-80" style={{ animationDelay: '0.6s' }} />
|
||||
<div className="absolute top-2/3 left-1/5 w-2 h-2 rounded-full bg-cyan-500 animate-twinkle opacity-75" style={{ animationDelay: '1.8s' }} />
|
||||
|
||||
{/* Decorative Grid and Ambient Lights */}
|
||||
<div className="absolute inset-0 bg-[radial-gradient(#334155_1px,transparent_1px)] [background-size:20px_20px] opacity-25" />
|
||||
<div className="absolute top-1/4 left-10 w-96 h-96 bg-cyan-500/20 rounded-full blur-[140px] pointer-events-none" />
|
||||
<div className="absolute bottom-10 right-10 w-96 h-96 bg-blue-600/20 rounded-full blur-[140px] pointer-events-none" />
|
||||
{/* Crossing Space Shuttle Rocket */}
|
||||
<div className="absolute top-0 left-0 animate-spaceship">
|
||||
<svg className="w-16 h-16 sm:w-20 sm:h-20 filter drop-shadow-[0_0_25px_rgba(6,182,212,0.8)]" viewBox="0 0 100 100" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M22 78L10 90L26 84L14 98L30 86L22 78Z" fill="url(#plasmaFlame)" className="animate-pulse" />
|
||||
<path d="M78 22C62 26 42 42 34 58L26 62L38 74L42 66C58 58 74 38 78 22Z" fill="url(#rocketBodyGradient)" stroke="#38bdf8" strokeWidth="2" strokeLinejoin="round" />
|
||||
<path d="M78 22C74 23 68 28 64 34L66 36C72 32 77 26 78 22Z" fill="#22d3ee" />
|
||||
<path d="M34 58L16 64L24 50L30 52Z" fill="#0284c7" stroke="#38bdf8" strokeWidth="1.5" strokeLinejoin="round" />
|
||||
<path d="M42 66L48 84L50 76L46 70Z" fill="#0284c7" stroke="#38bdf8" strokeWidth="1.5" strokeLinejoin="round" />
|
||||
<circle cx="58" cy="42" r="5" fill="#0f172a" stroke="#38bdf8" strokeWidth="1.5" />
|
||||
<circle cx="58" cy="42" r="3.5" fill="#22d3ee" />
|
||||
<circle cx="59.5" cy="40.5" r="1" fill="#ffffff" />
|
||||
<circle cx="48" cy="52" r="4" fill="#0f172a" stroke="#38bdf8" strokeWidth="1.5" />
|
||||
<circle cx="48" cy="52" r="2.5" fill="#22d3ee" />
|
||||
|
||||
{/* Brand Header Banner */}
|
||||
<div className="relative z-10 flex items-center justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<BrandLogo size="lg" showSubtitle={true} />
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-1.5 px-3.5 py-1.5 rounded-full bg-slate-900/80 border border-slate-700/60 text-xs font-semibold text-slate-300 backdrop-blur-md">
|
||||
<FiCheckCircle className="text-emerald-400" />
|
||||
<span>+1,200 Cursos Disponibles</span>
|
||||
</div>
|
||||
<defs>
|
||||
<linearGradient id="plasmaFlame" x1="30" y1="78" x2="10" y2="98" gradientUnits="userSpaceOnUse">
|
||||
<stop stopColor="#38bdf8" />
|
||||
<stop offset="0.5" stopColor="#06b6d4" />
|
||||
<stop offset="1" stopColor="transparent" />
|
||||
</linearGradient>
|
||||
<linearGradient id="rocketBodyGradient" x1="78" y1="22" x2="26" y2="74" gradientUnits="userSpaceOnUse">
|
||||
<stop stopColor="#e0f2fe" />
|
||||
<stop offset="0.4" stopColor="#38bdf8" />
|
||||
<stop offset="0.8" stopColor="#0284c7" />
|
||||
<stop offset="1" stopColor="#0f172a" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
{/* Hero Illustration Box */}
|
||||
<div className="relative z-10 my-auto py-6 max-w-2xl mx-auto w-full">
|
||||
|
||||
{/* Main Visual Frame */}
|
||||
<div className="relative rounded-3xl p-1.5 bg-gradient-to-b from-slate-700/60 via-slate-800/40 to-cyan-500/30 border border-slate-700/80 shadow-2xl backdrop-blur-md group">
|
||||
|
||||
{/* Embedded 3D Learning Image */}
|
||||
<div className="relative overflow-hidden rounded-[20px] aspect-[4/3]">
|
||||
<img
|
||||
src="/login-banner.jpg"
|
||||
alt="Aprende y crece en RyCourses"
|
||||
className="w-full h-full object-cover transform group-hover:scale-105 transition-transform duration-700"
|
||||
/>
|
||||
<div className="absolute inset-0 bg-gradient-to-t from-slate-950 via-slate-950/20 to-transparent" />
|
||||
</div>
|
||||
|
||||
{/* Top Badge Overlay */}
|
||||
<div className="absolute top-5 left-5 z-20 flex items-center gap-2 py-1.5 px-3.5 rounded-full bg-slate-950/80 border border-cyan-500/40 text-cyan-300 text-xs font-bold shadow-lg backdrop-blur-md">
|
||||
<FiStar className="text-yellow-400 fill-yellow-400" />
|
||||
<span>Aprende con Tecnologías Reales</span>
|
||||
</div>
|
||||
|
||||
{/* Bottom Overlay Card */}
|
||||
<div className="absolute bottom-5 left-5 right-5 z-20 p-4 rounded-2xl bg-slate-950/85 border border-slate-800/90 shadow-2xl backdrop-blur-xl space-y-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-10 h-10 rounded-xl bg-cyan-500/20 border border-cyan-500/40 flex items-center justify-center text-cyan-400 font-bold text-lg">
|
||||
🚀
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-xs font-bold text-white tracking-tight">
|
||||
Aumenta tus habilidades profesionales
|
||||
</p>
|
||||
<p className="text-[11px] text-slate-400">
|
||||
Cursos prácticos creados por mentores de la industria.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="hidden sm:flex items-center gap-1 bg-amber-500/10 px-2.5 py-1 rounded-lg border border-amber-500/30 text-amber-300 text-xs font-extrabold">
|
||||
<FiStar className="fill-amber-400 text-amber-400" />
|
||||
<span>4.9 / 5.0</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
{/* Feature Highlights Grid */}
|
||||
<div className="relative z-10 grid grid-cols-3 gap-4 pt-4 border-t border-slate-800/60">
|
||||
<div className="flex items-center gap-3 p-3 rounded-2xl bg-slate-900/60 border border-slate-800/60">
|
||||
<div className="w-9 h-9 rounded-xl bg-cyan-500/10 border border-cyan-500/30 flex items-center justify-center text-cyan-400 shrink-0">
|
||||
<FiBookOpen className="text-base" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-xs font-bold text-white">Acceso De Por Vida</p>
|
||||
<p className="text-[10px] text-slate-400">Estudia a tu propio ritmo</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-3 p-3 rounded-2xl bg-slate-900/60 border border-slate-800/60">
|
||||
<div className="w-9 h-9 rounded-xl bg-blue-500/10 border border-blue-500/30 flex items-center justify-center text-blue-400 shrink-0">
|
||||
<FiAward className="text-base" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-xs font-bold text-white">Certificados</p>
|
||||
<p className="text-[10px] text-slate-400">Acredita tus proyectos</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-3 p-3 rounded-2xl bg-slate-900/60 border border-slate-800/60">
|
||||
<div className="w-9 h-9 rounded-xl bg-emerald-500/10 border border-emerald-500/30 flex items-center justify-center text-emerald-400 shrink-0">
|
||||
<FiUsers className="text-base" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-xs font-bold text-white">+50,000 Alumnos</p>
|
||||
<p className="text-[10px] text-slate-400">Comunidad global activa</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
{/* ============================================================== */}
|
||||
{/* RIGHT COLUMN: FORM & AUTHENTICATION */}
|
||||
{/* ============================================================== */}
|
||||
<div className="lg:col-span-6 xl:col-span-5 flex flex-col justify-between p-6 sm:p-10 lg:p-12 z-10 bg-slate-950/90 backdrop-blur-2xl border-l border-slate-800/80">
|
||||
{/* Top Navigation Bar */}
|
||||
<div className="w-full max-w-6xl mx-auto flex items-center justify-between z-10 pt-2 pb-4">
|
||||
{/* Reusable UI ThemeToggle Component */}
|
||||
<ThemeToggle variant="pill" />
|
||||
|
||||
{/* Top Header Navigation */}
|
||||
<div className="flex items-center justify-between">
|
||||
<Link to="/" className="inline-flex items-center gap-2 lg:hidden">
|
||||
<BrandLogo size="md" showSubtitle={false} />
|
||||
</Link>
|
||||
{/* Back Link Button */}
|
||||
<Link to="/">
|
||||
<Button variant="outline" size="sm" leftIcon={<FiArrowLeft />}>
|
||||
Volver al inicio
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<div className="hidden lg:block" />
|
||||
{/* Main Login Card Container */}
|
||||
<div className="my-auto w-full max-w-md mx-auto z-10">
|
||||
<Card variant="glass" padding="lg" className="relative space-y-6 shadow-2xl">
|
||||
|
||||
<Link
|
||||
to="/"
|
||||
className="flex items-center gap-1.5 text-xs font-semibold text-slate-400 hover:text-cyan-400 transition-colors py-2 px-3 rounded-xl bg-slate-900/80 border border-slate-800 hover:border-cyan-500/40"
|
||||
>
|
||||
<FiArrowLeft className="text-sm" />
|
||||
<span>Volver al inicio</span>
|
||||
</Link>
|
||||
</div>
|
||||
{/* Subtle Top Border Gradient Accent */}
|
||||
<div className="absolute top-0 left-6 right-6 h-[1.5px] bg-gradient-to-r from-transparent via-cyan-500/60 to-transparent pointer-events-none" />
|
||||
|
||||
{/* Login Form Main Container */}
|
||||
<div className="my-auto py-8 max-w-md w-full mx-auto space-y-7">
|
||||
|
||||
{/* Section Titles */}
|
||||
<div className="space-y-2">
|
||||
<div className="inline-flex items-center gap-2 px-3 py-1 rounded-full bg-cyan-500/10 border border-cyan-500/20 text-cyan-400 text-xs font-medium">
|
||||
<span className="w-2 h-2 rounded-full bg-cyan-400 animate-pulse" />
|
||||
Tu Portal Educativo
|
||||
{/* Centered Brand Logo & Titles inside Form Card */}
|
||||
<div className="text-center space-y-2 pt-1">
|
||||
<div className="flex justify-center pb-1">
|
||||
<BrandLogo size="lg" showSubtitle={true} />
|
||||
</div>
|
||||
<h1 className="text-3xl sm:text-4xl font-extrabold text-white tracking-tight">
|
||||
¡Hola de nuevo! <span className="inline-block animate-bounce">👋</span>
|
||||
<h1 className="text-2xl sm:text-3xl font-bold tracking-tight pt-2 text-slate-900 dark:text-white">
|
||||
Iniciar Sesión
|
||||
</h1>
|
||||
<p className="text-xs sm:text-sm text-slate-400">
|
||||
Ingresa tus credenciales para continuar tu ruta de aprendizaje
|
||||
<p className="text-xs sm:text-sm text-slate-600 dark:text-slate-400">
|
||||
Ingresa tus credenciales para acceder a tu cuenta.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Quick Demo Access Bar */}
|
||||
<div className="p-3.5 rounded-2xl bg-slate-900/90 border border-slate-800 space-y-2">
|
||||
<p className="text-[11px] font-semibold text-slate-400 uppercase tracking-wider">
|
||||
Acceso Rápido Demo:
|
||||
</p>
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleMockLogin('Carlos Estudiante', 'buyer', 'https://images.unsplash.com/photo-1534528741775-53994a69daeb?w=100&auto=format&fit=crop&q=80')}
|
||||
className="flex items-center justify-center gap-2 py-2 px-3 rounded-xl bg-slate-800/80 hover:bg-cyan-950/60 border border-slate-700 hover:border-cyan-500/50 text-xs font-medium text-slate-200 hover:text-cyan-300 transition group"
|
||||
>
|
||||
<FiBookOpen className="text-cyan-400 group-hover:scale-110 transition-transform" />
|
||||
<span>Como Estudiante</span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleMockLogin('Dra. Elena Instructor', 'seller', 'https://images.unsplash.com/photo-1573496359142-b8d87734a5a2?w=100&auto=format&fit=crop&q=80')}
|
||||
className="flex items-center justify-center gap-2 py-2 px-3 rounded-xl bg-slate-800/80 hover:bg-violet-950/60 border border-slate-700 hover:border-violet-500/50 text-xs font-medium text-slate-200 hover:text-violet-300 transition group"
|
||||
>
|
||||
<FiUsers className="text-violet-400 group-hover:scale-110 transition-transform" />
|
||||
<span>Como Instructor</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Login Form */}
|
||||
{/* Form */}
|
||||
<form onSubmit={handleSubmit} className="space-y-4">
|
||||
|
||||
{/* Email field */}
|
||||
{/* Email field using UI Input */}
|
||||
<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 text-base" />
|
||||
<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/90 border border-slate-800 focus:border-cyan-500 rounded-xl text-xs text-white placeholder-slate-500 focus:outline-none focus:ring-2 focus:ring-cyan-500/30 transition"
|
||||
/>
|
||||
</div>
|
||||
<Input
|
||||
label="Correo electrónico"
|
||||
type="email"
|
||||
required
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
placeholder="[email protected]"
|
||||
leftIcon={<FiMail className="text-slate-400" />}
|
||||
/>
|
||||
{isValidEmail && (
|
||||
<div className="flex items-center gap-1 text-[11px] text-emerald-500 font-medium mt-1 animate-fadeIn">
|
||||
<FiCheckCircle className="text-xs" /> Formato válido
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Password field */}
|
||||
{/* Password field using UI Input (built-in password eye toggle) */}
|
||||
<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] font-medium text-cyan-400 hover:text-cyan-300 hover:underline transition">
|
||||
<Input
|
||||
label="Contraseña"
|
||||
type="password"
|
||||
required
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
placeholder="••••••••"
|
||||
leftIcon={<FiLock className="text-slate-400" />}
|
||||
/>
|
||||
<div className="flex justify-end mt-1">
|
||||
<a href="#" className="text-[11px] font-medium text-cyan-600 dark:text-cyan-400 hover:underline transition">
|
||||
¿Olvidaste tu contraseña?
|
||||
</a>
|
||||
</div>
|
||||
<div className="relative">
|
||||
<FiLock className="absolute left-3.5 top-3.5 text-slate-500 text-base" />
|
||||
<input
|
||||
type={showPassword ? 'text' : 'password'}
|
||||
required
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
placeholder="••••••••"
|
||||
className="w-full pl-10 pr-10 py-3 bg-slate-900/90 border border-slate-800 focus:border-cyan-500 rounded-xl text-xs text-white placeholder-slate-500 focus:outline-none focus:ring-2 focus:ring-cyan-500/30 transition"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowPassword(!showPassword)}
|
||||
className="absolute right-3.5 top-3.5 text-slate-500 hover:text-slate-300 transition"
|
||||
>
|
||||
{showPassword ? <FiEyeOff className="text-base" /> : <FiEye className="text-base" />}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Remember Me */}
|
||||
<div className="flex items-center justify-between pt-1">
|
||||
<label className="flex items-center gap-2 cursor-pointer text-xs text-slate-400 hover:text-slate-300">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={rememberMe}
|
||||
onChange={(e) => setRememberMe(e.target.checked)}
|
||||
className="rounded border-slate-700 bg-slate-900 text-cyan-500 focus:ring-cyan-500/50"
|
||||
/>
|
||||
<span>Recordar este dispositivo</span>
|
||||
</label>
|
||||
{/* Remember Me using UI Checkbox */}
|
||||
<div className="pt-0.5">
|
||||
<Checkbox
|
||||
checked={rememberMe}
|
||||
onChange={(e) => setRememberMe(e.target.checked)}
|
||||
label="Recordar sesión"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Submit Button */}
|
||||
<button
|
||||
{/* Submit Button using UI Button */}
|
||||
<Button
|
||||
type="submit"
|
||||
className="w-full py-3.5 px-4 bg-gradient-to-r from-blue-600 via-cyan-500 to-teal-400 hover:opacity-95 text-white font-bold rounded-xl text-xs sm:text-sm shadow-lg shadow-cyan-500/25 transition-all flex items-center justify-center gap-2 group cursor-pointer"
|
||||
variant="gradient"
|
||||
size="lg"
|
||||
fullWidth
|
||||
isLoading={isLoading}
|
||||
loadingText="Verificando credenciales..."
|
||||
rightIcon={<FiArrowRight className="text-base" />}
|
||||
>
|
||||
<span>Ingresar a RyCourses</span>
|
||||
<FiArrowRight className="text-base group-hover:translate-x-1 transition-transform" />
|
||||
</button>
|
||||
Acceder a la cuenta
|
||||
</Button>
|
||||
</form>
|
||||
|
||||
{/* Register Callout */}
|
||||
<div className="text-center text-xs text-slate-400 pt-2 border-t border-slate-900">
|
||||
¿Aún no eres miembro de RyCourses?{' '}
|
||||
<Link to="/register" className="text-cyan-400 font-bold hover:text-cyan-300 hover:underline transition">
|
||||
Regístrate gratis aquí
|
||||
<div className="text-center text-xs pt-3 border-t border-slate-200 dark:border-slate-800 text-slate-600 dark:text-slate-400">
|
||||
¿Aún no tienes una cuenta?{' '}
|
||||
<Link to="/register" className="text-cyan-600 dark:text-cyan-400 font-semibold hover:underline transition">
|
||||
Regístrate aquí
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
{/* Footer info */}
|
||||
<div className="text-center lg:text-left text-[11px] text-slate-600">
|
||||
© 2026 RyCourses Platform. Todos los derechos reservados.
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Footer info */}
|
||||
<div className="w-full max-w-6xl mx-auto text-center text-[11px] z-10 pt-4 pb-2 text-slate-500 dark:text-slate-500">
|
||||
© 2026 RyCourses. Todos los derechos reservados.
|
||||
</div>
|
||||
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
+160
-211
@@ -1,19 +1,20 @@
|
||||
import React, { useState } from 'react';
|
||||
import { useNavigate, Link } from 'react-router';
|
||||
import { BrandLogo } from '../components/common/BrandLogo';
|
||||
import {
|
||||
Button,
|
||||
Input,
|
||||
Checkbox,
|
||||
ThemeToggle,
|
||||
Card
|
||||
} from '../components/ui';
|
||||
import {
|
||||
FiMail,
|
||||
FiLock,
|
||||
FiUser,
|
||||
FiArrowRight,
|
||||
FiArrowLeft,
|
||||
FiCheckCircle,
|
||||
FiStar,
|
||||
FiBookOpen,
|
||||
FiAward,
|
||||
FiUsers,
|
||||
FiEye,
|
||||
FiEyeOff
|
||||
FiCheckCircle
|
||||
} from 'react-icons/fi';
|
||||
import { FaGraduationCap, FaChalkboardTeacher } from 'react-icons/fa';
|
||||
import type { User } from '../types/course';
|
||||
@@ -25,288 +26,236 @@ interface RegisterViewProps {
|
||||
|
||||
export const RegisterView: React.FC<RegisterViewProps> = ({ onShowToast, onLogin }) => {
|
||||
const navigate = useNavigate();
|
||||
|
||||
const [name, setName] = useState('');
|
||||
const [email, setEmail] = useState('');
|
||||
const [password, setPassword] = useState('');
|
||||
const [showPassword, setShowPassword] = useState(false);
|
||||
const [accountType, setAccountType] = useState<'buyer' | 'seller'>('buyer');
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
const isValidEmail = email.includes('@') && email.includes('.');
|
||||
|
||||
const handleSubmit = (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
if (name && email && password) {
|
||||
if (!name || !email || !password) return;
|
||||
|
||||
setIsLoading(true);
|
||||
setTimeout(() => {
|
||||
const newUser: User = {
|
||||
id: Math.random().toString(36).substr(2, 9),
|
||||
id: Math.random().toString(36).substring(2, 11),
|
||||
name,
|
||||
email,
|
||||
role: accountType,
|
||||
avatar: 'https://images.unsplash.com/photo-1534528741775-53994a69daeb?w=100&auto=format&fit=crop&q=80',
|
||||
avatar: accountType === 'seller'
|
||||
? 'https://images.unsplash.com/photo-1573496359142-b8d87734a5a2?w=100&auto=format&fit=crop&q=80'
|
||||
: 'https://images.unsplash.com/photo-1534528741775-53994a69daeb?w=100&auto=format&fit=crop&q=80',
|
||||
};
|
||||
onLogin(newUser);
|
||||
onShowToast(`¡Cuenta de ${accountType === 'buyer' ? 'Estudiante' : 'Instructor'} creada con éxito!`);
|
||||
setIsLoading(false);
|
||||
navigate(accountType === 'seller' ? '/seller' : '/courses');
|
||||
}
|
||||
}, 450);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen grid grid-cols-1 lg:grid-cols-12 bg-slate-950 text-slate-100 font-sans selection:bg-cyan-500 selection:text-slate-950 relative overflow-hidden">
|
||||
<div className="min-h-screen flex flex-col justify-between font-sans selection:bg-cyan-500 relative overflow-hidden p-6 sm:p-10 lg:p-12 transition-colors duration-300 bg-slate-50 dark:bg-slate-950 text-slate-900 dark:text-slate-100">
|
||||
|
||||
{/* Ambient background glow */}
|
||||
<div className="absolute -top-40 -left-40 w-96 h-96 bg-blue-600/15 rounded-full blur-[120px] pointer-events-none" />
|
||||
<div className="absolute top-1/2 right-1/3 w-80 h-80 bg-cyan-600/10 rounded-full blur-[140px] pointer-events-none" />
|
||||
{/* Top-Right Background Radial Gradient Glow */}
|
||||
<div
|
||||
className="absolute -top-32 -right-32 w-[600px] sm:w-[850px] h-[600px] sm:h-[850px] rounded-full pointer-events-none opacity-30 dark:opacity-35 blur-[150px]"
|
||||
style={{
|
||||
background: 'radial-gradient(circle at center, rgba(6, 182, 212, 0.45) 0%, rgba(37, 99, 235, 0.3) 45%, transparent 70%)'
|
||||
}}
|
||||
/>
|
||||
<div className="absolute bottom-0 left-0 w-[450px] h-[450px] rounded-full pointer-events-none opacity-15 dark:opacity-20 blur-[160px] bg-blue-900/30" />
|
||||
<div className="absolute inset-0 bg-[radial-gradient(#94a3b8_1px,transparent_1px)] dark:bg-[radial-gradient(#334155_1px,transparent_1px)] [background-size:24px_24px] opacity-20 dark:opacity-10 pointer-events-none" />
|
||||
|
||||
{/* ============================================================== */}
|
||||
{/* LEFT COLUMN: BRANDING BANNER */}
|
||||
{/* ============================================================== */}
|
||||
<div className="hidden lg:flex lg:col-span-6 xl:col-span-7 relative flex-col justify-between p-10 xl:p-14 overflow-hidden bg-slate-900/40 border-r border-slate-800/60">
|
||||
{/* Interactive Background Atmosphere: Twinkling Star Particles & Flying Space Shuttle */}
|
||||
<div className="absolute inset-0 pointer-events-none overflow-hidden z-0">
|
||||
<div className="absolute top-12 left-1/6 w-1.5 h-1.5 rounded-full bg-cyan-400 animate-twinkle opacity-75" style={{ animationDelay: '0s' }} />
|
||||
<div className="absolute top-1/4 right-1/5 w-2 h-2 rounded-full bg-blue-400 animate-twinkle opacity-85" style={{ animationDelay: '1.4s' }} />
|
||||
<div className="absolute top-1/3 left-1/4 w-1 h-1 rounded-full bg-teal-400 animate-twinkle opacity-70" style={{ animationDelay: '2.1s' }} />
|
||||
<div className="absolute top-1/2 right-1/3 w-1.5 h-1.5 rounded-full bg-cyan-400 animate-twinkle opacity-80" style={{ animationDelay: '0.6s' }} />
|
||||
<div className="absolute top-2/3 left-1/5 w-2 h-2 rounded-full bg-cyan-500 animate-twinkle opacity-75" style={{ animationDelay: '1.8s' }} />
|
||||
|
||||
{/* Ambient Lights */}
|
||||
<div className="absolute inset-0 bg-[radial-gradient(#334155_1px,transparent_1px)] [background-size:20px_20px] opacity-25" />
|
||||
<div className="absolute top-1/4 left-10 w-96 h-96 bg-cyan-500/20 rounded-full blur-[140px] pointer-events-none" />
|
||||
<div className="absolute bottom-10 right-10 w-96 h-96 bg-blue-600/20 rounded-full blur-[140px] pointer-events-none" />
|
||||
{/* Crossing Space Shuttle Rocket */}
|
||||
<div className="absolute top-0 left-0 animate-spaceship">
|
||||
<svg className="w-16 h-16 sm:w-20 sm:h-20 filter drop-shadow-[0_0_25px_rgba(6,182,212,0.8)]" viewBox="0 0 100 100" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M22 78L10 90L26 84L14 98L30 86L22 78Z" fill="url(#plasmaFlameReg)" className="animate-pulse" />
|
||||
<path d="M78 22C62 26 42 42 34 58L26 62L38 74L42 66C58 58 74 38 78 22Z" fill="url(#rocketBodyGradientReg)" stroke="#38bdf8" strokeWidth="2" strokeLinejoin="round" />
|
||||
<path d="M78 22C74 23 68 28 64 34L66 36C72 32 77 26 78 22Z" fill="#22d3ee" />
|
||||
<path d="M34 58L16 64L24 50L30 52Z" fill="#0284c7" stroke="#38bdf8" strokeWidth="1.5" strokeLinejoin="round" />
|
||||
<path d="M42 66L48 84L50 76L46 70Z" fill="#0284c7" stroke="#38bdf8" strokeWidth="1.5" strokeLinejoin="round" />
|
||||
<circle cx="58" cy="42" r="5" fill="#0f172a" stroke="#38bdf8" strokeWidth="1.5" />
|
||||
<circle cx="58" cy="42" r="3.5" fill="#22d3ee" />
|
||||
<circle cx="59.5" cy="40.5" r="1" fill="#ffffff" />
|
||||
<circle cx="48" cy="52" r="4" fill="#0f172a" stroke="#38bdf8" strokeWidth="1.5" />
|
||||
<circle cx="48" cy="52" r="2.5" fill="#22d3ee" />
|
||||
|
||||
{/* Brand Header */}
|
||||
<div className="relative z-10 flex items-center justify-between">
|
||||
<BrandLogo size="lg" showSubtitle={true} />
|
||||
|
||||
<div className="flex items-center gap-1.5 px-3.5 py-1.5 rounded-full bg-slate-900/80 border border-slate-700/60 text-xs font-semibold text-slate-300 backdrop-blur-md">
|
||||
<FiCheckCircle className="text-emerald-400" />
|
||||
<span>Regístrate 100% Gratis</span>
|
||||
</div>
|
||||
<defs>
|
||||
<linearGradient id="plasmaFlameReg" x1="30" y1="78" x2="10" y2="98" gradientUnits="userSpaceOnUse">
|
||||
<stop stopColor="#38bdf8" />
|
||||
<stop offset="0.5" stopColor="#06b6d4" />
|
||||
<stop offset="1" stopColor="transparent" />
|
||||
</linearGradient>
|
||||
<linearGradient id="rocketBodyGradientReg" x1="78" y1="22" x2="26" y2="74" gradientUnits="userSpaceOnUse">
|
||||
<stop stopColor="#e0f2fe" />
|
||||
<stop offset="0.4" stopColor="#38bdf8" />
|
||||
<stop offset="0.8" stopColor="#0284c7" />
|
||||
<stop offset="1" stopColor="#0f172a" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
{/* Hero Visual Box */}
|
||||
<div className="relative z-10 my-auto py-6 max-w-2xl mx-auto w-full">
|
||||
<div className="relative rounded-3xl p-1.5 bg-gradient-to-b from-slate-700/60 via-slate-800/40 to-cyan-500/30 border border-slate-700/80 shadow-2xl backdrop-blur-md group">
|
||||
|
||||
<div className="relative overflow-hidden rounded-[20px] aspect-[4/3]">
|
||||
<img
|
||||
src="/login-banner.jpg"
|
||||
alt="Comienza tu aprendizaje en RyCourses"
|
||||
className="w-full h-full object-cover transform group-hover:scale-105 transition-transform duration-700"
|
||||
/>
|
||||
<div className="absolute inset-0 bg-gradient-to-t from-slate-950 via-slate-950/20 to-transparent" />
|
||||
</div>
|
||||
|
||||
<div className="absolute top-5 left-5 z-20 flex items-center gap-2 py-1.5 px-3.5 rounded-full bg-slate-950/80 border border-cyan-500/40 text-cyan-300 text-xs font-bold shadow-lg backdrop-blur-md">
|
||||
<FiStar className="text-yellow-400 fill-yellow-400" />
|
||||
<span>Aprende a Tu Propio Ritmo</span>
|
||||
</div>
|
||||
|
||||
<div className="absolute bottom-5 left-5 right-5 z-20 p-4 rounded-2xl bg-slate-950/85 border border-slate-800/90 shadow-2xl backdrop-blur-xl space-y-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-10 h-10 rounded-xl bg-cyan-500/20 border border-cyan-500/40 flex items-center justify-center text-cyan-400 font-bold text-lg">
|
||||
🎓
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-xs font-bold text-white tracking-tight">
|
||||
Obtén certificados con reconocimiento internacional
|
||||
</p>
|
||||
<p className="text-[11px] text-slate-400">
|
||||
Proyectos prácticos para enriquecer tu portafolio profesional.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Feature Highlights Grid */}
|
||||
<div className="relative z-10 grid grid-cols-3 gap-4 pt-4 border-t border-slate-800/60">
|
||||
<div className="flex items-center gap-3 p-3 rounded-2xl bg-slate-900/60 border border-slate-800/60">
|
||||
<div className="w-9 h-9 rounded-xl bg-cyan-500/10 border border-cyan-500/30 flex items-center justify-center text-cyan-400 shrink-0">
|
||||
<FiBookOpen className="text-base" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-xs font-bold text-white">Cursos Prácticos</p>
|
||||
<p className="text-[10px] text-slate-400">Proyectos reales</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-3 p-3 rounded-2xl bg-slate-900/60 border border-slate-800/60">
|
||||
<div className="w-9 h-9 rounded-xl bg-blue-500/10 border border-blue-500/30 flex items-center justify-center text-blue-400 shrink-0">
|
||||
<FiAward className="text-base" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-xs font-bold text-white">Badges Digitales</p>
|
||||
<p className="text-[10px] text-slate-400">Comparte en Linkedin</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-3 p-3 rounded-2xl bg-slate-900/60 border border-slate-800/60">
|
||||
<div className="w-9 h-9 rounded-xl bg-emerald-500/10 border border-emerald-500/30 flex items-center justify-center text-emerald-400 shrink-0">
|
||||
<FiUsers className="text-base" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-xs font-bold text-white">Mentores 24/7</p>
|
||||
<p className="text-[10px] text-slate-400">Soporte continuo</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
{/* ============================================================== */}
|
||||
{/* RIGHT COLUMN: FORM & REGISTER */}
|
||||
{/* ============================================================== */}
|
||||
<div className="lg:col-span-6 xl:col-span-5 flex flex-col justify-between p-6 sm:p-10 lg:p-12 z-10 bg-slate-950/90 backdrop-blur-2xl border-l border-slate-800/80">
|
||||
{/* Top Navigation Bar */}
|
||||
<div className="w-full max-w-6xl mx-auto flex items-center justify-between z-10 pt-2 pb-4">
|
||||
{/* Reusable UI ThemeToggle Component */}
|
||||
<ThemeToggle variant="pill" />
|
||||
|
||||
{/* Top Navigation Header */}
|
||||
<div className="flex items-center justify-between">
|
||||
<Link to="/" className="inline-flex items-center gap-2 lg:hidden">
|
||||
<BrandLogo size="md" showSubtitle={false} />
|
||||
</Link>
|
||||
{/* Back Link Button */}
|
||||
<Link to="/">
|
||||
<Button variant="outline" size="sm" leftIcon={<FiArrowLeft />}>
|
||||
Volver al inicio
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<div className="hidden lg:block" />
|
||||
{/* Main Centered Register Card Container */}
|
||||
<div className="my-auto w-full max-w-md mx-auto z-10">
|
||||
<Card variant="glass" padding="lg" className="relative space-y-5 shadow-2xl">
|
||||
|
||||
<Link
|
||||
to="/"
|
||||
className="flex items-center gap-1.5 text-xs font-semibold text-slate-400 hover:text-cyan-400 transition-colors py-2 px-3 rounded-xl bg-slate-900/80 border border-slate-800 hover:border-cyan-500/40"
|
||||
>
|
||||
<FiArrowLeft className="text-sm" />
|
||||
<span>Volver al inicio</span>
|
||||
</Link>
|
||||
</div>
|
||||
{/* Subtle Top Border Gradient Accent */}
|
||||
<div className="absolute top-0 left-6 right-6 h-[1.5px] bg-gradient-to-r from-transparent via-cyan-500/60 to-transparent pointer-events-none" />
|
||||
|
||||
{/* Form Body Container */}
|
||||
<div className="my-auto py-8 max-w-md w-full mx-auto space-y-6">
|
||||
|
||||
{/* Titles */}
|
||||
<div className="space-y-2">
|
||||
<div className="inline-flex items-center gap-2 px-3 py-1 rounded-full bg-blue-500/10 border border-blue-500/20 text-cyan-400 text-xs font-medium">
|
||||
<span className="w-2 h-2 rounded-full bg-cyan-400 animate-pulse" />
|
||||
Únete a RyCourses
|
||||
{/* Centered Brand Logo & Header Titles inside Form Card */}
|
||||
<div className="text-center space-y-2 pt-1">
|
||||
<div className="flex justify-center pb-1">
|
||||
<BrandLogo size="lg" showSubtitle={true} />
|
||||
</div>
|
||||
<h1 className="text-3xl sm:text-4xl font-extrabold text-white tracking-tight">
|
||||
Crear Cuenta Gratis ✨
|
||||
<h1 className="text-2xl sm:text-3xl font-bold tracking-tight pt-2 text-slate-900 dark:text-white">
|
||||
Crear Cuenta
|
||||
</h1>
|
||||
<p className="text-xs sm:text-sm text-slate-400">
|
||||
Comienza a aprender o enseñar hoy mismo en la plataforma líder.
|
||||
<p className="text-xs sm:text-sm text-slate-600 dark:text-slate-400">
|
||||
Crea tu perfil en RyCourses para comenzar tu experiencia.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Account type switcher */}
|
||||
<div className="grid grid-cols-2 gap-2 p-1.5 bg-slate-900/90 rounded-2xl border border-slate-800">
|
||||
<div className="grid grid-cols-2 gap-2 p-1 rounded-xl border bg-slate-100 dark:bg-slate-950/80 border-slate-200 dark: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-bold transition-all ${
|
||||
className={`flex items-center justify-center gap-2 py-2 rounded-lg text-xs font-medium transition-all cursor-pointer ${
|
||||
accountType === 'buyer'
|
||||
? 'bg-gradient-to-r from-blue-600 to-cyan-500 text-white shadow-lg shadow-cyan-500/20'
|
||||
: 'text-slate-400 hover:text-white'
|
||||
? 'bg-white dark:bg-slate-800 text-cyan-600 dark:text-cyan-400 border border-slate-200 dark:border-slate-700 font-semibold shadow-sm'
|
||||
: 'text-slate-600 dark:text-slate-400 hover:text-slate-900 dark:hover:text-white'
|
||||
}`}
|
||||
>
|
||||
<FaGraduationCap className="text-base" /> Estudiante
|
||||
<FaGraduationCap className="text-sm" /> Estudiante
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setAccountType('seller')}
|
||||
className={`flex items-center justify-center gap-2 py-2.5 rounded-xl text-xs font-bold transition-all ${
|
||||
className={`flex items-center justify-center gap-2 py-2 rounded-lg text-xs font-medium transition-all cursor-pointer ${
|
||||
accountType === 'seller'
|
||||
? 'bg-gradient-to-r from-violet-600 to-purple-500 text-white shadow-lg shadow-purple-500/20'
|
||||
: 'text-slate-400 hover:text-white'
|
||||
? 'bg-white dark:bg-slate-800 text-violet-600 dark:text-violet-400 border border-slate-200 dark:border-slate-700 font-semibold shadow-sm'
|
||||
: 'text-slate-600 dark:text-slate-400 hover:text-slate-900 dark:hover:text-white'
|
||||
}`}
|
||||
>
|
||||
<FaChalkboardTeacher className="text-base" /> Instructor
|
||||
<FaChalkboardTeacher className="text-sm" /> Instructor
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Form */}
|
||||
<form onSubmit={handleSubmit} className="space-y-3.5">
|
||||
<div>
|
||||
<label className="block text-xs font-semibold text-slate-300 mb-1">
|
||||
Nombre Completo
|
||||
</label>
|
||||
<div className="relative">
|
||||
<FiUser className="absolute left-3.5 top-3.5 text-slate-500 text-base" />
|
||||
<input
|
||||
type="text"
|
||||
required
|
||||
value={name}
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
placeholder="Ej. Sofía Rodríguez"
|
||||
className="w-full pl-10 pr-4 py-2.5 bg-slate-900/90 border border-slate-800 focus:border-cyan-500 rounded-xl text-xs text-white placeholder-slate-500 focus:outline-none focus:ring-2 focus:ring-cyan-500/30 transition"
|
||||
/>
|
||||
</div>
|
||||
<Input
|
||||
label="Nombre Completo"
|
||||
type="text"
|
||||
required
|
||||
value={name}
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
placeholder="Ej. Sofía Rodríguez"
|
||||
leftIcon={<FiUser className="text-slate-400" />}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-xs font-semibold text-slate-300 mb-1">
|
||||
Correo Electrónico
|
||||
</label>
|
||||
<div className="relative">
|
||||
<FiMail className="absolute left-3.5 top-3.5 text-slate-500 text-base" />
|
||||
<input
|
||||
type="email"
|
||||
required
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
placeholder="[email protected]"
|
||||
className="w-full pl-10 pr-4 py-2.5 bg-slate-900/90 border border-slate-800 focus:border-cyan-500 rounded-xl text-xs text-white placeholder-slate-500 focus:outline-none focus:ring-2 focus:ring-cyan-500/30 transition"
|
||||
/>
|
||||
</div>
|
||||
<Input
|
||||
label="Correo Electrónico"
|
||||
type="email"
|
||||
required
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
placeholder="[email protected]"
|
||||
leftIcon={<FiMail className="text-slate-400" />}
|
||||
/>
|
||||
{isValidEmail && (
|
||||
<div className="flex items-center gap-1 text-[11px] text-emerald-500 font-medium mt-1 animate-fadeIn">
|
||||
<FiCheckCircle className="text-xs" /> Formato válido
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-xs font-semibold text-slate-300 mb-1">
|
||||
Contraseña
|
||||
</label>
|
||||
<div className="relative">
|
||||
<FiLock className="absolute left-3.5 top-3.5 text-slate-500 text-base" />
|
||||
<input
|
||||
type={showPassword ? 'text' : 'password'}
|
||||
required
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
placeholder="Mínimo 8 caracteres"
|
||||
className="w-full pl-10 pr-10 py-2.5 bg-slate-900/90 border border-slate-800 focus:border-cyan-500 rounded-xl text-xs text-white placeholder-slate-500 focus:outline-none focus:ring-2 focus:ring-cyan-500/30 transition"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowPassword(!showPassword)}
|
||||
className="absolute right-3.5 top-3 text-slate-500 hover:text-slate-300 transition"
|
||||
>
|
||||
{showPassword ? <FiEyeOff className="text-base" /> : <FiEye className="text-base" />}
|
||||
</button>
|
||||
</div>
|
||||
<Input
|
||||
label="Contraseña"
|
||||
type="password"
|
||||
required
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
placeholder="Mínimo 8 caracteres"
|
||||
leftIcon={<FiLock className="text-slate-400" />}
|
||||
/>
|
||||
</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 y Políticas de Privacidad.</span>
|
||||
<div className="pt-0.5">
|
||||
<Checkbox
|
||||
checked={true}
|
||||
readOnly
|
||||
label={
|
||||
<span>
|
||||
Acepto los{' '}
|
||||
<Link to="/terms" target="_blank" className="text-cyan-600 dark:text-cyan-400 font-semibold hover:underline">
|
||||
Términos de Servicio y Políticas de Privacidad
|
||||
</Link>.
|
||||
</span>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<button
|
||||
<Button
|
||||
type="submit"
|
||||
className="w-full py-3.5 px-4 bg-gradient-to-r from-blue-600 via-cyan-500 to-teal-400 hover:opacity-95 text-white font-bold rounded-xl text-xs sm:text-sm shadow-lg shadow-cyan-500/25 transition-all flex items-center justify-center gap-2 group cursor-pointer"
|
||||
variant="gradient"
|
||||
size="lg"
|
||||
fullWidth
|
||||
isLoading={isLoading}
|
||||
loadingText="Creando perfil..."
|
||||
rightIcon={<FiArrowRight className="text-base" />}
|
||||
>
|
||||
<span>Crear Cuenta como {accountType === 'buyer' ? 'Estudiante' : 'Instructor'}</span>
|
||||
<FiArrowRight className="text-base group-hover:translate-x-1 transition-transform" />
|
||||
</button>
|
||||
Crear cuenta como {accountType === 'buyer' ? 'Estudiante' : 'Instructor'}
|
||||
</Button>
|
||||
</form>
|
||||
|
||||
{/* Login Link */}
|
||||
<div className="text-center text-xs text-slate-400 pt-2 border-t border-slate-900">
|
||||
<div className="text-center text-xs pt-3 border-t border-slate-200 dark:border-slate-800 text-slate-600 dark:text-slate-400">
|
||||
¿Ya tienes una cuenta registrada?{' '}
|
||||
<Link to="/login" className="text-cyan-400 font-bold hover:text-cyan-300 hover:underline transition">
|
||||
<Link to="/login" className="text-cyan-600 dark:text-cyan-400 font-semibold hover:underline transition">
|
||||
Inicia sesión aquí
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
{/* Footer info */}
|
||||
<div className="text-center lg:text-left text-[11px] text-slate-600">
|
||||
© 2026 RyCourses Platform. Todos los derechos reservados.
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Footer info */}
|
||||
<div className="w-full max-w-6xl mx-auto text-center text-[11px] z-10 pt-4 pb-2 text-slate-500 dark:text-slate-500">
|
||||
© 2026 RyCourses. Todos los derechos reservados.
|
||||
</div>
|
||||
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
+130
-24
@@ -1,23 +1,37 @@
|
||||
import React from 'react';
|
||||
import type { Course } from '../types/course';
|
||||
import { useNavigate, Link } from 'react-router';
|
||||
import type { Course, User } 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';
|
||||
import { Card, Button, Badge } from '../components/ui';
|
||||
import {
|
||||
FiDollarSign,
|
||||
FiGlobe,
|
||||
FiVideo,
|
||||
FiArrowRight,
|
||||
FiHelpCircle
|
||||
} from 'react-icons/fi';
|
||||
|
||||
interface SellerViewProps {
|
||||
currentUser: User | null;
|
||||
courses: Course[];
|
||||
onOpenCreateCourse: () => void;
|
||||
onPreviewCourse: (course: Course) => void;
|
||||
onDeleteCourse: (courseId: string) => void;
|
||||
}
|
||||
|
||||
export const SellerView: React.FC<SellerViewProps> = ({
|
||||
courses,
|
||||
onOpenCreateCourse,
|
||||
onPreviewCourse,
|
||||
onDeleteCourse,
|
||||
currentUser,
|
||||
}) => {
|
||||
const navigate = useNavigate();
|
||||
|
||||
const handleCreateCourseClick = () => {
|
||||
if (!currentUser) {
|
||||
navigate('/login');
|
||||
} else {
|
||||
navigate('/seller/create');
|
||||
}
|
||||
};
|
||||
|
||||
const handleScrollToCalculator = () => {
|
||||
const el = document.getElementById('seller-calculator');
|
||||
if (el) {
|
||||
@@ -26,28 +40,120 @@ export const SellerView: React.FC<SellerViewProps> = ({
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-0">
|
||||
{/* Seller Hero Section */}
|
||||
<div className="space-y-16 pb-20 selection:bg-cyan-500">
|
||||
|
||||
{/* 1. Hero Landing Section */}
|
||||
<SellerHero
|
||||
onOpenCreateCourse={onOpenCreateCourse}
|
||||
onOpenCreateCourse={handleCreateCourseClick}
|
||||
onScrollToCalculator={handleScrollToCalculator}
|
||||
/>
|
||||
|
||||
{/* Instructor Dashboard Stats */}
|
||||
<SellerDashboard />
|
||||
{/* 2. Direct Navigation CTA to Teacher Portal */}
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div className="p-8 rounded-3xl bg-gradient-to-r from-slate-900 via-indigo-950 to-slate-900 border border-slate-800 flex flex-col md:flex-row items-center justify-between gap-6 shadow-2xl">
|
||||
<div className="space-y-2 text-center md:text-left">
|
||||
<Badge variant="primary" size="md">¿Ya eres instructor registrado?</Badge>
|
||||
<h2 className="text-xl sm:text-2xl font-bold text-white">Accede directamente a tu Panel de Docente</h2>
|
||||
<p className="text-xs sm:text-sm text-slate-400">Gestiona tus cursos publicados, ingresos acumulados y responde a tus alumnos.</p>
|
||||
</div>
|
||||
|
||||
{/* Published Courses Management List */}
|
||||
<SellerCourseList
|
||||
courses={courses}
|
||||
onOpenCreateModal={onOpenCreateCourse}
|
||||
onPreviewCourse={onPreviewCourse}
|
||||
onDeleteCourse={onDeleteCourse}
|
||||
/>
|
||||
<Link to={currentUser ? "/teachers" : "/login"}>
|
||||
<Button variant="gradient" size="lg" rightIcon={<FiArrowRight />}>
|
||||
{currentUser ? "Ir a mi Panel Docente" : "Iniciar Sesión como Instructor"}
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 3. Why Teach on RyCourses Benefits Grid */}
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 space-y-10">
|
||||
<div className="text-center space-y-3 max-w-2xl mx-auto">
|
||||
<Badge variant="primary" size="md">Beneficios Únicos</Badge>
|
||||
<h2 className="text-3xl sm:text-4xl font-extrabold text-white tracking-tight">
|
||||
¿Por qué enseñar en RyCourses?
|
||||
</h2>
|
||||
<p className="text-xs sm:text-sm text-slate-400">
|
||||
Diseñamos las herramientas necesarias para que te enfoques en crear contenido extraordinario mientras nosotros nos encargamos de todo lo demás.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
|
||||
<Card variant="elevated" padding="lg" className="space-y-4">
|
||||
<div className="w-12 h-12 rounded-2xl bg-emerald-500/10 text-emerald-400 flex items-center justify-center text-2xl">
|
||||
<FiDollarSign />
|
||||
</div>
|
||||
<h3 className="text-lg font-bold text-white">80% de Comisión Directa</h3>
|
||||
<p className="text-xs text-slate-400 leading-relaxed">
|
||||
Conserva hasta el 80% de cada venta generada. Pagos automáticos semanales procesados a tu cuenta bancaria o PayPal.
|
||||
</p>
|
||||
</Card>
|
||||
|
||||
<Card variant="elevated" padding="lg" className="space-y-4">
|
||||
<div className="w-12 h-12 rounded-2xl bg-cyan-500/10 text-cyan-400 flex items-center justify-center text-2xl">
|
||||
<FiGlobe />
|
||||
</div>
|
||||
<h3 className="text-lg font-bold text-white">Audiencia Global</h3>
|
||||
<p className="text-xs text-slate-400 leading-relaxed">
|
||||
Llega a más de 50,000 estudiantes activos en Latinoamérica y España buscando aprender nuevas tecnologías y habilidades.
|
||||
</p>
|
||||
</Card>
|
||||
|
||||
<Card variant="elevated" padding="lg" className="space-y-4">
|
||||
<div className="w-12 h-12 rounded-2xl bg-violet-500/10 text-violet-400 flex items-center justify-center text-2xl">
|
||||
<FiVideo />
|
||||
</div>
|
||||
<h3 className="text-lg font-bold text-white">Reproductor HD & CDN Ultra Rápido</h3>
|
||||
<p className="text-xs text-slate-400 leading-relaxed">
|
||||
Tus videos se transmiten sin interrupciones con encriptación anti-descarga y calidad adaptable en cualquier dispositivo.
|
||||
</p>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 4. Earnings Calculator Section */}
|
||||
<div id="seller-calculator">
|
||||
<SellerCalculator
|
||||
onStartCourse={handleCreateCourseClick}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 5. Frequently Asked Questions */}
|
||||
<div className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 space-y-8">
|
||||
<div className="text-center space-y-3">
|
||||
<Badge variant="secondary" size="md">Preguntas Frecuentes</Badge>
|
||||
<h2 className="text-2xl sm:text-3xl font-extrabold text-white">Todo lo que necesitas saber</h2>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
<Card variant="elevated" padding="md" className="space-y-2">
|
||||
<h4 className="font-bold text-sm text-white flex items-center gap-2">
|
||||
<FiHelpCircle className="text-cyan-400" /> ¿Tengo que pagar alguna tarifa para subir mi curso?
|
||||
</h4>
|
||||
<p className="text-xs text-slate-400 pl-6">
|
||||
No. Publicar tu curso en RyCourses es 100% gratuito. Solo cobramos una comisión cuando realizas una venta efectiva.
|
||||
</p>
|
||||
</Card>
|
||||
|
||||
<Card variant="elevated" padding="md" className="space-y-2">
|
||||
<h4 className="font-bold text-sm text-white flex items-center gap-2">
|
||||
<FiHelpCircle className="text-cyan-400" /> ¿Quién conserva los derechos de propiedad intelectual de mi contenido?
|
||||
</h4>
|
||||
<p className="text-xs text-slate-400 pl-6">
|
||||
Tú mantienes el 100% de la propiedad intelectual y derechos de autor de todos los videos y materiales que crees.
|
||||
</p>
|
||||
</Card>
|
||||
|
||||
<Card variant="elevated" padding="md" className="space-y-2">
|
||||
<h4 className="font-bold text-sm text-white flex items-center gap-2">
|
||||
<FiHelpCircle className="text-cyan-400" /> ¿Cómo recibo mis pagos?
|
||||
</h4>
|
||||
<p className="text-xs text-slate-400 pl-6">
|
||||
Puedes solicitar retiros en cualquier momento desde tu panel de instructor. Depositamos vía Stripe Connect o PayPal.
|
||||
</p>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Revenue Calculator */}
|
||||
<SellerCalculator
|
||||
onStartCourse={onOpenCreateCourse}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,489 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Link } from 'react-router';
|
||||
import type { Course } from '../types/course';
|
||||
import { BrandLogo } from '../components/common/BrandLogo';
|
||||
import {
|
||||
Card,
|
||||
Button,
|
||||
Badge,
|
||||
Input,
|
||||
ThemeToggle
|
||||
} from '../components/ui';
|
||||
import {
|
||||
FiGrid,
|
||||
FiBookOpen,
|
||||
FiDollarSign,
|
||||
FiUsers,
|
||||
FiStar,
|
||||
FiPlusCircle,
|
||||
FiTrendingUp,
|
||||
FiSearch,
|
||||
FiEye,
|
||||
FiTrash2,
|
||||
FiMessageSquare,
|
||||
FiAward,
|
||||
FiMenu,
|
||||
FiX,
|
||||
FiArrowLeft,
|
||||
FiDownload
|
||||
} from 'react-icons/fi';
|
||||
|
||||
interface TeachersViewProps {
|
||||
courses: Course[];
|
||||
onOpenCreateCourse: () => void;
|
||||
onPreviewCourse: (course: Course) => void;
|
||||
onDeleteCourse: (courseId: string) => void;
|
||||
onShowToast: (msg: string) => void;
|
||||
}
|
||||
|
||||
export const TeachersView: React.FC<TeachersViewProps> = ({
|
||||
courses,
|
||||
onOpenCreateCourse,
|
||||
onPreviewCourse,
|
||||
onDeleteCourse,
|
||||
onShowToast,
|
||||
}) => {
|
||||
const [activeTab, setActiveTab] = useState<'overview' | 'my-courses' | 'payouts' | 'reviews'>('overview');
|
||||
const [sidebarOpen, setSidebarOpen] = useState(false);
|
||||
const [searchTerm, setSearchTerm] = useState('');
|
||||
const [payoutRequested, setPayoutRequested] = useState(false);
|
||||
|
||||
// Filter instructor courses
|
||||
const instructorCourses = courses.filter((c) =>
|
||||
c.title.toLowerCase().includes(searchTerm.toLowerCase()) || c.category.toLowerCase().includes(searchTerm.toLowerCase())
|
||||
);
|
||||
|
||||
const totalEarnings = 14280.50;
|
||||
const pendingPayout = 2450.00;
|
||||
const totalStudents = courses.reduce((acc, curr) => acc + curr.studentsCount, 0);
|
||||
const avgRating = (courses.reduce((acc, curr) => acc + curr.rating, 0) / (courses.length || 1)).toFixed(1);
|
||||
|
||||
const handleRequestPayout = () => {
|
||||
setPayoutRequested(true);
|
||||
onShowToast('¡Solicitud de retiro de $2,450.00 recibida y en proceso!');
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen transition-colors duration-300 bg-slate-50 dark:bg-slate-950 text-slate-900 dark:text-slate-100 flex flex-col md:flex-row font-sans selection:bg-cyan-500">
|
||||
|
||||
{/* Mobile Sidebar Overlay */}
|
||||
{sidebarOpen && (
|
||||
<div
|
||||
className="fixed inset-0 bg-slate-950/80 backdrop-blur-sm z-40 md:hidden"
|
||||
onClick={() => setSidebarOpen(false)}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* 1. Left Sidebar Navigation */}
|
||||
<aside className={`
|
||||
fixed md:sticky top-0 left-0 h-screen w-72 bg-white dark:bg-slate-900 border-r border-slate-200 dark:border-slate-800 z-50 flex flex-col justify-between p-5 transition-transform duration-300 shadow-xl
|
||||
${sidebarOpen ? 'translate-x-0' : '-translate-x-full md:translate-x-0'}
|
||||
`}>
|
||||
<div className="space-y-6">
|
||||
{/* Sidebar Brand Header */}
|
||||
<div className="flex items-center justify-between pb-3 border-b border-slate-200 dark:border-slate-800">
|
||||
<Link to="/" className="hover:opacity-90 transition">
|
||||
<BrandLogo size="sm" showSubtitle={false} />
|
||||
</Link>
|
||||
|
||||
<button
|
||||
onClick={() => setSidebarOpen(false)}
|
||||
className="md:hidden p-1.5 rounded-lg text-slate-500 hover:text-slate-900 dark:text-slate-400 dark:hover:text-white"
|
||||
>
|
||||
<FiX className="text-xl" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Teacher Badge Pill */}
|
||||
<div className="px-3 py-2 rounded-xl bg-cyan-500/10 border border-cyan-500/25 flex items-center gap-2.5">
|
||||
<div className="w-7 h-7 rounded-lg bg-cyan-500/20 text-cyan-600 dark:text-cyan-400 flex items-center justify-center text-sm font-bold shrink-0">
|
||||
T
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-xs font-bold text-slate-900 dark:text-white leading-none">Panel Docente</p>
|
||||
<p className="text-[10px] text-cyan-600 dark:text-cyan-400 font-medium mt-0.5">Instructores & Creadores</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Quick Create Course CTA */}
|
||||
<Button
|
||||
variant="gradient"
|
||||
size="md"
|
||||
fullWidth
|
||||
leftIcon={<FiPlusCircle />}
|
||||
onClick={onOpenCreateCourse}
|
||||
className="shadow-lg shadow-cyan-500/20"
|
||||
>
|
||||
Crear Nuevo Curso
|
||||
</Button>
|
||||
|
||||
{/* Navigation Links */}
|
||||
<nav className="space-y-1.5">
|
||||
<button
|
||||
onClick={() => { setActiveTab('overview'); setSidebarOpen(false); }}
|
||||
className={`w-full flex items-center gap-3 px-3.5 py-3 rounded-xl text-xs font-semibold transition-all ${
|
||||
activeTab === 'overview'
|
||||
? 'bg-slate-100 dark:bg-slate-800 text-cyan-600 dark:text-cyan-400 border border-slate-200 dark:border-cyan-500/30 shadow-sm'
|
||||
: 'text-slate-600 dark:text-slate-400 hover:text-slate-900 dark:hover:text-white hover:bg-slate-100 dark:hover:bg-slate-800/60'
|
||||
}`}
|
||||
>
|
||||
<FiGrid className="text-base" />
|
||||
<span>Resumen & Métricas</span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={() => { setActiveTab('my-courses'); setSidebarOpen(false); }}
|
||||
className={`w-full flex items-center justify-between px-3.5 py-3 rounded-xl text-xs font-semibold transition-all ${
|
||||
activeTab === 'my-courses'
|
||||
? 'bg-slate-100 dark:bg-slate-800 text-cyan-600 dark:text-cyan-400 border border-slate-200 dark:border-cyan-500/30 shadow-sm'
|
||||
: 'text-slate-600 dark:text-slate-400 hover:text-slate-900 dark:hover:text-white hover:bg-slate-100 dark:hover:bg-slate-800/60'
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
<FiBookOpen className="text-base" />
|
||||
<span>Mis Cursos</span>
|
||||
</div>
|
||||
<Badge variant="primary" size="sm">{courses.length}</Badge>
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={() => { setActiveTab('payouts'); setSidebarOpen(false); }}
|
||||
className={`w-full flex items-center justify-between px-3.5 py-3 rounded-xl text-xs font-semibold transition-all ${
|
||||
activeTab === 'payouts'
|
||||
? 'bg-slate-100 dark:bg-slate-800 text-cyan-600 dark:text-cyan-400 border border-slate-200 dark:border-cyan-500/30 shadow-sm'
|
||||
: 'text-slate-600 dark:text-slate-400 hover:text-slate-900 dark:hover:text-white hover:bg-slate-100 dark:hover:bg-slate-800/60'
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
<FiDollarSign className="text-base" />
|
||||
<span>Ganancias & Retiros</span>
|
||||
</div>
|
||||
<Badge variant="success" size="sm">${pendingPayout}</Badge>
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={() => { setActiveTab('reviews'); setSidebarOpen(false); }}
|
||||
className={`w-full flex items-center gap-3 px-3.5 py-3 rounded-xl text-xs font-semibold transition-all ${
|
||||
activeTab === 'reviews'
|
||||
? 'bg-slate-100 dark:bg-slate-800 text-cyan-600 dark:text-cyan-400 border border-slate-200 dark:border-cyan-500/30 shadow-sm'
|
||||
: 'text-slate-600 dark:text-slate-400 hover:text-slate-900 dark:hover:text-white hover:bg-slate-100 dark:hover:bg-slate-800/60'
|
||||
}`}
|
||||
>
|
||||
<FiMessageSquare className="text-base" />
|
||||
<span>Reseñas de Alumnos</span>
|
||||
</button>
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
{/* Sidebar Footer */}
|
||||
<div className="pt-4 border-t border-slate-200 dark:border-slate-800 space-y-3">
|
||||
<div className="flex items-center justify-between px-2">
|
||||
<span className="text-xs text-slate-600 dark:text-slate-400 font-medium">Modo</span>
|
||||
<ThemeToggle variant="pill" size="sm" />
|
||||
</div>
|
||||
|
||||
<Link to="/">
|
||||
<Button variant="outline" size="sm" fullWidth leftIcon={<FiArrowLeft />}>
|
||||
Volver al inicio
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
{/* 2. Main Content Area */}
|
||||
<div className="flex-1 flex flex-col min-w-0">
|
||||
|
||||
{/* Top Header Bar */}
|
||||
<header className="sticky top-0 z-30 h-16 bg-white/90 dark:bg-slate-900/90 border-b border-slate-200 dark:border-slate-800 backdrop-blur-xl px-4 sm:px-8 flex items-center justify-between gap-4">
|
||||
<div className="flex items-center gap-3">
|
||||
<button
|
||||
onClick={() => setSidebarOpen(true)}
|
||||
className="md:hidden text-slate-600 dark:text-slate-400 hover:text-slate-900 dark:hover:text-white p-2"
|
||||
>
|
||||
<FiMenu className="text-xl" />
|
||||
</button>
|
||||
<h1 className="text-base sm:text-lg font-bold text-slate-900 dark:text-white tracking-tight">
|
||||
{activeTab === 'overview' && 'Resumen del Instructor'}
|
||||
{activeTab === 'my-courses' && 'Catálogo de Mis Cursos'}
|
||||
{activeTab === 'payouts' && 'Finanzas & Solicitud de Retiros'}
|
||||
{activeTab === 'reviews' && 'Calificaciones & Feedback de Estudiantes'}
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-3">
|
||||
<Badge variant="primary" size="md">
|
||||
<span className="w-2 h-2 rounded-full bg-cyan-400 animate-pulse mr-1" />
|
||||
Docente Verificado
|
||||
</Badge>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{/* Dynamic Content Views */}
|
||||
<main className="p-4 sm:p-8 space-y-8 flex-1">
|
||||
|
||||
{/* TAB 1: OVERVIEW */}
|
||||
{activeTab === 'overview' && (
|
||||
<div className="space-y-8">
|
||||
|
||||
{/* Stat Cards Grid */}
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-5">
|
||||
<Card variant="elevated" padding="md" className="space-y-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-xs text-slate-500 dark:text-slate-400 font-medium">Ingresos Totales</span>
|
||||
<div className="w-9 h-9 rounded-xl bg-emerald-500/10 text-emerald-600 dark:text-emerald-400 flex items-center justify-center text-lg">
|
||||
<FiDollarSign />
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-2xl font-black text-slate-900 dark:text-white font-mono">${totalEarnings.toLocaleString()}</div>
|
||||
<div className="text-[11px] text-emerald-600 dark:text-emerald-400 font-semibold flex items-center gap-1">
|
||||
<FiTrendingUp /> +18.4% este mes
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Card variant="elevated" padding="md" className="space-y-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-xs text-slate-500 dark:text-slate-400 font-medium">Estudiantes Inscritos</span>
|
||||
<div className="w-9 h-9 rounded-xl bg-cyan-500/10 text-cyan-600 dark:text-cyan-400 flex items-center justify-center text-lg">
|
||||
<FiUsers />
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-2xl font-black text-slate-900 dark:text-white font-mono">{totalStudents.toLocaleString()}</div>
|
||||
<div className="text-[11px] text-slate-500 dark:text-slate-400">En {courses.length} cursos activos</div>
|
||||
</Card>
|
||||
|
||||
<Card variant="elevated" padding="md" className="space-y-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-xs text-slate-500 dark:text-slate-400 font-medium">Calificación Promedio</span>
|
||||
<div className="w-9 h-9 rounded-xl bg-amber-500/10 text-amber-600 dark:text-amber-400 flex items-center justify-center text-lg">
|
||||
<FiStar />
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-2xl font-black text-slate-900 dark:text-white font-mono">{avgRating} / 5.0</div>
|
||||
<div className="text-[11px] text-amber-600 dark:text-amber-400 font-medium">Basado en 340 reseñas</div>
|
||||
</Card>
|
||||
|
||||
<Card variant="elevated" padding="md" className="space-y-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-xs text-slate-500 dark:text-slate-400 font-medium">Saldo Disponible</span>
|
||||
<div className="w-9 h-9 rounded-xl bg-violet-500/10 text-violet-600 dark:text-violet-400 flex items-center justify-center text-lg">
|
||||
<FiAward />
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-2xl font-black text-slate-900 dark:text-white font-mono">${pendingPayout.toLocaleString()}</div>
|
||||
<Button
|
||||
variant="primary"
|
||||
size="xs"
|
||||
onClick={handleRequestPayout}
|
||||
disabled={payoutRequested}
|
||||
className="mt-1"
|
||||
>
|
||||
{payoutRequested ? 'Retiro solicitado' : 'Solicitar Retiro'}
|
||||
</Button>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Course Performance Summary */}
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<h3 className="text-sm font-extrabold text-slate-900 dark:text-white">Rendimiento por Curso</h3>
|
||||
<Button variant="ghost" size="xs" onClick={() => setActiveTab('my-courses')}>
|
||||
Ver todos los cursos
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-5">
|
||||
{courses.slice(0, 3).map((c) => (
|
||||
<Card key={c.id} variant="interactive" padding="md" className="space-y-3">
|
||||
<div className="flex items-center gap-3">
|
||||
<img src={c.thumbnail} alt={c.title} className="w-14 h-10 rounded-lg object-cover border border-slate-300 dark:border-slate-700" />
|
||||
<div className="min-w-0 flex-1">
|
||||
<h4 className="font-bold text-xs text-slate-900 dark:text-white truncate">{c.title}</h4>
|
||||
<span className="text-[10px] text-cyan-600 dark:text-cyan-400">{c.category}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-2 pt-2 border-t border-slate-200 dark:border-slate-800 text-xs">
|
||||
<div>
|
||||
<span className="text-[10px] text-slate-500 dark:text-slate-400 block">Estudiantes</span>
|
||||
<span className="font-bold font-mono text-slate-800 dark:text-slate-200">{c.studentsCount}</span>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-[10px] text-slate-500 dark:text-slate-400 block">Ingresos</span>
|
||||
<span className="font-bold font-mono text-emerald-600 dark:text-emerald-400">${(c.price * c.studentsCount * 0.8).toFixed(2)}</span>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* TAB 2: MY COURSES */}
|
||||
{activeTab === 'my-courses' && (
|
||||
<div className="space-y-6">
|
||||
|
||||
{/* Search & Filter Bar */}
|
||||
<div className="flex flex-col sm:flex-row items-center justify-between gap-4 bg-white dark:bg-slate-900 p-4 rounded-2xl border border-slate-200 dark:border-slate-800 shadow-sm">
|
||||
<div className="w-full sm:max-w-md">
|
||||
<Input
|
||||
placeholder="Buscar entre mis cursos..."
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
leftIcon={<FiSearch className="text-slate-400" />}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Button variant="gradient" size="md" leftIcon={<FiPlusCircle />} onClick={onOpenCreateCourse}>
|
||||
Crear Nuevo Curso
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Courses Grid */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
{instructorCourses.map((c) => (
|
||||
<Card key={c.id} variant="elevated" padding="none" className="overflow-hidden flex flex-col justify-between">
|
||||
<div>
|
||||
<div className="relative h-44">
|
||||
<img src={c.thumbnail} alt={c.title} className="w-full h-full object-cover" />
|
||||
<div className="absolute top-3 right-3">
|
||||
<Badge variant="success" size="sm">Publicado</Badge>
|
||||
</div>
|
||||
<div className="absolute bottom-3 left-3">
|
||||
<Badge variant="secondary" size="sm">{c.category}</Badge>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="p-5 space-y-3">
|
||||
<h3 className="font-bold text-sm text-slate-900 dark:text-white line-clamp-2">{c.title}</h3>
|
||||
<div className="flex items-center justify-between text-xs text-slate-500 dark:text-slate-400 pt-1">
|
||||
<span className="flex items-center gap-1"><FiUsers className="text-cyan-600 dark:text-cyan-400" /> {c.studentsCount} alumnos</span>
|
||||
<span className="flex items-center gap-1"><FiStar className="text-amber-500 dark:text-amber-400" /> {c.rating}</span>
|
||||
<span className="font-mono font-bold text-slate-900 dark:text-white">${c.price.toFixed(2)}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="p-4 bg-slate-100/80 dark:bg-slate-950/60 border-t border-slate-200 dark:border-slate-800 flex items-center justify-between gap-2">
|
||||
<Button variant="outline" size="sm" leftIcon={<FiEye />} onClick={() => onPreviewCourse(c)}>
|
||||
Ver Vista Previa
|
||||
</Button>
|
||||
<Button variant="ghost" size="sm" className="text-rose-600 dark:text-rose-400 hover:text-rose-700" onClick={() => onDeleteCourse(c.id)}>
|
||||
<FiTrash2 />
|
||||
</Button>
|
||||
</div>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* TAB 3: PAYOUTS */}
|
||||
{activeTab === 'payouts' && (
|
||||
<div className="space-y-6">
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
|
||||
<Card variant="elevated" padding="lg" className="space-y-4 md:col-span-1">
|
||||
<h3 className="font-extrabold text-base text-slate-900 dark:text-white">Solicitud de Retiro</h3>
|
||||
<div className="space-y-2">
|
||||
<span className="text-xs text-slate-500 dark:text-slate-400">Saldo Disponible para Transferencia</span>
|
||||
<div className="text-3xl font-black text-emerald-600 dark:text-emerald-400 font-mono">${pendingPayout.toFixed(2)}</div>
|
||||
</div>
|
||||
|
||||
<p className="text-xs text-slate-500 dark:text-slate-400">
|
||||
Las transferencias se procesan automáticamente a tu cuenta bancaria registrada o cuenta PayPal asociada.
|
||||
</p>
|
||||
|
||||
<Button
|
||||
variant="gradient"
|
||||
fullWidth
|
||||
size="md"
|
||||
onClick={handleRequestPayout}
|
||||
disabled={payoutRequested}
|
||||
>
|
||||
{payoutRequested ? 'Solicitud Enviada' : 'Transferir a mi Cuenta ($2,450.00)'}
|
||||
</Button>
|
||||
</Card>
|
||||
|
||||
{/* History Table */}
|
||||
<Card variant="elevated" padding="none" className="md:col-span-2 overflow-hidden">
|
||||
<div className="p-5 border-b border-slate-200 dark:border-slate-800 flex items-center justify-between">
|
||||
<h3 className="font-bold text-sm text-slate-900 dark:text-white">Historial Reciente de Retiros</h3>
|
||||
<Button variant="outline" size="xs" leftIcon={<FiDownload />}>
|
||||
Exportar CSV
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<table className="w-full text-left text-xs text-slate-700 dark:text-slate-300">
|
||||
<thead className="bg-slate-100 dark:bg-slate-950 text-slate-600 dark:text-slate-400 uppercase tracking-wider font-semibold border-b border-slate-200 dark:border-slate-800">
|
||||
<tr>
|
||||
<th className="p-4">Fecha</th>
|
||||
<th className="p-4">Monto</th>
|
||||
<th className="p-4">Método</th>
|
||||
<th className="p-4">Estado</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-slate-200 dark:divide-slate-800/80">
|
||||
<tr>
|
||||
<td className="p-4 font-mono">15 Feb 2026</td>
|
||||
<td className="p-4 font-mono font-bold text-emerald-600 dark:text-emerald-400">$3,420.00</td>
|
||||
<td className="p-4">Cuenta IBAN ****4921</td>
|
||||
<td className="p-4"><Badge variant="success" size="sm">Completado</Badge></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className="p-4 font-mono">01 Feb 2026</td>
|
||||
<td className="p-4 font-mono font-bold text-emerald-600 dark:text-emerald-400">$4,180.00</td>
|
||||
<td className="p-4">PayPal (carlos@rycourses.pro)</td>
|
||||
<td className="p-4"><Badge variant="success" size="sm">Completado</Badge></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* TAB 4: REVIEWS */}
|
||||
{activeTab === 'reviews' && (
|
||||
<div className="space-y-4 max-w-3xl">
|
||||
<Card variant="elevated" padding="md" className="space-y-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="font-bold text-sm text-slate-900 dark:text-white">Marcos Rodríguez</span>
|
||||
<span className="text-xs text-slate-500 dark:text-slate-400">• Curso: React & TypeScript Pro</span>
|
||||
</div>
|
||||
<div className="flex items-center text-amber-500 dark:text-amber-400 text-xs">
|
||||
<FiStar /><FiStar /><FiStar /><FiStar /><FiStar />
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-xs text-slate-700 dark:text-slate-300 leading-relaxed">
|
||||
"Explicación súper clara de la arquitectura de componentes y estado global. El proyecto final me ayudó a conseguir trabajo."
|
||||
</p>
|
||||
</Card>
|
||||
|
||||
<Card variant="elevated" padding="md" className="space-y-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="font-bold text-sm text-slate-900 dark:text-white">Sofía Martinez</span>
|
||||
<span className="text-xs text-slate-500 dark:text-slate-400">• Curso: UX/UI Masterclass</span>
|
||||
</div>
|
||||
<div className="flex items-center text-amber-500 dark:text-amber-400 text-xs">
|
||||
<FiStar /><FiStar /><FiStar /><FiStar /><FiStar />
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-xs text-slate-700 dark:text-slate-300 leading-relaxed">
|
||||
"Excelente contenido. Muy práctico y los recursos descargables son una maravilla."
|
||||
</p>
|
||||
</Card>
|
||||
</div>
|
||||
)}
|
||||
|
||||
</main>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,148 @@
|
||||
import React from 'react';
|
||||
import { Link } from 'react-router';
|
||||
import { BrandLogo } from '../components/common/BrandLogo';
|
||||
import { Card, Button, Badge } from '../components/ui';
|
||||
import {
|
||||
FiFileText,
|
||||
FiShield,
|
||||
FiLock,
|
||||
FiCreditCard,
|
||||
FiArrowLeft,
|
||||
FiCheckCircle,
|
||||
FiHelpCircle,
|
||||
FiUserCheck
|
||||
} from 'react-icons/fi';
|
||||
|
||||
export const TermsView: React.FC = () => {
|
||||
return (
|
||||
<div className="min-h-screen transition-colors duration-300 bg-slate-50 dark:bg-slate-950 text-slate-900 dark:text-slate-100 selection:bg-cyan-500 py-12 px-4 sm:px-6 lg:px-8">
|
||||
|
||||
{/* Container */}
|
||||
<div className="max-w-4xl mx-auto space-y-10">
|
||||
|
||||
{/* Navigation & Header */}
|
||||
<div className="flex items-center justify-between">
|
||||
<Link to="/">
|
||||
<BrandLogo size="md" />
|
||||
</Link>
|
||||
|
||||
<Link to="/">
|
||||
<Button variant="outline" size="sm" leftIcon={<FiArrowLeft />}>
|
||||
Volver al inicio
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
{/* Hero Title */}
|
||||
<div className="text-center space-y-4 pt-4">
|
||||
<Badge variant="primary" size="lg">
|
||||
<FiShield className="mr-1" /> Legal & Cumplimiento
|
||||
</Badge>
|
||||
<h1 className="text-3xl sm:text-5xl font-extrabold text-slate-900 dark:text-white tracking-tight">
|
||||
Términos de Servicio y Políticas de Privacidad
|
||||
</h1>
|
||||
<p className="text-xs sm:text-sm text-slate-600 dark:text-slate-400 max-w-2xl mx-auto">
|
||||
Última actualización: <strong className="text-slate-900 dark:text-slate-200">26 de Agosto de 2026</strong>. Por favor, lee atentamente estos términos antes de utilizar los servicios de RyCourses.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Terms Content Cards */}
|
||||
<div className="space-y-6">
|
||||
|
||||
{/* Section 1: Aceptación */}
|
||||
<Card variant="elevated" padding="lg" className="space-y-4">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-10 h-10 rounded-xl bg-cyan-500/10 text-cyan-600 dark:text-cyan-400 flex items-center justify-center text-xl shrink-0">
|
||||
<FiFileText />
|
||||
</div>
|
||||
<h2 className="text-lg font-bold text-slate-900 dark:text-white">1. Aceptación de los Términos</h2>
|
||||
</div>
|
||||
<p className="text-xs sm:text-sm text-slate-600 dark:text-slate-300 leading-relaxed pl-13">
|
||||
Al acceder, registrarte o utilizar la plataforma de educación en línea <strong>RyCourses</strong>, aceptas quedar vinculado legalmente por los presentes Términos de Servicio y por nuestra Política de Privacidad. Si no estás de acuerdo con alguno de estos términos, no debes utilizar nuestros servicios.
|
||||
</p>
|
||||
</Card>
|
||||
|
||||
{/* Section 2: Cuentas y Seguridad */}
|
||||
<Card variant="elevated" padding="lg" className="space-y-4">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-10 h-10 rounded-xl bg-violet-500/10 text-violet-600 dark:text-violet-400 flex items-center justify-center text-xl shrink-0">
|
||||
<FiUserCheck />
|
||||
</div>
|
||||
<h2 className="text-lg font-bold text-slate-900 dark:text-white">2. Cuentas de Usuario y Seguridad</h2>
|
||||
</div>
|
||||
<div className="text-xs sm:text-sm text-slate-600 dark:text-slate-300 leading-relaxed pl-13 space-y-2">
|
||||
<p>
|
||||
Para acceder a la mayoría de las funciones de la plataforma (comprar cursos o publicar contenido como instructor), debes crear una cuenta personal.
|
||||
</p>
|
||||
<ul className="list-disc pl-5 space-y-1 text-slate-500 dark:text-slate-400">
|
||||
<li>Debes proporcionar información verdadera, completa y actualizada durante el registro.</li>
|
||||
<li>Eres responsable de mantener la confidencialidad de tus credenciales de acceso.</li>
|
||||
<li>No está permitido compartir accesos de cuenta entre múltiples usuarios.</li>
|
||||
</ul>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* Section 3: Pagos y Reembolsos */}
|
||||
<Card variant="elevated" padding="lg" className="space-y-4">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-10 h-10 rounded-xl bg-emerald-500/10 text-emerald-600 dark:text-emerald-400 flex items-center justify-center text-xl shrink-0">
|
||||
<FiCreditCard />
|
||||
</div>
|
||||
<h2 className="text-lg font-bold text-slate-900 dark:text-white">3. Pagos, Precios y Garantía de Reembolso</h2>
|
||||
</div>
|
||||
<div className="text-xs sm:text-sm text-slate-600 dark:text-slate-300 leading-relaxed pl-13 space-y-2">
|
||||
<p>
|
||||
Todos los pagos procesados en RyCourses se realizan mediante pasarelas de pago cifradas de grado bancario (Stripe y PayPal).
|
||||
</p>
|
||||
<div className="p-4 rounded-xl bg-emerald-500/10 border border-emerald-500/20 text-emerald-700 dark:text-emerald-300 text-xs font-semibold flex items-center gap-2">
|
||||
<FiCheckCircle className="text-base shrink-0" />
|
||||
Garantía de reembolso de 30 días sin preguntas si no estás satisfecho con tu curso.
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* Section 4: Propiedad Intelectual */}
|
||||
<Card variant="elevated" padding="lg" className="space-y-4">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-10 h-10 rounded-xl bg-amber-500/10 text-amber-600 dark:text-amber-400 flex items-center justify-center text-xl shrink-0">
|
||||
<FiShield />
|
||||
</div>
|
||||
<h2 className="text-lg font-bold text-slate-900 dark:text-white">4. Propiedad Intelectual y Derechos de Autor</h2>
|
||||
</div>
|
||||
<p className="text-xs sm:text-sm text-slate-600 dark:text-slate-300 leading-relaxed pl-13">
|
||||
Todo el contenido audiovisual, código fuente, logotipos y material didáctico publicado en RyCourses pertenece a sus respectivos autores e instructores. La compra de un curso concede una licencia de acceso personal, no exclusiva e intransferible. Queda estrictamente prohibida la reproducción, redistribución o reventa no autorizada del contenido.
|
||||
</p>
|
||||
</Card>
|
||||
|
||||
{/* Section 5: Privacidad de Datos */}
|
||||
<Card variant="elevated" padding="lg" className="space-y-4">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-10 h-10 rounded-xl bg-sky-500/10 text-sky-600 dark:text-sky-400 flex items-center justify-center text-xl shrink-0">
|
||||
<FiLock />
|
||||
</div>
|
||||
<h2 className="text-lg font-bold text-slate-900 dark:text-white">5. Política de Privacidad y Protección de Datos (GDPR)</h2>
|
||||
</div>
|
||||
<p className="text-xs sm:text-sm text-slate-600 dark:text-slate-300 leading-relaxed pl-13">
|
||||
En RyCourses nos tomamos muy en serio tu privacidad. Tus datos personales son almacenados con encriptación de 256 bits y nunca serán vendidos ni comercializados con terceros. Utilizaremos tu correo electrónico únicamente para notificaciones de cuenta, avances de tus cursos y actualizaciones importantes de la plataforma.
|
||||
</p>
|
||||
</Card>
|
||||
|
||||
</div>
|
||||
|
||||
{/* Footer Contact CTA */}
|
||||
<div className="text-center pt-6 space-y-3">
|
||||
<p className="text-xs text-slate-500 dark:text-slate-400">
|
||||
¿Tienes alguna duda sobre nuestros Términos de Servicio o Privacidad?
|
||||
</p>
|
||||
<a href="mailto:[email protected]">
|
||||
<Button variant="outline" size="sm" leftIcon={<FiHelpCircle />}>
|
||||
Contactar al equipo legal (soporte@rycourses.com)
|
||||
</Button>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
);
|
||||
};
|
||||
Reference in new issue
Block a user