26 files changed
+3730
-1030
No files matched your search
+51
-13
@@ -7,11 +7,13 @@ import { Footer } from './components/layout/Footer';
|
|||||||
import { HomeView } from './views/HomeView';
|
import { HomeView } from './views/HomeView';
|
||||||
import { BuyerView } from './views/BuyerView';
|
import { BuyerView } from './views/BuyerView';
|
||||||
import { SellerView } from './views/SellerView';
|
import { SellerView } from './views/SellerView';
|
||||||
|
import { TeachersView } from './views/TeachersView';
|
||||||
import { CourseDetailView } from './views/CourseDetailView';
|
import { CourseDetailView } from './views/CourseDetailView';
|
||||||
import { CreateCourseView } from './views/CreateCourseView';
|
import { CreateCourseView } from './views/CreateCourseView';
|
||||||
import { AdminDashboardView } from './views/AdminDashboardView';
|
import { AdminDashboardView } from './views/AdminDashboardView';
|
||||||
import { LoginView } from './views/LoginView';
|
import { LoginView } from './views/LoginView';
|
||||||
import { RegisterView } from './views/RegisterView';
|
import { RegisterView } from './views/RegisterView';
|
||||||
|
import { TermsView } from './views/TermsView';
|
||||||
import { CourseModal } from './components/buyer/CourseModal';
|
import { CourseModal } from './components/buyer/CourseModal';
|
||||||
import { CartDrawer } from './components/buyer/CartDrawer';
|
import { CartDrawer } from './components/buyer/CartDrawer';
|
||||||
import { CreateCourseModal } from './components/seller/CreateCourseModal';
|
import { CreateCourseModal } from './components/seller/CreateCourseModal';
|
||||||
@@ -19,7 +21,9 @@ import { FiCheckCircle } from 'react-icons/fi';
|
|||||||
|
|
||||||
export function App() {
|
export function App() {
|
||||||
const location = useLocation();
|
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 [courses, setCourses] = useState<Course[]>(mockCourses);
|
||||||
const [cartItems, setCartItems] = useState<CartItem[]>([]);
|
const [cartItems, setCartItems] = useState<CartItem[]>([]);
|
||||||
@@ -27,6 +31,7 @@ export function App() {
|
|||||||
|
|
||||||
// Modals & Drawers state
|
// Modals & Drawers state
|
||||||
const [selectedCourseForModal, setSelectedCourseForModal] = useState<Course | null>(null);
|
const [selectedCourseForModal, setSelectedCourseForModal] = useState<Course | null>(null);
|
||||||
|
const [isTeacherPreviewModal, setIsTeacherPreviewModal] = useState(false);
|
||||||
const [isCartOpen, setIsCartOpen] = useState(false);
|
const [isCartOpen, setIsCartOpen] = useState(false);
|
||||||
const [isCreateModalOpen, setIsCreateModalOpen] = useState(false);
|
const [isCreateModalOpen, setIsCreateModalOpen] = useState(false);
|
||||||
|
|
||||||
@@ -95,18 +100,18 @@ export function App() {
|
|||||||
const cartCourseIds = cartItems.map((i) => i.course.id);
|
const cartCourseIds = cartItems.map((i) => i.course.id);
|
||||||
|
|
||||||
return (
|
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 */}
|
{/* Toast notification banner */}
|
||||||
{toastMessage && (
|
{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">
|
<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-400 text-lg shrink-0" />
|
<FiCheckCircle className="text-cyan-500 text-lg shrink-0" />
|
||||||
<span className="text-xs font-semibold">{toastMessage}</span>
|
<span className="text-xs font-semibold">{toastMessage}</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Main Navigation Bar (hidden on Auth pages) */}
|
{/* Main Navigation Bar (hidden on Auth & Dashboard pages) */}
|
||||||
{!isAuthPage && (
|
{!hideNavbarFooter && (
|
||||||
<Navbar
|
<Navbar
|
||||||
currentUser={currentUser}
|
currentUser={currentUser}
|
||||||
onLogout={handleLogout}
|
onLogout={handleLogout}
|
||||||
@@ -132,8 +137,12 @@ export function App() {
|
|||||||
onOpenPreview={(courseId) => {
|
onOpenPreview={(courseId) => {
|
||||||
const target = courses.find((c) => c.id === courseId) || courses[0];
|
const target = courses.find((c) => c.id === courseId) || courses[0];
|
||||||
setSelectedCourseForModal(target);
|
setSelectedCourseForModal(target);
|
||||||
|
setIsTeacherPreviewModal(false);
|
||||||
|
}}
|
||||||
|
onOpenDetails={(course) => {
|
||||||
|
setSelectedCourseForModal(course);
|
||||||
|
setIsTeacherPreviewModal(false);
|
||||||
}}
|
}}
|
||||||
onOpenDetails={(course) => setSelectedCourseForModal(course)}
|
|
||||||
onClearSearch={() => setSearchQuery('')}
|
onClearSearch={() => setSearchQuery('')}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
@@ -146,7 +155,10 @@ export function App() {
|
|||||||
cartCourseIds={cartCourseIds}
|
cartCourseIds={cartCourseIds}
|
||||||
searchQuery={searchQuery}
|
searchQuery={searchQuery}
|
||||||
onAddToCart={handleAddToCart}
|
onAddToCart={handleAddToCart}
|
||||||
onOpenDetails={(course) => setSelectedCourseForModal(course)}
|
onOpenDetails={(course) => {
|
||||||
|
setSelectedCourseForModal(course);
|
||||||
|
setIsTeacherPreviewModal(false);
|
||||||
|
}}
|
||||||
onClearSearch={() => setSearchQuery('')}
|
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
|
<Route
|
||||||
path="/seller"
|
path="/seller"
|
||||||
element={
|
element={
|
||||||
<SellerView
|
<SellerView
|
||||||
|
currentUser={currentUser}
|
||||||
courses={courses}
|
courses={courses}
|
||||||
onOpenCreateCourse={() => setIsCreateModalOpen(true)}
|
onPreviewCourse={(course) => {
|
||||||
onPreviewCourse={(course) => setSelectedCourseForModal(course)}
|
setSelectedCourseForModal(course);
|
||||||
|
setIsTeacherPreviewModal(true);
|
||||||
|
}}
|
||||||
onDeleteCourse={handleDeleteCourse}
|
onDeleteCourse={handleDeleteCourse}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
@@ -192,6 +222,10 @@ export function App() {
|
|||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
<Route
|
||||||
|
path="/terms"
|
||||||
|
element={<TermsView />}
|
||||||
|
/>
|
||||||
<Route
|
<Route
|
||||||
path="/login"
|
path="/login"
|
||||||
element={<LoginView onShowToast={showToast} onLogin={handleLogin} />}
|
element={<LoginView onShowToast={showToast} onLogin={handleLogin} />}
|
||||||
@@ -208,7 +242,11 @@ export function App() {
|
|||||||
course={selectedCourseForModal}
|
course={selectedCourseForModal}
|
||||||
isOpen={!!selectedCourseForModal}
|
isOpen={!!selectedCourseForModal}
|
||||||
isInCart={selectedCourseForModal ? cartCourseIds.includes(selectedCourseForModal.id) : false}
|
isInCart={selectedCourseForModal ? cartCourseIds.includes(selectedCourseForModal.id) : false}
|
||||||
onClose={() => setSelectedCourseForModal(null)}
|
isTeacherPreview={isTeacherPreviewModal}
|
||||||
|
onClose={() => {
|
||||||
|
setSelectedCourseForModal(null);
|
||||||
|
setIsTeacherPreviewModal(false);
|
||||||
|
}}
|
||||||
onAddToCart={handleAddToCart}
|
onAddToCart={handleAddToCart}
|
||||||
onBuyNow={handleBuyNow}
|
onBuyNow={handleBuyNow}
|
||||||
/>
|
/>
|
||||||
@@ -229,8 +267,8 @@ export function App() {
|
|||||||
onCreateCourse={handleCreateCourse}
|
onCreateCourse={handleCreateCourse}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Global Footer (hidden on Auth pages) */}
|
{/* Global Footer (hidden on Auth & Dashboard pages) */}
|
||||||
{!isAuthPage && <Footer />}
|
{!hideNavbarFooter && <Footer />}
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -11,7 +11,8 @@ import {
|
|||||||
FiShield,
|
FiShield,
|
||||||
FiChevronDown,
|
FiChevronDown,
|
||||||
FiChevronUp,
|
FiChevronUp,
|
||||||
FiShoppingCart
|
FiShoppingCart,
|
||||||
|
FiLock
|
||||||
} from 'react-icons/fi';
|
} from 'react-icons/fi';
|
||||||
import type { Course } from '../../types/course';
|
import type { Course } from '../../types/course';
|
||||||
|
|
||||||
@@ -19,6 +20,7 @@ interface CourseModalProps {
|
|||||||
course: Course | null;
|
course: Course | null;
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
isInCart: boolean;
|
isInCart: boolean;
|
||||||
|
isTeacherPreview?: boolean;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
onAddToCart: (course: Course) => void;
|
onAddToCart: (course: Course) => void;
|
||||||
onBuyNow: (course: Course) => void;
|
onBuyNow: (course: Course) => void;
|
||||||
@@ -28,6 +30,7 @@ export const CourseModal: React.FC<CourseModalProps> = ({
|
|||||||
course,
|
course,
|
||||||
isOpen,
|
isOpen,
|
||||||
isInCart,
|
isInCart,
|
||||||
|
isTeacherPreview = false,
|
||||||
onClose,
|
onClose,
|
||||||
onAddToCart,
|
onAddToCart,
|
||||||
onBuyNow,
|
onBuyNow,
|
||||||
@@ -53,24 +56,29 @@ export const CourseModal: React.FC<CourseModalProps> = ({
|
|||||||
return (
|
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">
|
<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
|
<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()}
|
onClick={(e) => e.stopPropagation()}
|
||||||
>
|
>
|
||||||
|
|
||||||
{/* Top Header Bar */}
|
{/* 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">
|
<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}
|
{course.category}
|
||||||
</span>
|
</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>
|
</div>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
onClick={onClose}
|
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"
|
aria-label="Cerrar modal"
|
||||||
>
|
>
|
||||||
<FiX className="text-xl" />
|
<FiX className="text-xl" />
|
||||||
@@ -85,20 +93,20 @@ export const CourseModal: React.FC<CourseModalProps> = ({
|
|||||||
|
|
||||||
{/* Main Info */}
|
{/* Main Info */}
|
||||||
<div className="lg:col-span-7 space-y-4">
|
<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}
|
{course.title}
|
||||||
</h2>
|
</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}
|
{course.subtitle}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
{/* Meta stats */}
|
{/* 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 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-400 font-bold">
|
<div className="flex items-center gap-1.5 text-amber-500 dark:text-amber-400 font-bold">
|
||||||
<FiStar className="fill-amber-400" />
|
<FiStar className="fill-amber-400" />
|
||||||
<span>{course.rating.toFixed(1)}</span>
|
<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>
|
||||||
<div>👥 {course.studentsCount.toLocaleString()} estudiantes</div>
|
<div>👥 {course.studentsCount.toLocaleString()} estudiantes</div>
|
||||||
<div className="flex items-center gap-1"><FiGlobe /> {course.language}</div>
|
<div className="flex items-center gap-1"><FiGlobe /> {course.language}</div>
|
||||||
@@ -106,14 +114,14 @@ export const CourseModal: React.FC<CourseModalProps> = ({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* What You Will Learn Card */}
|
{/* What You Will Learn Card */}
|
||||||
<div className="p-5 rounded-xl bg-slate-950/60 border border-slate-800 space-y-3">
|
<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-white flex items-center gap-2">
|
<h4 className="text-sm font-bold text-slate-900 dark:text-white flex items-center gap-2">
|
||||||
<FiAward className="text-cyan-400" /> Lo que aprenderás en este curso
|
<FiAward className="text-cyan-500 dark:text-cyan-400" /> Lo que aprenderás en este curso
|
||||||
</h4>
|
</h4>
|
||||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-2.5 pt-2">
|
<div className="grid grid-cols-1 sm:grid-cols-2 gap-2.5 pt-2">
|
||||||
{course.whatYouWillLearn.map((item, idx) => (
|
{course.whatYouWillLearn.map((item, idx) => (
|
||||||
<div key={idx} className="flex items-start gap-2 text-xs text-slate-300">
|
<div key={idx} className="flex items-start gap-2 text-xs text-slate-700 dark:text-slate-300">
|
||||||
<FiCheckCircle className="text-emerald-400 text-sm mt-0.5 shrink-0" />
|
<FiCheckCircle className="text-emerald-500 dark:text-emerald-400 text-sm mt-0.5 shrink-0" />
|
||||||
<span>{item}</span>
|
<span>{item}</span>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
@@ -163,25 +171,44 @@ export const CourseModal: React.FC<CourseModalProps> = ({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Price & Checkout Card */}
|
{/* 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 justify-between">
|
||||||
<div className="flex items-baseline gap-2">
|
<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)}
|
${course.price.toFixed(2)}
|
||||||
</span>
|
</span>
|
||||||
{course.originalPrice > course.price && (
|
{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)}
|
${course.originalPrice.toFixed(2)}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
{discountPercent > 0 && (
|
{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
|
{discountPercent}% Descuento
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* 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
|
||||||
|
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">
|
<div className="space-y-2">
|
||||||
<button
|
<button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
@@ -198,27 +225,28 @@ export const CourseModal: React.FC<CourseModalProps> = ({
|
|||||||
disabled={isInCart}
|
disabled={isInCart}
|
||||||
className={`w-full py-2.5 rounded-xl text-sm font-bold flex items-center justify-center gap-2 transition-all ${
|
className={`w-full py-2.5 rounded-xl text-sm font-bold flex items-center justify-center gap-2 transition-all ${
|
||||||
isInCart
|
isInCart
|
||||||
? 'bg-slate-800 text-slate-400 border border-slate-700 cursor-default'
|
? '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-800 hover:bg-slate-700 text-white border border-slate-700'
|
: '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 />
|
<FiShoppingCart />
|
||||||
<span>{isInCart ? 'Ya está en el carrito' : 'Añadir al Carrito'}</span>
|
<span>{isInCart ? 'Ya está en el carrito' : 'Añadir al Carrito'}</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
<div className="space-y-1.5 text-[11px] text-slate-400 pt-2 border-t border-slate-800">
|
<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">
|
<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>
|
||||||
<div className="flex items-center gap-2">
|
<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>
|
||||||
<div className="flex items-center gap-2">
|
<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>
|
||||||
<div className="flex items-center gap-2">
|
<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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -227,13 +255,13 @@ export const CourseModal: React.FC<CourseModalProps> = ({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Nav Tabs inside modal */}
|
{/* 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
|
<button
|
||||||
onClick={() => setActiveTab('curriculum')}
|
onClick={() => setActiveTab('curriculum')}
|
||||||
className={`pb-3 transition relative ${
|
className={`pb-3 transition relative ${
|
||||||
activeTab === 'curriculum'
|
activeTab === 'curriculum'
|
||||||
? 'text-cyan-400 border-b-2 border-cyan-400'
|
? 'text-cyan-600 dark:text-cyan-400 border-b-2 border-cyan-500'
|
||||||
: 'text-slate-400 hover:text-slate-200'
|
: 'text-slate-500 dark:text-slate-400 hover:text-slate-900 dark:hover:text-slate-200'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
Contenido del Curso ({course.modules.length} Módulos)
|
Contenido del Curso ({course.modules.length} Módulos)
|
||||||
@@ -242,8 +270,8 @@ export const CourseModal: React.FC<CourseModalProps> = ({
|
|||||||
onClick={() => setActiveTab('instructor')}
|
onClick={() => setActiveTab('instructor')}
|
||||||
className={`pb-3 transition relative ${
|
className={`pb-3 transition relative ${
|
||||||
activeTab === 'instructor'
|
activeTab === 'instructor'
|
||||||
? 'text-cyan-400 border-b-2 border-cyan-400'
|
? 'text-cyan-600 dark:text-cyan-400 border-b-2 border-cyan-500'
|
||||||
: 'text-slate-400 hover:text-slate-200'
|
: 'text-slate-500 dark:text-slate-400 hover:text-slate-900 dark:hover:text-slate-200'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
Instructor
|
Instructor
|
||||||
@@ -252,8 +280,8 @@ export const CourseModal: React.FC<CourseModalProps> = ({
|
|||||||
onClick={() => setActiveTab('reviews')}
|
onClick={() => setActiveTab('reviews')}
|
||||||
className={`pb-3 transition relative ${
|
className={`pb-3 transition relative ${
|
||||||
activeTab === 'reviews'
|
activeTab === 'reviews'
|
||||||
? 'text-cyan-400 border-b-2 border-cyan-400'
|
? 'text-cyan-600 dark:text-cyan-400 border-b-2 border-cyan-500'
|
||||||
: 'text-slate-400 hover:text-slate-200'
|
: 'text-slate-500 dark:text-slate-400 hover:text-slate-900 dark:hover:text-slate-200'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
Valoraciones ({course.reviews.length})
|
Valoraciones ({course.reviews.length})
|
||||||
@@ -263,7 +291,7 @@ export const CourseModal: React.FC<CourseModalProps> = ({
|
|||||||
{/* Tab Content 1: Curriculum */}
|
{/* Tab Content 1: Curriculum */}
|
||||||
{activeTab === 'curriculum' && (
|
{activeTab === 'curriculum' && (
|
||||||
<div className="space-y-4">
|
<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>
|
<span>{course.modules.length} módulos • {course.lessonsCount} lecciones • {course.duration} duración total</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -271,29 +299,29 @@ export const CourseModal: React.FC<CourseModalProps> = ({
|
|||||||
{course.modules.map((mod) => {
|
{course.modules.map((mod) => {
|
||||||
const isExpanded = !!expandedModules[mod.id];
|
const isExpanded = !!expandedModules[mod.id];
|
||||||
return (
|
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
|
<button
|
||||||
onClick={() => toggleModule(mod.id)}
|
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">
|
<div className="font-bold text-sm text-slate-800 dark:text-slate-200 flex items-center gap-2">
|
||||||
{isExpanded ? <FiChevronUp className="text-cyan-400" /> : <FiChevronDown className="text-slate-500" />}
|
{isExpanded ? <FiChevronUp className="text-cyan-500 dark:text-cyan-400" /> : <FiChevronDown className="text-slate-400 dark:text-slate-500" />}
|
||||||
<span>{mod.title}</span>
|
<span>{mod.title}</span>
|
||||||
</div>
|
</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
|
{mod.lessons.length} lecciones
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
{isExpanded && (
|
{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) => (
|
{mod.lessons.map((lesson) => (
|
||||||
<div
|
<div
|
||||||
key={lesson.id}
|
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="flex items-center gap-2.5 text-slate-700 dark: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="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 />
|
<FiPlay />
|
||||||
</div>
|
</div>
|
||||||
<span>{lesson.title}</span>
|
<span>{lesson.title}</span>
|
||||||
@@ -301,11 +329,11 @@ export const CourseModal: React.FC<CourseModalProps> = ({
|
|||||||
|
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
{lesson.isFreePreview && (
|
{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
|
Vista Previa
|
||||||
</span>
|
</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>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
@@ -320,7 +348,7 @@ export const CourseModal: React.FC<CourseModalProps> = ({
|
|||||||
|
|
||||||
{/* Tab Content 2: Instructor */}
|
{/* Tab Content 2: Instructor */}
|
||||||
{activeTab === '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">
|
<div className="flex flex-col sm:flex-row items-start sm:items-center gap-4">
|
||||||
<img
|
<img
|
||||||
src={course.instructor.avatar}
|
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"
|
className="w-16 h-16 rounded-full object-cover ring-2 ring-cyan-500/50"
|
||||||
/>
|
/>
|
||||||
<div>
|
<div>
|
||||||
<h3 className="text-lg font-bold text-white">{course.instructor.name}</h3>
|
<h3 className="text-lg font-bold text-slate-900 dark:text-white">{course.instructor.name}</h3>
|
||||||
<p className="text-xs text-cyan-400 font-medium">{course.instructor.role}</p>
|
<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">
|
<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-400 font-bold">
|
<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
|
<FiStar className="fill-amber-400" /> {course.instructor.rating} Calificación
|
||||||
</span>
|
</span>
|
||||||
<span>👨🎓 {course.instructor.totalStudents.toLocaleString()} Estudiantes</span>
|
<span>👨🎓 {course.instructor.totalStudents.toLocaleString()} Estudiantes</span>
|
||||||
@@ -341,7 +369,7 @@ export const CourseModal: React.FC<CourseModalProps> = ({
|
|||||||
</div>
|
</div>
|
||||||
</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}
|
{course.instructor.bio}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -351,7 +379,7 @@ export const CourseModal: React.FC<CourseModalProps> = ({
|
|||||||
{activeTab === 'reviews' && (
|
{activeTab === 'reviews' && (
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
{course.reviews.map((rev) => (
|
{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 justify-between">
|
||||||
<div className="flex items-center gap-2.5">
|
<div className="flex items-center gap-2.5">
|
||||||
<img
|
<img
|
||||||
@@ -360,7 +388,7 @@ export const CourseModal: React.FC<CourseModalProps> = ({
|
|||||||
className="w-8 h-8 rounded-full object-cover"
|
className="w-8 h-8 rounded-full object-cover"
|
||||||
/>
|
/>
|
||||||
<div>
|
<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>
|
<span className="text-[10px] text-slate-500">{rev.date}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -372,7 +400,7 @@ export const CourseModal: React.FC<CourseModalProps> = ({
|
|||||||
</div>
|
</div>
|
||||||
</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}"
|
"{rev.comment}"
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ export const BrandLogo: React.FC<BrandLogoProps> = ({ size = 'md', showSubtitle
|
|||||||
return (
|
return (
|
||||||
<div className="flex items-center gap-3 group select-none cursor-pointer">
|
<div className="flex items-center gap-3 group select-none cursor-pointer">
|
||||||
{/* Official PNG Logo Image with sleek container styling */}
|
{/* 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
|
<img
|
||||||
src="/logo.png"
|
src="/logo.png"
|
||||||
alt="RyCourses Emblem"
|
alt="RyCourses Emblem"
|
||||||
@@ -32,12 +32,12 @@ export const BrandLogo: React.FC<BrandLogoProps> = ({ size = 'md', showSubtitle
|
|||||||
{/* Typography */}
|
{/* Typography */}
|
||||||
<div>
|
<div>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<span className={`${textSizes[size]} font-extrabold tracking-tight text-white group-hover:text-cyan-300 transition-colors`}>
|
<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-400 to-blue-400">Courses</span>
|
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>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
{showSubtitle && (
|
{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>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import { Link } from 'react-router';
|
||||||
import {
|
import {
|
||||||
FiGlobe,
|
FiGlobe,
|
||||||
FiShield,
|
FiShield,
|
||||||
@@ -74,8 +75,8 @@ export const Footer: React.FC = () => {
|
|||||||
<ul className="space-y-2.5 text-xs">
|
<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">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">RyCourses para Empresas</a></li>
|
||||||
<li><a href="#" className="hover:text-cyan-300 transition">Términos de Servicio</a></li>
|
<li><Link to="/terms" className="hover:text-cyan-300 transition">Términos de Servicio</Link></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">Política de Privacidad</Link></li>
|
||||||
<li><a href="#" className="hover:text-cyan-300 transition">Centro de Ayuda</a></li>
|
<li><a href="#" className="hover:text-cyan-300 transition">Centro de Ayuda</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,19 +1,16 @@
|
|||||||
|
|
||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import { useNavigate, useLocation } from 'react-router';
|
import { useNavigate, useLocation } from 'react-router';
|
||||||
import {
|
import {
|
||||||
FiSearch,
|
|
||||||
FiShoppingCart,
|
FiShoppingCart,
|
||||||
FiMenu,
|
FiMenu,
|
||||||
FiX,
|
FiX,
|
||||||
FiPlusCircle,
|
|
||||||
FiHome,
|
FiHome,
|
||||||
FiGrid,
|
FiGrid,
|
||||||
FiUser,
|
FiUser
|
||||||
FiShield,
|
|
||||||
} from 'react-icons/fi';
|
} from 'react-icons/fi';
|
||||||
import { FaChalkboardTeacher, FaSignOutAlt } from 'react-icons/fa';
|
import { FaChalkboardTeacher, FaSignOutAlt } from 'react-icons/fa';
|
||||||
import { BrandLogo } from '../common/BrandLogo';
|
import { BrandLogo } from '../common/BrandLogo';
|
||||||
|
import { ThemeToggle, Button } from '../ui';
|
||||||
import type { User } from '../../types/course';
|
import type { User } from '../../types/course';
|
||||||
|
|
||||||
interface NavbarProps {
|
interface NavbarProps {
|
||||||
@@ -31,15 +28,13 @@ export const Navbar: React.FC<NavbarProps> = ({
|
|||||||
onLogout,
|
onLogout,
|
||||||
cartCount,
|
cartCount,
|
||||||
onOpenCart,
|
onOpenCart,
|
||||||
searchQuery,
|
|
||||||
onSearchChange,
|
|
||||||
}) => {
|
}) => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const [mobileMenuOpen, setMobileMenuOpen] = useState(false);
|
const [mobileMenuOpen, setMobileMenuOpen] = useState(false);
|
||||||
const [cartHovered, setCartHovered] = useState(false);
|
const [cartHovered, setCartHovered] = useState(false);
|
||||||
|
|
||||||
// Inject keyframe animation styles inline (no external CSS needed)
|
// Inject keyframe animation styles inline
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const styleId = 'cart-run-keyframes';
|
const styleId = 'cart-run-keyframes';
|
||||||
if (!document.getElementById(styleId)) {
|
if (!document.getElementById(styleId)) {
|
||||||
@@ -64,17 +59,9 @@ export const Navbar: React.FC<NavbarProps> = ({
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const currentPath = location.pathname;
|
const currentPath = location.pathname;
|
||||||
const isSellerMode = currentPath === '/seller' || currentPath.startsWith('/seller/');
|
|
||||||
|
|
||||||
const handleSearchInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
||||||
onSearchChange(e.target.value);
|
|
||||||
if (currentPath !== '/courses' && e.target.value.trim() !== '') {
|
|
||||||
navigate('/courses');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<header className="sticky top-0 z-40 w-full border-b border-slate-800/80 bg-slate-950/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="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||||
<div className="flex items-center justify-between h-20 gap-4">
|
<div className="flex items-center justify-between h-20 gap-4">
|
||||||
|
|
||||||
@@ -84,13 +71,14 @@ export const Navbar: React.FC<NavbarProps> = ({
|
|||||||
<BrandLogo size="md" />
|
<BrandLogo size="md" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Quick Nav Links */}
|
{/* Main Nav Links */}
|
||||||
<nav className="hidden lg:flex items-center gap-1">
|
<nav className="hidden lg:flex items-center gap-1">
|
||||||
<button
|
<button
|
||||||
onClick={() => navigate('/')}
|
onClick={() => navigate('/')}
|
||||||
className={`flex items-center gap-1.5 px-3 py-2 rounded-xl text-xs font-semibold transition-all ${currentPath === '/'
|
className={`flex items-center gap-1.5 px-3.5 py-2 rounded-xl text-xs font-semibold transition-all ${
|
||||||
? 'bg-slate-800 text-cyan-400 border border-cyan-500/30'
|
currentPath === '/'
|
||||||
: 'text-slate-300 hover:text-white hover:bg-slate-900'
|
? '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 />
|
<FiHome />
|
||||||
@@ -99,91 +87,62 @@ export const Navbar: React.FC<NavbarProps> = ({
|
|||||||
|
|
||||||
<button
|
<button
|
||||||
onClick={() => navigate('/courses')}
|
onClick={() => navigate('/courses')}
|
||||||
className={`flex items-center gap-1.5 px-3 py-2 rounded-xl text-xs font-semibold transition-all ${currentPath === '/courses'
|
className={`flex items-center gap-1.5 px-3.5 py-2 rounded-xl text-xs font-semibold transition-all ${
|
||||||
? 'bg-slate-800 text-cyan-400 border border-cyan-500/30'
|
currentPath === '/courses'
|
||||||
: 'text-slate-300 hover:text-white hover:bg-slate-900'
|
? '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 />
|
<FiGrid />
|
||||||
<span>Catálogo</span>
|
<span>Catálogo</span>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
|
{/* Landing link for instructor sales */}
|
||||||
<button
|
<button
|
||||||
onClick={() => navigate('/admin')}
|
onClick={() => navigate('/seller')}
|
||||||
className={`flex items-center gap-1.5 px-3 py-2 rounded-xl text-xs font-semibold transition-all ${currentPath === '/admin'
|
className={`flex items-center gap-1.5 px-3.5 py-2 rounded-xl text-xs font-semibold transition-all ${
|
||||||
? 'bg-slate-800 text-cyan-400 border border-cyan-500/30'
|
currentPath === '/seller'
|
||||||
: 'text-slate-300 hover:text-white hover:bg-slate-900'
|
? '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 />
|
<FaChalkboardTeacher />
|
||||||
<span>Admin</span>
|
<span>Vender Cursos</span>
|
||||||
</button>
|
</button>
|
||||||
</nav>
|
</nav>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 3. Right Actions & Role Switcher */}
|
{/* 2. Right Actions & ThemeToggle Beside Cart */}
|
||||||
<div className="flex items-center gap-2.5 shrink-0">
|
<div className="flex items-center gap-3 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>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Login & Register Buttons OR User Profile */}
|
{/* Login & Register Buttons OR User Profile */}
|
||||||
{!currentUser ? (
|
{!currentUser ? (
|
||||||
<div className="hidden xl:flex items-center gap-2 pl-2 border-l border-slate-800">
|
<div className="flex items-center gap-2">
|
||||||
<button
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
onClick={() => navigate('/login')}
|
onClick={() => navigate('/login')}
|
||||||
className="px-3 py-1.5 text-xs font-semibold text-slate-300 hover:text-white transition"
|
|
||||||
>
|
>
|
||||||
Iniciar Sesión
|
Iniciar Sesión
|
||||||
</button>
|
</Button>
|
||||||
<button
|
<Button
|
||||||
|
variant="gradient"
|
||||||
|
size="sm"
|
||||||
onClick={() => navigate('/register')}
|
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
|
Registrarse
|
||||||
</button>
|
</Button>
|
||||||
</div>
|
</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="relative cursor-pointer flex items-center gap-2" title="Mi Cuenta">
|
||||||
<div className="hidden sm:block text-right">
|
<div className="hidden sm:block text-right">
|
||||||
<p className="text-[11px] font-bold text-slate-200 leading-none">{currentUser.name}</p>
|
<p className="text-[11px] font-bold leading-none text-slate-800 dark:text-slate-200">
|
||||||
<p className="text-[9px] text-cyan-400 font-medium capitalize mt-0.5">{currentUser.role === 'buyer' ? 'Estudiante' : 'Instructor'}</p>
|
{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>
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
{currentUser.avatar ? (
|
{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"
|
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()}
|
{currentUser.name.charAt(0).toUpperCase()}
|
||||||
</div>
|
</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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Dropdown Menu */}
|
{/* 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">
|
<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 />
|
<FaSignOutAlt />
|
||||||
<span>Cerrar Sesión</span>
|
<span>Cerrar Sesión</span>
|
||||||
</button>
|
</button>
|
||||||
@@ -213,16 +172,17 @@ export const Navbar: React.FC<NavbarProps> = ({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Cart button (Buyer mode) - Fixed Far Right, inline animation */}
|
{/* ThemeToggle and Cart Button Grouped Together */}
|
||||||
{!isSellerMode && (
|
<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
|
<button
|
||||||
onClick={onOpenCart}
|
onClick={onOpenCart}
|
||||||
onMouseEnter={() => setCartHovered(true)}
|
onMouseEnter={() => setCartHovered(true)}
|
||||||
onMouseLeave={() => setCartHovered(false)}
|
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"
|
aria-label="Carrito de compras"
|
||||||
>
|
>
|
||||||
{/* Cart icon with inline running animation */}
|
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
@@ -232,9 +192,9 @@ export const Navbar: React.FC<NavbarProps> = ({
|
|||||||
animation: cartHovered ? 'cart-run 0.45s ease-in-out infinite' : 'none',
|
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={{
|
<span style={{
|
||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
top: '25%',
|
top: '25%',
|
||||||
@@ -270,89 +230,61 @@ export const Navbar: React.FC<NavbarProps> = ({
|
|||||||
}} />
|
}} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Badge counter */}
|
{/* Badge counter: Perfectly placed floating badge */}
|
||||||
{cartCount > 0 && (
|
{cartCount > 0 && (
|
||||||
<span
|
<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">
|
||||||
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',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{cartCount}
|
{cartCount}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</button>
|
</button>
|
||||||
)}
|
</div>
|
||||||
|
|
||||||
{/* Mobile menu trigger */}
|
{/* Mobile menu trigger */}
|
||||||
<button
|
<button
|
||||||
onClick={() => setMobileMenuOpen(!mobileMenuOpen)}
|
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" />}
|
{mobileMenuOpen ? <FiX className="text-2xl" /> : <FiMenu className="text-2xl" />}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Mobile search & menu expanded */}
|
{/* Mobile menu expanded */}
|
||||||
{mobileMenuOpen && (
|
{mobileMenuOpen && (
|
||||||
<div className="md:hidden py-4 border-t border-slate-800 space-y-4">
|
<div className="md:hidden py-4 border-t space-y-4 border-slate-200 dark:border-slate-800">
|
||||||
<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="grid grid-cols-3 gap-2">
|
<div className="grid grid-cols-3 gap-2">
|
||||||
<button
|
<button
|
||||||
onClick={() => { navigate('/'); setMobileMenuOpen(false); }}
|
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
|
<FiHome /> Inicio
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={() => { navigate('/courses'); setMobileMenuOpen(false); }}
|
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
|
<FiGrid /> Catálogo
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={() => { navigate('/seller'); setMobileMenuOpen(false); }}
|
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
|
<FaChalkboardTeacher /> Vender
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</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 ? (
|
{!currentUser ? (
|
||||||
<>
|
<>
|
||||||
<button
|
<button
|
||||||
onClick={() => { navigate('/login'); setMobileMenuOpen(false); }}
|
onClick={() => { navigate('/login'); setMobileMenuOpen(false); }}
|
||||||
className="w-full py-2.5 bg-slate-800 text-white font-semibold rounded-lg text-xs flex items-center justify-center gap-2"
|
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
|
<FiUser /> Iniciar Sesión
|
||||||
</button>
|
</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 ? (
|
{currentUser.avatar ? (
|
||||||
<img src={currentUser.avatar} alt="Avatar" className="w-6 h-6 rounded-full object-cover" />
|
<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()}
|
{currentUser.name.charAt(0).toUpperCase()}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div className="text-left">
|
<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>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
onClick={() => { onLogout(); setMobileMenuOpen(false); }}
|
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
|
<FaSignOutAlt /> Cerrar Sesión
|
||||||
</button>
|
</button>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</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>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import React, { useState } from 'react';
|
import React, { useState, useRef } from 'react';
|
||||||
import {
|
import {
|
||||||
FiX,
|
FiX,
|
||||||
FiCheck,
|
FiCheck,
|
||||||
@@ -7,7 +7,12 @@ import {
|
|||||||
FiBookOpen,
|
FiBookOpen,
|
||||||
FiDollarSign,
|
FiDollarSign,
|
||||||
FiPlus,
|
FiPlus,
|
||||||
FiCheckCircle
|
FiCheckCircle,
|
||||||
|
FiUploadCloud,
|
||||||
|
FiVideo,
|
||||||
|
FiFilm,
|
||||||
|
FiLink,
|
||||||
|
FiImage
|
||||||
} from 'react-icons/fi';
|
} from 'react-icons/fi';
|
||||||
import type { Course, CourseLevel } from '../../types/course';
|
import type { Course, CourseLevel } from '../../types/course';
|
||||||
import { mockInstructors } from '../../data/mockCourses';
|
import { mockInstructors } from '../../data/mockCourses';
|
||||||
@@ -24,6 +29,8 @@ export const CreateCourseModal: React.FC<CreateCourseModalProps> = ({
|
|||||||
onCreateCourse,
|
onCreateCourse,
|
||||||
}) => {
|
}) => {
|
||||||
const [step, setStep] = useState<1 | 2 | 3>(1);
|
const [step, setStep] = useState<1 | 2 | 3>(1);
|
||||||
|
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||||
|
const imageInputRef = useRef<HTMLInputElement>(null);
|
||||||
|
|
||||||
// Form State
|
// Form State
|
||||||
const [title, setTitle] = useState('');
|
const [title, setTitle] = useState('');
|
||||||
@@ -32,10 +39,22 @@ export const CreateCourseModal: React.FC<CreateCourseModalProps> = ({
|
|||||||
const [level, setLevel] = useState<CourseLevel>('Todos los niveles');
|
const [level, setLevel] = useState<CourseLevel>('Todos los niveles');
|
||||||
const [price, setPrice] = useState('29.99');
|
const [price, setPrice] = useState('29.99');
|
||||||
const [originalPrice, setOriginalPrice] = useState('99.99');
|
const [originalPrice, setOriginalPrice] = useState('99.99');
|
||||||
|
|
||||||
|
// Thumbnail Image Drag & Drop State
|
||||||
|
const [thumbnailFile, setThumbnailFile] = useState<File | null>(null);
|
||||||
const [thumbnail, setThumbnail] = useState(
|
const [thumbnail, setThumbnail] = useState(
|
||||||
'https://images.unsplash.com/photo-1516321318423-f06f85e504b3?w=800&auto=format&fit=crop&q=80'
|
'https://images.unsplash.com/photo-1516321318423-f06f85e504b3?w=800&auto=format&fit=crop&q=80'
|
||||||
);
|
);
|
||||||
|
|
||||||
const [duration, setDuration] = useState('20h 30min');
|
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([
|
const [whatYouWillLearn, setWhatYouWillLearn] = useState([
|
||||||
'Aprenderás conceptos clave y buenas prácticas de la industria',
|
'Aprenderás conceptos clave y buenas prácticas de la industria',
|
||||||
'Construirás proyectos prácticos de portafolio para conseguir empleo'
|
'Construirás proyectos prácticos de portafolio para conseguir empleo'
|
||||||
@@ -44,6 +63,77 @@ export const CreateCourseModal: React.FC<CreateCourseModalProps> = ({
|
|||||||
|
|
||||||
if (!isOpen) return null;
|
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 = () => {
|
const handleAddOutcome = () => {
|
||||||
if (newOutcome.trim()) {
|
if (newOutcome.trim()) {
|
||||||
setWhatYouWillLearn([...whatYouWillLearn, newOutcome.trim()]);
|
setWhatYouWillLearn([...whatYouWillLearn, newOutcome.trim()]);
|
||||||
@@ -55,9 +145,7 @@ export const CreateCourseModal: React.FC<CreateCourseModalProps> = ({
|
|||||||
setWhatYouWillLearn(whatYouWillLearn.filter((_, i) => i !== index));
|
setWhatYouWillLearn(whatYouWillLearn.filter((_, i) => i !== index));
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSubmit = (e: React.FormEvent) => {
|
const handleFinalPublish = () => {
|
||||||
e.preventDefault();
|
|
||||||
|
|
||||||
const createdCourse: Course = {
|
const createdCourse: Course = {
|
||||||
id: `course-${Date.now()}`,
|
id: `course-${Date.now()}`,
|
||||||
title: title || 'Nuevo Curso Especializado',
|
title: title || 'Nuevo Curso Especializado',
|
||||||
@@ -103,88 +191,88 @@ export const CreateCourseModal: React.FC<CreateCourseModalProps> = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
onCreateCourse(createdCourse);
|
onCreateCourse(createdCourse);
|
||||||
onClose();
|
handleCloseModal();
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
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="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
|
<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()}
|
onClick={(e) => e.stopPropagation()}
|
||||||
>
|
>
|
||||||
|
|
||||||
{/* Header */}
|
{/* 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="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 />
|
<FiBookOpen />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<h3 className="text-base font-bold text-white">Publicar Nuevo Curso</h3>
|
<h3 className="text-base font-bold text-slate-900 dark:text-white">Publicar Nuevo Curso & Cargar Videos</h3>
|
||||||
<p className="text-xs text-slate-400">Paso {step} de 3</p>
|
<p className="text-xs text-slate-500 dark:text-slate-400">Paso {step} de 3</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
onClick={onClose}
|
type="button"
|
||||||
className="p-2 rounded-xl text-slate-400 hover:text-white hover:bg-slate-800 transition"
|
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" />
|
<FiX className="text-xl" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Step Progress Bar */}
|
{/* 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
|
<div
|
||||||
className="bg-gradient-to-r from-violet-600 to-indigo-500 h-full transition-all duration-300"
|
className="bg-gradient-to-r from-violet-600 to-indigo-500 h-full transition-all duration-300"
|
||||||
style={{ width: `${(step / 3) * 100}%` }}
|
style={{ width: `${(step / 3) * 100}%` }}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Form Body */}
|
{/* Modal Container Body */}
|
||||||
<form onSubmit={handleSubmit} className="p-6 space-y-5">
|
<div className="p-6 space-y-5">
|
||||||
|
|
||||||
{/* Step 1: Basic Information */}
|
{/* Step 1: Basic Information */}
|
||||||
{step === 1 && (
|
{step === 1 && (
|
||||||
<div className="space-y-4 animate-in fade-in">
|
<div className="space-y-4 animate-in fade-in">
|
||||||
<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">
|
||||||
Título del Curso *
|
Título del Curso *
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
required
|
|
||||||
value={title}
|
value={title}
|
||||||
onChange={(e) => setTitle(e.target.value)}
|
onChange={(e) => setTitle(e.target.value)}
|
||||||
|
onKeyDown={(e) => e.key === 'Enter' && handleNextStep()}
|
||||||
placeholder="Ej. Master en Desarrollo de Software Full Stack"
|
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>
|
||||||
|
|
||||||
<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 *
|
Subtítulo / Breve Descripción *
|
||||||
</label>
|
</label>
|
||||||
<textarea
|
<textarea
|
||||||
rows={2}
|
rows={2}
|
||||||
required
|
|
||||||
value={subtitle}
|
value={subtitle}
|
||||||
onChange={(e) => setSubtitle(e.target.value)}
|
onChange={(e) => setSubtitle(e.target.value)}
|
||||||
placeholder="Explica en 2 frases lo que el estudiante logrará al finalizar este curso."
|
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>
|
||||||
|
|
||||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||||
<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">
|
||||||
Categoría
|
Categoría
|
||||||
</label>
|
</label>
|
||||||
<select
|
<select
|
||||||
value={category}
|
value={category}
|
||||||
onChange={(e) => setCategory(e.target.value)}
|
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="Desarrollo Web">Desarrollo Web</option>
|
||||||
<option value="Inteligencia Artificial">Inteligencia Artificial</option>
|
<option value="Inteligencia Artificial">Inteligencia Artificial</option>
|
||||||
@@ -196,13 +284,13 @@ export const CreateCourseModal: React.FC<CreateCourseModalProps> = ({
|
|||||||
</div>
|
</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">
|
||||||
Nivel del Curso
|
Nivel del Curso
|
||||||
</label>
|
</label>
|
||||||
<select
|
<select
|
||||||
value={level}
|
value={level}
|
||||||
onChange={(e) => setLevel(e.target.value as CourseLevel)}
|
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="Principiante">Principiante</option>
|
||||||
<option value="Intermedio">Intermedio</option>
|
<option value="Intermedio">Intermedio</option>
|
||||||
@@ -214,12 +302,82 @@ export const CreateCourseModal: React.FC<CreateCourseModalProps> = ({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Step 2: Pricing & Duration */}
|
{/* Step 2: Video Upload & Pricing */}
|
||||||
{step === 2 && (
|
{step === 2 && (
|
||||||
<div className="space-y-4 animate-in fade-in">
|
<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 className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||||
<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">
|
||||||
Precio de Venta ($ USD) *
|
Precio de Venta ($ USD) *
|
||||||
</label>
|
</label>
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
@@ -227,17 +385,33 @@ export const CreateCourseModal: React.FC<CreateCourseModalProps> = ({
|
|||||||
<input
|
<input
|
||||||
type="number"
|
type="number"
|
||||||
step="0.01"
|
step="0.01"
|
||||||
required
|
|
||||||
value={price}
|
value={price}
|
||||||
onChange={(e) => setPrice(e.target.value)}
|
onChange={(e) => setPrice(e.target.value)}
|
||||||
|
onKeyDown={(e) => e.key === 'Enter' && handleNextStep()}
|
||||||
placeholder="29.99"
|
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>
|
</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)
|
Precio Original Tachado ($ USD)
|
||||||
</label>
|
</label>
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
@@ -247,60 +421,86 @@ export const CreateCourseModal: React.FC<CreateCourseModalProps> = ({
|
|||||||
step="0.01"
|
step="0.01"
|
||||||
value={originalPrice}
|
value={originalPrice}
|
||||||
onChange={(e) => setOriginalPrice(e.target.value)}
|
onChange={(e) => setOriginalPrice(e.target.value)}
|
||||||
|
onKeyDown={(e) => e.key === 'Enter' && handleNextStep()}
|
||||||
placeholder="89.99"
|
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>
|
||||||
</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>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Step 3: Media & What you'll learn */}
|
{/* Step 3: Thumbnail Drag & Drop & Learning Outcomes */}
|
||||||
{step === 3 && (
|
{step === 3 && (
|
||||||
<div className="space-y-4 animate-in fade-in">
|
<div className="space-y-4 animate-in fade-in">
|
||||||
<div>
|
|
||||||
<label className="block text-xs font-bold text-slate-300 mb-1.5">
|
{/* Thumbnail Image Drag & Drop */}
|
||||||
URL de Imagen de Portada (Thumbnail)
|
<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>
|
||||||
|
|
||||||
|
<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>
|
</label>
|
||||||
<div className="flex gap-2">
|
|
||||||
<input
|
<input
|
||||||
type="url"
|
type="url"
|
||||||
value={thumbnail}
|
value={thumbnail}
|
||||||
onChange={(e) => setThumbnail(e.target.value)}
|
onChange={(e) => {
|
||||||
|
setThumbnailFile(null);
|
||||||
|
setThumbnail(e.target.value);
|
||||||
|
}}
|
||||||
placeholder="https://images.unsplash.com/..."
|
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>
|
</div>
|
||||||
{thumbnail && (
|
|
||||||
<img
|
|
||||||
src={thumbnail}
|
|
||||||
alt="Preview"
|
|
||||||
className="mt-2 h-24 w-40 object-cover rounded-lg border border-slate-700"
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
</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">
|
||||||
Lo que aprenderá el alumno (Puntos clave)
|
Lo que aprenderá el alumno (Puntos clave)
|
||||||
</label>
|
</label>
|
||||||
<div className="flex gap-2 mb-2">
|
<div className="flex gap-2 mb-2">
|
||||||
@@ -308,8 +508,9 @@ export const CreateCourseModal: React.FC<CreateCourseModalProps> = ({
|
|||||||
type="text"
|
type="text"
|
||||||
value={newOutcome}
|
value={newOutcome}
|
||||||
onChange={(e) => setNewOutcome(e.target.value)}
|
onChange={(e) => setNewOutcome(e.target.value)}
|
||||||
|
onKeyDown={(e) => e.key === 'Enter' && handleAddOutcome()}
|
||||||
placeholder="Ej. Crear APIs RESTful y GraphQL con Node.js"
|
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
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
@@ -324,15 +525,15 @@ export const CreateCourseModal: React.FC<CreateCourseModalProps> = ({
|
|||||||
{whatYouWillLearn.map((item, idx) => (
|
{whatYouWillLearn.map((item, idx) => (
|
||||||
<div
|
<div
|
||||||
key={idx}
|
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">
|
<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>
|
</span>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => handleRemoveOutcome(idx)}
|
onClick={() => handleRemoveOutcome(idx)}
|
||||||
className="text-slate-500 hover:text-rose-400 pl-2"
|
className="text-slate-400 hover:text-rose-500 pl-2"
|
||||||
>
|
>
|
||||||
<FiX />
|
<FiX />
|
||||||
</button>
|
</button>
|
||||||
@@ -344,12 +545,12 @@ export const CreateCourseModal: React.FC<CreateCourseModalProps> = ({
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Bottom Actions */}
|
{/* 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 ? (
|
{step > 1 ? (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => setStep((s) => (s - 1) as any)}
|
onClick={handlePrevStep}
|
||||||
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"
|
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
|
<FiArrowLeft /> Anterior
|
||||||
</button>
|
</button>
|
||||||
@@ -360,12 +561,7 @@ export const CreateCourseModal: React.FC<CreateCourseModalProps> = ({
|
|||||||
{step < 3 ? (
|
{step < 3 ? (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => {
|
onClick={handleNextStep}
|
||||||
if (step === 1 && !title.trim()) {
|
|
||||||
setTitle('Nuevo Curso Especializado');
|
|
||||||
}
|
|
||||||
setStep((s) => (s + 1) as any);
|
|
||||||
}}
|
|
||||||
className="px-5 py-2 bg-violet-600 hover:bg-violet-500 text-white text-xs font-bold rounded-xl flex items-center gap-1.5 transition shadow-lg shadow-violet-600/30"
|
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>
|
<span>Siguiente</span>
|
||||||
@@ -373,16 +569,17 @@ export const CreateCourseModal: React.FC<CreateCourseModalProps> = ({
|
|||||||
</button>
|
</button>
|
||||||
) : (
|
) : (
|
||||||
<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"
|
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" />
|
<FiCheck className="text-base" />
|
||||||
<span>Publicar Curso Ahora</span>
|
<span>Publicar Curso y Videos</span>
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</form>
|
</div>
|
||||||
|
|
||||||
</div>
|
</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";
|
@import "tailwindcss";
|
||||||
|
|
||||||
|
@custom-variant dark (&:where(.dark, .dark *));
|
||||||
|
|
||||||
@layer base {
|
@layer base {
|
||||||
html {
|
html {
|
||||||
scroll-behavior: smooth;
|
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 */
|
/* Custom modern scrollbar */
|
||||||
::-webkit-scrollbar {
|
::-webkit-scrollbar {
|
||||||
width: 8px;
|
width: 8px;
|
||||||
@@ -26,30 +56,39 @@
|
|||||||
background: #475569;
|
background: #475569;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Cart Running Animation */
|
/* Full-Screen Crossing Flying Spaceship Animation */
|
||||||
@keyframes cart-run {
|
@keyframes float-spaceship {
|
||||||
0% {
|
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% {
|
100% {
|
||||||
transform: translateX(0) rotate(0deg) translateY(0);
|
transform: translate(110vw, 5vh) rotate(24deg) scale(0.7);
|
||||||
|
opacity: 0.1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.animate-cart-run {
|
.animate-spaceship {
|
||||||
animation: cart-run 0.5s ease-in-out infinite;
|
animation: float-spaceship 20s cubic-bezier(0.35, 0, 0.25, 1) infinite;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Speed Streaks for Cart */
|
/* Speed Streaks for Cart */
|
||||||
@@ -58,10 +97,12 @@
|
|||||||
opacity: 0;
|
opacity: 0;
|
||||||
transform: translateX(0) scaleX(0.2);
|
transform: translateX(0) scaleX(0.2);
|
||||||
}
|
}
|
||||||
|
|
||||||
50% {
|
50% {
|
||||||
opacity: 0.8;
|
opacity: 0.8;
|
||||||
transform: translateX(-8px) scaleX(1);
|
transform: translateX(-8px) scaleX(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
100% {
|
100% {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
transform: translateX(-16px) scaleX(0);
|
transform: translateX(-16px) scaleX(0);
|
||||||
@@ -77,23 +118,42 @@
|
|||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.group-hover\\:animate-cart-run:hover .speed-line-1 {
|
.group:hover .speed-line-1 {
|
||||||
animation: speed-streak 0.6s infinite linear;
|
animation: speed-streak 0.6s infinite linear;
|
||||||
top: 30%;
|
top: 30%;
|
||||||
right: 110%;
|
right: 110%;
|
||||||
width: 12px;
|
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;
|
animation: speed-streak 0.5s infinite linear 0.2s;
|
||||||
top: 60%;
|
top: 60%;
|
||||||
right: 100%;
|
right: 100%;
|
||||||
width: 8px;
|
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;
|
animation: speed-streak 0.7s infinite linear 0.4s;
|
||||||
bottom: 10%;
|
bottom: 10%;
|
||||||
right: 120%;
|
right: 120%;
|
||||||
width: 14px;
|
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;
|
||||||
|
}
|
||||||
+415
-224
@@ -1,5 +1,15 @@
|
|||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
|
import { Link } from 'react-router';
|
||||||
import type { Course } from '../types/course';
|
import type { Course } from '../types/course';
|
||||||
|
import { BrandLogo } from '../components/common/BrandLogo';
|
||||||
|
import {
|
||||||
|
Card,
|
||||||
|
Button,
|
||||||
|
Badge,
|
||||||
|
Input,
|
||||||
|
Select,
|
||||||
|
ThemeToggle
|
||||||
|
} from '../components/ui';
|
||||||
import {
|
import {
|
||||||
FiDollarSign,
|
FiDollarSign,
|
||||||
FiUsers,
|
FiUsers,
|
||||||
@@ -9,10 +19,12 @@ import {
|
|||||||
FiTrash2,
|
FiTrash2,
|
||||||
FiShield,
|
FiShield,
|
||||||
FiSearch,
|
FiSearch,
|
||||||
FiFilter,
|
|
||||||
FiActivity,
|
FiActivity,
|
||||||
FiPieChart,
|
FiPieChart,
|
||||||
FiSettings
|
FiSettings,
|
||||||
|
FiMenu,
|
||||||
|
FiX,
|
||||||
|
FiArrowLeft
|
||||||
} from 'react-icons/fi';
|
} from 'react-icons/fi';
|
||||||
|
|
||||||
interface AdminDashboardViewProps {
|
interface AdminDashboardViewProps {
|
||||||
@@ -26,13 +38,28 @@ export const AdminDashboardView: React.FC<AdminDashboardViewProps> = ({
|
|||||||
onDeleteCourse,
|
onDeleteCourse,
|
||||||
onShowToast,
|
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 [statusFilter, setStatusFilter] = useState<'all' | 'published' | 'pending'>('all');
|
||||||
const [searchTerm, setSearchTerm] = useState('');
|
const [searchTerm, setSearchTerm] = useState('');
|
||||||
|
const [userSearchTerm, setUserSearchTerm] = useState('');
|
||||||
|
|
||||||
// Course status state management for demonstration
|
// Course status state management for demonstration
|
||||||
const [courseStatuses, setCourseStatuses] = useState<Record<string, 'published' | 'pending' | 'rejected'>>({});
|
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) => {
|
const getStatus = (courseId: string) => {
|
||||||
return courseStatuses[courseId] || 'published';
|
return courseStatuses[courseId] || 'published';
|
||||||
};
|
};
|
||||||
@@ -41,17 +68,19 @@ export const AdminDashboardView: React.FC<AdminDashboardViewProps> = ({
|
|||||||
const current = getStatus(courseId);
|
const current = getStatus(courseId);
|
||||||
const nextStatus = current === 'published' ? 'pending' : 'published';
|
const nextStatus = current === 'published' ? 'pending' : 'published';
|
||||||
setCourseStatuses({ ...courseStatuses, [courseId]: nextStatus });
|
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 toggleUserStatus = (userId: string, userName: string) => {
|
||||||
const [users] = useState([
|
setUsers(users.map((u) => {
|
||||||
{ id: 'u-1', name: 'Ing. Carlos Mendoza', email: '[email protected]', role: 'Instructor', courses: 4, joined: '12 Ene 2026', status: 'Activo' },
|
if (u.id === userId) {
|
||||||
{ id: 'u-2', name: 'Dra. Elena Torres', email: '[email protected]', role: 'Instructor', courses: 2, joined: '04 Feb 2026', status: 'Activo' },
|
const next = u.status === 'Activo' ? 'Suspendido' : 'Activo';
|
||||||
{ id: 'u-3', name: 'Marcos Rodriguez', email: '[email protected]', role: 'Estudiante', courses: 5, joined: '18 Feb 2026', status: 'Activo' },
|
onShowToast(`Estado de ${userName} cambiado a ${next}`);
|
||||||
{ id: 'u-4', name: 'Sofía Martinez', email: '[email protected]', role: 'Estudiante', courses: 2, joined: '20 Feb 2026', status: 'Activo' },
|
return { ...u, status: next };
|
||||||
{ id: 'u-5', name: 'Admin Principal', email: '[email protected]', role: 'Administrador', courses: 0, joined: '01 Ene 2026', status: 'Activo' },
|
}
|
||||||
]);
|
return u;
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
const filteredCourses = courses.filter((c) => {
|
const filteredCourses = courses.filter((c) => {
|
||||||
const matchesSearch = c.title.toLowerCase().includes(searchTerm.toLowerCase()) || c.instructor.name.toLowerCase().includes(searchTerm.toLowerCase());
|
const matchesSearch = c.title.toLowerCase().includes(searchTerm.toLowerCase()) || c.instructor.name.toLowerCase().includes(searchTerm.toLowerCase());
|
||||||
@@ -60,205 +89,344 @@ export const AdminDashboardView: React.FC<AdminDashboardViewProps> = ({
|
|||||||
return matchesSearch && matchesStatus;
|
return matchesSearch && matchesStatus;
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
const filteredUsers = users.filter((u) =>
|
||||||
<div className="min-h-screen bg-slate-950 text-slate-100 pb-20">
|
u.name.toLowerCase().includes(userSearchTerm.toLowerCase()) || u.email.toLowerCase().includes(userSearchTerm.toLowerCase()) || u.role.toLowerCase().includes(userSearchTerm.toLowerCase())
|
||||||
|
);
|
||||||
|
|
||||||
{/* Top Banner */}
|
const totalGrossRevenue = 184920.00;
|
||||||
<div className="bg-gradient-to-r from-slate-900 via-indigo-950 to-slate-900 border-b border-slate-800 py-8">
|
const netCommission = totalGrossRevenue * (commissionRate / 100);
|
||||||
<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">
|
return (
|
||||||
<div>
|
<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">
|
||||||
<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
|
{/* 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>
|
</div>
|
||||||
<h1 className="text-2xl sm:text-4xl font-extrabold text-white">
|
|
||||||
Control & Moderación RyCourses
|
{/* 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>
|
||||||
|
<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>
|
||||||
|
|
||||||
|
{/* 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>
|
||||||
|
|
||||||
|
<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>
|
||||||
|
<Badge variant="primary" size="sm">{courses.length}</Badge>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<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>
|
||||||
|
<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>
|
||||||
|
|
||||||
|
{/* 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' && '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>
|
</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>
|
||||||
|
|
||||||
<div className="flex items-center gap-3">
|
<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">
|
<Badge variant="success" size="md">
|
||||||
<span className="w-2 h-2 rounded-full bg-emerald-400 animate-pulse" /> Servidores Normales (99.9% Uptime)
|
<span className="w-2 h-2 rounded-full bg-emerald-400 animate-pulse mr-1" />
|
||||||
</span>
|
Servidor Producción
|
||||||
</div>
|
</Badge>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8 space-y-8">
|
{/* Dynamic Content Views */}
|
||||||
|
<main className="p-4 sm:p-8 space-y-8 flex-1">
|
||||||
|
|
||||||
{/* KPI Platform Stat Cards */}
|
{/* 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">
|
<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="p-5 rounded-2xl bg-slate-900/80 border border-slate-800 space-y-2 shadow-xl">
|
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<span className="text-xs text-slate-400 font-medium">Ingresos Totales Plataforma</span>
|
<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-400 flex items-center justify-center text-lg">
|
<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 />
|
<FiDollarSign />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-2xl font-black text-white font-mono">$184,920.00</div>
|
<div className="text-2xl font-black text-slate-900 dark:text-white font-mono">${totalGrossRevenue.toLocaleString()}</div>
|
||||||
<div className="text-[11px] text-emerald-400 font-semibold flex items-center gap-1">
|
<div className="text-[11px] text-emerald-600 dark:text-emerald-400 font-semibold flex items-center gap-1">
|
||||||
<FiTrendingUp /> +24.8% en ventas este mes
|
<FiTrendingUp /> +24.8% en crecimiento
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
</Card>
|
||||||
|
|
||||||
<div className="p-5 rounded-2xl bg-slate-900/80 border border-slate-800 space-y-2 shadow-xl">
|
<Card variant="elevated" padding="md" className="space-y-2">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<span className="text-xs text-slate-400 font-medium">Usuarios Registrados</span>
|
<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-400 flex items-center justify-center text-lg">
|
<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 />
|
<FiUsers />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-2xl font-black text-white font-mono">54,320</div>
|
<div className="text-2xl font-black text-slate-900 dark:text-white font-mono">54,320</div>
|
||||||
<div className="text-[11px] text-slate-400">
|
<div className="text-[11px] text-slate-500 dark:text-slate-400">
|
||||||
<strong className="text-cyan-400">1,240</strong> Instructores activos
|
<strong className="text-cyan-600 dark:text-cyan-400">1,240</strong> Instructores activos
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
</Card>
|
||||||
|
|
||||||
<div className="p-5 rounded-2xl bg-slate-900/80 border border-slate-800 space-y-2 shadow-xl">
|
<Card variant="elevated" padding="md" className="space-y-2">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<span className="text-xs text-slate-400 font-medium">Cursos en Catálogo</span>
|
<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-400 flex items-center justify-center text-lg">
|
<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 />
|
<FiBookOpen />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-2xl font-black text-white font-mono">{courses.length} Publicados</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-400 font-medium">2 pendientes de aprobación</div>
|
<div className="text-[11px] text-amber-600 dark:text-amber-400 font-medium">2 pendientes de revisión</div>
|
||||||
</div>
|
</Card>
|
||||||
|
|
||||||
<div className="p-5 rounded-2xl bg-slate-900/80 border border-slate-800 space-y-2 shadow-xl">
|
<Card variant="elevated" padding="md" className="space-y-2">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<span className="text-xs text-slate-400 font-medium">Comisión Neta (20%)</span>
|
<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-400 flex items-center justify-center text-lg">
|
<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 />
|
<FiPieChart />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-2xl font-black text-white font-mono">$36,984.00</div>
|
<div className="text-2xl font-black text-slate-900 dark:text-white font-mono">${netCommission.toLocaleString()}</div>
|
||||||
<div className="text-[11px] text-slate-400">Generado para la plataforma</div>
|
<div className="text-[11px] text-slate-500 dark:text-slate-400">Ingreso neto RyCourses</div>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 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>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Tab Navigation */}
|
{/* TAB 1: COURSES MANAGEMENT */}
|
||||||
<div className="flex border-b border-slate-800 space-x-4 text-xs font-bold">
|
|
||||||
<button
|
|
||||||
onClick={() => setActiveTab('courses')}
|
|
||||||
className={`pb-3.5 px-2 flex items-center gap-2 transition-colors border-b-2 ${
|
|
||||||
activeTab === 'courses'
|
|
||||||
? 'text-cyan-400 border-cyan-400'
|
|
||||||
: 'text-slate-400 border-transparent hover:text-white'
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
<FiBookOpen /> Gestor de Cursos ({courses.length})
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<button
|
|
||||||
onClick={() => setActiveTab('users')}
|
|
||||||
className={`pb-3.5 px-2 flex items-center gap-2 transition-colors border-b-2 ${
|
|
||||||
activeTab === 'users'
|
|
||||||
? 'text-cyan-400 border-cyan-400'
|
|
||||||
: 'text-slate-400 border-transparent hover:text-white'
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
<FiUsers /> Usuarios e Instructores ({users.length})
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<button
|
|
||||||
onClick={() => setActiveTab('finance')}
|
|
||||||
className={`pb-3.5 px-2 flex items-center gap-2 transition-colors border-b-2 ${
|
|
||||||
activeTab === 'finance'
|
|
||||||
? 'text-cyan-400 border-cyan-400'
|
|
||||||
: 'text-slate-400 border-transparent hover:text-white'
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
<FiDollarSign /> Finanzas & Pagos
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<button
|
|
||||||
onClick={() => setActiveTab('settings')}
|
|
||||||
className={`pb-3.5 px-2 flex items-center gap-2 transition-colors border-b-2 ${
|
|
||||||
activeTab === 'settings'
|
|
||||||
? 'text-cyan-400 border-cyan-400'
|
|
||||||
: 'text-slate-400 border-transparent hover:text-white'
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
<FiSettings /> Ajustes Globales
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* TAB 1: Gestor de Cursos */}
|
|
||||||
{activeTab === 'courses' && (
|
{activeTab === 'courses' && (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
|
|
||||||
{/* Filter and Search Bar */}
|
{/* 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="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="relative flex-1 w-full max-w-md">
|
<div className="w-full sm:max-w-md">
|
||||||
<FiSearch className="absolute left-3.5 top-3 text-slate-500" />
|
<Input
|
||||||
<input
|
placeholder="Buscar por título de curso o nombre del instructor..."
|
||||||
type="text"
|
|
||||||
value={searchTerm}
|
value={searchTerm}
|
||||||
onChange={(e) => setSearchTerm(e.target.value)}
|
onChange={(e) => setSearchTerm(e.target.value)}
|
||||||
placeholder="Buscar curso por título o instructor..."
|
leftIcon={<FiSearch className="text-slate-400" />}
|
||||||
className="w-full pl-10 pr-4 py-2 bg-slate-950 border border-slate-800 rounded-xl text-xs text-white placeholder-slate-500 focus:outline-none focus:ring-2 focus:ring-cyan-500"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center gap-2 w-full sm:w-auto">
|
<div className="flex items-center gap-3 w-full sm:w-auto">
|
||||||
<FiFilter className="text-slate-400 text-sm" />
|
<Select
|
||||||
<select
|
|
||||||
value={statusFilter}
|
value={statusFilter}
|
||||||
onChange={(e) => setStatusFilter(e.target.value as any)}
|
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"
|
options={[
|
||||||
>
|
{ value: 'all', label: 'Todos los estados' },
|
||||||
<option value="all">Todos los estados</option>
|
{ value: 'published', label: 'Publicados' },
|
||||||
<option value="published">Publicados</option>
|
{ value: 'pending', label: 'En Revisión' },
|
||||||
<option value="pending">Pendientes de revisión</option>
|
]}
|
||||||
</select>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Courses Management Table */}
|
{/* Courses Management Table */}
|
||||||
<div className="bg-slate-900 border border-slate-800 rounded-2xl overflow-hidden shadow-xl">
|
<Card variant="elevated" padding="none" className="overflow-hidden">
|
||||||
<div className="overflow-x-auto">
|
<div className="overflow-x-auto">
|
||||||
<table className="w-full text-left text-xs text-slate-300">
|
<table className="w-full text-left text-xs text-slate-700 dark:text-slate-300">
|
||||||
<thead className="bg-slate-950 text-slate-400 uppercase tracking-wider font-semibold border-b border-slate-800">
|
<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>
|
<tr>
|
||||||
<th className="p-4">Curso</th>
|
<th className="p-4">Curso</th>
|
||||||
<th className="p-4">Categoría</th>
|
<th className="p-4">Categoría</th>
|
||||||
<th className="p-4">Instructor</th>
|
<th className="p-4">Instructor</th>
|
||||||
<th className="p-4">Precio</th>
|
<th className="p-4">Precio</th>
|
||||||
<th className="p-4">Estudiantes</th>
|
<th className="p-4">Alumnos</th>
|
||||||
<th className="p-4">Estado</th>
|
<th className="p-4">Estado</th>
|
||||||
<th className="p-4 text-right">Acciones Admin</th>
|
<th className="p-4 text-right">Acciones Admin</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody className="divide-y divide-slate-800/80">
|
<tbody className="divide-y divide-slate-200 dark:divide-slate-800/80">
|
||||||
{filteredCourses.map((c) => {
|
{filteredCourses.map((c) => {
|
||||||
const status = getStatus(c.id);
|
const status = getStatus(c.id);
|
||||||
return (
|
return (
|
||||||
<tr key={c.id} className="hover:bg-slate-800/40 transition">
|
<tr key={c.id} className="hover:bg-slate-100/60 dark:hover:bg-slate-800/40 transition">
|
||||||
<td className="p-4">
|
<td className="p-4">
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<img
|
<img
|
||||||
src={c.thumbnail}
|
src={c.thumbnail}
|
||||||
alt={c.title}
|
alt={c.title}
|
||||||
className="w-12 h-8 rounded object-cover border border-slate-700 shrink-0"
|
className="w-12 h-8 rounded object-cover border border-slate-300 dark:border-slate-700 shrink-0"
|
||||||
/>
|
/>
|
||||||
<div>
|
<div>
|
||||||
<h4 className="font-bold text-white max-w-xs line-clamp-1">{c.title}</h4>
|
<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">ID: {c.id}</span>
|
<span className="text-[10px] text-slate-500 dark:text-slate-500">ID: {c.id}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
|
|
||||||
<td className="p-4">
|
<td className="p-4">
|
||||||
<span className="px-2 py-0.5 rounded bg-slate-800 text-cyan-400 font-medium">
|
<Badge variant="primary" size="sm">{c.category}</Badge>
|
||||||
{c.category}
|
|
||||||
</span>
|
|
||||||
</td>
|
</td>
|
||||||
|
|
||||||
<td className="p-4">
|
<td className="p-4">
|
||||||
@@ -268,52 +436,43 @@ export const AdminDashboardView: React.FC<AdminDashboardViewProps> = ({
|
|||||||
alt={c.instructor.name}
|
alt={c.instructor.name}
|
||||||
className="w-6 h-6 rounded-full object-cover"
|
className="w-6 h-6 rounded-full object-cover"
|
||||||
/>
|
/>
|
||||||
<span className="font-medium text-slate-200">{c.instructor.name}</span>
|
<span className="font-medium text-slate-800 dark:text-slate-200">{c.instructor.name}</span>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
|
|
||||||
<td className="p-4 font-mono font-bold text-white">
|
<td className="p-4 font-mono font-bold text-slate-900 dark:text-white">
|
||||||
${c.price.toFixed(2)}
|
${c.price.toFixed(2)}
|
||||||
</td>
|
</td>
|
||||||
|
|
||||||
<td className="p-4 font-mono text-slate-300">
|
<td className="p-4 font-mono text-slate-700 dark:text-slate-300">
|
||||||
{c.studentsCount.toLocaleString()}
|
{c.studentsCount.toLocaleString()}
|
||||||
</td>
|
</td>
|
||||||
|
|
||||||
<td className="p-4">
|
<td className="p-4">
|
||||||
{status === 'published' ? (
|
{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">
|
<Badge variant="success" size="sm"><FiCheckCircle /> Publicado</Badge>
|
||||||
<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">
|
<Badge variant="warning" size="sm"><FiActivity /> En Revisión</Badge>
|
||||||
<FiActivity /> En Revisión
|
|
||||||
</span>
|
|
||||||
)}
|
)}
|
||||||
</td>
|
</td>
|
||||||
|
|
||||||
<td className="p-4 text-right">
|
<td className="p-4 text-right">
|
||||||
<div className="flex items-center justify-end gap-2">
|
<div className="flex items-center justify-end gap-2">
|
||||||
<button
|
<Button
|
||||||
|
variant={status === 'published' ? 'outline' : 'success'}
|
||||||
|
size="xs"
|
||||||
onClick={() => toggleCourseStatus(c.id, c.title)}
|
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'}
|
{status === 'published' ? 'Pausar' : 'Aprobar'}
|
||||||
</button>
|
</Button>
|
||||||
|
|
||||||
<button
|
<Button
|
||||||
onClick={() => {
|
variant="danger"
|
||||||
onDeleteCourse(c.id);
|
size="xs"
|
||||||
}}
|
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 />
|
<FiTrash2 />
|
||||||
</button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -322,120 +481,152 @@ export const AdminDashboardView: React.FC<AdminDashboardViewProps> = ({
|
|||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</Card>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* TAB 2: Usuarios & Instructores */}
|
{/* TAB 2: USERS MANAGEMENT */}
|
||||||
{activeTab === 'users' && (
|
{activeTab === 'users' && (
|
||||||
<div className="bg-slate-900 border border-slate-800 rounded-2xl overflow-hidden shadow-xl">
|
<div className="space-y-6">
|
||||||
<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>
|
<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">
|
||||||
<span className="text-xs text-slate-400">Total: {users.length} registrados</span>
|
<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>
|
</div>
|
||||||
|
|
||||||
<table className="w-full text-left text-xs text-slate-300">
|
<Badge variant="secondary" size="md">
|
||||||
<thead className="bg-slate-950 text-slate-400 uppercase tracking-wider font-semibold border-b border-slate-800">
|
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>
|
<tr>
|
||||||
<th className="p-4">Usuario</th>
|
<th className="p-4">Usuario</th>
|
||||||
<th className="p-4">Correo</th>
|
<th className="p-4">Correo</th>
|
||||||
<th className="p-4">Rol</th>
|
<th className="p-4">Rol</th>
|
||||||
<th className="p-4">Cursos Asoc.</th>
|
<th className="p-4">Cursos</th>
|
||||||
<th className="p-4">Registro</th>
|
<th className="p-4">Registro</th>
|
||||||
<th className="p-4">Estado</th>
|
<th className="p-4">Estado</th>
|
||||||
|
<th className="p-4 text-right">Acción</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody className="divide-y divide-slate-800/80">
|
<tbody className="divide-y divide-slate-200 dark:divide-slate-800/80">
|
||||||
{users.map((u) => (
|
{filteredUsers.map((u) => (
|
||||||
<tr key={u.id} className="hover:bg-slate-800/40 transition">
|
<tr key={u.id} className="hover:bg-slate-100/60 dark:hover:bg-slate-800/40 transition">
|
||||||
<td className="p-4 font-bold text-white">{u.name}</td>
|
<td className="p-4 font-bold text-slate-900 dark:text-white">{u.name}</td>
|
||||||
<td className="p-4 text-slate-400 font-mono">{u.email}</td>
|
<td className="p-4 text-slate-500 dark:text-slate-400 font-mono">{u.email}</td>
|
||||||
<td className="p-4">
|
<td className="p-4">
|
||||||
<span className={`px-2.5 py-1 rounded-md text-[10px] font-bold ${
|
<Badge
|
||||||
u.role === 'Administrador'
|
variant={u.role === 'Administrador' ? 'danger' : u.role === 'Instructor' ? 'secondary' : 'primary'}
|
||||||
? 'bg-rose-500/20 text-rose-300 border border-rose-500/30'
|
size="sm"
|
||||||
: 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}
|
{u.role}
|
||||||
</span>
|
</Badge>
|
||||||
</td>
|
</td>
|
||||||
<td className="p-4 font-mono">{u.courses}</td>
|
<td className="p-4 font-mono">{u.courses}</td>
|
||||||
<td className="p-4 text-slate-400">{u.joined}</td>
|
<td className="p-4 text-slate-500 dark:text-slate-400">{u.joined}</td>
|
||||||
<td className="p-4">
|
<td className="p-4">
|
||||||
<span className="text-emerald-400 font-semibold">{u.status}</span>
|
<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>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
))}
|
))}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
</Card>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* TAB 3: Finanzas */}
|
{/* TAB 3: FINANCIALS */}
|
||||||
{activeTab === 'finance' && (
|
{activeTab === 'finance' && (
|
||||||
|
<div className="space-y-6">
|
||||||
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
<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">
|
<Card variant="elevated" padding="lg" className="space-y-4">
|
||||||
<h3 className="font-bold text-white text-base">Desglose de Repartición Financiera</h3>
|
<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="space-y-3 text-xs">
|
||||||
<div className="flex justify-between p-3 bg-slate-950 rounded-xl border border-slate-800">
|
<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-400">Ventas Brutas Acumuladas:</span>
|
<span className="text-slate-600 dark:text-slate-400">Ventas Brutas Acumuladas:</span>
|
||||||
<span className="font-mono font-bold text-white">$184,920.00</span>
|
<span className="font-mono font-bold text-slate-900 dark:text-white">${totalGrossRevenue.toLocaleString()}</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex justify-between p-3 bg-slate-950 rounded-xl border border-slate-800">
|
<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-400">Pago a Instructores (80%):</span>
|
<span className="text-slate-600 dark:text-slate-400">Pago a Instructores ({100 - commissionRate}%):</span>
|
||||||
<span className="font-mono font-bold text-emerald-400">$147,936.00</span>
|
<span className="font-mono font-bold text-emerald-600 dark:text-emerald-400">${(totalGrossRevenue * ((100 - commissionRate) / 100)).toLocaleString()}</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex justify-between p-3 bg-slate-950 rounded-xl border border-slate-800">
|
<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-400">Comisión Neta Plataforma (20%):</span>
|
<span className="text-slate-600 dark:text-slate-400">Comisión Neta RyCourses ({commissionRate}%):</span>
|
||||||
<span className="font-mono font-bold text-cyan-400">$36,984.00</span>
|
<span className="font-mono font-bold text-cyan-600 dark:text-cyan-400">${netCommission.toLocaleString()}</span>
|
||||||
</div>
|
</div>
|
||||||
</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>
|
</div>
|
||||||
|
|
||||||
<div className="bg-slate-900 border border-slate-800 rounded-2xl p-6 space-y-4">
|
|
||||||
<h3 className="font-bold text-white text-base">Métodos de Pago Integrados</h3>
|
|
||||||
<p className="text-xs text-slate-400">
|
|
||||||
Stripe & PayPal operando de forma continua para procesamiento automático de cobros y giros a instructores.
|
|
||||||
</p>
|
|
||||||
<div className="p-4 rounded-xl bg-emerald-500/10 border border-emerald-500/20 text-emerald-400 text-xs font-semibold flex items-center gap-2">
|
|
||||||
<FiCheckCircle /> Pagos automáticos semanales habilitados.
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* TAB 4: Ajustes Globales */}
|
{/* TAB 4: GLOBAL SETTINGS */}
|
||||||
{activeTab === 'settings' && (
|
{activeTab === 'settings' && (
|
||||||
<div className="bg-slate-900 border border-slate-800 rounded-2xl p-6 space-y-6 max-w-2xl">
|
<Card variant="elevated" padding="lg" className="max-w-xl space-y-6">
|
||||||
<h3 className="font-bold text-white text-base">Configuración de Plataforma</h3>
|
<h3 className="font-bold text-slate-900 dark: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>
|
<div className="space-y-4">
|
||||||
<label className="block text-slate-300 font-semibold mb-1">Porcentaje de Comisión Base (%)</label>
|
<Input
|
||||||
<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" />
|
label="Nombre de la Plataforma"
|
||||||
</div>
|
value={siteName}
|
||||||
|
onChange={(e) => setSiteName(e.target.value)}
|
||||||
|
/>
|
||||||
|
|
||||||
<div className="pt-2">
|
<Input
|
||||||
<button
|
label="Porcentaje de Comisión Base Plataforma (%)"
|
||||||
type="button"
|
type="number"
|
||||||
onClick={() => onShowToast('Ajustes guardados correctamente.')}
|
value={commissionRate}
|
||||||
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"
|
onChange={(e) => setCommissionRate(Number(e.target.value))}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
variant="gradient"
|
||||||
|
fullWidth
|
||||||
|
size="md"
|
||||||
|
onClick={() => onShowToast('Ajustes globales guardados con éxito')}
|
||||||
>
|
>
|
||||||
Guardar Cambios
|
Guardar Cambios
|
||||||
</button>
|
</Button>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
</Card>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
</main>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
+273
-31
@@ -1,7 +1,19 @@
|
|||||||
import React, { useState } from 'react';
|
import React, { useState, useRef } from 'react';
|
||||||
import { useNavigate } from 'react-router';
|
import { useNavigate } from 'react-router';
|
||||||
import type { Course } from '../types/course';
|
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 {
|
interface CreateCourseViewProps {
|
||||||
onCreateCourse: (course: Course) => void;
|
onCreateCourse: (course: Course) => void;
|
||||||
@@ -13,6 +25,8 @@ export const CreateCourseView: React.FC<CreateCourseViewProps> = ({
|
|||||||
onShowToast,
|
onShowToast,
|
||||||
}) => {
|
}) => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||||
|
const imageInputRef = useRef<HTMLInputElement>(null);
|
||||||
|
|
||||||
const [title, setTitle] = useState('');
|
const [title, setTitle] = useState('');
|
||||||
const [subtitle, setSubtitle] = 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 [level, setLevel] = useState<'Principiante' | 'Intermedio' | 'Avanzado' | 'Todos los niveles'>('Principiante');
|
||||||
const [price, setPrice] = useState(29.99);
|
const [price, setPrice] = useState(29.99);
|
||||||
const [originalPrice, setOriginalPrice] = useState(89.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 [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 }[]>([
|
// Video Upload States
|
||||||
{ title: 'Módulo 1: Introducción y Fundamentos', lessonsCount: 4 },
|
const [videoFile, setVideoFile] = useState<File | null>(null);
|
||||||
{ title: 'Módulo 2: Proyecto Práctico Principal', lessonsCount: 6 },
|
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 [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 = () => {
|
const handleAddModule = () => {
|
||||||
if (newModuleTitle.trim()) {
|
if (newModuleTitle.trim()) {
|
||||||
setModules([...modules, { title: newModuleTitle, lessonsCount: 3 }]);
|
setModules([...modules, { title: newModuleTitle, lessonsCount: 3, videoName: 'clase_muestra.mp4' }]);
|
||||||
setNewModuleTitle('');
|
setNewModuleTitle('');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -63,7 +135,7 @@ export const CreateCourseView: React.FC<CreateCourseViewProps> = ({
|
|||||||
id: 'inst-1',
|
id: 'inst-1',
|
||||||
name: 'Tu Nombre (Instructor)',
|
name: 'Tu Nombre (Instructor)',
|
||||||
role: 'Instructor Experto',
|
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,
|
rating: 5.0,
|
||||||
totalStudents: 1,
|
totalStudents: 1,
|
||||||
totalCourses: 1,
|
totalCourses: 1,
|
||||||
@@ -92,28 +164,28 @@ export const CreateCourseView: React.FC<CreateCourseViewProps> = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
onCreateCourse(newCourse);
|
onCreateCourse(newCourse);
|
||||||
onShowToast('¡Tu curso ha sido publicado exitosamente en el Marketplace!');
|
onShowToast('¡Tu curso, portada y videos han sido publicados exitosamente!');
|
||||||
navigate('/seller');
|
navigate('/teachers');
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
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">
|
<div className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 space-y-8">
|
||||||
|
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div className="flex items-center justify-between border-b border-slate-800 pb-5">
|
<div className="flex items-center justify-between border-b border-slate-800 pb-5">
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
<button
|
<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"
|
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
|
<FiArrowLeft /> Regresar a Panel de Instructor
|
||||||
</button>
|
</button>
|
||||||
<h1 className="text-2xl sm:text-3xl font-extrabold text-white">
|
<h1 className="text-2xl sm:text-3xl font-extrabold text-white">
|
||||||
Publicar Nuevo Curso
|
Publicar Nuevo Curso & Cargar Contenido
|
||||||
</h1>
|
</h1>
|
||||||
<p className="text-xs text-slate-400">
|
<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>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -193,10 +265,117 @@ export const CreateCourseView: React.FC<CreateCourseViewProps> = ({
|
|||||||
</div>
|
</div>
|
||||||
</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">
|
<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">
|
<div className="flex items-center justify-between">
|
||||||
2. Precio e Imagen de Portada
|
<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>
|
</h3>
|
||||||
|
|
||||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||||
@@ -216,7 +395,7 @@ export const CreateCourseView: React.FC<CreateCourseViewProps> = ({
|
|||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-xs font-semibold text-slate-300 mb-1">
|
<label className="block text-xs font-semibold text-slate-300 mb-1">
|
||||||
Precio Original Táchado ($ USD)
|
Precio Original Tachado ($ USD)
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
type="number"
|
type="number"
|
||||||
@@ -229,34 +408,97 @@ export const CreateCourseView: React.FC<CreateCourseViewProps> = ({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* 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>
|
||||||
|
|
||||||
|
<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>
|
<div>
|
||||||
<label className="block text-xs font-semibold text-slate-300 mb-1">
|
<p className="text-xs font-bold text-white">Haz clic o arrastra tu imagen de portada aquí</p>
|
||||||
URL de la Imagen de Portada
|
<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>
|
</label>
|
||||||
<div className="flex gap-2">
|
|
||||||
<input
|
<input
|
||||||
type="url"
|
type="url"
|
||||||
value={thumbnail}
|
value={thumbnail}
|
||||||
onChange={(e) => setThumbnail(e.target.value)}
|
onChange={(e) => {
|
||||||
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"
|
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>
|
||||||
<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>
|
||||||
</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">
|
<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">
|
<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>
|
</h3>
|
||||||
|
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
{modules.map((m, idx) => (
|
{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">
|
<div key={idx} className="p-4 bg-slate-950 border border-slate-800 rounded-xl text-xs flex items-center justify-between gap-3">
|
||||||
<span className="font-semibold text-white">{m.title}</span>
|
<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
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => handleRemoveModule(idx)}
|
onClick={() => handleRemoveModule(idx)}
|
||||||
@@ -290,7 +532,7 @@ export const CreateCourseView: React.FC<CreateCourseViewProps> = ({
|
|||||||
<div className="flex items-center justify-end gap-4 pt-4">
|
<div className="flex items-center justify-end gap-4 pt-4">
|
||||||
<button
|
<button
|
||||||
type="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"
|
className="px-6 py-3.5 bg-slate-800 hover:bg-slate-700 text-slate-300 font-semibold rounded-xl text-xs transition"
|
||||||
>
|
>
|
||||||
Cancelar
|
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"
|
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 />
|
<FiUploadCloud />
|
||||||
<span>Publicar Curso en RyCourses</span>
|
<span>Publicar Curso y Videos</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
+155
-245
@@ -1,18 +1,19 @@
|
|||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import { useNavigate, Link } from 'react-router';
|
import { useNavigate, Link } from 'react-router';
|
||||||
import { BrandLogo } from '../components/common/BrandLogo';
|
import { BrandLogo } from '../components/common/BrandLogo';
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
Input,
|
||||||
|
Checkbox,
|
||||||
|
ThemeToggle,
|
||||||
|
Card
|
||||||
|
} from '../components/ui';
|
||||||
import {
|
import {
|
||||||
FiMail,
|
FiMail,
|
||||||
FiLock,
|
FiLock,
|
||||||
FiEye,
|
|
||||||
FiEyeOff,
|
|
||||||
FiArrowRight,
|
FiArrowRight,
|
||||||
FiArrowLeft,
|
FiArrowLeft,
|
||||||
FiStar,
|
FiCheckCircle
|
||||||
FiCheckCircle,
|
|
||||||
FiBookOpen,
|
|
||||||
FiAward,
|
|
||||||
FiUsers
|
|
||||||
} from 'react-icons/fi';
|
} from 'react-icons/fi';
|
||||||
import type { User } from '../types/course';
|
import type { User } from '../types/course';
|
||||||
|
|
||||||
@@ -23,303 +24,212 @@ interface LoginViewProps {
|
|||||||
|
|
||||||
export const LoginView: React.FC<LoginViewProps> = ({ onShowToast, onLogin }) => {
|
export const LoginView: React.FC<LoginViewProps> = ({ onShowToast, onLogin }) => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
const [email, setEmail] = useState('');
|
const [email, setEmail] = useState('');
|
||||||
const [password, setPassword] = useState('');
|
const [password, setPassword] = useState('');
|
||||||
const [showPassword, setShowPassword] = useState(false);
|
|
||||||
const [rememberMe, setRememberMe] = useState(true);
|
const [rememberMe, setRememberMe] = useState(true);
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
|
||||||
const handleMockLogin = (name: string, role: 'buyer' | 'seller', avatarUrl?: string) => {
|
const isValidEmail = email.includes('@') && email.includes('.');
|
||||||
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 handleSubmit = (e: React.FormEvent) => {
|
const handleSubmit = (e: React.FormEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
if (email && password) {
|
if (!email || !password) return;
|
||||||
handleMockLogin('Usuario Estudiante', 'buyer');
|
|
||||||
|
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 (
|
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 */}
|
{/* Top-Right Background Radial Gradient Glow */}
|
||||||
<div className="absolute -top-40 -left-40 w-96 h-96 bg-cyan-600/15 rounded-full blur-[120px] pointer-events-none" />
|
<div
|
||||||
<div className="absolute top-1/2 right-1/3 w-80 h-80 bg-blue-600/10 rounded-full blur-[140px] pointer-events-none" />
|
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" />
|
||||||
|
|
||||||
{/* ============================================================== */}
|
{/* Interactive Background Atmosphere: Twinkling Star Particles & Flying Space Shuttle */}
|
||||||
{/* LEFT COLUMN: BRANDING & GRAPHIC BANNER */}
|
<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="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">
|
<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 */}
|
{/* Crossing Space Shuttle Rocket */}
|
||||||
<div className="absolute inset-0 bg-[radial-gradient(#334155_1px,transparent_1px)] [background-size:20px_20px] opacity-25" />
|
<div className="absolute top-0 left-0 animate-spaceship">
|
||||||
<div className="absolute top-1/4 left-10 w-96 h-96 bg-cyan-500/20 rounded-full blur-[140px] pointer-events-none" />
|
<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">
|
||||||
<div className="absolute bottom-10 right-10 w-96 h-96 bg-blue-600/20 rounded-full blur-[140px] pointer-events-none" />
|
<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 */}
|
<defs>
|
||||||
<div className="relative z-10 flex items-center justify-between">
|
<linearGradient id="plasmaFlame" x1="30" y1="78" x2="10" y2="98" gradientUnits="userSpaceOnUse">
|
||||||
<div className="flex items-center gap-3">
|
<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>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 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" />
|
||||||
|
|
||||||
|
{/* Back Link Button */}
|
||||||
|
<Link to="/">
|
||||||
|
<Button variant="outline" size="sm" leftIcon={<FiArrowLeft />}>
|
||||||
|
Volver al inicio
|
||||||
|
</Button>
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 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">
|
||||||
|
|
||||||
|
{/* 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" />
|
||||||
|
|
||||||
|
{/* 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} />
|
<BrandLogo size="lg" showSubtitle={true} />
|
||||||
</div>
|
</div>
|
||||||
|
<h1 className="text-2xl sm:text-3xl font-bold tracking-tight pt-2 text-slate-900 dark:text-white">
|
||||||
<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">
|
Iniciar Sesión
|
||||||
<FiCheckCircle className="text-emerald-400" />
|
|
||||||
<span>+1,200 Cursos Disponibles</span>
|
|
||||||
</div>
|
|
||||||
</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 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>
|
|
||||||
|
|
||||||
<div className="hidden lg:block" />
|
|
||||||
|
|
||||||
<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>
|
|
||||||
|
|
||||||
{/* 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
|
|
||||||
</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>
|
</h1>
|
||||||
<p className="text-xs sm:text-sm text-slate-400">
|
<p className="text-xs sm:text-sm text-slate-600 dark:text-slate-400">
|
||||||
Ingresa tus credenciales para continuar tu ruta de aprendizaje
|
Ingresa tus credenciales para acceder a tu cuenta.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Quick Demo Access Bar */}
|
{/* Form */}
|
||||||
<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 onSubmit={handleSubmit} className="space-y-4">
|
<form onSubmit={handleSubmit} className="space-y-4">
|
||||||
|
|
||||||
{/* Email field */}
|
{/* Email field using UI Input */}
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-xs font-semibold text-slate-300 mb-1.5">
|
<Input
|
||||||
Correo Electrónico
|
label="Correo electrónico"
|
||||||
</label>
|
|
||||||
<div className="relative">
|
|
||||||
<FiMail className="absolute left-3.5 top-3.5 text-slate-500 text-base" />
|
|
||||||
<input
|
|
||||||
type="email"
|
type="email"
|
||||||
required
|
required
|
||||||
value={email}
|
value={email}
|
||||||
onChange={(e) => setEmail(e.target.value)}
|
onChange={(e) => setEmail(e.target.value)}
|
||||||
placeholder="estudiante@ejemplo.com"
|
placeholder="nombre@empresa.com"
|
||||||
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"
|
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>
|
</div>
|
||||||
|
|
||||||
{/* Password field */}
|
{/* Password field using UI Input (built-in password eye toggle) */}
|
||||||
<div>
|
<div>
|
||||||
<div className="flex items-center justify-between mb-1.5">
|
<Input
|
||||||
<label className="block text-xs font-semibold text-slate-300">
|
label="Contraseña"
|
||||||
Contraseña
|
type="password"
|
||||||
</label>
|
|
||||||
<a href="#" className="text-[11px] font-medium text-cyan-400 hover:text-cyan-300 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
|
required
|
||||||
value={password}
|
value={password}
|
||||||
onChange={(e) => setPassword(e.target.value)}
|
onChange={(e) => setPassword(e.target.value)}
|
||||||
placeholder="••••••••"
|
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"
|
leftIcon={<FiLock className="text-slate-400" />}
|
||||||
/>
|
/>
|
||||||
<button
|
<div className="flex justify-end mt-1">
|
||||||
type="button"
|
<a href="#" className="text-[11px] font-medium text-cyan-600 dark:text-cyan-400 hover:underline transition">
|
||||||
onClick={() => setShowPassword(!showPassword)}
|
¿Olvidaste tu contraseña?
|
||||||
className="absolute right-3.5 top-3.5 text-slate-500 hover:text-slate-300 transition"
|
</a>
|
||||||
>
|
|
||||||
{showPassword ? <FiEyeOff className="text-base" /> : <FiEye className="text-base" />}
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Remember Me */}
|
{/* Remember Me using UI Checkbox */}
|
||||||
<div className="flex items-center justify-between pt-1">
|
<div className="pt-0.5">
|
||||||
<label className="flex items-center gap-2 cursor-pointer text-xs text-slate-400 hover:text-slate-300">
|
<Checkbox
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
checked={rememberMe}
|
checked={rememberMe}
|
||||||
onChange={(e) => setRememberMe(e.target.checked)}
|
onChange={(e) => setRememberMe(e.target.checked)}
|
||||||
className="rounded border-slate-700 bg-slate-900 text-cyan-500 focus:ring-cyan-500/50"
|
label="Recordar sesión"
|
||||||
/>
|
/>
|
||||||
<span>Recordar este dispositivo</span>
|
|
||||||
</label>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Submit Button */}
|
{/* Submit Button using UI Button */}
|
||||||
<button
|
<Button
|
||||||
type="submit"
|
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>
|
Acceder a la cuenta
|
||||||
<FiArrowRight className="text-base group-hover:translate-x-1 transition-transform" />
|
</Button>
|
||||||
</button>
|
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
{/* Register Callout */}
|
{/* Register Callout */}
|
||||||
<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">
|
||||||
¿Aún no eres miembro de RyCourses?{' '}
|
¿Aún no tienes una cuenta?{' '}
|
||||||
<Link to="/register" className="text-cyan-400 font-bold hover:text-cyan-300 hover:underline transition">
|
<Link to="/register" className="text-cyan-600 dark:text-cyan-400 font-semibold hover:underline transition">
|
||||||
Regístrate gratis aquí
|
Regístrate aquí
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Footer info */}
|
{/* Footer info */}
|
||||||
<div className="text-center lg:text-left text-[11px] text-slate-600">
|
<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 Platform. Todos los derechos reservados.
|
© 2026 RyCourses. Todos los derechos reservados.
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
+139
-190
@@ -1,19 +1,20 @@
|
|||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import { useNavigate, Link } from 'react-router';
|
import { useNavigate, Link } from 'react-router';
|
||||||
import { BrandLogo } from '../components/common/BrandLogo';
|
import { BrandLogo } from '../components/common/BrandLogo';
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
Input,
|
||||||
|
Checkbox,
|
||||||
|
ThemeToggle,
|
||||||
|
Card
|
||||||
|
} from '../components/ui';
|
||||||
import {
|
import {
|
||||||
FiMail,
|
FiMail,
|
||||||
FiLock,
|
FiLock,
|
||||||
FiUser,
|
FiUser,
|
||||||
FiArrowRight,
|
FiArrowRight,
|
||||||
FiArrowLeft,
|
FiArrowLeft,
|
||||||
FiCheckCircle,
|
FiCheckCircle
|
||||||
FiStar,
|
|
||||||
FiBookOpen,
|
|
||||||
FiAward,
|
|
||||||
FiUsers,
|
|
||||||
FiEye,
|
|
||||||
FiEyeOff
|
|
||||||
} from 'react-icons/fi';
|
} from 'react-icons/fi';
|
||||||
import { FaGraduationCap, FaChalkboardTeacher } from 'react-icons/fa';
|
import { FaGraduationCap, FaChalkboardTeacher } from 'react-icons/fa';
|
||||||
import type { User } from '../types/course';
|
import type { User } from '../types/course';
|
||||||
@@ -25,288 +26,236 @@ interface RegisterViewProps {
|
|||||||
|
|
||||||
export const RegisterView: React.FC<RegisterViewProps> = ({ onShowToast, onLogin }) => {
|
export const RegisterView: React.FC<RegisterViewProps> = ({ onShowToast, onLogin }) => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
const [name, setName] = useState('');
|
const [name, setName] = useState('');
|
||||||
const [email, setEmail] = useState('');
|
const [email, setEmail] = useState('');
|
||||||
const [password, setPassword] = useState('');
|
const [password, setPassword] = useState('');
|
||||||
const [showPassword, setShowPassword] = useState(false);
|
|
||||||
const [accountType, setAccountType] = useState<'buyer' | 'seller'>('buyer');
|
const [accountType, setAccountType] = useState<'buyer' | 'seller'>('buyer');
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
|
||||||
|
const isValidEmail = email.includes('@') && email.includes('.');
|
||||||
|
|
||||||
const handleSubmit = (e: React.FormEvent) => {
|
const handleSubmit = (e: React.FormEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
if (name && email && password) {
|
if (!name || !email || !password) return;
|
||||||
|
|
||||||
|
setIsLoading(true);
|
||||||
|
setTimeout(() => {
|
||||||
const newUser: User = {
|
const newUser: User = {
|
||||||
id: Math.random().toString(36).substr(2, 9),
|
id: Math.random().toString(36).substring(2, 11),
|
||||||
name,
|
name,
|
||||||
email,
|
email,
|
||||||
role: accountType,
|
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);
|
onLogin(newUser);
|
||||||
onShowToast(`¡Cuenta de ${accountType === 'buyer' ? 'Estudiante' : 'Instructor'} creada con éxito!`);
|
onShowToast(`¡Cuenta de ${accountType === 'buyer' ? 'Estudiante' : 'Instructor'} creada con éxito!`);
|
||||||
|
setIsLoading(false);
|
||||||
navigate(accountType === 'seller' ? '/seller' : '/courses');
|
navigate(accountType === 'seller' ? '/seller' : '/courses');
|
||||||
}
|
}, 450);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
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 */}
|
{/* Top-Right Background Radial Gradient Glow */}
|
||||||
<div className="absolute -top-40 -left-40 w-96 h-96 bg-blue-600/15 rounded-full blur-[120px] pointer-events-none" />
|
<div
|
||||||
<div className="absolute top-1/2 right-1/3 w-80 h-80 bg-cyan-600/10 rounded-full blur-[140px] pointer-events-none" />
|
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%)'
|
||||||
{/* 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">
|
|
||||||
|
|
||||||
{/* 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" />
|
|
||||||
|
|
||||||
{/* 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>
|
|
||||||
</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 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>
|
<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" />
|
||||||
|
|
||||||
<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">
|
{/* Interactive Background Atmosphere: Twinkling Star Particles & Flying Space Shuttle */}
|
||||||
<FiStar className="text-yellow-400 fill-yellow-400" />
|
<div className="absolute inset-0 pointer-events-none overflow-hidden z-0">
|
||||||
<span>Aprende a Tu Propio Ritmo</span>
|
<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>
|
<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' }} />
|
||||||
|
|
||||||
<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">
|
{/* Crossing Space Shuttle Rocket */}
|
||||||
<div className="flex items-center justify-between">
|
<div className="absolute top-0 left-0 animate-spaceship">
|
||||||
<div className="flex items-center gap-3">
|
<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">
|
||||||
<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">
|
<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" />
|
||||||
</div>
|
<path d="M78 22C74 23 68 28 64 34L66 36C72 32 77 26 78 22Z" fill="#22d3ee" />
|
||||||
<div>
|
<path d="M34 58L16 64L24 50L30 52Z" fill="#0284c7" stroke="#38bdf8" strokeWidth="1.5" strokeLinejoin="round" />
|
||||||
<p className="text-xs font-bold text-white tracking-tight">
|
<path d="M42 66L48 84L50 76L46 70Z" fill="#0284c7" stroke="#38bdf8" strokeWidth="1.5" strokeLinejoin="round" />
|
||||||
Obtén certificados con reconocimiento internacional
|
<circle cx="58" cy="42" r="5" fill="#0f172a" stroke="#38bdf8" strokeWidth="1.5" />
|
||||||
</p>
|
<circle cx="58" cy="42" r="3.5" fill="#22d3ee" />
|
||||||
<p className="text-[11px] text-slate-400">
|
<circle cx="59.5" cy="40.5" r="1" fill="#ffffff" />
|
||||||
Proyectos prácticos para enriquecer tu portafolio profesional.
|
<circle cx="48" cy="52" r="4" fill="#0f172a" stroke="#38bdf8" strokeWidth="1.5" />
|
||||||
</p>
|
<circle cx="48" cy="52" r="2.5" fill="#22d3ee" />
|
||||||
</div>
|
|
||||||
</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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
{/* Top Navigation Bar */}
|
||||||
</div>
|
<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" />
|
||||||
|
|
||||||
{/* Feature Highlights Grid */}
|
{/* Back Link Button */}
|
||||||
<div className="relative z-10 grid grid-cols-3 gap-4 pt-4 border-t border-slate-800/60">
|
<Link to="/">
|
||||||
<div className="flex items-center gap-3 p-3 rounded-2xl bg-slate-900/60 border border-slate-800/60">
|
<Button variant="outline" size="sm" leftIcon={<FiArrowLeft />}>
|
||||||
<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">
|
Volver al inicio
|
||||||
<FiBookOpen className="text-base" />
|
</Button>
|
||||||
</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 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>
|
|
||||||
|
|
||||||
<div className="hidden lg:block" />
|
|
||||||
|
|
||||||
<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>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Form Body Container */}
|
{/* Main Centered Register Card Container */}
|
||||||
<div className="my-auto py-8 max-w-md w-full mx-auto space-y-6">
|
<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">
|
||||||
|
|
||||||
{/* Titles */}
|
{/* Subtle Top Border Gradient Accent */}
|
||||||
<div className="space-y-2">
|
<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" />
|
||||||
<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" />
|
{/* Centered Brand Logo & Header Titles inside Form Card */}
|
||||||
Únete a RyCourses
|
<div className="text-center space-y-2 pt-1">
|
||||||
|
<div className="flex justify-center pb-1">
|
||||||
|
<BrandLogo size="lg" showSubtitle={true} />
|
||||||
</div>
|
</div>
|
||||||
<h1 className="text-3xl sm:text-4xl font-extrabold text-white tracking-tight">
|
<h1 className="text-2xl sm:text-3xl font-bold tracking-tight pt-2 text-slate-900 dark:text-white">
|
||||||
Crear Cuenta Gratis ✨
|
Crear Cuenta
|
||||||
</h1>
|
</h1>
|
||||||
<p className="text-xs sm:text-sm text-slate-400">
|
<p className="text-xs sm:text-sm text-slate-600 dark:text-slate-400">
|
||||||
Comienza a aprender o enseñar hoy mismo en la plataforma líder.
|
Crea tu perfil en RyCourses para comenzar tu experiencia.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Account type switcher */}
|
{/* 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
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => setAccountType('buyer')}
|
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'
|
accountType === 'buyer'
|
||||||
? 'bg-gradient-to-r from-blue-600 to-cyan-500 text-white shadow-lg shadow-cyan-500/20'
|
? '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-400 hover:text-white'
|
: '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>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => setAccountType('seller')}
|
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'
|
accountType === 'seller'
|
||||||
? 'bg-gradient-to-r from-violet-600 to-purple-500 text-white shadow-lg shadow-purple-500/20'
|
? '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-400 hover:text-white'
|
: '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>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Form */}
|
{/* Form */}
|
||||||
<form onSubmit={handleSubmit} className="space-y-3.5">
|
<form onSubmit={handleSubmit} className="space-y-3.5">
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-xs font-semibold text-slate-300 mb-1">
|
<Input
|
||||||
Nombre Completo
|
label="Nombre Completo"
|
||||||
</label>
|
|
||||||
<div className="relative">
|
|
||||||
<FiUser className="absolute left-3.5 top-3.5 text-slate-500 text-base" />
|
|
||||||
<input
|
|
||||||
type="text"
|
type="text"
|
||||||
required
|
required
|
||||||
value={name}
|
value={name}
|
||||||
onChange={(e) => setName(e.target.value)}
|
onChange={(e) => setName(e.target.value)}
|
||||||
placeholder="Ej. Sofía Rodríguez"
|
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"
|
leftIcon={<FiUser className="text-slate-400" />}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-xs font-semibold text-slate-300 mb-1">
|
<Input
|
||||||
Correo Electrónico
|
label="Correo Electrónico"
|
||||||
</label>
|
|
||||||
<div className="relative">
|
|
||||||
<FiMail className="absolute left-3.5 top-3.5 text-slate-500 text-base" />
|
|
||||||
<input
|
|
||||||
type="email"
|
type="email"
|
||||||
required
|
required
|
||||||
value={email}
|
value={email}
|
||||||
onChange={(e) => setEmail(e.target.value)}
|
onChange={(e) => setEmail(e.target.value)}
|
||||||
placeholder="[email protected]"
|
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"
|
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>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-xs font-semibold text-slate-300 mb-1">
|
<Input
|
||||||
Contraseña
|
label="Contraseña"
|
||||||
</label>
|
type="password"
|
||||||
<div className="relative">
|
|
||||||
<FiLock className="absolute left-3.5 top-3.5 text-slate-500 text-base" />
|
|
||||||
<input
|
|
||||||
type={showPassword ? 'text' : 'password'}
|
|
||||||
required
|
required
|
||||||
value={password}
|
value={password}
|
||||||
onChange={(e) => setPassword(e.target.value)}
|
onChange={(e) => setPassword(e.target.value)}
|
||||||
placeholder="Mínimo 8 caracteres"
|
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"
|
leftIcon={<FiLock className="text-slate-400" />}
|
||||||
/>
|
/>
|
||||||
<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>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center gap-2 pt-1 text-xs text-slate-400">
|
<div className="pt-0.5">
|
||||||
<FiCheckCircle className="text-emerald-400 shrink-0" />
|
<Checkbox
|
||||||
<span>Acepto los Términos y Políticas de Privacidad.</span>
|
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>
|
</div>
|
||||||
|
|
||||||
<button
|
<Button
|
||||||
type="submit"
|
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>
|
Crear cuenta como {accountType === 'buyer' ? 'Estudiante' : 'Instructor'}
|
||||||
<FiArrowRight className="text-base group-hover:translate-x-1 transition-transform" />
|
</Button>
|
||||||
</button>
|
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
{/* Login Link */}
|
{/* 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?{' '}
|
¿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í
|
Inicia sesión aquí
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Footer info */}
|
{/* Footer info */}
|
||||||
<div className="text-center lg:text-left text-[11px] text-slate-600">
|
<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 Platform. Todos los derechos reservados.
|
© 2026 RyCourses. Todos los derechos reservados.
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
+128
-22
@@ -1,23 +1,37 @@
|
|||||||
import React from 'react';
|
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 { SellerHero } from '../components/seller/SellerHero';
|
||||||
import { SellerDashboard } from '../components/seller/SellerDashboard';
|
|
||||||
import { SellerCourseList } from '../components/seller/SellerCourseList';
|
|
||||||
import { SellerCalculator } from '../components/seller/SellerCalculator';
|
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 {
|
interface SellerViewProps {
|
||||||
|
currentUser: User | null;
|
||||||
courses: Course[];
|
courses: Course[];
|
||||||
onOpenCreateCourse: () => void;
|
|
||||||
onPreviewCourse: (course: Course) => void;
|
onPreviewCourse: (course: Course) => void;
|
||||||
onDeleteCourse: (courseId: string) => void;
|
onDeleteCourse: (courseId: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const SellerView: React.FC<SellerViewProps> = ({
|
export const SellerView: React.FC<SellerViewProps> = ({
|
||||||
courses,
|
currentUser,
|
||||||
onOpenCreateCourse,
|
|
||||||
onPreviewCourse,
|
|
||||||
onDeleteCourse,
|
|
||||||
}) => {
|
}) => {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
const handleCreateCourseClick = () => {
|
||||||
|
if (!currentUser) {
|
||||||
|
navigate('/login');
|
||||||
|
} else {
|
||||||
|
navigate('/seller/create');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const handleScrollToCalculator = () => {
|
const handleScrollToCalculator = () => {
|
||||||
const el = document.getElementById('seller-calculator');
|
const el = document.getElementById('seller-calculator');
|
||||||
if (el) {
|
if (el) {
|
||||||
@@ -26,28 +40,120 @@ export const SellerView: React.FC<SellerViewProps> = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-0">
|
<div className="space-y-16 pb-20 selection:bg-cyan-500">
|
||||||
{/* Seller Hero Section */}
|
|
||||||
|
{/* 1. Hero Landing Section */}
|
||||||
<SellerHero
|
<SellerHero
|
||||||
onOpenCreateCourse={onOpenCreateCourse}
|
onOpenCreateCourse={handleCreateCourseClick}
|
||||||
onScrollToCalculator={handleScrollToCalculator}
|
onScrollToCalculator={handleScrollToCalculator}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Instructor Dashboard Stats */}
|
{/* 2. Direct Navigation CTA to Teacher Portal */}
|
||||||
<SellerDashboard />
|
<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 */}
|
<Link to={currentUser ? "/teachers" : "/login"}>
|
||||||
<SellerCourseList
|
<Button variant="gradient" size="lg" rightIcon={<FiArrowRight />}>
|
||||||
courses={courses}
|
{currentUser ? "Ir a mi Panel Docente" : "Iniciar Sesión como Instructor"}
|
||||||
onOpenCreateModal={onOpenCreateCourse}
|
</Button>
|
||||||
onPreviewCourse={onPreviewCourse}
|
</Link>
|
||||||
onDeleteCourse={onDeleteCourse}
|
</div>
|
||||||
/>
|
</div>
|
||||||
|
|
||||||
{/* Revenue Calculator */}
|
{/* 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
|
<SellerCalculator
|
||||||
onStartCourse={onOpenCreateCourse}
|
onStartCourse={handleCreateCourseClick}
|
||||||
/>
|
/>
|
||||||
</div>
|
</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>
|
||||||
|
|
||||||
|
</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