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
+91 -83
View File
@@ -64,100 +64,104 @@ export default function MediaBlock({ title, url, type, config, editMode, onChang
{/* Block Header */}
<div className="space-y-2">
{editMode ? (
<div className="space-y-2 p-6 glass border-white/5 bg-white/5 mb-4">
<label className="text-xs font-bold text-gray-500 uppercase tracking-widest">Activity Title (Optional)</label>
<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">
<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
type="text"
value={title || ""}
onChange={(e) => onChange({ title: e.target.value })}
placeholder="e.g. Explainer Video, Audio 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"
placeholder="e.g. Masterclass Stream, Acoustic Analysis..."
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>
) : (
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>
{editMode && (
<div className="space-y-6 p-6 glass border-blue-500/10 mb-8 bg-blue-500/5">
<div className="flex items-center gap-4 mb-2">
<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="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
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
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>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
{sourceType === "url" ? (
<div className="space-y-2">
<label className="text-xs font-bold text-gray-500 uppercase tracking-widest">Media URL</label>
<input
type="text"
value={url.startsWith("/") ? "" : url}
onChange={(e) => onChange({ url: e.target.value })}
placeholder="YouTube, Vimeo or static link"
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"
/>
</div>
) : (
<div className="space-y-2">
<label className="text-xs font-bold text-gray-500 uppercase tracking-widest">File Manager</label>
<FileUpload
currentUrl={url.startsWith("/") ? url : undefined}
onUploadComplete={(newUrl) => onChange({ url: newUrl })}
/>
</div>
)}
<div className="grid grid-cols-1 md:grid-cols-2 gap-8 relative z-10">
{sourceType === "url" ? (
<div className="space-y-3">
<label className="text-[10px] font-black text-slate-400 dark:text-gray-500 uppercase tracking-widest pl-1">Media Source Locator</label>
<input
type="text"
value={url.startsWith("/") ? "" : url}
onChange={(e) => onChange({ url: e.target.value })}
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 className="space-y-3">
<label className="text-[10px] font-black text-slate-400 dark:text-gray-500 uppercase tracking-widest pl-1">Asset Pipeline</label>
<FileUpload
currentUrl={url.startsWith("/") ? url : undefined}
onUploadComplete={(newUrl) => onChange({ url: newUrl })}
/>
</div>
)}
<div className="space-y-2">
<label className="text-xs font-bold text-gray-500 uppercase tracking-widest">Playback Limit (0 = Unlimited)</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">
<div className="space-y-3">
<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="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-4 h-4 rounded border-gray-600 text-blue-600 focus:ring-blue-500 bg-gray-700"
type="number"
value={maxPlays}
onChange={(e) => onChange({ config: { ...config, maxPlays: parseInt(e.target.value) || 0 } })}
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"
/>
<label htmlFor={`show-transcript-${title}`} className="text-sm text-gray-300 font-medium select-none cursor-pointer">
Show Interactive Transcript
</label>
<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>
</div>
<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>
<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>
{/* Markers Editor */}
<div className="space-y-4 pt-6 border-t border-white/10">
<label className="text-xs font-bold text-gray-500 uppercase tracking-widest block">Interactive Questions (Timestamps)</label>
<div className="space-y-6 pt-10 border-t border-slate-100 dark:border-white/10 relative z-10">
<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) => (
<div key={idx} className="bg-white/5 p-4 rounded-lg border border-white/5 space-y-3">
<div className="flex items-center gap-2">
<span className="text-xs font-mono bg-blue-500/20 text-blue-400 px-2 py-1 rounded">
<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-6">
<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')}
</span>
</div>
<input
value={marker.question}
onChange={(e) => {
@@ -165,7 +169,8 @@ export default function MediaBlock({ title, url, type, config, editMode, onChang
newMarkers[idx].question = e.target.value;
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
onClick={() => {
@@ -187,17 +192,17 @@ export default function MediaBlock({ title, url, type, config, editMode, onChang
newMarkers.splice(idx, 1);
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>
</div>
{/* Options Management */}
<div className="pl-14 space-y-2">
<label className="text-[10px] font-bold text-gray-500 uppercase">Options</label>
<div className="pl-24 space-y-3">
<label className="text-[10px] font-black text-slate-400 dark:text-gray-500 uppercase tracking-widest">Response Vectors</label>
{marker.options.map((opt, optIdx) => (
<div key={optIdx} className="flex items-center gap-2">
<div key={optIdx} className="flex items-center gap-3">
<input
type="radio"
name={`correct-${idx}`}
@@ -207,7 +212,7 @@ export default function MediaBlock({ title, url, type, config, editMode, onChang
newMarkers[idx].correctIndex = optIdx;
onChange({ config: { ...config, markers: newMarkers } });
}}
className="accent-green-500"
className="w-5 h-5 accent-green-500 cursor-pointer"
/>
<input
value={opt}
@@ -216,7 +221,7 @@ export default function MediaBlock({ title, url, type, config, editMode, onChang
newMarkers[idx].options[optIdx] = e.target.value;
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
onClick={() => {
@@ -227,9 +232,9 @@ export default function MediaBlock({ title, url, type, config, editMode, onChang
}
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>
</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}`);
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>
</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
code-type="number"
type="number"
placeholder="Sec"
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
type="text"
placeholder="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
onClick={() => {
@@ -281,12 +286,12 @@ export default function MediaBlock({ title, url, type, config, editMode, onChang
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
</button>
</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.
</p>
</div>
@@ -305,10 +310,13 @@ export default function MediaBlock({ title, url, type, config, editMode, onChang
/>
{!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">
<span className="text-xs text-gray-500 uppercase font-medium">Plays Remaining</span>
<span className={`text-sm font-bold ${maxPlays - localPlays <= 1 ? 'text-orange-400' : 'text-blue-400'}`}>
{Math.max(0, maxPlays - localPlays)} / {maxPlays}
<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">
<div className="flex items-center gap-3">
<div className="w-2 h-2 rounded-full bg-blue-500 animate-pulse"></div>
<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>
</div>
)}