dashboard con sidebar para el instructor

This commit is contained in:
Miguel Ortega committed 2026-09-01 15:13:44 -05:00
1 parent dd43fa04c1
commit 8ea93d67f9
9 files changed
+1282 -6

No files matched your search

+2 -2
View File
@@ -86,7 +86,7 @@ export default function HomeContent() {
placeholder="Buscar cursos..." placeholder="Buscar cursos..."
value={searchQuery} value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)} 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> </div>
@@ -179,7 +179,7 @@ export default function HomeContent() {
className={`flex items-center gap-1 ${isDark ? "text-slate-400" : "text-slate-500"}`} className={`flex items-center gap-1 ${isDark ? "text-slate-400" : "text-slate-500"}`}
> >
<FiUsers className="text-violet-400" /> <FiUsers className="text-violet-400" />
{inst.totalStudents.toLocaleString()} alumnos {inst.totalStudents.toString()} alumnos
</span> </span>
</div> </div>
</div> </div>
+48
View File
@@ -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>
</>
);
}
+582
View File
@@ -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>
);
}
+68
View File
@@ -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>
);
}
+293
View File
@@ -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>
);
}
+219
View File
@@ -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>
);
}
+66
View File
@@ -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>
);
}
+3 -3
View File
@@ -45,7 +45,7 @@ export const CourseCard: React.FC<CourseCardProps> = ({ course }) => {
<div> <div>
<div className={`relative aspect-video w-full overflow-hidden cursor-pointer ${isDark ? 'bg-slate-800' : 'bg-slate-100'}`} onClick={handleCourseClick}> <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" /> <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 && ( {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)}`}> <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>
</div> </div>
<span className={`font-mono text-[11px] ${isDark ? 'text-slate-500' : 'text-slate-400'}`}> <span className={`font-mono text-[11px] ${isDark ? 'text-slate-500' : 'text-slate-400'}`}>
({course.reviewsCount.toLocaleString()}) ({course.reviewsCount.toString()})
</span> </span>
<span className={isDark ? 'text-slate-600' : 'text-slate-300'}>•</span> <span className={isDark ? 'text-slate-600' : 'text-slate-300'}>•</span>
<span className={`text-[11px] ${isDark ? 'text-slate-400' : 'text-slate-500'}`}> <span className={`text-[11px] ${isDark ? 'text-slate-400' : 'text-slate-500'}`}>
{course.studentsCount.toLocaleString()} alumnos {course.studentsCount.toString()} alumnos
</span> </span>
</div> </div>
</div> </div>
+1 -1
View File
@@ -11,7 +11,7 @@ export default function LayoutBase({ ...props }: IProps) {
const { isDark } = useTheme(); const { isDark } = useTheme();
return ( 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"}`} /> <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 /> <Navbar />