refactor: update UI components and pages with a refreshed visual design and improved styling.

This commit is contained in:
2026-03-03 12:42:37 -03:00
parent 9123337200
commit 15f2649777
27 changed files with 1864 additions and 1557 deletions
@@ -75,15 +75,15 @@ export default function AnnouncementsPage() {
Comunicados del Curso Comunicados del Curso
</h2> </h2>
{/* Search Bar */} {/* Search Bar */}
<div className="glass p-4 rounded-2xl flex items-center gap-4"> <div className="bg-white dark:bg-white/5 border border-slate-200 dark:border-white/10 p-6 rounded-[2rem] flex items-center gap-4 shadow-sm">
<div className="relative flex-1"> <div className="relative flex-1">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-gray-500" /> <Search className="absolute left-4 top-1/2 -translate-y-1/2 w-4 h-4 text-slate-400 dark:text-gray-500" />
<input <input
type="text" type="text"
placeholder="Search announcements..." placeholder="Find an announcement..."
value={searchTerm} value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)} onChange={(e) => setSearchTerm(e.target.value)}
className="w-full bg-slate-100 dark:bg-black/20 border border-slate-200 dark:border-white/10 rounded-xl py-2 pl-10 pr-4 text-sm focus:outline-none focus:border-orange-500/50 transition-all text-slate-900 dark:text-white placeholder-slate-400 dark:placeholder-gray-500" className="w-full bg-slate-50 dark:bg-black/20 border border-slate-200 dark:border-white/10 rounded-2xl py-3.5 pl-11 pr-4 text-sm focus:outline-none focus:ring-2 focus:ring-orange-500/50 transition-all text-slate-900 dark:text-white placeholder-slate-400 dark:placeholder-gray-500 font-bold shadow-inner"
/> />
</div> </div>
</div> </div>
@@ -95,12 +95,12 @@ export default function AnnouncementsPage() {
<p className="text-gray-400">Loading announcements...</p> <p className="text-gray-400">Loading announcements...</p>
</div> </div>
) : filteredAnnouncements.length > 0 ? ( ) : filteredAnnouncements.length > 0 ? (
<div className="grid gap-6"> <div className="grid gap-8">
{filteredAnnouncements.map((a) => ( {filteredAnnouncements.map((a) => (
<div key={a.id} className={`relative p-6 rounded-2xl border transition-all duration-300 ${a.is_pinned ? 'bg-orange-500/10 border-orange-500/30' : 'bg-slate-50 dark:bg-white/5 border-slate-100 dark:border-white/10 hover:border-slate-200 dark:hover:border-white/20'}`}> <div key={a.id} className={`relative p-8 rounded-[2.5rem] border transition-all duration-300 shadow-sm ${a.is_pinned ? 'bg-orange-50 dark:bg-orange-500/10 border-orange-200 dark:border-orange-500/30' : 'bg-white dark:bg-white/5 border-slate-200 dark:border-white/10 hover:bg-slate-50 dark:hover:bg-white/10'}`}>
<div className="flex items-start justify-between mb-4"> <div className="flex items-start justify-between mb-6">
<div className="flex items-center gap-3"> <div className="flex items-center gap-4">
<div className="w-10 h-10 rounded-full bg-gradient-to-br from-orange-500 to-red-500 flex items-center justify-center text-gray-900 dark:text-white font-bold overflow-hidden"> <div className="w-12 h-12 rounded-2xl bg-gradient-to-br from-orange-500 to-red-500 flex items-center justify-center text-white font-black overflow-hidden shadow-lg shadow-orange-500/20 border-2 border-white dark:border-white/10">
{a.author_avatar ? ( {a.author_avatar ? (
<img src={a.author_avatar} alt={a.author_name} className="w-full h-full object-cover" /> <img src={a.author_avatar} alt={a.author_name} className="w-full h-full object-cover" />
) : ( ) : (
@@ -108,8 +108,8 @@ export default function AnnouncementsPage() {
)} )}
</div> </div>
<div> <div>
<h4 className="font-semibold text-slate-900 dark:text-white">{a.author_name}</h4> <h4 className="font-black text-slate-900 dark:text-white uppercase tracking-tight">{a.author_name}</h4>
<div className="flex items-center gap-2 text-sm text-slate-500 dark:text-gray-400"> <div className="flex items-center gap-2 text-[11px] text-slate-400 dark:text-gray-500 font-bold uppercase tracking-widest mt-0.5">
<span>{formatDistanceToNow(new Date(a.created_at), { addSuffix: true, locale: es })}</span> <span>{formatDistanceToNow(new Date(a.created_at), { addSuffix: true, locale: es })}</span>
{a.cohort_ids && a.cohort_ids.length > 0 && ( {a.cohort_ids && a.cohort_ids.length > 0 && (
<> <>
@@ -133,12 +133,12 @@ export default function AnnouncementsPage() {
</button> </button>
</div> </div>
</div> </div>
<h3 className="text-xl font-bold text-slate-900 dark:text-white mb-2">{a.title}</h3> <h3 className="text-2xl font-black text-slate-900 dark:text-white mb-3 uppercase tracking-tight leading-tight">{a.title}</h3>
<p className="text-slate-600 dark:text-gray-300 whitespace-pre-wrap">{a.content}</p> <p className="text-slate-600 dark:text-gray-300 whitespace-pre-wrap leading-relaxed font-medium italic">{a.content}</p>
{/* Display Target Cohort Names if segmented */} {/* Display Target Cohort Names if segmented */}
{a.cohort_ids && a.cohort_ids.length > 0 && ( {a.cohort_ids && a.cohort_ids.length > 0 && (
<div className="mt-4 flex flex-wrap gap-2"> <div className="mt-8 flex flex-wrap gap-2 pt-6 border-t border-slate-100 dark:border-white/5">
{a.cohort_ids.map(cid => { {a.cohort_ids.map(cid => {
const cohort = cohorts.find(c => c.id === cid); const cohort = cohorts.find(c => c.id === cid);
return ( return (
@@ -153,10 +153,12 @@ export default function AnnouncementsPage() {
))} ))}
</div> </div>
) : ( ) : (
<div className="bg-slate-50 dark:bg-white/5 border border-slate-100 dark:border-white/10 rounded-2xl p-20 text-center"> <div className="bg-slate-50 dark:bg-white/5 border-2 border-dashed border-slate-200 dark:border-white/10 rounded-[2.5rem] p-24 text-center shadow-inner">
<Megaphone className="w-12 h-12 text-slate-400 dark:text-gray-600 mx-auto mb-4" /> <div className="w-20 h-20 rounded-3xl bg-white dark:bg-white/5 flex items-center justify-center mx-auto mb-6 shadow-sm">
<h3 className="text-xl font-bold text-slate-900 dark:text-white mb-2">No announcements found</h3> <Megaphone className="w-10 h-10 text-slate-400 dark:text-gray-600 opacity-40" />
<p className="text-slate-500 dark:text-gray-400">Start by creating a new announcement for your students.</p> </div>
<h3 className="text-xl font-black text-slate-900 dark:text-white mb-2 uppercase tracking-tight">No announcements yet</h3>
<p className="text-xs text-slate-400 dark:text-gray-500 font-bold uppercase tracking-widest italic">Start communicating with your student body today.</p>
</div> </div>
)} )}
</div> </div>
@@ -212,53 +214,55 @@ function NewAnnouncementModal({ courseId, cohorts, onClose, onSuccess }: { cours
}; };
return ( return (
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/60 dark:bg-black/80 backdrop-blur-sm animate-in fade-in duration-300"> <div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-slate-900/40 dark:bg-black/80 backdrop-blur-md animate-in fade-in duration-300">
<div className="bg-white dark:bg-[#1a1c1e] border border-slate-200 dark:border-white/10 rounded-3xl w-full max-w-2xl overflow-hidden shadow-2xl animate-in fade-in zoom-in duration-200"> <div className="bg-white dark:bg-[#1a1c1e] border border-slate-200 dark:border-white/10 rounded-[2.5rem] w-full max-w-2xl overflow-hidden shadow-2xl animate-in zoom-in-95 duration-200">
<div className="p-6 border-b border-slate-100 dark:border-white/5 flex items-center justify-between"> <div className="p-10 border-b border-slate-100 dark:border-white/5 flex items-center justify-between bg-slate-50/50 dark:bg-transparent">
<h2 className="text-xl font-bold flex items-center gap-2 text-slate-900 dark:text-white"> <div>
<Megaphone className="w-5 h-5 text-orange-500" /> <h2 className="text-2xl font-black flex items-center gap-3 text-slate-900 dark:text-white uppercase tracking-tight">
Create New Announcement <Megaphone className="w-6 h-6 text-orange-500" />
</h2> New Announcement
<button onClick={onClose} className="text-slate-400 hover:text-slate-900 dark:text-gray-500 dark:hover:text-white transition-colors"> </h2>
<Plus className="w-6 h-6 rotate-45" /> <p className="text-xs text-slate-400 dark:text-gray-500 font-bold uppercase tracking-widest mt-1">Share news and updates with your course</p>
</div>
<button onClick={onClose} className="p-3 hover:bg-slate-200 dark:hover:bg-white/10 rounded-2xl transition-all group active:scale-95">
<Plus size={20} className="text-slate-400 group-hover:text-slate-900 dark:group-hover:text-white transition-colors rotate-45" />
</button> </button>
</div> </div>
<form onSubmit={handleSubmit} className="p-6 space-y-6"> <form onSubmit={handleSubmit} className="p-10 space-y-8">
<div className="space-y-2"> <div className="space-y-3">
<label className="text-sm font-bold text-gray-400">Title</label> <label className="text-[10px] font-black uppercase tracking-[0.2em] text-slate-400 dark:text-gray-500 ml-1">Topic Title</label>
<input <input
required required
value={title} value={title}
onChange={(e) => setTitle(e.target.value)} onChange={(e) => setTitle(e.target.value)}
className="w-full bg-slate-50 dark:bg-white/5 border border-slate-200 dark:border-white/10 rounded-xl px-4 py-3 focus:outline-none focus:border-orange-500/50 text-slate-900 dark:text-white placeholder-slate-400 dark:placeholder-gray-500" className="w-full bg-slate-50 dark:bg-black/20 border border-slate-200 dark:border-white/10 rounded-2xl px-5 py-4 focus:outline-none focus:ring-2 focus:ring-orange-500/50 text-slate-900 dark:text-white placeholder-slate-400 dark:placeholder-gray-500 font-bold shadow-inner"
placeholder="Announcement title" placeholder="What's this announcement about?"
/> />
</div> </div>
<div className="space-y-2"> <div className="space-y-3">
<label className="text-sm font-bold text-gray-400">Content</label> <label className="text-[10px] font-black uppercase tracking-[0.2em] text-slate-400 dark:text-gray-500 ml-1">Message Body</label>
<textarea <textarea
required required
rows={5} rows={5}
value={content} value={content}
onChange={(e) => setContent(e.target.value)} onChange={(e) => setContent(e.target.value)}
className="w-full bg-slate-50 dark:bg-white/5 border border-slate-200 dark:border-white/10 rounded-xl px-4 py-3 focus:outline-none focus:border-orange-500/50 resize-none text-slate-900 dark:text-white placeholder-slate-400 dark:placeholder-gray-500" className="w-full bg-slate-50 dark:bg-black/20 border border-slate-200 dark:border-white/10 rounded-2xl px-5 py-4 focus:outline-none focus:ring-2 focus:ring-orange-500/50 resize-none text-slate-900 dark:text-white placeholder-slate-400 dark:placeholder-gray-500 font-medium italic shadow-inner"
placeholder="Type your message here..." placeholder="Type your detailed message here..."
/> />
</div> </div>
<div className="space-y-3"> <div className="space-y-4">
<label className="text-sm font-bold text-gray-400 flex items-center gap-2"> <label className="text-[10px] font-black uppercase tracking-[0.2em] text-slate-400 dark:text-gray-500 flex items-center gap-2 ml-1">
<Users className="w-4 h-4" /> <Users className="w-4 h-4 text-blue-500" />
Target Segments (Optional) Target Segments (Optional)
</label> </label>
<p className="text-xs text-gray-500">Select specific cohorts to receive this announcement. Leave empty to send to all students.</p> <div className="flex flex-wrap gap-2 max-h-40 overflow-y-auto p-1 custom-scrollbar">
<div className="flex flex-wrap gap-2 max-h-32 overflow-y-auto p-1">
{cohorts.map(c => ( {cohorts.map(c => (
<button <button
key={c.id} key={c.id}
type="button" type="button"
onClick={() => toggleCohort(c.id)} onClick={() => toggleCohort(c.id)}
className={`px-3 py-1.5 rounded-lg text-xs font-bold transition-all border ${selectedCohorts.includes(c.id) ? 'bg-blue-600 border-blue-500 text-white' : 'bg-slate-50 dark:bg-white/5 border-slate-100 dark:border-white/10 text-slate-500 dark:text-gray-400 hover:bg-slate-100 dark:hover:bg-white/10'}`} className={`px-4 py-2 rounded-xl text-[10px] font-black uppercase tracking-widest transition-all border shadow-sm active:scale-95 ${selectedCohorts.includes(c.id) ? 'bg-blue-600 border-blue-500 text-white shadow-blue-500/20' : 'bg-slate-50 dark:bg-white/5 border-slate-200 dark:border-white/10 text-slate-400 dark:text-gray-500 hover:border-blue-500/30'}`}
> >
{c.name} {c.name}
</button> </button>
@@ -266,29 +270,30 @@ function NewAnnouncementModal({ courseId, cohorts, onClose, onSuccess }: { cours
</div> </div>
</div> </div>
<div className="flex items-center gap-3 p-4 bg-orange-500/5 border border-orange-500/10 rounded-2xl"> <div className="flex items-center gap-4 p-5 bg-orange-50 dark:bg-orange-500/5 border border-orange-100 dark:border-orange-500/10 rounded-[2rem] shadow-sm">
<input <div className="relative inline-flex items-center cursor-pointer">
type="checkbox" <input
id="pin" type="checkbox"
checked={isPinned} id="pin"
onChange={(e) => setIsPinned(e.target.checked)} checked={isPinned}
className="w-5 h-5 rounded border-slate-200 dark:border-white/10 bg-slate-50 dark:bg-white/5 text-orange-600 focus:ring-orange-500/50" onChange={(e) => setIsPinned(e.target.checked)}
/> className="w-6 h-6 rounded-lg border-slate-300 dark:border-white/10 bg-white dark:bg-black/40 text-orange-600 focus:ring-orange-500/50 shadow-inner"
<label htmlFor="pin" className="text-sm font-medium text-slate-600 dark:text-gray-300 cursor-pointer"> />
Pin this announcement to the top </div>
<label htmlFor="pin" className="text-xs font-black uppercase tracking-widest text-orange-800/80 dark:text-orange-400/80 cursor-pointer">
Pin this announcement to the top of the course feed
</label> </label>
</div> </div>
<div className="flex justify-end gap-4 pt-6">
<div className="flex justify-end gap-3 pt-4"> <button type="button" onClick={onClose} className="px-8 py-3 font-black text-[10px] uppercase tracking-widest text-slate-400 dark:text-gray-500 hover:text-slate-900 dark:hover:text-white transition-all active:scale-95">
<button type="button" onClick={onClose} className="px-6 py-2.5 font-bold text-gray-500 hover:bg-white/5 rounded-xl transition-colors"> Discard
Cancel
</button> </button>
<button <button
type="submit" type="submit"
disabled={loading} disabled={loading || !title || !content}
className="px-8 py-2.5 bg-orange-600 hover:bg-orange-500 disabled:opacity-50 text-white rounded-xl font-bold flex items-center gap-2 shadow-lg shadow-orange-500/20" className="px-10 py-3 bg-orange-600 hover:bg-orange-500 disabled:bg-slate-200 dark:disabled:bg-white/5 text-white disabled:text-slate-400 rounded-2xl font-black text-[10px] uppercase tracking-[0.2em] shadow-xl shadow-orange-500/20 transition-all active:scale-95 disabled:shadow-none group flex items-center gap-3"
> >
{loading ? <Loader2 className="w-4 h-4 animate-spin" /> : <Megaphone className="w-4 h-4" />} {loading ? <Loader2 className="w-4 h-4 animate-spin" /> : <Megaphone className="w-5 h-5 group-hover:scale-110 transition-transform" />}
Publish Announcement Publish Announcement
</button> </button>
</div> </div>
@@ -52,7 +52,7 @@ export default function CourseCalendarPage({ params }: { params: { id: string }
// Padding for first week // Padding for first week
for (let i = 0; i < firstDay; i++) { for (let i = 0; i < firstDay; i++) {
days.push(<div key={`empty-${i}`} className="h-32 border border-white/5 bg-white/2"></div>); days.push(<div key={`empty-${i}`} className="h-32 border-r border-b border-slate-100 dark:border-white/5 bg-slate-50/50 dark:bg-white/2"></div>);
} }
// Days of month // Days of month
@@ -61,19 +61,19 @@ export default function CourseCalendarPage({ params }: { params: { id: string }
const dayLessons = lessons.filter(l => l.due_date && l.due_date.startsWith(dateStr)); const dayLessons = lessons.filter(l => l.due_date && l.due_date.startsWith(dateStr));
days.push( days.push(
<div key={day} className="h-32 border border-white/5 p-2 relative hover:bg-white/5 transition-colors group"> <div key={day} className="h-36 border-r border-b border-slate-100 dark:border-white/5 p-3 relative hover:bg-slate-50 dark:hover:bg-white/5 transition-all group cursor-pointer">
<span className="text-sm font-bold text-gray-400">{day}</span> <span className="text-xs font-black text-slate-400 dark:text-gray-500 uppercase tracking-widest">{day}</span>
<div className="mt-1 space-y-1 overflow-y-auto max-h-24"> <div className="mt-2 space-y-1.5 overflow-y-auto max-h-24 custom-scrollbar">
{dayLessons.map(lesson => ( {dayLessons.map(lesson => (
<div <div
key={lesson.id} key={lesson.id}
className={`text-[10px] p-1 rounded truncate flex items-center gap-1 ${lesson.important_date_type === 'exam' ? 'bg-red-500/20 text-red-400 border border-red-500/30' : className={`text-[9px] p-1.5 rounded-lg truncate flex items-center gap-1.5 font-bold uppercase tracking-tight shadow-sm border ${lesson.important_date_type === 'exam' ? 'bg-red-50 text-red-700 border-red-100 dark:bg-red-500/20 dark:text-red-400 dark:border-red-500/30' :
lesson.important_date_type === 'assignment' ? 'bg-blue-500/20 text-blue-400 border border-blue-500/30' : lesson.important_date_type === 'assignment' ? 'bg-blue-50 text-blue-700 border-blue-100 dark:bg-blue-500/20 dark:text-blue-400 dark:border-blue-500/30' :
lesson.important_date_type === 'live-session' ? 'bg-purple-500/20 text-purple-400 border border-purple-500/30' : lesson.important_date_type === 'live-session' ? 'bg-purple-50 text-purple-700 border-purple-100 dark:bg-purple-500/20 dark:text-purple-400 dark:border-purple-500/30' :
'bg-green-500/20 text-green-400 border border-green-500/30' 'bg-emerald-50 text-emerald-700 border-emerald-100 dark:bg-green-500/20 dark:text-green-400 dark:border-green-500/30'
}`} }`}
> >
<span className="w-1.5 h-1.5 rounded-full bg-current"></span> <span className={`w-1.5 h-1.5 rounded-full ${lesson.important_date_type === 'exam' ? 'bg-red-500' : lesson.important_date_type === 'assignment' ? 'bg-blue-500' : lesson.important_date_type === 'live-session' ? 'bg-purple-500' : 'bg-emerald-500'}`}></span>
{lesson.title} {lesson.title}
</div> </div>
))} ))}
@@ -115,24 +115,24 @@ export default function CourseCalendarPage({ params }: { params: { id: string }
</div> </div>
<div className="flex gap-4"> <div className="flex gap-4">
<div className="flex items-center gap-2 text-[10px] font-bold uppercase tracking-widest text-gray-500"> <div className="flex items-center gap-2 text-[10px] font-black uppercase tracking-widest text-slate-400 dark:text-gray-500">
<span className="w-2 h-2 rounded-full bg-red-500"></span> Examen <span className="w-2.5 h-2.5 rounded-full bg-red-500 shadow-sm shadow-red-500/20"></span> Examen
</div> </div>
<div className="flex items-center gap-2 text-[10px] font-bold uppercase tracking-widest text-gray-500"> <div className="flex items-center gap-2 text-[10px] font-black uppercase tracking-widest text-slate-400 dark:text-gray-500">
<span className="w-2 h-2 rounded-full bg-blue-500"></span> Tarea <span className="w-2.5 h-2.5 rounded-full bg-blue-500 shadow-sm shadow-blue-500/20"></span> Tarea
</div> </div>
<div className="flex items-center gap-2 text-[10px] font-bold uppercase tracking-widest text-gray-500"> <div className="flex items-center gap-2 text-[10px] font-black uppercase tracking-widest text-slate-400 dark:text-gray-500">
<span className="w-2 h-2 rounded-full bg-purple-500"></span> En Vivo <span className="w-2.5 h-2.5 rounded-full bg-purple-500 shadow-sm shadow-purple-500/20"></span> En Vivo
</div> </div>
<div className="flex items-center gap-2 text-[10px] font-bold uppercase tracking-widest text-gray-500"> <div className="flex items-center gap-2 text-[10px] font-black uppercase tracking-widest text-slate-400 dark:text-gray-500">
<span className="w-2 h-2 rounded-full bg-green-500"></span> Lección <span className="w-2.5 h-2.5 rounded-full bg-emerald-500 shadow-sm shadow-emerald-500/20"></span> Lección
</div> </div>
</div> </div>
</div> </div>
<div className="grid grid-cols-7 border-t border-l border-white/5 rounded-xl overflow-hidden shadow-2xl overflow-hidden"> <div className="grid grid-cols-7 border-t border-l border-slate-200 dark:border-white/5 rounded-[2.5rem] overflow-hidden shadow-sm bg-white dark:bg-white/[0.02]">
{['Dom', 'Lun', 'Mar', 'Mié', 'Jue', 'Vie', 'Sáb'].map(day => ( {['Dom', 'Lun', 'Mar', 'Mié', 'Jue', 'Vie', 'Sáb'].map(day => (
<div key={day} className="bg-white/5 py-4 text-center text-xs font-black uppercase tracking-widest text-gray-500 border-r border-b border-white/5"> <div key={day} className="bg-slate-50 dark:bg-white/5 py-4 text-center text-xs font-black uppercase tracking-widest text-slate-400 dark:text-gray-500 border-r border-b border-slate-200 dark:border-white/5">
{day} {day}
</div> </div>
))} ))}
@@ -144,26 +144,26 @@ export default function CourseCalendarPage({ params }: { params: { id: string }
<AlertCircle className="w-5 h-5 text-blue-500" /> <AlertCircle className="w-5 h-5 text-blue-500" />
Upcoming Deadlines Upcoming Deadlines
</h4> </h4>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4"> <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{lessons {lessons
.filter(l => l.due_date && new Date(l.due_date) >= new Date()) .filter(l => l.due_date && new Date(l.due_date) >= new Date())
.sort((a, b) => new Date(a.due_date!).getTime() - new Date(b.due_date!).getTime()) .sort((a, b) => new Date(a.due_date!).getTime() - new Date(b.due_date!).getTime())
.slice(0, 6) .slice(0, 6)
.map(lesson => ( .map(lesson => (
<div key={lesson.id} className="glass p-4 border-white/5 hover:border-blue-500/30 transition-all group"> <div key={lesson.id} className="bg-slate-50 dark:bg-white/5 border border-slate-200 dark:border-white/10 rounded-3xl p-6 hover:bg-white dark:hover:bg-white/10 transition-all group shadow-sm">
<div className="flex justify-between items-start"> <div className="flex justify-between items-start">
<div> <div>
<div className={`text-[10px] font-black uppercase tracking-widest mb-1 ${lesson.important_date_type === 'exam' ? 'text-red-400' : <div className={`text-[10px] font-black uppercase tracking-[0.2em] mb-2 ${lesson.important_date_type === 'exam' ? 'text-red-500' :
lesson.important_date_type === 'assignment' ? 'text-blue-400' : lesson.important_date_type === 'assignment' ? 'text-blue-500' :
'text-green-400' 'text-emerald-500'
}`}> }`}>
{lesson.important_date_type || 'Activity'} {lesson.important_date_type || 'Activity'}
</div> </div>
<h5 className="font-bold group-hover:text-blue-400 transition-colors">{lesson.title}</h5> <h5 className="font-bold text-slate-900 dark:text-white group-hover:text-blue-600 dark:group-hover:text-blue-400 transition-colors uppercase tracking-tight">{lesson.title}</h5>
</div> </div>
<div className="text-right"> <div className="text-right">
<div className="text-sm font-black">{new Date(lesson.due_date!).toLocaleDateString()}</div> <div className="text-sm font-black text-slate-900 dark:text-white">{new Date(lesson.due_date!).toLocaleDateString()}</div>
<div className="text-[10px] text-gray-500 uppercase font-bold">Due Date</div> <div className="text-[10px] text-slate-400 dark:text-gray-500 uppercase font-black mt-0.5 tracking-tighter">Due Date</div>
</div> </div>
</div> </div>
</div> </div>
+38 -28
View File
@@ -95,60 +95,70 @@ export default function CourseFilesPage() {
/> />
<label <label
htmlFor="file-upload" htmlFor="file-upload"
className={`flex items-center gap-2 px-5 py-2.5 bg-blue-600 hover:bg-blue-500 text-white rounded-xl font-bold text-sm shadow-md shadow-blue-600/20 transition-all cursor-pointer active:scale-95 ${isUploading ? 'opacity-50 cursor-wait' : ''}`} className={`flex items-center gap-2 px-6 py-3 bg-blue-600 hover:bg-blue-500 text-white rounded-2xl font-black text-[10px] uppercase tracking-[0.2em] shadow-xl shadow-blue-500/20 transition-all cursor-pointer active:scale-95 ${isUploading ? 'opacity-50 cursor-wait' : ''}`}
> >
{!isUploading && <Upload className="w-4 h-4" />} {!isUploading && <Upload className="w-4 h-4" />}
{isUploading ? `Subiendo ${uploadProgress}%` : 'Subir Archivo'} {isUploading ? `UPLOADING ${uploadProgress}%` : 'SUBIR ARCHIVO'}
</label> </label>
</div> </div>
} }
> >
<div className="space-y-6"> <div className="space-y-6">
<div className="bg-white dark:bg-white/5 border border-slate-200 dark:border-white/10 rounded-[2.5rem] overflow-hidden shadow-sm">
<div className="glass rounded-xl overflow-hidden"> <table className="w-full text-left border-collapse">
<table className="w-full text-left"> <thead>
<thead className="bg-white/5 border-b border-white/10 text-gray-400 font-medium"> <tr className="bg-slate-50 dark:bg-white/5 border-b border-slate-200 dark:border-white/5">
<tr> <th className="p-6 text-[10px] font-black uppercase tracking-[0.2em] text-slate-400 dark:text-gray-500">Asset Name</th>
<th className="p-4">Name</th> <th className="p-6 text-[10px] font-black uppercase tracking-[0.2em] text-slate-400 dark:text-gray-500">MIME Type</th>
<th className="p-4">Type</th> <th className="p-6 text-[10px] font-black uppercase tracking-[0.2em] text-slate-400 dark:text-gray-500">Size</th>
<th className="p-4">Size</th> <th className="p-6 text-[10px] font-black uppercase tracking-[0.2em] text-slate-400 dark:text-gray-500">Uploaded</th>
<th className="p-4">Uploaded</th> <th className="p-6 text-[10px] font-black uppercase tracking-[0.2em] text-slate-400 dark:text-gray-500 text-right">Actions</th>
<th className="p-4 text-right">Actions</th>
</tr> </tr>
</thead> </thead>
<tbody className="divide-y divide-white/5"> <tbody className="divide-y divide-slate-100 dark:divide-white/5">
{isLoading ? ( {isLoading ? (
<tr><td colSpan={5} className="p-8 text-center text-gray-500">Loading files...</td></tr> <tr><td colSpan={5} className="p-12 text-center text-slate-400 dark:text-gray-500 font-bold italic animate-pulse">Scanning server for assets...</td></tr>
) : assets.length === 0 ? ( ) : assets.length === 0 ? (
<tr><td colSpan={5} className="p-12 text-center text-gray-500">No files uploaded yet.</td></tr> <tr>
<td colSpan={5} className="p-24 text-center">
<div className="flex flex-col items-center justify-center gap-4 text-slate-300 dark:text-gray-600">
<div className="w-20 h-20 rounded-3xl bg-slate-50 dark:bg-white/5 border-2 border-dashed border-slate-100 dark:border-white/5 flex items-center justify-center">
<FileIcon className="w-10 h-10 opacity-20" />
</div>
<p className="text-xs font-black uppercase tracking-widest">No assets found for this course</p>
</div>
</td>
</tr>
) : ( ) : (
assets.map((asset) => ( assets.map((asset) => (
<tr key={asset.id} className="hover:bg-white/5 transition-colors"> <tr key={asset.id} className="hover:bg-slate-50 dark:hover:bg-white/[0.02] transition-colors group">
<td className="p-4"> <td className="p-6">
<div className="flex items-center gap-3"> <div className="flex items-center gap-5">
{getIcon(asset.mimetype)} <div className="w-14 h-14 rounded-2xl bg-slate-50 dark:bg-white/5 border border-slate-100 dark:border-white/10 flex items-center justify-center shadow-sm group-hover:scale-110 transition-transform">
{getIcon(asset.mimetype)}
</div>
<div> <div>
<div className="font-medium text-gray-900 dark:text-white">{asset.filename}</div> <div className="font-black text-slate-900 dark:text-white group-hover:text-blue-600 dark:group-hover:text-blue-400 transition-colors uppercase tracking-tight text-sm">{asset.filename}</div>
<div className="text-xs text-blue-400">{getImageUrl(asset.storage_path.replace('uploads/', '/assets/'))}</div> <div className="text-[11px] text-blue-500/60 font-mono mt-1 break-all max-w-sm">{getImageUrl(asset.storage_path.replace('uploads/', '/assets/'))}</div>
</div> </div>
</div> </div>
</td> </td>
<td className="p-4 text-gray-400 font-mono text-sm">{asset.mimetype}</td> <td className="p-6 text-slate-400 dark:text-gray-500 font-mono text-xs uppercase font-bold">{asset.mimetype}</td>
<td className="p-4 text-gray-400 text-sm">{formatSize(asset.size_bytes)}</td> <td className="p-6 text-slate-900 dark:text-white font-black text-sm uppercase tracking-tighter">{formatSize(asset.size_bytes)}</td>
<td className="p-4 text-gray-400 text-sm">{new Date(asset.created_at).toLocaleDateString()}</td> <td className="p-6 text-slate-400 dark:text-gray-500 text-xs font-bold">{new Date(asset.created_at).toLocaleDateString()}</td>
<td className="p-4 text-right"> <td className="p-6 text-right">
<div className="flex justify-end gap-2"> <div className="flex justify-end gap-3">
<button <button
onClick={() => copyToClipboard(asset.storage_path.replace('uploads/', '/assets/'))} onClick={() => copyToClipboard(asset.storage_path.replace('uploads/', '/assets/'))}
title="Copy Internal URL" title="Copy Internal URL"
className="p-2 hover:bg-white/10 rounded-lg transition-colors text-blue-400" className="w-10 h-10 flex items-center justify-center bg-slate-100 dark:bg-white/5 hover:bg-blue-600 hover:text-white rounded-xl transition-all text-blue-500 shadow-sm active:scale-90"
> >
<Copy className="w-4 h-4" /> <Copy className="w-4 h-4" />
</button> </button>
<button <button
onClick={() => handleDelete(asset.id)} onClick={() => handleDelete(asset.id)}
title="Delete File" title="Delete File"
className="p-2 hover:bg-white/10 rounded-lg transition-colors text-red-400" className="w-10 h-10 flex items-center justify-center bg-slate-100 dark:bg-white/5 hover:bg-red-600 hover:text-white rounded-xl transition-all text-red-500 shadow-sm active:scale-90"
> >
<Trash2 className="w-4 h-4" /> <Trash2 className="w-4 h-4" />
</button> </button>
+95 -85
View File
@@ -149,9 +149,9 @@ export default function GradebookPage() {
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<button <button
onClick={() => setShowBulkEnroll(true)} onClick={() => setShowBulkEnroll(true)}
className="bg-white/5 hover:bg-white/10 border border-white/10 px-4 py-2 rounded-xl flex items-center gap-2 transition-all font-medium text-sm" className="bg-slate-50 dark:bg-white/5 hover:bg-slate-100 dark:hover:bg-white/10 border border-slate-200 dark:border-white/10 px-4 py-2 rounded-xl flex items-center gap-2 transition-all font-bold text-xs uppercase tracking-widest text-slate-600 dark:text-gray-400 shadow-sm"
> >
<Users size={16} /> Inscripción Masiva <Users size={16} className="text-blue-500" /> Inscripción Masiva
</button> </button>
<button <button
onClick={exportCSV} onClick={exportCSV}
@@ -166,66 +166,73 @@ export default function GradebookPage() {
{/* Bulk Enroll Modal */} {/* Bulk Enroll Modal */}
{showBulkEnroll && ( {showBulkEnroll && (
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/60 dark:bg-black/80 backdrop-blur-sm animate-in fade-in duration-300"> <div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-slate-900/40 dark:bg-black/80 backdrop-blur-md animate-in fade-in duration-300">
<div className="bg-white dark:bg-[#1a1d23] border border-slate-200 dark:border-white/10 rounded-2xl w-full max-w-2xl overflow-hidden shadow-2xl animate-in fade-in zoom-in duration-200"> <div className="bg-white dark:bg-[#1a1d23] border border-slate-200 dark:border-white/10 rounded-[2.5rem] w-full max-w-2xl overflow-hidden shadow-2xl animate-in zoom-in-95 duration-200">
<div className="p-6 border-b border-slate-100 dark:border-white/5 flex items-center justify-between"> <div className="p-8 border-b border-slate-100 dark:border-white/5 flex items-center justify-between bg-slate-50/50 dark:bg-transparent">
<h3 className="text-xl font-bold flex items-center gap-2 text-slate-900 dark:text-white"> <div>
<Users className="text-blue-500 dark:text-blue-400" /> Inscripción Masiva de Estudiantes <h3 className="text-2xl font-black flex items-center gap-3 text-slate-900 dark:text-white uppercase tracking-tight">
</h3> <Users className="text-blue-600 dark:text-blue-400" /> Inscripción Masiva
</h3>
<p className="text-xs text-slate-400 dark:text-gray-500 font-bold uppercase tracking-widest mt-1">Enroll multiple students at once</p>
</div>
<button <button
onClick={() => { onClick={() => {
setShowBulkEnroll(false); setShowBulkEnroll(false);
setBulkResults(null); setBulkResults(null);
setBulkEmails(""); setBulkEmails("");
}} }}
className="p-1 hover:bg-white/5 rounded-lg transition-colors" className="p-3 hover:bg-slate-200 dark:hover:bg-white/10 rounded-2xl transition-all group active:scale-95"
> >
<X size={20} /> <X size={20} className="text-slate-400 group-hover:text-slate-900 dark:group-hover:text-white" />
</button> </button>
</div> </div>
<div className="p-8 space-y-6"> <div className="p-8 space-y-8">
{!bulkResults ? ( {!bulkResults ? (
<> <>
<div> <div>
<label className="block text-sm font-medium text-slate-500 dark:text-gray-400 mb-2"> <label className="block text-xs font-black uppercase tracking-widest text-slate-400 dark:text-gray-500 mb-3">
Ingresa las direcciones de correo (separadas por comas o saltos de línea) Enter email addresses (separated by commas or new lines)
</label> </label>
<textarea <textarea
value={bulkEmails} value={bulkEmails}
onChange={(e) => setBulkEmails(e.target.value)} onChange={(e) => setBulkEmails(e.target.value)}
placeholder="student1@example.com&#10;student2@example.com" placeholder="student1@example.com&#10;student2@example.com"
className="w-full h-48 bg-slate-50 dark:bg-black/20 border border-slate-200 dark:border-white/10 rounded-xl p-4 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500/50 text-slate-900 dark:text-white placeholder-slate-400 dark:placeholder-gray-600 resize-none font-mono" className="w-full h-48 bg-slate-50 dark:bg-black/20 border border-slate-200 dark:border-white/10 rounded-3xl p-5 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500/50 text-slate-900 dark:text-white placeholder-slate-400 dark:placeholder-gray-600 resize-none font-mono shadow-inner leading-relaxed"
/> />
</div> </div>
<div className="bg-blue-500/10 border border-blue-500/20 rounded-xl p-4 flex gap-3 italic text-sm text-blue-400"> <div className="bg-blue-50 dark:bg-blue-500/10 border border-blue-100 dark:border-blue-500/20 rounded-2xl p-5 flex gap-4">
<AlertTriangle size={18} className="shrink-0" /> <div className="w-10 h-10 rounded-xl bg-white dark:bg-black/20 flex items-center justify-center shrink-0 shadow-sm text-blue-500">
Los estudiantes deben tener una cuenta en esta organización para ser inscritos. <AlertTriangle size={20} />
</div>
<p className="text-xs text-blue-800/80 dark:text-blue-300 leading-relaxed font-bold uppercase tracking-tight">
Students must already have an account in this organization directory to be successfully enrolled into the course.
</p>
</div> </div>
</> </>
) : ( ) : (
<div className="space-y-6"> <div className="space-y-8">
<div className="grid grid-cols-3 gap-4"> <div className="grid grid-cols-3 gap-6">
<div className="bg-green-500/10 border border-green-500/20 rounded-xl p-4 text-center"> <div className="bg-green-50 dark:bg-green-500/10 border border-green-100 dark:border-green-500/20 rounded-[2rem] p-6 text-center shadow-sm">
<div className="text-2xl font-black text-green-400">{bulkResults.successful_emails.length}</div> <div className="text-3xl font-black text-green-600 dark:text-green-400">{bulkResults.successful_emails.length}</div>
<div className="text-xs uppercase tracking-widest text-gray-500 font-bold mt-1">Enrolled</div> <div className="text-[10px] uppercase tracking-[0.2em] text-green-700/50 dark:text-gray-500 font-black mt-1">Enrolled</div>
</div> </div>
<div className="bg-yellow-500/10 border border-yellow-500/20 rounded-xl p-4 text-center"> <div className="bg-yellow-50 dark:bg-yellow-500/10 border border-yellow-100 dark:border-yellow-500/20 rounded-[2rem] p-6 text-center shadow-sm">
<div className="text-2xl font-black text-yellow-400">{bulkResults.already_enrolled_emails.length}</div> <div className="text-3xl font-black text-yellow-600 dark:text-yellow-400">{bulkResults.already_enrolled_emails.length}</div>
<div className="text-xs uppercase tracking-widest text-gray-500 font-bold mt-1">Skipped</div> <div className="text-[10px] uppercase tracking-[0.2em] text-yellow-700/50 dark:text-gray-500 font-black mt-1">Skipped</div>
</div> </div>
<div className="bg-red-500/10 border border-red-500/20 rounded-xl p-4 text-center"> <div className="bg-red-50 dark:bg-red-500/10 border border-red-100 dark:border-red-500/20 rounded-[2rem] p-6 text-center shadow-sm">
<div className="text-2xl font-black text-red-400">{bulkResults.failed_emails.length}</div> <div className="text-3xl font-black text-red-600 dark:text-red-400">{bulkResults.failed_emails.length}</div>
<div className="text-xs uppercase tracking-widest text-gray-500 font-bold mt-1">Failed</div> <div className="text-[10px] uppercase tracking-[0.2em] text-red-700/50 dark:text-gray-500 font-black mt-1">Failed</div>
</div> </div>
</div> </div>
{bulkResults.failed_emails.length > 0 && ( {bulkResults.failed_emails.length > 0 && (
<div> <div className="space-y-3">
<p className="text-xs font-bold text-gray-500 uppercase tracking-widest mb-2">Failed Emails (Not Found):</p> <p className="text-[10px] font-black text-red-500/60 dark:text-gray-500 uppercase tracking-[0.2em]">Failed Emails (Not Found):</p>
<div className="bg-black/20 rounded-xl p-3 text-xs text-red-400 max-h-32 overflow-y-auto font-mono"> <div className="bg-red-50/50 dark:bg-black/20 border border-red-100 dark:border-white/10 rounded-2xl p-5 text-xs text-red-600 dark:text-red-400 max-h-48 overflow-y-auto font-mono custom-scrollbar leading-relaxed">
{bulkResults.failed_emails.map(e => ( {bulkResults.failed_emails.map(e => (
<div key={e}>{e}</div> <div key={e} className="py-1 border-b border-red-500/5 last:border-0">{e}</div>
))} ))}
</div> </div>
</div> </div>
@@ -234,21 +241,21 @@ export default function GradebookPage() {
)} )}
</div> </div>
<div className="p-6 bg-slate-50 dark:bg-white/5 border-t border-slate-100 dark:border-white/5 flex justify-end gap-3"> <div className="p-8 bg-slate-50/50 dark:bg-white/5 border-t border-slate-100 dark:border-white/5 flex justify-end items-center gap-4">
{!bulkResults ? ( {!bulkResults ? (
<> <>
<button <button
onClick={() => setShowBulkEnroll(false)} onClick={() => setShowBulkEnroll(false)}
className="px-6 py-2 rounded-xl hover:bg-slate-200 dark:hover:bg-white/5 transition-colors font-medium text-slate-600 dark:text-gray-400" className="px-8 py-3 rounded-2xl hover:bg-slate-200 dark:hover:bg-white/5 transition-all font-black text-[10px] uppercase tracking-widest text-slate-400 dark:text-gray-400 active:scale-95"
> >
Cancel Cancel
</button> </button>
<button <button
onClick={handleBulkEnroll} onClick={handleBulkEnroll}
disabled={bulkLoading || !bulkEmails.trim()} disabled={bulkLoading || !bulkEmails.trim()}
className="btn-premium px-8 flex items-center gap-2 disabled:opacity-50 disabled:cursor-not-allowed" className="flex items-center gap-3 px-10 py-3 bg-blue-600 hover:bg-blue-500 disabled:bg-slate-200 dark:disabled:bg-white/5 text-white disabled:text-slate-400 rounded-2xl font-black text-[10px] uppercase tracking-[0.2em] shadow-xl shadow-blue-500/20 transition-all active:scale-95 disabled:shadow-none disabled:cursor-not-allowed group"
> >
{bulkLoading ? <Loader2 className="animate-spin w-4 h-4" /> : 'Procesar Inscripción'} {bulkLoading ? <Loader2 className="animate-spin w-4 h-4" /> : <CheckCircle size={16} className="group-hover:scale-110 transition-transform" />} Procesar Inscripción
</button> </button>
</> </>
) : ( ) : (
@@ -258,7 +265,7 @@ export default function GradebookPage() {
setBulkResults(null); setBulkResults(null);
setBulkEmails(""); setBulkEmails("");
}} }}
className="btn-premium px-8" className="btn-premium px-12 py-3 rounded-2xl font-black text-[10px] uppercase tracking-widest active:scale-95 shadow-xl shadow-blue-500/20"
> >
Done Done
</button> </button>
@@ -274,41 +281,41 @@ export default function GradebookPage() {
Registro de Calificaciones Registro de Calificaciones
</h2> </h2>
{/* Controls & Stats */} {/* Controls & Stats */}
<div className="flex flex-col md:flex-row gap-6 justify-between items-center bg-white/5 border border-white/10 rounded-2xl p-6"> {/* Controls & Stats */}
<div className="flex items-center gap-4 w-full md:w-auto"> <div className="flex flex-col md:flex-row gap-8 justify-between items-stretch bg-white dark:bg-white/5 border border-slate-200 dark:border-white/10 rounded-[2.5rem] p-8 shadow-sm">
<div className="relative"> <div className="flex flex-col sm:flex-row items-center gap-4 flex-1">
<div className="relative w-full sm:w-auto">
<Users size={16} className="absolute left-4 top-1/2 -translate-y-1/2 text-slate-400 dark:text-gray-500 pointer-events-none z-10" />
<select <select
value={selectedCohortId} value={selectedCohortId}
onChange={(e) => setSelectedCohortId(e.target.value)} onChange={(e) => setSelectedCohortId(e.target.value)}
className="appearance-none bg-slate-100 dark:bg-black/20 text-slate-900 dark:text-white border border-slate-200 dark:border-white/10 rounded-xl pl-4 pr-10 py-3 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500/50 min-w-[200px]" className="appearance-none bg-slate-50 dark:bg-black/20 text-slate-900 dark:text-white border border-slate-200 dark:border-white/10 rounded-2xl pl-11 pr-10 py-3.5 text-xs font-black uppercase tracking-widest focus:outline-none focus:ring-2 focus:ring-blue-500/50 min-w-full sm:min-w-[240px] shadow-inner cursor-pointer"
> >
<option value="">All Cohorts</option> <option value="">All Cohorts</option>
{cohorts.map(c => <option key={c.id} value={c.id}>{c.name}</option>)} {cohorts.map(c => <option key={c.id} value={c.id}>{c.name}</option>)}
</select> </select>
<div className="absolute right-3 top-1/2 -translate-y-1/2 pointer-events-none text-gray-500">
</div>
</div> </div>
<div className="relative flex-1 md:w-64"> <div className="relative w-full flex-1">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 text-gray-500 w-4 h-4" /> <Search className="absolute left-4 top-1/2 -translate-y-1/2 text-slate-400 dark:text-gray-500 w-4 h-4" />
<input <input
type="text" type="text"
placeholder="Search students..." placeholder="Find a student fast..."
value={searchQuery} value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)} onChange={(e) => setSearchQuery(e.target.value)}
className="w-full bg-slate-100 dark:bg-black/20 border border-slate-200 dark:border-white/10 rounded-xl pl-10 pr-4 py-3 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500/50 text-slate-900 dark:text-white placeholder-slate-400 dark:placeholder-gray-600" className="w-full bg-slate-50 dark:bg-black/20 border border-slate-200 dark:border-white/10 rounded-2xl pl-11 pr-4 py-3.5 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500/50 text-slate-900 dark:text-white placeholder-slate-400 dark:placeholder-gray-600 font-bold shadow-inner"
/> />
</div> </div>
</div> </div>
<div className="flex gap-8 border-l border-white/10 pl-8"> <div className="flex gap-10 border-l border-slate-100 dark:border-white/10 pl-10 items-center">
<div> <div>
<p className="text-xs font-bold text-slate-500 dark:text-gray-500 uppercase tracking-wider mb-1">Students</p> <p className="text-[10px] font-black text-slate-400 dark:text-gray-500 uppercase tracking-[0.2em] mb-1.5">Total Enrollment</p>
<p className="text-2xl font-black text-slate-900 dark:text-white">{filteredStudents.length}</p> <p className="text-3xl font-black text-slate-900 dark:text-white uppercase tracking-tighter">{filteredStudents.length}</p>
</div> </div>
<div className="w-px h-10 bg-slate-100 dark:bg-white/5" />
<div> <div>
<p className="text-xs font-bold text-gray-500 uppercase tracking-wider mb-1">Avg Score</p> <p className="text-[10px] font-black text-slate-400 dark:text-gray-500 uppercase tracking-[0.2em] mb-1.5">Avg Performance</p>
<p className={`text-2xl font-black ${averageScore < 0.7 ? 'text-orange-400' : 'text-green-400'}`}> <p className={`text-3xl font-black tracking-tighter ${averageScore < 0.7 ? 'text-orange-500' : 'text-blue-600'}`}>
{(averageScore * 100).toFixed(0)}% {(averageScore * 100).toFixed(0)}%
</p> </p>
</div> </div>
@@ -316,45 +323,45 @@ export default function GradebookPage() {
</div> </div>
{/* Student List */} {/* Student List */}
<div className="rounded-3xl border border-white/10 bg-white/[0.02] overflow-hidden"> <div className="rounded-[2.5rem] border border-slate-200 dark:border-white/10 bg-white dark:bg-white/[0.02] overflow-hidden shadow-sm">
<table className="w-full text-left border-collapse"> <table className="w-full text-left border-collapse">
<thead> <thead>
<tr className="bg-white/5 border-b border-white/5"> <tr className="bg-slate-50 dark:bg-white/5 border-b border-slate-200 dark:border-white/5">
<th className="p-6 text-xs font-black uppercase tracking-widest text-gray-500">Student</th> <th className="p-6 text-[10px] font-black uppercase tracking-[0.2em] text-slate-400 dark:text-gray-500">Student</th>
<th className="p-6 text-xs font-black uppercase tracking-widest text-gray-500">Progress</th> <th className="p-6 text-[10px] font-black uppercase tracking-[0.2em] text-slate-400 dark:text-gray-500">Learning Progress</th>
<th className="p-6 text-xs font-black uppercase tracking-widest text-gray-500">Avg. Score</th> <th className="p-6 text-[10px] font-black uppercase tracking-[0.2em] text-slate-400 dark:text-gray-500">Score Rating</th>
<th className="p-6 text-xs font-black uppercase tracking-widest text-gray-500">Last Active</th> <th className="p-6 text-[10px] font-black uppercase tracking-[0.2em] text-slate-400 dark:text-gray-500">Activity</th>
<th className="p-6 text-xs font-black uppercase tracking-widest text-gray-500 text-right">Status</th> <th className="p-6 text-[10px] font-black uppercase tracking-[0.2em] text-slate-400 dark:text-gray-500 text-right">Status</th>
</tr> </tr>
</thead> </thead>
<tbody className="divide-y divide-white/5"> <tbody className="divide-y divide-slate-100 dark:divide-white/5">
{filteredStudents.length === 0 ? ( {filteredStudents.length === 0 ? (
<tr> <tr>
<td colSpan={5} className="p-12 text-center text-gray-600 italic">No students found.</td> <td colSpan={5} className="p-12 text-center text-gray-600 italic">No students found.</td>
</tr> </tr>
) : filteredStudents.map((s) => ( ) : filteredStudents.map((s) => (
<tr key={s.user_id} className="hover:bg-white/[0.02] transition-colors group"> <tr key={s.user_id} className="hover:bg-slate-50 dark:hover:bg-white/[0.02] transition-colors group">
<td className="p-6"> <td className="p-6">
<div className="flex items-center gap-4"> <div className="flex items-center gap-5">
<div className="w-10 h-10 rounded-full bg-gradient-to-br from-blue-500 to-indigo-600 flex items-center justify-center text-gray-900 dark:text-white font-bold text-sm"> <div className="w-12 h-12 rounded-2xl bg-gradient-to-br from-blue-500 to-indigo-600 flex items-center justify-center text-white font-black text-lg shadow-lg shadow-blue-500/20">
{s.full_name.charAt(0)} {s.full_name.charAt(0)}
</div> </div>
<div> <div>
<div className="font-bold text-gray-900 dark:text-white group-hover:text-blue-400 transition-colors">{s.full_name}</div> <div className="font-black text-slate-900 dark:text-white group-hover:text-blue-600 dark:group-hover:text-blue-400 transition-colors uppercase tracking-tight text-sm">{s.full_name}</div>
<div className="text-xs text-gray-500 flex items-center gap-1 mt-0.5"> <div className="text-[11px] text-slate-400 dark:text-gray-500 flex items-center gap-1.5 mt-1 font-medium italic">
<Mail size={10} /> {s.email} <Mail size={12} className="text-blue-400" /> {s.email}
</div> </div>
</div> </div>
</div> </div>
</td> </td>
<td className="p-6 align-middle"> <td className="p-6 align-middle">
<div className="w-full max-w-[140px]"> <div className="w-full max-w-[160px]">
<div className="flex justify-between text-xs mb-1 font-bold text-gray-400"> <div className="flex justify-between text-[10px] mb-2 font-black text-slate-400 uppercase tracking-widest">
<span>{(s.progress * 100).toFixed(0)}%</span> <span>{(s.progress * 100).toFixed(0)}% Complete</span>
</div> </div>
<div className="h-1.5 bg-white/10 rounded-full overflow-hidden"> <div className="h-2 bg-slate-100 dark:bg-white/10 rounded-full overflow-hidden shadow-inner">
<div <div
className="h-full bg-blue-500 rounded-full" className="h-full bg-gradient-to-r from-blue-600 to-blue-400 rounded-full shadow-lg shadow-blue-500/30 transition-all duration-500"
style={{ width: `${s.progress * 100}%` }} style={{ width: `${s.progress * 100}%` }}
/> />
</div> </div>
@@ -362,31 +369,34 @@ export default function GradebookPage() {
</td> </td>
<td className="p-6 align-middle"> <td className="p-6 align-middle">
{s.average_score !== null ? ( {s.average_score !== null ? (
<span className={`font-black ${(s.average_score || 0) >= 0.8 ? 'text-green-400' : (s.average_score || 0) >= 0.6 ? 'text-yellow-400' : 'text-red-400'}`}> <div className="flex flex-col">
{((s.average_score || 0) * 100).toFixed(0)}% <span className={`text-lg font-black tracking-tighter ${(s.average_score || 0) >= 0.8 ? 'text-green-600 dark:text-green-400' : (s.average_score || 0) >= 0.6 ? 'text-orange-500' : 'text-red-500'}`}>
</span> {((s.average_score || 0) * 100).toFixed(0)}%
</span>
<span className="text-[9px] font-black uppercase tracking-widest text-slate-400 mt-0.5">Weighted Avg</span>
</div>
) : ( ) : (
<span className="text-gray-600 text-xs italic">No grades</span> <span className="text-slate-400 dark:text-gray-600 text-[10px] font-black uppercase tracking-widest italic bg-slate-100 dark:bg-white/5 px-2 py-1 rounded-md">Pending</span>
)} )}
</td> </td>
<td className="p-6 align-middle"> <td className="p-6 align-middle">
<div className="text-sm text-gray-400 flex items-center gap-2"> <div className="text-xs text-slate-500 dark:text-gray-400 font-bold flex items-center gap-2 uppercase tracking-tight">
<Clock size={14} /> <Clock size={16} className="text-slate-400" />
{s.last_active_at ? new Date(s.last_active_at).toLocaleDateString() : 'Never'} {s.last_active_at ? new Date(s.last_active_at).toLocaleDateString() : 'No Activity'}
</div> </div>
</td> </td>
<td className="p-6 text-right align-middle"> <td className="p-6 text-right align-middle">
{s.progress >= 1 ? ( {s.progress >= 1 ? (
<span className="inline-flex items-center gap-1.5 px-3 py-1 rounded-full text-[10px] font-black uppercase tracking-widest bg-green-500/10 text-green-400 border border-green-500/20"> <span className="inline-flex items-center gap-2 px-3.5 py-1.5 rounded-full text-[10px] font-black uppercase tracking-[0.2em] bg-green-500/10 text-green-600 dark:text-green-400 border border-green-500/20 shadow-sm">
<CheckCircle size={12} /> Completed <CheckCircle size={12} /> Graduated
</span> </span>
) : s.last_active_at && (Date.now() - new Date(s.last_active_at).getTime() > 7 * 24 * 60 * 60 * 1000) ? ( ) : s.last_active_at && (Date.now() - new Date(s.last_active_at).getTime() > 7 * 24 * 60 * 60 * 1000) ? (
<span className="inline-flex items-center gap-1.5 px-3 py-1 rounded-full text-[10px] font-black uppercase tracking-widest bg-red-500/10 text-red-400 border border-red-500/20"> <span className="inline-flex items-center gap-2 px-3.5 py-1.5 rounded-full text-[10px] font-black uppercase tracking-[0.2em] bg-red-500/10 text-red-600 dark:text-red-400 border border-red-500/20 shadow-sm">
<AlertCircle size={12} /> Inactive <AlertCircle size={12} /> At Risk
</span> </span>
) : ( ) : (
<span className="inline-flex items-center gap-1.5 px-3 py-1 rounded-full text-[10px] font-black uppercase tracking-widest bg-blue-500/10 text-blue-400 border border-blue-500/20"> <span className="inline-flex items-center gap-2 px-3.5 py-1.5 rounded-full text-[10px] font-black uppercase tracking-[0.2em] bg-blue-500/10 text-blue-600 dark:text-blue-400 border border-blue-500/20 shadow-sm">
<GraduationCap size={12} /> Active <GraduationCap size={12} /> Engaged
</span> </span>
)} )}
</td> </td>
@@ -10,7 +10,8 @@ import {
AlertCircle, AlertCircle,
CheckCircle2, CheckCircle2,
TrendingUp, TrendingUp,
Settings Settings,
Loader2
} from "lucide-react"; } from "lucide-react";
import { clsx, type ClassValue } from "clsx"; import { clsx, type ClassValue } from "clsx";
import { twMerge } from "tailwind-merge"; import { twMerge } from "tailwind-merge";
@@ -84,13 +85,15 @@ export default function GradingPolicyPage() {
pageDescription="Configura los tipos de evaluación y la distribución de pesos del curso." pageDescription="Configura los tipos de evaluación y la distribución de pesos del curso."
pageActions={ pageActions={
<div className={cn( <div className={cn(
"flex items-center gap-2 px-4 py-2 rounded-xl border transition-all duration-500", "flex items-center gap-3 px-6 py-2.5 rounded-2xl border transition-all duration-500 shadow-sm",
isBalanced ? "bg-green-500/10 border-green-500/30 text-green-400 shadow-lg shadow-green-500/5" isBalanced ? "bg-green-50 dark:bg-green-500/10 border-green-200 dark:border-green-500/30 text-green-600 dark:text-green-400"
: "bg-amber-500/10 border-amber-500/30 text-amber-400" : "bg-amber-50 dark:bg-amber-500/10 border-amber-200 dark:border-amber-500/30 text-amber-600 dark:text-amber-400"
)}> )}>
{isBalanced ? <CheckCircle2 className="w-5 h-5" /> : <AlertCircle className="w-5 h-5" />} {isBalanced ? <CheckCircle2 className="w-5 h-5" /> : <AlertCircle className="w-5 h-5 animate-pulse" />}
<span className="font-bold text-lg">{totalWeight}%</span> <div className="flex flex-col items-start leading-none">
<span className="text-sm opacity-70">Total Weight</span> <span className="font-black text-xl tracking-tighter">{totalWeight}%</span>
<span className="text-[9px] font-black uppercase tracking-widest opacity-60">Total Defined</span>
</div>
</div> </div>
} }
> >
@@ -98,37 +101,38 @@ export default function GradingPolicyPage() {
<div className="grid grid-cols-1 lg:grid-cols-3 gap-8"> <div className="grid grid-cols-1 lg:grid-cols-3 gap-8">
{/* Categories List */} {/* Categories List */}
<div className="lg:col-span-2 space-y-4"> <div className="lg:col-span-2 space-y-4">
<h2 className="text-sm font-semibold uppercase tracking-wider text-slate-500 dark:text-gray-500 mb-6 flex items-center gap-2"> <h2 className="text-[10px] font-black uppercase tracking-[0.2em] text-slate-400 dark:text-gray-500 mb-8 flex items-center gap-2.5">
<Settings className="w-4 h-4" /> Assessment Categories <Settings className="w-4 h-4 text-blue-500" /> Course Grading Structure
</h2> </h2>
{categories.length === 0 ? ( {categories.length === 0 ? (
<div className="bg-slate-50 dark:bg-white/5 border border-slate-100 dark:border-white/10 rounded-2xl p-12 text-center"> <div className="bg-slate-50 dark:bg-white/5 border-2 border-dashed border-slate-200 dark:border-white/10 rounded-[2.5rem] p-24 text-center shadow-inner">
<TrendingUp className="w-12 h-12 text-slate-400 dark:text-gray-600 mx-auto mb-4" /> <TrendingUp className="w-12 h-12 text-slate-400 dark:text-gray-600 mx-auto mb-6 opacity-40 shadow-sm" />
<p className="text-slate-500 dark:text-gray-400 italic">No grading categories defined yet.</p> <p className="text-xs font-black uppercase tracking-widest text-slate-400 dark:text-gray-500 italic">No grading categories defined for this curriculum</p>
</div> </div>
) : ( ) : (
categories.map((cat) => ( categories.map((cat) => (
<div <div
key={cat.id} key={cat.id}
className="group bg-slate-50 dark:bg-white/5 border border-slate-100 dark:border-white/10 p-6 rounded-2xl flex items-center justify-between hover:border-blue-500/50 hover:bg-slate-100 dark:hover:bg-white/[0.07] transition-all duration-300" className="group bg-white dark:bg-white/5 border border-slate-200 dark:border-white/10 p-6 rounded-3xl flex items-center justify-between hover:bg-slate-50 dark:hover:bg-white/[0.08] transition-all shadow-sm"
> >
<div className="flex items-center gap-4"> <div className="flex items-center gap-5">
<div className="w-12 h-12 rounded-xl bg-blue-500/10 flex items-center justify-center text-blue-600 dark:text-blue-400 font-bold group-hover:scale-110 transition-transform"> <div className="w-16 h-16 rounded-2xl bg-blue-50 dark:bg-blue-500/10 border border-blue-100 dark:border-blue-500/20 flex flex-col items-center justify-center text-blue-600 dark:text-blue-400 shadow-sm group-hover:scale-110 transition-transform">
{cat.weight}% <span className="text-2xl font-black">{cat.weight}</span>
<span className="text-[10px] font-black uppercase tracking-tighter">Weight</span>
</div> </div>
<div> <div>
<h3 className="text-lg font-semibold text-slate-900 dark:text-white">{cat.name}</h3> <h3 className="text-lg font-black text-slate-900 dark:text-white uppercase tracking-tight">{cat.name}</h3>
<div className="flex items-center gap-2 mt-1"> <div className="flex items-center gap-2 mt-1">
<span className="text-xs text-slate-500 dark:text-gray-500 bg-slate-100 dark:bg-white/5 px-2 py-0.5 rounded-full capitalize"> <span className="text-[10px] font-black uppercase tracking-widest text-blue-500/60 dark:text-gray-500 bg-slate-100 dark:bg-white/5 px-2.5 py-1 rounded-md">
Weight: {cat.weight}% Primary Assessment Category
</span> </span>
</div> </div>
</div> </div>
</div> </div>
<button <button
onClick={() => handleDelete(cat.id)} onClick={() => handleDelete(cat.id)}
className="p-3 bg-red-500/10 text-red-500 dark:text-red-400 rounded-xl opacity-0 group-hover:opacity-100 hover:bg-red-500 hover:text-white transition-all duration-300" className="p-4 bg-red-50 dark:bg-red-500/10 text-red-500 dark:text-red-400 rounded-2xl opacity-0 group-hover:opacity-100 hover:bg-red-600 hover:text-white transition-all shadow-sm active:scale-95"
> >
<Trash2 className="w-5 h-5" /> <Trash2 className="w-5 h-5" />
</button> </button>
@@ -139,20 +143,20 @@ export default function GradingPolicyPage() {
{/* Add New Category Form */} {/* Add New Category Form */}
<div className="space-y-6"> <div className="space-y-6">
<div className="bg-slate-50 dark:bg-gradient-to-br dark:from-gray-800/50 dark:to-gray-900/50 p-8 rounded-3xl border border-slate-200 dark:border-white/10 sticky top-8"> <div className="bg-white dark:bg-white/5 p-10 rounded-[2.5rem] border border-slate-200 dark:border-white/10 sticky top-8 shadow-sm">
<h2 className="text-xl font-bold mb-6 flex items-center gap-2 text-slate-900 dark:text-white"> <h2 className="text-2xl font-black mb-8 flex items-center gap-3 text-slate-900 dark:text-white uppercase tracking-tight">
<Plus className="w-5 h-5 text-blue-500 dark:text-blue-400" /> New Format <Plus className="w-6 h-6 text-blue-500" /> New Format
</h2> </h2>
<div className="space-y-4"> <div className="space-y-4">
<div> <div className="space-y-3">
<label className="text-xs font-semibold text-slate-500 dark:text-gray-400 uppercase tracking-widest ml-1">Assessment Type</label> <label className="text-[10px] font-black uppercase tracking-[0.2em] text-slate-400 dark:text-gray-500 ml-1">Assessment Type</label>
<select <select
value={newName} value={newName}
onChange={(e) => setNewName(e.target.value)} onChange={(e) => setNewName(e.target.value)}
className="w-full bg-slate-100 dark:bg-white/5 border border-slate-200 dark:border-white/10 rounded-xl px-4 py-3 mt-1.5 focus:outline-none focus:border-blue-500 transition-all text-slate-900 dark:text-gray-100 appearance-none" className="w-full bg-slate-50 dark:bg-black/20 border border-slate-200 dark:border-white/10 rounded-2xl px-5 py-4 focus:outline-none focus:ring-2 focus:ring-blue-500/50 transition-all text-slate-900 dark:text-gray-100 appearance-none font-bold shadow-inner"
> >
<option value="">Select a type...</option> <option value="">Choose assessment...</option>
<option value="Continuous Assessment">Continuous Assessment (Min 4)</option> <option value="Continuous Assessment">Continuous Assessment (Min 4)</option>
<option value="Midterm">Midterm</option> <option value="Midterm">Midterm</option>
<option value="Final Test">Final Test</option> <option value="Final Test">Final Test</option>
@@ -160,26 +164,26 @@ export default function GradingPolicyPage() {
</select> </select>
</div> </div>
<div> <div className="space-y-3">
<label className="text-xs font-semibold text-slate-500 dark:text-gray-400 uppercase tracking-widest ml-1">Weight (%)</label> <label className="text-[10px] font-black uppercase tracking-[0.2em] text-slate-400 dark:text-gray-500 ml-1">Weight Value (%)</label>
<div className="relative mt-1.5"> <div className="relative">
<input <input
type="number" type="number"
placeholder="20" placeholder="20"
value={newWeight || ""} value={newWeight || ""}
onChange={(e) => setNewWeight(parseInt(e.target.value) || 0)} onChange={(e) => setNewWeight(parseInt(e.target.value) || 0)}
className="w-full bg-slate-100 dark:bg-white/5 border border-slate-200 dark:border-white/10 rounded-xl px-4 py-3 focus:outline-none focus:border-blue-500 transition-all text-slate-900 dark:text-gray-100 pl-10" className="w-full bg-slate-50 dark:bg-black/20 border border-slate-200 dark:border-white/10 rounded-2xl px-5 py-4 focus:outline-none focus:ring-2 focus:ring-blue-500/50 transition-all text-slate-900 dark:text-gray-100 pl-11 font-black text-lg shadow-inner"
/> />
<Percent className="w-4 h-4 text-slate-400 dark:text-gray-500 absolute left-4 top-1/2 -translate-y-1/2" /> <Percent className="w-4 h-4 text-blue-500 absolute left-4 top-1/2 -translate-y-1/2" />
</div> </div>
</div> </div>
<button <button
onClick={handleAdd} onClick={handleAdd}
disabled={submitting || !newName || newWeight <= 0} disabled={submitting || !newName || newWeight <= 0}
className="w-full bg-blue-600 hover:bg-blue-500 disabled:bg-slate-200 dark:disabled:bg-gray-700 disabled:text-slate-400 dark:disabled:text-gray-500 text-white font-bold py-4 rounded-2xl mt-4 transition-all shadow-lg shadow-blue-500/20 active:scale-95 flex items-center justify-center gap-2" className="w-full bg-blue-600 hover:bg-blue-500 disabled:bg-slate-100 dark:disabled:bg-white/5 disabled:text-slate-400 dark:disabled:text-gray-500 text-white font-black py-4 rounded-[1.5rem] mt-6 transition-all shadow-xl shadow-blue-500/20 active:scale-95 flex items-center justify-center gap-3 uppercase tracking-[0.2em] text-[10px]"
> >
{submitting ? "Adding..." : ( {submitting ? <Loader2 className="w-4 h-4 animate-spin" /> : (
<> <>
<Plus className="w-5 h-5" /> <Plus className="w-5 h-5" />
Add Category Add Category
@@ -188,10 +192,12 @@ export default function GradingPolicyPage() {
</button> </button>
{!isBalanced && ( {!isBalanced && (
<div className="mt-6 p-4 rounded-xl bg-amber-500/10 border border-amber-500/20 flex gap-3"> <div className="mt-8 p-5 rounded-2xl bg-amber-50 dark:bg-amber-500/10 border border-amber-100 dark:border-amber-500/20 flex gap-4">
<AlertCircle className="w-5 h-5 text-amber-600 dark:text-amber-500 shrink-0 mt-0.5" /> <div className="w-10 h-10 rounded-xl bg-white dark:bg-black/20 flex items-center justify-center shrink-0 shadow-sm text-amber-500">
<p className="text-sm text-amber-800 dark:text-amber-200/80 leading-relaxed"> <AlertCircle size={20} className="animate-pulse" />
The total weight of all categories must be exactly 100% for the course to be valid for certification. Currently: <strong>{totalWeight}%</strong> </div>
<p className="text-[11px] text-amber-900/80 dark:text-amber-200/80 leading-relaxed font-bold uppercase tracking-tight">
The total weight must be exactly 100% for certification eligibility. Current sum: <span className="text-amber-600 font-black">{totalWeight}%</span>
</p> </p>
</div> </div>
)} )}
@@ -7,6 +7,7 @@ import { cmsApi, Lesson, Block, GradingCategory, LibraryBlock, Rubric, RubricLev
import { import {
Layout, Layout,
CheckCircle2, CheckCircle2,
Check,
Pencil, Pencil,
Save, Save,
Trash2, Trash2,
@@ -368,10 +369,10 @@ export default function LessonEditor({ params }: { params: { id: string; lessonI
<div className="max-w-4xl mx-auto space-y-12 pb-40 px-4"> <div className="max-w-4xl mx-auto space-y-12 pb-40 px-4">
<div className="flex flex-col md:flex-row justify-between items-start md:items-center gap-6 border-b border-slate-200 dark:border-white/5 pb-8"> <div className="flex flex-col md:flex-row justify-between items-start md:items-center gap-6 border-b border-slate-200 dark:border-white/5 pb-8">
<div className="space-y-1"> <div className="space-y-1">
<div className="flex items-center gap-2 text-[10px] text-blue-600 dark:text-blue-500 font-bold uppercase tracking-[0.2em]"> <div className="flex items-center gap-3 text-[10px] font-black uppercase tracking-[0.2em]">
<Link href={`/courses/${params.id}`} className="hover:text-slate-900 dark:text-white transition-colors">Outline</Link> <Link href={`/courses/${params.id}`} className="text-slate-400 hover:text-blue-600 dark:hover:text-blue-400 transition-colors">Outline</Link>
<span className="text-slate-300 dark:text-gray-700">/</span> <span className="text-slate-300 dark:text-gray-700">/</span>
<span>Activity</span> <span className="text-blue-600 dark:text-blue-500">Activity Builder</span>
</div> </div>
<div className="flex items-center gap-4"> <div className="flex items-center gap-4">
{editingId === 'lesson-title' ? ( {editingId === 'lesson-title' ? (
@@ -381,10 +382,10 @@ export default function LessonEditor({ params }: { params: { id: string; lessonI
value={editValue} value={editValue}
onChange={(e) => setEditValue(e.target.value)} onChange={(e) => setEditValue(e.target.value)}
onKeyDown={(e) => e.key === 'Enter' && handleSaveLessonTitle()} onKeyDown={(e) => e.key === 'Enter' && handleSaveLessonTitle()}
className="text-4xl font-black bg-transparent border-b-2 border-blue-500 focus:outline-none" className="text-4xl font-black bg-slate-50 dark:bg-white/5 border border-slate-200 dark:border-white/10 rounded-[1.5rem] px-6 py-3 focus:outline-none focus:ring-4 focus:ring-blue-500/20 transition-all text-slate-900 dark:text-white shadow-inner"
/> />
<button onClick={handleSaveLessonTitle} className="text-green-400"><Save className="w-6 h-6" /></button> <button onClick={handleSaveLessonTitle} className="p-3 bg-green-500 text-white rounded-2xl shadow-xl shadow-green-500/20 active:scale-95 transition-all"><Save className="w-6 h-6" /></button>
<button onClick={() => setEditingId(null)} className="text-gray-400"><X className="w-6 h-6" /></button> <button onClick={() => setEditingId(null)} className="p-3 bg-slate-100 dark:bg-white/10 text-slate-400 rounded-2xl active:scale-95 transition-all"><X className="w-6 h-6" /></button>
</div> </div>
) : ( ) : (
<div className="flex items-center gap-4 group"> <div className="flex items-center gap-4 group">
@@ -402,28 +403,43 @@ export default function LessonEditor({ params }: { params: { id: string; lessonI
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
{editMode ? ( {editMode ? (
<> <div className="flex items-center gap-4">
<button onClick={() => setEditMode(false)} className="px-6 py-2.5 glass text-xs font-bold uppercase tracking-widest hover:bg-slate-50 dark:hover:bg-white/5 transition-all text-slate-600 dark:text-white">Discard</button> <button
<button onClick={handleSave} disabled={isSaving} className="btn-premium px-8 py-2.5 min-w-[140px] text-xs font-bold uppercase tracking-widest text-white"> onClick={() => setEditMode(false)}
{isSaving ? "Saving..." : "Publish Changes"} className="px-8 py-3 bg-white dark:bg-white/5 border border-slate-200 dark:border-white/10 rounded-2xl text-[10px] font-black uppercase tracking-[0.2em] hover:bg-slate-50 dark:hover:bg-white/10 transition-all text-slate-500 dark:text-white active:scale-95 shadow-sm"
>
Discard
</button> </button>
</> <button
onClick={handleSave}
disabled={isSaving}
className="px-10 py-3 bg-blue-600 text-white rounded-2xl font-black text-[10px] uppercase tracking-[0.2em] shadow-xl shadow-blue-500/30 hover:bg-blue-500 transition-all active:scale-95 disabled:bg-slate-200"
>
{isSaving ? "Saving..." : "Save Changes"}
</button>
</div>
) : ( ) : (
<button onClick={() => setEditMode(true)} className="px-8 py-3 glass text-xs font-bold uppercase tracking-widest hover:border-blue-500/50 transition-all flex items-center gap-2 group text-slate-700 dark:text-white"> <button
<span className="group-hover:rotate-12 transition-transform"></span> Edit Activity onClick={() => setEditMode(true)}
className="group px-10 py-3.5 bg-white dark:bg-white/5 border border-slate-200 dark:border-white/10 rounded-[1.5rem] text-[10px] font-black uppercase tracking-[0.3em] hover:border-blue-500/50 hover:shadow-xl transition-all flex items-center gap-3 text-slate-700 dark:text-white active:scale-95 shadow-md"
>
<span className="group-hover:rotate-12 transition-transform text-lg"></span> Edit Activity
</button> </button>
)} )}
</div> </div>
</div> </div>
{editMode && ( {editMode && (
<div className="bg-slate-50 dark:bg-white/5 border border-slate-200 dark:border-white/10 rounded-3xl p-8 space-y-6 animate-in fade-in slide-in-from-top-4 duration-500"> <div className="bg-white dark:bg-white/5 border border-slate-200 dark:border-white/10 rounded-[2.5rem] p-10 space-y-8 animate-in fade-in slide-in-from-top-4 duration-500 shadow-sm hover:shadow-md transition-all">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div> <div>
<h3 className="text-xl font-bold flex items-center gap-2 text-slate-900 dark:text-white"> <h3 className="text-2xl font-black flex items-center gap-3 text-slate-900 dark:text-white uppercase tracking-tight">
<span className="text-blue-500"></span> Grading Configuration <div className="w-12 h-12 rounded-2xl bg-indigo-50 dark:bg-indigo-500/10 border border-indigo-100 dark:border-indigo-500/20 flex items-center justify-center text-indigo-600 dark:text-indigo-400 shadow-sm">
<Target size={24} />
</div>
Grading Policy
</h3> </h3>
<p className="text-sm text-slate-500 dark:text-gray-500 mt-1">Determine if this activity contributes to the final grade</p> <p className="text-sm font-medium text-slate-500 dark:text-gray-500 mt-2 ml-15">Configure how this activity impacts the student gradebook</p>
</div> </div>
<label className="relative inline-flex items-center cursor-pointer group"> <label className="relative inline-flex items-center cursor-pointer group">
<input <input
@@ -432,19 +448,22 @@ export default function LessonEditor({ params }: { params: { id: string; lessonI
onChange={(e) => setIsGraded(e.target.checked)} onChange={(e) => setIsGraded(e.target.checked)}
className="sr-only peer" className="sr-only peer"
/> />
<div className="w-14 h-8 bg-slate-200 dark:bg-gray-700 peer-focus:outline-none rounded-full peer peer-checked:after:translate-x-full rtl:peer-checked:after:-translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[4px] after:start-[4px] after:bg-white after:border-slate-300 dark:after:border-gray-300 after:border after:rounded-full after:h-6 after:w-6 after:transition-all peer-checked:bg-blue-600 group-hover:after:scale-110 transition-all shadow-sm"></div> <div className="w-16 h-9 bg-slate-200 dark:bg-gray-700 peer-focus:outline-none rounded-full peer peer-checked:after:translate-x-full rtl:peer-checked:after:-translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[4.5px] after:start-[5px] after:bg-white after:border-slate-300 dark:after:border-gray-300 after:border after:rounded-full after:h-7 after:w-7 after:transition-all peer-checked:bg-blue-600 group-hover:after:scale-105 transition-all shadow-md"></div>
<span className="ms-3 text-sm font-bold uppercase tracking-widest text-slate-400 dark:text-gray-400 peer-checked:text-blue-600 dark:peer-checked:text-blue-400 transition-colors"> <span className="ms-4 text-[10px] font-black uppercase tracking-[0.2em] text-slate-400 dark:text-gray-400 peer-checked:text-blue-600 dark:peer-checked:text-blue-400 transition-colors">
{isGraded ? "Graded" : "Not Graded"} {isGraded ? "Graded" : "Unscored"}
</span> </span>
</label> </label>
</div> </div>
<div className="flex items-center justify-between pt-6 border-t border-slate-200 dark:border-white/5"> <div className="flex items-center justify-between pt-10 border-t border-slate-100 dark:border-white/5">
<div> <div>
<h3 className="text-xl font-bold flex items-center gap-2 text-slate-900 dark:text-white"> <h3 className="text-2xl font-black flex items-center gap-3 text-slate-900 dark:text-white uppercase tracking-tight">
<span className="text-blue-500">🔓</span> Course Preview <div className="w-12 h-12 rounded-2xl bg-amber-50 dark:bg-amber-500/10 border border-amber-100 dark:border-amber-500/20 flex items-center justify-center text-amber-600 dark:text-amber-400 shadow-sm">
<Eye size={24} />
</div>
Discovery Settings
</h3> </h3>
<p className="text-sm text-slate-500 dark:text-gray-500 mt-1">Allow students to view this lesson without being enrolled</p> <p className="text-sm font-medium text-slate-500 dark:text-gray-500 mt-2 ml-15">Control how this lesson appears to prospective students</p>
</div> </div>
<label className="relative inline-flex items-center cursor-pointer group"> <label className="relative inline-flex items-center cursor-pointer group">
<input <input
@@ -453,96 +472,112 @@ export default function LessonEditor({ params }: { params: { id: string; lessonI
onChange={(e) => setIsPreviewable(e.target.checked)} onChange={(e) => setIsPreviewable(e.target.checked)}
className="sr-only peer" className="sr-only peer"
/> />
<div className="w-14 h-8 bg-slate-200 dark:bg-gray-700 peer-focus:outline-none rounded-full peer peer-checked:after:translate-x-full rtl:peer-checked:after:-translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[4px] after:start-[4px] after:bg-white after:border-slate-300 dark:after:border-gray-300 after:border after:rounded-full after:h-6 after:w-6 after:transition-all peer-checked:bg-blue-600 group-hover:after:scale-110 transition-all shadow-sm"></div> <div className="w-16 h-9 bg-slate-200 dark:bg-gray-700 peer-focus:outline-none rounded-full peer peer-checked:after:translate-x-full rtl:peer-checked:after:-translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[4.5px] after:start-[5px] after:bg-white after:border-slate-300 dark:after:border-gray-300 after:border after:rounded-full after:h-7 after:w-7 after:transition-all peer-checked:bg-amber-600 group-hover:after:scale-105 transition-all shadow-md"></div>
<span className="ms-3 text-sm font-bold uppercase tracking-widest text-slate-400 dark:text-gray-400 peer-checked:text-blue-600 dark:peer-checked:text-blue-400 transition-colors"> <span className="ms-4 text-[10px] font-black uppercase tracking-[0.2em] text-slate-400 dark:text-gray-400 peer-checked:text-amber-600 dark:peer-checked:text-amber-400 transition-colors">
{isPreviewable ? "Preview Enabled" : "Preview Disabled"} {isPreviewable ? "Preview Public" : "Private"}
</span> </span>
</label> </label>
</div> </div>
{isGraded && ( {isGraded && (
<> <>
<div className="grid grid-cols-1 md:grid-cols-2 gap-8"> <div className="grid grid-cols-1 md:grid-cols-2 gap-10">
<div className="space-y-4"> <div className="space-y-4">
<span className="text-[10px] font-black uppercase tracking-widest text-slate-500 dark:text-gray-500 mb-2 block">Assessment Category</span> <span className="text-[10px] font-black uppercase tracking-[0.2em] text-slate-400 dark:text-gray-500 mb-2 block">Assessment Category</span>
<select <div className="relative group/select">
value={selectedCategoryId} <select
onChange={(e) => setSelectedCategoryId(e.target.value)} value={selectedCategoryId}
className="w-full bg-slate-100 dark:bg-white/5 border border-slate-200 dark:border-white/10 rounded-xl px-4 py-3 text-sm focus:outline-none focus:border-blue-500 transition-all appearance-none font-bold text-slate-900 dark:text-white" onChange={(e) => setSelectedCategoryId(e.target.value)}
> className="w-full bg-slate-50 dark:bg-white/5 border border-slate-200 dark:border-white/10 rounded-[1.25rem] px-5 py-4 text-sm focus:outline-none focus:ring-4 focus:ring-blue-500/20 transition-all appearance-none font-bold text-slate-900 dark:text-white shadow-inner"
<option value="">Select Category...</option> >
{gradingCategories.map((cat) => ( <option value="">Select Category...</option>
<option key={cat.id} value={cat.id}> {gradingCategories.map((cat) => (
{cat.name} ({cat.weight}%) <option key={cat.id} value={cat.id}>
</option> {cat.name} ({cat.weight}%)
))} </option>
</select> ))}
<div className="text-[10px] text-slate-500 dark:text-gray-500 italic mt-1 pl-1"> </select>
Manage categories in <Link href={`/courses/${params.id}/grading`} className="text-blue-600 dark:text-blue-400 hover:underline">Grading Policy</Link> <div className="absolute right-5 top-1/2 -translate-y-1/2 pointer-events-none text-slate-400">
<ChevronDown size={18} />
</div>
</div>
<div className="text-[10px] text-slate-400 dark:text-gray-500 italic mt-2 pl-2 flex items-center gap-2">
<div className="w-1 h-1 rounded-full bg-slate-300"></div>
Define rules in <Link href={`/courses/${params.id}/grading`} className="text-blue-600 dark:text-blue-400 hover:text-blue-700 font-black uppercase tracking-widest decoration-blue-500/30 underline-offset-4 underline">Grading Policy</Link>
</div> </div>
</div> </div>
<div className="space-y-4"> <div className="space-y-4">
<span className="text-[10px] font-black uppercase tracking-widest text-slate-500 dark:text-gray-500 mb-2 block">Rubric (Optional)</span> <span className="text-[10px] font-black uppercase tracking-[0.2em] text-slate-400 dark:text-gray-500 mb-2 block">Evaluation Rubric (Optional)</span>
<div className="bg-slate-100 dark:bg-white/5 border border-slate-200 dark:border-white/10 rounded-xl p-4 max-h-48 overflow-y-auto space-y-2"> <div className="bg-slate-50 dark:bg-black/20 border border-slate-100 dark:border-white/10 rounded-[1.5rem] p-6 max-h-56 overflow-y-auto space-y-3 shadow-inner">
{courseRubrics.length === 0 ? ( {courseRubrics.length === 0 ? (
<p className="text-xs text-slate-400 dark:text-gray-500 italic p-2 text-center">No rubrics found in this course.</p> <div className="flex flex-col items-center justify-center p-8 text-center text-slate-400 dark:text-gray-500 italic">
<div className="w-10 h-10 bg-slate-100 dark:bg-white/5 rounded-full flex items-center justify-center mb-3">
<Library size={16} />
</div>
<p className="text-xs font-medium">No rubrics found for this course.</p>
</div>
) : ( ) : (
courseRubrics.map(rubric => { courseRubrics.map(rubric => {
const isAssigned = assignedRubricIds.includes(rubric.id); const isAssigned = assignedRubricIds.includes(rubric.id);
return ( return (
<label key={rubric.id} className="flex items-center justify-between p-2 rounded-lg hover:bg-slate-200 dark:hover:bg-white/5 transition-all cursor-pointer group"> <label key={rubric.id} className={`flex items-center justify-between p-4 rounded-xl transition-all cursor-pointer group border ${isAssigned ? 'bg-blue-50 dark:bg-blue-500/10 border-blue-200 dark:border-blue-500/30' : 'bg-white dark:bg-white/5 border-slate-100 dark:border-white/5 hover:border-blue-300 dark:hover:border-blue-500/50 hover:shadow-sm'}`}>
<div className="flex items-center gap-3"> <div className="flex items-center gap-4">
<input <div className={`w-5 h-5 rounded flex items-center justify-center border transition-all ${isAssigned ? 'bg-blue-600 border-blue-600' : 'bg-slate-50 dark:bg-gray-800 border-slate-200 dark:border-gray-700'}`}>
type="checkbox" <input
checked={isAssigned} type="checkbox"
onChange={() => toggleRubric(rubric.id, isAssigned)} checked={isAssigned}
className="w-4 h-4 rounded border-slate-300 dark:border-gray-700 bg-slate-100 dark:bg-gray-800 text-blue-500 focus:ring-blue-500" onChange={() => toggleRubric(rubric.id, isAssigned)}
/> className="hidden"
<span className="text-sm font-medium text-slate-600 dark:text-gray-200 group-hover:text-blue-600 dark:group-hover:text-blue-400 transition-colors">{rubric.name}</span> />
{isAssigned && <Check size={14} className="text-white" />}
</div>
<span className={`text-sm font-black uppercase tracking-tight transition-colors ${isAssigned ? 'text-blue-700 dark:text-blue-400' : 'text-slate-500 dark:text-gray-400 group-hover:text-blue-600'}`}>
{rubric.name}
</span>
</div> </div>
<span className="text-[10px] font-bold text-slate-500 bg-slate-200 dark:bg-white/5 px-1.5 py-0.5 rounded">{rubric.total_points} pts</span> <span className="text-[9px] font-black text-slate-400 dark:text-gray-500 bg-slate-100 dark:bg-white/10 px-2.5 py-1 rounded-full border border-slate-200 dark:border-white/10 uppercase tracking-tighter">{rubric.total_points} PTS</span>
</label> </label>
); );
}) })
)} )}
</div> </div>
<div className="text-[10px] text-gray-500 italic mt-1 pl-1"> <div className="text-[10px] text-slate-400 dark:text-gray-500 italic mt-2 pl-2 flex items-center gap-2">
Manage rubrics in <Link href={`/courses/${params.id}/rubrics`} className="text-blue-400 hover:underline">Rubrics Manager</Link> <div className="w-1 h-1 rounded-full bg-slate-300"></div>
Create more in <Link href={`/courses/${params.id}/rubrics`} className="text-blue-600 dark:text-blue-400 hover:text-blue-700 font-black uppercase tracking-widest decoration-blue-500/30 underline-offset-4 underline">Rubrics Manager</Link>
</div> </div>
</div> </div>
</div> </div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-8 pt-6 border-t border-white/5 animate-in fade-in duration-500"> <div className="grid grid-cols-1 md:grid-cols-2 gap-10 pt-10 border-t border-slate-100 dark:border-white/5 animate-in fade-in duration-500">
<div className="space-y-4"> <div className="space-y-4">
<label className="block"> <label className="block">
<span className="text-[10px] font-black uppercase tracking-widest text-slate-500 dark:text-gray-500 mb-2 block">Maximum Attempts</span> <span className="text-[10px] font-black uppercase tracking-[0.2em] text-slate-400 dark:text-gray-500 mb-2 block">Maximum Attempts</span>
<div className="flex items-center gap-3"> <div className="flex items-center gap-5">
<input <input
type="number" type="number"
value={maxAttempts || ""} value={maxAttempts || ""}
onChange={(e) => setMaxAttempts(e.target.value ? parseInt(e.target.value) : null)} onChange={(e) => setMaxAttempts(e.target.value ? parseInt(e.target.value) : null)}
placeholder="Unlimited" placeholder="Unlimited"
className="bg-slate-100 dark:bg-white/5 border border-slate-200 dark:border-white/10 rounded-xl px-4 py-3 text-sm focus:outline-none focus:border-blue-500 transition-all w-32 text-slate-900 dark:text-white" className="bg-slate-50 dark:bg-white/5 border border-slate-200 dark:border-white/10 rounded-[1.25rem] px-5 py-4 text-sm focus:outline-none focus:ring-4 focus:ring-blue-500/20 transition-all w-40 text-slate-900 dark:text-white shadow-inner font-black"
/> />
<span className="text-xs text-slate-500 dark:text-gray-500">Leave empty for unlimited</span> <span className="text-[10px] font-bold uppercase tracking-widest text-slate-400 dark:text-gray-500">Clear for infinite</span>
</div> </div>
</label> </label>
</div> </div>
<div className="space-y-3">
<div className="space-y-2"> <span className="text-[10px] font-black uppercase tracking-[0.2em] text-slate-400 dark:text-gray-500 block mb-2">Self-Correction System</span>
<span className="text-[10px] font-black uppercase tracking-widest text-slate-500 dark:text-gray-500 block mb-2">After Submission</span> <label className="flex items-center gap-4 cursor-pointer group relative">
<label className="flex items-center gap-3 cursor-pointer group relative">
<input <input
type="checkbox" type="checkbox"
checked={allowRetry} checked={allowRetry}
onChange={(e) => setAllowRetry(e.target.checked)} onChange={(e) => setAllowRetry(e.target.checked)}
className="sr-only peer" className="sr-only peer"
/> />
<div className="w-10 h-6 bg-slate-200 dark:bg-gray-700 rounded-full peer peer-checked:bg-blue-600 after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:after:translate-x-4 shadow-sm"></div> <div className="w-12 h-7 bg-slate-200 dark:bg-gray-700 rounded-full peer peer-checked:bg-blue-600 after:content-[''] after:absolute after:top-[3.5px] after:left-[4px] after:bg-white after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:after:translate-x-5 shadow-sm"></div>
<span className="text-sm font-bold text-slate-400 dark:text-gray-400 peer-checked:text-slate-900 dark:peer-checked:text-white transition-colors">Allow Instant Corrections</span> <span className="text-sm font-black uppercase tracking-tight text-slate-400 dark:text-gray-400 peer-checked:text-blue-700 dark:peer-checked:text-blue-400 transition-colors">Instant Feedback Mode</span>
</label> </label>
<p className="text-[10px] text-slate-500 dark:text-gray-600 italic">Enables &quot;Check Answer&quot; buttons for individual blocks</p> <p className="text-[11px] font-medium text-slate-400 dark:text-gray-600 italic ml-1">Allows &quot;Validate Answer&quot; buttons in interactive blocks</p>
</div> </div>
</div> </div>
@@ -649,44 +684,147 @@ export default function LessonEditor({ params }: { params: { id: string; lessonI
{ {
editMode && ( editMode && (
<div className="bg-slate-50 dark:bg-white/5 border border-slate-200 dark:border-white/10 rounded-3xl p-8 space-y-6 animate-in fade-in slide-in-from-top-4 duration-500"> <div className="bg-white dark:bg-black/20 border border-slate-200 dark:border-white/10 rounded-[2.5rem] p-10 space-y-8 animate-in fade-in slide-in-from-top-4 duration-500 shadow-sm hover:shadow-md transition-all">
<div> <div>
<h3 className="text-xl font-bold flex items-center gap-2 text-slate-900 dark:text-white"> <h3 className="text-2xl font-black flex items-center gap-3 text-slate-900 dark:text-white uppercase tracking-tight">
<span className="text-blue-500">📅</span> Scheduling & Deadlines <div className="w-12 h-12 rounded-2xl bg-blue-50 dark:bg-blue-500/10 border border-blue-100 dark:border-blue-500/20 flex items-center justify-center text-blue-600 dark:text-blue-400 shadow-sm">
<Settings size={24} />
</div>
Scheduling & Sequencing
</h3> </h3>
<p className="text-sm text-slate-500 dark:text-gray-500 mt-1">Set deadlines and mark important dates for this activity</p> <p className="text-sm font-medium text-slate-500 dark:text-gray-500 mt-2 ml-15">Establish temporal constraints and learning paths</p>
</div> </div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-10">
<div className="grid grid-cols-1 md:grid-cols-2 gap-8">
<div className="space-y-4"> <div className="space-y-4">
<label className="block"> <label className="block">
<span className="text-[10px] font-black uppercase tracking-widest text-slate-500 dark:text-gray-500 mb-2 block">Due Date</span> <span className="text-[10px] font-black uppercase tracking-[0.2em] text-slate-400 dark:text-gray-500 mb-2 block">Final Deadline</span>
<input <input
type="date" type="date"
value={dueDate} value={dueDate}
onChange={(e) => setDueDate(e.target.value)} onChange={(e) => setDueDate(e.target.value)}
className="w-full bg-slate-100 dark:bg-white/5 border border-slate-200 dark:border-white/10 rounded-xl px-4 py-3 text-sm focus:outline-none focus:border-blue-500 transition-all font-bold text-slate-900 dark:text-white" className="w-full bg-slate-50 dark:bg-white/5 border border-slate-200 dark:border-white/10 rounded-[1.25rem] px-5 py-4 text-sm focus:outline-none focus:ring-4 focus:ring-blue-500/20 transition-all font-black text-slate-900 dark:text-white shadow-inner"
/> />
</label> </label>
</div> </div>
<div className="space-y-4"> <div className="space-y-4">
<label className="block"> <label className="block">
<span className="text-[10px] font-black uppercase tracking-widest text-slate-500 dark:text-gray-500 mb-2 block">Date Type</span> <span className="text-[10px] font-black uppercase tracking-[0.2em] text-slate-400 dark:text-gray-500 mb-2 block">Event Marker Type</span>
<select <div className="relative group/select">
value={importantDateType} <select
onChange={(e) => setImportantDateType(e.target.value)} value={importantDateType}
className="w-full bg-slate-100 dark:bg-white/5 border border-slate-200 dark:border-white/10 rounded-xl px-4 py-3 text-sm focus:outline-none focus:border-blue-500 transition-all appearance-none font-bold text-slate-900 dark:text-white" onChange={(e) => setImportantDateType(e.target.value)}
> className="w-full bg-slate-50 dark:bg-white/5 border border-slate-200 dark:border-white/10 rounded-[1.25rem] px-5 py-4 text-sm focus:outline-none focus:ring-4 focus:ring-blue-500/20 transition-all appearance-none font-black text-slate-900 dark:text-white shadow-inner"
<option value="">Standard Activity</option> >
<option value="exam">Exam</option> <option value="">Standard Activity</option>
<option value="assignment">Assignment</option> <option value="exam">🏆 Final Exam</option>
<option value="milestone">Milestone</option> <option value="assignment">📝 Main Assignment</option>
<option value="live-session">Live Session</option> <option value="milestone">🚩 Project Milestone</option>
</select> <option value="live-session">🎥 Live Training</option>
</select>
<div className="absolute right-5 top-1/2 -translate-y-1/2 pointer-events-none text-slate-400">
<ChevronDown size={18} />
</div>
</div>
</label> </label>
</div> </div>
</div> </div>
{/* Prerequisites Sub-Panel inside Scheduling */}
<div className="pt-10 border-t border-slate-100 dark:border-white/5">
<div className="flex items-center gap-3 mb-8 uppercase tracking-[0.2em] text-[10px] font-black text-slate-400 dark:text-gray-500">
<span className="w-1.5 h-1.5 rounded-full bg-blue-600 dark:bg-blue-500 shadow-lg shadow-blue-500/50"></span>
Learning Dependency Graph
</div>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-10">
<div className="space-y-4">
<h4 className="text-[11px] font-black uppercase tracking-widest text-slate-900 dark:text-gray-200">Prerequisites</h4>
<div className="bg-slate-50 dark:bg-black/40 border border-slate-100 dark:border-white/10 rounded-[2rem] p-6 max-h-72 overflow-y-auto space-y-3 shadow-inner">
{allLessons.length === 0 ? (
<p className="text-xs text-slate-400 italic p-6 text-center">No other modules in focus.</p>
) : (
allLessons.map(l => {
const dep = dependencies.find(d => d.prerequisite_lesson_id === l.id);
const isAssigned = !!dep;
return (
<div key={l.id} className={`p-4 rounded-[1.25rem] transition-all group border ${isAssigned ? 'bg-indigo-50 dark:bg-indigo-500/10 border-indigo-200 dark:border-indigo-500/30' : 'bg-white dark:bg-white/5 border-slate-50 dark:border-white/5 hover:border-indigo-300 shadow-sm'}`}>
<label className="flex items-center justify-between cursor-pointer">
<div className="flex items-center gap-4">
<div className={`w-5 h-5 rounded flex items-center justify-center border transition-all ${isAssigned ? 'bg-indigo-600 border-indigo-600' : 'bg-slate-50 dark:bg-gray-800 border-slate-200 dark:border-gray-700'}`}>
<input
type="checkbox"
checked={isAssigned}
onChange={() => toggleDependency(l.id, isAssigned)}
className="hidden"
/>
{isAssigned && <Check size={14} className="text-white" />}
</div>
<span className={`text-sm font-black uppercase tracking-tight transition-colors ${isAssigned ? 'text-indigo-700 dark:text-indigo-400' : 'text-slate-500 dark:text-gray-400 group-hover:text-indigo-600'}`}>
{l.title}
</span>
</div>
{l.is_graded && (
<span className="text-[8px] font-black uppercase tracking-widest px-2.5 py-1 bg-indigo-500/10 text-indigo-600 dark:text-indigo-400 rounded-lg border border-indigo-500/20">GRADED</span>
)}
</label>
{isAssigned && l.is_graded && (
<div className="mt-4 pl-9 flex items-center gap-4 animate-in slide-in-from-left-4 duration-500">
<span className="text-[10px] text-slate-400 dark:text-gray-500 font-extrabold uppercase tracking-widest">MIN % SCORE</span>
<input
type="number"
min="0"
max="100"
value={dep.min_score_percentage || 0}
onChange={async (e) => {
const minScore = parseFloat(e.target.value);
try {
const updated = await cmsApi.assignDependency(params.lessonId, {
prerequisite_lesson_id: l.id,
min_score_percentage: minScore
});
setDependencies(dependencies.map(d => d.id === updated.id ? updated : d));
} catch (err) {
console.error("Failed to update min score", err);
}
}}
className="w-16 bg-white dark:bg-black/60 border border-slate-200 dark:border-white/10 rounded-lg px-2 py-1.5 text-xs text-indigo-600 dark:text-indigo-400 font-black text-center focus:outline-none focus:ring-4 focus:ring-indigo-500/20 shadow-inner"
/>
</div>
)}
</div>
);
})
)}
</div>
</div>
<div className="flex flex-col justify-center gap-6">
<div className="p-6 rounded-[2rem] bg-indigo-50 dark:bg-indigo-500/5 border border-indigo-100 dark:border-indigo-500/10 shadow-sm">
<div className="flex items-start gap-4">
<div className="w-12 h-12 rounded-2xl bg-indigo-100 dark:bg-indigo-500/20 flex items-center justify-center text-indigo-600 dark:text-indigo-400 shadow-sm shrink-0">
<Layout size={24} />
</div>
<div>
<h5 className="text-sm font-black uppercase tracking-tight text-indigo-700 dark:text-indigo-300">Intelligent Flow</h5>
<p className="text-[11px] font-medium text-indigo-500/80 dark:text-indigo-300/60 leading-relaxed mt-2">
Locked nodes appear with a lock visualizer until prerequisites are cleared by the student.
</p>
</div>
</div>
</div>
<div className="p-6 rounded-[2rem] bg-emerald-50 dark:bg-emerald-500/5 border border-emerald-100 dark:border-emerald-500/10 shadow-sm">
<div className="flex items-start gap-4">
<div className="w-12 h-12 rounded-2xl bg-emerald-100 dark:bg-emerald-500/20 flex items-center justify-center text-emerald-600 dark:text-emerald-400 shadow-sm shrink-0">
<CheckCircle2 size={24} />
</div>
<div>
<h5 className="text-sm font-black uppercase tracking-tight text-emerald-700 dark:text-emerald-300">Mastery Gates</h5>
<p className="text-[11px] font-medium text-emerald-500/80 dark:text-emerald-300/60 leading-relaxed mt-2">
When score thresholds are set, success in prior activities is mandatory for progression.
</p>
</div>
</div>
</div>
</div>
</div>
</div>
</div> </div>
) )
} }
@@ -694,34 +832,45 @@ export default function LessonEditor({ params }: { params: { id: string; lessonI
{/* AI Magic Section */} {/* AI Magic Section */}
{ {
editMode && ( editMode && (
<div className="bg-slate-50 dark:bg-white/5 border border-slate-200 dark:border-white/10 rounded-3xl p-8 space-y-6 animate-in fade-in slide-in-from-top-4 duration-500"> <div className="bg-white dark:bg-white/5 border border-slate-200 dark:border-white/10 rounded-[2.5rem] p-10 space-y-10 animate-in fade-in slide-in-from-top-4 duration-500 shadow-sm hover:shadow-md transition-all group/ai overflow-hidden relative">
<div className="flex items-center gap-3"> <div className="absolute top-0 right-0 w-64 h-64 bg-indigo-500/5 rounded-full blur-[80px] -translate-y-1/2 translate-x-1/2 group-hover/ai:bg-indigo-500/10 transition-colors"></div>
<span className="text-2xl">🪄</span> <div className="flex items-center gap-4 z-10 relative">
<div className="w-14 h-14 rounded-2xl bg-indigo-600 dark:bg-white/10 flex items-center justify-center text-white dark:text-indigo-400 shadow-xl shadow-indigo-500/20">
<Brain size={32} className="animate-pulse" />
</div>
<div> <div>
<h3 className="text-xl font-bold italic tracking-tight text-slate-900 dark:text-white">AI Content Assistant</h3> <h3 className="text-2xl font-black italic tracking-tight text-slate-900 dark:text-white uppercase">AI Content Copilot</h3>
<p className="text-xs text-slate-500 dark:text-gray-400 mt-1 uppercase tracking-widest font-bold">Automate your content creation with Local AI</p> <p className="text-[10px] text-slate-400 dark:text-gray-400 mt-1 uppercase tracking-[0.2em] font-black">Powered by Neural Processing Unit</p>
</div> </div>
</div> </div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4"> <div className="grid grid-cols-1 md:grid-cols-2 gap-8 z-10 relative">
<button <button
onClick={handleSummarize} onClick={handleSummarize}
disabled={isGeneratingSummary} disabled={isGeneratingSummary}
className={`p-6 rounded-2xl border transition-all text-left flex flex-col gap-2 shadow-sm ${isGeneratingSummary ? 'bg-indigo-500/20 border-indigo-500/50 text-indigo-600 dark:text-indigo-300 animate-pulse' : summary ? 'bg-green-500/10 border-green-500/30 text-green-600 dark:text-green-400' : 'bg-indigo-500/10 border-indigo-500/30 text-indigo-600 dark:text-indigo-400 hover:border-indigo-500/60'}`} className={`group/btn p-8 rounded-[2rem] border transition-all text-left flex flex-col gap-4 shadow-sm relative overflow-hidden ${isGeneratingSummary ? 'bg-indigo-50 dark:bg-indigo-500/20 border-indigo-200 dark:border-indigo-500/50 text-indigo-600 dark:text-indigo-300 animate-pulse' : summary ? 'bg-indigo-50/50 dark:bg-white/5 border-indigo-200 dark:border-indigo-500/20 text-indigo-600 dark:text-indigo-400 hover:border-indigo-400' : 'bg-white dark:bg-white/5 border-slate-100 dark:border-white/10 text-slate-400 dark:text-indigo-400 hover:border-indigo-500/50 hover:bg-indigo-50 hover:text-indigo-600 dark:hover:bg-indigo-500/10'}`}
> >
<span className="text-xl">{isGeneratingSummary ? '⏳' : '✍️'}</span> <div className="w-12 h-12 rounded-2xl bg-indigo-100 dark:bg-indigo-500/20 flex items-center justify-center text-indigo-600 dark:text-indigo-400 shadow-sm group-hover/btn:scale-110 transition-transform">
<div className="text-[10px] font-black uppercase tracking-widest opacity-80">Summarization</div> {isGeneratingSummary ? '⏳' : <ChevronDown className="rotate-90" />}
<div className="font-bold">{isGeneratingSummary ? 'Generating...' : summary ? 'Update Summary' : 'Generate Summary'}</div> </div>
<div className="space-y-1">
<div className="text-[10px] font-black uppercase tracking-[0.2em] opacity-80">Semantic Analysis</div>
<div className="font-black text-xl tracking-tight">{isGeneratingSummary ? 'Processing Content...' : summary ? 'Refresh Summary' : 'Extract Summary'}</div>
</div>
</button> </button>
<button <button
onClick={handleGenerateQuiz} onClick={handleGenerateQuiz}
disabled={isGeneratingQuiz} disabled={isGeneratingQuiz}
className={`p-6 border rounded-2xl transition-all text-left flex flex-col gap-2 shadow-sm ${isGeneratingQuiz ? 'bg-purple-500/20 border-purple-500/50 text-purple-600 dark:text-purple-300 animate-pulse' : 'bg-purple-500/10 border-purple-500/30 hover:border-purple-500/60 text-purple-600 dark:text-purple-400'}`} className={`group/btn p-8 border rounded-[2rem] transition-all text-left flex flex-col gap-4 shadow-sm relative overflow-hidden ${isGeneratingQuiz ? 'bg-purple-50 dark:bg-purple-500/20 border-purple-200 dark:border-purple-500/50 text-purple-600 dark:text-purple-300 animate-pulse' : 'bg-white dark:bg-white/5 border-slate-100 dark:border-white/10 text-slate-400 dark:text-purple-400 hover:border-purple-500/50 hover:bg-purple-50 hover:text-purple-600 dark:hover:bg-purple-500/10'}`}
> >
<span className="text-xl">{isGeneratingQuiz ? '⏳' : '💡'}</span> <div className="w-12 h-12 rounded-2xl bg-purple-100 dark:bg-purple-500/20 flex items-center justify-center text-purple-600 dark:text-purple-400 shadow-sm group-hover/btn:scale-110 transition-transform">
<div className="text-[10px] font-black uppercase tracking-widest opacity-80">Assessments</div> {isGeneratingQuiz ? '⏳' : <Target />}
<div className="font-bold">{isGeneratingQuiz ? 'Building...' : 'Generate Quiz'}</div> </div>
<div className="space-y-1">
<div className="text-[10px] font-black uppercase tracking-[0.2em] opacity-80">Knowledge Check</div>
<div className="font-black text-xl tracking-tight">{isGeneratingQuiz ? 'Building Quiz...' : 'Generate New Test'}</div>
</div>
</button> </button>
</div> </div>
</div> </div>
@@ -731,27 +880,33 @@ export default function LessonEditor({ params }: { params: { id: string; lessonI
{/* AI Summary Visualization */} {/* AI Summary Visualization */}
{ {
(summary || editMode) && ( (summary || editMode) && (
<div className="bg-gradient-to-br from-indigo-500/10 to-blue-500/10 border border-indigo-500/20 rounded-3xl p-8 space-y-6 animate-in fade-in duration-700"> <div className="bg-white dark:bg-white/5 border border-indigo-100 dark:border-indigo-500/20 rounded-[2.5rem] p-10 space-y-8 animate-in fade-in duration-700 shadow-sm relative overflow-hidden group/summary">
<div className="flex items-center justify-between"> <div className="absolute top-0 left-0 w-1 h-full bg-indigo-500/40"></div>
<div className="flex items-center gap-3"> <div className="flex items-center justify-between relative z-10">
<span className="text-2xl"></span> <div className="flex items-center gap-4">
<div className="w-12 h-12 rounded-2xl bg-indigo-50 dark:bg-indigo-500/10 flex items-center justify-center text-indigo-600 dark:text-indigo-400">
<Brain size={24} />
</div>
<div> <div>
<h3 className="text-xl font-bold font-black italic tracking-tight text-slate-900 dark:text-white">AI Lesson Summary</h3> <h3 className="text-xl font-black italic tracking-tight text-slate-900 dark:text-white uppercase">Neural Insight Summary</h3>
<p className="text-xs text-slate-500 dark:text-gray-400 mt-1 uppercase tracking-widest font-bold">Key insights generated from content</p> <p className="text-[10px] text-slate-400 dark:text-gray-400 mt-1 uppercase tracking-[0.2em] font-black">Synthesized from activity contents</p>
</div> </div>
</div> </div>
</div> </div>
{editMode ? ( {editMode ? (
<textarea <textarea
value={summary} value={summary}
onChange={(e) => setSummary(e.target.value)} onChange={(e) => setSummary(e.target.value)}
placeholder="A concise summary of the lesson content..." placeholder="Write a concise executive summary..."
className="w-full bg-slate-50 dark:bg-white/5 border border-slate-200 dark:border-white/10 rounded-2xl p-6 text-sm text-slate-700 dark:text-gray-300 focus:outline-none focus:border-blue-500/50 min-h-[120px] transition-all" className="w-full bg-slate-50 dark:bg-black/40 border border-slate-200 dark:border-white/10 rounded-[2rem] p-8 text-sm text-slate-700 dark:text-gray-300 focus:outline-none focus:ring-4 focus:ring-indigo-500/10 min-h-[160px] transition-all font-medium leading-relaxed shadow-inner"
/> />
) : ( ) : (
<div className="text-sm text-slate-600 dark:text-gray-400 leading-relaxed italic border-l-2 border-indigo-500/30 pl-6 py-2"> <div className="relative">
&quot;{summary}&quot; <span className="absolute -left-4 -top-4 text-6xl text-indigo-500/10 font-serif">&ldquo;</span>
<div className="text-lg text-slate-600 dark:text-gray-300 leading-relaxed italic pl-6 font-medium">
{summary}
</div>
<span className="absolute -right-2 -bottom-4 text-6xl text-indigo-500/10 font-serif">&rdquo;</span>
</div> </div>
)} )}
</div> </div>
@@ -762,40 +917,42 @@ export default function LessonEditor({ params }: { params: { id: string; lessonI
{blocks.map((block, index) => ( {blocks.map((block, index) => (
<div key={block.id} className="relative group/block animate-in fade-in slide-in-from-bottom-4 duration-500" style={{ animationDelay: `${index * 100}ms` }}> <div key={block.id} className="relative group/block animate-in fade-in slide-in-from-bottom-4 duration-500" style={{ animationDelay: `${index * 100}ms` }}>
{editMode && ( {editMode && (
<div className="absolute -left-16 top-0 h-full flex flex-col items-center gap-2 opacity-100 transition-all"> <div className="absolute -left-20 top-0 h-full flex flex-col items-center gap-3 opacity-0 group-hover/block:opacity-100 transition-all duration-300 -translate-x-4 group-hover/block:translate-x-0">
<span className="text-[10px] font-black text-slate-300 dark:text-gray-700 uppercase vertical-text mb-2">Move</span> <div className="flex flex-col gap-1.5 p-2 bg-white dark:bg-white/5 border border-slate-200 dark:border-white/10 rounded-[1.5rem] shadow-xl">
<button <button
onClick={() => moveBlock(index, 'up')} onClick={() => moveBlock(index, 'up')}
disabled={index === 0} disabled={index === 0}
className="w-10 h-10 rounded-xl bg-slate-100 dark:bg-white/5 text-slate-400 dark:text-gray-400 flex items-center justify-center hover:bg-blue-600 dark:hover:bg-blue-500 hover:text-white transition-all border border-slate-200 dark:border-white/10 disabled:opacity-20 disabled:cursor-not-allowed group-hover/block:scale-110 shadow-sm" className="w-10 h-10 rounded-xl bg-slate-50 dark:bg-white/5 text-slate-400 dark:text-gray-400 flex items-center justify-center hover:bg-blue-600 dark:hover:bg-blue-500 hover:text-white transition-all disabled:opacity-20 disabled:cursor-not-allowed shadow-sm border border-slate-100 dark:border-transparent"
title="Move Up" title="Subir Bloque"
> >
<ChevronUp className="w-5 h-5" /> <ChevronUp className="w-5 h-5" />
</button> </button>
<button <button
onClick={() => moveBlock(index, 'down')} onClick={() => moveBlock(index, 'down')}
disabled={index === blocks.length - 1} disabled={index === blocks.length - 1}
className="w-10 h-10 rounded-xl bg-slate-100 dark:bg-white/5 text-slate-400 dark:text-gray-400 flex items-center justify-center hover:bg-blue-600 dark:hover:bg-blue-500 hover:text-white transition-all border border-slate-200 dark:border-white/10 disabled:opacity-20 disabled:cursor-not-allowed group-hover/block:scale-110 shadow-sm" className="w-10 h-10 rounded-xl bg-slate-50 dark:bg-white/5 text-slate-400 dark:text-gray-400 flex items-center justify-center hover:bg-blue-600 dark:hover:bg-blue-500 hover:text-white transition-all disabled:opacity-20 disabled:cursor-not-allowed shadow-sm border border-slate-100 dark:border-transparent"
title="Move Down" title="Bajar Bloque"
> >
<ChevronDown className="w-5 h-5" /> <ChevronDown className="w-5 h-5" />
</button> </button>
<div className="h-4"></div> </div>
<button
onClick={() => openSaveToLibraryModal(block)} <div className="flex flex-col gap-1.5 p-2 bg-white dark:bg-white/5 border border-slate-200 dark:border-white/10 rounded-[1.5rem] shadow-xl">
className="w-10 h-10 rounded-xl bg-emerald-500/10 text-emerald-600 dark:text-emerald-400 flex items-center justify-center hover:bg-emerald-500 hover:text-white transition-all border border-emerald-500/20 group-hover/block:scale-110 shadow-sm" <button
title="Save to Library" onClick={() => openSaveToLibraryModal(block)}
> className="w-10 h-10 rounded-xl bg-emerald-50 text-emerald-600 dark:bg-emerald-500/10 dark:text-emerald-400 flex items-center justify-center hover:bg-emerald-600 hover:text-white transition-all border border-emerald-100 dark:border-transparent shadow-sm"
<BookMarked className="w-5 h-5" /> title="Guardar en Biblioteca"
</button> >
<button <BookMarked className="w-5 h-5" />
onClick={() => removeBlock(block.id)} </button>
className="w-10 h-10 rounded-xl bg-red-500/10 text-red-600 dark:text-red-400 flex items-center justify-center hover:bg-red-500 hover:text-white transition-all border border-red-500/20 group-hover/block:scale-110 shadow-sm" <button
title="Remove Block" onClick={() => removeBlock(block.id)}
> className="w-10 h-10 rounded-xl bg-red-50 text-red-600 dark:bg-red-500/10 dark:text-red-400 flex items-center justify-center hover:bg-red-600 hover:text-white transition-all border border-red-100 dark:border-transparent shadow-sm"
<Trash2 className="w-5 h-5" /> title="Eliminar Bloque"
</button> >
<div className="w-0.5 flex-1 bg-white/5 mt-2"></div> <Trash2 className="w-5 h-5" />
</button>
</div>
</div> </div>
)} )}
@@ -935,120 +1092,68 @@ export default function LessonEditor({ params }: { params: { id: string; lessonI
))} ))}
{editMode && ( {editMode && (
<div className="pt-12 border-t border-white/5"> <div className="pt-20 border-t border-slate-100 dark:border-white/5">
<div className="flex flex-col items-center gap-6"> <div className="flex flex-col items-center gap-12">
<span className="text-[10px] text-gray-500 font-bold uppercase tracking-[0.3em]">Add Content Block</span> <div className="text-center space-y-2">
<div className="flex items-center gap-4"> <h3 className="text-sm font-black uppercase tracking-[0.4em] text-slate-400 dark:text-gray-500">Infinite Canvas</h3>
<button <p className="text-[10px] font-bold text-slate-300 dark:text-gray-600 uppercase tracking-widest">Select a specialized block to expand your activity</p>
onClick={() => addBlock('description')} </div>
className="flex flex-col items-center gap-2 p-6 glass hover:border-blue-500/50 transition-all group w-32"
>
<span className="text-2xl group-hover:scale-110 transition-transform">📄</span>
<span className="text-[10px] font-bold uppercase tracking-widest text-gray-400">Text</span>
</button>
<button
onClick={() => addBlock('media')}
className="flex flex-col items-center gap-2 p-6 glass hover:border-blue-500/50 transition-all group w-32"
>
<span className="text-2xl group-hover:scale-110 transition-transform">🎬</span>
<span className="text-[10px] font-bold uppercase tracking-widest text-gray-400">Media</span>
</button>
<button
onClick={() => addBlock('video_marker')}
className="flex flex-col items-center gap-2 p-6 glass hover:border-indigo-500/50 transition-all group w-32"
>
<span className="text-2xl group-hover:scale-110 transition-transform"></span>
<span className="text-[10px] font-bold uppercase tracking-widest text-gray-400">Video+Q</span>
</button>
<button
onClick={() => addBlock('quiz')}
className="flex flex-col items-center gap-2 p-6 glass hover:border-blue-500/50 transition-all group w-32"
>
<span className="text-2xl group-hover:scale-110 transition-transform">💡</span>
<span className="text-[10px] font-bold uppercase tracking-widest text-gray-400">Quiz</span>
</button>
<button
onClick={() => addBlock('fill-in-the-blanks')}
className="flex flex-col items-center gap-2 p-6 glass hover:border-blue-500/50 transition-all group w-32"
>
<span className="text-2xl group-hover:scale-110 transition-transform"></span>
<span className="text-[10px] font-bold uppercase tracking-widest text-gray-400">Blanks</span>
</button>
<button
onClick={() => addBlock('matching')}
className="flex flex-col items-center gap-2 p-6 glass hover:border-blue-500/50 transition-all group w-32"
>
<span className="text-2xl group-hover:scale-110 transition-transform">🔗</span>
<span className="text-[10px] font-bold uppercase tracking-widest text-gray-400">Match</span>
</button>
<button
onClick={() => addBlock('ordering')}
className="flex flex-col items-center gap-2 p-6 glass hover:border-blue-500/50 transition-all group w-32"
>
<span className="text-2xl group-hover:scale-110 transition-transform">🔢</span>
<span className="text-[10px] font-bold uppercase tracking-widest text-gray-400">Order</span>
</button>
<button
onClick={() => addBlock('short-answer')}
className="flex flex-col items-center gap-2 p-6 glass hover:border-blue-500/50 transition-all group w-32"
>
<span className="text-2xl group-hover:scale-110 transition-transform">💬</span>
<span className="text-[10px] font-bold uppercase tracking-widest text-gray-400">Short</span>
</button>
<button
onClick={() => addBlock('document')}
className="flex flex-col items-center gap-2 p-6 glass hover:border-blue-500/50 transition-all group w-32"
>
<span className="text-2xl group-hover:scale-110 transition-transform">📚</span>
<span className="text-[10px] font-bold uppercase tracking-widest text-gray-400">Reading</span>
</button>
<button
onClick={() => addBlock('audio-response')}
className="flex flex-col items-center gap-2 p-6 glass hover:border-purple-500/50 transition-all group w-32"
>
<span className="text-2xl group-hover:scale-110 transition-transform">🎤</span>
<span className="text-[10px] font-bold uppercase tracking-widest text-gray-400">Audio</span>
</button>
<button
onClick={() => addBlock('hotspot')}
className="flex flex-col items-center gap-2 p-6 glass hover:border-amber-500/50 transition-all group w-32"
>
<span className="text-2xl group-hover:scale-110 transition-transform">🔍</span>
<span className="text-[10px] font-bold uppercase tracking-widest text-gray-400">Hotspot</span>
</button>
<button
onClick={() => addBlock('memory-match')}
className="flex flex-col items-center gap-2 p-6 glass hover:border-purple-500/50 transition-all group w-32"
>
<span className="text-2xl group-hover:scale-110 transition-transform">🧠</span>
<span className="text-[10px] font-bold uppercase tracking-widest text-gray-400">Memory</span>
</button>
<button
onClick={() => addBlock('peer-review')}
className="flex flex-col items-center gap-2 p-6 glass hover:border-purple-500/50 transition-all group w-32"
>
<span className="text-2xl group-hover:scale-110 transition-transform">👥</span>
<span className="text-[10px] font-bold uppercase tracking-widest text-gray-400">Peer Rev</span>
</button>
<div className="w-px h-12 bg-white/5"></div> <div className="grid grid-cols-2 md:grid-cols-4 lg:grid-cols-7 gap-4 w-full">
{[
{ type: 'description', icon: '📄', label: 'Text Block', color: 'blue' },
{ type: 'media', icon: '🎬', label: 'Hyper Media', color: 'indigo' },
{ type: 'video_marker', icon: '⏱️', label: 'Interactive', color: 'violet' },
{ type: 'document', icon: '📚', label: 'Resources', color: 'slate' },
{ type: 'quiz', icon: '💡', label: 'Knowledge', color: 'blue' },
{ type: 'fill-in-the-blanks', icon: '✍️', label: 'Syntax', color: 'indigo' },
{ type: 'matching', icon: '🔗', label: 'Relations', color: 'violet' },
{ type: 'ordering', icon: '🔢', label: 'Sequence', color: 'blue' },
{ type: 'short-answer', icon: '💬', label: 'Open-Ended', color: 'indigo' },
{ type: 'hotspot', icon: '🔍', label: 'Hotspot', color: 'amber' },
{ type: 'audio-response', icon: '🎤', label: 'Phonetic', color: 'purple' },
{ type: 'memory-match', icon: '🧠', label: 'Cognitive', color: 'fuchsia' },
{ type: 'peer-review', icon: '👥', label: 'Peer Review', color: 'rose' },
].map((item) => (
<button
key={item.type}
onClick={() => addBlock(item.type as any)}
className="flex flex-col items-center justify-center gap-3 p-6 bg-white dark:bg-white/5 border border-slate-100 dark:border-white/10 rounded-[2rem] hover:border-blue-500/50 hover:shadow-xl hover:-translate-y-1 transition-all active:scale-95 group shadow-sm shrink-0"
>
<span className="text-3xl group-hover:scale-125 transition-transform duration-300">{item.icon}</span>
<div className="flex flex-col items-center">
<span className="text-[8px] font-black uppercase tracking-widest text-slate-400 dark:text-gray-500 group-hover:text-blue-600 transition-colors uppercase">{item.label}</span>
</div>
</button>
))}
</div>
<div className="flex flex-wrap items-center justify-center gap-6 pt-8">
<button <button
onClick={handleGenerateQuiz} onClick={handleGenerateQuiz}
disabled={isGeneratingQuiz} disabled={isGeneratingQuiz}
className="flex flex-col items-center gap-2 p-6 bg-gradient-to-b from-indigo-500/20 to-blue-500/20 border border-indigo-500/30 hover:border-indigo-500/60 rounded-3xl transition-all group w-36" className="px-10 py-5 bg-gradient-to-br from-indigo-600 to-blue-700 text-white rounded-[2rem] shadow-2xl shadow-blue-500/40 hover:scale-105 active:scale-95 transition-all flex items-center gap-4 group disabled:opacity-50"
> >
<span className="text-2xl group-hover:scale-110 transition-transform">{isGeneratingQuiz ? '⏳' : '✨'}</span> <div className="w-10 h-10 rounded-xl bg-white/20 flex items-center justify-center text-xl group-hover:rotate-12 transition-transform">
<span className="text-[10px] font-black uppercase tracking-widest text-indigo-400">{isGeneratingQuiz ? 'Building...' : 'AI Builder'}</span> {isGeneratingQuiz ? '⏳' : '✨'}
</div>
<div className="text-left">
<div className="text-[10px] font-black uppercase tracking-[0.2em] opacity-70">Neural Engine</div>
<div className="font-black text-sm uppercase tracking-wider">{isGeneratingQuiz ? 'Constructing...' : 'AI Builder'}</div>
</div>
</button> </button>
{/* Browse Library */}
<button <button
onClick={() => setIsLibraryPanelOpen(true)} onClick={() => setIsLibraryPanelOpen(true)}
className="flex flex-col items-center gap-2 p-6 bg-gradient-to-b from-emerald-500/20 to-teal-500/20 border border-emerald-500/30 hover:border-emerald-500/60 rounded-3xl transition-all group w-36" className="px-10 py-5 bg-white dark:bg-white/5 border border-slate-200 dark:border-white/10 text-slate-900 dark:text-white rounded-[2rem] shadow-xl hover:border-emerald-500/50 hover:bg-emerald-50 dark:hover:bg-emerald-500/10 active:scale-95 transition-all flex items-center gap-4 group"
> >
<Library className="w-6 h-6 text-emerald-400 group-hover:scale-110 transition-transform" /> <div className="w-10 h-10 rounded-xl bg-emerald-500/10 flex items-center justify-center text-emerald-600 dark:text-emerald-400 group-hover:scale-110 transition-transform">
<span className="text-[10px] font-black uppercase tracking-widest text-emerald-400">Library</span> <Library size={24} />
</div>
<div className="text-left">
<div className="text-[10px] font-black uppercase tracking-[0.2em] text-slate-400">Personal Vault</div>
<div className="font-black text-sm uppercase tracking-wider">Cloud Library</div>
</div>
</button> </button>
</div> </div>
</div> </div>
@@ -1059,67 +1164,75 @@ export default function LessonEditor({ params }: { params: { id: string; lessonI
<Modal <Modal
isOpen={isAIQuizModalOpen} isOpen={isAIQuizModalOpen}
onClose={() => !isGeneratingQuiz && setIsAIQuizModalOpen(false)} onClose={() => !isGeneratingQuiz && setIsAIQuizModalOpen(false)}
title="AI Quiz Customization" title="AI Neural Configuration"
> >
<form onSubmit={handleConfirmGenerateQuiz} className="space-y-6"> <form onSubmit={handleConfirmGenerateQuiz} className="space-y-8 p-4">
<div className="p-4 rounded-xl bg-purple-500/5 border border-purple-500/10"> <div className="p-6 rounded-[1.5rem] bg-indigo-50 dark:bg-indigo-500/10 border border-indigo-100 dark:border-indigo-500/20 flex gap-4">
<p className="text-xs text-purple-300 leading-relaxed font-medium"> <div className="w-10 h-10 rounded-xl bg-white dark:bg-white/10 flex items-center justify-center text-indigo-600 dark:text-indigo-400 shadow-sm shrink-0">
Tell the AI what to focus on and what type of questions you prefer. <Brain size={20} />
</div>
<p className="text-xs text-indigo-700 dark:text-indigo-300 leading-relaxed font-black uppercase tracking-tight">
Define the thematic focus. The model will synthesize activity blocks based on your curriculum constraints.
</p> </p>
</div> </div>
<div> <div className="space-y-3">
<label className="block text-[10px] font-black uppercase tracking-widest text-gray-500 mb-2"> <label className="block text-[10px] font-black uppercase tracking-[0.2em] text-slate-400 dark:text-gray-500">
Focus / Context Thematic Context / Focus Area
</label> </label>
<textarea <textarea
autoFocus autoFocus
value={aiQuizContext} value={aiQuizContext}
onChange={(e) => setAiQuizContext(e.target.value)} onChange={(e) => setAiQuizContext(e.target.value)}
placeholder="e.g. Focus on past tense verbs, or use vocabulary related to travel..." placeholder="e.g. Focus on cognitive load management, or prioritize practical applications..."
className="w-full bg-black/40 border border-white/10 rounded-xl px-4 py-3 text-sm focus:outline-none focus:border-purple-500 transition-all text-gray-900 dark:text-white h-24 resize-none" className="w-full bg-slate-50 dark:bg-black/40 border border-slate-200 dark:border-white/10 rounded-[1.5rem] p-6 text-sm focus:outline-none focus:ring-4 focus:ring-indigo-500/10 transition-all text-slate-900 dark:text-white h-32 resize-none shadow-inner font-medium"
disabled={isGeneratingQuiz} disabled={isGeneratingQuiz}
/> />
</div> </div>
<div> <div className="space-y-3">
<label className="block text-[10px] font-black uppercase tracking-widest text-gray-500 mb-2"> <label className="block text-[10px] font-black uppercase tracking-[0.2em] text-slate-400 dark:text-gray-500">
Question Type Block Architecture
</label> </label>
<select <div className="relative group/select">
value={aiQuizType} <select
onChange={(e) => setAiQuizType(e.target.value)} value={aiQuizType}
className="w-full bg-black/40 border border-white/10 rounded-xl px-4 py-3 text-sm focus:outline-none focus:border-purple-500 transition-all text-gray-900 dark:text-white appearance-none font-bold" onChange={(e) => setAiQuizType(e.target.value)}
disabled={isGeneratingQuiz} className="w-full bg-slate-50 dark:bg-black/40 border border-slate-200 dark:border-white/10 rounded-2xl px-5 py-4 text-sm focus:outline-none focus:ring-4 focus:ring-indigo-500/10 transition-all text-slate-900 dark:text-white appearance-none font-black uppercase tracking-tight shadow-inner"
> disabled={isGeneratingQuiz}
<option value="multiple-choice">Multiple Choice</option> >
<option value="true-false">True / False</option> <option value="multiple-choice">Multiple Choice Matrix</option>
<option value="vocabulary">Vocabulary Focus</option> <option value="true-false">Binary Validation (T/F)</option>
<option value="grammar">Grammar Focus</option> <option value="vocabulary">Lexical Focus / Vocab</option>
</select> <option value="grammar">Structural / Grammar Focus</option>
</select>
<div className="absolute right-5 top-1/2 -translate-y-1/2 pointer-events-none text-slate-400">
<ChevronDown size={18} />
</div>
</div>
</div> </div>
<div className="flex gap-3 pt-2"> <div className="flex gap-4 pt-4">
<button <button
type="button" type="button"
onClick={() => setIsAIQuizModalOpen(false)} onClick={() => setIsAIQuizModalOpen(false)}
disabled={isGeneratingQuiz} disabled={isGeneratingQuiz}
className="flex-1 px-4 py-2.5 bg-white/5 hover:bg-white/10 border border-white/10 rounded-lg transition-all text-sm font-medium" className="flex-1 px-8 py-4 bg-white dark:bg-white/5 border border-slate-200 dark:border-white/10 rounded-2xl transition-all text-[10px] font-black uppercase tracking-[0.2em] text-slate-500 hover:bg-slate-50 active:scale-95 shadow-sm"
> >
Cancel Abort
</button> </button>
<button <button
type="submit" type="submit"
disabled={isGeneratingQuiz} disabled={isGeneratingQuiz}
className="flex-[2] px-4 py-2.5 bg-gradient-to-r from-purple-600 to-indigo-600 hover:from-purple-500 hover:to-indigo-500 text-gray-900 dark:text-white rounded-lg transition-all shadow-lg shadow-purple-500/20 font-bold text-sm flex items-center justify-center gap-2" className="flex-[2] px-8 py-4 bg-indigo-600 text-white rounded-2xl transition-all shadow-xl shadow-indigo-500/40 font-black text-[10px] uppercase tracking-[0.2em] flex items-center justify-center gap-3 hover:bg-indigo-500 active:scale-95 disabled:bg-slate-300"
> >
{isGeneratingQuiz ? ( {isGeneratingQuiz ? (
<> <>
<div className="w-4 h-4 border-2 border-white/20 border-t-white rounded-full animate-spin" /> <div className="w-4 h-4 border-2 border-white/20 border-t-white rounded-full animate-spin" />
Generating... Synthesizing...
</> </>
) : ( ) : (
"Generate Quiz" "Initiate Generation"
)} )}
</button> </button>
</div> </div>
+60 -54
View File
@@ -228,7 +228,7 @@ export default function CourseEditor({ params }: { params: { id: string } }) {
pageTitle="Editor de Curso" pageTitle="Editor de Curso"
pageDescription={`Diseña la estructura de tu curso y el contenido de las lecciones.`} pageDescription={`Diseña la estructura de tu curso y el contenido de las lecciones.`}
pageActions={ pageActions={
<> <div className="flex items-center gap-4">
<button <button
onClick={async () => { onClick={async () => {
try { try {
@@ -240,105 +240,108 @@ export default function CourseEditor({ params }: { params: { id: string } }) {
alert("Failed to start preview."); alert("Failed to start preview.");
} }
}} }}
className="flex items-center gap-2 px-6 py-2.5 bg-slate-50 dark:bg-white/5 hover:bg-slate-100 dark:hover:bg-white/10 border border-slate-200 dark:border-white/10 rounded-xl text-sm font-bold transition-all active:scale-95 text-slate-700 dark:text-white shadow-sm" className="flex items-center gap-3 px-6 py-3 bg-white dark:bg-white/5 hover:bg-slate-50 dark:hover:bg-white/10 border border-slate-200 dark:border-white/10 rounded-2xl text-[10px] uppercase tracking-widest font-black transition-all active:scale-95 text-slate-600 dark:text-white shadow-sm"
> >
<PlayCircle size={18} /> Previsualizar <PlayCircle size={20} className="text-blue-500" /> PREVIEW
</button> </button>
<button <button
onClick={handlePublish} onClick={handlePublish}
disabled={saving} disabled={saving}
className={`flex items-center gap-2 px-6 py-2.5 bg-blue-600 hover:bg-blue-500 text-white rounded-xl font-bold text-sm shadow-md shadow-blue-600/20 transition-all active:scale-95 ${saving ? 'opacity-50 cursor-not-allowed' : ''}`} className={`flex items-center gap-3 px-8 py-3 bg-blue-600 hover:bg-blue-500 text-white rounded-2xl font-black text-[10px] uppercase tracking-[0.2em] shadow-xl shadow-blue-500/20 transition-all active:scale-95 ${saving ? 'opacity-50 cursor-not-allowed' : ''}`}
> >
{saving ? ( {saving ? (
<div className="w-5 h-5 border-2 border-white/20 border-t-white rounded-full animate-spin" /> <div className="w-4 h-4 border-2 border-white/20 border-t-white rounded-full animate-spin" />
) : ( ) : (
<Send size={18} /> <Send size={18} />
)} )}
{saving ? 'Publicando...' : 'Publicar Curso'} {saving ? 'PUBLISHING...' : 'PUBLISH COURSE'}
</button> </button>
</> </div>
} }
> >
<div className="space-y-6"> <div className="space-y-6">
{modules.map((module: FullModule, mIndex: number) => ( {modules.map((module: FullModule, mIndex: number) => (
<div key={module.id} className="glass rounded-xl overflow-hidden border-slate-200 dark:border-white/5 shadow-sm"> <div key={module.id} className="bg-white dark:bg-white/5 rounded-[2.5rem] overflow-hidden border border-slate-200 dark:border-white/10 shadow-sm group/module">
<div className="bg-slate-50 dark:bg-white/5 px-6 py-4 flex justify-between items-center border-b border-slate-200 dark:border-white/5"> <div className="bg-slate-50 dark:bg-white/5 px-8 p-6 flex justify-between items-center border-b border-slate-200 dark:border-white/5">
<div className="flex items-center gap-4 flex-1"> <div className="flex items-center gap-6 flex-1">
<div className="flex flex-col"> <div className="flex flex-col gap-1">
<button <button
onClick={() => handleReorderModule(mIndex, 'up')} onClick={() => handleReorderModule(mIndex, 'up')}
disabled={mIndex === 0} disabled={mIndex === 0}
className="text-slate-400 dark:text-gray-500 hover:text-blue-600 dark:hover:text-blue-400 disabled:opacity-0 transition-colors" className="p-1 text-slate-400 dark:text-gray-500 hover:text-blue-600 dark:hover:text-blue-400 disabled:opacity-0 transition-colors bg-white dark:bg-white/5 rounded-md border border-slate-100 dark:border-white/5"
> >
<ChevronUp className="w-4 h-4" /> <ChevronUp className="w-3 h-3" />
</button> </button>
<button <button
onClick={() => handleReorderModule(mIndex, 'down')} onClick={() => handleReorderModule(mIndex, 'down')}
disabled={mIndex === modules.length - 1} disabled={mIndex === modules.length - 1}
className="text-slate-400 dark:text-gray-500 hover:text-blue-600 dark:hover:text-blue-400 disabled:opacity-0 transition-colors" className="p-1 text-slate-400 dark:text-gray-500 hover:text-blue-600 dark:hover:text-blue-400 disabled:opacity-0 transition-colors bg-white dark:bg-white/5 rounded-md border border-slate-100 dark:border-white/5"
> >
<ChevronDown className="w-4 h-4" /> <ChevronDown className="w-3 h-3" />
</button> </button>
</div> </div>
<GripVertical className="text-slate-400 dark:text-gray-600 w-5 h-5 cursor-grab active:cursor-grabbing" /> <GripVertical className="text-slate-300 dark:text-gray-600 w-5 h-5 cursor-grab active:cursor-grabbing hover:text-blue-500 transition-colors" />
{editingId === module.id ? ( {editingId === module.id ? (
<div className="flex items-center gap-2 flex-1"> <div className="flex items-center gap-3 flex-1 max-w-2xl">
<input <input
autoFocus autoFocus
value={editValue} value={editValue}
onChange={(e) => setEditValue(e.target.value)} onChange={(e) => setEditValue(e.target.value)}
onKeyDown={(e) => e.key === 'Enter' && handleSaveTitle(module.id, 'module')} onKeyDown={(e) => e.key === 'Enter' && handleSaveTitle(module.id, 'module')}
className="bg-slate-100 dark:bg-black/40 border border-blue-500/50 rounded px-3 py-1 flex-1 text-slate-900 dark:text-white focus:outline-none" className="bg-white dark:bg-black/40 border border-blue-500/50 rounded-2xl px-5 py-3 flex-1 text-slate-900 dark:text-white focus:outline-none focus:ring-2 focus:ring-blue-500/30 text-lg font-black uppercase tracking-tight shadow-inner"
/> />
<button onClick={() => handleSaveTitle(module.id, 'module')} className="text-green-400 hover:text-green-300"> <button onClick={() => handleSaveTitle(module.id, 'module')} className="w-12 h-12 flex items-center justify-center bg-blue-50 dark:bg-blue-500/10 text-blue-600 dark:text-blue-400 rounded-xl hover:bg-blue-600 hover:text-white transition-all shadow-lg shadow-blue-500/10">
<Save className="w-5 h-5" /> <Save className="w-5 h-5" />
</button> </button>
<button onClick={() => setEditingId(null)} className="text-gray-400 hover:text-red-400"> <button onClick={() => setEditingId(null)} className="w-12 h-12 flex items-center justify-center bg-red-50 dark:bg-red-500/10 text-red-500 dark:text-red-400 rounded-xl hover:bg-red-600 hover:text-white transition-all">
<X className="w-5 h-5" /> <X className="w-5 h-5" />
</button> </button>
</div> </div>
) : ( ) : (
<div className="flex items-center gap-3 group flex-1"> <div className="flex items-center gap-3 group flex-1">
<span <div className="flex flex-col">
onClick={() => { setEditingId(module.id); setEditValue(module.title); }} <span className="text-[10px] font-black uppercase tracking-widest text-slate-400 dark:text-gray-500 mb-0.5">Module {mIndex + 1}</span>
className="font-bold text-lg text-blue-600 dark:text-blue-400 cursor-pointer hover:text-blue-700 dark:hover:text-blue-300 transition-colors" <span
> onClick={() => { setEditingId(module.id); setEditValue(module.title); }}
{module.title || `Module ${module.position}`} className="font-black text-2xl text-slate-900 dark:text-white cursor-pointer hover:text-blue-600 dark:hover:text-blue-400 transition-colors uppercase tracking-tight"
</span> >
{module.title || `Untitled Section`}
</span>
</div>
<button <button
onClick={() => { setEditingId(module.id); setEditValue(module.title); }} onClick={() => { setEditingId(module.id); setEditValue(module.title); }}
className="opacity-0 group-hover:opacity-100 text-slate-400 hover:text-slate-900 dark:text-white transition-opacity" className="opacity-0 group-hover:opacity-100 p-2 text-slate-400 hover:text-blue-600 dark:text-gray-600 transition-all active:scale-90"
> >
<Pencil className="w-4 h-4" /> <Pencil className="w-4 h-4" />
</button> </button>
</div> </div>
)} )}
</div> </div>
<div className="flex items-center gap-3"> <div className="flex items-center gap-2">
<button <button
onClick={() => handleDeleteModule(module.id)} onClick={() => handleDeleteModule(module.id)}
className="text-slate-400 hover:text-red-500 dark:hover:text-red-400 transition-colors" className="w-11 h-11 flex items-center justify-center bg-transparent border border-transparent hover:border-red-500/30 hover:bg-red-50 dark:hover:bg-red-500/10 text-slate-300 hover:text-red-600 transition-all rounded-xl active:scale-95"
> >
<Trash2 className="w-4 h-4" /> <Trash2 className="w-5 h-5" />
</button> </button>
</div> </div>
</div> </div>
<div className="p-6 space-y-3"> <div className="p-10 space-y-4 bg-slate-50/30 dark:bg-transparent">
{module.lessons.map((lesson: Lesson, lIndex: number) => ( {module.lessons.map((lesson: Lesson, lIndex: number) => (
<div key={lesson.id} className="flex items-center gap-3 group/row"> <div key={lesson.id} className="flex items-center gap-5 group/row">
<div className="flex flex-col opacity-0 group-hover/row:opacity-100 transition-opacity"> <div className="flex flex-col gap-1 opacity-0 group-hover/row:opacity-100 transition-all scale-95 group-hover/row:scale-100">
<button <button
onClick={() => handleReorderLesson(module.id, lIndex, 'up')} onClick={() => handleReorderLesson(module.id, lIndex, 'up')}
disabled={lIndex === 0} disabled={lIndex === 0}
className="text-slate-400 dark:text-gray-500 hover:text-blue-600 dark:hover:text-blue-400 disabled:opacity-0" className="p-1 text-slate-400 dark:text-gray-500 hover:text-blue-600 dark:hover:text-blue-400 disabled:opacity-0 bg-white dark:bg-white/5 rounded-md border border-slate-100 dark:border-white/5"
> >
<ChevronUp className="w-3 h-3" /> <ChevronUp className="w-3 h-3" />
</button> </button>
<button <button
onClick={() => handleReorderLesson(module.id, lIndex, 'down')} onClick={() => handleReorderLesson(module.id, lIndex, 'down')}
disabled={lIndex === module.lessons.length - 1} disabled={lIndex === module.lessons.length - 1}
className="text-slate-400 dark:text-gray-500 hover:text-blue-600 dark:hover:text-blue-400 disabled:opacity-0" className="p-1 text-slate-400 dark:text-gray-500 hover:text-blue-600 dark:hover:text-blue-400 disabled:opacity-0 bg-white dark:bg-white/5 rounded-md border border-slate-100 dark:border-white/5"
> >
<ChevronDown className="w-3 h-3" /> <ChevronDown className="w-3 h-3" />
</button> </button>
@@ -346,38 +349,38 @@ export default function CourseEditor({ params }: { params: { id: string } }) {
<div className="flex-1"> <div className="flex-1">
{editingId === lesson.id ? ( {editingId === lesson.id ? (
<div className="flex items-center gap-2 glass border-blue-500/30 p-2 rounded-lg bg-slate-50 dark:bg-black/20 shadow-inner"> <div className="flex items-center gap-3 bg-white dark:bg-black/20 border-2 border-blue-500/50 p-3 rounded-2xl shadow-inner animate-in slide-in-from-left-2 duration-200">
<input <input
autoFocus autoFocus
value={editValue} value={editValue}
onChange={(e) => setEditValue(e.target.value)} onChange={(e) => setEditValue(e.target.value)}
onKeyDown={(e) => e.key === 'Enter' && handleSaveTitle(lesson.id, 'lesson')} onKeyDown={(e) => e.key === 'Enter' && handleSaveTitle(lesson.id, 'lesson')}
className="bg-transparent border-none flex-1 text-slate-900 dark:text-white focus:outline-none font-medium" className="bg-transparent border-none flex-1 text-slate-900 dark:text-white focus:outline-none font-black uppercase tracking-tight"
/> />
<button onClick={() => handleSaveTitle(lesson.id, 'lesson')} className="text-green-600 dark:text-green-400"> <button onClick={() => handleSaveTitle(lesson.id, 'lesson')} className="w-9 h-9 flex items-center justify-center bg-blue-50 dark:bg-blue-500/10 text-blue-600 dark:text-blue-400 rounded-xl hover:bg-blue-600 hover:text-white transition-all">
<Save className="w-4 h-4" /> <Save className="w-4 h-4" />
</button> </button>
<button onClick={() => setEditingId(null)} className="text-slate-400 dark:text-gray-400"> <button onClick={() => setEditingId(null)} className="w-9 h-9 flex items-center justify-center bg-red-50 dark:bg-red-500/10 text-red-500 dark:text-red-400 rounded-xl hover:bg-red-600 hover:text-white transition-all">
<X className="w-4 h-4" /> <X className="w-4 h-4" />
</button> </button>
</div> </div>
) : ( ) : (
<div className="flex items-center justify-between glass border-slate-200 dark:border-white/5 p-4 rounded-xl hover:bg-slate-50 dark:hover:bg-white/10 hover:border-blue-500/30 transition-all cursor-pointer group/lesson shadow-sm"> <div className="flex items-center justify-between bg-white dark:bg-black/20 border border-slate-200 dark:border-white/5 p-5 rounded-[1.5rem] hover:bg-slate-50 dark:hover:bg-white/10 hover:border-blue-500/30 transition-all cursor-pointer group/lesson shadow-sm">
<Link href={`/courses/${params.id}/lessons/${lesson.id}`} className="flex-1 flex items-center gap-4"> <Link href={`/courses/${params.id}/lessons/${lesson.id}`} className="flex-1 flex items-center gap-5">
<div className="p-2 bg-blue-600/10 dark:bg-blue-500/20 rounded-lg text-blue-600 dark:text-blue-400 group-hover/lesson:scale-110 transition-transform"> <div className="w-12 h-12 rounded-xl bg-blue-50 dark:bg-blue-500/10 border border-blue-100 dark:border-blue-500/20 flex items-center justify-center text-blue-600 dark:text-blue-400 group-hover/lesson:scale-110 transition-transform shadow-sm">
{lesson.content_type === 'video' ? <PlayCircle className="w-5 h-5" /> : <FileText className="w-5 h-5" />} {lesson.content_type === 'video' ? <PlayCircle className="w-6 h-6" /> : <FileText className="w-6 h-6" />}
</div> </div>
<div className="flex flex-col"> <div className="flex flex-col">
<span <span
onClick={(e) => { e.preventDefault(); e.stopPropagation(); startEditing(lesson.id, lesson.title); }} onClick={(e) => { e.preventDefault(); e.stopPropagation(); startEditing(lesson.id, lesson.title); }}
className="font-bold text-slate-900 dark:text-white hover:text-blue-600 dark:hover:text-blue-400 transition-colors" className="font-black text-slate-800 dark:text-white hover:text-blue-600 dark:hover:text-blue-400 transition-colors uppercase tracking-tight text-sm"
> >
{lesson.title} {lesson.title}
</span> </span>
<div className="flex items-center gap-3 text-xs text-slate-500 dark:text-gray-500 uppercase mt-0.5 font-bold"> <div className="flex items-center gap-3 text-[10px] text-slate-400 dark:text-gray-500 uppercase mt-1 font-black tracking-widest">
<span>{lesson.content_type}</span> <span className="px-2 py-0.5 bg-slate-100 dark:bg-white/5 rounded-md">{lesson.content_type}</span>
{lesson.due_date && ( {lesson.due_date && (
<div className="flex items-center gap-1 text-orange-600 dark:text-orange-400"> <div className="flex items-center gap-1.5 text-orange-600 dark:text-orange-400/80">
<Calendar className="w-3 h-3" /> <Calendar className="w-3 h-3" />
{new Date(lesson.due_date).toLocaleDateString()} {new Date(lesson.due_date).toLocaleDateString()}
</div> </div>
@@ -385,16 +388,16 @@ export default function CourseEditor({ params }: { params: { id: string } }) {
</div> </div>
</div> </div>
</Link> </Link>
<div className="flex items-center gap-4"> <div className="flex items-center gap-2">
<button <button
onClick={(e) => { e.preventDefault(); e.stopPropagation(); startEditing(lesson.id, lesson.title); }} onClick={(e) => { e.preventDefault(); e.stopPropagation(); startEditing(lesson.id, lesson.title); }}
className="opacity-0 group-hover/lesson:opacity-100 text-slate-400 hover:text-slate-900 dark:text-white transition-opacity" className="opacity-0 group-hover/lesson:opacity-100 w-10 h-10 flex items-center justify-center bg-transparent hover:bg-blue-50 dark:hover:bg-white/5 rounded-xl text-slate-400 hover:text-blue-600 transition-all"
> >
<Pencil className="w-4 h-4" /> <Pencil className="w-4 h-4" />
</button> </button>
<button <button
onClick={(e) => { e.preventDefault(); e.stopPropagation(); handleDeleteLesson(module.id, lesson.id); }} onClick={(e) => { e.preventDefault(); e.stopPropagation(); handleDeleteLesson(module.id, lesson.id); }}
className="opacity-0 group-hover/lesson:opacity-100 text-slate-400 hover:text-red-500 dark:hover:text-red-400 transition-opacity" className="opacity-0 group-hover/lesson:opacity-100 w-10 h-10 flex items-center justify-center bg-transparent hover:bg-red-50 dark:hover:bg-red-500/10 rounded-xl text-slate-400 hover:text-red-500 transition-all"
> >
<Trash2 className="w-4 h-4" /> <Trash2 className="w-4 h-4" />
</button> </button>
@@ -407,9 +410,9 @@ export default function CourseEditor({ params }: { params: { id: string } }) {
<button <button
onClick={() => handleAddLesson(module.id)} onClick={() => handleAddLesson(module.id)}
className="w-full py-3 border border-dashed border-slate-300 dark:border-white/10 rounded-xl text-sm text-slate-500 dark:text-gray-400 hover:text-slate-900 dark:hover:text-white hover:border-blue-500/50 hover:bg-slate-50 dark:hover:bg-white/5 transition-all mt-3 flex items-center justify-center gap-2 font-bold" className="w-full py-4 border-2 border-dashed border-slate-200 dark:border-white/10 rounded-2xl text-[10px] font-black uppercase tracking-[0.2em] text-slate-400 dark:text-gray-500 hover:text-blue-600 dark:hover:text-blue-400 hover:border-blue-500/40 hover:bg-blue-50/50 dark:hover:bg-blue-500/5 transition-all mt-4 flex items-center justify-center gap-3 active:scale-95"
> >
<Plus className="w-4 h-4" /> New Lesson <Plus className="w-4 h-4" /> Add Lesson to Module
</button> </button>
</div> </div>
</div> </div>
@@ -417,9 +420,12 @@ export default function CourseEditor({ params }: { params: { id: string } }) {
<button <button
onClick={handleAddModule} onClick={handleAddModule}
className="w-full py-6 border-2 border-dashed border-slate-300 dark:border-white/10 rounded-2xl font-bold text-slate-500 dark:text-gray-400 hover:text-slate-900 dark:hover:text-white hover:border-blue-500/50 hover:bg-slate-50 dark:hover:bg-white/5 transition-all flex items-center justify-center gap-3 text-lg" className="w-full py-12 border-2 border-dashed border-slate-200 dark:border-white/10 rounded-[2.5rem] font-black text-xs uppercase tracking-[0.3em] text-slate-400 dark:text-gray-500 hover:text-blue-600 dark:hover:text-blue-400 hover:border-blue-500/40 hover:bg-blue-50/50 dark:hover:bg-blue-500/5 transition-all flex items-center justify-center gap-4 group shadow-inner"
> >
<Plus className="w-6 h-6" /> Add New Module <div className="w-12 h-12 rounded-2xl bg-white dark:bg-white/5 flex items-center justify-center border border-slate-100 dark:border-white/5 shadow-sm group-hover:scale-110 transition-transform">
<Plus className="w-6 h-6" />
</div>
Add New Curriculum Module
</button> </button>
</div> </div>
</CourseEditorLayout> </CourseEditorLayout>
@@ -119,23 +119,23 @@ export default function PeerReviewDashboard() {
<ArrowLeft className="w-6 h-6" /> <ArrowLeft className="w-6 h-6" />
</button> </button>
<div> <div>
<h1 className="text-3xl font-bold bg-gradient-to-r from-purple-400 to-blue-400 bg-clip-text text-transparent"> <h1 className="text-4xl font-black bg-gradient-to-r from-purple-600 to-blue-600 bg-clip-text text-transparent uppercase tracking-tighter">
Peer Review Management Peer Assessment
</h1> </h1>
<p className="text-gray-400 mt-1">Monitor and manage student peer assessments</p> <p className="text-slate-500 dark:text-gray-400 mt-1 font-medium">Monitor and manage student peer feedback loops</p>
</div> </div>
</div> </div>
</div> </div>
<CourseEditorLayout activeTab="peer-reviews"> <CourseEditorLayout activeTab="peer-reviews">
<div className="grid grid-cols-1 lg:grid-cols-4 gap-8"> <div className="grid grid-cols-1 lg:grid-cols-4 gap-10">
{/* Lessons List */} {/* Lessons List */}
<div className="space-y-4"> <div className="space-y-6">
<h3 className="text-sm font-bold text-gray-500 uppercase tracking-widest px-2">Activities</h3> <h3 className="text-[10px] font-black text-slate-400 dark:text-gray-500 uppercase tracking-[0.2em] px-4">Learning Activities</h3>
<div className="space-y-2"> <div className="space-y-2">
{lessons.length === 0 ? ( {lessons.length === 0 ? (
<div className="p-4 bg-white/5 border border-white/10 rounded-xl text-sm text-gray-500 italic"> <div className="p-6 bg-slate-50 dark:bg-white/5 border border-slate-200 dark:border-white/10 rounded-2xl text-sm text-slate-400 italic">
No peer review activities found in this course. No peer review activities found.
</div> </div>
) : ( ) : (
lessons.map(lesson => ( lessons.map(lesson => (
@@ -145,13 +145,13 @@ export default function PeerReviewDashboard() {
setSelectedLessonId(lesson.id); setSelectedLessonId(lesson.id);
setSelectedSubmissionId(null); setSelectedSubmissionId(null);
}} }}
className={`w-full text-left p-4 rounded-xl border transition-all ${selectedLessonId === lesson.id className={`w-full text-left p-5 rounded-[1.5rem] border transition-all active:scale-95 ${selectedLessonId === lesson.id
? "bg-purple-500/10 border-purple-500/50 text-purple-400 shadow-lg shadow-purple-500/5" ? "bg-purple-50 dark:bg-purple-500/10 border-purple-200 dark:border-purple-500/50 text-purple-600 dark:text-purple-400 shadow-md shadow-purple-500/5 font-black uppercase tracking-tight"
: "bg-white/5 border-white/10 text-gray-400 hover:border-white/20" : "bg-white dark:bg-white/5 border-slate-200 dark:border-white/10 text-slate-500 dark:text-gray-400 hover:border-purple-500/30 font-bold"
}`} }`}
> >
<div className="font-bold truncate">{lesson.title}</div> <div className="truncate text-sm">{lesson.title}</div>
<div className="text-[10px] uppercase font-black tracking-tighter opacity-70 mt-1">Peer Assessment</div> <div className="text-[9px] uppercase font-black tracking-widest opacity-60 mt-1">Requirement</div>
</button> </button>
)) ))
)} )}
@@ -159,100 +159,111 @@ export default function PeerReviewDashboard() {
</div> </div>
{/* Submissions List */} {/* Submissions List */}
<div className="lg:col-span-3 space-y-6"> <div className="lg:col-span-3 space-y-8">
<div className="glass p-6 border-white/10"> <div className="bg-white dark:bg-black/20 border border-slate-200 dark:border-white/10 rounded-[2.5rem] p-10 shadow-sm">
<div className="flex flex-col md:flex-row md:items-center justify-between gap-4 mb-6"> <div className="flex flex-col md:flex-row md:items-center justify-between gap-6 mb-10">
<h2 className="text-xl font-bold flex items-center gap-2"> <h2 className="text-2xl font-black flex items-center gap-4 uppercase tracking-tight text-slate-900 dark:text-white">
<Users className="text-blue-400" /> <div className="w-12 h-12 rounded-2xl bg-blue-50 dark:bg-blue-500/10 border border-blue-100 dark:border-blue-500/20 flex items-center justify-center text-blue-600 dark:text-blue-400 shadow-sm">
<Users size={24} />
</div>
Submissions Submissions
</h2> </h2>
<div className="relative w-full md:w-64"> <div className="relative w-full md:w-80 group">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 text-gray-500 w-4 h-4" /> <Search className="absolute left-4 top-1/2 -translate-y-1/2 text-slate-300 dark:text-gray-500 w-5 h-5 group-focus-within:text-blue-500 transition-colors" />
<input <input
type="text" type="text"
placeholder="Search students..." placeholder="Search student or email..."
value={searchTerm} value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)} onChange={(e) => setSearchTerm(e.target.value)}
className="w-full bg-black/20 border border-white/10 rounded-xl py-2 pl-10 pr-4 text-sm focus:outline-none focus:border-blue-500/50 transition-all" className="w-full bg-slate-50 dark:bg-black/20 border border-slate-200 dark:border-white/10 rounded-[1.25rem] py-4 pl-12 pr-6 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500/30 text-slate-900 dark:text-white transition-all shadow-inner"
/> />
</div> </div>
</div> </div>
{submissionsLoading ? ( {submissionsLoading ? (
<div className="flex justify-center py-20"> <div className="flex flex-col items-center justify-center py-32 space-y-4">
<Loader2 className="w-8 h-8 text-blue-500 animate-spin" /> <Loader2 className="w-12 h-12 text-blue-500 animate-spin" />
<span className="text-xs font-black uppercase tracking-widest text-slate-400">Loading records...</span>
</div> </div>
) : filteredSubmissions.length === 0 ? ( ) : filteredSubmissions.length === 0 ? (
<div className="text-center py-20 bg-black/20 rounded-2xl border border-dashed border-white/10"> <div className="text-center py-32 bg-slate-50 dark:bg-black/20 rounded-3xl border border-dashed border-slate-200 dark:border-white/10">
<MessageSquare className="w-12 h-12 text-gray-600 mx-auto mb-4" /> <div className="w-20 h-20 bg-white dark:bg-white/5 rounded-2xl flex items-center justify-center mx-auto mb-6 shadow-sm">
<p className="text-gray-500 italic">No submissions found for this activity.</p> <MessageSquare className="w-10 h-10 text-slate-300 dark:text-gray-700" />
</div>
<p className="text-slate-500 dark:text-gray-500 font-bold uppercase tracking-tight">No submissions found for this activity.</p>
</div> </div>
) : ( ) : (
<div className="space-y-3 overflow-y-auto max-h-[600px] pr-2 custom-scrollbar"> <div className="space-y-4 overflow-y-auto max-h-[700px] pr-4 custom-scrollbar">
{filteredSubmissions.map(sub => ( {filteredSubmissions.map(sub => (
<div key={sub.id} className="group"> <div key={sub.id} className="group">
<div <div
onClick={() => setSelectedSubmissionId(selectedSubmissionId === sub.id ? null : sub.id)} onClick={() => setSelectedSubmissionId(selectedSubmissionId === sub.id ? null : sub.id)}
className={`p-4 rounded-xl border transition-all cursor-pointer ${selectedSubmissionId === sub.id className={`p-6 rounded-[1.5rem] border transition-all cursor-pointer shadow-sm active:scale-[0.99] ${selectedSubmissionId === sub.id
? "bg-blue-500/5 border-blue-500/30" ? "bg-blue-50 dark:bg-blue-500/5 border-blue-200 dark:border-blue-500/30"
: "bg-white/[0.02] border-white/5 hover:bg-white/[0.05] hover:border-white/20" : "bg-slate-50/50 dark:bg-white/[0.02] border-slate-200 dark:border-white/5 hover:bg-white dark:hover:bg-white/[0.05] hover:border-blue-500/30"
}`} }`}
> >
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div className="flex items-center gap-4"> <div className="flex items-center gap-5">
<div className="w-10 h-10 rounded-full bg-gradient-to-br from-blue-500 to-indigo-600 flex items-center justify-center font-bold text-sm"> <div className="w-12 h-12 rounded-2xl bg-gradient-to-br from-blue-600 to-indigo-700 flex items-center justify-center font-black text-white text-lg shadow-lg shadow-blue-500/20">
{sub.full_name.charAt(0)} {sub.full_name.charAt(0)}
</div> </div>
<div> <div>
<div className="font-bold text-gray-900 dark:text-white group-hover:text-blue-400 transition-colors">{sub.full_name}</div> <div className="font-black text-slate-900 dark:text-white group-hover:text-blue-600 dark:group-hover:text-blue-400 transition-colors uppercase tracking-tight text-sm">{sub.full_name}</div>
<div className="text-xs text-gray-500">{sub.email}</div> <div className="text-[10px] font-bold text-slate-400 dark:text-gray-500 uppercase tracking-widest">{sub.email}</div>
</div> </div>
</div> </div>
<div className="flex items-center gap-8"> <div className="flex items-center gap-10">
<div className="text-right"> <div className="text-right">
<div className="text-sm font-black text-gray-900 dark:text-white flex items-center gap-1.5 justify-end"> <div className="text-lg font-black text-slate-900 dark:text-white flex items-center gap-2 justify-end">
<Award className="w-4 h-4 text-yellow-400" /> <Award className="w-5 h-5 text-yellow-500" />
{sub.average_score !== null ? `${(sub.average_score).toFixed(1)}/10` : '—'} {sub.average_score !== null ? `${(sub.average_score).toFixed(1)}/10` : '—'}
</div> </div>
<div className="text-[10px] text-gray-500 font-bold uppercase tracking-widest">Avg. Score</div> <div className="text-[9px] text-slate-400 dark:text-gray-500 font-black uppercase tracking-[0.2em]">Rating</div>
</div> </div>
<div className="text-right"> <div className="text-right">
<div className={`text-sm font-black flex items-center gap-1.5 justify-end ${sub.review_count >= 2 ? 'text-green-400' : 'text-orange-400'}`}> <div className={`text-lg font-black flex items-center gap-2 justify-end ${sub.review_count >= 2 ? 'text-green-600' : 'text-orange-500'}`}>
<CheckCircle className="w-4 h-4" /> <CheckCircle className="w-5 h-5" />
{sub.review_count} {sub.review_count}
</div> </div>
<div className="text-[10px] text-gray-500 font-bold uppercase tracking-widest">Reviews</div> <div className="text-[9px] text-slate-400 dark:text-gray-500 font-black uppercase tracking-[0.2em]">Feedback</div>
</div> </div>
<div className="text-right hidden md:block"> <div className="text-right hidden xl:block">
<div className="text-sm font-medium text-gray-400 flex items-center gap-1.5 justify-end"> <div className="text-sm font-bold text-slate-400 dark:text-gray-500 flex items-center gap-2 justify-end">
<Clock className="w-4 h-4" /> <Clock className="w-4 h-4" />
{new Date(sub.submitted_at).toLocaleDateString()} {new Date(sub.submitted_at).toLocaleDateString()}
</div> </div>
<div className="text-[10px] text-gray-500 font-bold uppercase tracking-widest">Submitted</div> <div className="text-[9px] text-slate-400 dark:text-gray-500 font-black uppercase tracking-[0.2em]">Delivery</div>
</div> </div>
<ChevronRight className={`w-5 h-5 text-gray-600 transition-transform ${selectedSubmissionId === sub.id ? 'rotate-90' : ''}`} /> <ChevronRight className={`w-6 h-6 text-slate-300 transition-all ${selectedSubmissionId === sub.id ? 'rotate-90 text-blue-500' : ''}`} />
</div> </div>
</div> </div>
</div> </div>
{/* Reviews Detail Drawer-like expansion */} {/* Reviews Detail Drawer-like expansion */}
{selectedSubmissionId === sub.id && ( {selectedSubmissionId === sub.id && (
<div className="mt-2 ml-4 p-6 bg-black/40 border-l-2 border-blue-500/50 rounded-r-xl space-y-4 animate-in slide-in-from-left-2 duration-200"> <div className="mt-4 ml-8 p-10 bg-slate-50 dark:bg-black/40 border-l-4 border-blue-500 dark:border-blue-500/50 rounded-r-[2rem] space-y-8 animate-in slide-in-from-left-4 duration-300 shadow-inner">
<h4 className="text-sm font-bold uppercase tracking-widest text-blue-400">Review Details</h4> <h4 className="text-[10px] font-black uppercase tracking-[0.3em] text-blue-600 dark:text-blue-400 ml-1">Submission Feedback Details</h4>
{reviewsLoading ? ( {reviewsLoading ? (
<div className="flex py-4"><Loader2 className="w-5 h-5 animate-spin text-blue-500" /></div> <div className="flex py-10"><Loader2 className="w-8 h-8 animate-spin text-blue-500" /></div>
) : reviews.length === 0 ? ( ) : reviews.length === 0 ? (
<p className="text-sm text-gray-500 italic">No reviews received yet.</p> <div className="p-8 bg-white dark:bg-white/5 border border-slate-200 dark:border-white/10 rounded-2xl text-center text-slate-400 italic">
No peer reviews have been submitted for this user yet.
</div>
) : ( ) : (
<div className="grid grid-cols-1 md:grid-cols-2 gap-4"> <div className="grid grid-cols-1 md:grid-cols-2 gap-8">
{reviews.map(review => ( {reviews.map(review => (
<div key={review.id} className="p-4 bg-white/5 border border-white/10 rounded-xl space-y-2"> <div key={review.id} className="p-6 bg-white dark:bg-white/5 border border-slate-100 dark:border-white/10 rounded-2xl space-y-4 shadow-sm group/review hover:border-blue-500/30 transition-all">
<div className="flex justify-between items-center"> <div className="flex justify-between items-center pb-3 border-b border-slate-50 dark:border-white/5">
<span className="text-xs font-bold text-gray-400">Review Task</span> <span className="text-[9px] font-black text-slate-400 dark:text-gray-500 uppercase tracking-widest">Peer Evaluator</span>
<span className="text-sm font-black text-yellow-500">{review.score}/10</span> <span className="px-3 py-1 bg-yellow-50 dark:bg-yellow-500/10 text-yellow-600 dark:text-yellow-500 text-sm font-black rounded-lg">
{review.score}/10
</span>
</div> </div>
<p className="text-sm text-gray-300 leading-relaxed italic">"{review.feedback}"</p> <p className="text-sm text-slate-600 dark:text-gray-300 leading-relaxed italic font-medium px-2">
"{review.feedback}"
</p>
</div> </div>
))} ))}
</div> </div>
@@ -17,9 +17,9 @@ export default function RubricsPage() {
pageTitle="Gestión de Rúbricas" pageTitle="Gestión de Rúbricas"
pageDescription="Crea y gestiona rúbricas de evaluación para tu curso." pageDescription="Crea y gestiona rúbricas de evaluación para tu curso."
pageActions={ pageActions={
<div className="hidden md:flex items-center gap-2 bg-blue-500/10 border border-blue-500/20 px-4 py-2 rounded-2xl text-blue-400 text-sm"> <div className="hidden md:flex items-center gap-3 bg-blue-50 dark:bg-blue-500/10 border border-blue-100 dark:border-blue-500/20 px-5 py-2.5 rounded-2xl text-blue-600 dark:text-blue-400 text-[10px] font-black uppercase tracking-widest shadow-sm">
<Info className="w-4 h-4" /> <Info className="w-4 h-4" />
<span>Las rúbricas pueden asignarse a múltiples lecciones.</span> <span>Reusable rubrics can be assigned to multiple lessons across the course.</span>
</div> </div>
} }
> >
@@ -87,60 +87,62 @@ export default function LiveSessionsPage() {
pageActions={ pageActions={
<button <button
onClick={() => setIsCreateModalOpen(true)} onClick={() => setIsCreateModalOpen(true)}
className="flex items-center gap-2 px-5 py-2.5 bg-blue-600 hover:bg-blue-500 text-white rounded-xl font-bold text-sm shadow-md shadow-blue-600/20 transition-all active:scale-95" className="flex items-center gap-2 px-6 py-3 bg-blue-600 hover:bg-blue-500 text-white rounded-2xl font-black text-[10px] uppercase tracking-[0.2em] shadow-xl shadow-blue-500/20 transition-all active:scale-95"
> >
<Plus size={16} /> <Plus size={18} />
Programar Sesión PROGRAM SESSION
</button> </button>
} }
> >
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6"> <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{meetings.length === 0 ? ( {meetings.length === 0 ? (
<div className="col-span-full py-20 bg-white/[0.02] border border-dashed border-white/10 rounded-3xl text-center"> <div className="col-span-full py-32 bg-slate-50 dark:bg-white/[0.02] border border-dashed border-slate-200 dark:border-white/10 rounded-[3rem] text-center">
<Video className="w-16 h-16 text-gray-700 mx-auto mb-4" /> <div className="w-24 h-24 bg-white dark:bg-white/5 border border-slate-100 dark:border-white/5 rounded-[2rem] flex items-center justify-center mx-auto mb-6 shadow-sm">
<h3 className="text-xl font-bold text-gray-400 mb-2">No active sessions</h3> <Video className="w-12 h-12 text-slate-300 dark:text-gray-700" />
<p className="text-gray-500 max-w-sm mx-auto">Start by scheduling your first virtual meeting for this course.</p> </div>
<h3 className="text-2xl font-black text-slate-900 dark:text-white mb-2 uppercase tracking-tight">No active sessions</h3>
<p className="text-slate-500 dark:text-gray-500 max-w-sm mx-auto font-medium">Start by scheduling your first virtual meeting for this course.</p>
</div> </div>
) : ( ) : (
meetings.map(meeting => ( meetings.map(meeting => (
<div key={meeting.id} className="glass p-6 border-white/10 hover:border-blue-500/30 transition-all group relative overflow-hidden"> <div key={meeting.id} className="bg-white dark:bg-black/20 rounded-[2rem] p-8 border border-slate-200 dark:border-white/10 hover:border-blue-500/30 transition-all group relative overflow-hidden shadow-sm hover:shadow-md">
<div className="absolute top-0 right-0 p-2 opacity-0 group-hover:opacity-100 transition-opacity"> <div className="absolute top-4 right-4 opacity-0 group-hover:opacity-100 transition-opacity">
<button <button
onClick={() => handleDeleteMeeting(meeting.id)} onClick={() => handleDeleteMeeting(meeting.id)}
className="p-2 hover:bg-red-500/20 text-red-400 rounded-lg transition-colors" className="p-3 bg-red-50 dark:bg-red-500/10 hover:bg-red-500 text-red-500 hover:text-white rounded-xl transition-all shadow-sm"
> >
<Trash2 size={16} /> <Trash2 size={18} />
</button> </button>
</div> </div>
<div className="flex items-start gap-4 mb-6"> <div className="flex items-start gap-5 mb-8">
<div className="w-12 h-12 rounded-2xl bg-blue-500/20 border border-blue-500/30 flex items-center justify-center text-blue-400 shrink-0"> <div className="w-14 h-14 rounded-2xl bg-blue-50 dark:bg-blue-500/10 border border-blue-100 dark:border-blue-500/20 flex items-center justify-center text-blue-600 dark:text-blue-400 shrink-0 shadow-sm transition-transform group-hover:scale-110">
<Video size={24} /> <Video size={28} />
</div> </div>
<div> <div>
<h4 className="font-bold text-lg leading-tight mb-1 group-hover:text-blue-400 transition-colors uppercase tracking-tight">{meeting.title}</h4> <h4 className="font-black text-xl leading-tight mb-2 text-slate-900 dark:text-white group-hover:text-blue-600 dark:group-hover:text-blue-400 transition-colors uppercase tracking-tight">{meeting.title}</h4>
<div className="flex items-center gap-2 text-[10px] font-black uppercase tracking-widest text-gray-500"> <div className="flex items-center gap-2 text-[10px] font-black uppercase tracking-widest text-slate-400 dark:text-gray-500">
<Globe size={10} /> <Globe size={12} className="text-blue-500/50" />
{meeting.provider} Session {meeting.provider} Session
</div> </div>
</div> </div>
</div> </div>
<div className="space-y-3 mb-8"> <div className="space-y-4 mb-10">
<div className="flex items-center gap-3 text-sm text-gray-400"> <div className="flex items-center gap-3 text-sm text-slate-600 dark:text-gray-400 bg-slate-50 dark:bg-white/5 p-3 rounded-xl border border-slate-100 dark:border-white/5 shadow-inner">
<CalendarIcon size={16} className="text-blue-500" /> <CalendarIcon size={18} className="text-blue-500" />
<span className="font-medium">{new Date(meeting.start_at).toLocaleDateString(undefined, { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' })}</span> <span className="font-bold tracking-tight uppercase text-[11px]">{new Date(meeting.start_at).toLocaleDateString(undefined, { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' })}</span>
</div> </div>
<div className="flex items-center gap-3 text-sm text-gray-400"> <div className="flex items-center gap-3 text-sm text-slate-600 dark:text-gray-400 bg-slate-50 dark:bg-white/5 p-3 rounded-xl border border-slate-100 dark:border-white/5 shadow-inner">
<Clock size={16} className="text-blue-500" /> <Clock size={18} className="text-blue-500" />
<span className="font-medium"> <span className="font-black text-[11px] uppercase tracking-widest">
{new Date(meeting.start_at).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })} {new Date(meeting.start_at).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}
<span className="mx-2 opacity-30">|</span> <span className="mx-3 opacity-30">|</span>
{meeting.duration_minutes} minutes {meeting.duration_minutes} MIN
</span> </span>
</div> </div>
{meeting.description && ( {meeting.description && (
<p className="text-xs text-gray-500 line-clamp-2 mt-2 leading-relaxed italic"> <p className="text-xs text-slate-500 dark:text-gray-500 line-clamp-3 mt-4 leading-relaxed italic px-2">
"{meeting.description}" "{meeting.description}"
</p> </p>
)} )}
@@ -148,17 +150,17 @@ export default function LiveSessionsPage() {
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<button <button
className="flex-1 py-2 bg-white/5 hover:bg-white/10 border border-white/10 rounded-xl text-xs font-bold transition-all flex items-center justify-center gap-2" className="flex-1 py-3 bg-slate-50 dark:bg-white/5 hover:bg-slate-100 dark:hover:bg-white/10 border border-slate-200 dark:border-white/10 rounded-xl text-[10px] font-black uppercase tracking-widest text-slate-600 dark:text-white transition-all flex items-center justify-center gap-2 shadow-sm active:scale-95"
onClick={() => { onClick={() => {
const url = meeting.join_url || `https://meet.jit.si/${meeting.meeting_id}`; const url = meeting.join_url || `https://meet.jit.si/${meeting.meeting_id}`;
window.open(url, '_blank'); window.open(url, '_blank');
}} }}
> >
<LinkIcon size={14} /> <LinkIcon size={14} className="text-blue-500" />
Preview Link PREVIEW LINK
</button> </button>
<div className={`px-3 py-2 rounded-xl text-[10px] font-black uppercase tracking-widest ${meeting.is_active ? 'bg-green-500/20 text-green-400' : 'bg-gray-500/20 text-gray-500'}`}> <div className={`px-4 py-3 rounded-xl text-[10px] font-black uppercase tracking-widest border ${meeting.is_active ? 'bg-green-50 dark:bg-green-500/10 text-green-600 dark:text-green-400 border-green-100 dark:border-green-500/20' : 'bg-slate-100 dark:bg-gray-500/20 text-slate-400 dark:text-gray-500 border-slate-200 dark:border-white/5'}`}>
{meeting.is_active ? 'Scheduled' : 'Past'} {meeting.is_active ? 'SCHEDULED' : 'PAST'}
</div> </div>
</div> </div>
</div> </div>
@@ -169,65 +171,67 @@ export default function LiveSessionsPage() {
{/* Create Meeting Modal */} {/* Create Meeting Modal */}
{isCreateModalOpen && ( {isCreateModalOpen && (
<div className="fixed inset-0 z-[100] flex items-center justify-center p-4 bg-black/80 backdrop-blur-sm"> <div className="fixed inset-0 z-[100] flex items-center justify-center p-4 bg-slate-900/60 dark:bg-black/80 backdrop-blur-md">
<div className="bg-[#16181b] border border-white/10 rounded-3xl w-full max-w-lg overflow-hidden shadow-2xl scale-in-center"> <div className="bg-white dark:bg-[#16181b] border border-slate-200 dark:border-white/10 rounded-[2.5rem] w-full max-w-lg overflow-hidden shadow-2xl animate-in fade-in zoom-in duration-200">
<div className="p-6 border-b border-white/5 flex items-center justify-between"> <div className="p-8 border-b border-slate-100 dark:border-white/5 flex items-center justify-between bg-slate-50/50 dark:bg-transparent">
<h2 className="text-xl font-bold flex items-center gap-2 font-black uppercase tracking-tighter"> <h2 className="text-2xl font-black flex items-center gap-3 uppercase tracking-tighter text-slate-900 dark:text-white">
<Video className="text-blue-500" /> <div className="p-2 bg-blue-50 dark:bg-blue-500/10 rounded-xl text-blue-600 dark:text-blue-400">
<Video size={24} />
</div>
Schedule Session Schedule Session
</h2> </h2>
<button onClick={() => setIsCreateModalOpen(false)} className="p-2 hover:bg-white/10 rounded-full transition-colors"> <button onClick={() => setIsCreateModalOpen(false)} className="w-10 h-10 flex items-center justify-center hover:bg-slate-100 dark:hover:bg-white/10 text-slate-400 rounded-full transition-colors">
<X size={20} /> <X size={24} />
</button> </button>
</div> </div>
<div className="p-8 space-y-4"> <div className="p-10 space-y-6">
<div className="space-y-2"> <div className="space-y-2">
<label className="text-xs font-black uppercase tracking-widest text-gray-500">Meeting Title</label> <label className="text-[10px] font-black uppercase tracking-widest text-slate-400 dark:text-gray-500 ml-1">Meeting Title</label>
<input <input
type="text" type="text"
value={newMeeting.title} value={newMeeting.title}
onChange={(e) => setNewMeeting({ ...newMeeting, title: e.target.value })} onChange={(e) => setNewMeeting({ ...newMeeting, title: e.target.value })}
placeholder="e.g., Weekly Office Hours" placeholder="e.g., Weekly Office Hours"
className="w-full bg-black/20 border border-white/10 rounded-xl py-3 px-4 text-sm focus:outline-none focus:border-blue-500/50" className="w-full bg-slate-50 dark:bg-black/20 border border-slate-200 dark:border-white/10 rounded-2xl py-4 px-5 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500/30 text-slate-900 dark:text-white placeholder:text-slate-300 transition-all font-medium shadow-inner"
/> />
</div> </div>
<div className="space-y-2"> <div className="space-y-2">
<label className="text-xs font-black uppercase tracking-widest text-gray-500">Description (Optional)</label> <label className="text-[10px] font-black uppercase tracking-widest text-slate-400 dark:text-gray-500 ml-1">Description (Optional)</label>
<textarea <textarea
value={newMeeting.description} value={newMeeting.description}
onChange={(e) => setNewMeeting({ ...newMeeting, description: e.target.value })} onChange={(e) => setNewMeeting({ ...newMeeting, description: e.target.value })}
placeholder="What will be covered?" placeholder="What will be covered?"
className="w-full bg-black/20 border border-white/10 rounded-xl py-3 px-4 text-sm focus:outline-none focus:border-blue-500/50 resize-none h-20" className="w-full bg-slate-50 dark:bg-black/20 border border-slate-200 dark:border-white/10 rounded-2xl py-4 px-5 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500/30 text-slate-900 dark:text-white placeholder:text-slate-300 transition-all font-medium h-24 resize-none shadow-inner"
/> />
</div> </div>
<div className="grid grid-cols-2 gap-4"> <div className="grid grid-cols-2 gap-6">
<div className="space-y-2"> <div className="space-y-2">
<label className="text-xs font-black uppercase tracking-widest text-gray-500">Start Time</label> <label className="text-[10px] font-black uppercase tracking-widest text-slate-400 dark:text-gray-500 ml-1">Start Time</label>
<input <input
type="datetime-local" type="datetime-local"
value={newMeeting.start_at} value={newMeeting.start_at}
onChange={(e) => setNewMeeting({ ...newMeeting, start_at: e.target.value })} onChange={(e) => setNewMeeting({ ...newMeeting, start_at: e.target.value })}
className="w-full bg-black/20 border border-white/10 rounded-xl py-3 px-4 text-sm focus:outline-none focus:border-blue-500/50 text-gray-900 dark:text-white" className="w-full bg-slate-50 dark:bg-black/20 border border-slate-200 dark:border-white/10 rounded-2xl py-4 px-5 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500/30 text-slate-900 dark:text-white transition-all font-medium shadow-inner"
/> />
</div> </div>
<div className="space-y-2"> <div className="space-y-2">
<label className="text-xs font-black uppercase tracking-widest text-gray-500">Duration (Min)</label> <label className="text-[10px] font-black uppercase tracking-widest text-slate-400 dark:text-gray-500 ml-1">Duration (Min)</label>
<input <input
type="number" type="number"
value={newMeeting.duration_minutes} value={newMeeting.duration_minutes}
onChange={(e) => setNewMeeting({ ...newMeeting, duration_minutes: parseInt(e.target.value) })} onChange={(e) => setNewMeeting({ ...newMeeting, duration_minutes: parseInt(e.target.value) })}
className="w-full bg-black/20 border border-white/10 rounded-xl py-3 px-4 text-sm focus:outline-none focus:border-blue-500/50" className="w-full bg-slate-50 dark:bg-black/20 border border-slate-200 dark:border-white/10 rounded-2xl py-4 px-5 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500/30 text-slate-900 dark:text-white transition-all font-medium shadow-inner"
/> />
</div> </div>
</div> </div>
<div className="pt-6"> <div className="pt-8 pb-4">
<button <button
onClick={handleCreateMeeting} onClick={handleCreateMeeting}
disabled={!newMeeting.title} disabled={!newMeeting.title}
className="w-full py-4 bg-gradient-to-r from-blue-600 to-indigo-600 hover:from-blue-500 hover:to-indigo-500 disabled:opacity-50 disabled:cursor-not-allowed rounded-2xl text-sm font-black uppercase tracking-[0.2em] transition-all shadow-xl shadow-blue-500/10" className="w-full py-5 bg-gradient-to-r from-blue-600 to-indigo-600 hover:from-blue-500 hover:to-indigo-500 disabled:opacity-50 disabled:cursor-not-allowed rounded-2xl text-xs font-black uppercase tracking-[0.3em] transition-all shadow-xl shadow-blue-500/20 active:scale-95 text-white"
> >
Create Session Confirm & Create Session
</button> </button>
</div> </div>
</div> </div>
@@ -16,7 +16,8 @@ import {
Mail, Mail,
Plus, Plus,
UserCircle, UserCircle,
MoreHorizontal MoreHorizontal,
ChevronRight
} from "lucide-react"; } from "lucide-react";
import CourseEditorLayout from "@/components/CourseEditorLayout"; import CourseEditorLayout from "@/components/CourseEditorLayout";
@@ -137,21 +138,21 @@ export default function CourseStudentsPage() {
Listado de Estudiantes Listado de Estudiantes
</h2> </h2>
{/* Search and Filters */} {/* Search and Filters */}
<div className="glass p-4 rounded-2xl flex flex-col md:flex-row items-center gap-4"> <div className="bg-slate-50/50 dark:bg-white/5 border border-slate-200 dark:border-white/10 p-5 rounded-3xl flex flex-col md:flex-row items-center gap-4 shadow-sm">
<div className="relative flex-1 w-full"> <div className="relative flex-1 w-full">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 text-gray-500 w-4 h-4" /> <Search className="absolute left-3.5 top-1/2 -translate-y-1/2 text-slate-400 dark:text-gray-500 w-4 h-4" />
<input <input
type="text" type="text"
placeholder="Search by name or email..." placeholder="Search by name or email..."
value={searchTerm} value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)} onChange={(e) => setSearchTerm(e.target.value)}
className="w-full bg-slate-100 dark:bg-black/20 border border-slate-200 dark:border-white/10 rounded-xl py-2 pl-10 pr-4 text-sm focus:outline-none focus:border-blue-500/50 transition-all font-medium text-slate-900 dark:text-white placeholder-slate-400 dark:placeholder-gray-600" className="w-full bg-white dark:bg-black/20 border border-slate-200 dark:border-white/10 rounded-2xl py-2.5 pl-11 pr-4 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500/50 transition-all font-bold text-slate-900 dark:text-white placeholder-slate-400 dark:placeholder-gray-600 shadow-inner"
/> />
</div> </div>
<div className="flex items-center gap-2 w-full md:w-auto"> <div className="flex items-center gap-3 w-full md:w-auto">
<Filter size={16} className="text-gray-400" /> <Filter size={16} className="text-slate-400 dark:text-gray-400" />
<select <select
className="bg-slate-100 dark:bg-black/20 border border-slate-200 dark:border-white/10 rounded-xl px-4 py-2 text-sm focus:outline-none focus:border-blue-500/50 text-slate-900 dark:text-white min-w-[150px]" className="bg-white dark:bg-black/20 border border-slate-200 dark:border-white/10 rounded-2xl px-5 py-2.5 text-xs font-black uppercase tracking-widest focus:outline-none focus:ring-2 focus:ring-blue-500/50 text-slate-900 dark:text-white min-w-[180px] shadow-sm cursor-pointer"
value={selectedCohortId} value={selectedCohortId}
onChange={(e) => setSelectedCohortId(e.target.value)} onChange={(e) => setSelectedCohortId(e.target.value)}
> >
@@ -162,16 +163,16 @@ export default function CourseStudentsPage() {
</div> </div>
{/* Student List */} {/* Student List */}
<div className="rounded-3xl border border-white/10 bg-white/[0.02] overflow-hidden"> <div className="rounded-3xl border border-slate-200 dark:border-white/10 bg-white dark:bg-white/[0.02] overflow-hidden shadow-sm">
<table className="w-full text-left border-collapse"> <table className="w-full text-left border-collapse">
<thead> <thead>
<tr className="bg-white/5 border-b border-white/5 font-bold text-xs uppercase tracking-widest text-gray-500 uppercase"> <tr className="bg-slate-50 dark:bg-white/5 border-b border-slate-200 dark:border-white/5 font-black text-[10px] uppercase tracking-[0.2em] text-slate-400 dark:text-gray-500">
<th className="p-6">Student</th> <th className="p-6">Student</th>
<th className="p-6">Enrollment Date</th> <th className="p-6 text-center">Enrollment Status</th>
<th className="p-6 text-right">Actions</th> <th className="p-6 text-right">Actions</th>
</tr> </tr>
</thead> </thead>
<tbody className="divide-y divide-white/5"> <tbody className="divide-y divide-slate-100 dark:divide-white/5">
{filteredStudents.length === 0 ? ( {filteredStudents.length === 0 ? (
<tr> <tr>
<td colSpan={3} className="p-12 text-center text-slate-500 dark:text-gray-500 italic">No students found.</td> <td colSpan={3} className="p-12 text-center text-slate-500 dark:text-gray-500 italic">No students found.</td>
@@ -180,41 +181,45 @@ export default function CourseStudentsPage() {
<tr key={student.user_id} className="hover:bg-white/[0.02] transition-colors group"> <tr key={student.user_id} className="hover:bg-white/[0.02] transition-colors group">
<td className="p-6"> <td className="p-6">
<div className="flex items-center gap-4"> <div className="flex items-center gap-4">
<div className="w-10 h-10 rounded-full bg-gradient-to-br from-blue-500 to-indigo-600 flex items-center justify-center font-bold text-sm"> <div className="w-12 h-12 rounded-2xl bg-gradient-to-br from-blue-500 to-indigo-600 flex items-center justify-center font-black text-white text-lg shadow-lg shadow-blue-500/20">
{student.full_name.charAt(0)} {student.full_name.charAt(0)}
</div> </div>
<div> <div>
<div className="font-bold text-gray-900 dark:text-white group-hover:text-blue-400 transition-colors uppercase tracking-tight">{student.full_name}</div> <div className="font-black text-slate-900 dark:text-white group-hover:text-blue-600 dark:group-hover:text-blue-400 transition-colors uppercase tracking-tight text-sm">{student.full_name}</div>
<div className="text-xs text-gray-500 flex items-center gap-1 mt-0.5"><Mail size={12} /> {student.email}</div> <div className="text-xs text-slate-400 dark:text-gray-500 flex items-center gap-1.5 mt-1 font-medium italic"><Mail size={12} className="text-blue-400" /> {student.email}</div>
</div> </div>
</div> </div>
</td> </td>
<td className="p-6 text-gray-400 text-sm font-medium"> <td className="p-6 text-center">
{/* In a real app we'd have the enrollment date here */} <div className="inline-flex items-center gap-2 px-3 py-1.5 bg-green-500/10 text-green-600 dark:text-green-400 rounded-full border border-green-500/20 text-[10px] font-black uppercase tracking-widest">
Available upon request <CheckCircle2 size={12} /> Active
</div>
</td> </td>
<td className="p-6 text-right"> <td className="p-6 text-right">
<div className="flex items-center justify-end gap-2"> <div className="flex items-center justify-end gap-2">
<div className="relative group/actions"> <div className="relative group/actions">
<button className="p-2 hover:bg-white/10 rounded-lg transition-colors text-gray-500"> <button className="p-2.5 hover:bg-slate-100 dark:hover:bg-white/10 rounded-xl transition-all text-slate-400 dark:text-gray-500 active:scale-90">
<MoreHorizontal size={20} /> <MoreHorizontal size={20} />
</button> </button>
<div className="absolute right-0 top-full mt-2 w-48 bg-[#1a1c1e] border border-white/10 rounded-xl shadow-2xl invisible group-hover/actions:visible z-10 p-2"> <div className="absolute right-0 top-full mt-2 w-56 bg-white dark:bg-[#1a1c1e] border border-slate-200 dark:border-white/10 rounded-2xl shadow-2xl invisible group-hover/actions:visible z-10 p-2 animate-in fade-in slide-in-from-top-2 duration-200">
<div className="text-[10px] font-black uppercase tracking-widest text-gray-600 px-3 py-2 border-b border-white/5 mb-1">Move to Cohort</div> <div className="text-[10px] font-black uppercase tracking-widest text-slate-400 dark:text-gray-600 px-3 py-2.5 border-b border-slate-100 dark:border-white/5 mb-1.5 flex items-center gap-2">
<Filter size={10} /> Move to Cohort
</div>
{cohorts.map(c => ( {cohorts.map(c => (
<button <button
key={c.id} key={c.id}
onClick={() => handleCohortAssignment(student.user_id, c.id)} onClick={() => handleCohortAssignment(student.user_id, c.id)}
className="w-full text-left px-3 py-2 text-xs hover:bg-white/5 rounded-lg transition-colors" className="w-full text-left px-4 py-2.5 text-xs font-bold text-slate-700 dark:text-gray-300 hover:bg-slate-50 dark:hover:bg-white/5 hover:text-blue-600 dark:hover:text-white rounded-xl transition-all flex items-center justify-between group/item"
> >
{c.name} {c.name}
<ChevronRight size={14} className="opacity-0 group-hover/item:opacity-100 -translate-x-2 group-hover/item:translate-x-0 transition-all" />
</button> </button>
))} ))}
<button <button
className="w-full text-left px-3 py-2 text-xs text-red-400 hover:bg-red-500/10 rounded-lg transition-colors mt-2 border-t border-white/5" className="w-full text-left px-4 py-2.5 text-xs font-black uppercase tracking-widest text-red-500 hover:bg-red-50 dark:hover:bg-red-500/10 rounded-xl transition-all mt-2 border-t border-slate-100 dark:border-white/5 pt-3"
onClick={() => { if (confirm("Unenroll student?")) handleCohortAssignment(student.user_id, "", true) }} onClick={() => { if (confirm("Unenroll student?")) handleCohortAssignment(student.user_id, "", true) }}
> >
Unenroll Unenroll Student
</button> </button>
</div> </div>
</div> </div>
@@ -231,58 +236,68 @@ export default function CourseStudentsPage() {
{/* Enroll Modal */} {/* Enroll Modal */}
{ {
isEnrollModalOpen && ( isEnrollModalOpen && (
<div className="fixed inset-0 z-[100] flex items-center justify-center p-4 bg-black/80 backdrop-blur-sm animate-in fade-in duration-200"> <div className="fixed inset-0 z-[100] flex items-center justify-center p-4 bg-slate-900/40 dark:bg-black/80 backdrop-blur-md animate-in fade-in duration-300">
<div className="bg-[#16181b] border border-white/10 rounded-3xl w-full max-w-2xl overflow-hidden shadow-2xl scale-in-center"> <div className="bg-white dark:bg-[#16181b] border border-slate-200 dark:border-white/10 rounded-[2.5rem] w-full max-w-2xl overflow-hidden shadow-2xl scale-in-center">
<div className="p-6 border-b border-white/5 flex items-center justify-between"> <div className="p-8 border-b border-slate-100 dark:border-white/5 flex items-center justify-between bg-slate-50/50 dark:bg-transparent">
<h2 className="text-xl font-bold flex items-center gap-2"> <div>
<UserPlus className="text-blue-500" /> <h2 className="text-2xl font-black flex items-center gap-3 text-slate-900 dark:text-white">
Enroll Organization Students <UserPlus className="text-blue-600 dark:text-blue-500" />
</h2> Enroll Students
<button onClick={() => setIsEnrollModalOpen(false)} className="p-2 hover:bg-white/10 rounded-full transition-colors"> </h2>
<X size={20} /> <p className="text-xs text-slate-400 dark:text-gray-500 font-bold uppercase tracking-widest mt-1">Select from organization directory</p>
</div>
<button onClick={() => setIsEnrollModalOpen(false)} className="p-3 hover:bg-slate-200 dark:hover:bg-white/10 rounded-2xl transition-all group active:scale-90">
<X size={20} className="text-slate-400 group-hover:text-slate-900 dark:group-hover:text-white" />
</button> </button>
</div> </div>
<div className="p-8 space-y-6"> <div className="p-8 space-y-8">
<div className="relative"> <div className="relative">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 text-gray-500 w-4 h-4" /> <Search className="absolute left-4 top-1/2 -translate-y-1/2 text-slate-400 dark:text-gray-500 w-5 h-5" />
<input <input
type="text" type="text"
placeholder="Search and select students from organization..." placeholder="Search by name or email..."
value={enrollSearch} value={enrollSearch}
onChange={(e) => setEnrollSearch(e.target.value)} onChange={(e) => setEnrollSearch(e.target.value)}
className="w-full bg-black/20 border border-white/10 rounded-xl py-3 pl-10 pr-4 text-sm focus:outline-none" className="w-full bg-slate-100 dark:bg-black/20 border border-slate-200 dark:border-white/10 rounded-[1.5rem] py-4 pl-12 pr-4 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500/50 font-bold text-slate-900 dark:text-white shadow-inner"
/> />
</div> </div>
<div className="space-y-2 max-h-[300px] overflow-y-auto pr-2 custom-scrollbar"> <div className="space-y-3 max-h-[400px] overflow-y-auto pr-2 custom-scrollbar">
{orgUsersLoading ? ( {orgUsersLoading ? (
<div className="flex justify-center p-8"><Loader2 className="w-8 h-8 animate-spin text-blue-500" /></div> <div className="flex justify-center p-12 text-blue-500 items-center flex-col gap-4">
<Loader2 className="w-10 h-10 animate-spin" />
<span className="text-xs font-black uppercase tracking-widest text-slate-400">Fetching Directory...</span>
</div>
) : allOrgUsers.filter(u => u.full_name.toLowerCase().includes(enrollSearch.toLowerCase())).length === 0 ? ( ) : allOrgUsers.filter(u => u.full_name.toLowerCase().includes(enrollSearch.toLowerCase())).length === 0 ? (
<div className="text-center p-8 text-gray-500 italic">No remaining students to enroll.</div> <div className="text-center p-12 text-slate-400 dark:text-gray-500 italic font-medium">No remaining students found in organization directory.</div>
) : ( ) : (
allOrgUsers.filter(u => u.full_name.toLowerCase().includes(enrollSearch.toLowerCase())).map(user => ( allOrgUsers.filter(u => u.full_name.toLowerCase().includes(enrollSearch.toLowerCase())).map(user => (
<div key={user.id} className="flex items-center justify-between p-4 bg-white/5 border border-white/10 rounded-xl"> <div key={user.id} className="flex items-center justify-between p-5 bg-slate-50 dark:bg-white/5 border border-slate-200 dark:border-white/10 rounded-3xl group/user hover:bg-white dark:hover:bg-white/[0.08] transition-all shadow-sm">
<div className="flex items-center gap-3"> <div className="flex items-center gap-4">
<UserCircle className="text-gray-500" /> <div className="w-12 h-12 rounded-2xl bg-white dark:bg-black/20 border border-slate-100 dark:border-white/10 flex items-center justify-center shadow-sm">
<UserCircle className="text-slate-400 dark:text-gray-500" size={24} />
</div>
<div> <div>
<div className="font-bold text-sm tracking-tight">{user.full_name}</div> <div className="font-black text-slate-900 dark:text-white text-sm tracking-tight group-hover/user:text-blue-600 dark:group-hover/user:text-blue-400 transition-colors uppercase">{user.full_name}</div>
<div className="text-xs text-gray-500">{user.email}</div> <div className="text-xs text-slate-400 dark:text-gray-500 font-medium flex items-center gap-1.5 mt-0.5 italic"><Mail size={10} className="text-blue-400" /> {user.email}</div>
</div> </div>
</div> </div>
<button <button
onClick={() => handleEnroll([user.email])} onClick={() => handleEnroll([user.email])}
className="px-4 py-1.5 bg-blue-600 hover:bg-blue-500 text-gray-900 dark:text-white text-xs font-bold rounded-lg transition-all" className="px-6 py-2 bg-blue-600 hover:bg-blue-500 text-white text-xs font-black uppercase tracking-widest rounded-xl transition-all shadow-md shadow-blue-500/20 active:scale-95"
> >
Enroll Now Enroll
</button> </button>
</div> </div>
)) ))
)} )}
</div> </div>
<div className="bg-blue-500/10 border border-blue-500/20 p-4 rounded-xl flex gap-3"> <div className="bg-blue-50 dark:bg-blue-500/10 border border-blue-200 dark:border-blue-500/20 p-5 rounded-3xl flex gap-4 shadow-sm">
<Plus size={20} className="text-blue-400 shrink-0" /> <div className="w-10 h-10 rounded-2xl bg-white dark:bg-black/20 flex items-center justify-center shrink-0 shadow-sm">
<div className="text-xs text-blue-300 leading-relaxed font-medium"> <Plus size={20} className="text-blue-600 dark:text-blue-400" />
</div>
<div className="text-xs text-blue-800/80 dark:text-blue-300 leading-relaxed font-bold uppercase tracking-wide">
You can also enroll external students by going to the <strong>Gradebook</strong> and using the Bulk Enroll feature. You can also enroll external students by going to the <strong>Gradebook</strong> and using the Bulk Enroll feature.
</div> </div>
</div> </div>
+41 -31
View File
@@ -119,31 +119,36 @@ export default function CourseTeamPage() {
{loading ? ( {loading ? (
<div className="py-20 text-center text-gray-500 animate-pulse">Loading team members...</div> <div className="py-20 text-center text-gray-500 animate-pulse">Loading team members...</div>
) : instructors.length === 0 ? ( ) : instructors.length === 0 ? (
<div className="py-20 glass rounded-2xl flex flex-col items-center justify-center text-gray-500 gap-4"> <div className="py-24 bg-slate-50 dark:bg-white/5 border-2 border-dashed border-slate-200 dark:border-white/10 rounded-[2.5rem] flex flex-col items-center justify-center text-slate-400 dark:text-gray-500 gap-4 shadow-inner">
<GraduationCap className="w-12 h-12 opacity-20" /> <div className="w-20 h-20 rounded-3xl bg-white dark:bg-white/5 flex items-center justify-center shadow-sm">
<p>No instructors assigned yet.</p> <GraduationCap className="w-10 h-10 opacity-40" />
</div>
<div className="text-center">
<p className="font-black uppercase tracking-widest text-xs">No instructors assigned yet</p>
<p className="text-[10px] mt-1 font-medium italic">Start by adding your first team member</p>
</div>
</div> </div>
) : ( ) : (
instructors.map((inst) => ( instructors.map((inst) => (
<div key={inst.id} className="glass p-6 rounded-2xl border-white/5 flex items-center justify-between group"> <div key={inst.id} className="bg-white dark:bg-white/5 border border-slate-200 dark:border-white/10 p-6 rounded-[2rem] flex items-center justify-between group hover:bg-slate-50 dark:hover:bg-white/[0.08] transition-all shadow-sm">
<div className="flex items-center gap-6"> <div className="flex items-center gap-6">
<div className="w-12 h-12 rounded-full bg-gradient-to-br from-blue-500 to-indigo-600 flex items-center justify-center text-xl font-bold border-2 border-white/10 shadow-xl"> <div className="w-14 h-14 rounded-2xl bg-gradient-to-br from-blue-500 to-indigo-600 flex items-center justify-center text-2xl font-black text-white border-4 border-white dark:border-white/10 shadow-xl shadow-blue-500/20">
{inst.full_name?.charAt(0) || inst.email?.charAt(0)} {inst.full_name?.charAt(0) || inst.email?.charAt(0)}
</div> </div>
<div className="space-y-1"> <div className="space-y-1">
<h3 className="text-lg font-bold flex items-center gap-2"> <h3 className="text-lg font-black text-slate-900 dark:text-white flex items-center gap-3 uppercase tracking-tight">
{inst.full_name} {inst.full_name}
{inst.role === 'primary' && ( {inst.role === 'primary' && (
<span className="text-[10px] font-black uppercase px-2 py-0.5 bg-orange-500/10 text-orange-400 border border-orange-500/20 rounded-full"> <span className="text-[9px] font-black uppercase px-2.5 py-1 bg-blue-600 text-white border border-blue-500 shadow-sm shadow-blue-500/20 rounded-full">
Owner Owner
</span> </span>
)} )}
</h3> </h3>
<div className="flex items-center gap-3 text-sm text-slate-600 dark:text-gray-400 font-medium"> <div className="flex items-center gap-3 text-xs text-slate-500 dark:text-gray-400 font-bold uppercase tracking-wider">
<span className="flex items-center gap-1.5"> <span className="flex items-center gap-1.5 lowercase font-medium text-slate-400 normal-case">
<Mail className="w-3.5 h-3.5 opacity-60" /> {inst.email} <Mail className="w-3.5 h-3.5 text-blue-400 shrink-0" /> {inst.email}
</span> </span>
<span className="w-1 h-1 rounded-full bg-gray-700" /> <span className="w-1 h-1 rounded-full bg-slate-300 dark:bg-gray-700" />
<span className="flex items-center gap-1.5"> <span className="flex items-center gap-1.5">
{getRoleIcon(inst.role)} {getRoleLabel(inst.role)} {getRoleIcon(inst.role)} {getRoleLabel(inst.role)}
</span> </span>
@@ -171,31 +176,31 @@ export default function CourseTeamPage() {
{/* Add Member Modal */} {/* Add Member Modal */}
{ {
isAddModalOpen && ( isAddModalOpen && (
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/60 dark:bg-black/80 backdrop-blur-sm animate-in fade-in duration-300"> <div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-slate-900/40 dark:bg-black/80 backdrop-blur-md animate-in fade-in duration-300">
<div className="bg-white dark:bg-[#1a1c22] border border-slate-200 dark:border-white/10 rounded-3xl w-full max-w-lg overflow-hidden shadow-2xl animate-in zoom-in-95 duration-300"> <div className="bg-white dark:bg-[#1a1c22] border border-slate-200 dark:border-white/10 rounded-[2.5rem] w-full max-w-lg overflow-hidden shadow-2xl animate-in zoom-in-95 duration-300">
<div className="p-8 border-b border-white/5 flex justify-between items-center"> <div className="p-8 border-b border-slate-100 dark:border-white/5 flex justify-between items-center bg-slate-50/50 dark:bg-transparent">
<div> <div>
<h2 className="text-2xl font-bold text-slate-900 dark:text-white">Add Team Member</h2> <h2 className="text-2xl font-black text-slate-900 dark:text-white uppercase tracking-tight">Add Team Member</h2>
<p className="text-sm text-slate-500 dark:text-gray-400 mt-1">Search for a user by name or email</p> <p className="text-xs text-slate-400 dark:text-gray-500 mt-1 font-bold uppercase tracking-widest">Search for a user in the directory</p>
</div> </div>
<button onClick={() => setIsAddModalOpen(false)} className="p-2 hover:bg-white/5 rounded-full transition-colors text-gray-500"> <button onClick={() => setIsAddModalOpen(false)} className="p-3 hover:bg-slate-200 dark:hover:bg-white/10 rounded-2xl transition-all text-slate-400 dark:text-gray-500 active:scale-90 group">
<X className="w-6 h-6" /> <X className="w-6 h-6 group-hover:text-slate-900 dark:group-hover:text-white transition-colors" />
</button> </button>
</div> </div>
<div className="p-8 space-y-8"> <div className="p-8 space-y-8">
<form onSubmit={handleSearch} className="relative"> <form onSubmit={handleSearch} className="relative">
<Search className="absolute left-4 top-1/2 -translate-y-1/2 w-5 h-5 text-gray-500" /> <Search className="absolute left-4 top-1/2 -translate-y-1/2 w-5 h-5 text-slate-400 dark:text-gray-500" />
<input <input
autoFocus autoFocus
placeholder="Type name or email..." placeholder="Enter name or direct email..."
value={searchQuery} value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)} onChange={(e) => setSearchQuery(e.target.value)}
className="w-full bg-slate-50 dark:bg-white/5 border border-slate-200 dark:border-white/10 rounded-2xl pl-12 pr-4 py-4 text-sm focus:outline-none focus:border-blue-500 transition-all font-medium text-slate-900 dark:text-white placeholder-slate-400 dark:placeholder-gray-500" className="w-full bg-slate-100 dark:bg-white/5 border border-slate-200 dark:border-white/10 rounded-2xl pl-12 pr-4 py-4 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500/50 transition-all font-bold text-slate-900 dark:text-white placeholder-slate-400 dark:placeholder-gray-500 shadow-inner"
/> />
{searching && ( {searching && (
<div className="absolute right-4 top-1/2 -translate-y-1/2"> <div className="absolute right-4 top-1/2 -translate-y-1/2">
<div className="w-5 h-5 border-2 border-white/20 border-t-blue-500 rounded-full animate-spin" /> <div className="w-5 h-5 border-2 border-slate-200 dark:border-white/20 border-t-blue-500 rounded-full animate-spin" />
</div> </div>
)} )}
</form> </form>
@@ -228,31 +233,36 @@ export default function CourseTeamPage() {
</label> </label>
</div> </div>
<div className="space-y-3 max-h-60 overflow-y-auto pr-2 custom-scrollbar"> <div className="space-y-4 max-h-72 overflow-y-auto pr-2 custom-scrollbar">
{users.length > 0 ? ( {users.length > 0 ? (
users.map(u => ( users.map(u => (
<button <button
key={u.id} key={u.id}
onClick={() => handleAddMember(u)} onClick={() => handleAddMember(u)}
className="w-full flex items-center justify-between p-4 rounded-2xl bg-slate-50 dark:bg-white/5 border border-slate-100 dark:border-white/10 hover:border-blue-500/30 hover:bg-slate-100 dark:hover:bg-white/10 transition-all group" className="w-full flex items-center justify-between p-5 rounded-3xl bg-slate-50 dark:bg-white/5 border border-slate-100 dark:border-white/10 hover:border-blue-500/30 hover:bg-white dark:hover:bg-white/10 transition-all group shadow-sm active:scale-[0.98]"
> >
<div className="flex items-center gap-4 text-left"> <div className="flex items-center gap-4 text-left">
<div className="w-10 h-10 rounded-full bg-white/10 flex items-center justify-center font-bold text-sm"> <div className="w-12 h-12 rounded-2xl bg-white dark:bg-black/20 border border-slate-100 dark:border-white/10 flex items-center justify-center font-black text-blue-500 text-lg shadow-sm">
{u.full_name.charAt(0)} {u.full_name.charAt(0)}
</div> </div>
<div> <div>
<div className="font-bold text-sm text-slate-900 dark:text-white">{u.full_name}</div> <div className="font-black text-slate-900 dark:text-white text-sm tracking-tight group-hover:text-blue-600 dark:group-hover:text-blue-400 transition-colors uppercase">{u.full_name}</div>
<div className="text-xs text-slate-500 dark:text-gray-500">{u.email}</div> <div className="text-xs text-slate-400 dark:text-gray-500 font-medium italic flex items-center gap-1.5 mt-0.5"><Mail size={12} className="text-blue-400" /> {u.email}</div>
</div> </div>
</div> </div>
<div className="w-8 h-8 rounded-lg bg-blue-500/0 flex items-center justify-center group-hover:bg-blue-500/20 transition-all"> <div className="w-10 h-10 rounded-xl bg-blue-500/0 flex items-center justify-center group-hover:bg-blue-600 group-hover:text-white transition-all shadow-lg shadow-blue-500/0 group-hover:shadow-blue-500/20">
<Plus className="w-4 h-4 text-blue-400 opacity-0 group-hover:opacity-100" /> <Plus className="w-5 h-5 text-blue-600 group-hover:text-white" />
</div> </div>
</button> </button>
)) ))
) : searchQuery && !searching ? ( ) : searchQuery && !searching ? (
<div className="py-8 text-center text-gray-600 italic text-sm">No users found matching your search.</div> <div className="py-12 text-center text-slate-400 dark:text-gray-600 italic text-sm font-medium border-2 border-dashed border-slate-100 dark:border-white/5 rounded-[2rem]">No users found matching your search.</div>
) : null} ) : (
<div className="py-12 bg-blue-50/30 dark:bg-blue-500/5 rounded-3xl border border-blue-100/50 dark:border-blue-500/10 flex flex-col items-center justify-center text-blue-600/50 dark:text-blue-400/30 gap-3">
<Search className="w-8 h-8" />
<p className="text-[10px] font-black uppercase tracking-[0.2em]">Enter a name to search</p>
</div>
)}
</div> </div>
</div> </div>
</div> </div>
@@ -199,68 +199,71 @@ export default function RubricEditor({ rubricId, courseId, onClose, onSaved }: R
); );
return ( return (
<div className="bg-[#1a1d23] rounded-3xl border border-white/10 overflow-hidden shadow-2xl flex flex-col max-h-[90vh]"> <div className="bg-white dark:bg-[#1a1d23] rounded-[2.5rem] border border-slate-200 dark:border-white/10 overflow-hidden shadow-2xl flex flex-col max-h-[90vh]">
{/* Header */} {/* Header */}
<div className="p-6 border-b border-white/10 flex items-center justify-between bg-white/[0.02]"> <div className="p-8 border-b border-slate-100 dark:border-white/10 flex items-center justify-between bg-slate-50/50 dark:bg-white/[0.02]">
<div className="flex-1"> <div className="flex-1">
<input <input
type="text" type="text"
value={rubric.name} value={rubric.name}
onChange={(e) => handleUpdateRubric(e.target.value, rubric.description || "")} onChange={(e) => handleUpdateRubric(e.target.value, rubric.description || "")}
placeholder="Rubric Name" placeholder="Rubric Name"
className="bg-transparent text-2xl font-bold text-white focus:outline-none focus:ring-1 focus:ring-blue-500/50 rounded px-2 w-full" className="bg-transparent text-3xl font-black text-slate-900 dark:text-white focus:outline-none focus:ring-1 focus:ring-blue-500/30 rounded px-2 w-full uppercase tracking-tighter"
/> />
<input <input
type="text" type="text"
value={rubric.description || ""} value={rubric.description || ""}
onChange={(e) => handleUpdateRubric(rubric.name, e.target.value)} onChange={(e) => handleUpdateRubric(rubric.name, e.target.value)}
placeholder="Add a description..." placeholder="Add a description..."
className="bg-transparent text-sm text-gray-400 focus:outline-none focus:ring-1 focus:ring-blue-500/50 rounded px-2 mt-1 w-full" className="bg-transparent text-sm font-bold text-slate-400 dark:text-gray-500 focus:outline-none focus:ring-1 focus:ring-blue-500/30 rounded px-2 mt-2 w-full italic"
/> />
</div> </div>
<div className="flex items-center gap-4 ml-4"> <div className="flex items-center gap-6 ml-4">
<div className="text-right mr-4"> <div className="text-right">
<span className="text-xs text-gray-500 uppercase tracking-widest block font-semibold">Total Points</span> <span className="text-[10px] text-slate-400 dark:text-gray-500 uppercase tracking-[0.2em] block font-black mb-1">Total Points</span>
<span className="text-2xl font-bold text-blue-400">{rubric.total_points}</span> <span className="text-3xl font-black text-blue-600 dark:text-blue-400 tracking-tighter">{rubric.total_points}</span>
</div> </div>
<button <button
onClick={onClose} onClick={onClose}
className="p-2 hover:bg-white/10 rounded-full text-gray-400 hover:text-white transition-colors" className="w-12 h-12 flex items-center justify-center hover:bg-red-50 dark:hover:bg-white/10 rounded-full text-slate-400 hover:text-red-500 transition-all active:scale-95 border border-transparent hover:border-red-100"
> >
<X className="w-6 h-6" /> <X className="w-8 h-8" />
</button> </button>
</div> </div>
</div> </div>
{/* Content */} {/* Content */}
<div className="flex-1 overflow-y-auto p-8 space-y-12"> <div className="flex-1 overflow-y-auto p-8 space-y-12">
<div className="flex items-center justify-between mb-2"> <div className="flex items-center justify-between mb-8">
<h3 className="text-lg font-semibold flex items-center gap-2 text-gray-200"> <h3 className="text-xl font-black flex items-center gap-3 text-slate-900 dark:text-gray-200 uppercase tracking-tight">
Criteria <div className="w-10 h-10 rounded-xl bg-blue-50 dark:bg-blue-500/10 border border-blue-100 dark:border-blue-500/20 flex items-center justify-center text-blue-600 dark:text-blue-400 shadow-sm">
<span className="text-xs font-normal text-gray-500 bg-white/5 px-2 py-0.5 rounded-full"> <Plus size={20} />
</div>
Evaluation Criteria
<span className="text-[10px] font-black text-slate-400 dark:text-gray-500 bg-slate-100 dark:bg-white/5 border border-slate-200 dark:border-white/10 px-3 py-1 rounded-full uppercase tracking-widest ml-2">
{rubric.criteria.length} items {rubric.criteria.length} items
</span> </span>
</h3> </h3>
<button <button
onClick={handleAddCriterion} onClick={handleAddCriterion}
className="flex items-center gap-2 px-4 py-2 bg-blue-600/10 text-blue-400 border border-blue-500/30 rounded-xl hover:bg-blue-600 hover:text-white transition-all transform active:scale-95" className="flex items-center gap-2 px-6 py-3 bg-blue-600 hover:bg-blue-500 text-white rounded-2xl font-black text-[10px] uppercase tracking-[0.2em] transition-all shadow-xl shadow-blue-500/20 active:scale-95"
> >
<Plus className="w-4 h-4" /> <Plus className="w-4 h-4" />
Add Criterion ADD NEW CRITERION
</button> </button>
</div> </div>
<div className="space-y-6"> <div className="space-y-8 pb-10">
{rubric.criteria.length === 0 ? ( {rubric.criteria.length === 0 ? (
<div className="text-center py-12 border-2 border-dashed border-white/5 rounded-3xl bg-white/[0.01]"> <div className="text-center py-20 border-2 border-dashed border-slate-200 dark:border-white/5 rounded-[3rem] bg-slate-50/50 dark:bg-white/[0.01]">
<p className="text-gray-500 italic">No criteria added yet. Add your first evaluation criterion.</p> <p className="text-slate-400 dark:text-gray-500 italic font-medium">Define your first grading criterion to start building the rubric.</p>
</div> </div>
) : ( ) : (
rubric.criteria.map((item, idx) => ( rubric.criteria.map((item, idx) => (
<div key={item.id} className="group bg-white/[0.03] border border-white/10 rounded-3xl p-6 hover:border-blue-500/30 transition-all"> <div key={item.id} className="group bg-slate-50/30 dark:bg-white/[0.03] border border-slate-200 dark:border-white/10 rounded-[2.5rem] p-10 hover:border-blue-500/30 transition-all shadow-sm hover:shadow-md">
<div className="flex items-start gap-4"> <div className="flex items-start gap-6">
<div className="mt-2 text-gray-600 group-hover:text-blue-500/50 transition-colors"> <div className="mt-2 text-slate-300 dark:text-gray-600 group-hover:text-blue-500 transition-colors">
<GripVertical className="w-5 h-5 cursor-grab active:cursor-grabbing" /> <GripVertical className="w-6 h-6 cursor-grab active:cursor-grabbing" />
</div> </div>
<div className="flex-1 space-y-4"> <div className="flex-1 space-y-4">
<div className="flex items-center justify-between gap-4"> <div className="flex items-center justify-between gap-4">
@@ -269,29 +272,29 @@ export default function RubricEditor({ rubricId, courseId, onClose, onSaved }: R
type="text" type="text"
value={item.name} value={item.name}
onChange={(e) => handleUpdateCriterion(item.id, { name: e.target.value })} onChange={(e) => handleUpdateCriterion(item.id, { name: e.target.value })}
className="bg-transparent text-lg font-bold text-gray-100 focus:outline-none focus:border-b border-blue-500/50 w-full" className="bg-transparent text-xl font-black text-slate-900 dark:text-gray-100 focus:outline-none focus:ring-2 focus:ring-blue-500/20 rounded-lg px-2 w-full uppercase tracking-tight"
/> />
<input <input
type="text" type="text"
value={item.description || ""} value={item.description || ""}
onChange={(e) => handleUpdateCriterion(item.id, { description: e.target.value })} onChange={(e) => handleUpdateCriterion(item.id, { description: e.target.value })}
placeholder="Description of what to evaluate..." placeholder="Focus area (e.g. Critical Thinking, Technical Accuracy...)"
className="bg-transparent text-sm text-gray-500 focus:outline-none w-full mt-1" className="bg-transparent text-sm font-medium text-slate-500 dark:text-gray-500 focus:outline-none w-full mt-2 italic px-2"
/> />
</div> </div>
<div className="flex items-center gap-4"> <div className="flex items-center gap-6">
<div className="flex flex-col items-end"> <div className="flex flex-col items-end">
<label className="text-[10px] text-gray-500 uppercase tracking-widest font-bold mb-1">Max Points</label> <label className="text-[9px] text-slate-400 dark:text-gray-500 uppercase tracking-widest font-black mb-1 mr-1">Points</label>
<input <input
type="number" type="number"
value={item.max_points} value={item.max_points}
onChange={(e) => handleUpdateCriterion(item.id, { max_points: parseInt(e.target.value) || 0 })} onChange={(e) => handleUpdateCriterion(item.id, { max_points: parseInt(e.target.value) || 0 })}
className="bg-white/5 border border-white/10 rounded-lg px-2 py-1 w-16 text-center text-blue-400 font-bold focus:outline-none focus:border-blue-500" className="bg-white dark:bg-white/5 border border-slate-200 dark:border-white/10 rounded-xl px-3 py-2 w-20 text-center text-blue-600 dark:text-blue-400 font-black text-lg focus:outline-none focus:ring-2 focus:ring-blue-500/30 transition-all shadow-sm"
/> />
</div> </div>
<button <button
onClick={() => handleDeleteCriterion(item.id)} onClick={() => handleDeleteCriterion(item.id)}
className="p-2 text-red-500/50 hover:text-red-500 hover:bg-red-500/10 rounded-lg transition-all" className="p-3 bg-red-50 dark:bg-red-500/10 text-red-500 hover:bg-red-500 hover:text-white rounded-xl transition-all shadow-sm active:scale-95"
> >
<Trash2 className="w-5 h-5" /> <Trash2 className="w-5 h-5" />
</button> </button>
@@ -299,45 +302,45 @@ export default function RubricEditor({ rubricId, courseId, onClose, onSaved }: R
</div> </div>
{/* Levels */} {/* Levels */}
<div className="pl-4 border-l-2 border-white/5 space-y-4 pt-2"> <div className="pl-8 border-l-4 border-slate-100 dark:border-white/5 space-y-6 pt-4 mt-4">
<div className="flex items-center justify-between mb-2"> <div className="flex items-center justify-between mb-4">
<h4 className="text-xs font-bold text-gray-500 uppercase tracking-widest">Performance Levels</h4> <h4 className="text-[10px] font-black text-slate-400 dark:text-gray-500 uppercase tracking-[0.2em]">Achievement Levels</h4>
<button <button
onClick={() => handleAddLevel(item.id)} onClick={() => handleAddLevel(item.id)}
className="text-xs text-blue-400 hover:text-blue-300 flex items-center gap-1 font-semibold" className="text-[10px] font-black uppercase tracking-widest text-blue-600 hover:text-blue-500 flex items-center gap-2 px-3 py-1 bg-blue-50 dark:bg-blue-500/10 rounded-lg border border-blue-100 dark:border-blue-500/20 transition-all"
> >
<Plus className="w-3 h-3" /> Add Level <Plus size={12} /> ADD LEVEL
</button> </button>
</div> </div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4"> <div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-4 gap-6">
{item.levels.map(level => ( {item.levels.map(level => (
<div key={level.id} className="bg-white/5 border border-white/10 rounded-2xl p-4 flex flex-col gap-2 relative group/level hover:bg-white/[0.08] transition-all"> <div key={level.id} className="bg-white dark:bg-white/5 border border-slate-200 dark:border-white/10 rounded-[1.5rem] p-6 flex flex-col gap-4 relative group/level hover:border-blue-500/30 transition-all shadow-sm">
<button <button
onClick={() => handleDeleteLevel(level.id, item.id)} onClick={() => handleDeleteLevel(level.id, item.id)}
className="absolute top-2 right-2 p-1 text-gray-600 hover:text-red-500 opacity-0 group-level-hover:opacity-100 transition-all" className="absolute top-4 right-4 p-2 bg-red-50 dark:bg-red-500/10 text-red-500 rounded-lg opacity-0 group-hover/level:opacity-100 transition-all active:scale-90"
> >
<Trash2 className="w-3 h-3" /> <Trash2 className="w-4 h-4" />
</button> </button>
<input <input
type="text" type="text"
value={level.name} value={level.name}
onChange={(e) => handleUpdateLevel(level.id, item.id, { name: e.target.value })} onChange={(e) => handleUpdateLevel(level.id, item.id, { name: e.target.value })}
placeholder="Level Name (e.g. Excellent)" placeholder="Level (e.g. Master)"
className="bg-transparent text-sm font-semibold text-gray-200 focus:outline-none" className="bg-transparent text-sm font-black text-slate-900 dark:text-gray-200 focus:outline-none uppercase tracking-tight"
/> />
<textarea <textarea
value={level.description || ""} value={level.description || ""}
onChange={(e) => handleUpdateLevel(level.id, item.id, { description: e.target.value })} onChange={(e) => handleUpdateLevel(level.id, item.id, { description: e.target.value })}
placeholder="Criteria description..." placeholder="Grade description..."
className="bg-transparent text-xs text-gray-500 focus:outline-none resize-none h-12" className="bg-transparent text-xs font-medium text-slate-500 dark:text-gray-500 focus:outline-none resize-none h-20 leading-relaxed italic"
/> />
<div className="flex items-center gap-2 mt-1"> <div className="flex items-center justify-between mt-2 pt-4 border-t border-slate-50 dark:border-white/5">
<span className="text-[10px] text-gray-600 uppercase font-bold">Points:</span> <span className="text-[9px] text-slate-400 dark:text-gray-600 uppercase font-black tracking-widest">Points Value</span>
<input <input
type="number" type="number"
value={level.points} value={level.points}
onChange={(e) => handleUpdateLevel(level.id, item.id, { points: parseInt(e.target.value) || 0 })} onChange={(e) => handleUpdateLevel(level.id, item.id, { points: parseInt(e.target.value) || 0 })}
className="bg-white/5 border border-white/10 rounded px-1.5 w-12 text-xs text-center text-blue-400 focus:outline-none" className="bg-slate-50 dark:bg-white/5 border border-slate-200 dark:border-white/10 rounded-lg px-2 py-1 w-14 text-xs font-black text-center text-blue-600 dark:text-blue-400 focus:outline-none focus:ring-2 focus:ring-blue-500/30 shadow-inner"
/> />
</div> </div>
</div> </div>
@@ -353,22 +356,22 @@ export default function RubricEditor({ rubricId, courseId, onClose, onSaved }: R
</div> </div>
{/* Footer */} {/* Footer */}
<div className="p-6 border-t border-white/10 bg-white/[0.02] flex justify-end gap-4"> <div className="p-8 border-t border-slate-100 dark:border-white/10 bg-slate-50 dark:bg-white/[0.02] flex justify-end gap-5">
<button <button
onClick={onClose} onClick={onClose}
className="px-6 py-2 rounded-xl text-gray-400 hover:text-white hover:bg-white/5 transition-all font-medium" className="px-8 py-3 rounded-2xl text-[10px] font-black uppercase tracking-[0.2em] text-slate-500 hover:text-slate-900 dark:text-gray-400 dark:hover:text-white hover:bg-slate-100 dark:hover:bg-white/5 transition-all"
> >
Close Cancel
</button> </button>
<button <button
onClick={() => { onClick={() => {
if (onSaved) onSaved(); if (onSaved) onSaved();
onClose(); onClose();
}} }}
className="px-8 py-2 bg-blue-600 text-white rounded-xl font-bold hover:bg-blue-500 shadow-lg shadow-blue-500/20 active:scale-95 transition-all flex items-center gap-2" className="px-10 py-3 bg-blue-600 text-white rounded-2xl font-black text-[10px] uppercase tracking-[0.3em] hover:bg-blue-500 shadow-xl shadow-blue-500/20 active:scale-95 transition-all flex items-center gap-3"
> >
<Check className="w-5 h-5" /> <Check className="w-5 h-5" />
Done Save & Finish
</button> </button>
</div> </div>
</div> </div>
@@ -98,7 +98,7 @@ export default function RubricList({ courseId, onEdit }: RubricListProps) {
placeholder="Search rubrics..." placeholder="Search rubrics..."
value={searchQuery} value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)} onChange={(e) => setSearchQuery(e.target.value)}
className="w-full bg-white/5 border border-white/10 rounded-2xl pl-11 pr-4 py-3 focus:outline-none focus:border-blue-500/50 transition-all text-gray-100" className="w-full bg-slate-50 dark:bg-white/5 border border-slate-200 dark:border-white/10 rounded-2xl pl-11 pr-4 py-4 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500/30 transition-all text-slate-900 dark:text-gray-100 placeholder:text-slate-300 shadow-inner font-medium"
/> />
</div> </div>
@@ -109,14 +109,14 @@ export default function RubricList({ courseId, onEdit }: RubricListProps) {
value={newRubricName} value={newRubricName}
onChange={(e) => setNewRubricName(e.target.value)} onChange={(e) => setNewRubricName(e.target.value)}
onKeyDown={(e) => e.key === 'Enter' && handleCreate()} onKeyDown={(e) => e.key === 'Enter' && handleCreate()}
className="flex-1 md:w-64 bg-white/5 border border-white/10 rounded-xl px-4 py-2 focus:outline-none focus:border-blue-500/50 transition-all" className="flex-1 md:w-64 bg-slate-50 dark:bg-white/5 border border-slate-200 dark:border-white/10 rounded-2xl px-5 py-3 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500/30 transition-all text-slate-900 dark:text-white placeholder:text-slate-300 shadow-inner font-medium"
/> />
<button <button
onClick={handleCreate} onClick={handleCreate}
disabled={creating || !newRubricName} disabled={creating || !newRubricName}
className="bg-blue-600 hover:bg-blue-500 disabled:bg-gray-700 disabled:text-gray-500 text-white font-bold px-6 py-2 rounded-xl transition-all shadow-lg shadow-blue-500/20 active:scale-95 flex items-center gap-2" className="bg-blue-600 hover:bg-blue-500 disabled:bg-slate-100 disabled:text-slate-300 text-white font-black text-xs uppercase tracking-[0.2em] px-8 py-3 rounded-2xl transition-all shadow-xl shadow-blue-500/20 active:scale-95 flex items-center gap-2"
> >
<Plus className="w-4 h-4" /> <Plus className="w-5 h-5" />
Create Create
</button> </button>
</div> </div>
@@ -124,26 +124,26 @@ export default function RubricList({ courseId, onEdit }: RubricListProps) {
{/* Rubrics Grid */} {/* Rubrics Grid */}
{filteredRubrics.length === 0 ? ( {filteredRubrics.length === 0 ? (
<div className="bg-white/5 border border-white/10 rounded-3xl p-16 text-center"> <div className="bg-slate-50 dark:bg-white/5 border border-dashed border-slate-200 dark:border-white/10 rounded-[3rem] p-24 text-center">
<div className="w-16 h-16 bg-blue-500/10 rounded-2xl flex items-center justify-center mx-auto mb-6"> <div className="w-20 h-20 bg-white dark:bg-white/5 border border-slate-100 dark:border-white/5 rounded-2xl flex items-center justify-center mx-auto mb-8 shadow-sm">
<FileText className="w-8 h-8 text-blue-400" /> <FileText className="w-10 h-10 text-slate-300 dark:text-blue-400" />
</div> </div>
<h3 className="text-xl font-bold text-gray-200 mb-2">No rubrics found</h3> <h3 className="text-2xl font-black text-slate-900 dark:text-white mb-3 uppercase tracking-tight">No rubrics created</h3>
<p className="text-gray-500 max-w-md mx-auto"> <p className="text-slate-500 dark:text-gray-500 max-w-sm mx-auto font-medium">
Create a rubric to start using advanced grading criteria in your course lessons. Standardize your evaluation process by creating detailed rubrics for your course assignments.
</p> </p>
</div> </div>
) : ( ) : (
<div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-6"> <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
{filteredRubrics.map((rubric) => ( {filteredRubrics.map((rubric) => (
<div <div
key={rubric.id} key={rubric.id}
className="group bg-white/5 border border-white/10 rounded-3xl p-6 hover:border-blue-500/50 hover:bg-white/[0.08] transition-all duration-300 flex flex-col justify-between" className="group bg-white dark:bg-black/20 border border-slate-200 dark:border-white/10 rounded-[2.5rem] p-10 hover:border-blue-500/30 hover:shadow-xl transition-all duration-300 flex flex-col justify-between shadow-sm"
> >
<div> <div>
<div className="flex items-start justify-between mb-4"> <div className="flex items-start justify-between mb-6">
<div className="w-12 h-12 rounded-xl bg-indigo-500/10 flex items-center justify-center text-indigo-400"> <div className="w-14 h-14 rounded-2xl bg-indigo-50 dark:bg-indigo-500/10 border border-indigo-100 dark:border-indigo-500/20 flex items-center justify-center text-indigo-600 dark:text-indigo-400 shadow-sm transition-transform group-hover:scale-110">
<FileText className="w-6 h-6" /> <FileText className="w-8 h-8" />
</div> </div>
<div className="flex items-center gap-1"> <div className="flex items-center gap-1">
<button <button
@@ -155,32 +155,32 @@ export default function RubricList({ courseId, onEdit }: RubricListProps) {
</button> </button>
<button <button
onClick={() => handleDelete(rubric.id)} onClick={() => handleDelete(rubric.id)}
className="p-2 text-gray-500 hover:text-red-400 hover:bg-red-400/10 rounded-lg transition-all" className="p-3 text-slate-400 hover:text-red-500 hover:bg-red-50 dark:hover:bg-red-500/10 rounded-xl transition-all shadow-sm active:scale-90"
title="Delete Rubric" title="Delete Rubric"
> >
<Trash2 className="w-4 h-4" /> <Trash2 className="w-5 h-5" />
</button> </button>
</div> </div>
</div> </div>
<h3 className="text-xl font-bold text-gray-100 group-hover:text-blue-400 transition-colors mb-2"> <h3 className="text-2xl font-black text-slate-900 dark:text-white group-hover:text-blue-600 dark:group-hover:text-blue-400 transition-colors mb-2 uppercase tracking-tight">
{rubric.name} {rubric.name}
</h3> </h3>
<p className="text-gray-500 text-sm line-clamp-2 min-h-[2.5rem]"> <p className="text-slate-500 dark:text-gray-500 text-sm line-clamp-2 min-h-[3rem] font-medium leading-relaxed">
{rubric.description || "No description provided."} {rubric.description || "Establish consistent grading standards with custom criteria."}
</p> </p>
</div> </div>
<div className="mt-8 flex items-center justify-between"> <div className="mt-12 flex items-center justify-between">
<div className="flex flex-col"> <div className="flex flex-col">
<span className="text-[10px] text-gray-500 uppercase tracking-widest font-bold mb-1">Total Points</span> <span className="text-[10px] text-slate-400 dark:text-gray-500 uppercase tracking-[0.2em] font-black mb-1">Total Points</span>
<span className="text-xl font-black text-white">{rubric.total_points}</span> <span className="text-3xl font-black text-slate-900 dark:text-white tracking-tighter">{rubric.total_points}</span>
</div> </div>
<button <button
onClick={() => onEdit(rubric.id)} onClick={() => onEdit(rubric.id)}
className="px-4 py-2 bg-white/5 border border-white/10 rounded-xl text-sm font-semibold hover:bg-blue-600 hover:text-white hover:border-blue-600 transition-all active:scale-95" className="px-6 py-3 bg-slate-50 dark:bg-white/5 border border-slate-200 dark:border-white/10 rounded-[1.25rem] text-[10px] font-black uppercase tracking-widest text-slate-600 dark:text-white hover:bg-blue-600 hover:text-white hover:border-blue-600 transition-all active:scale-95 shadow-sm"
> >
Manage Criteria EDIT CRITERIA
</button> </button>
</div> </div>
</div> </div>
@@ -25,18 +25,19 @@ export default function AudioResponseBlock({
<div className="space-y-8" id={id}> <div className="space-y-8" id={id}>
<div className="space-y-2"> <div className="space-y-2">
{editMode ? ( {editMode ? (
<div className="space-y-2 p-6 glass border-white/5 bg-white/5 mb-4"> <div className="space-y-4 p-8 bg-slate-50 dark:bg-white/5 border border-slate-200 dark:border-white/10 rounded-[2rem] mb-6 shadow-inner relative overflow-hidden">
<label className="text-xs font-bold text-gray-500 uppercase tracking-widest">Activity Title (Optional)</label> <div className="absolute top-0 left-0 w-1 h-full bg-purple-500/40"></div>
<label className="text-[10px] font-black text-slate-400 dark:text-gray-500 uppercase tracking-[0.2em]">Activity Title (Optional)</label>
<input <input
type="text" type="text"
value={title || ""} value={title || ""}
onChange={(e) => onChange({ title: e.target.value })} onChange={(e) => onChange({ title: e.target.value })}
placeholder="e.g. Speaking Practice, Pronunciation Test..." placeholder="e.g. Speaking Practice, Pronunciation Test..."
className="w-full bg-white/5 border border-white/10 rounded-lg px-4 py-2 text-sm font-bold focus:border-blue-500/50 focus:outline-none" className="w-full bg-white dark:bg-black/40 border border-slate-200 dark:border-white/10 rounded-2xl px-6 py-3 text-sm font-black uppercase tracking-tight focus:ring-4 focus:ring-purple-500/10 focus:border-purple-500 transition-all outline-none shadow-sm"
/> />
</div> </div>
) : ( ) : (
<h3 className="text-xl font-bold border-l-4 border-purple-500 pl-4 py-1 tracking-tight text-white"> <h3 className="text-2xl font-black italic tracking-tight text-slate-900 dark:text-white uppercase border-l-4 border-purple-600 pl-6 py-1">
{title || "Audio Response"} {title || "Audio Response"}
</h3> </h3>
)} )}
@@ -44,39 +45,40 @@ export default function AudioResponseBlock({
{editMode ? ( {editMode ? (
<div className="space-y-6"> <div className="space-y-6">
<div className="p-6 glass border-white/5 space-y-4"> <div className="p-8 bg-white dark:bg-white/5 border border-slate-100 dark:border-white/10 rounded-[2.5rem] space-y-4 shadow-sm relative overflow-hidden group/prompt">
<label className="text-xs font-bold text-gray-500 uppercase tracking-widest flex items-center gap-2"> <div className="absolute top-0 right-0 w-32 h-32 bg-purple-500/5 rounded-full blur-3xl -translate-y-1/2 translate-x-1/2"></div>
<Mic className="w-4 h-4" /> <label className="text-[10px] font-black text-slate-400 dark:text-gray-500 uppercase tracking-[0.2em] flex items-center gap-3">
Question Prompt <Mic className="w-4 h-4 text-purple-600" />
Phonetic Inquiry (Prompt)
</label> </label>
<textarea <textarea
value={prompt} value={prompt}
onChange={(e) => onChange({ prompt: e.target.value })} onChange={(e) => onChange({ prompt: e.target.value })}
className="w-full bg-white/5 border border-white/10 rounded-xl p-4 min-h-[100px] text-lg font-medium focus:outline-none focus:border-purple-500/50 transition-all" className="w-full bg-slate-50 dark:bg-black/40 border border-slate-100 dark:border-white/10 rounded-2xl p-6 min-h-[120px] text-xl font-black uppercase tracking-tight text-slate-800 dark:text-white focus:outline-none focus:ring-4 focus:ring-purple-500/10 transition-all shadow-inner placeholder:opacity-20"
placeholder="What question should the student answer? (e.g. 'Describe your daily routine in English')" placeholder="WHAT IS THE ENIGMA TO BE SPOKEN?..."
/> />
</div> </div>
<div className="p-6 glass border-white/5 space-y-4"> <div className="p-8 bg-white dark:bg-white/5 border border-slate-100 dark:border-white/10 rounded-[2.5rem] space-y-4 shadow-sm">
<label className="text-xs font-bold text-gray-500 uppercase tracking-widest flex items-center gap-2"> <label className="text-[10px] font-black text-slate-400 dark:text-gray-500 uppercase tracking-[0.2em] flex items-center gap-3">
<Tag className="w-4 h-4" /> <Tag className="w-4 h-4 text-purple-600" />
Expected Keywords (Optional) Lexical Anchors (Expected Keywords)
</label> </label>
<textarea <textarea
value={keywords.join("\n")} value={keywords.join("\n")}
onChange={(e) => onChange({ keywords: e.target.value.split("\n").filter(k => k.trim() !== "") })} onChange={(e) => onChange({ keywords: e.target.value.split("\n").filter(k => k.trim() !== "") })}
className="w-full bg-white/5 border border-white/10 rounded-xl p-4 min-h-[100px] text-sm font-medium focus:outline-none focus:border-purple-500/50 transition-all" className="w-full bg-slate-50 dark:bg-black/40 border border-slate-100 dark:border-white/10 rounded-2xl p-6 min-h-[100px] text-sm font-bold text-slate-700 dark:text-gray-300 focus:outline-none focus:ring-4 focus:ring-purple-500/10 transition-all shadow-inner"
placeholder="breakfast&#10;morning&#10;routine&#10;work" placeholder="Keyword Alpha&#10;Keyword Beta..."
/> />
<p className="text-[10px] text-gray-500 uppercase tracking-wider"> <p className="text-[9px] text-slate-400 dark:text-gray-600 uppercase font-black italic pl-1 italic">
One keyword per line. Used for automatic evaluation of speech content. Singular entry per line. Syntactic scan for automatic validation.
</p> </p>
</div> </div>
<div className="p-6 glass border-white/5 space-y-4"> <div className="p-8 bg-white dark:bg-white/5 border border-slate-100 dark:border-white/10 rounded-[2.5rem] space-y-4 shadow-sm">
<label className="text-xs font-bold text-gray-500 uppercase tracking-widest flex items-center gap-2"> <label className="text-[10px] font-black text-slate-400 dark:text-gray-500 uppercase tracking-[0.2em] flex items-center gap-3">
<Clock className="w-4 h-4" /> <Clock className="w-4 h-4 text-purple-600" />
Time Limit (Optional) Temporal Ceiling (Seconds)
</label> </label>
<input <input
type="number" type="number"
@@ -85,46 +87,51 @@ export default function AudioResponseBlock({
placeholder="60" placeholder="60"
min="10" min="10"
max="300" max="300"
className="w-full bg-white/5 border border-white/10 rounded-lg px-4 py-2 text-sm font-medium focus:border-purple-500/50 focus:outline-none" className="w-full bg-slate-50 dark:bg-black/40 border border-slate-100 dark:border-white/10 rounded-2xl px-6 py-4 text-sm font-black text-slate-800 dark:text-white focus:ring-4 focus:ring-purple-500/10 transition-all outline-none"
/> />
<p className="text-[10px] text-gray-500 uppercase tracking-wider"> <p className="text-[9px] text-slate-400 dark:text-gray-600 uppercase font-black italic pl-1 italic">
Maximum recording time in seconds (10-300). Leave empty for no limit. Maximum temporal window (10-300). Null for unconstrained stream.
</p> </p>
</div> </div>
</div> </div>
) : ( ) : (
<div className="p-8 glass border-white/5 rounded-3xl space-y-8"> <div className="p-10 bg-white dark:bg-white/5 border border-slate-100 dark:border-white/10 rounded-[3rem] space-y-10 shadow-sm relative overflow-hidden group/audioplay">
<div className="flex items-start gap-4"> <div className="absolute top-0 right-0 w-64 h-64 bg-purple-500/5 rounded-full blur-[80px] -translate-y-1/2 translate-x-1/2 group-hover/audioplay:bg-purple-500/10 transition-colors"></div>
<div className="p-3 bg-purple-500/20 rounded-xl">
<Mic className="w-6 h-6 text-purple-400" /> <div className="flex items-start gap-8 relative z-10">
<div className="p-5 bg-purple-100 dark:bg-purple-500/10 rounded-2xl shadow-inner border border-purple-200 dark:border-purple-500/20">
<Mic className="w-8 h-8 text-purple-700 dark:text-purple-400" />
</div> </div>
<div className="flex-1"> <div className="flex-1 space-y-4">
<p className="text-xl font-bold text-gray-100 mb-2">{prompt || "Record your audio response:"}</p> <p className="text-2xl font-black text-slate-800 dark:text-gray-100 tracking-tight uppercase italic leading-tight">{prompt || "Awaiting phonetic capture..."}</p>
{keywords.length > 0 && (
<div className="flex flex-wrap gap-2 mt-3"> <div className="flex flex-wrap items-center gap-6">
<span className="text-xs text-gray-500 uppercase tracking-wider">Expected topics:</span> {keywords.length > 0 && (
{keywords.slice(0, 5).map((kw, i) => ( <div className="flex flex-wrap gap-2">
<span key={i} className="px-2 py-1 bg-purple-500/10 border border-purple-500/20 rounded text-xs text-purple-300"> {keywords.slice(0, 5).map((kw, i) => (
{kw} <span key={i} className="px-3 py-1 bg-slate-50 dark:bg-purple-500/5 border border-slate-100 dark:border-purple-500/20 rounded-lg text-[9px] font-black uppercase text-purple-600 dark:text-purple-400 tracking-widest shadow-sm">
</span> {kw}
))} </span>
</div> ))}
)} </div>
{timeLimit && ( )}
<p className="text-xs text-gray-500 mt-2 flex items-center gap-1"> {timeLimit && (
<Clock className="w-3 h-3" /> <p className="text-[10px] font-black uppercase tracking-[0.2em] text-slate-400 dark:text-gray-500 flex items-center gap-2">
Maximum {timeLimit} seconds <Clock className="w-3 h-3 text-purple-500" />
</p> Temporal Window: {timeLimit}s
)} </p>
)}
</div>
</div> </div>
</div> </div>
<div className="p-6 bg-purple-500/5 border-2 border-dashed border-purple-500/20 rounded-2xl text-center"> <div className="p-10 bg-purple-50/50 dark:bg-purple-500/5 border-2 border-dashed border-purple-200 dark:border-purple-500/20 rounded-[2.5rem] text-center space-y-3 relative z-10 group-hover/audioplay:border-purple-400 transition-colors duration-500">
<p className="text-sm text-gray-400"> <div className="w-16 h-1 w-1 bg-purple-200 dark:bg-purple-500/20 mx-auto rounded-full mb-4"></div>
🎤 Audio recording will be available in the Experience player <p className="text-[10px] font-black text-purple-900 dark:text-purple-400 uppercase tracking-[0.3em]">
Phonetic Engine Interface
</p> </p>
<p className="text-xs text-gray-600 mt-2"> <p className="text-[9px] text-slate-400 dark:text-gray-600 uppercase font-black italic">
Students will use their microphone to record their response Capture node will manifest within the Experience stream
</p> </p>
</div> </div>
</div> </div>
@@ -79,139 +79,146 @@ export default function DescriptionBlock({ id, title, content, editMode, courseI
{/* Block Header */} {/* Block Header */}
<div className="space-y-2"> <div className="space-y-2">
{editMode ? ( {editMode ? (
<div className="space-y-2"> <div className="space-y-3">
<label className="text-xs font-bold text-gray-500 uppercase tracking-widest">Activity Title (Optional)</label> <label className="text-[10px] font-black text-slate-400 dark:text-gray-500 uppercase tracking-[0.2em]">Activity Title (Optional)</label>
<input <input
type="text" type="text"
value={title || ""} value={title || ""}
onChange={(e) => onChange({ title: e.target.value })} onChange={(e) => onChange({ title: e.target.value })}
placeholder="e.g. Introduction, Context..." placeholder="e.g. Fundamental Concepts, Learning Objectives..."
className="w-full bg-white/5 border border-white/10 rounded-lg px-4 py-2 text-sm font-bold focus:border-blue-500/50 focus:outline-none" className="w-full bg-slate-50 dark:bg-white/5 border border-slate-200 dark:border-white/10 rounded-2xl px-5 py-3 text-sm font-black uppercase tracking-tight focus:ring-4 focus:ring-blue-500/10 focus:border-blue-500 transition-all outline-none"
/> />
</div> </div>
) : ( ) : (
title && <h3 className="text-xl font-bold border-l-4 border-blue-500 pl-4 py-1 tracking-tight text-white">{title}</h3> title && <h3 className="text-2xl font-black italic tracking-tight text-slate-900 dark:text-white uppercase border-l-4 border-blue-600 pl-6 py-1">{title}</h3>
)} )}
</div> </div>
{editMode ? ( {editMode ? (
<div className="space-y-4"> <div className="space-y-4">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div className="flex items-center gap-2"> <div className="flex items-center justify-between">
<label className="text-xs font-bold text-gray-500 uppercase tracking-widest">Instructional Content</label> <div className="flex items-center gap-3">
<div className="h-4 w-px bg-white/10 mx-2" /> <label className="text-[10px] font-black text-slate-400 dark:text-gray-500 uppercase tracking-[0.2em]">Instructional Canvas</label>
{/* Toolbar */} <div className="h-4 w-px bg-slate-200 dark:bg-white/10 mx-2" />
{!showPreview && ( {/* Toolbar */}
<div className="flex items-center gap-1"> {!showPreview && (
<button onClick={() => insertMarkdown("**", "**")} className="p-1.5 hover:bg-white/10 rounded-md transition-colors text-gray-400 hover:text-white" title="Bold"><Bold size={14} /></button> <div className="flex items-center gap-1.5">
<button onClick={() => insertMarkdown("*", "*")} className="p-1.5 hover:bg-white/10 rounded-md transition-colors text-gray-400 hover:text-white" title="Italic"><Italic size={14} /></button> <button onClick={() => insertMarkdown("**", "**")} className="p-2 hover:bg-slate-100 dark:hover:bg-white/10 rounded-xl transition-all text-slate-500 dark:text-gray-400 hover:text-slate-900 dark:hover:text-white shadow-sm border border-transparent hover:border-slate-200" title="Bold"><Bold size={16} /></button>
<button onClick={() => insertMarkdown("[", "](url)")} className="p-1.5 hover:bg-white/10 rounded-md transition-colors text-gray-400 hover:text-white" title="Link"><LinkIcon size={14} /></button> <button onClick={() => insertMarkdown("*", "*")} className="p-2 hover:bg-slate-100 dark:hover:bg-white/10 rounded-xl transition-all text-slate-500 dark:text-gray-400 hover:text-slate-900 dark:hover:text-white shadow-sm border border-transparent hover:border-slate-200" title="Italic"><Italic size={16} /></button>
<div className="w-px h-3 bg-white/10 mx-1" /> <button onClick={() => insertMarkdown("[", "](url)")} className="p-2 hover:bg-slate-100 dark:hover:bg-white/10 rounded-xl transition-all text-slate-500 dark:text-gray-400 hover:text-slate-900 dark:hover:text-white shadow-sm border border-transparent hover:border-slate-200" title="Link"><LinkIcon size={16} /></button>
<button <div className="w-px h-4 bg-slate-200 dark:bg-white/10 mx-2" />
onClick={() => { setPickerType("image"); setIsAssetPickerOpen(true); }} <button
className="p-1.5 hover:bg-white/10 rounded-md transition-colors text-blue-400 hover:text-blue-300" onClick={() => { setPickerType("image"); setIsAssetPickerOpen(true); }}
title="Insert Image" className="p-2 hover:bg-blue-50 dark:hover:bg-blue-500/10 rounded-xl transition-all text-blue-600 dark:text-blue-400 hover:text-blue-700 shadow-sm border border-transparent hover:border-blue-200"
> title="Insert Image"
<ImageIcon size={14} /> >
</button> <ImageIcon size={16} />
<button </button>
onClick={() => { setPickerType("file"); setIsAssetPickerOpen(true); }} <button
className="p-1.5 hover:bg-white/10 rounded-md transition-colors text-purple-400 hover:text-purple-300" onClick={() => { setPickerType("file"); setIsAssetPickerOpen(true); }}
title="Insert File Link" className="p-2 hover:bg-purple-50 dark:hover:bg-purple-500/10 rounded-xl transition-all text-purple-600 dark:text-purple-400 hover:text-purple-700 shadow-sm border border-transparent hover:border-purple-200"
> title="Insert File Link"
<FileText size={14} /> >
</button> <FileText size={16} />
<div className="w-px h-3 bg-white/10 mx-1" /> </button>
<button <div className="w-px h-4 bg-slate-200 dark:bg-white/10 mx-2" />
onClick={handleReviewText} <button
disabled={isReviewing} onClick={handleReviewText}
className={`p-1.5 rounded-md transition-all flex items-center gap-1.5 text-xs font-bold ${isReviewing ? 'bg-indigo-500/20 text-indigo-300 animate-pulse' : 'hover:bg-indigo-500/20 text-indigo-400 hover:text-indigo-300'}`} disabled={isReviewing}
title="AI Suggest Improvements" className={`px-4 py-2 rounded-xl transition-all flex items-center gap-2 text-[10px] font-black uppercase tracking-widest border ${isReviewing ? 'bg-indigo-50 dark:bg-indigo-500/20 text-indigo-400 border-indigo-200 animate-pulse shadow-inner' : 'bg-indigo-50 dark:bg-indigo-500/5 text-indigo-600 dark:text-indigo-400 hover:bg-indigo-600 hover:text-white border-indigo-100 hover:border-indigo-600 shadow-sm active:scale-95'}`}
> title="AI Suggest Improvements"
<Sparkles size={14} className={isReviewing ? 'animate-spin' : ''} /> >
{isReviewing ? 'Analyzing...' : 'AI Suggest'} <Sparkles size={14} className={isReviewing ? 'animate-spin' : ''} />
</button> {isReviewing ? 'Synthesizing...' : 'AI Refine'}
</div> </button>
)} </div>
</div> )}
<div className="flex bg-white/5 rounded-lg p-1 border border-white/5"> </div>
<button <div className="flex bg-slate-100 dark:bg-white/5 rounded-2xl p-1.5 border border-slate-200 dark:border-white/5 shadow-inner">
onClick={() => setShowPreview(false)} <button
className={`px-3 py-1 flex items-center gap-2 text-[10px] uppercase font-black tracking-widest rounded-md transition-all ${!showPreview ? "bg-blue-500 text-white shadow-lg" : "text-gray-500 hover:text-white"}`} onClick={() => setShowPreview(false)}
> className={`px-5 py-2 flex items-center gap-2 text-[10px] uppercase font-black tracking-widest rounded-xl transition-all ${!showPreview ? "bg-white dark:bg-indigo-600 text-slate-900 dark:text-white shadow-md" : "text-slate-400 hover:text-slate-600"}`}
<PenLine size={12} /> Write >
</button> <PenLine size={12} /> Composition
<button </button>
onClick={() => setShowPreview(true)} <button
className={`px-3 py-1 flex items-center gap-2 text-[10px] uppercase font-black tracking-widest rounded-md transition-all ${showPreview ? "bg-blue-500 text-white shadow-lg" : "text-gray-500 hover:text-white"}`} onClick={() => setShowPreview(true)}
> className={`px-5 py-2 flex items-center gap-2 text-[10px] uppercase font-black tracking-widest rounded-xl transition-all ${showPreview ? "bg-white dark:bg-indigo-600 text-slate-900 dark:text-white shadow-md" : "text-slate-400 hover:text-slate-600"}`}
<Eye size={12} /> Preview >
</button> <Eye size={12} /> Rendering
</button>
</div>
</div> </div>
</div> </div>
{showPreview ? ( {showPreview ? (
<div className="min-h-[200px] p-8 rounded-2xl glass border-white/5 bg-white/5"> <div className="min-h-[250px] p-10 rounded-[2.5rem] bg-white dark:bg-white/5 border border-slate-200 dark:border-white/10 shadow-sm overflow-hidden relative">
<div className="prose prose-invert max-w-none prose-p:text-gray-300 prose-p:leading-relaxed prose-p:text-lg prose-headings:text-white prose-a:text-blue-400 prose-img:rounded-xl"> <div className="absolute top-0 right-0 w-32 h-32 bg-blue-500/5 rounded-full blur-3xl -translate-y-1/2 translate-x-1/2"></div>
<ReactMarkdown urlTransform={getImageUrl}>{content || "Nothing to preview..."}</ReactMarkdown> <div className="prose dark:prose-invert max-w-none prose-p:text-slate-600 dark:prose-p:text-gray-300 prose-p:leading-relaxed prose-p:text-lg prose-headings:text-slate-900 dark:prose-headings:text-white prose-a:text-blue-600 dark:prose-a:text-blue-400 prose-img:rounded-3xl prose-img:shadow-2xl relative z-10">
<ReactMarkdown urlTransform={getImageUrl}>{content || "Draft empty. Initiate composition above..."}</ReactMarkdown>
</div> </div>
</div> </div>
) : ( ) : (
<div className="relative"> <div className="relative group/canvas">
<textarea <textarea
ref={textareaRef} ref={textareaRef}
value={content} value={content}
onChange={(e) => onChange({ content: e.target.value })} onChange={(e) => onChange({ content: e.target.value })}
placeholder="Explain the activity (Markdown supported)..." placeholder="Unfold your vision. Markdown architectural syntax supported..."
className="w-full h-80 bg-white/5 border border-white/10 rounded-xl p-6 text-lg tracking-tight focus:border-blue-500/50 focus:outline-none transition-all resize-none shadow-inner custom-scrollbar" className="w-full h-[400px] bg-slate-50 dark:bg-black/40 border border-slate-200 dark:border-white/10 rounded-[2rem] p-10 text-lg tracking-tight focus:ring-8 focus:ring-blue-500/5 focus:border-blue-500/50 focus:outline-none transition-all resize-none shadow-inner custom-scrollbar relative z-10 font-medium leading-relaxed"
/> />
<div className="absolute bottom-4 right-4 text-[10px] text-gray-600 font-bold uppercase tracking-widest pointer-events-none"> <div className="absolute bottom-6 right-8 text-[10px] text-slate-300 dark:text-gray-600 font-black uppercase tracking-[0.3em] pointer-events-none z-20 group-hover/canvas:text-blue-400/50 transition-colors">
Markdown Mode Structural Markdown Mode
</div> </div>
</div> </div>
)} )}
{suggestion && ( {suggestion && (
<div className="bg-indigo-500/10 border border-indigo-500/30 rounded-2xl p-6 space-y-4 animate-in fade-in slide-in-from-top-4 duration-500 shadow-xl shadow-indigo-500/5"> <div className="bg-white dark:bg-indigo-500/10 border border-indigo-200 dark:border-indigo-500/30 rounded-[2.5rem] p-10 space-y-8 animate-in fade-in slide-in-from-top-6 duration-700 shadow-2xl relative overflow-hidden">
<div className="flex items-center justify-between"> <div className="absolute top-0 right-0 w-64 h-64 bg-indigo-500/5 rounded-full blur-[80px] -translate-y-1/2 translate-x-1/2"></div>
<div className="flex items-center gap-2"> <div className="flex items-center justify-between relative z-10">
<Wand2 size={16} className="text-indigo-400" /> <div className="flex items-center gap-4">
<span className="text-xs font-black uppercase tracking-widest text-indigo-300">AI Teacher Suggestions</span> <div className="w-12 h-12 rounded-2xl bg-indigo-600 text-white flex items-center justify-center shadow-lg shadow-indigo-500/30">
</div> <Wand2 size={24} />
<div className="flex items-center gap-2"> </div>
<button <div>
onClick={() => setSuggestion(null)} <h4 className="text-xl font-black italic tracking-tight text-slate-900 dark:text-white uppercase">Neural Refinement Suggestions</h4>
className="p-1.5 hover:bg-white/10 rounded-lg text-gray-400 transition-colors" <p className="text-[10px] text-slate-400 dark:text-gray-400 mt-1 uppercase tracking-[0.2em] font-black">AI-optimized pedagogical structure</p>
> </div>
<CloseIcon size={14} />
</button>
</div> </div>
<button
onClick={() => setSuggestion(null)}
className="w-10 h-10 flex items-center justify-center hover:bg-slate-100 dark:hover:bg-white/10 rounded-full text-slate-400 transition-colors"
>
<CloseIcon size={20} />
</button>
</div> </div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6"> <div className="grid grid-cols-1 md:grid-cols-2 gap-10 relative z-10">
<div className="space-y-2"> <div className="space-y-4">
<span className="text-[10px] font-bold text-gray-500 uppercase tracking-widest pl-1">Improved Version</span> <span className="text-[10px] font-black text-slate-400 dark:text-gray-500 uppercase tracking-widest pl-1">Proposed Architecture</span>
<div className="p-4 bg-black/40 rounded-xl border border-white/5 text-sm text-gray-300 leading-relaxed italic"> <div className="p-8 bg-slate-50 dark:bg-black/40 rounded-[2rem] border border-slate-200 dark:border-white/5 text-sm text-slate-600 dark:text-gray-300 leading-relaxed italic shadow-inner">
{suggestion.suggestion} &quot;{suggestion.suggestion}&quot;
</div> </div>
</div> </div>
<div className="space-y-3"> <div className="space-y-6">
<span className="text-[10px] font-bold text-gray-500 uppercase tracking-widest pl-1">Key Changes</span> <span className="text-[10px] font-black text-slate-400 dark:text-gray-500 uppercase tracking-widest pl-1">Pedagogical Insights</span>
<div className="text-xs text-gray-400 leading-relaxed pl-1 whitespace-pre-line"> <div className="text-sm text-slate-500 dark:text-gray-400 font-medium leading-relaxed pl-1 whitespace-pre-line border-l-2 border-indigo-100 pl-6 py-2">
{suggestion.comments} {suggestion.comments}
</div> </div>
<div className="pt-4 flex gap-3"> <div className="pt-6 flex gap-4">
<button <button
onClick={applySuggestion} onClick={applySuggestion}
className="px-4 py-2 bg-indigo-600 hover:bg-indigo-500 text-white text-xs font-bold rounded-lg transition-all shadow-lg shadow-indigo-500/20 flex items-center gap-2 group active:scale-95" className="px-8 py-4 bg-indigo-600 hover:bg-indigo-500 text-white text-[10px] font-black uppercase tracking-[0.2em] rounded-2xl transition-all shadow-xl shadow-indigo-500/30 flex items-center gap-3 active:scale-95"
> >
<Check size={14} className="group-hover:scale-110 transition-transform" /> <Check size={16} />
Apply Changes Update Syllabus
</button> </button>
<button <button
onClick={() => setSuggestion(null)} onClick={() => setSuggestion(null)}
className="px-4 py-2 bg-white/5 hover:bg-white/10 text-gray-400 hover:text-white text-xs font-bold rounded-lg border border-white/10 transition-all active:scale-95" className="px-8 py-4 bg-white dark:bg-white/5 hover:bg-slate-50 text-slate-400 dark:text-gray-400 hover:text-slate-900 text-[10px] font-black uppercase tracking-[0.2em] rounded-2xl border border-slate-200 dark:border-white/10 transition-all active:scale-95 shadow-sm"
> >
Dismiss Discard
</button> </button>
</div> </div>
</div> </div>
@@ -220,8 +227,8 @@ export default function DescriptionBlock({ id, title, content, editMode, courseI
)} )}
</div> </div>
) : ( ) : (
<div className="prose prose-invert max-w-none prose-p:text-gray-300 prose-p:leading-relaxed prose-p:text-lg prose-headings:text-white prose-a:text-blue-400 prose-img:rounded-xl"> <div className="prose dark:prose-invert max-w-none prose-p:text-slate-600 dark:prose-p:text-gray-300 prose-p:leading-relaxed prose-p:text-lg prose-headings:text-slate-900 dark:prose-headings:text-white prose-a:text-blue-600 dark:prose-a:text-blue-400 prose-img:rounded-3xl prose-img:shadow-xl">
<ReactMarkdown urlTransform={getImageUrl}>{content || "No description provided."}</ReactMarkdown> <ReactMarkdown urlTransform={getImageUrl}>{content || "Conceptual flow not established yet."}</ReactMarkdown>
</div> </div>
)} )}
@@ -21,30 +21,32 @@ export default function DocumentBlock({ title, url, editMode, onChange }: Docume
{/* Block Header */} {/* Block Header */}
<div className="space-y-2"> <div className="space-y-2">
{editMode ? ( {editMode ? (
<div className="space-y-2 p-6 glass border-white/5 bg-white/5 mb-4"> <div className="space-y-4 p-8 bg-slate-50 dark:bg-white/5 border border-slate-200 dark:border-white/10 rounded-[2rem] mb-6 shadow-inner relative overflow-hidden">
<label className="text-xs font-bold text-gray-500 uppercase tracking-widest">Document Activity Title</label> <div className="absolute top-0 left-0 w-1 h-full bg-indigo-500/40"></div>
<label className="text-[10px] font-black text-slate-400 dark:text-gray-500 uppercase tracking-[0.2em]">Document Activity Title</label>
<input <input
type="text" type="text"
value={title || ""} value={title || ""}
onChange={(e) => onChange({ title: e.target.value })} onChange={(e) => onChange({ title: e.target.value })}
placeholder="e.g. Course Syllabus, Reading Guide..." placeholder="e.g. Course Syllabus, Reading Guide..."
className="w-full bg-white/5 border border-white/10 rounded-lg px-4 py-2 text-sm font-bold focus:border-blue-500/50 focus:outline-none" className="w-full bg-white dark:bg-black/40 border border-slate-200 dark:border-white/10 rounded-2xl px-6 py-3 text-sm font-black uppercase tracking-tight focus:ring-4 focus:ring-indigo-500/10 focus:border-indigo-500 transition-all outline-none shadow-sm"
/> />
</div> </div>
) : ( ) : (
title && <h3 className="text-xl font-bold border-l-4 border-indigo-500 pl-4 py-1 tracking-tight text-white">{title}</h3> title && <h3 className="text-2xl font-black italic tracking-tight text-slate-900 dark:text-white uppercase border-l-4 border-indigo-600 pl-6 py-1">{title}</h3>
)} )}
</div> </div>
{editMode && ( {editMode && (
<div className="p-6 glass border-blue-500/10 mb-8 bg-blue-500/5"> <div className="p-10 bg-white dark:bg-white/5 border border-indigo-500/10 dark:border-indigo-500/20 mb-10 rounded-[2.5rem] shadow-xl shadow-indigo-500/5 relative overflow-hidden">
<label className="text-xs font-bold text-gray-500 uppercase tracking-widest mb-4 block">Upload Document (PDF, DOCX, PPTX)</label> <div className="absolute top-0 right-0 w-64 h-64 bg-indigo-500/5 rounded-full blur-[80px] -translate-y-1/2 translate-x-1/2"></div>
<label className="text-[10px] font-black text-slate-400 dark:text-gray-500 uppercase tracking-widest mb-6 block pl-1 relative z-10">Asset Pipeline (PDF, DOCX, PPTX)</label>
<FileUpload <FileUpload
currentUrl={url} currentUrl={url}
onUploadComplete={(newUrl) => onChange({ url: newUrl })} onUploadComplete={(newUrl) => onChange({ url: newUrl })}
/> />
<p className="text-[10px] text-gray-500 uppercase leading-relaxed mt-4 px-2"> <p className="text-[9px] text-slate-400 dark:text-gray-500 uppercase font-black italic mt-6 px-2 leading-relaxed relative z-10">
Supported formats: PDF (can be previewed), DOCX, PPTX (download only). Standards: PDF (Native Rendering), DOCX/PPTX (Direct Retrieval).
</p> </p>
</div> </div>
)} )}
@@ -54,51 +56,54 @@ export default function DocumentBlock({ title, url, editMode, onChange }: Docume
{url ? ( {url ? (
<div className="space-y-4"> <div className="space-y-4">
{isPdf ? ( {isPdf ? (
<div className="glass rounded-2xl overflow-hidden border-white/5 bg-white/5 aspect-[4/3] w-full"> <div className="bg-white dark:bg-white/5 rounded-[2.5rem] overflow-hidden border border-slate-100 dark:border-white/10 shadow-2xl relative">
<iframe <div className="aspect-[4/3] w-full bg-slate-100 dark:bg-black/20">
src={`${displayUrl}#toolbar=0`} <iframe
className="w-full h-full border-none" src={`${displayUrl}#toolbar=0`}
title={title || "Document Preview"} className="w-full h-full border-none"
/> title={title || "Document Preview"}
<div className="p-4 border-t border-white/5 flex items-center justify-between bg-black/40"> />
<span className="text-xs font-bold text-gray-400 uppercase tracking-widest flex items-center gap-2"> </div>
<Eye size={14} className="text-indigo-400" /> PDF Preview <div className="p-6 border-t border-slate-100 dark:border-white/10 flex items-center justify-between bg-slate-50 dark:bg-black/40">
<span className="text-[10px] font-black text-slate-400 dark:text-gray-500 uppercase tracking-[0.2em] flex items-center gap-3">
<Eye size={16} className="text-indigo-600 dark:text-indigo-400" /> Neural Vision Enabled
</span> </span>
<a <a
href={displayUrl} href={displayUrl}
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
className="flex items-center gap-2 px-4 py-2 bg-indigo-500/20 hover:bg-indigo-500/40 text-indigo-400 rounded-lg text-xs font-black uppercase tracking-widest transition-all" className="flex items-center gap-3 px-6 py-3 bg-indigo-600 text-white rounded-xl text-[10px] font-black uppercase tracking-widest hover:scale-[1.05] active:scale-95 transition-all shadow-lg shadow-indigo-500/30"
> >
<Download size={14} /> Full Screen / Download <Download size={16} /> Asset Extraction
</a> </a>
</div> </div>
</div> </div>
) : ( ) : (
<div className="glass p-12 rounded-3xl border border-white/5 bg-white/5 flex flex-col items-center text-center gap-6"> <div className="bg-white dark:bg-white/5 p-16 rounded-[3rem] border border-slate-100 dark:border-white/10 shadow-sm flex flex-col items-center text-center gap-8 relative overflow-hidden">
<div className="w-20 h-20 rounded-2xl bg-indigo-500/10 flex items-center justify-center text-indigo-400"> <div className="absolute top-0 left-0 w-full h-1 bg-gradient-to-r from-transparent via-indigo-500/40 to-transparent"></div>
<FileText size={40} /> <div className="w-24 h-24 rounded-[2rem] bg-indigo-50 dark:bg-indigo-500/10 flex items-center justify-center text-indigo-600 dark:text-indigo-400 shadow-inner">
<FileText size={48} />
</div> </div>
<div> <div className="space-y-3">
<p className="text-lg font-bold text-white mb-2">Non-PDF Document</p> <p className="text-2xl font-black text-slate-900 dark:text-white uppercase tracking-tight italic">Legacy Container</p>
<p className="text-sm text-gray-500 max-w-sm mx-auto uppercase tracking-widest font-black leading-relaxed"> <p className="text-[10px] text-slate-400 dark:text-gray-500 max-w-xs mx-auto uppercase tracking-[0.2em] font-black leading-loose">
This file cannot be previewed directly. Please download it to read. Source material requires local decompression for full synaptic ingestion.
</p> </p>
</div> </div>
<a <a
href={displayUrl} href={displayUrl}
download download
className="flex items-center gap-3 px-8 py-4 bg-indigo-600 hover:bg-indigo-500 text-white rounded-2xl font-black uppercase tracking-widest transition-all shadow-xl shadow-indigo-500/20 active:scale-95" className="flex items-center gap-4 px-10 py-5 bg-indigo-600 hover:bg-indigo-500 text-white rounded-[2rem] font-black text-[10px] uppercase tracking-[0.3em] transition-all shadow-2xl shadow-indigo-500/40 active:scale-95 group"
> >
<Download size={20} /> Download File <Download size={20} className="group-hover:translate-y-1 transition-transform" /> Trigger Download
</a> </a>
</div> </div>
)} )}
</div> </div>
) : ( ) : (
<div className="glass border-dashed border-white/10 p-12 rounded-3xl flex flex-col items-center gap-4 text-gray-500"> <div className="bg-slate-50 dark:bg-black/20 border-2 border-dashed border-slate-200 dark:border-white/10 p-20 rounded-[3rem] flex flex-col items-center gap-6 text-slate-300 dark:text-gray-700">
<FileText size={48} className="opacity-20" /> <FileText size={64} className="opacity-10" />
<p className="font-bold uppercase tracking-widest text-xs">No file selected</p> <p className="font-black uppercase tracking-[0.3em] text-[10px]">No Neural Data Linked</p>
</div> </div>
)} )}
</div> </div>
@@ -50,42 +50,45 @@ export default function FillInTheBlanksBlock({ id, title, content, editMode, onC
<div className="space-y-8" id={id}> <div className="space-y-8" id={id}>
<div className="space-y-2"> <div className="space-y-2">
{editMode ? ( {editMode ? (
<div className="space-y-2 p-6 glass border-white/5 bg-white/5 mb-4"> <div className="space-y-4 p-8 bg-slate-50 dark:bg-white/5 border border-slate-200 dark:border-white/10 rounded-[2rem] mb-6 shadow-inner relative overflow-hidden">
<label className="text-xs font-bold text-gray-500 uppercase tracking-widest">Activity Title (Optional)</label> <div className="absolute top-0 left-0 w-1 h-full bg-blue-500/40"></div>
<label className="text-[10px] font-black text-slate-400 dark:text-gray-500 uppercase tracking-[0.2em]">Activity Title (Optional)</label>
<input <input
type="text" type="text"
value={title || ""} value={title || ""}
onChange={(e) => onChange({ title: e.target.value })} onChange={(e) => onChange({ title: e.target.value })}
placeholder="e.g. Fill the gaps, Quote..." placeholder="e.g. Fill the gaps, Quote..."
className="w-full bg-white/5 border border-white/10 rounded-lg px-4 py-2 text-sm font-bold focus:border-blue-500/50 focus:outline-none" className="w-full bg-white dark:bg-black/40 border border-slate-200 dark:border-white/10 rounded-2xl px-6 py-3 text-sm font-black uppercase tracking-tight focus:ring-4 focus:ring-blue-500/10 focus:border-blue-500 transition-all outline-none shadow-sm"
/> />
</div> </div>
) : ( ) : (
<h3 className="text-xl font-bold border-l-4 border-blue-500 pl-4 py-1 tracking-tight text-white"> <h3 className="text-2xl font-black italic tracking-tight text-slate-900 dark:text-white uppercase border-l-4 border-blue-600 pl-6 py-1">
{title || "Fill in the Blanks"} {title || "Fill in the Blanks"}
</h3> </h3>
)} )}
</div> </div>
{editMode ? ( {editMode ? (
<div className="space-y-4"> <div className="space-y-8">
<div className="p-6 glass border-white/5 space-y-4"> <div className="p-10 bg-white dark:bg-white/5 border border-slate-200 dark:border-white/10 rounded-[2.5rem] space-y-4 shadow-sm relative overflow-hidden">
<label className="text-xs font-bold text-gray-500 uppercase tracking-widest">Text with blanks (use [[answer]])</label> <div className="absolute top-0 right-0 w-32 h-32 bg-blue-500/5 rounded-full blur-3xl -translate-y-1/2 translate-x-1/2"></div>
<label className="text-[10px] font-black text-slate-400 dark:text-gray-500 uppercase tracking-widest pl-1">Semantic Matrix (use [[answer]])</label>
<textarea <textarea
value={content} value={content}
onChange={(e) => onChange({ content: e.target.value })} onChange={(e) => onChange({ content: e.target.value })}
className="w-full bg-white/5 border border-white/10 rounded-xl p-4 min-h-[150px] text-lg font-medium focus:outline-none focus:border-blue-500/50 transition-all" className="w-full bg-slate-50 dark:bg-black/40 border border-slate-200 dark:border-white/10 rounded-2xl p-6 min-h-[150px] text-lg font-black tracking-tight text-slate-800 dark:text-white focus:ring-4 focus:ring-blue-500/10 focus:border-blue-500 transition-all placeholder:text-slate-300 resize-none shadow-inner"
placeholder="Example: The [[capital]] of France is [[Paris]]." placeholder="Example: The [[capital]] of France is [[Paris]]."
/> />
<p className="text-[10px] text-gray-500 uppercase tracking-wider">Tip: Surround any word with double brackets to create a blank.</p> <p className="text-[9px] text-slate-400 dark:text-gray-500 uppercase font-black italic pl-1 leading-relaxed">Neural Protocol: Encapsulate target markers within double square brackets.</p>
</div> </div>
</div> </div>
) : ( ) : (
<div className="p-8 glass border-white/5 rounded-3xl space-y-8"> <div className="p-10 bg-white dark:bg-white/5 border border-slate-100 dark:border-white/10 rounded-[3rem] space-y-10 shadow-sm hover:shadow-xl transition-all duration-700 relative overflow-hidden group/fbitem">
<div className="text-lg leading-loose text-gray-100"> <div className="absolute top-0 right-0 w-64 h-64 bg-indigo-500/5 rounded-full blur-[80px] -translate-y-1/2 translate-x-1/2 group-hover/fbitem:bg-indigo-500/10 transition-colors"></div>
<div className="text-2xl font-black text-slate-800 dark:text-gray-100 uppercase tracking-tight leading-loose relative z-10">
{parsed.parts.map((part, i) => ( {parsed.parts.map((part, i) => (
part.type === 'text' ? ( part.type === 'text' ? (
<span key={i}>{part.value}</span> <span key={i} className="mx-0.5">{part.value}</span>
) : ( ) : (
<input <input
key={i} key={i}
@@ -97,12 +100,12 @@ export default function FillInTheBlanksBlock({ id, title, content, editMode, onC
setUserAnswers(newAnswers); setUserAnswers(newAnswers);
}} }}
disabled={submitted} disabled={submitted}
className={`mx-1 px-2 py-0 border-b-2 bg-transparent transition-all focus:outline-none text-center rounded-t-sm ${submitted className={`mx-2 px-4 py-1 border-b-4 bg-slate-50 dark:bg-black/20 transition-all focus:outline-none text-center rounded-t-xl font-black uppercase tracking-widest placeholder:text-slate-300 ${submitted
? (isCorrect(part.index!) ? "border-green-500 text-green-400 bg-green-500/10" : "border-red-500 text-red-100 bg-red-500/10") ? (isCorrect(part.index!) ? "border-green-500 text-green-700 dark:text-green-400 bg-green-50 dark:bg-green-500/10" : "border-red-500 text-red-700 dark:text-red-100 bg-red-50 dark:bg-red-500/10")
: "border-blue-500/30 focus:border-blue-500 text-blue-400 focus:bg-blue-500/5" : "border-blue-500/40 focus:border-blue-600 text-blue-600 dark:text-blue-400 focus:bg-blue-500/5"
}`} }`}
style={{ width: `${Math.max((part.answer?.length || 5) * 12, 60)}px` }} style={{ width: `${Math.max((part.answer?.length || 5) * 16, 120)}px` }}
placeholder="..." placeholder="?"
/> />
) )
))} ))}
@@ -111,18 +114,18 @@ export default function FillInTheBlanksBlock({ id, title, content, editMode, onC
{!submitted && parsed.answers.length > 0 && ( {!submitted && parsed.answers.length > 0 && (
<button <button
onClick={() => setSubmitted(true)} onClick={() => setSubmitted(true)}
className="btn-premium w-full py-4 font-black text-xs uppercase tracking-[0.2em] shadow-xl shadow-blue-500/20" className="w-full py-6 bg-blue-600 text-white font-black text-[10px] uppercase tracking-[0.3em] shadow-2xl shadow-blue-500/40 hover:scale-[1.02] active:scale-[0.98] transition-all rounded-[2rem] flex items-center justify-center gap-4 relative z-10"
> >
Validate Answers Execute Linguistic Validation
</button> </button>
)} )}
{submitted && ( {submitted && (
<button <button
onClick={handleReset} onClick={handleReset}
className="w-full py-4 glass text-blue-400 font-black text-xs uppercase tracking-[0.2em] hover:bg-white/5 transition-all rounded-xl border-white/5" className="w-full py-6 bg-white dark:bg-white/5 border border-slate-200 dark:border-white/10 text-blue-600 dark:text-blue-400 font-black text-[10px] uppercase tracking-[0.3em] hover:bg-slate-50 transition-all rounded-[2rem] active:scale-[0.98] shadow-sm relative z-10"
> >
Try Again Reset Semantic Chain
</button> </button>
)} )}
</div> </div>
@@ -75,26 +75,29 @@ export default function HotspotBlock({
if (!editMode) { if (!editMode) {
return ( return (
<div className="space-y-4" id={id}> <div className="space-y-6" id={id}>
<div className="flex items-center gap-3"> <div className="flex items-center gap-4">
<div className="p-2 rounded-lg bg-amber-500/20 text-amber-500"> <div className="p-3 rounded-2xl bg-amber-50 dark:bg-amber-500/10 text-amber-600 dark:text-amber-500 shadow-inner">
<Search size={20} /> <Search size={24} />
</div> </div>
<div> <div>
<h3 className="text-lg font-bold text-white transition-colors">{title || "Image Hunt"}</h3> <h3 className="text-2xl font-black italic tracking-tight text-slate-900 dark:text-white uppercase transition-colors">{title || "Visual Scrutiny"}</h3>
<p className="text-xs text-gray-500 uppercase tracking-widest font-black">{description || "Find the hidden spots!"}</p> <p className="text-[10px] text-slate-400 dark:text-gray-500 uppercase tracking-[0.2em] font-black">{description || "Identify the hidden visual nodes"}</p>
</div> </div>
</div> </div>
<div className="relative aspect-video rounded-2xl overflow-hidden border border-white/5 bg-black/40"> <div className="relative aspect-video rounded-[3rem] overflow-hidden border border-slate-100 dark:border-white/5 bg-slate-50 dark:bg-black/40 shadow-xl group/hsview">
{imageUrl ? ( {imageUrl ? (
<Image src={getImageUrl(imageUrl)} alt={title || ""} fill unoptimized className="object-cover opacity-50" /> <Image src={getImageUrl(imageUrl)} alt={title || ""} fill unoptimized className="object-cover opacity-60 grayscale group-hover/hsview:grayscale-0 transition-all duration-700" />
) : ( ) : (
<div className="absolute inset-0 flex items-center justify-center text-gray-600 italic text-sm">No image provided.</div> <div className="absolute inset-0 flex flex-col items-center justify-center text-slate-300 dark:text-gray-700 gap-4">
<ImageIcon size={48} className="opacity-20" />
<p className="text-[9px] font-black uppercase tracking-widest italic">Temporal image stream offline.</p>
</div>
)} )}
<div className="absolute inset-0 flex items-center justify-center backdrop-blur-[2px]"> <div className="absolute inset-0 flex flex-col items-center justify-center backdrop-blur-[4px] bg-slate-900/10 dark:bg-black/40 p-8 text-center group-hover/hsview:backdrop-blur-0 transition-all duration-700">
<div className="text-center space-y-2"> <div className="space-y-4 max-w-sm">
<Crosshair className="w-12 h-12 text-white/20 mx-auto" /> <Crosshair className="w-16 h-16 text-white/40 mx-auto animate-pulse" />
<p className="text-xs font-bold text-white/40 uppercase tracking-widest">Interactive Game Preview (Switch to Student View to Play)</p> <p className="text-[10px] font-black text-white uppercase tracking-[0.3em] leading-relaxed">Structural Game Synchronization Pending<br /><span className="text-white/40 italic">(Activate Simulation Mode to Initialize)</span></p>
</div> </div>
</div> </div>
</div> </div>
@@ -104,46 +107,50 @@ export default function HotspotBlock({
return ( return (
<div className="space-y-6" id={id}> <div className="space-y-6" id={id}>
<div className="p-6 glass border-white/5 bg-white/5 space-y-6 rounded-3xl"> <div className="p-10 bg-white dark:bg-white/5 border border-slate-100 dark:border-white/10 space-y-10 rounded-[3rem] shadow-sm relative overflow-hidden group/hseditor shadow-xl shadow-slate-200/50 dark:shadow-none">
<div className="grid grid-cols-1 md:grid-cols-2 gap-6"> <div className="absolute top-0 right-0 w-64 h-64 bg-amber-500/5 rounded-full blur-[80px] -translate-y-1/2 translate-x-1/2 group-hover/hseditor:bg-amber-500/10 transition-colors"></div>
<div className="space-y-4">
<div className="space-y-2"> <div className="grid grid-cols-1 md:grid-cols-2 gap-10 relative z-10">
<label className="text-[10px] font-black uppercase tracking-widest text-gray-500">Game Title</label> <div className="space-y-6">
<div className="space-y-3">
<label className="text-[10px] font-black uppercase tracking-[0.2em] text-slate-400 dark:text-gray-500 pl-1">Operational Protocol (Title)</label>
<input <input
type="text" type="text"
value={title || ""} value={title || ""}
onChange={(e) => onChange({ title: e.target.value })} onChange={(e) => onChange({ title: e.target.value })}
placeholder="e.g. Parts of the Body..." placeholder="e.g. Parts of the Body..."
className="w-full bg-white/5 border border-white/10 rounded-xl px-4 py-2 text-sm font-bold focus:border-amber-500/50 focus:outline-none" className="w-full bg-slate-50 dark:bg-black/40 border border-slate-100 dark:border-white/10 rounded-2xl px-6 py-4 text-sm font-black uppercase tracking-tight text-slate-800 dark:text-white focus:ring-4 focus:ring-amber-500/10 focus:border-amber-500 transition-all outline-none"
/> />
</div> </div>
<div className="space-y-2"> <div className="space-y-3">
<label className="text-[10px] font-black uppercase tracking-widest text-gray-500">Student Instructions</label> <label className="text-[10px] font-black uppercase tracking-[0.2em] text-slate-400 dark:text-gray-500 pl-1">Tactical Instructions</label>
<input <input
type="text" type="text"
value={description || ""} value={description || ""}
onChange={(e) => onChange({ description: e.target.value })} onChange={(e) => onChange({ description: e.target.value })}
placeholder="e.g. Find and click on the following items..." placeholder="e.g. Find and click on the following items..."
className="w-full bg-white/5 border border-white/10 rounded-xl px-4 py-2 text-sm focus:border-amber-500/50 focus:outline-none" className="w-full bg-slate-50 dark:bg-black/40 border border-slate-100 dark:border-white/10 rounded-2xl px-6 py-4 text-sm font-bold text-slate-600 dark:text-gray-300 focus:ring-4 focus:ring-amber-500/10 focus:border-amber-500 transition-all outline-none"
/> />
</div> </div>
</div> </div>
<div className="space-y-2"> <div className="space-y-3">
<label className="text-[10px] font-black uppercase tracking-widest text-gray-500">Game Image</label> <label className="text-[10px] font-black uppercase tracking-[0.2em] text-slate-400 dark:text-gray-500 pl-1">Visual Baseline (Main Image)</label>
{!imageUrl ? ( {!imageUrl ? (
<button <button
onClick={() => setIsAssetPickerOpen(true)} onClick={() => setIsAssetPickerOpen(true)}
className="w-full aspect-video rounded-2xl border-2 border-dashed border-white/10 hover:border-amber-500/50 hover:bg-amber-500/5 transition-all flex flex-col items-center justify-center gap-2 group" className="w-full aspect-video rounded-[2rem] border-2 border-dashed border-slate-200 dark:border-white/10 hover:border-amber-500/50 hover:bg-white dark:hover:bg-amber-500/5 transition-all flex flex-col items-center justify-center gap-4 group/imgup"
> >
<ImageIcon className="text-gray-600 group-hover:text-amber-500 transition-colors" size={32} /> <div className="p-4 rounded-2xl bg-slate-50 dark:bg-white/5 group-hover/imgup:bg-amber-500/10 transition-colors">
<span className="text-xs font-bold text-gray-500 uppercase tracking-widest group-hover:text-amber-300">Choose Image</span> <ImageIcon className="text-slate-300 dark:text-gray-600 group-hover/imgup:text-amber-600 transition-colors" size={32} />
</div>
<span className="text-[10px] font-black text-slate-400 dark:text-gray-500 uppercase tracking-widest group-hover/imgup:text-amber-700">Initialize Visual Stream</span>
</button> </button>
) : ( ) : (
<div className="relative aspect-video rounded-2xl overflow-hidden group"> <div className="relative aspect-video rounded-[2rem] overflow-hidden border border-slate-100 dark:border-white/10 group/imgpreview shadow-inner">
<Image src={getImageUrl(imageUrl)} alt="Hotspot base" fill unoptimized className="object-cover" /> <Image src={getImageUrl(imageUrl)} alt="Hotspot base" fill unoptimized className="object-cover select-none" />
<div className="absolute inset-0 bg-black/60 opacity-0 group-hover:opacity-100 transition-opacity flex items-center justify-center gap-3"> <div className="absolute inset-0 bg-slate-900/60 opacity-0 group-hover/imgpreview:opacity-100 transition-all duration-500 flex items-center justify-center gap-4 backdrop-blur-sm">
<button onClick={() => setIsAssetPickerOpen(true)} className="p-2 bg-white/10 hover:bg-white/20 rounded-lg text-white transition-all"><ImageIcon size={18} /></button> <button onClick={() => setIsAssetPickerOpen(true)} className="p-4 bg-white/10 hover:bg-white hover:text-slate-900 rounded-2xl text-white transition-all transform hover:scale-110"><ImageIcon size={20} /></button>
<button onClick={() => onChange({ imageUrl: undefined })} className="p-2 bg-red-500/20 hover:bg-red-500/40 rounded-lg text-red-400 transition-all"><Trash2 size={18} /></button> <button onClick={() => onChange({ imageUrl: undefined })} className="p-4 bg-red-500/20 hover:bg-red-500 rounded-2xl text-white transition-all transform hover:scale-110"><Trash2 size={20} /></button>
</div> </div>
</div> </div>
)} )}
@@ -151,24 +158,24 @@ export default function HotspotBlock({
</div> </div>
{imageUrl && ( {imageUrl && (
<div className="space-y-4 pt-4 border-t border-white/5"> <div className="space-y-8 pt-10 border-t border-slate-50 dark:border-white/5">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between px-2">
<label className="text-[10px] font-black uppercase tracking-widest text-gray-500">Define Hotspots (Click on the image below)</label> <label className="text-[10px] font-black uppercase tracking-[0.2em] text-slate-400 dark:text-gray-500 italic">Structural Node Mapping (Click Image Area)</label>
<span className="text-[10px] font-bold text-amber-500 bg-amber-500/10 px-2 py-1 rounded uppercase tracking-widest">{hotspots.length} Defined</span> <span className="text-[9px] font-black text-amber-700 dark:text-amber-400 bg-amber-50 dark:bg-amber-500/10 px-4 py-2 rounded-xl border border-amber-100 dark:border-amber-500/20 shadow-inner uppercase tracking-widest">{hotspots.length} Nodes Synchronized</span>
</div> </div>
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6"> <div className="grid grid-cols-1 lg:grid-cols-3 gap-10">
<div className="lg:col-span-2"> <div className="lg:col-span-2">
<div <div
ref={containerRef} ref={containerRef}
onClick={handleImageClick} onClick={handleImageClick}
className="relative aspect-video rounded-2xl overflow-hidden border-2 border-white/10 cursor-crosshair shadow-2xl" className="relative aspect-video rounded-[2.5rem] overflow-hidden border-2 border-slate-100 dark:border-white/10 cursor-crosshair bg-slate-50 dark:bg-black group/mpro shadow-2xl transition-all duration-500 hover:shadow-amber-500/10"
> >
<Image src={getImageUrl(imageUrl)} alt="Define Hotspots" fill unoptimized className="object-cover select-none" /> <Image src={getImageUrl(imageUrl)} alt="Define Hotspots" fill unoptimized className="object-cover select-none group-hover/mpro:opacity-80 transition-opacity" />
{hotspots.map((h) => ( {hotspots.map((h, hidx) => (
<div <div
key={h.id} key={h.id}
className="absolute group/pin" className="absolute group/pin z-20"
style={{ style={{
left: `${h.x}%`, left: `${h.x}%`,
top: `${h.y}%`, top: `${h.y}%`,
@@ -176,19 +183,21 @@ export default function HotspotBlock({
}} }}
> >
<div <div
className="bg-amber-500/30 border-2 border-amber-400 rounded-full flex items-center justify-center relative transition-transform hover:scale-110" className="bg-amber-500/20 border-2 border-amber-500 dark:border-amber-400 rounded-full flex items-center justify-center relative transition-all duration-500 hover:scale-125 hover:bg-amber-500/40"
style={{ style={{
width: `${h.radius * 2}vw`, width: `${h.radius * 2.5}vw`,
height: `${h.radius * 2}vw`, height: `${h.radius * 2.5}vw`,
maxWidth: '100px', maxWidth: '120px',
maxHeight: '100px' maxHeight: '120px',
minWidth: '40px',
minHeight: '40px'
}} }}
> >
<div className="bg-amber-500 rounded-full p-1 text-black shadow-lg"> <div className="bg-amber-600 dark:bg-amber-500 rounded-full p-2 text-white shadow-2xl ring-4 ring-amber-500/20">
<MapPin size={12} strokeWidth={3} /> <Crosshair size={14} strokeWidth={3} className="group-hover/pin:rotate-90 transition-transform duration-500" />
</div> </div>
<div className="absolute top-full mt-2 left-1/2 -translate-x-1/2 px-2 py-1 bg-black/80 rounded text-[10px] font-bold text-white whitespace-nowrap opacity-0 group-hover/pin:opacity-100 transition-opacity"> <div className="absolute top-full mt-3 left-1/2 -translate-x-1/2 px-4 py-2 bg-slate-900 dark:bg-white text-white dark:text-slate-900 rounded-xl text-[10px] font-black uppercase tracking-tight whitespace-nowrap opacity-0 group-hover/pin:opacity-100 transition-all transform translate-y-2 group-hover/pin:translate-y-0 shadow-2xl z-50">
{h.label} NODE #{hidx + 1}: {h.label}
</div> </div>
</div> </div>
</div> </div>
@@ -196,38 +205,48 @@ export default function HotspotBlock({
</div> </div>
</div> </div>
<div className="space-y-3 max-h-[400px] overflow-y-auto custom-scrollbar pr-2"> <div className="space-y-4 max-h-[500px] overflow-y-auto custom-scrollbar pr-3">
{hotspots.length === 0 ? ( {hotspots.length === 0 ? (
<div className="h-full flex flex-col items-center justify-center text-center p-6 border-2 border-dashed border-white/5 rounded-2xl"> <div className="h-full flex flex-col items-center justify-center text-center p-12 bg-slate-50 dark:bg-black/20 border-2 border-dashed border-slate-200 dark:border-white/10 rounded-[2.5rem] space-y-4">
<Plus className="text-gray-700 mb-2" size={24} /> <div className="p-4 bg-white dark:bg-white/5 rounded-2xl shadow-inner italic">
<p className="text-xs text-gray-600 font-bold uppercase tracking-widest">Click on the image to add hotspots</p> <Plus className="text-slate-300 dark:text-gray-700" size={32} />
</div>
<p className="text-[10px] text-slate-400 dark:text-gray-500 font-black uppercase tracking-widest">Execute visual contact on the map area to generate nodes.</p>
</div> </div>
) : ( ) : (
hotspots.map((h, idx) => ( hotspots.map((h, idx) => (
<div key={h.id} className="p-4 bg-white/5 border border-white/10 rounded-xl space-y-3 animate-in fade-in slide-in-from-right-4 duration-300"> <div key={h.id} className="p-6 bg-slate-50 dark:bg-white/5 border border-slate-100 dark:border-white/10 rounded-[1.5rem] space-y-5 animate-in fade-in slide-in-from-right-4 duration-500 group/hscard hover:border-amber-500/30 transition-all shadow-sm">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<span className="text-[10px] font-black text-amber-500 uppercase tracking-widest">Hotspot #{idx + 1}</span>
<button onClick={() => removeHotspot(idx)} className="p-1 hover:bg-red-500/20 text-red-500 rounded transition-colors"><Trash2 size={12} /></button>
</div>
<div className="space-y-2">
<input
type="text"
value={h.label}
onChange={(e) => updateHotspot(idx, { label: e.target.value })}
placeholder="Item name..."
className="w-full bg-black/40 border border-white/10 rounded-lg px-3 py-1.5 text-xs font-bold focus:border-amber-500/50 focus:outline-none"
/>
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<label className="text-[9px] font-black uppercase tracking-widest text-gray-600">Radius</label> <span className="w-6 h-6 rounded-lg bg-amber-500 flex items-center justify-center text-[10px] font-black text-white shadow-lg">#{idx + 1}</span>
<span className="text-[10px] font-black text-slate-400 dark:text-gray-500 uppercase tracking-[0.2em] italic">Structural Node</span>
</div>
<button onClick={() => removeHotspot(idx)} className="p-2 bg-red-50 dark:bg-red-500/5 hover:bg-red-500 hover:text-white rounded-xl text-red-500 transition-all active:scale-90"><Trash2 size={14} /></button>
</div>
<div className="space-y-4">
<div className="space-y-2">
<label className="text-[9px] font-black uppercase text-slate-400 dark:text-gray-600 pl-1">Identification Label</label>
<input
type="text"
value={h.label}
onChange={(e) => updateHotspot(idx, { label: e.target.value })}
placeholder="Item name..."
className="w-full bg-white dark:bg-black/40 border border-slate-200 dark:border-white/10 rounded-xl px-4 py-2 text-xs font-black uppercase tracking-tight text-slate-800 dark:text-white focus:ring-4 focus:ring-amber-500/10 outline-none transition-all shadow-inner"
/>
</div>
<div className="space-y-2">
<div className="flex items-center justify-between pl-1">
<label className="text-[9px] font-black uppercase text-slate-400 dark:text-gray-600">Influence Radius</label>
<span className="text-[10px] font-black text-amber-600 dark:text-amber-400 font-mono">{h.radius}%</span>
</div>
<input <input
type="range" type="range"
min="2" min="2"
max="15" max="15"
value={h.radius} value={h.radius}
onChange={(e) => updateHotspot(idx, { radius: parseInt(e.target.value) })} onChange={(e) => updateHotspot(idx, { radius: parseInt(e.target.value) })}
className="flex-1 accent-amber-500 h-1 bg-white/10 rounded-lg appearance-none cursor-pointer" className="w-full h-1.5 bg-slate-200 dark:bg-white/10 rounded-lg appearance-none cursor-pointer accent-amber-500 transition-all"
/> />
<span className="text-[10px] font-bold text-gray-400 w-6">{h.radius}%</span>
</div> </div>
</div> </div>
</div> </div>
@@ -43,27 +43,28 @@ export default function MatchingBlock({ id, title, pairs, editMode, onChange }:
<div className="space-y-8" id={id}> <div className="space-y-8" id={id}>
<div className="space-y-2"> <div className="space-y-2">
{editMode ? ( {editMode ? (
<div className="space-y-2 p-6 glass border-white/5 bg-white/5 mb-4"> <div className="space-y-4 p-8 bg-slate-50 dark:bg-white/5 border border-slate-200 dark:border-white/10 rounded-[2rem] mb-6 shadow-inner relative overflow-hidden">
<label className="text-xs font-bold text-gray-500 uppercase tracking-widest">Activity Title (Optional)</label> <div className="absolute top-0 left-0 w-1 h-full bg-blue-500/40"></div>
<label className="text-[10px] font-black text-slate-400 dark:text-gray-500 uppercase tracking-[0.2em]">Activity Title (Optional)</label>
<input <input
type="text" type="text"
value={title || ""} value={title || ""}
onChange={(e) => onChange({ title: e.target.value })} onChange={(e) => onChange({ title: e.target.value })}
placeholder="e.g. Match the concepts..." placeholder="e.g. Match the concepts..."
className="w-full bg-white/5 border border-white/10 rounded-lg px-4 py-2 text-sm font-bold focus:border-blue-500/50 focus:outline-none" className="w-full bg-white dark:bg-black/40 border border-slate-200 dark:border-white/10 rounded-2xl px-6 py-3 text-sm font-black uppercase tracking-tight focus:ring-4 focus:ring-blue-500/10 focus:border-blue-500 transition-all outline-none shadow-sm"
/> />
</div> </div>
) : ( ) : (
<h3 className="text-xl font-bold border-l-4 border-blue-500 pl-4 py-1 tracking-tight text-white"> <h3 className="text-2xl font-black italic tracking-tight text-slate-900 dark:text-white uppercase border-l-4 border-blue-600 pl-6 py-1">
{title || "Concept Matching"} {title || "Concept Matching"}
</h3> </h3>
)} )}
</div> </div>
{editMode ? ( {editMode ? (
<div className="space-y-4"> <div className="space-y-6">
{pairs.map((pair, idx) => ( {pairs.map((pair, idx) => (
<div key={idx} className="flex gap-4 items-center animate-in slide-in-from-left-4 duration-300"> <div key={idx} className="flex gap-4 items-center bg-white dark:bg-white/5 p-4 rounded-[2rem] border border-slate-100 dark:border-white/10 shadow-sm animate-in slide-in-from-left-4 duration-300 group/pair">
<input <input
value={pair.left} value={pair.left}
onChange={(e) => { onChange={(e) => {
@@ -71,10 +72,10 @@ export default function MatchingBlock({ id, title, pairs, editMode, onChange }:
newPairs[idx].left = e.target.value; newPairs[idx].left = e.target.value;
onChange({ pairs: newPairs }); onChange({ pairs: newPairs });
}} }}
className="flex-1 bg-white/5 border border-white/10 rounded-xl px-4 py-3 text-sm focus:border-blue-500/50 focus:outline-none" className="flex-1 bg-slate-50 dark:bg-black/40 border border-slate-100 dark:border-white/10 rounded-xl px-5 py-3 text-sm font-bold text-slate-700 dark:text-gray-200 focus:ring-4 focus:ring-blue-500/10 transition-all outline-none shadow-inner"
placeholder="Term A" placeholder="Synaptic Origin"
/> />
<span className="text-gray-500 font-bold"></span> <div className="w-10 h-10 rounded-full bg-blue-50 dark:bg-blue-500/10 flex items-center justify-center text-blue-600 font-black shadow-inner"></div>
<input <input
value={pair.right} value={pair.right}
onChange={(e) => { onChange={(e) => {
@@ -82,47 +83,50 @@ export default function MatchingBlock({ id, title, pairs, editMode, onChange }:
newPairs[idx].right = e.target.value; newPairs[idx].right = e.target.value;
onChange({ pairs: newPairs }); onChange({ pairs: newPairs });
}} }}
className="flex-1 bg-white/5 border border-white/10 rounded-xl px-4 py-3 text-sm focus:border-blue-500/50 focus:outline-none" className="flex-1 bg-slate-50 dark:bg-black/40 border border-slate-100 dark:border-white/10 rounded-xl px-5 py-3 text-sm font-bold text-slate-700 dark:text-gray-200 focus:ring-4 focus:ring-blue-500/10 transition-all outline-none shadow-inner"
placeholder="Definition B" placeholder="Semantic Destination"
/> />
<button <button
onClick={() => { onClick={() => {
const newPairs = pairs.filter((_, i) => i !== idx); const newPairs = pairs.filter((_, i) => i !== idx);
onChange({ pairs: newPairs }); onChange({ pairs: newPairs });
}} }}
className="p-2 text-gray-500 hover:text-red-400" className="p-3 bg-red-50 dark:bg-red-500/5 hover:bg-red-500 hover:text-white rounded-xl text-red-500 transition-all active:scale-90 opacity-0 group-hover/pair:opacity-100"
> >
× <span className="text-xl">×</span>
</button> </button>
</div> </div>
))} ))}
<button <button
onClick={() => onChange({ pairs: [...pairs, { left: "", right: "" }] })} onClick={() => onChange({ pairs: [...pairs, { left: "", right: "" }] })}
className="w-full py-4 border-dashed border-2 border-white/10 text-gray-400 hover:text-white hover:border-blue-500/30 transition-all font-bold text-xs uppercase tracking-widest rounded-xl" className="w-full py-6 bg-slate-50 dark:bg-black/20 border-2 border-dashed border-slate-200 dark:border-white/10 text-slate-400 dark:text-gray-600 hover:text-blue-600 dark:hover:text-blue-400 hover:border-blue-500/40 hover:bg-white dark:hover:bg-white/5 transition-all font-black text-[10px] uppercase tracking-[0.3em] rounded-[2rem] shadow-inner"
> >
+ Add Pair + Integrate New Synaptic Pair
</button> </button>
</div> </div>
) : ( ) : (
<div className="grid grid-cols-1 md:grid-cols-2 gap-12 p-8 glass border-white/5 rounded-3xl relative"> <div className="grid grid-cols-1 md:grid-cols-2 gap-12 p-10 bg-white dark:bg-white/5 border border-slate-100 dark:border-white/10 rounded-[3rem] shadow-sm relative overflow-hidden group/match">
<div className="space-y-4"> <div className="absolute top-0 right-0 w-64 h-64 bg-blue-500/5 rounded-full blur-[80px] -translate-y-1/2 translate-x-1/2 group-hover/match:bg-blue-500/10 transition-colors"></div>
<label className="text-[10px] font-black uppercase tracking-widest text-gray-500 mb-4 block">Term</label>
<div className="space-y-6 relative z-10">
<label className="text-[10px] font-black uppercase tracking-[0.2em] text-slate-400 dark:text-gray-500 mb-6 block pl-1 italic">Synaptic Origin (Term)</label>
{pairs.map((pair, i) => ( {pairs.map((pair, i) => (
<button <button
key={i} key={i}
onClick={() => !submitted && setSelectedLeft(i)} onClick={() => !submitted && setSelectedLeft(i)}
className={`w-full p-4 rounded-xl border text-left text-sm font-bold transition-all ${selectedLeft === i ? "border-blue-500 bg-blue-500/10 text-white shadow-lg" : className={`w-full p-6 h-20 rounded-[1.5rem] border-2 text-left text-sm font-black uppercase tracking-tight transition-all duration-500 flex items-center shadow-sm ${selectedLeft === i ? "border-blue-600 bg-blue-600 text-white shadow-2xl shadow-blue-500/40 -translate-y-1 scale-[1.02]" :
matches[i] !== undefined ? "border-blue-500/20 bg-blue-500/5 text-blue-400" : matches[i] !== undefined ? "border-blue-500/20 bg-blue-50 dark:bg-blue-500/10 text-blue-600 dark:text-blue-400 ring-4 ring-blue-500/5 scale-[0.98] opacity-60" :
"border-white/5 bg-white/5 text-gray-200 hover:border-white/20" "border-slate-100 dark:border-white/10 bg-slate-50 dark:bg-black/20 text-slate-700 dark:text-gray-300 hover:border-blue-400 hover:bg-white dark:hover:bg-white/5"
}`} }`}
> >
{pair.left} <span className="truncate">{pair.left}</span>
{matches[i] !== undefined && <span className="ml-auto text-xs opacity-40"></span>}
</button> </button>
))} ))}
</div> </div>
<div className="space-y-4"> <div className="space-y-6 relative z-10">
<label className="text-[10px] font-black uppercase tracking-widest text-gray-500 mb-4 block">Definition</label> <label className="text-[10px] font-black uppercase tracking-[0.2em] text-slate-400 dark:text-gray-500 mb-6 block pl-1 italic">Semantic Target (Definition)</label>
{shuffledRight.map((item, i) => { {shuffledRight.map((item, i) => {
const matchedLeftIdx = Object.keys(matches).find(k => matches[parseInt(k)] === item.originalIdx); const matchedLeftIdx = Object.keys(matches).find(k => matches[parseInt(k)] === item.originalIdx);
const isCorrect = submitted && matchedLeftIdx !== undefined && parseInt(matchedLeftIdx) === item.originalIdx; const isCorrect = submitted && matchedLeftIdx !== undefined && parseInt(matchedLeftIdx) === item.originalIdx;
@@ -133,38 +137,38 @@ export default function MatchingBlock({ id, title, pairs, editMode, onChange }:
key={i} key={i}
disabled={selectedLeft === null || submitted} disabled={selectedLeft === null || submitted}
onClick={() => handleMatch(selectedLeft!, item.originalIdx)} onClick={() => handleMatch(selectedLeft!, item.originalIdx)}
className={`w-full p-4 rounded-xl border text-left text-sm font-bold transition-all ${selectedLeft !== null && matchedLeftIdx === undefined ? "hover:border-blue-500/50 hover:bg-white/5" : "" className={`w-full p-6 min-h-20 rounded-[1.5rem] border-2 text-left text-[11px] font-bold uppercase tracking-wider leading-relaxed transition-all duration-500 shadow-sm ${selectedLeft !== null && matchedLeftIdx === undefined ? "hover:border-blue-500/50 hover:bg-white dark:hover:bg-white/5 hover:-translate-y-0.5" : ""
} ${isCorrect ? "border-green-500 bg-green-500/20 text-green-400" : } ${isCorrect ? "border-green-500 bg-green-50 dark:bg-green-500/10 text-green-700 dark:text-green-400 shadow-xl shadow-green-500/20" :
isWrong ? "border-red-500 bg-red-500/20 text-red-100" : isWrong ? "border-red-500 bg-red-50 dark:bg-red-500/10 text-red-700 dark:text-red-100 shadow-xl shadow-red-500/20" :
matchedLeftIdx !== undefined ? "border-blue-500/30 bg-blue-500/5 text-blue-400" : matchedLeftIdx !== undefined ? "border-blue-500/20 bg-blue-50 dark:bg-blue-500/10 text-blue-600 dark:text-blue-400 ring-4 ring-blue-500/5 scale-[0.98] opacity-60" :
"border-white/5 bg-white/5 text-gray-200" "border-slate-100 dark:border-white/10 bg-slate-50 dark:bg-black/20 text-slate-700 dark:text-gray-300"
}`} }`}
> >
<div className="flex items-center justify-between"> <div className="flex items-center gap-4">
<span>{item.value}</span> <span className="flex-1">{item.value}</span>
{isCorrect && <span></span>} {isCorrect && <span className="text-xl rotate-12 transition-transform duration-700 group-hover/match:rotate-0"></span>}
{isWrong && <span></span>} {isWrong && <span className="text-xl animate-pulse"></span>}
</div> </div>
</button> </button>
); );
})} })}
</div> </div>
<div className="md:col-span-2 pt-8 border-t border-white/5"> <div className="md:col-span-2 pt-10 border-t border-slate-50 dark:border-white/5 relative z-10">
{!submitted && Object.keys(matches).length === pairs.length && ( {!submitted && Object.keys(matches).length === pairs.length && (
<button <button
onClick={() => setSubmitted(true)} onClick={() => setSubmitted(true)}
className="btn-premium w-full py-4 font-black text-xs uppercase tracking-[0.2em] shadow-xl shadow-blue-500/20" className="w-full py-6 bg-blue-600 text-white font-black text-[10px] uppercase tracking-[0.3em] shadow-2xl shadow-blue-500/40 hover:scale-[1.02] active:scale-[0.98] transition-all rounded-[2rem] flex items-center justify-center gap-4 animate-in fade-in zoom-in duration-500"
> >
Validate Matching Reconcile Semantic Network
</button> </button>
)} )}
{submitted && ( {submitted && (
<button <button
onClick={handleReset} onClick={handleReset}
className="w-full py-4 glass text-blue-400 font-black text-xs uppercase tracking-[0.2em] hover:bg-white/5 transition-all rounded-xl border-white/5" className="w-full py-6 bg-white dark:bg-white/5 border border-slate-200 dark:border-white/10 text-blue-600 dark:text-blue-400 font-black text-[10px] uppercase tracking-[0.3em] hover:bg-slate-50 transition-all rounded-[2rem] active:scale-[0.98] shadow-sm"
> >
Try Again Reset Synaptic Grid
</button> </button>
)} )}
</div> </div>
+91 -83
View File
@@ -64,100 +64,104 @@ export default function MediaBlock({ title, url, type, config, editMode, onChang
{/* Block Header */} {/* Block Header */}
<div className="space-y-2"> <div className="space-y-2">
{editMode ? ( {editMode ? (
<div className="space-y-2 p-6 glass border-white/5 bg-white/5 mb-4"> <div className="space-y-4 p-8 bg-slate-50 dark:bg-white/5 border border-slate-200 dark:border-white/10 rounded-[2rem] mb-6 shadow-inner relative overflow-hidden">
<label className="text-xs font-bold text-gray-500 uppercase tracking-widest">Activity Title (Optional)</label> <div className="absolute top-0 left-0 w-1 h-full bg-blue-500/40"></div>
<label className="text-[10px] font-black text-slate-400 dark:text-gray-500 uppercase tracking-[0.2em]">Activity Title (Optional)</label>
<input <input
type="text" type="text"
value={title || ""} value={title || ""}
onChange={(e) => onChange({ title: e.target.value })} onChange={(e) => onChange({ title: e.target.value })}
placeholder="e.g. Explainer Video, Audio Guide..." placeholder="e.g. Masterclass Stream, Acoustic Analysis..."
className="w-full bg-white/5 border border-white/10 rounded-lg px-4 py-2 text-sm font-bold focus:border-blue-500/50 focus:outline-none" className="w-full bg-white dark:bg-black/40 border border-slate-200 dark:border-white/10 rounded-2xl px-6 py-3 text-sm font-black uppercase tracking-tight focus:ring-4 focus:ring-blue-500/10 focus:border-blue-500 transition-all outline-none shadow-sm"
/> />
</div> </div>
) : ( ) : (
title && <h3 className="text-xl font-bold border-l-4 border-blue-500 pl-4 py-1 tracking-tight text-white">{title}</h3> title && <h3 className="text-2xl font-black italic tracking-tight text-slate-900 dark:text-white uppercase border-l-4 border-blue-600 pl-6 py-1">{title}</h3>
)} )}
</div> </div>
{editMode && ( {editMode && (
<div className="space-y-6 p-6 glass border-blue-500/10 mb-8 bg-blue-500/5"> <div className="space-y-8 p-10 bg-white dark:bg-white/5 border border-blue-500/10 dark:border-blue-500/20 mb-10 rounded-[2.5rem] shadow-xl shadow-blue-500/5 relative overflow-hidden">
<div className="flex items-center gap-4 mb-2"> <div className="absolute top-0 right-0 w-64 h-64 bg-blue-500/5 rounded-full blur-[80px] -translate-y-1/2 translate-x-1/2"></div>
<div className="flex items-center gap-4 relative z-10">
<button <button
onClick={() => setSourceType("url")} onClick={() => setSourceType("url")}
className={`px-4 py-2 text-[10px] uppercase font-black tracking-widest rounded-lg transition-all ${sourceType === "url" ? "bg-blue-500 text-white shadow-lg" : "text-gray-500 hover:text-white"}`} className={`px-6 py-2 text-[10px] uppercase font-black tracking-[0.2em] rounded-xl transition-all border ${sourceType === "url" ? "bg-blue-600 text-white border-blue-600 shadow-lg shadow-blue-500/30" : "bg-slate-50 dark:bg-white/5 text-slate-400 dark:text-gray-500 border-slate-100 hover:border-slate-200"}`}
> >
External URL External Stream
</button> </button>
<button <button
onClick={() => setSourceType("upload")} onClick={() => setSourceType("upload")}
className={`px-4 py-2 text-[10px] uppercase font-black tracking-widest rounded-lg transition-all ${sourceType === "upload" ? "bg-blue-500 text-white shadow-lg" : "text-gray-500 hover:text-white"}`} className={`px-6 py-2 text-[10px] uppercase font-black tracking-[0.2em] rounded-xl transition-all border ${sourceType === "upload" ? "bg-blue-600 text-white border-blue-600 shadow-lg shadow-blue-500/30" : "bg-slate-50 dark:bg-white/5 text-slate-400 dark:text-gray-500 border-slate-100 hover:border-slate-200"}`}
> >
Upload File Direct Asset
</button> </button>
</div> </div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6"> <div className="grid grid-cols-1 md:grid-cols-2 gap-6">
{sourceType === "url" ? ( <div className="grid grid-cols-1 md:grid-cols-2 gap-8 relative z-10">
<div className="space-y-2"> {sourceType === "url" ? (
<label className="text-xs font-bold text-gray-500 uppercase tracking-widest">Media URL</label> <div className="space-y-3">
<input <label className="text-[10px] font-black text-slate-400 dark:text-gray-500 uppercase tracking-widest pl-1">Media Source Locator</label>
type="text" <input
value={url.startsWith("/") ? "" : url} type="text"
onChange={(e) => onChange({ url: e.target.value })} value={url.startsWith("/") ? "" : url}
placeholder="YouTube, Vimeo or static link" onChange={(e) => onChange({ url: e.target.value })}
className="w-full bg-white/5 border border-white/10 rounded-lg px-4 py-2 text-sm focus:border-blue-500/50 focus:outline-none" placeholder="YouTube, Vimeo or static link"
/> className="w-full bg-slate-50 dark:bg-white/5 border border-slate-200 dark:border-white/10 rounded-2xl px-6 py-4 text-sm font-bold focus:ring-4 focus:ring-blue-500/10 focus:border-blue-500 transition-all outline-none shadow-inner"
</div> />
) : ( </div>
<div className="space-y-2"> ) : (
<label className="text-xs font-bold text-gray-500 uppercase tracking-widest">File Manager</label> <div className="space-y-3">
<FileUpload <label className="text-[10px] font-black text-slate-400 dark:text-gray-500 uppercase tracking-widest pl-1">Asset Pipeline</label>
currentUrl={url.startsWith("/") ? url : undefined} <FileUpload
onUploadComplete={(newUrl) => onChange({ url: newUrl })} currentUrl={url.startsWith("/") ? url : undefined}
/> onUploadComplete={(newUrl) => onChange({ url: newUrl })}
</div> />
)} </div>
)}
<div className="space-y-2"> <div className="space-y-3">
<label className="text-xs font-bold text-gray-500 uppercase tracking-widest">Playback Limit (0 = Unlimited)</label> <label className="text-[10px] font-black text-slate-400 dark:text-gray-500 uppercase tracking-widest pl-1">Play Capacity (0 = Inf)</label>
<input
type="number"
value={maxPlays}
onChange={(e) => onChange({ config: { ...config, maxPlays: parseInt(e.target.value) || 0 } })}
className="w-full bg-white/5 border border-white/10 rounded-lg px-4 py-2 text-sm focus:border-blue-500/50 focus:outline-none h-11"
/>
<p className="text-[10px] text-gray-500 uppercase leading-relaxed mt-2">Prevent content fatigue by limiting how many times a student can watch/listen.</p>
</div>
<div className="space-y-2">
<label className="text-xs font-bold text-gray-500 uppercase tracking-widest">Additional Options</label>
<div className="flex items-center gap-3 bg-white/5 border border-white/10 rounded-lg px-4 py-2 h-11">
<input <input
type="checkbox" type="number"
id={`show-transcript-${title}`} // Unique ID value={maxPlays}
checked={config.show_transcript !== false} // Default to true onChange={(e) => onChange({ config: { ...config, maxPlays: parseInt(e.target.value) || 0 } })}
onChange={(e) => onChange({ config: { ...config, show_transcript: e.target.checked } })} className="w-full bg-slate-50 dark:bg-white/5 border border-slate-200 dark:border-white/10 rounded-2xl px-6 py-4 text-sm font-bold focus:ring-4 focus:ring-blue-500/10 focus:border-blue-500 transition-all outline-none shadow-inner h-14"
className="w-4 h-4 rounded border-gray-600 text-blue-600 focus:ring-blue-500 bg-gray-700"
/> />
<label htmlFor={`show-transcript-${title}`} className="text-sm text-gray-300 font-medium select-none cursor-pointer"> <p className="text-[9px] text-slate-400 dark:text-gray-500 uppercase font-black italic pl-1 leading-relaxed">Throttle cognitive load by limiting session repetition.</p>
Show Interactive Transcript </div>
</label>
<div className="space-y-3">
<label className="text-[10px] font-black text-slate-400 dark:text-gray-500 uppercase tracking-widest pl-1">Modalities</label>
<div className="flex items-center gap-4 bg-slate-50 dark:bg-white/5 border border-slate-200 dark:border-white/10 rounded-2xl px-6 h-14 shadow-inner">
<input
type="checkbox"
id={`show-transcript-${title}`} // Unique ID
checked={config.show_transcript !== false} // Default to true
onChange={(e) => onChange({ config: { ...config, show_transcript: e.target.checked } })}
className="w-6 h-6 rounded-lg border-2 border-slate-200 dark:border-white/10 appearance-none checked:bg-blue-600 checked:border-blue-600 transition-all cursor-pointer shadow-sm"
/>
<label htmlFor={`show-transcript-${title}`} className="text-sm text-slate-600 dark:text-gray-300 font-black uppercase tracking-tight select-none cursor-pointer">
Interactive Subtitles
</label>
</div>
<p className="text-[9px] text-slate-400 dark:text-gray-500 uppercase font-black italic pl-1 leading-relaxed">Enable for accessibility; disable for auditory tests.</p>
</div> </div>
<p className="text-[10px] text-gray-500 uppercase leading-relaxed mt-2">Uncheck to hide transcription text (e.g. for listening tests).</p>
</div> </div>
</div> </div>
{/* Markers Editor */} {/* Markers Editor */}
<div className="space-y-4 pt-6 border-t border-white/10"> <div className="space-y-6 pt-10 border-t border-slate-100 dark:border-white/10 relative z-10">
<label className="text-xs font-bold text-gray-500 uppercase tracking-widest block">Interactive Questions (Timestamps)</label> <label className="text-[10px] font-black text-slate-400 dark:text-gray-500 uppercase tracking-[0.2em] block pl-1">Neural Interactivity Markers (Timestamps)</label>
<div className="space-y-2"> <div className="space-y-4">
{(config.markers || []).map((marker, idx) => ( {(config.markers || []).map((marker, idx) => (
<div key={idx} className="bg-white/5 p-4 rounded-lg border border-white/5 space-y-3"> <div key={idx} className="bg-slate-50 dark:bg-black/40 p-8 rounded-[2rem] border border-slate-200 dark:border-white/5 space-y-6 shadow-inner group/marker">
<div className="flex items-center gap-2"> <div className="flex items-center gap-6">
<span className="text-xs font-mono bg-blue-500/20 text-blue-400 px-2 py-1 rounded"> <div className="px-5 py-3 rounded-2xl bg-blue-600 text-white text-xs font-black italic shadow-lg shadow-blue-500/30">
{Math.floor(marker.timestamp / 60)}:{String(marker.timestamp % 60).padStart(2, '0')} {Math.floor(marker.timestamp / 60)}:{String(marker.timestamp % 60).padStart(2, '0')}
</span> </div>
<input <input
value={marker.question} value={marker.question}
onChange={(e) => { onChange={(e) => {
@@ -165,7 +169,8 @@ export default function MediaBlock({ title, url, type, config, editMode, onChang
newMarkers[idx].question = e.target.value; newMarkers[idx].question = e.target.value;
onChange({ config: { ...config, markers: newMarkers } }); onChange({ config: { ...config, markers: newMarkers } });
}} }}
className="text-sm bg-transparent border-b border-white/10 flex-1 focus:border-blue-500 outline-none" placeholder="Question for this timestamp..."
className="flex-1 bg-transparent border-b border-slate-200 dark:border-white/10 focus:border-blue-500 outline-none text-sm font-bold text-slate-700 dark:text-white py-2"
/> />
<button <button
onClick={() => { onClick={() => {
@@ -187,17 +192,17 @@ export default function MediaBlock({ title, url, type, config, editMode, onChang
newMarkers.splice(idx, 1); newMarkers.splice(idx, 1);
onChange({ config: { ...config, markers: newMarkers } }); onChange({ config: { ...config, markers: newMarkers } });
}} }}
className="text-red-400 hover:text-red-300 p-1" className="text-slate-400 hover:text-red-500 p-2 transition-colors"
> >
× <svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="lucide lucide-x"><path d="M18 6 6 18" /><path d="m6 6 12 12" /></svg>
</button> </button>
</div> </div>
{/* Options Management */} {/* Options Management */}
<div className="pl-14 space-y-2"> <div className="pl-24 space-y-3">
<label className="text-[10px] font-bold text-gray-500 uppercase">Options</label> <label className="text-[10px] font-black text-slate-400 dark:text-gray-500 uppercase tracking-widest">Response Vectors</label>
{marker.options.map((opt, optIdx) => ( {marker.options.map((opt, optIdx) => (
<div key={optIdx} className="flex items-center gap-2"> <div key={optIdx} className="flex items-center gap-3">
<input <input
type="radio" type="radio"
name={`correct-${idx}`} name={`correct-${idx}`}
@@ -207,7 +212,7 @@ export default function MediaBlock({ title, url, type, config, editMode, onChang
newMarkers[idx].correctIndex = optIdx; newMarkers[idx].correctIndex = optIdx;
onChange({ config: { ...config, markers: newMarkers } }); onChange({ config: { ...config, markers: newMarkers } });
}} }}
className="accent-green-500" className="w-5 h-5 accent-green-500 cursor-pointer"
/> />
<input <input
value={opt} value={opt}
@@ -216,7 +221,7 @@ export default function MediaBlock({ title, url, type, config, editMode, onChang
newMarkers[idx].options[optIdx] = e.target.value; newMarkers[idx].options[optIdx] = e.target.value;
onChange({ config: { ...config, markers: newMarkers } }); onChange({ config: { ...config, markers: newMarkers } });
}} }}
className={`text-xs bg-transparent border border-white/10 rounded px-2 py-1 flex-1 ${marker.correctIndex === optIdx ? 'text-green-400 border-green-500/30' : ''}`} className={`flex-1 bg-white dark:bg-black/20 border border-slate-200 dark:border-white/10 rounded-xl px-4 py-2 text-sm font-medium focus:ring-2 focus:ring-blue-500/20 focus:border-blue-500 transition-all outline-none shadow-sm ${marker.correctIndex === optIdx ? 'text-green-600 dark:text-green-400 border-green-500/30' : 'text-slate-700 dark:text-gray-300'}`}
/> />
<button <button
onClick={() => { onClick={() => {
@@ -227,9 +232,9 @@ export default function MediaBlock({ title, url, type, config, editMode, onChang
} }
onChange({ config: { ...config, markers: newMarkers } }); onChange({ config: { ...config, markers: newMarkers } });
}} }}
className="text-gray-600 hover:text-red-400 px-2" className="text-slate-400 hover:text-red-500 p-2 transition-colors"
> >
× <svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="lucide lucide-x"><path d="M18 6 6 18" /><path d="m6 6 12 12" /></svg>
</button> </button>
</div> </div>
))} ))}
@@ -239,27 +244,27 @@ export default function MediaBlock({ title, url, type, config, editMode, onChang
newMarkers[idx].options.push(`Option ${newMarkers[idx].options.length + 1}`); newMarkers[idx].options.push(`Option ${newMarkers[idx].options.length + 1}`);
onChange({ config: { ...config, markers: newMarkers } }); onChange({ config: { ...config, markers: newMarkers } });
}} }}
className="text-[10px] text-blue-400 hover:text-blue-300 uppercase font-bold tracking-widest mt-1" className="text-[10px] text-blue-600 hover:text-blue-700 dark:text-blue-400 dark:hover:text-blue-300 uppercase font-black tracking-widest mt-2 px-4 py-2 rounded-xl bg-blue-500/10 hover:bg-blue-500/20 transition-colors"
> >
+ Add Option + Add Response Vector
</button> </button>
</div> </div>
</div> </div>
))} ))}
</div> </div>
<div className="grid grid-cols-4 gap-2"> <div className="grid grid-cols-4 gap-4 p-6 bg-slate-100 dark:bg-white/5 rounded-2xl border border-slate-200 dark:border-white/10 shadow-inner">
<input <input
code-type="number" type="number"
placeholder="Sec" placeholder="Sec"
id="new-marker-time" id="new-marker-time"
className="col-span-1 bg-white/5 border border-white/10 rounded px-3 py-2 text-sm" className="col-span-1 bg-white dark:bg-black/40 border border-slate-200 dark:border-white/10 rounded-xl px-4 py-2 text-sm font-medium focus:ring-2 focus:ring-blue-500/20 focus:border-blue-500 transition-all outline-none shadow-sm"
/> />
<input <input
type="text" type="text"
placeholder="Question?" placeholder="Question?"
id="new-marker-question" id="new-marker-question"
className="col-span-2 bg-white/5 border border-white/10 rounded px-3 py-2 text-sm" className="col-span-2 bg-white dark:bg-black/40 border border-slate-200 dark:border-white/10 rounded-xl px-4 py-2 text-sm font-medium focus:ring-2 focus:ring-blue-500/20 focus:border-blue-500 transition-all outline-none shadow-sm"
/> />
<button <button
onClick={() => { onClick={() => {
@@ -281,12 +286,12 @@ export default function MediaBlock({ title, url, type, config, editMode, onChang
questionInput.value = ""; questionInput.value = "";
} }
}} }}
className="col-span-1 bg-blue-500 hover:bg-blue-600 text-white rounded text-xs font-bold uppercase" className="col-span-1 bg-blue-600 hover:bg-blue-700 text-white rounded-xl text-xs font-black uppercase tracking-widest shadow-lg shadow-blue-500/30 transition-all"
> >
Add Add
</button> </button>
</div> </div>
<p className="text-[10px] text-gray-500 uppercase leading-relaxed"> <p className="text-[10px] text-slate-400 dark:text-gray-500 uppercase font-black italic leading-relaxed pl-1">
Questions will pause the video at the specified second. Only simple Yes/No questions supported currently. Questions will pause the video at the specified second. Only simple Yes/No questions supported currently.
</p> </p>
</div> </div>
@@ -305,10 +310,13 @@ export default function MediaBlock({ title, url, type, config, editMode, onChang
/> />
{!editMode && maxPlays > 0 && ( {!editMode && maxPlays > 0 && (
<div className="mt-4 flex items-center justify-between px-4 py-2 glass bg-white/5 border-white/5 rounded-lg"> <div className="mt-6 flex items-center justify-between px-8 py-4 bg-white dark:bg-white/5 border border-slate-100 dark:border-white/10 rounded-[2rem] shadow-sm">
<span className="text-xs text-gray-500 uppercase font-medium">Plays Remaining</span> <div className="flex items-center gap-3">
<span className={`text-sm font-bold ${maxPlays - localPlays <= 1 ? 'text-orange-400' : 'text-blue-400'}`}> <div className="w-2 h-2 rounded-full bg-blue-500 animate-pulse"></div>
{Math.max(0, maxPlays - localPlays)} / {maxPlays} <span className="text-[10px] text-slate-400 dark:text-gray-500 uppercase font-black tracking-widest">Cognitive Reserve Remaining</span>
</div>
<span className={`text-sm font-black italic ${maxPlays - localPlays <= 1 ? 'text-orange-500' : 'text-blue-600 dark:text-blue-400'}`}>
{Math.max(0, maxPlays - localPlays)} / {maxPlays} Access Vectors
</span> </span>
</div> </div>
)} )}
@@ -39,25 +39,26 @@ export default function MemoryBlock({ id, title, pairs = [], editMode, onChange
if (!editMode) { if (!editMode) {
return ( return (
<div className="space-y-4" id={id}> <div className="space-y-8" id={id}>
<div className="flex items-center gap-4"> <div className="flex items-center gap-6">
<div className="w-12 h-12 rounded-xl bg-gradient-to-br from-indigo-500 to-purple-600 flex items-center justify-center text-white shadow-lg shadow-indigo-500/20"> <div className="w-16 h-16 rounded-[1.5rem] bg-indigo-600 dark:bg-indigo-500 flex items-center justify-center text-white shadow-2xl shadow-indigo-500/30 transform -rotate-3 hover:rotate-0 transition-transform duration-500">
<Brain size={24} /> <Brain size={32} strokeWidth={2.5} />
</div> </div>
<div> <div>
<h3 className="text-xl font-black tracking-tight text-white uppercase tracking-[0.1em]">{title || "Memory Match"}</h3> <h3 className="text-3xl font-black italic tracking-tight text-slate-900 dark:text-white uppercase leading-none">{title || "Synaptic Recall"}</h3>
<p className="text-[10px] font-black text-indigo-400 uppercase tracking-widest">Brain Training Exercise</p> <p className="text-[10px] font-black text-indigo-600 dark:text-indigo-400 uppercase tracking-[0.3em] mt-2">Mnemonic Calibration Engine</p>
</div> </div>
</div> </div>
<div className="grid grid-cols-2 sm:grid-cols-4 gap-3"> <div className="grid grid-cols-2 sm:grid-cols-4 gap-4">
{Array.from({ length: Math.min(pairs.length * 2, 8) }).map((_, i) => ( {Array.from({ length: Math.min(pairs.length * 2, 8) }).map((_, i) => (
<div key={i} className="h-24 rounded-2xl bg-white/5 border-2 border-dashed border-white/5 flex items-center justify-center"> <div key={i} className="h-28 rounded-[2rem] bg-slate-50 dark:bg-white/5 border-2 border-dashed border-slate-200 dark:border-white/10 flex items-center justify-center relative group/mcard">
<HelpCircle className="text-white/5" size={32} /> <HelpCircle className="text-slate-200 dark:text-gray-800 group-hover/mcard:text-indigo-500/40 transition-colors" size={40} strokeWidth={1} />
<div className="absolute inset-2 border border-indigo-500/5 rounded-[1.5rem]"></div>
</div> </div>
))} ))}
</div> </div>
<div className="text-center py-4 bg-indigo-500/5 rounded-2xl border border-indigo-500/10"> <div className="text-center py-6 bg-indigo-50 dark:bg-indigo-500/5 rounded-[2.5rem] border border-indigo-100 dark:border-indigo-500/10 shadow-inner">
<p className="text-[10px] font-bold text-indigo-300 uppercase tracking-widest">Memory Game with {pairs.length} pairs defined</p> <p className="text-[10px] font-black text-indigo-700 dark:text-indigo-300 uppercase tracking-[0.4em] italic">{pairs.length} Bilateral Nodes Configured</p>
</div> </div>
</div> </div>
); );
@@ -65,69 +66,71 @@ export default function MemoryBlock({ id, title, pairs = [], editMode, onChange
return ( return (
<div className="space-y-6" id={id}> <div className="space-y-6" id={id}>
<div className="p-8 glass border-white/5 bg-white/5 space-y-8 rounded-3xl"> <div className="p-10 bg-white dark:bg-white/5 border border-slate-100 dark:border-white/10 space-y-12 rounded-[3.5rem] shadow-xl shadow-slate-200/50 dark:shadow-none relative overflow-hidden group/memeditor">
<div className="space-y-2"> <div className="absolute top-0 left-0 w-80 h-80 bg-indigo-500/5 rounded-full blur-[100px] -translate-y-1/2 -translate-x-1/2 group-hover/memeditor:bg-indigo-500/10 transition-colors"></div>
<label className="text-[10px] font-black uppercase tracking-widest text-gray-500 pl-1">Game Description / Title</label>
<div className="space-y-4 relative z-10">
<label className="text-[10px] font-black uppercase tracking-[0.3em] text-slate-400 dark:text-gray-500 pl-2">System Designation (Title)</label>
<input <input
type="text" type="text"
value={title || ""} value={title || ""}
onChange={(e) => onChange({ title: e.target.value })} onChange={(e) => onChange({ title: e.target.value })}
placeholder="e.g. Vocabulary Memory Match..." placeholder="e.g. Vocabulary Memory Match..."
className="w-full bg-white/5 border border-white/10 rounded-xl px-4 py-3 text-sm font-bold focus:border-indigo-500/50 focus:outline-none transition-all" className="w-full bg-slate-50 dark:bg-black/40 border border-slate-100 dark:border-white/10 rounded-[2.5rem] px-8 py-5 text-lg font-black italic uppercase tracking-tight text-slate-800 dark:text-white focus:ring-8 focus:ring-indigo-500/5 focus:border-indigo-500 transition-all outline-none shadow-inner"
/> />
</div> </div>
<div className="space-y-4"> <div className="space-y-8 relative z-10">
<div className="flex items-center justify-between pl-1"> <div className="flex items-center justify-between px-2">
<label className="text-[10px] font-black uppercase tracking-widest text-gray-500">Pairs to Match</label> <label className="text-[10px] font-black uppercase tracking-[0.2em] text-slate-400 dark:text-gray-500 italic">Bilateral Pair Configuration</label>
<span className="text-[10px] font-bold text-indigo-400 bg-indigo-500/10 px-2 py-1 rounded-md uppercase tracking-widest">{pairs.length} Pairs</span> <span className="text-[9px] font-black text-indigo-700 dark:text-indigo-400 bg-indigo-50 dark:bg-indigo-500/10 px-4 py-2 rounded-xl border border-indigo-100 dark:border-indigo-500/20 shadow-inner uppercase tracking-widest">{pairs.length} Pairs Synchronized</span>
</div> </div>
<div className="space-y-3"> <div className="space-y-6">
{pairs.map((pair, idx) => ( {pairs.map((pair, idx) => (
<div key={pair.id || idx} className="grid grid-cols-1 sm:grid-cols-9 gap-4 p-4 bg-black/20 rounded-2xl border border-white/5 group animate-in slide-in-from-left-4 duration-300"> <div key={pair.id || idx} className="grid grid-cols-1 sm:grid-cols-10 gap-6 p-8 bg-slate-50/50 dark:bg-white/5 rounded-[2.5rem] border border-slate-100 dark:border-white/10 group animate-in slide-in-from-left-4 duration-500 hover:scale-[1.01] hover:bg-white dark:hover:bg-white/10 transition-all shadow-sm">
<div className="sm:col-span-4 space-y-1"> <div className="sm:col-span-4 space-y-3">
<span className="text-[9px] font-black uppercase tracking-tight text-gray-600 pl-1">Card A</span> <span className="text-[9px] font-black uppercase tracking-[0.2em] text-slate-400 dark:text-gray-600 pl-1 italic">Synapse Alpha</span>
<input <input
value={pair.left} value={pair.left}
onChange={(e) => updatePair(idx, { left: e.target.value })} onChange={(e) => updatePair(idx, { left: e.target.value })}
className="w-full bg-white/5 border border-white/10 rounded-xl px-4 py-2.5 text-sm font-bold focus:border-indigo-500/50 focus:outline-none transition-all" className="w-full bg-white dark:bg-black/40 border border-slate-200 dark:border-white/10 rounded-2xl px-6 py-4 text-sm font-black text-slate-800 dark:text-white focus:ring-4 focus:ring-indigo-500/10 focus:border-indigo-500 transition-all outline-none shadow-inner"
placeholder="Term, Image URL, or Word..." placeholder="Term / ID A..."
/> />
</div> </div>
<div className="sm:col-span-1 flex items-center justify-center pt-4 sm:pt-0"> <div className="sm:col-span-2 flex items-center justify-center pt-8 sm:pt-6">
<div className="w-8 h-8 rounded-full bg-indigo-500/10 border border-indigo-500/20 flex items-center justify-center"> <div className="w-12 h-12 rounded-full bg-white dark:bg-black/40 border border-slate-100 dark:border-white/10 flex items-center justify-center shadow-lg transform group-hover:rotate-180 transition-transform duration-700">
<div className="w-1 h-1 rounded-full bg-indigo-500" /> <div className="w-2 h-2 rounded-full bg-indigo-500 animate-pulse" />
</div> </div>
</div> </div>
<div className="sm:col-span-3 space-y-1"> <div className="sm:col-span-4 space-y-3 relative">
<span className="text-[9px] font-black uppercase tracking-tight text-gray-600 pl-1">Card B</span> <span className="text-[9px] font-black uppercase tracking-[0.2em] text-slate-400 dark:text-gray-600 pl-1 italic">Synapse Beta</span>
<input <div className="flex items-center gap-4">
value={pair.right} <input
onChange={(e) => updatePair(idx, { right: e.target.value })} value={pair.right}
className="w-full bg-white/5 border border-white/10 rounded-xl px-4 py-2.5 text-sm font-bold focus:border-indigo-500/50 focus:outline-none transition-all" onChange={(e) => updatePair(idx, { right: e.target.value })}
placeholder="Matching Item..." className="w-full bg-white dark:bg-black/40 border border-slate-200 dark:border-white/10 rounded-2xl px-6 py-4 text-sm font-black text-slate-800 dark:text-white focus:ring-4 focus:ring-indigo-500/10 focus:border-indigo-500 transition-all outline-none shadow-inner"
/> placeholder="Term / ID B..."
</div> />
<div className="sm:col-span-1 flex items-end justify-end pb-1 pr-1"> <button
<button onClick={() => removePair(idx)}
onClick={() => removePair(idx)} className="p-4 bg-red-50 dark:bg-red-500/5 hover:bg-red-500 hover:text-white rounded-2xl text-red-500 transition-all active:scale-90 opacity-0 group-hover:opacity-100"
className="p-2 text-gray-500 hover:text-red-400 hover:bg-red-500/10 rounded-lg transition-all" >
> <Trash2 size={18} />
<Trash2 size={16} /> </button>
</button> </div>
</div> </div>
</div> </div>
))} ))}
<button <button
onClick={addPair} onClick={addPair}
className="w-full py-6 border-dashed border-2 border-white/5 text-gray-500 hover:text-indigo-400 hover:border-indigo-500/30 hover:bg-indigo-500/5 transition-all font-black text-[10px] uppercase tracking-widest rounded-3xl flex flex-col items-center gap-2 group" className="w-full py-12 border-dashed border-4 border-slate-100 dark:border-white/10 text-slate-300 dark:text-gray-700 hover:text-indigo-600 dark:hover:text-indigo-400 hover:border-indigo-500/40 hover:bg-white dark:hover:bg-white/5 transition-all font-black text-[11px] uppercase tracking-[0.5em] rounded-[3rem] flex flex-col items-center gap-6 group/addbtn shadow-inner"
> >
<div className="p-2 rounded-xl bg-white/5 group-hover:bg-indigo-500/20 transition-all"> <div className="p-5 rounded-[1.5rem] bg-slate-50 dark:bg-white/5 group-hover/addbtn:bg-indigo-600 group-hover/addbtn:text-white group-hover/addbtn:rotate-90 transition-all duration-500 shadow-xl">
<Plus size={20} /> <Plus size={32} strokeWidth={3} />
</div> </div>
<span>Add New Memory Pair</span> <span>Initialize New Synaptic Link</span>
</button> </button>
</div> </div>
</div> </div>
@@ -1,6 +1,7 @@
"use client"; "use client";
import { useState, useMemo } from "react"; import { useState, useMemo } from "react";
import { ChevronUp, ChevronDown, X, CheckCircle2, XCircle, Clock } from "lucide-react";
interface OrderingBlockProps { interface OrderingBlockProps {
id: string; id: string;
@@ -39,29 +40,30 @@ export default function OrderingBlock({ id, title, items, editMode, onChange }:
<div className="space-y-8" id={id}> <div className="space-y-8" id={id}>
<div className="space-y-2"> <div className="space-y-2">
{editMode ? ( {editMode ? (
<div className="space-y-2 p-6 glass border-white/5 bg-white/5 mb-4"> <div className="space-y-4 p-8 bg-slate-50 dark:bg-white/5 border border-slate-200 dark:border-white/10 rounded-[2rem] mb-6 shadow-inner relative overflow-hidden">
<label className="text-xs font-bold text-gray-500 uppercase tracking-widest">Activity Title (Optional)</label> <div className="absolute top-0 left-0 w-1 h-full bg-blue-500/40"></div>
<label className="text-[10px] font-black text-slate-400 dark:text-gray-500 uppercase tracking-[0.2em]">Activity Title (Optional)</label>
<input <input
type="text" type="text"
value={title || ""} value={title || ""}
onChange={(e) => onChange({ title: e.target.value })} onChange={(e) => onChange({ title: e.target.value })}
placeholder="e.g. Sequence of Events..." placeholder="e.g. Sequence of Events..."
className="w-full bg-white/5 border border-white/10 rounded-lg px-4 py-2 text-sm font-bold focus:border-blue-500/50 focus:outline-none" className="w-full bg-white dark:bg-black/40 border border-slate-200 dark:border-white/10 rounded-2xl px-6 py-3 text-sm font-black uppercase tracking-tight focus:ring-4 focus:ring-blue-500/10 focus:border-blue-500 transition-all outline-none shadow-sm"
/> />
</div> </div>
) : ( ) : (
<h3 className="text-xl font-bold border-l-4 border-blue-500 pl-4 py-1 tracking-tight text-white"> <h3 className="text-2xl font-black italic tracking-tight text-slate-900 dark:text-white uppercase border-l-4 border-blue-600 pl-6 py-1 flex items-center gap-3">
{title || "Sequence Ordering"} <Clock className="w-6 h-6 text-blue-600" /> {title || "Sequence Ordering"}
</h3> </h3>
)} )}
</div> </div>
{editMode ? ( {editMode ? (
<div className="space-y-4"> <div className="space-y-6">
<p className="text-[10px] text-gray-500 uppercase tracking-widest mb-2">Define items in their CORRECT order:</p> <p className="text-[10px] text-slate-400 dark:text-gray-500 uppercase font-black tracking-[0.2em] pl-1">Target Chronology (Define items in CORRECT order):</p>
{items.map((item, idx) => ( {items.map((item, idx) => (
<div key={idx} className="flex gap-4 items-center animate-in slide-in-from-left-4 duration-300"> <div key={idx} className="flex gap-4 items-center bg-white dark:bg-white/5 p-4 rounded-[2rem] border border-slate-100 dark:border-white/10 shadow-sm animate-in slide-in-from-left-4 duration-300 group/ord">
<span className="text-blue-500 font-black w-6">{idx + 1}.</span> <span className="text-blue-600 font-black w-8 text-center italic">{idx + 1}.</span>
<input <input
value={item} value={item}
onChange={(e) => { onChange={(e) => {
@@ -69,10 +71,10 @@ export default function OrderingBlock({ id, title, items, editMode, onChange }:
newItems[idx] = e.target.value; newItems[idx] = e.target.value;
onChange({ items: newItems }); onChange({ items: newItems });
}} }}
className="flex-1 bg-white/5 border border-white/10 rounded-xl px-4 py-3 text-sm focus:border-blue-500/50 focus:outline-none" className="flex-1 bg-slate-50 dark:bg-black/40 border border-slate-100 dark:border-white/10 rounded-2xl px-6 py-3 text-sm font-bold text-slate-700 dark:text-gray-200 focus:ring-4 focus:ring-blue-500/10 transition-all outline-none shadow-inner"
placeholder={`Step ${idx + 1}`} placeholder={`Event Marker ${idx + 1}`}
/> />
<div className="flex gap-2"> <div className="flex gap-1">
<button <button
disabled={idx === 0} disabled={idx === 0}
onClick={() => { onClick={() => {
@@ -80,9 +82,9 @@ export default function OrderingBlock({ id, title, items, editMode, onChange }:
[newItems[idx], newItems[idx - 1]] = [newItems[idx - 1], newItems[idx]]; [newItems[idx], newItems[idx - 1]] = [newItems[idx - 1], newItems[idx]];
onChange({ items: newItems }); onChange({ items: newItems });
}} }}
className="p-2 text-gray-500 hover:text-white disabled:opacity-20" className="p-3 bg-slate-50 dark:bg-white/5 hover:bg-blue-600 hover:text-white rounded-xl text-slate-400 dark:text-gray-500 transition-all disabled:opacity-5 disabled:hover:bg-transparent"
> >
<ChevronUp size={16} strokeWidth={3} />
</button> </button>
<button <button
disabled={idx === items.length - 1} disabled={idx === items.length - 1}
@@ -91,9 +93,9 @@ export default function OrderingBlock({ id, title, items, editMode, onChange }:
[newItems[idx], newItems[idx + 1]] = [newItems[idx + 1], newItems[idx]]; [newItems[idx], newItems[idx + 1]] = [newItems[idx + 1], newItems[idx]];
onChange({ items: newItems }); onChange({ items: newItems });
}} }}
className="p-2 text-gray-500 hover:text-white disabled:opacity-20" className="p-3 bg-slate-50 dark:bg-white/5 hover:bg-blue-600 hover:text-white rounded-xl text-slate-400 dark:text-gray-500 transition-all disabled:opacity-5 disabled:hover:bg-transparent"
> >
<ChevronDown size={16} strokeWidth={3} />
</button> </button>
</div> </div>
<button <button
@@ -101,25 +103,27 @@ export default function OrderingBlock({ id, title, items, editMode, onChange }:
const newItems = items.filter((_, i) => i !== idx); const newItems = items.filter((_, i) => i !== idx);
onChange({ items: newItems }); onChange({ items: newItems });
}} }}
className="p-2 text-gray-500 hover:text-red-400" className="p-3 bg-red-50 dark:bg-red-500/5 hover:bg-red-500 hover:text-white rounded-xl text-red-500 transition-all opacity-0 group-hover/ord:opacity-100"
> >
× <X size={18} strokeWidth={3} />
</button> </button>
</div> </div>
))} ))}
<button <button
onClick={() => onChange({ items: [...items, ""] })} onClick={() => onChange({ items: [...items, ""] })}
className="w-full py-4 border-dashed border-2 border-white/10 text-gray-400 hover:text-white hover:border-blue-500/30 transition-all font-bold text-xs uppercase tracking-widest rounded-xl" className="w-full py-6 bg-slate-50 dark:bg-black/20 border-2 border-dashed border-slate-200 dark:border-white/10 text-slate-400 dark:text-gray-600 hover:text-blue-600 dark:hover:text-blue-400 hover:border-blue-500/40 hover:bg-white dark:hover:bg-white/5 transition-all font-black text-[10px] uppercase tracking-[0.3em] rounded-[2rem] shadow-inner"
> >
+ Add Step + Append Temporal Marker
</button> </button>
</div> </div>
) : ( ) : (
<div className="space-y-8 p-8 glass border-white/5 rounded-3xl"> <div className="space-y-12 p-10 bg-white dark:bg-white/5 border border-slate-100 dark:border-white/10 rounded-[3rem] shadow-sm relative overflow-hidden group/ordplay">
<div className="grid grid-cols-1 md:grid-cols-2 gap-12"> <div className="absolute top-0 right-0 w-64 h-64 bg-indigo-500/5 rounded-full blur-[80px] -translate-y-1/2 translate-x-1/2 group-hover/ordplay:bg-indigo-500/10 transition-colors"></div>
<div className="space-y-4">
<label className="text-[10px] font-black uppercase tracking-widest text-gray-500 mb-4 block">Available Items</label> <div className="grid grid-cols-1 md:grid-cols-2 gap-16 relative z-10">
<div className="flex flex-wrap gap-3"> <div className="space-y-8">
<label className="text-[10px] font-black uppercase tracking-[0.2em] text-slate-400 dark:text-gray-500 mb-8 block pl-1 italic">Synaptic Shards (Available Items)</label>
<div className="flex flex-wrap gap-4">
{shuffledItems.map((item, i) => { {shuffledItems.map((item, i) => {
const isPicked = userOrder.includes(item.originalIdx); const isPicked = userOrder.includes(item.originalIdx);
return ( return (
@@ -127,8 +131,8 @@ export default function OrderingBlock({ id, title, items, editMode, onChange }:
key={i} key={i}
disabled={isPicked || submitted} disabled={isPicked || submitted}
onClick={() => handlePick(item.originalIdx)} onClick={() => handlePick(item.originalIdx)}
className={`px-6 py-3 rounded-full border text-sm font-bold transition-all ${isPicked ? "opacity-20 grayscale border-white/5 bg-white/5" : className={`px-8 py-4 rounded-full border-2 text-[11px] font-black uppercase tracking-widest transition-all duration-500 shadow-sm ${isPicked ? "opacity-10 grayscale border-slate-100 bg-slate-50 -translate-y-1" :
"border-white/10 bg-white/5 text-gray-200 hover:border-blue-500/50 hover:bg-blue-500/5" "border-slate-100 dark:border-white/10 bg-slate-50 dark:bg-black/20 text-slate-700 dark:text-gray-300 hover:border-blue-600 hover:bg-blue-600 hover:text-white hover:shadow-2xl hover:shadow-blue-500/40 hover:-translate-y-1"
}`} }`}
> >
{item.value} {item.value}
@@ -138,10 +142,14 @@ export default function OrderingBlock({ id, title, items, editMode, onChange }:
</div> </div>
</div> </div>
<div className="space-y-4"> <div className="space-y-8">
<label className="text-[10px] font-black uppercase tracking-widest text-gray-500 mb-4 block">Your Sequence</label> <label className="text-[10px] font-black uppercase tracking-[0.2em] text-slate-400 dark:text-gray-500 mb-8 block pl-1 italic">Reconstructed Timeline</label>
<div className="space-y-3"> <div className="space-y-4">
{userOrder.length === 0 && <p className="text-xs text-gray-600 italic py-4">Click items to build the sequence...</p>} {userOrder.length === 0 && (
<div className="bg-slate-50 dark:bg-black/20 border-2 border-dashed border-slate-200 dark:border-white/10 p-12 rounded-[2.5rem] text-center">
<p className="text-[9px] text-slate-400 dark:text-gray-600 uppercase font-black tracking-widest italic pt-2">Sequencing standby: Capture shards to proceed...</p>
</div>
)}
{userOrder.map((idx, i) => { {userOrder.map((idx, i) => {
const isItemCorrect = submitted && idx === i; const isItemCorrect = submitted && idx === i;
const isItemWrong = submitted && idx !== i; const isItemWrong = submitted && idx !== i;
@@ -150,16 +158,16 @@ export default function OrderingBlock({ id, title, items, editMode, onChange }:
<div <div
key={i} key={i}
onClick={() => !submitted && handlePick(idx)} onClick={() => !submitted && handlePick(idx)}
className={`flex items-center gap-4 p-4 rounded-xl border text-sm font-bold transition-all cursor-pointer ${isItemCorrect ? "border-green-500 bg-green-500/20 text-green-400" : className={`flex items-center gap-6 p-6 rounded-[1.5rem] border-2 text-[11px] font-black uppercase tracking-[0.1em] transition-all duration-500 cursor-pointer shadow-sm ${isItemCorrect ? "border-green-500 bg-green-50 dark:bg-green-500/10 text-green-700 dark:text-green-400 shadow-xl shadow-green-500/20" :
isItemWrong ? "border-red-500 bg-red-500/20 text-red-100" : isItemWrong ? "border-red-500 bg-red-50 dark:bg-red-500/10 text-red-700 dark:text-red-100 shadow-xl shadow-red-500/20 translate-x-1" :
"border-blue-500/30 bg-blue-500/5 text-blue-400 hover:bg-blue-500/10" "border-blue-500/20 bg-blue-50 dark:bg-blue-500/5 text-blue-600 dark:text-blue-400 hover:bg-blue-600 hover:text-white hover:border-blue-600 hover:shadow-2xl hover:shadow-blue-500/30"
}`} }`}
> >
<span className="opacity-50 text-xs">{i + 1}.</span> <span className="opacity-40 text-xs italic">MARK {i + 1}</span>
<span className="flex-1">{items[idx]}</span> <span className="flex-1">{items[idx]}</span>
{!submitted && <span className="text-xs opacity-50">×</span>} {!submitted && <span className="text-[10px] opacity-30 hover:opacity-100 transition-opacity">SCRAPE</span>}
{isItemCorrect && <span></span>} {isItemCorrect && <CheckCircle2 className="w-6 h-6 text-green-500" />}
{isItemWrong && <span></span>} {isItemWrong && <XCircle className="w-6 h-6 text-red-500 animate-pulse" />}
</div> </div>
); );
})} })}
@@ -167,21 +175,21 @@ export default function OrderingBlock({ id, title, items, editMode, onChange }:
</div> </div>
</div> </div>
<div className="pt-8 border-t border-white/5"> <div className="pt-10 border-t border-slate-50 dark:border-white/5 relative z-10">
{!submitted && userOrder.length === items.length && ( {!submitted && userOrder.length === items.length && (
<button <button
onClick={() => setSubmitted(true)} onClick={() => setSubmitted(true)}
className="btn-premium w-full py-4 font-black text-xs uppercase tracking-[0.2em] shadow-xl shadow-blue-500/20" className="w-full py-6 bg-blue-600 text-white font-black text-[10px] uppercase tracking-[0.3em] shadow-2xl shadow-blue-500/40 hover:scale-[1.02] active:scale-[0.98] transition-all rounded-[2rem] flex items-center justify-center gap-4 animate-in fade-in zoom-in duration-500"
> >
Validate Sequence Validate Temporal Cohesion
</button> </button>
)} )}
{submitted && ( {submitted && (
<button <button
onClick={handleReset} onClick={handleReset}
className="w-full py-4 glass text-blue-400 font-black text-xs uppercase tracking-[0.2em] hover:bg-white/5 transition-all rounded-xl border-white/5" className="w-full py-6 bg-white dark:bg-white/5 border border-slate-200 dark:border-white/10 text-blue-600 dark:text-blue-400 font-black text-[10px] uppercase tracking-[0.3em] hover:bg-slate-50 transition-all rounded-[2rem] active:scale-[0.98] shadow-sm"
> >
Try Again Deconstruct Sequence
</button> </button>
)} )}
</div> </div>
@@ -1,6 +1,7 @@
"use client"; "use client";
import { useState } from "react"; import { useState } from "react";
import { Users, Send, CheckCircle2, ClipboardList, Info } from "lucide-react";
interface PeerReviewBlockProps { interface PeerReviewBlockProps {
id: string; id: string;
@@ -16,72 +17,84 @@ export default function PeerReviewBlock({ id, title, prompt, reviewCriteria, edi
<div className="space-y-8" id={id}> <div className="space-y-8" id={id}>
<div className="space-y-2"> <div className="space-y-2">
{editMode ? ( {editMode ? (
<div className="space-y-2 p-6 glass border-white/5 bg-white/5 mb-4"> <div className="space-y-4 p-8 bg-slate-50 dark:bg-white/5 border border-slate-200 dark:border-white/10 rounded-[2rem] mb-6 shadow-inner relative overflow-hidden">
<label className="text-xs font-bold text-gray-500 uppercase tracking-widest">Activity Title (Optional)</label> <div className="absolute top-0 left-0 w-1 h-full bg-purple-500/40"></div>
<label className="text-[10px] font-black text-slate-400 dark:text-gray-500 uppercase tracking-[0.2em]">Activity Title (Optional)</label>
<input <input
type="text" type="text"
value={title || ""} value={title || ""}
onChange={(e) => onChange({ title: e.target.value })} onChange={(e) => onChange({ title: e.target.value })}
placeholder="e.g. Final Project Submission..." placeholder="e.g. Final Project Submission..."
className="w-full bg-white/5 border border-white/10 rounded-lg px-4 py-2 text-sm font-bold focus:border-blue-500/50 focus:outline-none" className="w-full bg-white dark:bg-black/40 border border-slate-200 dark:border-white/10 rounded-2xl px-6 py-3 text-sm font-black uppercase tracking-tight focus:ring-4 focus:ring-purple-500/10 focus:border-purple-500 transition-all outline-none shadow-sm"
/> />
</div> </div>
) : ( ) : (
<h3 className="text-xl font-bold border-l-4 border-purple-500 pl-4 py-1 tracking-tight text-white flex items-center gap-2"> <h3 className="text-2xl font-black italic tracking-tight text-slate-900 dark:text-white uppercase border-l-4 border-purple-600 pl-6 py-1 flex items-center gap-3">
<span>👥</span> {title || "Peer Assessment"} <Users className="w-6 h-6 text-purple-600" /> {title || "Peer Assessment"}
</h3> </h3>
)} )}
</div> </div>
{editMode ? ( {editMode ? (
<div className="space-y-6"> <div className="space-y-8">
<div className="p-6 glass border-white/5 space-y-4"> <div className="p-8 bg-white dark:bg-white/5 border border-slate-100 dark:border-white/10 rounded-[2.5rem] space-y-4 shadow-sm relative overflow-hidden group/instr">
<label className="text-xs font-bold text-gray-500 uppercase tracking-widest">Assignment Instructions</label> <div className="absolute top-0 right-0 w-32 h-32 bg-blue-500/5 rounded-full blur-3xl -translate-y-1/2 translate-x-1/2"></div>
<label className="text-[10px] font-black text-slate-400 dark:text-gray-500 uppercase tracking-[0.2em] pl-1">Assignment Directive (Instructions)</label>
<textarea <textarea
value={prompt} value={prompt}
onChange={(e) => onChange({ prompt: e.target.value })} onChange={(e) => onChange({ prompt: e.target.value })}
className="w-full bg-white/5 border border-white/10 rounded-xl p-4 min-h-[100px] text-lg font-medium focus:outline-none focus:border-blue-500/50 transition-all" className="w-full bg-slate-50 dark:bg-black/40 border border-slate-100 dark:border-white/10 rounded-2xl p-6 min-h-[120px] text-lg font-black uppercase tracking-tight text-slate-800 dark:text-white focus:outline-none focus:ring-4 focus:ring-blue-500/10 transition-all shadow-inner placeholder:opacity-20"
placeholder="Describe what the student needs to submit (e.g. 'Write a 500-word essay about...')" placeholder="DESCRIBE THE CORE SUBMISSION PARAMETERS..."
/> />
</div> </div>
<div className="p-6 glass border-white/5 space-y-4"> <div className="p-8 bg-white dark:bg-white/5 border border-slate-100 dark:border-white/10 rounded-[2.5rem] space-y-4 shadow-sm relative overflow-hidden group/rubric">
<label className="text-xs font-bold text-gray-500 uppercase tracking-widest">Review Criteria (Rubric)</label> <div className="absolute bottom-0 left-0 w-32 h-32 bg-purple-500/5 rounded-full blur-3xl translate-y-1/2 -translate-x-1/2"></div>
<label className="text-[10px] font-black text-slate-400 dark:text-gray-500 uppercase tracking-[0.2em] pl-1">Evaluative Matrix (Rubric)</label>
<textarea <textarea
value={reviewCriteria || ""} value={reviewCriteria || ""}
onChange={(e) => onChange({ reviewCriteria: e.target.value })} onChange={(e) => onChange({ reviewCriteria: e.target.value })}
className="w-full bg-white/5 border border-white/10 rounded-xl p-4 min-h-[100px] text-sm font-medium focus:outline-none focus:border-blue-500/50 transition-all" className="w-full bg-slate-50 dark:bg-black/40 border border-slate-100 dark:border-white/10 rounded-2xl p-6 min-h-[120px] text-sm font-bold text-slate-700 dark:text-gray-300 focus:outline-none focus:ring-4 focus:ring-purple-500/10 transition-all shadow-inner"
placeholder="Guide the reviewer on how to evaluate the submission..." placeholder="Guide the peer reviewers on evaluative heuristics..."
/> />
<p className="text-[10px] text-gray-500 uppercase tracking-wider">Instructions for the student who will review this work.</p> <p className="text-[9px] text-slate-400 dark:text-gray-600 uppercase font-black italic pl-1 italic">Structural guidance for cross-student validation.</p>
</div> </div>
</div> </div>
) : ( ) : (
<div className="p-8 glass border-white/5 rounded-3xl space-y-8"> <div className="p-10 bg-white dark:bg-white/5 border border-slate-100 dark:border-white/10 rounded-[3rem] space-y-12 shadow-sm relative overflow-hidden group/peerplay">
<div className="space-y-4"> <div className="absolute top-0 right-0 w-96 h-96 bg-purple-500/5 rounded-full blur-[120px] -translate-y-1/2 translate-x-1/2 group-hover/peerplay:bg-purple-500/10 transition-colors"></div>
<h4 className="text-xs font-black uppercase tracking-widest text-gray-500">Instructions</h4>
<p className="text-lg text-gray-200 leading-relaxed whitespace-pre-wrap">{prompt || "Submit your work below."}</p> <div className="space-y-6 relative z-10">
<div className="flex items-center gap-2">
<Info className="w-4 h-4 text-slate-300 dark:text-gray-600" />
<label className="text-[10px] font-black uppercase tracking-[0.3em] text-slate-300 dark:text-gray-600 italic">Instructional Directive</label>
</div>
<p className="text-2xl font-black text-slate-800 dark:text-gray-100 leading-tight uppercase italic tracking-tight">{prompt || "Awaiting submission parameters..."}</p>
</div> </div>
<div className="p-6 bg-white/5 rounded-2xl border border-white/10"> <div className="p-10 bg-slate-50 dark:bg-white/5 border-2 border-dashed border-slate-200 dark:border-white/10 rounded-[2.5rem] relative z-10 hover:border-blue-500 transition-colors duration-500">
<div className="flex items-center gap-3 mb-4"> <div className="flex items-start gap-8 mb-8">
<div className="w-10 h-10 rounded-full bg-purple-500/20 flex items-center justify-center text-purple-400"> <div className="p-5 rounded-2xl bg-blue-100 dark:bg-blue-500/10 text-blue-700 dark:text-blue-400 shadow-xl ring-4 ring-blue-500/10 animate-bounce">
📤 <Send className="w-8 h-8" />
</div> </div>
<div> <div className="space-y-1">
<h5 className="font-bold text-sm">Student Submission Area</h5> <h5 className="text-[11px] font-black uppercase tracking-[0.2em] text-slate-900 dark:text-white">Submission Terminal Interface</h5>
<p className="text-xs text-gray-500">Students will see a text area here to submit their work.</p> <p className="text-[9px] text-slate-400 dark:text-gray-500 uppercase font-black tracking-widest italic pt-1">Active node for student document upload and textual stream.</p>
</div> </div>
</div> </div>
<div className="h-32 bg-black/20 rounded-xl border border-white/5 flex items-center justify-center text-gray-600 text-sm italic"> <div className="h-40 bg-white dark:bg-black/40 rounded-[2rem] border border-slate-100 dark:border-white/10 flex flex-col items-center justify-center text-slate-400 dark:text-gray-600 text-[10px] font-black uppercase tracking-[0.4em] italic shadow-inner">
[Submission Interface Preview] <div className="w-12 h-1 bg-slate-100 dark:bg-white/5 rounded-full mb-6"></div>
[ Simulation: Subjunctive Flux ]
</div> </div>
</div> </div>
{reviewCriteria && ( {reviewCriteria && (
<div className="space-y-2 border-t border-white/5 pt-6"> <div className="space-y-6 border-t border-slate-50 dark:border-white/5 pt-10 relative z-10">
<h4 className="text-xs font-black uppercase tracking-widest text-gray-500">Review Criteria</h4> <div className="flex items-center gap-2">
<p className="text-sm text-gray-400 whitespace-pre-wrap">{reviewCriteria}</p> <ClipboardList className="w-4 h-4 text-slate-300 dark:text-gray-600" />
<label className="text-[10px] font-black uppercase tracking-[0.3em] text-slate-300 dark:text-gray-600 italic">Heuristic Evaluative Metrics (Rubric)</label>
</div>
<p className="text-sm font-bold text-slate-600 dark:text-gray-400 whitespace-pre-wrap leading-relaxed">{reviewCriteria}</p>
</div> </div>
)} )}
</div> </div>
+102 -79
View File
@@ -1,6 +1,7 @@
"use client"; "use client";
import { useState } from "react"; import { useState } from "react";
import { Check, CheckCircle2, Plus, Target } from "lucide-react";
interface QuizQuestion { interface QuizQuestion {
id: string; id: string;
@@ -75,19 +76,20 @@ export default function QuizBlock({ id, title, quizData, editMode, onChange }: Q
{/* Block Header */} {/* Block Header */}
<div className="space-y-2"> <div className="space-y-2">
{editMode ? ( {editMode ? (
<div className="space-y-2 p-6 glass border-white/5 bg-white/5 mb-4"> <div className="space-y-4 p-8 bg-slate-50 dark:bg-white/5 border border-slate-200 dark:border-white/10 rounded-[2rem] mb-6 shadow-inner relative overflow-hidden">
<label className="text-xs font-bold text-gray-500 uppercase tracking-widest">Activity Title (Optional)</label> <div className="absolute top-0 left-0 w-1 h-full bg-blue-500/40"></div>
<label className="text-[10px] font-black text-slate-400 dark:text-gray-500 uppercase tracking-[0.2em]">Activity Title (Optional)</label>
<input <input
type="text" type="text"
value={title || ""} value={title || ""}
onChange={(e) => onChange({ title: e.target.value })} onChange={(e) => onChange({ title: e.target.value })}
placeholder="e.g. Final Evaluation, Knowledge Check..." placeholder="e.g. Mastery Challenge, Knowledge Synthesis..."
className="w-full bg-white/5 border border-white/10 rounded-lg px-4 py-2 text-sm font-bold focus:border-blue-500/50 focus:outline-none" className="w-full bg-white dark:bg-black/40 border border-slate-200 dark:border-white/10 rounded-2xl px-6 py-3 text-sm font-black uppercase tracking-tight focus:ring-4 focus:ring-blue-500/10 focus:border-blue-500 transition-all outline-none shadow-sm"
/> />
</div> </div>
) : ( ) : (
<h3 className="text-xl font-bold border-l-4 border-blue-500 pl-4 py-1 tracking-tight text-white"> <h3 className="text-2xl font-black italic tracking-tight text-slate-900 dark:text-white uppercase border-l-4 border-blue-600 pl-6 py-1">
{title || "Knowledge Check"} {title || "Knowledge Synthesis"}
</h3> </h3>
)} )}
</div> </div>
@@ -95,26 +97,26 @@ export default function QuizBlock({ id, title, quizData, editMode, onChange }: Q
{editMode ? ( {editMode ? (
<div className="space-y-6"> <div className="space-y-6">
{questions.map((q, idx) => ( {questions.map((q, idx) => (
<div key={q.id} className="p-8 glass border-white/5 space-y-6 rounded-3xl relative group/question animate-in slide-in-from-left-4 duration-500"> <div key={q.id} className="p-10 bg-white dark:bg-white/5 border border-slate-200 dark:border-white/10 space-y-8 rounded-[2.5rem] relative group/question animate-in slide-in-from-bottom-4 duration-500 shadow-sm hover:shadow-xl hover:border-blue-500/20 transition-all">
<div className="flex items-center justify-between gap-4"> <div className="flex items-center justify-between gap-6">
<div className="flex bg-white/5 rounded-lg p-1 border border-white/5"> <div className="flex bg-slate-100 dark:bg-white/5 rounded-2xl p-1.5 border border-slate-200 dark:border-white/5 shadow-inner">
<button <button
onClick={() => updateQuestion(idx, { type: 'multiple-choice', correct: [q.correct?.[0] || 0] })} onClick={() => updateQuestion(idx, { type: 'multiple-choice', correct: [q.correct?.[0] || 0] })}
className={`px-3 py-1.5 text-[9px] uppercase font-black tracking-widest rounded-md transition-all ${q.type === 'multiple-choice' ? "bg-blue-500 text-white shadow-lg" : "text-gray-500 hover:text-white"}`} className={`px-4 py-2 text-[9px] uppercase font-black tracking-widest rounded-xl transition-all ${q.type === 'multiple-choice' ? "bg-white dark:bg-blue-600 text-slate-900 dark:text-white shadow-md" : "text-slate-400 hover:text-slate-600"}`}
> >
MCQ Choice
</button> </button>
<button <button
onClick={() => updateQuestion(idx, { type: 'multiple-select', correct: [q.correct?.[0] || 0] })} onClick={() => updateQuestion(idx, { type: 'multiple-select', correct: [q.correct?.[0] || 0] })}
className={`px-3 py-1.5 text-[9px] uppercase font-black tracking-widest rounded-md transition-all ${q.type === 'multiple-select' ? "bg-blue-500 text-white shadow-lg" : "text-gray-500 hover:text-white"}`} className={`px-4 py-2 text-[9px] uppercase font-black tracking-widest rounded-xl transition-all ${q.type === 'multiple-select' ? "bg-white dark:bg-blue-600 text-slate-900 dark:text-white shadow-md" : "text-slate-400 hover:text-slate-600"}`}
> >
MSQ Multi
</button> </button>
<button <button
onClick={() => updateQuestion(idx, { type: 'true-false', options: ["True", "False"], correct: [0] })} onClick={() => updateQuestion(idx, { type: 'true-false', options: ["True", "False"], correct: [0] })}
className={`px-3 py-1.5 text-[9px] uppercase font-black tracking-widest rounded-md transition-all ${q.type === 'true-false' ? "bg-blue-500 text-white shadow-lg" : "text-gray-500 hover:text-white"}`} className={`px-4 py-2 text-[9px] uppercase font-black tracking-widest rounded-xl transition-all ${q.type === 'true-false' ? "bg-white dark:bg-blue-600 text-slate-900 dark:text-white shadow-md" : "text-slate-400 hover:text-slate-600"}`}
> >
T / F Binary
</button> </button>
</div> </div>
<button <button
@@ -122,73 +124,86 @@ export default function QuizBlock({ id, title, quizData, editMode, onChange }: Q
const newQuestions = questions.filter((_, i) => i !== idx); const newQuestions = questions.filter((_, i) => i !== idx);
onChange({ quiz_data: { questions: newQuestions } }); onChange({ quiz_data: { questions: newQuestions } });
}} }}
className="p-2 text-gray-500 hover:text-red-400 transition-colors" className="w-10 h-10 rounded-full bg-red-50 dark:bg-red-500/10 text-red-400 hover:bg-red-500 hover:text-white transition-all flex items-center justify-center border border-red-100 dark:border-transparent active:scale-90"
> >
<span className="text-xl">×</span> <span className="text-xl">×</span>
</button> </button>
</div> </div>
<input
value={q.question}
onChange={(e) => updateQuestion(idx, { question: e.target.value })}
className="w-full bg-white/5 border border-white/10 rounded-xl p-4 text-lg font-bold focus:outline-none focus:border-blue-500/50 transition-all placeholder:text-gray-600"
placeholder="What is the question?"
/>
<div className="space-y-3"> <div className="space-y-3">
<label className="text-[10px] font-black text-slate-400 dark:text-gray-500 uppercase tracking-widest pl-1">Inquiry Description</label>
<textarea
value={q.question}
onChange={(e) => updateQuestion(idx, { question: e.target.value })}
className="w-full bg-slate-50 dark:bg-black/40 border border-slate-200 dark:border-white/10 rounded-2xl p-6 text-lg font-black tracking-tight focus:ring-4 focus:ring-blue-500/10 focus:border-blue-500 transition-all placeholder:text-slate-300 resize-none shadow-inner h-24"
placeholder="What is the core problem being evaluated?"
/>
</div>
<div className="space-y-4">
<label className="text-[10px] font-black text-slate-400 dark:text-gray-500 uppercase tracking-widest pl-1">Configuration Matrix</label>
{q.type === 'true-false' ? ( {q.type === 'true-false' ? (
<div className="flex gap-4"> <div className="flex gap-4">
{["True", "False"].map((opt, oIdx) => ( {["True", "False"].map((opt, oIdx) => (
<button <button
key={oIdx} key={oIdx}
onClick={() => updateQuestion(idx, { correct: [oIdx] })} onClick={() => updateQuestion(idx, { correct: [oIdx] })}
className={`flex-1 py-4 rounded-xl border-2 transition-all font-black text-xs uppercase tracking-widest ${q.correct?.includes(oIdx) ? "border-blue-500 bg-blue-500/10 text-white" : "border-white/5 bg-white/5 text-gray-500"}`} className={`flex-1 py-5 rounded-2xl border-2 transition-all font-black text-[10px] uppercase tracking-[0.2em] shadow-sm ${q.correct?.includes(oIdx) ? "border-blue-600 bg-blue-50 dark:bg-blue-500/10 text-blue-700 dark:text-blue-400 scale-[1.02] shadow-blue-500/10" : "border-slate-100 dark:border-white/5 bg-slate-50 dark:bg-white/5 text-slate-400 dark:text-gray-500 hover:border-slate-200"}`}
> >
{opt} {opt}
</button> </button>
))} ))}
</div> </div>
) : ( ) : (
q.options?.map((opt, oIdx) => ( <div className="space-y-3">
<div key={oIdx} className="flex gap-3 items-center group/opt"> {q.options?.map((opt, oIdx) => (
<input <div key={oIdx} className="flex gap-4 items-center group/opt">
type={q.type === 'multiple-select' ? "checkbox" : "radio"} <div className="relative flex items-center justify-center">
checked={q.correct?.includes(oIdx)} <input
onChange={() => toggleCorrectOption(idx, oIdx, q.type === 'multiple-select')} type={q.type === 'multiple-select' ? "checkbox" : "radio"}
className="w-5 h-5 accent-blue-500 cursor-pointer" checked={q.correct?.includes(oIdx)}
/> onChange={() => toggleCorrectOption(idx, oIdx, q.type === 'multiple-select')}
<input className="w-6 h-6 rounded-lg border-2 border-slate-200 dark:border-white/10 appearance-none checked:bg-blue-600 checked:border-blue-600 transition-all cursor-pointer shadow-sm"
value={opt} />
onChange={(e) => { {q.correct?.includes(oIdx) && (
const newOpts = [...q.options]; <div className="absolute pointer-events-none text-white text-[10px]">
newOpts[oIdx] = e.target.value; <Check size={14} />
updateQuestion(idx, { options: newOpts }); </div>
}} )}
className="flex-1 bg-white/5 border border-white/10 rounded-xl px-4 py-3 text-sm font-medium focus:outline-none focus:border-blue-500/30 transition-all" </div>
placeholder={`Option ${oIdx + 1}`} <input
/> value={opt}
{q.options.length > 2 && ( onChange={(e) => {
<button const newOpts = [...q.options];
onClick={() => { newOpts[oIdx] = e.target.value;
const newOpts = q.options.filter((_, i) => i !== oIdx); updateQuestion(idx, { options: newOpts });
const newCorrect = q.correct?.filter(i => i !== oIdx).map(i => i > oIdx ? i - 1 : i);
updateQuestion(idx, { options: newOpts, correct: newCorrect?.length ? newCorrect : [0] });
}} }}
className="opacity-0 group-hover/opt:opacity-100 p-2 text-gray-500 hover:text-red-400 transition-all" className="flex-1 bg-slate-50 dark:bg-white/5 border border-slate-200 dark:border-white/10 rounded-2xl px-6 py-4 text-sm font-bold focus:ring-4 focus:ring-blue-500/5 focus:border-blue-500 transition-all outline-none shadow-inner"
> placeholder={`Option Vector ${String.fromCharCode(65 + oIdx)}`}
× />
</button> {q.options.length > 2 && (
)} <button
</div> onClick={() => {
)) const newOpts = q.options.filter((_, i) => i !== oIdx);
const newCorrect = q.correct?.filter(i => i !== oIdx).map(i => i > oIdx ? i - 1 : i);
updateQuestion(idx, { options: newOpts, correct: newCorrect?.length ? newCorrect : [0] });
}}
className="w-10 h-10 rounded-xl bg-slate-50 dark:bg-white/5 text-slate-300 hover:text-red-500 transition-all border border-slate-200 dark:border-transparent flex items-center justify-center active:scale-90"
>
×
</button>
)}
</div>
))}
</div>
)} )}
{q.type !== 'true-false' && ( {q.type !== 'true-false' && (
<button <button
onClick={() => updateQuestion(idx, { options: [...q.options, `Option ${q.options.length + 1}`] })} onClick={() => updateQuestion(idx, { options: [...q.options, `Option ${q.options.length + 1}`] })}
className="text-[10px] font-black uppercase tracking-widest text-blue-500/50 hover:text-blue-500 transition-colors pl-8 mt-2" className="text-[10px] font-black uppercase tracking-[0.2em] text-blue-600 hover:text-blue-700 transition-all flex items-center gap-2 pl-10 mt-4 active:scale-95"
> >
+ Add Option <Plus size={14} /> Add Theoretical Vector
</button> </button>
)} )}
</div> </div>
@@ -196,16 +211,17 @@ export default function QuizBlock({ id, title, quizData, editMode, onChange }: Q
))} ))}
<button <button
onClick={addQuestion} onClick={addQuestion}
className="w-full py-4 glass border-dashed border-2 border-white/10 text-gray-500 hover:text-white hover:border-blue-500/30 hover:bg-blue-500/5 transition-all font-bold text-xs uppercase tracking-widest rounded-2xl" className="w-full py-6 bg-white dark:bg-white/5 border-2 border-dashed border-slate-200 dark:border-white/10 text-slate-400 dark:text-gray-500 hover:text-blue-600 hover:border-blue-500 hover:bg-blue-50 dark:hover:bg-blue-500/5 transition-all font-black text-[10px] uppercase tracking-[0.3em] rounded-[2rem] active:scale-[0.98] shadow-sm"
> >
+ Add Question + Expand Assessment Vector
</button> </button>
</div> </div>
) : ( ) : (
<div className="space-y-8"> <div className="space-y-8">
{questions.map((q) => ( {questions.map((q) => (
<div key={q.id} className="space-y-4 p-6 glass border-white/5 rounded-2xl"> <div key={q.id} className="space-y-6 p-10 bg-white dark:bg-white/5 border border-slate-100 dark:border-white/5 rounded-[2.5rem] shadow-sm hover:shadow-xl transition-all duration-700 group/qitem relative overflow-hidden">
<h4 className="font-bold text-xl text-gray-100 leading-tight">{q.question}</h4> <div className="absolute top-0 right-0 w-32 h-32 bg-indigo-500/5 rounded-full blur-3xl -translate-y-1/2 translate-x-1/2 group-hover/qitem:bg-indigo-500/10 transition-colors"></div>
<h4 className="font-black text-2xl text-slate-900 dark:text-white leading-tight uppercase tracking-tight relative z-10">{q.question}</h4>
<div className="grid gap-3"> <div className="grid gap-3">
{q.options && q.options.length > 0 ? ( {q.options && q.options.length > 0 ? (
q.options.map((opt, oIdx) => { q.options.map((opt, oIdx) => {
@@ -215,27 +231,33 @@ export default function QuizBlock({ id, title, quizData, editMode, onChange }: Q
const isWrongSelection = !isCorrect && isSelected; const isWrongSelection = !isCorrect && isSelected;
const missedCorrect = isCorrect && !isSelected; const missedCorrect = isCorrect && !isSelected;
let style = "glass border-white/10 hover:bg-white/5"; const style = submitted
if (submitted) { ? (isActuallyCorrect ? "bg-green-50 dark:bg-green-500/10 border-green-200 dark:border-green-500/30 text-green-700 dark:text-green-400 shadow-green-500/5 shadow-inner"
if (isActuallyCorrect) style = "bg-green-500/20 border-green-500 text-green-400"; : isWrongSelection ? "bg-red-50 dark:bg-red-500/10 border-red-200 dark:border-red-500/30 text-red-700 dark:text-red-400 shadow-red-500/5 shadow-inner"
else if (isWrongSelection) style = "bg-red-500/20 border-red-500 text-red-100"; : missedCorrect ? "border-orange-500/50 bg-orange-50 dark:bg-orange-500/5 text-orange-700 dark:text-orange-400 border-dashed animate-pulse"
else if (missedCorrect) style = "border-orange-500/50 text-orange-400 animate-pulse"; : "opacity-40 grayscale border-slate-100 dark:border-white/5 bg-slate-50 dark:bg-white/5")
else style = "opacity-50 grayscale border-white/5"; : isSelected ? "bg-blue-50 dark:bg-blue-500/10 border-blue-600 text-blue-700 dark:text-blue-400 shadow-xl shadow-blue-500/10 ring-4 ring-blue-500/5"
} else if (isSelected) { : "bg-white dark:bg-white/5 border-slate-100 dark:border-white/5 text-slate-500 dark:text-gray-400 hover:border-blue-500/50 hover:bg-slate-50 dark:hover:bg-blue-500/5 shadow-sm";
style = "bg-blue-500/20 border-blue-500 text-white shadow-[0_0_20px_rgba(59,130,246,0.2)]";
}
return ( return (
<button <button
key={oIdx} key={oIdx}
onClick={() => handleAnswer(q.id, oIdx, q.type === 'multiple-select')} onClick={() => handleAnswer(q.id, oIdx, q.type === 'multiple-select')}
className={`p-5 rounded-xl border transition-all text-left text-sm font-bold ${style}`} className={`p-6 rounded-2xl border transition-all text-left text-sm font-black uppercase tracking-tight relative overflow-hidden group/optans ${style}`}
> >
<div className="flex items-center justify-between"> <div className="flex items-center justify-between relative z-10">
<span>{opt}</span> <span>{opt}</span>
{submitted && isActuallyCorrect && <span></span>} {submitted ? (
{submitted && isWrongSelection && <span></span>} <div className="flex items-center gap-2">
{submitted && missedCorrect && <span className="text-[10px] uppercase font-black tracking-tighter">Correct Answer</span>} {isActuallyCorrect && <span className="text-green-600 dark:text-green-400"><CheckCircle2 size={18} /></span>}
{isWrongSelection && <span className="text-red-600 dark:text-red-400"></span>}
{missedCorrect && <span className="text-[8px] uppercase font-black tracking-widest bg-orange-100 dark:bg-orange-500/20 text-orange-600 dark:text-orange-400 px-3 py-1 rounded-full">Recall target</span>}
</div>
) : (
<div className={`w-6 h-6 rounded-lg border-2 transition-all flex items-center justify-center ${isSelected ? 'bg-blue-600 border-blue-600 shadow-lg shadow-blue-500/40 text-white' : 'border-slate-200 dark:border-white/10'}`}>
{isSelected && <Check size={14} />}
</div>
)}
</div> </div>
</button> </button>
); );
@@ -251,17 +273,18 @@ export default function QuizBlock({ id, title, quizData, editMode, onChange }: Q
{!submitted && questions.length > 0 && ( {!submitted && questions.length > 0 && (
<button <button
onClick={() => setSubmitted(true)} onClick={() => setSubmitted(true)}
className="btn-premium w-full py-5 font-black text-xs uppercase tracking-[0.2em] shadow-xl shadow-blue-500/20" className="w-full py-6 bg-indigo-600 text-white font-black text-[10px] uppercase tracking-[0.3em] shadow-2xl shadow-indigo-500/40 hover:scale-[1.02] active:scale-[0.98] transition-all rounded-[2rem] flex items-center justify-center gap-4"
> >
Validate Answers <Target size={18} />
Validate Synaptic Map
</button> </button>
)} )}
{submitted && ( {submitted && (
<button <button
onClick={() => { setSubmitted(false); setUserAnswers({}); }} onClick={() => { setSubmitted(false); setUserAnswers({}); }}
className="w-full py-5 glass text-blue-400 font-black text-xs uppercase tracking-[0.2em] hover:bg-white/5 transition-all rounded-2xl" className="w-full py-6 bg-white dark:bg-white/5 border border-slate-200 dark:border-white/10 text-indigo-600 dark:text-indigo-400 font-black text-[10px] uppercase tracking-[0.3em] hover:bg-slate-50 transition-all rounded-[2rem] active:scale-[0.98] shadow-sm"
> >
Try Again Reset Neural Chain
</button> </button>
)} )}
</div> </div>
@@ -26,67 +26,70 @@ export default function ShortAnswerBlock({ id, title, prompt, correctAnswers, ed
<div className="space-y-8" id={id}> <div className="space-y-8" id={id}>
<div className="space-y-2"> <div className="space-y-2">
{editMode ? ( {editMode ? (
<div className="space-y-2 p-6 glass border-white/5 bg-white/5 mb-4"> <div className="space-y-4 p-8 bg-slate-50 dark:bg-white/5 border border-slate-200 dark:border-white/10 rounded-[2rem] mb-6 shadow-inner relative overflow-hidden">
<label className="text-xs font-bold text-gray-500 uppercase tracking-widest">Activity Title (Optional)</label> <div className="absolute top-0 left-0 w-1 h-full bg-blue-500/40"></div>
<label className="text-[10px] font-black text-slate-400 dark:text-gray-500 uppercase tracking-[0.2em]">Activity Title (Optional)</label>
<input <input
type="text" type="text"
value={title || ""} value={title || ""}
onChange={(e) => onChange({ title: e.target.value })} onChange={(e) => onChange({ title: e.target.value })}
placeholder="e.g. Critical Thinking, Quick Response..." placeholder="e.g. Critical Thinking, Quick Response..."
className="w-full bg-white/5 border border-white/10 rounded-lg px-4 py-2 text-sm font-bold focus:border-blue-500/50 focus:outline-none" className="w-full bg-white dark:bg-black/40 border border-slate-200 dark:border-white/10 rounded-2xl px-6 py-3 text-sm font-black uppercase tracking-tight focus:ring-4 focus:ring-blue-500/10 focus:border-blue-500 transition-all outline-none shadow-sm"
/> />
</div> </div>
) : ( ) : (
<h3 className="text-xl font-bold border-l-4 border-blue-500 pl-4 py-1 tracking-tight text-white"> <h3 className="text-2xl font-black italic tracking-tight text-slate-900 dark:text-white uppercase border-l-4 border-blue-600 pl-6 py-1">
{title || "Short Answer"} {title || "Short Answer"}
</h3> </h3>
)} )}
</div> </div>
{editMode ? ( {editMode ? (
<div className="space-y-6"> <div className="space-y-8">
<div className="p-6 glass border-white/5 space-y-4"> <div className="p-10 bg-white dark:bg-white/5 border border-slate-200 dark:border-white/10 rounded-[2.5rem] space-y-4 shadow-sm">
<label className="text-xs font-bold text-gray-500 uppercase tracking-widest">Question Prompt</label> <label className="text-[10px] font-black text-slate-400 dark:text-gray-500 uppercase tracking-widest pl-1">Inquiry Vector (Question Prompt)</label>
<textarea <textarea
value={prompt} value={prompt}
onChange={(e) => onChange({ prompt: e.target.value })} onChange={(e) => onChange({ prompt: e.target.value })}
className="w-full bg-white/5 border border-white/10 rounded-xl p-4 min-h-[100px] text-lg font-medium focus:outline-none focus:border-blue-500/50 transition-all" className="w-full bg-slate-50 dark:bg-black/40 border border-slate-200 dark:border-white/10 rounded-2xl p-6 min-h-[120px] text-lg font-black tracking-tight text-slate-800 dark:text-white focus:ring-4 focus:ring-blue-500/10 focus:border-blue-500 transition-all placeholder:text-slate-300 resize-none shadow-inner"
placeholder="Type the question for the student..." placeholder="Type the synaptic trigger for the student..."
/> />
</div> </div>
<div className="p-6 glass border-white/5 space-y-4"> <div className="p-10 bg-white dark:bg-white/5 border border-slate-200 dark:border-white/10 rounded-[2.5rem] space-y-4 shadow-sm relative overflow-hidden">
<label className="text-xs font-bold text-gray-500 uppercase tracking-widest">Correct Answers (One per line)</label> <div className="absolute top-0 right-0 w-32 h-32 bg-green-500/5 rounded-full blur-3xl -translate-y-1/2 translate-x-1/2"></div>
<label className="text-[10px] font-black text-slate-400 dark:text-gray-500 uppercase tracking-widest pl-1">Target Response Matrix (One per line)</label>
<textarea <textarea
value={correctAnswers ? correctAnswers.join("\n") : ""} value={correctAnswers ? correctAnswers.join("\n") : ""}
onChange={(e) => onChange({ correctAnswers: e.target.value.split("\n").filter(a => a.trim() !== "") })} onChange={(e) => onChange({ correctAnswers: e.target.value.split("\n").filter(a => a.trim() !== "") })}
className="w-full bg-white/5 border border-white/10 rounded-xl p-4 min-h-[100px] text-sm font-medium focus:outline-none focus:border-blue-500/50 transition-all" className="w-full bg-slate-50 dark:bg-black/40 border border-slate-200 dark:border-white/10 rounded-2xl p-6 min-h-[120px] text-sm font-bold text-green-700 dark:text-green-400 focus:ring-4 focus:ring-green-500/10 focus:border-green-500 transition-all outline-none shadow-inner"
placeholder="Answer 1&#10;Answer 2 (Alternative)" placeholder="Primary Solution&#10;Secondary Vector (Alternative)"
/> />
<p className="text-[10px] text-gray-500 uppercase tracking-wider">Validation is case-insensitive. Provide multiple alternatives if necessary.</p> <p className="text-[9px] text-slate-400 dark:text-gray-500 uppercase font-black italic pl-1 leading-relaxed">Validation employs case-insensitive string matching across all vectors.</p>
</div> </div>
</div> </div>
) : ( ) : (
<div className="p-8 glass border-white/5 rounded-3xl space-y-8"> <div className="p-10 bg-white dark:bg-white/5 border border-slate-100 dark:border-white/10 rounded-[3rem] space-y-10 shadow-sm hover:shadow-xl transition-all duration-700 relative overflow-hidden group/saitem">
<p className="text-xl font-bold text-gray-100">{prompt || "Please enter your answer below:"}</p> <div className="absolute top-0 right-0 w-64 h-64 bg-blue-500/5 rounded-full blur-[80px] -translate-y-1/2 translate-x-1/2 group-hover/saitem:bg-blue-500/10 transition-colors"></div>
<p className="text-2xl font-black text-slate-900 dark:text-white uppercase tracking-tight leading-tight relative z-10">{prompt || "Please formulate your response vector:"}</p>
<div className="space-y-4"> <div className="space-y-6 relative z-10">
<input <input
type="text" type="text"
value={userAnswer} value={userAnswer}
onChange={(e) => setUserAnswer(e.target.value)} onChange={(e) => setUserAnswer(e.target.value)}
disabled={submitted} disabled={submitted}
className={`w-full bg-white/5 border-2 rounded-2xl px-6 py-4 text-lg transition-all focus:outline-none ${submitted className={`w-full bg-slate-50 dark:bg-black/40 border-2 rounded-[2rem] px-8 py-6 text-xl font-black tracking-tight transition-all focus:outline-none shadow-inner ${submitted
? (isCorrect ? "border-green-500 bg-green-500/10 text-green-400" : "border-red-500 bg-red-500/10 text-red-100") ? (isCorrect ? "border-green-500 text-green-700 dark:text-green-400 ring-4 ring-green-500/5" : "border-red-500 text-red-700 dark:text-red-100 ring-4 ring-red-500/5")
: "border-white/10 focus:border-blue-500 text-white" : "border-slate-100 dark:border-white/10 focus:border-blue-600 text-slate-800 dark:text-white focus:ring-8 focus:ring-blue-500/5"
}`} }`}
placeholder="Type your answer..." placeholder="Ingest solution..."
/> />
{submitted && !isCorrect && ( {submitted && !isCorrect && (
<div className="p-4 bg-orange-500/10 border border-orange-500/20 rounded-xl"> <div className="p-6 bg-orange-50 dark:bg-orange-500/10 border border-orange-200 dark:border-orange-500/20 rounded-2xl animate-in slide-in-from-top-2 duration-500">
<p className="text-[10px] text-orange-400 uppercase font-black tracking-widest">Suggested Answer(s):</p> <p className="text-[10px] text-orange-600 dark:text-orange-400 uppercase font-black tracking-[0.2em]">Validated Synaptic Marker:</p>
<p className="text-sm text-gray-400 mt-1">{correctAnswers && correctAnswers[0]}</p> <p className="text-lg font-black text-slate-700 dark:text-gray-300 mt-2 italic">"{correctAnswers && correctAnswers[0]}"</p>
</div> </div>
)} )}
</div> </div>
@@ -95,18 +98,18 @@ export default function ShortAnswerBlock({ id, title, prompt, correctAnswers, ed
<button <button
onClick={() => setSubmitted(true)} onClick={() => setSubmitted(true)}
disabled={!userAnswer.trim()} disabled={!userAnswer.trim()}
className="btn-premium w-full py-4 font-black text-xs uppercase tracking-[0.2em] shadow-xl shadow-blue-500/20 disabled:opacity-50 disabled:grayscale" className="w-full py-6 bg-blue-600 text-white font-black text-[10px] uppercase tracking-[0.3em] shadow-2xl shadow-blue-500/40 hover:scale-[1.02] active:scale-[0.98] transition-all rounded-[2rem] flex items-center justify-center gap-4 disabled:opacity-20 disabled:hover:scale-100"
> >
Submit Answer Execute Validation
</button> </button>
)} )}
{submitted && ( {submitted && (
<button <button
onClick={handleReset} onClick={handleReset}
className="w-full py-4 glass text-blue-400 font-black text-xs uppercase tracking-[0.2em] hover:bg-white/5 transition-all rounded-xl border-white/5" className="w-full py-6 bg-white dark:bg-white/5 border border-slate-200 dark:border-white/10 text-blue-600 dark:text-blue-400 font-black text-[10px] uppercase tracking-[0.3em] hover:bg-slate-50 transition-all rounded-[2rem] active:scale-[0.98] shadow-sm"
> >
Try Again Reset Response Vector
</button> </button>
)} )}
</div> </div>
@@ -75,26 +75,27 @@ export default function VideoMarkerBlock({
if (!editMode) { if (!editMode) {
return ( return (
<div className="space-y-4"> <div className="space-y-6">
<h3 className="text-xs font-black uppercase tracking-widest text-gray-400"> <h3 className="text-[10px] font-black uppercase tracking-[0.2em] text-slate-400 dark:text-gray-500 pl-1">
{title || "Video con Marcadores"} {title || "Interactive Temporal Nodes"}
</h3> </h3>
<div className="glass-card p-6 border-indigo-500/20 bg-indigo-500/5"> <div className="bg-white dark:bg-white/5 border border-slate-100 dark:border-white/10 p-8 rounded-[2.5rem] shadow-sm relative overflow-hidden group/vm">
<div className="flex items-center gap-3 mb-4"> <div className="absolute top-0 right-0 w-32 h-32 bg-indigo-500/5 rounded-full blur-3xl -translate-y-1/2 translate-x-1/2 group-hover/vm:bg-indigo-500/10 transition-colors"></div>
<div className="p-2 rounded-lg bg-indigo-500/10 text-indigo-400"> <div className="flex items-center gap-4 mb-8 relative z-10">
<Clock size={20} /> <div className="p-3 rounded-2xl bg-indigo-50 dark:bg-indigo-500/10 text-indigo-600 dark:text-indigo-400 shadow-inner">
<Clock size={24} />
</div> </div>
<div> <div>
<p className="text-sm font-bold text-white">Video Interactivo</p> <p className="text-lg font-black text-slate-900 dark:text-white uppercase tracking-tight italic">Interactive Stream</p>
<p className="text-xs text-gray-500">{markers.length} marcadores configurados</p> <p className="text-[10px] text-slate-400 dark:text-gray-500 uppercase font-black tracking-widest">{markers.length} SYNC MARKERS DETECTED</p>
</div> </div>
</div> </div>
<div className="space-y-2"> <div className="space-y-3 relative z-10">
{markers.map((marker, idx) => ( {markers.map((marker, idx) => (
<div key={idx} className="flex items-center gap-2 text-xs text-gray-400"> <div key={idx} className="flex items-center gap-4 text-[10px] text-slate-500 dark:text-gray-400 font-black uppercase tracking-wider bg-slate-50/50 dark:bg-white/5 p-3 rounded-xl border border-slate-100 dark:border-white/5">
<span className="font-mono text-indigo-400">{formatTime(marker.timestamp)}</span> <span className="font-mono text-indigo-600 dark:text-indigo-400 bg-white dark:bg-black/40 px-2 py-1 rounded-lg border border-slate-100 dark:border-white/10 shadow-sm">{formatTime(marker.timestamp)}</span>
<span></span> <span className="opacity-20">/</span>
<span className="truncate">{marker.question}</span> <span className="truncate italic">{marker.question}</span>
</div> </div>
))} ))}
</div> </div>
@@ -106,137 +107,147 @@ export default function VideoMarkerBlock({
return ( return (
<div className="space-y-6 animate-in fade-in duration-300"> <div className="space-y-6 animate-in fade-in duration-300">
{/* Title Editor */} {/* Title Editor */}
<div className="space-y-2"> <div className="space-y-4 p-8 bg-slate-50 dark:bg-white/5 border border-slate-200 dark:border-white/10 rounded-[2rem] mb-6 shadow-inner relative overflow-hidden">
<label className="text-xs font-bold text-gray-500 uppercase tracking-widest">Título del Bloque</label> <div className="absolute top-0 left-0 w-1 h-full bg-indigo-500/40"></div>
<label className="text-[10px] font-black text-slate-400 dark:text-gray-500 uppercase tracking-[0.2em]">Activity Title (Optional)</label>
<input <input
type="text" type="text"
value={title} value={title}
onChange={(e) => onChange({ title: e.target.value })} onChange={(e) => onChange({ title: e.target.value })}
className="w-full bg-white/5 border border-white/10 rounded-lg px-4 py-2 text-white focus:outline-none focus:ring-2 focus:ring-indigo-500" className="w-full bg-white dark:bg-black/40 border border-slate-200 dark:border-white/10 rounded-2xl px-6 py-3 text-sm font-black uppercase tracking-tight focus:ring-4 focus:ring-indigo-500/10 focus:border-indigo-500 transition-all outline-none shadow-sm"
placeholder="Ej: Video Tutorial - Introducción" placeholder="e.g. Masterclass Stream, Acoustic Analysis..."
/> />
</div> </div>
{/* Video Preview with Timeline */} {/* Video Preview with Timeline */}
<div className="glass-card p-4 space-y-4"> <div className="bg-white dark:bg-white/5 border border-slate-200 dark:border-white/10 p-6 rounded-[3rem] space-y-6 shadow-xl relative overflow-hidden">
<div className="flex items-center justify-between"> <div className="absolute top-0 right-0 w-64 h-64 bg-indigo-500/5 rounded-full blur-[80px] -translate-y-1/2 translate-x-1/2"></div>
<h4 className="text-sm font-bold text-white flex items-center gap-2"> <div className="flex items-center justify-between relative z-10 px-2">
<Play size={16} className="text-indigo-400" /> <h4 className="text-[10px] font-black text-slate-900 dark:text-white flex items-center gap-3 uppercase tracking-widest italic">
Vista Previa del Video <Play size={16} className="text-indigo-600 dark:text-indigo-400" />
Temporal Scrutiny
</h4> </h4>
<span className="text-xs font-mono text-gray-500">{formatTime(currentTime)}</span> <span className="text-xs font-black font-mono text-indigo-600 dark:text-indigo-400 bg-indigo-50 dark:bg-black/40 px-3 py-1 rounded-full border border-indigo-100 dark:border-white/10">{formatTime(currentTime)}</span>
</div> </div>
<div className="rounded-lg overflow-hidden"> <div className="rounded-[2rem] overflow-hidden border border-slate-100 dark:border-white/10 shadow-2xl relative z-10">
<MediaPlayer <MediaPlayer
src={videoUrl} src={videoUrl}
type="video" type="video"
isGraded={isGraded} isGraded={isGraded}
showInteractive={false} // Interactive markers are separate here showInteractive={false}
onTimeUpdate={setCurrentTime} onTimeUpdate={setCurrentTime}
/> />
</div> </div>
<button <button
onClick={addMarker} onClick={addMarker}
className="w-full py-2 bg-indigo-600 hover:bg-indigo-500 text-white rounded-lg font-bold flex items-center justify-center gap-2 transition-all" className="w-full py-5 bg-indigo-600 hover:bg-indigo-500 text-white rounded-[2rem] font-black text-[10px] uppercase tracking-[0.3em] flex items-center justify-center gap-4 transition-all shadow-2xl shadow-indigo-500/40 active:scale-95 group relative z-10"
> >
<Plus size={16} /> <Plus size={18} className="group-hover:rotate-90 transition-transform" />
Agregar Marcador en {formatTime(currentTime)} Inject Marker at {formatTime(currentTime)}
</button> </button>
</div> </div>
{/* Markers List */} {/* Markers List */}
<div className="space-y-4"> <div className="space-y-6">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between px-2">
<h4 className="text-sm font-bold text-white flex items-center gap-2"> <h4 className="text-[10px] font-black text-slate-400 dark:text-gray-500 flex items-center gap-3 uppercase tracking-[0.2em]">
<Clock size={16} className="text-amber-400" /> <Clock size={16} className="text-amber-500" />
Marcadores ({markers.length}) Interactive Sequence ({markers.length})
</h4> </h4>
</div> </div>
{markers.length === 0 && ( {markers.length === 0 && (
<div className="glass-card p-8 text-center border-dashed"> <div className="bg-slate-50 dark:bg-black/20 border-2 border-dashed border-slate-200 dark:border-white/10 p-16 rounded-[3rem] text-center space-y-4">
<AlertCircle className="w-12 h-12 text-gray-600 mx-auto mb-3" /> <AlertCircle className="w-12 h-12 text-slate-300 dark:text-gray-700 mx-auto opacity-40" />
<p className="text-sm text-gray-500">No hay marcadores configurados.</p> <div className="space-y-1">
<p className="text-xs text-gray-600 mt-1">Reproduce el video y haz clic en &quot;Agregar Marcador&quot;</p> <p className="text-[10px] font-black text-slate-400 dark:text-gray-500 uppercase tracking-widest">No interactive nodes detected</p>
<p className="text-[9px] text-slate-300 dark:text-gray-600 uppercase font-black italic">Initiate playback and execute &quot;Inject Marker&quot;</p>
</div>
</div> </div>
)} )}
{markers.map((marker, idx) => ( {markers.map((marker, idx) => (
<div key={idx} className="glass-card p-4 space-y-3 border-l-4 border-indigo-500"> <div key={idx} className="bg-white dark:bg-white/5 border border-slate-100 dark:border-white/10 rounded-[2.5rem] shadow-sm overflow-hidden group/mkr transition-all duration-500 hover:shadow-xl hover:border-indigo-500/20">
<div className="flex items-center justify-between"> <div className="p-6 border-l-[6px] border-indigo-600 flex flex-col gap-6">
<div className="flex items-center gap-3"> <div className="flex items-center justify-between">
<span className="text-xs font-mono font-bold text-indigo-400 bg-indigo-500/10 px-2 py-1 rounded"> <div className="flex items-center gap-4">
{formatTime(marker.timestamp)} <span className="text-[10px] font-black text-indigo-700 dark:text-indigo-400 bg-indigo-50 dark:bg-indigo-500/10 px-4 py-2 rounded-xl border border-indigo-100 dark:border-indigo-500/20 shadow-inner italic">
</span> SYNC {formatTime(marker.timestamp)}
</span>
<button
onClick={() => setEditingIndex(editingIndex === idx ? null : idx)}
className="text-[9px] font-black uppercase tracking-[0.2em] text-slate-400 hover:text-indigo-600 transition-colors"
>
{editingIndex === idx ? "COLLAPSE" : "SCRUTINIZE"}
</button>
</div>
<button <button
onClick={() => setEditingIndex(editingIndex === idx ? null : idx)} onClick={() => deleteMarker(idx)}
className="text-xs text-gray-500 hover:text-white transition-colors" className="p-3 bg-red-50 dark:bg-red-500/5 hover:bg-red-500 hover:text-white rounded-xl text-red-500 transition-all active:scale-90"
> >
{editingIndex === idx ? "Colapsar" : "Editar"} <Trash2 size={16} />
</button> </button>
</div> </div>
<button
onClick={() => deleteMarker(idx)}
className="p-2 hover:bg-red-500/10 rounded-lg text-red-500 transition-all"
>
<Trash2 size={14} />
</button>
</div>
{editingIndex === idx ? ( {editingIndex === idx ? (
<div className="space-y-3 pt-2 border-t border-white/5"> <div className="space-y-6 pt-6 border-t border-slate-50 dark:border-white/5 animate-in slide-in-from-top-2 duration-500">
{/* Timestamp Editor */} <div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div className="space-y-1"> {/* Timestamp Editor */}
<label className="text-[10px] font-bold text-gray-600 uppercase tracking-widest">Timestamp (MM:SS)</label> <div className="space-y-3">
<input <label className="text-[9px] font-black text-slate-400 dark:text-gray-500 uppercase tracking-widest pl-1">Temporal Alignment (MM:SS)</label>
type="text"
value={formatTime(marker.timestamp)}
onChange={(e) => updateMarker(idx, { timestamp: parseTime(e.target.value) })}
className="w-full bg-black/20 border border-white/10 rounded px-3 py-1.5 text-sm font-mono text-white focus:outline-none focus:ring-1 focus:ring-indigo-500"
/>
</div>
{/* Question Editor */}
<div className="space-y-1">
<label className="text-[10px] font-bold text-gray-600 uppercase tracking-widest">Pregunta</label>
<input
type="text"
value={marker.question}
onChange={(e) => updateMarker(idx, { question: e.target.value })}
className="w-full bg-black/20 border border-white/10 rounded px-3 py-2 text-sm text-white focus:outline-none focus:ring-1 focus:ring-indigo-500"
placeholder="¿Qué concepto se explicó?"
/>
</div>
{/* Options Editor */}
<div className="space-y-2">
<label className="text-[10px] font-bold text-gray-600 uppercase tracking-widest">Opciones de Respuesta</label>
{marker.options.map((option, optIdx) => (
<div key={optIdx} className="flex items-center gap-2">
<input
type="radio"
name={`correct-${idx}`}
checked={marker.correctIndex === optIdx}
onChange={() => updateMarker(idx, { correctIndex: optIdx })}
className="w-4 h-4 text-green-500"
/>
<input <input
type="text" type="text"
value={option} value={formatTime(marker.timestamp)}
onChange={(e) => updateOption(idx, optIdx, e.target.value)} onChange={(e) => updateMarker(idx, { timestamp: parseTime(e.target.value) })}
className="flex-1 bg-black/20 border border-white/10 rounded px-3 py-1.5 text-sm text-white focus:outline-none focus:ring-1 focus:ring-green-500" className="w-full bg-slate-50 dark:bg-black/40 border border-slate-100 dark:border-white/10 rounded-2xl px-5 py-3 text-sm font-black font-mono text-slate-800 dark:text-white focus:ring-4 focus:ring-indigo-500/10 transition-all outline-none"
placeholder={`Opción ${optIdx + 1}`}
/> />
</div> </div>
))}
<p className="text-[10px] text-gray-600 italic">Selecciona el radio button de la respuesta correcta</p> {/* Question Editor */}
<div className="space-y-3">
<label className="text-[9px] font-black text-slate-400 dark:text-gray-500 uppercase tracking-widest pl-1">Inquiry Scoped Prompt</label>
<input
type="text"
value={marker.question}
onChange={(e) => updateMarker(idx, { question: e.target.value })}
className="w-full bg-slate-50 dark:bg-black/40 border border-slate-100 dark:border-white/10 rounded-2xl px-5 py-3 text-sm font-black text-slate-800 dark:text-white focus:ring-4 focus:ring-indigo-500/10 transition-all outline-none"
placeholder="¿Critical concept?"
/>
</div>
</div>
{/* Options Editor */}
<div className="space-y-4">
<label className="text-[9px] font-black text-slate-400 dark:text-gray-500 uppercase tracking-widest pl-1">Probability Vectors (Options)</label>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{marker.options.map((option, optIdx) => (
<div key={optIdx} className="flex items-center gap-3 bg-slate-50 dark:bg-black/20 p-3 rounded-[1.5rem] border border-slate-100 dark:border-white/10 group/opt">
<input
type="radio"
name={`correct-${idx}`}
checked={marker.correctIndex === optIdx}
onChange={() => updateMarker(idx, { correctIndex: optIdx })}
className="w-5 h-5 text-green-600 dark:text-green-500 bg-white dark:bg-black/40 border-slate-300 dark:border-white/10 focus:ring-green-500"
/>
<input
type="text"
value={option}
onChange={(e) => updateOption(idx, optIdx, e.target.value)}
className="flex-1 bg-transparent border-none p-0 text-sm font-bold text-slate-700 dark:text-white focus:ring-0 outline-none"
placeholder={`Vector ${optIdx + 1}`}
/>
</div>
))}
</div>
<p className="text-[9px] text-slate-400 dark:text-gray-600 uppercase font-black italic pl-1 italic">Identify the absolute truth vector via the terminal radio toggle.</p>
</div>
</div> </div>
</div> ) : (
) : ( <p className="text-xl font-black text-slate-800 dark:text-gray-200 tracking-tight uppercase italic">{marker.question}</p>
<p className="text-sm text-gray-400 truncate">{marker.question}</p> )}
)} </div>
</div> </div>
))} ))}
</div> </div>