dashboard con sidebar para el instructor
This commit is contained in:
1 parent
dd43fa04c1
commit
8ea93d67f9
9 files changed
+1282
-6
No files matched your search
@@ -86,7 +86,7 @@ export default function HomeContent() {
|
||||
placeholder="Buscar cursos..."
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
className={`w-full pl-9 pr-4 py-2.5 text-sm rounded-xl bg-transparent outline-none ${isDark ? "text-white placeholder:text-slate-500" : "text-slate-800 placeholder:text-slate-400"}`}
|
||||
className={`w-full pl-9 pr-4 py-2.5 text-sm rounded-xl bg-transparent outline-none ${isDark ? "text-white" : "text-slate-800"}`}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -179,7 +179,7 @@ export default function HomeContent() {
|
||||
className={`flex items-center gap-1 ${isDark ? "text-slate-400" : "text-slate-500"}`}
|
||||
>
|
||||
<FiUsers className="text-violet-400" />
|
||||
{inst.totalStudents.toLocaleString()} alumnos
|
||||
{inst.totalStudents.toString()} alumnos
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
import React from "react";
|
||||
import { FiBarChart2, FiDownload } from "react-icons/fi";
|
||||
|
||||
export default function AnalyticsPage() {
|
||||
return (
|
||||
<>
|
||||
<div className="max-w-7xl mx-auto space-y-8 duration-700">
|
||||
<div className="flex flex-col sm:flex-row justify-between items-start sm:items-center gap-4">
|
||||
<div>
|
||||
<h2 className="text-2xl font-bold text-slate-900 dark:text-white">Ganancias y Pagos</h2>
|
||||
</div>
|
||||
<button className="flex items-center gap-2 px-4 py-2 bg-white dark:bg-slate-800 hover:bg-slate-50 dark:hover:bg-slate-700 text-slate-900 dark:text-white border border-slate-200 dark:border-slate-700 font-medium rounded-xl transition-colors shadow-sm">
|
||||
<FiDownload /> Descargar Reporte
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col md:flex-row justify-evenly ">
|
||||
<div className="w-full md:w-1/3 min-w-70 bg-white dark:bg-slate-900/50 border border-slate-200 dark:border-slate-800 rounded-3xl p-6 shadow-sm dark:shadow-none">
|
||||
<p className="text-slate-500 dark:text-slate-400 font-medium mb-1">Ingresos este mes</p>
|
||||
<h3 className="text-3xl font-bold text-slate-900 dark:text-white mb-2">$1,240.00</h3>
|
||||
<p className="text-sm text-emerald-600 dark:text-emerald-400 font-medium">+15% respecto al mes anterior</p>
|
||||
</div>
|
||||
|
||||
<div className="w-full md:w-1/3 min-w-70 bg-white dark:bg-slate-900/50 border border-slate-200 dark:border-slate-800 rounded-3xl p-6 shadow-sm dark:shadow-none">
|
||||
<p className="text-slate-500 dark:text-slate-400 font-medium mb-1">Próximo Pago</p>
|
||||
<h3 className="text-3xl font-bold text-slate-900 dark:text-white mb-2">15 de Sep</h3>
|
||||
<p className="text-sm text-slate-500 dark:text-slate-400">Transferencia bancaria terminada en 4021</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h2 className="text-2xl font-bold text-slate-900 dark:text-white">Analíticas</h2>
|
||||
<p className="text-slate-500 dark:text-slate-400 mt-1">Métricas detalladas sobre el rendimiento de tus cursos.</p>
|
||||
</div>
|
||||
|
||||
<div className="bg-white dark:bg-slate-900/50 border border-slate-200 dark:border-slate-800 rounded-3xl p-8 shadow-sm dark:shadow-none flex flex-col items-center justify-center min-h-100 text-center">
|
||||
<div className="w-20 h-20 bg-indigo-50 dark:bg-indigo-500/10 rounded-full flex items-center justify-center mb-6">
|
||||
<FiBarChart2 className="text-3xl text-indigo-600 dark:text-indigo-400" />
|
||||
</div>
|
||||
<h3 className="text-xl font-bold text-slate-900 dark:text-white mb-2">Página de Analíticas en Construcción</h3>
|
||||
<p className="text-slate-500 dark:text-slate-400 max-w-md">
|
||||
Pronto podrás visualizar gráficos interactivos, retención de estudiantes y métricas detalladas aquí.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,582 @@
|
||||
"use client";
|
||||
|
||||
import React, { useState } from "react";
|
||||
import Link from "next/link";
|
||||
import {
|
||||
FiArrowLeft,
|
||||
FiUsers,
|
||||
FiPlayCircle,
|
||||
FiMessageSquare,
|
||||
FiStar,
|
||||
FiBookOpen,
|
||||
FiVideo,
|
||||
FiFileText,
|
||||
FiChevronDown,
|
||||
FiChevronRight,
|
||||
FiSend,
|
||||
FiCheck,
|
||||
} from "react-icons/fi";
|
||||
|
||||
// ── Mock data ─────────────────────────────────────────────────────────────────
|
||||
|
||||
const coursesData: Record<
|
||||
string,
|
||||
{
|
||||
id: number;
|
||||
title: string;
|
||||
status: string;
|
||||
price: string;
|
||||
rating: number;
|
||||
totalReviews: number;
|
||||
students: {
|
||||
id: number;
|
||||
name: string;
|
||||
email: string;
|
||||
progress: number;
|
||||
purchasedAt: string;
|
||||
avatar: string;
|
||||
}[];
|
||||
sections: {
|
||||
id: number;
|
||||
title: string;
|
||||
lessons: { id: number; title: string; type: "video" | "article"; duration: string }[];
|
||||
}[];
|
||||
messages: {
|
||||
id: number;
|
||||
student: string;
|
||||
avatar: string;
|
||||
message: string;
|
||||
date: string;
|
||||
replied: boolean;
|
||||
}[];
|
||||
reviews: {
|
||||
id: number;
|
||||
student: string;
|
||||
avatar: string;
|
||||
rating: number;
|
||||
comment: string;
|
||||
date: string;
|
||||
}[];
|
||||
}
|
||||
> = {
|
||||
"1": {
|
||||
id: 1,
|
||||
title: "Desarrollo Web Fullstack con Next.js",
|
||||
status: "Publicado",
|
||||
price: "$49.99",
|
||||
rating: 4.7,
|
||||
totalReviews: 128,
|
||||
students: [
|
||||
{ id: 1, name: "Ana García", email: "[email protected]", progress: 85, purchasedAt: "12 Ago 2026", avatar: "AG" },
|
||||
{ id: 2, name: "Carlos López", email: "[email protected]", progress: 42, purchasedAt: "18 Ago 2026", avatar: "CL" },
|
||||
{ id: 3, name: "María Rodríguez", email: "[email protected]", progress: 100, purchasedAt: "5 Jul 2026", avatar: "MR" },
|
||||
{ id: 4, name: "Juan Martínez", email: "[email protected]", progress: 15, purchasedAt: "27 Ago 2026", avatar: "JM" },
|
||||
{ id: 5, name: "Laura Sánchez", email: "[email protected]", progress: 63, purchasedAt: "1 Sep 2026", avatar: "LS" },
|
||||
],
|
||||
sections: [
|
||||
{
|
||||
id: 1,
|
||||
title: "Introducción al curso",
|
||||
lessons: [
|
||||
{ id: 1, title: "Bienvenida y configuración del entorno", type: "video", duration: "8:24" },
|
||||
{ id: 2, title: "¿Qué aprenderemos?", type: "article", duration: "3 min" },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
title: "Fundamentos de Next.js",
|
||||
lessons: [
|
||||
{ id: 3, title: "App Router vs Pages Router", type: "video", duration: "14:10" },
|
||||
{ id: 4, title: "Server y Client Components", type: "video", duration: "18:45" },
|
||||
{ id: 5, title: "Rutas dinámicas", type: "video", duration: "12:30" },
|
||||
{ id: 6, title: "Resumen de fundamentos", type: "article", duration: "5 min" },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
title: "Base de datos con Prisma",
|
||||
lessons: [
|
||||
{ id: 7, title: "Instalación y schema", type: "video", duration: "10:15" },
|
||||
{ id: 8, title: "Migraciones", type: "video", duration: "9:50" },
|
||||
{ id: 9, title: "CRUD completo", type: "video", duration: "22:00" },
|
||||
],
|
||||
},
|
||||
],
|
||||
messages: [
|
||||
{ id: 1, student: "Carlos López", avatar: "CL", message: "Hola, tengo un error al configurar Prisma con PostgreSQL. El schema no migra correctamente.", date: "Hace 2 horas", replied: false },
|
||||
{ id: 2, student: "Juan Martínez", avatar: "JM", message: "¿Podrías subir el código fuente de la sección 3? No encuentro el repositorio.", date: "Hace 5 horas", replied: true },
|
||||
{ id: 3, student: "Ana García", avatar: "AG", message: "Excelente curso, aprendí muchísimo. ¿Planeas hacer una parte 2?", date: "Ayer", replied: false },
|
||||
],
|
||||
reviews: [
|
||||
{ id: 1, student: "María Rodríguez", avatar: "MR", rating: 5, comment: "El mejor curso de Next.js que he tomado. Muy claro y completo.", date: "28 Ago 2026" },
|
||||
{ id: 2, student: "Ana García", avatar: "AG", rating: 5, comment: "Increíble. El instructor explica muy bien cada concepto.", date: "25 Ago 2026" },
|
||||
{ id: 3, student: "Carlos López", avatar: "CL", rating: 4, comment: "Muy buen contenido. Me gustaría más ejercicios prácticos.", date: "20 Ago 2026" },
|
||||
],
|
||||
},
|
||||
"2": {
|
||||
id: 2,
|
||||
title: "Masterclass de UI/UX Design",
|
||||
status: "Publicado",
|
||||
price: "$39.99",
|
||||
rating: 4.5,
|
||||
totalReviews: 89,
|
||||
students: [
|
||||
{ id: 1, name: "Laura Sánchez", email: "[email protected]", progress: 70, purchasedAt: "10 Ago 2026", avatar: "LS" },
|
||||
{ id: 2, name: "Pedro Ruiz", email: "[email protected]", progress: 30, purchasedAt: "20 Ago 2026", avatar: "PR" },
|
||||
{ id: 3, name: "Sofía Torres", email: "[email protected]", progress: 90, purchasedAt: "3 Ago 2026", avatar: "ST" },
|
||||
],
|
||||
sections: [
|
||||
{
|
||||
id: 1,
|
||||
title: "Principios de diseño",
|
||||
lessons: [
|
||||
{ id: 1, title: "Color, tipografía y espacio", type: "video", duration: "15:00" },
|
||||
{ id: 2, title: "Sistemas de diseño", type: "article", duration: "6 min" },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
title: "Figma avanzado",
|
||||
lessons: [
|
||||
{ id: 3, title: "Auto-layout y componentes", type: "video", duration: "20:00" },
|
||||
{ id: 4, title: "Prototipado interactivo", type: "video", duration: "17:30" },
|
||||
],
|
||||
},
|
||||
],
|
||||
messages: [
|
||||
{ id: 1, student: "Pedro Ruiz", avatar: "PR", message: "¿Hay alguna plantilla de Figma disponible para descargar?", date: "Hace 1 día", replied: false },
|
||||
],
|
||||
reviews: [
|
||||
{ id: 1, student: "Sofía Torres", avatar: "ST", rating: 5, comment: "Transformó por completo mi forma de diseñar.", date: "22 Ago 2026" },
|
||||
{ id: 2, student: "Laura Sánchez", avatar: "LS", rating: 4, comment: "Muy buenas explicaciones de Figma.", date: "18 Ago 2026" },
|
||||
],
|
||||
},
|
||||
"3": {
|
||||
id: 3,
|
||||
title: "Python para Data Science",
|
||||
status: "Borrador",
|
||||
price: "$59.99",
|
||||
rating: 0,
|
||||
totalReviews: 0,
|
||||
students: [],
|
||||
sections: [
|
||||
{
|
||||
id: 1,
|
||||
title: "Introducción a Python",
|
||||
lessons: [
|
||||
{ id: 1, title: "Variables y tipos de datos", type: "video", duration: "12:00" },
|
||||
{ id: 2, title: "Funciones y módulos", type: "video", duration: "10:30" },
|
||||
],
|
||||
},
|
||||
],
|
||||
messages: [],
|
||||
reviews: [],
|
||||
},
|
||||
};
|
||||
|
||||
// ── Avatar component ──────────────────────────────────────────────────────────
|
||||
|
||||
const avatarColors = [
|
||||
"from-violet-500 to-indigo-500",
|
||||
"from-emerald-500 to-teal-500",
|
||||
"from-orange-500 to-amber-500",
|
||||
"from-pink-500 to-rose-500",
|
||||
"from-cyan-500 to-blue-500",
|
||||
];
|
||||
|
||||
function Avatar({ initials, index = 0, size = "md" }: { initials: string; index?: number; size?: "sm" | "md" }) {
|
||||
const sz = size === "sm" ? "w-8 h-8 text-xs" : "w-10 h-10 text-sm";
|
||||
return (
|
||||
<div
|
||||
className={`${sz} rounded-full bg-linear-to-tr ${avatarColors[index % avatarColors.length]} flex items-center justify-center font-bold text-white shrink-0`}
|
||||
>
|
||||
{initials}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ── Stars component ───────────────────────────────────────────────────────────
|
||||
|
||||
function Stars({ rating, size = "sm" }: { rating: number; size?: "sm" | "lg" }) {
|
||||
const sz = size === "lg" ? "text-xl" : "text-sm";
|
||||
return (
|
||||
<div className={`flex gap-0.5 ${sz}`}>
|
||||
{[1, 2, 3, 4, 5].map((s) => (
|
||||
<FiStar
|
||||
key={s}
|
||||
className={s <= Math.round(rating) ? "text-amber-400 fill-amber-400" : "text-slate-300 dark:text-slate-600"}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ── Tab: Estudiantes ──────────────────────────────────────────────────────────
|
||||
|
||||
function StudentsTab({ students }: { students: (typeof coursesData)["1"]["students"] }) {
|
||||
if (students.length === 0) {
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center py-20 text-slate-400">
|
||||
<FiUsers className="text-5xl mb-4 opacity-30" />
|
||||
<p className="font-medium">Aún no hay estudiantes inscritos</p>
|
||||
<p className="text-sm mt-1 opacity-70">Aparecerán aquí cuando publiques el curso</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="bg-white dark:bg-slate-900/50 border border-slate-200 dark:border-slate-800 rounded-lg overflow-hidden">
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full text-left">
|
||||
<thead>
|
||||
<tr className="bg-slate-50 dark:bg-slate-800/50 border-b border-slate-200 dark:border-slate-800">
|
||||
<th className="px-6 py-4 text-xs font-semibold text-slate-500 dark:text-slate-400 uppercase tracking-wider">Estudiante</th>
|
||||
<th className="px-6 py-4 text-xs font-semibold text-slate-500 dark:text-slate-400 uppercase tracking-wider">Progreso</th>
|
||||
<th className="px-6 py-4 text-xs font-semibold text-slate-500 dark:text-slate-400 uppercase tracking-wider">Comprado</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-slate-100 dark:divide-slate-800/50">
|
||||
{students.map((s, i) => (
|
||||
<tr key={s.id} className="hover:bg-slate-50 dark:hover:bg-slate-800/30 transition-colors">
|
||||
<td className="px-6 py-4">
|
||||
<div className="flex items-center gap-3">
|
||||
<Avatar initials={s.avatar} index={i} />
|
||||
<div>
|
||||
<p className="font-medium text-slate-900 dark:text-white text-sm">{s.name}</p>
|
||||
<p className="text-xs text-slate-500">{s.email}</p>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-6 py-4">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-32 h-1.5 bg-slate-200 dark:bg-slate-700 rounded-full overflow-hidden">
|
||||
<div
|
||||
className={`h-full rounded-full ${s.progress === 100 ? "bg-emerald-500" : "bg-indigo-500"}`}
|
||||
style={{ width: `${s.progress}%` }}
|
||||
/>
|
||||
</div>
|
||||
<span className={`text-xs font-semibold ${s.progress === 100 ? "text-emerald-500" : "text-slate-600 dark:text-slate-300"}`}>
|
||||
{s.progress}%
|
||||
</span>
|
||||
{s.progress === 100 && <FiCheck className="text-emerald-500 text-xs" />}
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-6 py-4 text-sm text-slate-500 dark:text-slate-400">{s.purchasedAt}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ── Tab: Contenido ────────────────────────────────────────────────────────────
|
||||
|
||||
function ContentTab({ sections }: { sections: (typeof coursesData)["1"]["sections"] }) {
|
||||
const [expanded, setExpanded] = useState<number[]>([1]);
|
||||
|
||||
const toggle = (id: number) =>
|
||||
setExpanded((prev) => (prev.includes(id) ? prev.filter((x) => x !== id) : [...prev, id]));
|
||||
|
||||
const totalLessons = sections.reduce((a, s) => a + s.lessons.length, 0);
|
||||
|
||||
return (
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<p className="text-sm text-slate-500 dark:text-slate-400">
|
||||
{sections.length} secciones · {totalLessons} lecciones
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{sections.map((section) => {
|
||||
const isOpen = expanded.includes(section.id);
|
||||
return (
|
||||
<div key={section.id} className="bg-white dark:bg-slate-900/50 border border-slate-200 dark:border-slate-800 rounded-lg overflow-hidden">
|
||||
<button
|
||||
onClick={() => toggle(section.id)}
|
||||
className="w-full flex items-center justify-between px-6 py-4 text-left hover:bg-slate-50 dark:hover:bg-slate-800/30 transition-colors"
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-8 h-8 rounded-lg bg-indigo-100 dark:bg-indigo-500/10 flex items-center justify-center">
|
||||
<span className="text-xs font-bold text-indigo-600 dark:text-indigo-400">{section.id}</span>
|
||||
</div>
|
||||
<span className="font-semibold text-slate-900 dark:text-white">{section.title}</span>
|
||||
<span className="text-xs text-slate-500">({section.lessons.length} lecciones)</span>
|
||||
</div>
|
||||
{isOpen ? (
|
||||
<FiChevronDown className="text-slate-400 shrink-0" />
|
||||
) : (
|
||||
<FiChevronRight className="text-slate-400 shrink-0" />
|
||||
)}
|
||||
</button>
|
||||
|
||||
{isOpen && (
|
||||
<div className="border-t border-slate-100 dark:border-slate-800 divide-y divide-slate-100 dark:divide-slate-800/50">
|
||||
{section.lessons.map((lesson) => (
|
||||
<div key={lesson.id} className="flex items-center gap-4 px-6 py-3 hover:bg-slate-50 dark:hover:bg-slate-800/20 transition-colors">
|
||||
<div className="w-7 h-7 rounded-md bg-slate-100 dark:bg-slate-800 flex items-center justify-center shrink-0">
|
||||
{lesson.type === "video" ? (
|
||||
<FiVideo className="text-xs text-indigo-500" />
|
||||
) : (
|
||||
<FiFileText className="text-xs text-emerald-500" />
|
||||
)}
|
||||
</div>
|
||||
<span className="text-sm text-slate-700 dark:text-slate-300 flex-1">{lesson.title}</span>
|
||||
<span className="text-xs text-slate-400">{lesson.duration}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ── Tab: Mensajes ─────────────────────────────────────────────────────────────
|
||||
|
||||
function MessagesTab({ messages }: { messages: (typeof coursesData)["1"]["messages"] }) {
|
||||
const [replyingTo, setReplyingTo] = useState<number | null>(null);
|
||||
const [replyText, setReplyText] = useState("");
|
||||
|
||||
if (messages.length === 0) {
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center py-20 text-slate-400">
|
||||
<FiMessageSquare className="text-5xl mb-4 opacity-30" />
|
||||
<p className="font-medium">No hay mensajes aún</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
{messages.map((msg, i) => (
|
||||
<div key={msg.id} className="bg-white dark:bg-slate-900/50 border border-slate-200 dark:border-slate-800 rounded-lg p-5">
|
||||
<div className="flex items-start gap-4">
|
||||
<Avatar initials={msg.avatar} index={i} />
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center justify-between gap-2 flex-wrap">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="font-semibold text-slate-900 dark:text-white text-sm">{msg.student}</span>
|
||||
{msg.replied ? (
|
||||
<span className="px-2 py-0.5 text-[10px] font-medium rounded-full bg-emerald-100 text-emerald-700 dark:bg-emerald-500/10 dark:text-emerald-400">
|
||||
Respondido
|
||||
</span>
|
||||
) : (
|
||||
<span className="px-2 py-0.5 text-[10px] font-medium rounded-full bg-amber-100 text-amber-700 dark:bg-amber-500/10 dark:text-amber-400">
|
||||
Pendiente
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<span className="text-xs text-slate-400">{msg.date}</span>
|
||||
</div>
|
||||
<p className="text-sm text-slate-600 dark:text-slate-300 mt-2 leading-relaxed">{msg.message}</p>
|
||||
|
||||
{replyingTo === msg.id ? (
|
||||
<div className="mt-3 flex gap-2">
|
||||
<input
|
||||
value={replyText}
|
||||
onChange={(e) => setReplyText(e.target.value)}
|
||||
placeholder="Escribe tu respuesta..."
|
||||
className="flex-1 text-sm px-3 py-2 rounded-xl border border-slate-200 dark:border-slate-700 bg-slate-50 dark:bg-slate-800 text-slate-900 dark:text-white placeholder-slate-400 focus:outline-none focus:ring-2 focus:ring-indigo-500/40"
|
||||
/>
|
||||
<button
|
||||
onClick={() => { setReplyingTo(null); setReplyText(""); }}
|
||||
className="flex items-center gap-1.5 px-3 py-2 bg-indigo-600 hover:bg-indigo-700 text-white text-sm font-medium rounded-xl transition-colors"
|
||||
>
|
||||
<FiSend className="text-xs" /> Enviar
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
<button
|
||||
onClick={() => setReplyingTo(msg.id)}
|
||||
className="mt-2 text-xs font-medium text-indigo-600 dark:text-indigo-400 hover:text-indigo-700 dark:hover:text-indigo-300 transition-colors"
|
||||
>
|
||||
Responder
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ── Tab: Calificación ─────────────────────────────────────────────────────────
|
||||
|
||||
function RatingTab({ course }: { course: (typeof coursesData)["1"] }) {
|
||||
if (course.totalReviews === 0) {
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center py-20 text-slate-400">
|
||||
<FiStar className="text-5xl mb-4 opacity-30" />
|
||||
<p className="font-medium">Aún no hay calificaciones</p>
|
||||
<p className="text-sm mt-1 opacity-70">Aparecerán cuando los estudiantes dejen reseñas</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const starDist = [5, 4, 3, 2, 1].map((star) => ({
|
||||
star,
|
||||
count: course.reviews.filter((r) => r.rating === star).length,
|
||||
}));
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* Summary card */}
|
||||
<div className="bg-white dark:bg-slate-900/50 border border-slate-200 dark:border-slate-800 rounded-lg p-6">
|
||||
<div className="flex flex-col sm:flex-row items-center gap-8">
|
||||
<div className="text-center shrink-0">
|
||||
<p className="text-6xl font-black text-slate-900 dark:text-white">{course.rating.toFixed(1)}</p>
|
||||
<Stars rating={course.rating} size="lg" />
|
||||
<p className="text-sm text-slate-500 mt-2">{course.totalReviews} calificaciones</p>
|
||||
</div>
|
||||
<div className="flex-1 w-full space-y-2">
|
||||
{starDist.map(({ star, count }) => {
|
||||
const pct = course.totalReviews > 0 ? (count / course.reviews.length) * 100 : 0;
|
||||
return (
|
||||
<div key={star} className="flex items-center gap-3">
|
||||
<span className="text-xs text-slate-500 w-4 text-right">{star}</span>
|
||||
<FiStar className="text-amber-400 fill-amber-400 text-xs shrink-0" />
|
||||
<div className="flex-1 h-2 bg-slate-100 dark:bg-slate-800 rounded-full overflow-hidden">
|
||||
<div
|
||||
className="h-full bg-amber-400 rounded-full transition-all duration-500"
|
||||
style={{ width: `${pct}%` }}
|
||||
/>
|
||||
</div>
|
||||
<span className="text-xs text-slate-400 w-6">{count}</span>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Reviews */}
|
||||
<div className="space-y-3">
|
||||
{course.reviews.map((rev, i) => (
|
||||
<div key={rev.id} className="bg-white dark:bg-slate-900/50 border border-slate-200 dark:border-slate-800 rounded-lg p-5">
|
||||
<div className="flex items-start gap-4">
|
||||
<Avatar initials={rev.avatar} index={i} />
|
||||
<div className="flex-1">
|
||||
<div className="flex items-center justify-between flex-wrap gap-2">
|
||||
<span className="font-semibold text-slate-900 dark:text-white text-sm">{rev.student}</span>
|
||||
<span className="text-xs text-slate-400">{rev.date}</span>
|
||||
</div>
|
||||
<Stars rating={rev.rating} />
|
||||
<p className="text-sm text-slate-600 dark:text-slate-300 mt-2 leading-relaxed">{rev.comment}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ── Main page ─────────────────────────────────────────────────────────────────
|
||||
|
||||
const tabs = [
|
||||
{ id: "students", label: "Estudiantes", icon: FiUsers },
|
||||
{ id: "content", label: "Contenido", icon: FiPlayCircle },
|
||||
{ id: "messages", label: "Mensajes", icon: FiMessageSquare },
|
||||
{ id: "rating", label: "Calificación", icon: FiStar },
|
||||
];
|
||||
|
||||
export default function CourseDetailPage({ params }: { params: Promise<{ id: string }> }) {
|
||||
const { id } = React.use(params);
|
||||
const course = coursesData[id];
|
||||
const [activeTab, setActiveTab] = useState("students");
|
||||
|
||||
if (!course) {
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center py-32 text-slate-400">
|
||||
<FiBookOpen className="text-5xl mb-4 opacity-30" />
|
||||
<p className="font-medium text-lg">Curso no encontrado</p>
|
||||
<Link href="/instructor/courses" className="mt-4 text-indigo-500 hover:text-indigo-600 text-sm font-medium">
|
||||
← Volver a mis cursos
|
||||
</Link>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const pendingMessages = course.messages.filter((m) => !m.replied).length;
|
||||
|
||||
return (
|
||||
<div className="max-w-5xl mx-auto space-y-6">
|
||||
{/* Back + Header */}
|
||||
<div>
|
||||
<Link
|
||||
href="/instructor/courses"
|
||||
className="inline-flex items-center gap-2 text-sm text-slate-500 dark:text-slate-400 hover:text-indigo-600 dark:hover:text-indigo-400 transition-colors mb-4"
|
||||
>
|
||||
<FiArrowLeft /> Mis Cursos
|
||||
</Link>
|
||||
|
||||
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-4">
|
||||
<div>
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<span
|
||||
className={`px-2.5 py-1 text-xs font-medium rounded-full ${
|
||||
course.status === "Publicado"
|
||||
? "bg-emerald-100 text-emerald-700 dark:bg-emerald-500/10 dark:text-emerald-400"
|
||||
: "bg-amber-100 text-amber-700 dark:bg-amber-500/10 dark:text-amber-400"
|
||||
}`}
|
||||
>
|
||||
{course.status}
|
||||
</span>
|
||||
{course.rating > 0 && (
|
||||
<div className="flex items-center gap-1">
|
||||
<FiStar className="text-amber-400 fill-amber-400 text-xs" />
|
||||
<span className="text-sm font-semibold text-slate-700 dark:text-slate-300">{course.rating}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<h2 className="text-2xl font-bold text-slate-900 dark:text-white">{course.title}</h2>
|
||||
<p className="text-slate-500 dark:text-slate-400 text-sm mt-1">
|
||||
{course.students.length} estudiantes · {course.price}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Tabs */}
|
||||
<div className="flex gap-1 bg-slate-100 dark:bg-slate-800/50 p-1 rounded-lg w-fit">
|
||||
{tabs.map((tab) => {
|
||||
const Icon = tab.icon;
|
||||
const isActive = activeTab === tab.id;
|
||||
const badge = tab.id === "messages" && pendingMessages > 0 ? pendingMessages : null;
|
||||
return (
|
||||
<button
|
||||
key={tab.id}
|
||||
onClick={() => setActiveTab(tab.id)}
|
||||
className={`flex items-center gap-2 px-4 py-2 rounded-lg text-sm font-medium transition-all duration-200 ${
|
||||
isActive
|
||||
? "bg-white dark:bg-slate-900 text-indigo-600 dark:text-indigo-400 shadow-sm"
|
||||
: "text-slate-500 dark:text-slate-400 hover:text-slate-700 dark:hover:text-slate-300"
|
||||
}`}
|
||||
>
|
||||
<Icon className="text-base" />
|
||||
<span className="hidden sm:inline">{tab.label}</span>
|
||||
{badge && (
|
||||
<span className="w-4 h-4 rounded-full bg-amber-500 text-white text-[10px] font-bold flex items-center justify-center">
|
||||
{badge}
|
||||
</span>
|
||||
)}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
{/* Tab content */}
|
||||
<div>
|
||||
{activeTab === "students" && <StudentsTab students={course.students} />}
|
||||
{activeTab === "content" && <ContentTab sections={course.sections} />}
|
||||
{activeTab === "messages" && <MessagesTab messages={course.messages} />}
|
||||
{activeTab === "rating" && <RatingTab course={course} />}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
import React from "react";
|
||||
import Link from "next/link";
|
||||
import { FiPlus, FiBookOpen, FiUsers, FiStar } from "react-icons/fi";
|
||||
|
||||
const courses = [
|
||||
{ id: 1, title: "Desarrollo Web Fullstack con Next.js", status: "Publicado", students: 342, price: "$49.99", rating: 4.7 },
|
||||
{ id: 2, title: "Masterclass de UI/UX Design", status: "Publicado", students: 215, price: "$39.99", rating: 4.5 },
|
||||
{ id: 3, title: "Python para Data Science", status: "Borrador", students: 0, price: "$59.99", rating: 0 },
|
||||
];
|
||||
|
||||
export default function CoursesPage() {
|
||||
return (
|
||||
<div className="max-w-7xl mx-auto space-y-8 duration-700">
|
||||
<div className="flex flex-col sm:flex-row justify-between items-start sm:items-center gap-4">
|
||||
<div>
|
||||
<h2 className="text-2xl font-bold text-slate-900 dark:text-white">Mis Cursos</h2>
|
||||
<p className="text-slate-500 dark:text-slate-400 mt-1">Gestiona los cursos que has creado.</p>
|
||||
</div>
|
||||
<button className="flex items-center gap-2 px-4 py-2 bg-indigo-600 hover:bg-indigo-700 text-white font-medium rounded-lg transition-colors shadow-sm shadow-indigo-600/20">
|
||||
<FiPlus /> Nuevo Curso
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
{courses.map((course) => (
|
||||
<Link
|
||||
key={course.id}
|
||||
href={`/instructor/courses/${course.id}`}
|
||||
className="bg-white dark:bg-slate-900/50 border border-slate-200 dark:border-slate-800 rounded-lg overflow-hidden shadow-sm dark:shadow-none group transition-all hover:border-indigo-300 dark:hover:border-indigo-700 hover:shadow-md hover:-translate-y-0.5"
|
||||
>
|
||||
<div className="h-40 bg-slate-100 dark:bg-slate-800 flex items-center justify-center">
|
||||
<FiBookOpen className="text-4xl text-slate-300 dark:text-slate-600 group-hover:scale-110 group-hover:text-indigo-400 dark:group-hover:text-indigo-500 transition-all duration-300" />
|
||||
</div>
|
||||
<div className="p-6">
|
||||
<div className="flex items-center gap-2 mb-3">
|
||||
<span
|
||||
className={`px-2.5 py-1 text-xs font-medium rounded-lg ${
|
||||
course.status === "Publicado"
|
||||
? "bg-emerald-100 text-emerald-700 dark:bg-emerald-500/10 dark:text-emerald-400"
|
||||
: "bg-amber-100 text-amber-700 dark:bg-amber-500/10 dark:text-amber-400"
|
||||
}`}
|
||||
>
|
||||
{course.status}
|
||||
</span>
|
||||
{course.rating > 0 && (
|
||||
<div className="flex items-center gap-1 ml-auto">
|
||||
<FiStar className="text-amber-400 fill-amber-400 text-xs" />
|
||||
<span className="text-xs font-semibold text-slate-600 dark:text-slate-300">{course.rating}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<h3 className="font-semibold text-lg text-slate-900 dark:text-white mb-2 leading-tight line-clamp-2 group-hover:text-indigo-600 dark:group-hover:text-indigo-400 transition-colors">
|
||||
{course.title}
|
||||
</h3>
|
||||
<div className="flex items-center justify-between mt-4 pt-4 border-t border-slate-100 dark:border-slate-800">
|
||||
<div className="flex items-center gap-1.5 text-slate-500 dark:text-slate-400">
|
||||
<FiUsers className="text-xs" />
|
||||
<span className="text-sm">{course.students} estudiantes</span>
|
||||
</div>
|
||||
<span className="font-semibold text-slate-900 dark:text-white">{course.price}</span>
|
||||
</div>
|
||||
</div>
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,293 @@
|
||||
"use client";
|
||||
|
||||
import React, { useState, useRef, useEffect, useCallback } from "react";
|
||||
import { createPortal } from "react-dom";
|
||||
import Link from "next/link";
|
||||
import { usePathname } from "next/navigation";
|
||||
import {
|
||||
FiHome,
|
||||
FiBook,
|
||||
FiPieChart,
|
||||
FiSettings,
|
||||
FiLogOut,
|
||||
FiMenu,
|
||||
FiX,
|
||||
FiChevronDown,
|
||||
} from "react-icons/fi";
|
||||
import { ThemeToggle } from "@/components/ui";
|
||||
|
||||
const navigation = [
|
||||
{ name: "Dashboard", href: "/instructor", icon: FiHome },
|
||||
{ name: "Mis Cursos", href: "/instructor/courses", icon: FiBook },
|
||||
{ name: "Ingresos y Analíticas", href: "/instructor/analytics", icon: FiPieChart },
|
||||
];
|
||||
|
||||
// Bottom nav shows the most important items on mobile
|
||||
const bottomNavItems = [
|
||||
{ name: "Inicio", href: "/instructor", icon: FiHome },
|
||||
{ name: "Cursos", href: "/instructor/courses", icon: FiBook },
|
||||
{ name: "Ingresos y Analíticas", href: "/instructor/analytics", icon: FiPieChart },
|
||||
];
|
||||
|
||||
export default function InstructorLayout({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
const pathname = usePathname();
|
||||
const [sidebarOpen, setSidebarOpen] = useState(false);
|
||||
const [profileMenuOpen, setProfileMenuOpen] = useState(false);
|
||||
const [dropdownPos, setDropdownPos] = useState({ top: 0, right: 0 });
|
||||
const profileButtonRef = useRef<HTMLButtonElement>(null);
|
||||
const dropdownRef = useRef<HTMLDivElement>(null);
|
||||
const [mounted, setMounted] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
setMounted(true);
|
||||
}, []);
|
||||
|
||||
const handleProfileToggle = useCallback(() => {
|
||||
if (!profileMenuOpen && profileButtonRef.current) {
|
||||
const rect = profileButtonRef.current.getBoundingClientRect();
|
||||
setDropdownPos({
|
||||
top: rect.bottom + 8,
|
||||
right: window.innerWidth - rect.right,
|
||||
});
|
||||
}
|
||||
setProfileMenuOpen((prev) => !prev);
|
||||
}, [profileMenuOpen]);
|
||||
|
||||
useEffect(() => {
|
||||
function handleClickOutside(event: MouseEvent) {
|
||||
const target = event.target as Node;
|
||||
if (
|
||||
profileButtonRef.current &&
|
||||
!profileButtonRef.current.contains(target) &&
|
||||
dropdownRef.current &&
|
||||
!dropdownRef.current.contains(target)
|
||||
) {
|
||||
setProfileMenuOpen(false);
|
||||
}
|
||||
}
|
||||
if (profileMenuOpen) {
|
||||
document.addEventListener("mousedown", handleClickOutside);
|
||||
return () => document.removeEventListener("mousedown", handleClickOutside);
|
||||
}
|
||||
}, [profileMenuOpen]);
|
||||
|
||||
const SidebarContent = () => (
|
||||
<>
|
||||
<div>
|
||||
{/* Logo */}
|
||||
<div className="p-6 flex items-center justify-between gap-3">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-10 h-10 rounded-lg bg-linear-to-br from-indigo-500 to-purple-600 flex items-center justify-center shadow-lg shadow-indigo-500/30">
|
||||
<FiBook className="text-xl text-white" />
|
||||
</div>
|
||||
<span className="text-xl font-bold bg-clip-text text-transparent bg-linear-to-r from-slate-900 to-slate-600 dark:from-white dark:to-slate-400">
|
||||
Rynext Edu
|
||||
</span>
|
||||
</div>
|
||||
{/* Close button — only visible on mobile */}
|
||||
<button
|
||||
onClick={() => setSidebarOpen(false)}
|
||||
className="md:hidden p-2 rounded-lg text-slate-500 hover:text-slate-900 dark:hover:text-white hover:bg-slate-100 dark:hover:bg-slate-800 transition-colors"
|
||||
aria-label="Cerrar menú"
|
||||
>
|
||||
<FiX className="text-xl" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Navigation links */}
|
||||
<nav className="px-4 mt-6 space-y-2">
|
||||
{navigation.map((item) => {
|
||||
const isActive = pathname === item.href;
|
||||
return (
|
||||
<Link
|
||||
key={item.name}
|
||||
href={item.href}
|
||||
onClick={() => setSidebarOpen(false)}
|
||||
className={`group flex items-center gap-3 px-4 py-3 rounded-lg transition-all duration-200 ${
|
||||
isActive
|
||||
? "bg-indigo-500/10 text-indigo-600 dark:text-indigo-400 border border-indigo-500/20"
|
||||
: "text-slate-600 dark:text-slate-400 hover:text-slate-900 dark:hover:text-white hover:bg-slate-100 dark:hover:bg-slate-800/50"
|
||||
}`}
|
||||
>
|
||||
<item.icon
|
||||
className={`text-lg transition-colors ${
|
||||
isActive
|
||||
? "text-indigo-600 dark:text-indigo-400"
|
||||
: "text-slate-400 dark:text-slate-500 group-hover:text-slate-600 dark:group-hover:text-slate-300"
|
||||
}`}
|
||||
/>
|
||||
<span className="font-medium">{item.name}</span>
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
{/* Bottom section */}
|
||||
<div className="p-4 space-y-2">
|
||||
<div className="p-4">
|
||||
<p className="text-sm text-slate-800 dark:text-slate-300 font-medium">Plan Pro</p>
|
||||
<p className="text-xs text-slate-500 mt-1">2/5 cursos publicados</p>
|
||||
<div className="w-full bg-slate-200 dark:bg-slate-700 rounded-full h-1.5 mt-3">
|
||||
<div
|
||||
className="bg-linear-to-r from-indigo-500 to-purple-500 h-1.5 rounded-full"
|
||||
style={{ width: "40%" }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="flex h-screen bg-slate-50 dark:bg-slate-950 text-slate-900 dark:text-white font-sans overflow-hidden transition-colors duration-300">
|
||||
|
||||
{/* ── MOBILE OVERLAY ── */}
|
||||
{sidebarOpen && (
|
||||
<div
|
||||
className="fixed inset-0 z-30 bg-black/50 backdrop-blur-sm md:hidden"
|
||||
onClick={() => setSidebarOpen(false)}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* ── SIDEBAR — slide-in drawer on mobile, always visible on md+ ── */}
|
||||
<aside
|
||||
className={`
|
||||
fixed inset-y-0 left-0 z-40 w-64 flex flex-col justify-between
|
||||
bg-white/95 dark:bg-slate-900/95 backdrop-blur-xl
|
||||
border-r border-slate-200 dark:border-slate-800
|
||||
transition-transform duration-300 ease-in-out
|
||||
md:relative md:translate-x-0 md:z-auto
|
||||
${sidebarOpen ? "translate-x-0" : "-translate-x-full"}
|
||||
`}
|
||||
>
|
||||
<SidebarContent />
|
||||
</aside>
|
||||
|
||||
{/* ── MAIN CONTENT ── */}
|
||||
<main className="flex-1 flex flex-col h-screen overflow-hidden min-w-0">
|
||||
|
||||
{/* Top Header */}
|
||||
<header className="relative z-10 h-14 md:h-14 shrink-0 flex items-center justify-between px-4 md:px-8 bg-white/50 dark:bg-slate-900/30 backdrop-blur-md border-b border-slate-200 dark:border-slate-800 transition-colors duration-300">
|
||||
|
||||
{/* Hamburger — only on mobile */}
|
||||
<button
|
||||
onClick={() => setSidebarOpen(true)}
|
||||
className="md:hidden p-2 rounded-lg text-slate-500 hover:text-slate-900 dark:hover:text-white hover:bg-slate-100 dark:hover:bg-slate-800 transition-colors"
|
||||
aria-label="Abrir menú"
|
||||
>
|
||||
<FiMenu className="text-xl" />
|
||||
</button>
|
||||
|
||||
<h1 className="text-base md:text-xl font-semibold text-slate-900 dark:text-white">
|
||||
Panel de Instructor
|
||||
</h1>
|
||||
|
||||
<div className="flex items-center gap-3 md:gap-6">
|
||||
<ThemeToggle variant="switch" />
|
||||
|
||||
{/* Profile dropdown trigger */}
|
||||
<div className="relative">
|
||||
<button
|
||||
ref={profileButtonRef}
|
||||
onClick={handleProfileToggle}
|
||||
className="flex items-center gap-2 md:gap-3 cursor-pointer group focus:outline-none"
|
||||
>
|
||||
<div className="hidden md:block text-right">
|
||||
<p className="text-sm font-medium text-slate-900 dark:text-white group-hover:text-indigo-600 dark:group-hover:text-indigo-400 transition-colors">
|
||||
Instructor Name
|
||||
</p>
|
||||
<p className="text-xs text-slate-500">instructor@rynext.com</p>
|
||||
</div>
|
||||
<div className="w-9 h-9 md:w-10 md:h-10 rounded-full bg-linear-to-tr from-purple-500 to-indigo-500 p-0.5">
|
||||
<img
|
||||
src="https://api.dicebear.com/7.x/avataaars/svg?seed=Felix"
|
||||
alt="Avatar"
|
||||
className="w-full h-full rounded-full bg-slate-100 dark:bg-slate-900"
|
||||
/>
|
||||
</div>
|
||||
<FiChevronDown
|
||||
className={`hidden md:block text-sm text-slate-400 transition-transform duration-200 ${
|
||||
profileMenuOpen ? "rotate-180" : ""
|
||||
}`}
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Dropdown — rendered via portal in document.body to avoid overflow/stacking issues */}
|
||||
{mounted && profileMenuOpen && createPortal(
|
||||
<div
|
||||
ref={dropdownRef}
|
||||
style={{ position: "fixed", top: dropdownPos.top, right: dropdownPos.right }}
|
||||
className="w-52 rounded-lg shadow-xl bg-white dark:bg-slate-900 border border-slate-200 dark:border-slate-700 overflow-hidden z-50"
|
||||
>
|
||||
{/* User info header */}
|
||||
<div className="px-4 py-3 border-b border-slate-100 dark:border-slate-800">
|
||||
<p className="text-sm font-semibold text-slate-900 dark:text-white">Instructor Name</p>
|
||||
<p className="text-xs text-slate-500 truncate">instructor@rynext.com</p>
|
||||
</div>
|
||||
|
||||
{/* Menu items */}
|
||||
<div className="py-1.5">
|
||||
<Link
|
||||
href="/instructor/settings"
|
||||
onClick={() => setProfileMenuOpen(false)}
|
||||
className="flex items-center gap-3 px-4 py-2.5 text-sm text-slate-700 dark:text-slate-300 hover:bg-slate-50 dark:hover:bg-slate-800 hover:text-indigo-600 dark:hover:text-indigo-400 transition-colors duration-150"
|
||||
>
|
||||
<FiSettings className="text-base" />
|
||||
<span className="font-medium">Ajustes</span>
|
||||
</Link>
|
||||
|
||||
<div className="mx-3 my-1 border-t border-slate-100 dark:border-slate-800" />
|
||||
|
||||
<button
|
||||
className="flex items-center gap-3 px-4 py-2.5 w-full text-sm text-red-500 dark:text-red-400 hover:bg-red-50 dark:hover:bg-red-400/10 transition-colors duration-150"
|
||||
onClick={() => setProfileMenuOpen(false)}
|
||||
>
|
||||
<FiLogOut className="text-base" />
|
||||
<span className="font-medium">Cerrar Sesión</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>,
|
||||
document.body
|
||||
)}
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{/* Page Content — extra bottom padding on mobile for the bottom nav */}
|
||||
<div className="flex-1 overflow-y-auto p-4 md:p-8 pb-20 md:pb-8">
|
||||
{children}
|
||||
</div>
|
||||
</main>
|
||||
|
||||
{/* ── BOTTOM NAVIGATION BAR — only on mobile ── */}
|
||||
<nav className="fixed bottom-0 left-0 right-0 z-20 md:hidden bg-white/90 dark:bg-slate-900/90 backdrop-blur-xl border-t border-slate-200 dark:border-slate-800">
|
||||
<div className="flex items-center justify-around h-16 px-2">
|
||||
{bottomNavItems.map((item) => {
|
||||
const isActive = pathname === item.href;
|
||||
return (
|
||||
<Link
|
||||
key={item.name}
|
||||
href={item.href}
|
||||
className={`flex flex-col items-center gap-0.5 px-3 py-2 rounded-xl transition-all duration-200 ${
|
||||
isActive
|
||||
? "text-indigo-600 dark:text-indigo-400"
|
||||
: "text-slate-400 dark:text-slate-500"
|
||||
}`}
|
||||
>
|
||||
<item.icon className="text-xl" />
|
||||
<span className="text-[10px] font-medium">{item.name}</span>
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</nav>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,219 @@
|
||||
import React from "react";
|
||||
import {
|
||||
FiTrendingUp,
|
||||
FiUsers,
|
||||
FiBookOpen,
|
||||
FiDollarSign,
|
||||
FiArrowUpRight,
|
||||
FiArrowDownRight,
|
||||
FiClock,
|
||||
FiStar
|
||||
} from "react-icons/fi";
|
||||
|
||||
const stats = [
|
||||
{
|
||||
name: "Ganancias Totales",
|
||||
value: "$12,450.00",
|
||||
change: "+12.5%",
|
||||
trend: "up",
|
||||
icon: FiDollarSign,
|
||||
color: "from-emerald-500 to-emerald-600",
|
||||
bgColor: "bg-emerald-50 dark:bg-emerald-500/10",
|
||||
iconColor: "text-emerald-600 dark:text-emerald-500"
|
||||
},
|
||||
{
|
||||
name: "Cursos Activos",
|
||||
value: "8",
|
||||
change: "+2",
|
||||
trend: "up",
|
||||
icon: FiBookOpen,
|
||||
color: "from-indigo-500 to-indigo-600",
|
||||
bgColor: "bg-indigo-50 dark:bg-indigo-500/10",
|
||||
iconColor: "text-indigo-600 dark:text-indigo-500"
|
||||
},
|
||||
{
|
||||
name: "Total Estudiantes",
|
||||
value: "1,204",
|
||||
change: "+18.2%",
|
||||
trend: "up",
|
||||
icon: FiUsers,
|
||||
color: "from-blue-500 to-blue-600",
|
||||
bgColor: "bg-blue-50 dark:bg-blue-500/10",
|
||||
iconColor: "text-blue-600 dark:text-blue-500"
|
||||
},
|
||||
{
|
||||
name: "Calificación Promedio",
|
||||
value: "4.8",
|
||||
change: "-0.1",
|
||||
trend: "down",
|
||||
icon: FiStar,
|
||||
color: "from-amber-500 to-amber-600",
|
||||
bgColor: "bg-amber-50 dark:bg-amber-500/10",
|
||||
iconColor: "text-amber-600 dark:text-amber-500"
|
||||
}
|
||||
];
|
||||
|
||||
const recentCourses = [
|
||||
{
|
||||
id: 1,
|
||||
title: "Desarrollo Web Fullstack con Next.js",
|
||||
students: 342,
|
||||
revenue: "$4,250",
|
||||
status: "Activo",
|
||||
rating: 4.9
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
title: "Masterclass de UI/UX Design",
|
||||
students: 215,
|
||||
revenue: "$2,890",
|
||||
status: "Activo",
|
||||
rating: 4.7
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
title: "Python para Data Science",
|
||||
students: 189,
|
||||
revenue: "$1,950",
|
||||
status: "Borrador",
|
||||
rating: 0
|
||||
},
|
||||
];
|
||||
|
||||
export default function InstructorDashboard() {
|
||||
return (
|
||||
<div className="max-w-7xl mx-auto space-y-8 duration-700">
|
||||
|
||||
{/* Welcome Banner */}
|
||||
<div className="relative overflow-hidden rounded-lg bg-linear-to-r from-indigo-600 to-purple-700 dark:from-indigo-900 dark:to-purple-900 p-8 sm:p-10 shadow-lg">
|
||||
<div className="absolute top-0 right-0 -mt-20 -mr-20 w-80 h-80 bg-white opacity-10 rounded-lg blur-3xl"></div>
|
||||
<div className="absolute bottom-0 left-0 -mb-20 -ml-20 w-60 h-60 bg-indigo-300 dark:bg-indigo-500 opacity-20 rounded-lg blur-3xl"></div>
|
||||
|
||||
<div className="relative z-10">
|
||||
<h2 className="text-3xl font-bold text-white mb-2">¡Hola de nuevo, Instructor! 👋</h2>
|
||||
<p className="text-indigo-100 dark:text-indigo-200 max-w-2xl text-lg">
|
||||
Tus cursos están teniendo un excelente rendimiento este mes. Has ganado <span className="text-white font-semibold">$1,240.00</span> en los últimos 7 días.
|
||||
</p>
|
||||
<div className="mt-6 flex gap-4">
|
||||
<button className="px-6 py-3 bg-white text-indigo-700 dark:text-indigo-900 font-semibold rounded-lg hover:bg-neutral-50 transition-colors shadow-lg shadow-black/5 dark:shadow-white/10">
|
||||
Crear Nuevo Curso
|
||||
</button>
|
||||
<button className="px-6 py-3 bg-white/20 dark:bg-indigo-800/50 text-white font-medium rounded-lg hover:bg-white/30 dark:hover:bg-indigo-700/50 backdrop-blur-md border border-white/30 dark:border-indigo-700/50 transition-colors">
|
||||
Ver Reportes
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Stats Grid */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
|
||||
{stats.map((stat, index) => (
|
||||
<div
|
||||
key={stat.name}
|
||||
className="bg-white dark:bg-slate-900/50 border border-slate-200 dark:border-slate-800 backdrop-blur-xl p-6 rounded-lg hover:border-slate-300 dark:hover:border-slate-700 transition-colors group shadow-sm dark:shadow-none"
|
||||
style={{ animationDelay: `${index * 100}ms` }}
|
||||
>
|
||||
<div className="flex justify-between items-start mb-4">
|
||||
<div className={`p-3 rounded-lg ${stat.bgColor} ${stat.iconColor} ring-1 ring-inset ring-slate-200 dark:ring-white/10`}>
|
||||
<stat.icon className="text-2xl" />
|
||||
</div>
|
||||
<span className={`flex items-center gap-1 text-sm font-medium px-2.5 py-1 rounded-lg ${
|
||||
stat.trend === 'up'
|
||||
? 'text-emerald-700 bg-emerald-100 dark:text-emerald-400 dark:bg-emerald-400/10'
|
||||
: 'text-red-700 bg-red-100 dark:text-red-400 dark:bg-red-400/10'
|
||||
}`}>
|
||||
{stat.trend === 'up' ? <FiArrowUpRight /> : <FiArrowDownRight />}
|
||||
{stat.change}
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-slate-500 dark:text-slate-400 text-sm font-medium mb-1">{stat.name}</p>
|
||||
<h3 className="text-3xl font-bold text-slate-900 dark:text-white tracking-tight">{stat.value}</h3>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Charts & Lists Section */}
|
||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
||||
{/* Recent Courses */}
|
||||
<div className="lg:col-span-2 bg-white dark:bg-slate-900/50 border border-slate-200 dark:border-slate-800 backdrop-blur-xl rounded-lg p-6 shadow-sm dark:shadow-none">
|
||||
<div className="flex items-center justify-between mb-6">
|
||||
<h3 className="text-xl font-bold text-slate-900 dark:text-white">Cursos de Mayor Rendimiento</h3>
|
||||
<button className="text-sm font-medium text-indigo-600 dark:text-indigo-400 hover:text-indigo-700 dark:hover:text-indigo-300 transition-colors">
|
||||
Ver Todos
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full text-left border-collapse">
|
||||
<thead>
|
||||
<tr className="border-b border-slate-200 dark:border-slate-800 text-sm font-medium text-slate-500 dark:text-slate-400">
|
||||
<th className="pb-4 font-medium">Nombre del Curso</th>
|
||||
<th className="pb-4 font-medium">Estudiantes</th>
|
||||
<th className="pb-4 font-medium">Ganancias</th>
|
||||
<th className="pb-4 font-medium">Estado</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="text-sm">
|
||||
{recentCourses.map((course, idx) => (
|
||||
<tr key={course.id} className="border-b border-slate-100 dark:border-slate-800/50 last:border-0 hover:bg-slate-50 dark:hover:bg-slate-800/30 transition-colors">
|
||||
<td className="py-4">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-10 h-10 rounded-lg bg-slate-100 dark:bg-slate-800 flex items-center justify-center shrink-0">
|
||||
<FiBookOpen className="text-slate-500 dark:text-slate-400" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="font-medium text-slate-900 dark:text-white">{course.title}</p>
|
||||
<div className="flex items-center gap-1 text-xs text-slate-500 mt-0.5">
|
||||
<FiStar className="text-amber-500" />
|
||||
<span>{course.rating > 0 ? course.rating : 'N/A'}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td className="py-4 text-slate-600 dark:text-slate-300">{course.students}</td>
|
||||
<td className="py-4 font-medium text-emerald-600 dark:text-emerald-400">{course.revenue}</td>
|
||||
<td className="py-4">
|
||||
<span className={`px-3 py-1 rounded-lg text-xs font-medium ${
|
||||
course.status === 'Activo'
|
||||
? 'bg-emerald-100 text-emerald-700 border border-emerald-200 dark:bg-emerald-500/10 dark:text-emerald-400 dark:border-emerald-500/20'
|
||||
: 'bg-slate-100 text-slate-600 border border-slate-200 dark:bg-slate-500/10 dark:text-slate-400 dark:border-slate-500/20'
|
||||
}`}>
|
||||
{course.status}
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Quick Activity */}
|
||||
<div className="bg-white dark:bg-slate-900/50 border border-slate-200 dark:border-slate-800 backdrop-blur-xl rounded-lg p-6 shadow-sm dark:shadow-none">
|
||||
<h3 className="text-xl font-bold text-slate-900 dark:text-white mb-6">Actividad Reciente</h3>
|
||||
|
||||
<div className="space-y-6">
|
||||
{[1, 2, 3, 4].map((i) => (
|
||||
<div key={i} className="flex gap-4 relative">
|
||||
{i !== 4 && <div className="absolute left-4 top-10 -bottom-6 w-px bg-slate-200 dark:bg-slate-800"></div>}
|
||||
<div className="w-8 h-8 rounded-lg bg-indigo-50 dark:bg-indigo-500/10 border border-indigo-100 dark:border-indigo-500/20 flex items-center justify-center shrink-0 z-10">
|
||||
<FiTrendingUp className="text-indigo-600 dark:text-indigo-400 text-sm" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm font-medium text-slate-900 dark:text-white">Nueva Venta Realizada</p>
|
||||
<p className="text-xs text-slate-500 dark:text-slate-400 mt-1">Alguien compró "Desarrollo Web Fullstack con Next.js"</p>
|
||||
<p className="text-xs text-slate-400 dark:text-slate-500 mt-2 flex items-center gap-1">
|
||||
<FiClock /> Hace {i * 2} horas
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
import React from "react";
|
||||
import { FiUser, FiBell, FiLock, FiCreditCard } from "react-icons/fi";
|
||||
|
||||
export default function SettingsPage() {
|
||||
return (
|
||||
<div className="max-w-7xl mx-auto space-y-8 duration-700">
|
||||
<div>
|
||||
<h2 className="text-2xl font-bold text-slate-900 dark:text-white">Ajustes</h2>
|
||||
<p className="text-slate-500 dark:text-slate-400 mt-1">Configura tu perfil de instructor y preferencias.</p>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-4 gap-8">
|
||||
<div className="col-span-1 space-y-2">
|
||||
<button className="w-full flex items-center gap-3 px-4 py-3 bg-indigo-50 dark:bg-indigo-500/10 text-indigo-700 dark:text-indigo-400 font-medium rounded-xl text-left">
|
||||
<FiUser /> Perfil Público
|
||||
</button>
|
||||
<button className="w-full flex items-center gap-3 px-4 py-3 text-slate-600 dark:text-slate-400 hover:bg-slate-50 dark:hover:bg-slate-800/50 font-medium rounded-xl text-left transition-colors">
|
||||
<FiCreditCard /> Pagos
|
||||
</button>
|
||||
<button className="w-full flex items-center gap-3 px-4 py-3 text-slate-600 dark:text-slate-400 hover:bg-slate-50 dark:hover:bg-slate-800/50 font-medium rounded-xl text-left transition-colors">
|
||||
<FiBell /> Notificaciones
|
||||
</button>
|
||||
<button className="w-full flex items-center gap-3 px-4 py-3 text-slate-600 dark:text-slate-400 hover:bg-slate-50 dark:hover:bg-slate-800/50 font-medium rounded-xl text-left transition-colors">
|
||||
<FiLock /> Seguridad
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="col-span-1 md:col-span-3 bg-white dark:bg-slate-900/50 border border-slate-200 dark:border-slate-800 rounded-3xl p-6 md:p-8 shadow-sm dark:shadow-none">
|
||||
<h3 className="text-xl font-bold text-slate-900 dark:text-white mb-6">Información del Perfil</h3>
|
||||
|
||||
<div className="flex items-center gap-6 mb-8">
|
||||
<div className="w-24 h-24 rounded-full bg-slate-200 dark:bg-slate-700 shrink-0 flex items-center justify-center text-3xl font-bold text-slate-500 dark:text-slate-400">
|
||||
IN
|
||||
</div>
|
||||
<div>
|
||||
<button className="px-4 py-2 bg-slate-100 dark:bg-slate-800 text-slate-900 dark:text-white hover:bg-slate-200 dark:hover:bg-slate-700 font-medium rounded-xl text-sm transition-colors mb-2">
|
||||
Cambiar Avatar
|
||||
</button>
|
||||
<p className="text-xs text-slate-500">Recomendado: 500x500px, JPG o PNG.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-6 max-w-2xl">
|
||||
<div className="grid grid-cols-2 gap-6">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">Nombre</label>
|
||||
<input type="text" defaultValue="Instructor" className="w-full px-4 py-2.5 bg-slate-50 dark:bg-slate-900 border border-slate-200 dark:border-slate-700 rounded-xl focus:outline-none focus:ring-2 focus:ring-indigo-500 dark:text-white" />
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">Apellidos</label>
|
||||
<input type="text" defaultValue="Name" className="w-full px-4 py-2.5 bg-slate-50 dark:bg-slate-900 border border-slate-200 dark:border-slate-700 rounded-xl focus:outline-none focus:ring-2 focus:ring-indigo-500 dark:text-white" />
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">Biografía</label>
|
||||
<textarea rows={4} defaultValue="Experto en desarrollo web con más de 10 años de experiencia..." className="w-full px-4 py-2.5 bg-slate-50 dark:bg-slate-900 border border-slate-200 dark:border-slate-700 rounded-xl focus:outline-none focus:ring-2 focus:ring-indigo-500 dark:text-white"></textarea>
|
||||
</div>
|
||||
<button className="px-6 py-2.5 bg-indigo-600 hover:bg-indigo-700 text-white font-medium rounded-xl transition-colors shadow-sm shadow-indigo-600/20">
|
||||
Guardar Cambios
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -45,7 +45,7 @@ export const CourseCard: React.FC<CourseCardProps> = ({ course }) => {
|
||||
<div>
|
||||
<div className={`relative aspect-video w-full overflow-hidden cursor-pointer ${isDark ? 'bg-slate-800' : 'bg-slate-100'}`} onClick={handleCourseClick}>
|
||||
<img src={course.thumbnail} alt={course.title} className="w-full h-full object-cover group-hover:scale-105 transition-transform duration-500" />
|
||||
<div className="absolute inset-0 bg-gradient-to-t from-slate-950/80 via-transparent to-black/20" />
|
||||
<div className="absolute inset-0 bg-linear-to-t from-slate-950/80 via-transparent to-black/20" />
|
||||
|
||||
{course.badge && (
|
||||
<span className={`absolute top-3 left-3 flex items-center gap-1 px-2.5 py-1 rounded-md text-[10px] font-black uppercase tracking-wider shadow-md ${getBadgeStyle(course.badge)}`}>
|
||||
@@ -123,11 +123,11 @@ export const CourseCard: React.FC<CourseCardProps> = ({ course }) => {
|
||||
</div>
|
||||
</div>
|
||||
<span className={`font-mono text-[11px] ${isDark ? 'text-slate-500' : 'text-slate-400'}`}>
|
||||
({course.reviewsCount.toLocaleString()})
|
||||
({course.reviewsCount.toString()})
|
||||
</span>
|
||||
<span className={isDark ? 'text-slate-600' : 'text-slate-300'}>•</span>
|
||||
<span className={`text-[11px] ${isDark ? 'text-slate-400' : 'text-slate-500'}`}>
|
||||
{course.studentsCount.toLocaleString()} alumnos
|
||||
{course.studentsCount.toString()} alumnos
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -11,7 +11,7 @@ export default function LayoutBase({ ...props }: IProps) {
|
||||
const { isDark } = useTheme();
|
||||
|
||||
return (
|
||||
<div className={`relative flex flex-col min-h-screen min-w-screen transition-colors duration-300 ${isDark ? "text-slate-100" : "text-slate-900"}`}>
|
||||
<div className={`relative flex flex-col min-h-screen transition-colors duration-300 ${isDark ? "text-slate-100" : "text-slate-900"}`}>
|
||||
<div className={`fixed inset-0 -z-10 transition-all duration-300 ${isDark ? "bg-linear-to-b from-slate-950 via-slate-950 to-[#1a0a4a]" : "bg-white"}`} />
|
||||
|
||||
<Navbar />
|
||||
|
||||
Reference in new issue
Block a user