feat: reestruturação da barra lateral de módulos e aulas no CourseView
Build and Deploy (Gitea) / build-and-deploy (push) Successful in 58s
Details
Build and Deploy (Gitea) / build-and-deploy (push) Successful in 58s
Details
This commit is contained in:
parent
c4feb1b109
commit
f1dde389d2
|
|
@ -48,6 +48,7 @@ export default function CourseView({ courseId, token, onBack, initialLessonId }:
|
||||||
const [course, setCourse] = useState<Course | null>(null);
|
const [course, setCourse] = useState<Course | null>(null);
|
||||||
const [modules, setModules] = useState<Module[]>([]);
|
const [modules, setModules] = useState<Module[]>([]);
|
||||||
const [activeLesson, setActiveLesson] = useState<Lesson | null>(null);
|
const [activeLesson, setActiveLesson] = useState<Lesson | null>(null);
|
||||||
|
const [expandedModules, setExpandedModules] = useState<string[]>([]);
|
||||||
const [noteContent, setNoteContent] = useState('');
|
const [noteContent, setNoteContent] = useState('');
|
||||||
const [isLoading, setIsLoading] = useState(true);
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
@ -95,8 +96,7 @@ export default function CourseView({ courseId, token, onBack, initialLessonId }:
|
||||||
}
|
}
|
||||||
|
|
||||||
if (selectedLsn) {
|
if (selectedLsn) {
|
||||||
setActiveLesson(selectedLsn);
|
handleSelectLesson(selectedLsn, data.modules.find((m: any) => m.lessons.some((l: any) => l.id === selectedLsn.id))?.id);
|
||||||
setNoteContent(selectedLsn.note || '');
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
|
|
@ -106,12 +106,31 @@ export default function CourseView({ courseId, token, onBack, initialLessonId }:
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSelectLesson = (lesson: Lesson) => {
|
const handleSelectLesson = (lesson: Lesson, moduleId?: string) => {
|
||||||
setActiveLesson(lesson);
|
setActiveLesson(lesson);
|
||||||
setNoteContent(lesson.note || '');
|
setNoteContent(lesson.note || '');
|
||||||
setSaveStatus('idle');
|
setSaveStatus('idle');
|
||||||
|
if (moduleId) {
|
||||||
|
setExpandedModules(prev => prev.includes(moduleId) ? prev : [...prev, moduleId]);
|
||||||
|
} else {
|
||||||
|
// Find module if not passed
|
||||||
|
const parentMod = modules.find(m => m.lessons.some(l => l.id === lesson.id));
|
||||||
|
if (parentMod) {
|
||||||
|
setExpandedModules(prev => prev.includes(parentMod.id) ? prev : [...prev, parentMod.id]);
|
||||||
|
}
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const toggleModule = (moduleId: string) => {
|
||||||
|
setExpandedModules(prev =>
|
||||||
|
prev.includes(moduleId) ? prev.filter(id => id !== moduleId) : [...prev, moduleId]
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const totalLessonsCount = modules.reduce((acc, mod) => acc + mod.lessons.length, 0);
|
||||||
|
const completedLessonsCount = modules.reduce((acc, mod) => acc + mod.lessons.filter(l => l.progress?.completed).length, 0);
|
||||||
|
const overallProgress = totalLessonsCount === 0 ? 0 : Math.round((completedLessonsCount / totalLessonsCount) * 100);
|
||||||
|
|
||||||
const handleProgressUpdate = async (watchedPercentage: number) => {
|
const handleProgressUpdate = async (watchedPercentage: number) => {
|
||||||
if (!activeLesson) return;
|
if (!activeLesson) return;
|
||||||
|
|
||||||
|
|
@ -351,88 +370,113 @@ export default function CourseView({ courseId, token, onBack, initialLessonId }:
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Overall Progress Block */}
|
||||||
|
<div className="glass-card rounded-xl p-4 mb-4 border border-white/5 flex items-center justify-between">
|
||||||
|
<div className="flex items-center space-x-3">
|
||||||
|
<svg viewBox="0 0 36 36" className="w-10 h-10 -rotate-90">
|
||||||
|
<path className="text-zinc-800" strokeWidth="3" stroke="currentColor" fill="none" d="M18 2.0845 a 15.9155 15.9155 0 0 1 0 31.831 a 15.9155 15.9155 0 0 1 0 -31.831" />
|
||||||
|
<path className="text-[#E50914] transition-all duration-1000 ease-out" strokeDasharray={`${overallProgress}, 100`} strokeWidth="3" strokeLinecap="round" stroke="currentColor" fill="none" d="M18 2.0845 a 15.9155 15.9155 0 0 1 0 31.831 a 15.9155 15.9155 0 0 1 0 -31.831" />
|
||||||
|
</svg>
|
||||||
|
<div>
|
||||||
|
<h4 className="text-sm font-bold text-white">Meu progresso - {overallProgress}%</h4>
|
||||||
|
<p className="text-xs text-zinc-500">{completedLessonsCount} de {totalLessonsCount} {totalLessonsCount === 1 ? 'aula' : 'aulas'}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
{modules.map((mod, modIdx) => (
|
{modules.map((mod, modIdx) => {
|
||||||
<div
|
const isExpanded = expandedModules.includes(mod.id);
|
||||||
key={mod.id}
|
const moduleTotal = mod.lessons.length;
|
||||||
className="glass-card rounded-xl overflow-hidden"
|
const moduleCompleted = mod.lessons.filter(l => l.progress?.completed).length;
|
||||||
>
|
const isModuleFullyCompleted = moduleTotal > 0 && moduleTotal === moduleCompleted;
|
||||||
{/* Module Title bar */}
|
|
||||||
<div className="glass-accordion-header px-4 py-3 flex items-center justify-between">
|
|
||||||
<div className="flex items-center space-x-2">
|
|
||||||
<span className="text-[#E50914] text-xs font-black">M{modIdx + 1}</span>
|
|
||||||
<h4 className="font-sans font-bold text-xs text-zinc-200 tracking-tight leading-snug line-clamp-1">
|
|
||||||
{mod.title}
|
|
||||||
</h4>
|
|
||||||
</div>
|
|
||||||
<span className="text-[10px] text-zinc-500 font-bold whitespace-nowrap">
|
|
||||||
{mod.lessons.length} {mod.lessons.length === 1 ? 'aula' : 'aulas'}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Lessons list inside module */}
|
return (
|
||||||
<div className="divide-y divide-white/5">
|
<div
|
||||||
{mod.lessons.length === 0 ? (
|
key={mod.id}
|
||||||
<div className="p-3 text-xs text-zinc-600 text-center italic">Nenhuma aula neste módulo.</div>
|
className="glass-card rounded-xl overflow-hidden border border-white/5"
|
||||||
) : (
|
>
|
||||||
mod.lessons.map(lesson => {
|
{/* Module Title bar (Clickable) */}
|
||||||
const isActive = activeLesson?.id === lesson.id;
|
<button
|
||||||
const isLsnCompleted = lesson.progress?.completed;
|
onClick={() => toggleModule(mod.id)}
|
||||||
|
className="w-full text-left px-4 py-3.5 flex items-center justify-between hover:bg-white/5 transition-colors"
|
||||||
|
>
|
||||||
|
<div className="flex items-center space-x-3">
|
||||||
|
{isModuleFullyCompleted ? (
|
||||||
|
<CheckCircle2 className="w-4 h-4 text-emerald-500 fill-emerald-500/10 flex-shrink-0" />
|
||||||
|
) : (
|
||||||
|
<Circle className="w-4 h-4 text-[#E50914] flex-shrink-0" />
|
||||||
|
)}
|
||||||
|
<div>
|
||||||
|
<h4 className="font-sans font-bold text-xs text-zinc-200 tracking-tight leading-snug line-clamp-1">
|
||||||
|
{mod.title}
|
||||||
|
</h4>
|
||||||
|
<p className="text-[10px] text-zinc-500 mt-0.5">
|
||||||
|
{moduleTotal} {moduleTotal === 1 ? 'aula' : 'aulas'}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
|
||||||
return (
|
{/* Lessons list inside module */}
|
||||||
<button
|
{isExpanded && (
|
||||||
key={lesson.id}
|
<div className="px-4 pb-3">
|
||||||
onClick={() => handleSelectLesson(lesson)}
|
<div className="ml-2 border-l border-zinc-700 pl-4 py-1 space-y-1">
|
||||||
className={`w-full px-4 py-3 text-left flex items-start space-x-3 transition-colors ${
|
{mod.lessons.length === 0 ? (
|
||||||
isActive
|
<div className="py-2 text-xs text-zinc-600 italic">Nenhuma aula neste módulo.</div>
|
||||||
? 'bg-[#E50914]/10 text-white'
|
) : (
|
||||||
: 'hover:bg-zinc-800/40 text-zinc-400 hover:text-zinc-200'
|
mod.lessons.map(lesson => {
|
||||||
}`}
|
const isActive = activeLesson?.id === lesson.id;
|
||||||
>
|
const isLsnCompleted = lesson.progress?.completed;
|
||||||
<div className="mt-0.5 flex-shrink-0">
|
|
||||||
{isLsnCompleted ? (
|
|
||||||
<CheckCircle2 className="w-4 h-4 text-emerald-500 fill-emerald-500/10" />
|
|
||||||
) : (
|
|
||||||
<Circle className="w-4 h-4 text-zinc-600" />
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="space-y-0.5">
|
return (
|
||||||
<span className={`font-medium text-xs leading-snug line-clamp-2 ${isActive ? 'text-[#E50914] font-bold' : ''}`}>
|
<button
|
||||||
{lesson.title}
|
key={lesson.id}
|
||||||
</span>
|
onClick={() => handleSelectLesson(lesson, mod.id)}
|
||||||
{lesson.progress?.watchedPercentage > 0 && !isLsnCompleted && (
|
className="relative w-full py-2.5 text-left flex items-start space-x-3 group"
|
||||||
<div className="flex items-center space-x-1.5 pt-1">
|
>
|
||||||
<div className="w-16 bg-zinc-800 h-1 rounded-full overflow-hidden">
|
{/* Timeline Dot indicator overlaying the border */}
|
||||||
<div
|
<div className={`absolute -left-[21px] top-3.5 w-2 h-2 rounded-full ring-4 ring-[#141414] ${
|
||||||
className="bg-amber-500 h-full"
|
isLsnCompleted ? 'bg-emerald-500' : isActive ? 'bg-[#E50914]' : 'bg-zinc-600'
|
||||||
style={{ width: `${lesson.progress.watchedPercentage}%` }}
|
}`} />
|
||||||
/>
|
|
||||||
|
<div className="space-y-0.5">
|
||||||
|
<span className={`font-medium text-xs leading-snug line-clamp-2 transition-colors ${
|
||||||
|
isActive ? 'text-[#E50914] font-bold' : 'text-zinc-400 group-hover:text-zinc-200'
|
||||||
|
}`}>
|
||||||
|
{lesson.order}. {lesson.title}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<span className="text-[9px] text-zinc-500 font-bold">{lesson.progress.watchedPercentage}%</span>
|
</button>
|
||||||
</div>
|
);
|
||||||
)}
|
})
|
||||||
</div>
|
)}
|
||||||
</button>
|
|
||||||
);
|
|
||||||
})
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Module Activity Button */}
|
{/* Module Activity Button (End of Timeline) */}
|
||||||
{mod.lessons.length > 0 && (
|
{mod.lessons.length > 0 && (
|
||||||
<div className="p-3 border-t border-white/5 bg-black/20">
|
<div className="relative pt-4 pb-1">
|
||||||
<button
|
<div className="absolute -left-[21px] top-[22px] w-2 h-2 rounded-full bg-zinc-700 ring-4 ring-[#141414]" />
|
||||||
onClick={() => setActiveActivityModuleId(mod.id)}
|
<button
|
||||||
className="w-full flex items-center justify-center space-x-2 glass-btn-primary hover:scale-[1.02] text-white transition-all py-2.5 px-3 rounded-lg text-xs font-extrabold uppercase tracking-wider cursor-pointer shadow-md group"
|
onClick={() => isModuleFullyCompleted && setActiveActivityModuleId(mod.id)}
|
||||||
>
|
disabled={!isModuleFullyCompleted}
|
||||||
<Award className="w-4 h-4 text-white flex-shrink-0" />
|
className={`w-full flex items-center justify-center space-x-2 transition-all py-2.5 px-3 rounded-lg text-xs font-extrabold uppercase tracking-wider shadow-md border ${
|
||||||
<span className="truncate">Avaliação do Módulo {modIdx + 1}: {mod.title}</span>
|
isModuleFullyCompleted
|
||||||
</button>
|
? 'glass-btn-primary hover:scale-[1.02] text-white cursor-pointer border-transparent'
|
||||||
|
: 'bg-zinc-800/50 text-zinc-500 cursor-not-allowed border-zinc-700/50'
|
||||||
|
}`}
|
||||||
|
title={!isModuleFullyCompleted ? "Conclua todas as aulas deste módulo para liberar a avaliação" : ""}
|
||||||
|
>
|
||||||
|
<Award className="w-4 h-4 flex-shrink-0" />
|
||||||
|
<span className="truncate">Avaliação do Módulo</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
);
|
||||||
))}
|
})}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue