ggop213's picture
fix(ui): eradicate all AI sparkle icons, 4-point stars, and cartoon flare symbols site-wide
4a4de05
Raw History Blame Contribute Delete
40.4 kB
import { useState, useEffect } from 'react';
import type { CourseModuleV2 } from '../../types/getcertV2';
import { NapkinSpinner } from '../NapkinSpinner';
import {
Clock,
CheckCircle2,
ExternalLink,
FileText,
BookOpen,
Paperclip,
ListChecks,
Target,
FlaskConical,
Lock,
ArrowRight,
ArrowLeft,
Tv,
GraduationCap,
Layers,
Link2
} from 'lucide-react';
import { resolveCanonicalCourseTitle } from '../../utils/courseTitleResolver';
interface LearningModuleProps {
courseName: string;
courseSummary: string;
modules: CourseModuleV2[];
onTakeAssessment: () => void;
onBackToDashboard: () => void;
generatingAssessment?: boolean;
completedModuleIds?: string[] | null;
onCompletedModulesChange?: (ids: string[]) => void;
}
export function formatDurationSeconds(totalSeconds: number): string {
if (!totalSeconds || isNaN(totalSeconds) || totalSeconds <= 0) return '';
const hours = Math.floor(totalSeconds / 3600);
const minutes = Math.floor((totalSeconds % 3600) / 60);
const seconds = totalSeconds % 60;
if (hours > 0) {
if (minutes > 0 && seconds > 0) return `${hours}h ${minutes}m ${seconds}s`;
if (minutes > 0) return `${hours}h ${minutes}m`;
if (seconds > 0) return `${hours}h ${seconds}s`;
return `${hours}h`;
}
if (minutes > 0) {
return seconds > 0 ? `${minutes}m ${seconds}s` : `${minutes}m`;
}
return `${seconds}s`;
}
function extractYouTubeVideoId(url: string): string | null {
const value = url.trim();
const patterns = [
/youtube\.com\/watch\?v=([a-zA-Z0-9_-]{11})/,
/youtu\.be\/([a-zA-Z0-9_-]{11})/,
/youtube\.com\/embed\/([a-zA-Z0-9_-]{11})/,
/youtube\.com\/shorts\/([a-zA-Z0-9_-]{11})/,
];
for (const pattern of patterns) {
const match = value.match(pattern);
if (match?.[1]) {
return match[1];
}
}
return null;
}
export function LearningModule({
courseName,
courseSummary,
modules,
onTakeAssessment,
onBackToDashboard,
generatingAssessment = false,
completedModuleIds = null,
onCompletedModulesChange,
}: LearningModuleProps) {
const [localCompleted, setLocalCompleted] = useState<Set<string>>(new Set());
const completedModules = new Set<string>(
completedModuleIds !== null && completedModuleIds !== undefined ? completedModuleIds : [...localCompleted],
);
// Active module currently being watched/studied
const [activeModuleId, setActiveModuleId] = useState<string>(() => {
if (!modules || modules.length === 0) return '';
const firstUncompleted = modules.find((m) => !completedModules.has(m.id));
return firstUncompleted ? firstUncompleted.id : modules[0].id;
});
// Active tab in the lesson briefing section
const [activeTab, setActiveTab] = useState<'notes' | 'takeaways' | 'resources' | 'syllabus'>('notes');
const activeModule = modules.find((m) => m.id === activeModuleId) || modules[0] || null;
const activeModuleIndex = modules.findIndex((m) => m.id === (activeModule?.id || ''));
const markModuleComplete = (moduleId: string) => {
if (completedModules.has(moduleId)) return;
const next = new Set([...completedModules, moduleId]);
if (onCompletedModulesChange) {
onCompletedModulesChange(modules.filter((m) => next.has(m.id)).map((m) => m.id));
} else {
setLocalCompleted(next);
}
};
const allModulesCompleted = modules.length > 0 && completedModules.size === modules.length;
const isActiveCompleted = activeModule ? completedModules.has(activeModule.id) : false;
// Extract video and other resources for active module
const activeResources = activeModule?.resources || [];
const parsedVideoResources = activeResources.map((resource) => ({
resource,
videoId: resource.type === 'youtube' ? extractYouTubeVideoId(resource.url) : null,
}));
const primaryVideo = parsedVideoResources.find((item) => !!item.videoId) || null;
const secondaryVideos = parsedVideoResources.filter((item) => !!item.videoId && item !== primaryVideo);
const linkedResources = parsedVideoResources.filter((item) => !item.videoId);
const completionPercent = modules.length === 0 ? 0 : Math.round((completedModules.size / modules.length) * 100);
// Map of videoId -> formatted duration string (e.g. '26m 52s', '20h 47m')
const [videoDurations, setVideoDurations] = useState<Record<string, string>>({});
// 1. Initial resolution from pre-cached or resource duration metadata
useEffect(() => {
const initialMap: Record<string, string> = {};
modules.forEach((mod) => {
mod.resources?.forEach((res) => {
if (res.type === 'youtube') {
const id = extractYouTubeVideoId(res.url);
if (
id &&
res.duration &&
!res.duration.includes('>20m') &&
!res.duration.includes('Full Course')
) {
initialMap[id] = res.duration;
}
}
});
});
if (Object.keys(initialMap).length > 0) {
setVideoDurations((prev) => ({ ...initialMap, ...prev }));
}
}, [modules]);
// 2. Query /api/v2/video-info for exact server-verified duration
useEffect(() => {
const videoIdsToFetch = new Set<string>();
modules.forEach((mod) => {
mod.resources?.forEach((res) => {
if (res.type === 'youtube') {
const id = extractYouTubeVideoId(res.url);
const current = videoDurations[id] || (res as any)?.duration || '';
if (
id &&
(!current ||
current.includes('>20m') ||
current.includes('Full Course') ||
current.includes('Loading'))
) {
videoIdsToFetch.add(id);
}
}
});
});
videoIdsToFetch.forEach((vid) => {
fetch(`/api/v2/video-info?videoId=${encodeURIComponent(vid)}`)
.then((res) => (res.ok ? res.json() : null))
.then((payload) => {
if (payload?.success && payload?.data?.durationFormatted) {
setVideoDurations((prev) => ({
...prev,
[vid]: payload.data.durationFormatted,
}));
}
})
.catch(() => {});
});
}, [modules, videoDurations]);
// 3. Client-side YouTube Iframe postMessage listener (real-time zero latency)
useEffect(() => {
const handleMessage = (event: MessageEvent) => {
try {
if (
typeof event.origin === 'string' &&
!event.origin.includes('youtube.com') &&
!event.origin.includes('youtube-nocookie.com')
) {
return;
}
const data = typeof event.data === 'string' ? JSON.parse(event.data) : event.data;
const currentVid = primaryVideo?.videoId;
if (data?.event === 'infoDelivery' && data?.info?.duration && currentVid) {
const seconds = Math.round(data.info.duration);
if (seconds > 0) {
const formatted = formatDurationSeconds(seconds);
setVideoDurations((prev) => {
if (prev[currentVid] === formatted) return prev;
return { ...prev, [currentVid]: formatted };
});
}
}
} catch {}
};
window.addEventListener('message', handleMessage);
return () => window.removeEventListener('message', handleMessage);
}, [primaryVideo?.videoId]);
const activeVideoId = primaryVideo?.videoId || null;
const rawActiveDur = activeVideoId
? (videoDurations[activeVideoId] || (primaryVideo?.resource as any)?.duration || '')
: '';
const activeDurationFormatted =
rawActiveDur && !rawActiveDur.includes('>20m') && !rawActiveDur.includes('Full Course')
? rawActiveDur
: '';
const displayCourseName = resolveCanonicalCourseTitle(courseName);
return (
<div className="bg-slate-50 min-h-screen text-slate-900 font-sans pb-28 lg:pb-16">
{/* 1. Institutional Header Strip */}
<header className="border-b border-slate-200 bg-white/95 backdrop-blur-md sticky top-0 z-40 px-3 sm:px-6 py-2.5 sm:py-3 shadow-xs">
<div className="max-w-7xl mx-auto flex items-center justify-between gap-2">
{/* Back button + Course Title */}
<div className="flex items-center gap-2.5 sm:gap-3 min-w-0">
<button
onClick={onBackToDashboard}
className="inline-flex items-center gap-1.5 text-xs font-semibold text-slate-700 hover:text-slate-900 px-2.5 sm:px-3.5 py-1.5 rounded-lg bg-slate-50 hover:bg-slate-100 border border-slate-200 transition-colors shrink-0"
title="Return to Dashboard"
>
<ArrowLeft className="w-3.5 h-3.5" />
<span className="hidden sm:inline">Back to Dashboard</span>
<span className="sm:hidden">Exit</span>
</button>
<div className="h-4 w-px bg-slate-200 shrink-0"></div>
<div className="min-w-0 flex-1">
<div className="hidden sm:flex items-center gap-1.5">
<span className="w-1.5 h-1.5 rounded-full bg-blue-600" />
<span className="text-[10px] sm:text-[11px] font-semibold uppercase tracking-wider text-slate-500 truncate">
ACTIVE ACCREDITATION TRACK
</span>
</div>
<div className="flex items-center gap-1.5 min-w-0">
<h2 className="text-sm sm:text-base md:text-lg font-bold text-slate-900 leading-tight truncate tracking-tight" title={displayCourseName}>
{displayCourseName}
</h2>
</div>
</div>
</div>
{/* Progress Indicator */}
<div className="flex items-center gap-2.5 shrink-0">
<div className="flex items-center gap-1.5 bg-blue-50 px-2.5 py-1 rounded-full border border-blue-200/80 text-xs">
<span className="text-[11px] sm:text-xs text-[#132F50] font-semibold">
{completionPercent}% Ready
</span>
</div>
</div>
</div>
</header>
{/* 2. Main Studio Workspace */}
<main className="max-w-7xl mx-auto px-0 sm:px-4 lg:px-6 pt-0 sm:pt-6">
<div className="grid grid-cols-1 lg:grid-cols-12 gap-0 lg:gap-6 items-start">
{/* LEFT: Cinema Video Stage + Interactive Lesson Workbook (12 cols mobile, 8 cols desktop) */}
<div className="lg:col-span-8 flex flex-col">
{/* VIDEO PLAYER CHASSIS */}
<div className="relative">
<div className="overflow-hidden bg-slate-950 sm:p-2 sm:rounded-2xl border border-slate-800 shadow-sm relative">
{/* 16:9 Video Canvas */}
<div className="relative w-full aspect-video bg-black overflow-hidden sm:rounded-xl">
{primaryVideo?.videoId ? (
<iframe
id="course-video-iframe"
className="absolute inset-0 w-full h-full"
src={`https://www.youtube-nocookie.com/embed/${primaryVideo.videoId}?rel=0&modestbranding=1&enablejsapi=1`}
title={primaryVideo.resource.title}
loading="eager"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
referrerPolicy="strict-origin-when-cross-origin"
allowFullScreen
/>
) : (
<div className="absolute inset-0 flex flex-col items-center justify-center p-6 text-center text-slate-300">
<div className="w-12 h-12 rounded-full bg-slate-800 border border-slate-700 flex items-center justify-center text-xl mb-3 text-slate-400">
<Tv className="w-6 h-6" />
</div>
<h4 className="font-bold text-sm text-white mb-1">
Lecture Reference Available
</h4>
<p className="text-xs text-slate-400 max-w-sm mb-4">
{activeModule?.title || 'This module contains direct interactive reading references.'}
</p>
{activeResources[0]?.url && (
<a
href={activeResources[0].url}
target="_blank"
rel="noreferrer"
className="bg-[#132F50] hover:bg-[#0e233d] text-white text-xs font-semibold px-4 py-2 rounded-lg flex items-center gap-1.5 transition-colors"
>
<span>Open Resource</span>
<ExternalLink className="w-3.5 h-3.5" />
</a>
)}
</div>
)}
</div>
{/* Video Sub-Bar */}
<div className="bg-slate-950 text-white px-3 sm:px-4 py-2 sm:py-2.5 flex items-center justify-between text-xs border-t sm:border-t-0 sm:mt-1 sm:rounded-lg border-slate-800">
<div className="flex items-center gap-2 truncate">
{activeDurationFormatted && (
<span className="bg-slate-800 text-blue-300 text-[10px] px-2 py-0.5 rounded font-mono font-medium shrink-0">
{activeDurationFormatted}
</span>
)}
<span className="text-xs font-medium text-slate-300 truncate">
{primaryVideo?.resource.title || activeModule?.title}
</span>
</div>
{primaryVideo?.resource.url && (
<div className="flex items-center gap-3 shrink-0 ml-2">
<a
href={primaryVideo.resource.url}
target="_blank"
rel="noreferrer"
className="text-[11px] text-blue-400 hover:text-blue-300 flex items-center gap-1 font-medium transition-colors"
>
<span>YouTube Source</span>
<ExternalLink className="w-3 h-3" />
</a>
</div>
)}
</div>
</div>
</div>
{/* LESSON DETAILS & ACTION RIBBON */}
<div className="px-4 py-4 sm:px-0">
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-3 pb-4 border-b border-slate-200">
<div>
<div className="flex items-center gap-2 mb-1.5 flex-wrap">
<span className="bg-blue-50 text-[#132F50] border border-blue-200/80 font-semibold text-[10px] px-2.5 py-0.5 rounded-full uppercase tracking-wider">
Module {activeModuleIndex + 1} of {modules.length}
</span>
<span className="text-xs text-slate-600 font-medium inline-flex items-center gap-1 bg-slate-100 px-2 py-0.5 rounded-md">
<Clock className="w-3.5 h-3.5 text-slate-500 shrink-0" />
{activeDurationFormatted ? (
<span>{activeDurationFormatted}</span>
) : (
<span className="text-slate-400 font-mono text-xs">Runtime indexing...</span>
)}
</span>
{isActiveCompleted && (
<span className="bg-emerald-50 text-emerald-800 text-[10px] px-2.5 py-0.5 rounded-full border border-emerald-200 font-semibold flex items-center gap-1">
<CheckCircle2 className="w-3 h-3 text-emerald-600 shrink-0" />
<span>Completed</span>
</span>
)}
</div>
<h1 className="text-lg sm:text-xl font-extrabold text-slate-900 leading-tight">
{activeModule?.title}
</h1>
</div>
{/* Desktop Quick Complete Button */}
<div className="hidden sm:flex items-center gap-2 shrink-0">
{activeModule && (
isActiveCompleted ? (
<div className="bg-emerald-50 text-emerald-800 border border-emerald-200 text-xs font-semibold px-3 py-1.5 rounded-lg flex items-center gap-1.5">
<CheckCircle2 className="w-3.5 h-3.5 text-emerald-600" />
<span>Module Completed</span>
</div>
) : (
<button
onClick={() => markModuleComplete(activeModule.id)}
className="bg-[#132F50] hover:bg-[#0e233d] text-white font-semibold text-xs px-4 py-2 rounded-lg flex items-center gap-1.5 transition-all shadow-xs cursor-pointer active:scale-[0.98]"
>
<CheckCircle2 className="w-3.5 h-3.5" />
<span>Mark Module Done</span>
</button>
)
)}
</div>
</div>
{/* SEGMENTED TABS */}
<div className="mt-4 flex items-center gap-2 overflow-x-auto pb-1 text-xs font-semibold scrollbar-none">
<button
onClick={() => setActiveTab('notes')}
className={`px-3.5 py-2 rounded-lg shrink-0 flex items-center gap-1.5 transition-all cursor-pointer ${
activeTab === 'notes'
? 'bg-[#132F50] text-white shadow-2xs'
: 'bg-white text-slate-600 hover:bg-slate-100 border border-slate-200'
}`}
>
<BookOpen className="w-3.5 h-3.5" />
<span>Lesson Notes</span>
</button>
<button
onClick={() => setActiveTab('takeaways')}
className={`px-3.5 py-2 rounded-lg shrink-0 flex items-center gap-1.5 transition-all cursor-pointer ${
activeTab === 'takeaways'
? 'bg-[#132F50] text-white shadow-2xs'
: 'bg-white text-slate-600 hover:bg-slate-100 border border-slate-200'
}`}
>
<Layers className="w-3.5 h-3.5" />
<span>Key Principles</span>
</button>
<button
onClick={() => setActiveTab('resources')}
className={`px-3.5 py-2 rounded-lg shrink-0 flex items-center gap-1.5 transition-all cursor-pointer ${
activeTab === 'resources'
? 'bg-[#132F50] text-white shadow-2xs'
: 'bg-white text-slate-600 hover:bg-slate-100 border border-slate-200'
}`}
>
<Paperclip className="w-3.5 h-3.5" />
<span>Documentation & References ({linkedResources.length})</span>
</button>
{/* Full Syllabus Tab for mobile/tablets */}
<button
onClick={() => setActiveTab('syllabus')}
className={`lg:hidden px-3.5 py-2 rounded-lg shrink-0 flex items-center gap-1.5 transition-all cursor-pointer ${
activeTab === 'syllabus'
? 'bg-blue-600 text-white shadow-2xs'
: 'bg-blue-50 text-blue-900 border border-blue-200'
}`}
>
<ListChecks className="w-3.5 h-3.5" />
<span>Syllabus Playlist ({modules.length})</span>
</button>
</div>
{/* TAB CONTENT PANELS */}
<div className="mt-4 space-y-4">
{/* 1. LESSON NOTES TAB */}
{activeTab === 'notes' && (
<div className="bg-white rounded-2xl border border-slate-200/90 p-5 sm:p-6 shadow-xs space-y-4">
<div>
<div className="flex items-center gap-2 mb-2">
<span className="text-sm font-bold text-slate-900 uppercase tracking-wider">
Session Learning Objectives:
</span>
</div>
<p className="text-xs sm:text-sm text-slate-600 leading-relaxed font-normal">
{activeModule?.description || courseSummary}
</p>
</div>
{/* Certification Exam Intel Callout */}
<div className="p-4 bg-blue-50/70 border border-blue-200/80 rounded-xl">
<div className="flex items-start gap-3">
<Target className="w-4 h-4 text-blue-700 shrink-0 mt-0.5" />
<div>
<span className="text-xs font-bold text-blue-950 uppercase tracking-wider block">
Certification Exam Telemetry:
</span>
<p className="text-xs text-blue-900/90 mt-1 leading-relaxed font-normal">
Pay close attention to practical syntax and underlying logic in this video. The final proctored evaluation creates dynamic scenario questions derived directly from this module's technical audio transcripts.
</p>
</div>
</div>
</div>
{/* Secondary video embeds if module has more than one */}
{secondaryVideos.length > 0 && (
<div className="mt-4 pt-4 border-t border-slate-100 space-y-3">
<span className="text-xs font-semibold uppercase text-slate-500 tracking-wider">
Supplemental Video Exercises:
</span>
{secondaryVideos.map(({ resource, videoId }, idx) => (
<div key={idx} className="rounded-xl border border-slate-200 p-3 bg-slate-50">
<div className="aspect-video rounded-lg overflow-hidden bg-black mb-2">
<iframe
src={`https://www.youtube-nocookie.com/embed/${videoId}?rel=0`}
title={resource.title}
className="w-full h-full"
allowFullScreen
/>
</div>
<div className="text-xs font-semibold text-slate-800">{resource.title}</div>
</div>
))}
</div>
)}
</div>
)}
{/* 2. ARCHITECTURE TAKEAWAYS TAB */}
{activeTab === 'takeaways' && (
<div className="bg-white rounded-2xl border border-slate-200/90 p-5 sm:p-6 shadow-xs space-y-4">
<h3 className="text-sm font-bold text-slate-900 uppercase tracking-wider mb-2">
Core Engineering Principles & Best Practices:
</h3>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3 text-xs">
<div className="bg-slate-50 border border-slate-200 rounded-xl p-3.5">
<span className="font-semibold text-slate-900 mb-1 flex items-center gap-1.5">
<Target className="w-3.5 h-3.5 text-blue-600" /> Core Objective
</span>
<p className="text-slate-600 leading-relaxed font-normal mt-1">
Build deep mental models rather than memorizing syntax. Understand failure modes and debugging strategies.
</p>
</div>
<div className="bg-slate-50 border border-slate-200 rounded-xl p-3.5">
<span className="font-semibold text-slate-900 mb-1 flex items-center gap-1.5">
<FlaskConical className="w-3.5 h-3.5 text-blue-600" /> Verification Focus
</span>
<p className="text-slate-600 leading-relaxed font-normal mt-1">
Recruiter-verifiable diplomas grade candidates on situational problem solving, architecture design, and code safety.
</p>
</div>
</div>
<div className="pt-3 border-t border-slate-100">
<h4 className="text-xs font-semibold uppercase text-slate-500 mb-1.5 tracking-wider">Course Synopsis:</h4>
<p className="text-xs text-slate-600 leading-relaxed font-normal">{courseSummary}</p>
</div>
</div>
)}
{/* 3. CHEATSHEETS & ATTACHMENTS TAB */}
{activeTab === 'resources' && (
<div className="bg-white rounded-2xl border border-slate-200/90 p-5 sm:p-6 shadow-xs space-y-3">
<h3 className="text-xs uppercase tracking-wider text-slate-500 font-semibold mb-3 flex items-center gap-1.5">
<Paperclip className="w-3.5 h-3.5" />
<span>Documentation, Code & Official References</span>
</h3>
{linkedResources.length === 0 ? (
<p className="text-xs text-slate-500 italic py-2 font-normal">
All study materials for this module are incorporated directly in the primary video lecture above.
</p>
) : (
<div className="space-y-2.5">
{linkedResources.map(({ resource }, idx) => (
<a
key={idx}
href={resource.url}
target="_blank"
rel="noreferrer"
className="flex items-center justify-between p-3.5 rounded-xl border border-slate-200 hover:border-[#132F50] bg-slate-50/50 hover:bg-blue-50/30 transition-all text-xs group"
>
<div className="flex items-center gap-2.5">
<span className="text-[#132F50] text-sm">
{resource.type === 'documentation' ? (
<FileText className="w-4 h-4" />
) : (
<Link2 className="w-4 h-4" />
)}
</span>
<div>
<div className="font-semibold text-slate-800 group-hover:text-blue-700 transition-colors">
{resource.title}
</div>
<div className="text-[10px] text-slate-400 font-mono">
Type: {resource.type}
</div>
</div>
</div>
<span className="text-xs font-semibold text-[#132F50] group-hover:text-blue-700 flex items-center gap-1">
<span>Open</span>
<ExternalLink className="w-3 h-3" />
</span>
</a>
))}
</div>
)}
</div>
)}
{/* 4. FULL SYLLABUS TAB (MOBILE / TABLET) */}
{activeTab === 'syllabus' && (
<div className="bg-white rounded-2xl border border-slate-200/90 p-5 sm:p-6 shadow-xs space-y-3 lg:hidden">
<h3 className="text-xs uppercase tracking-wider text-slate-500 font-semibold mb-2">
Tap any module to switch video:
</h3>
<div className="space-y-2.5">
{modules.map((module, idx) => {
const isDone = completedModules.has(module.id);
const isCurrent = module.id === activeModuleId;
const modYt = module.resources?.find((r) => r.type === 'youtube');
const modVid = modYt ? extractYouTubeVideoId(modYt.url) : null;
const modDur = modVid ? (videoDurations[modVid] || (modYt as any)?.duration || '') : '';
return (
<div
key={module.id}
onClick={() => {
setActiveModuleId(module.id);
setActiveTab('notes');
window.scrollTo({ top: 0, behavior: 'smooth' });
}}
className={`p-3.5 rounded-xl border transition-all cursor-pointer ${
isCurrent
? 'border-[#132F50] bg-blue-50/70 shadow-xs'
: 'border-slate-200 hover:border-slate-300 bg-white'
}`}
>
<div className="flex items-center justify-between text-[10px] font-mono mb-1">
<span className={isCurrent ? 'text-blue-700 font-bold' : 'text-slate-400'}>
{isCurrent ? 'NOW PLAYING' : `MODULE ${idx + 1}`}
</span>
<div className="flex items-center gap-1.5">
<span className="text-slate-500 font-mono text-[10px] font-medium flex items-center gap-1">
<Clock className="w-2.5 h-2.5 text-slate-400 shrink-0" />
<span>{modDur || (isCurrent && activeDurationFormatted) || 'Loading...'}</span>
</span>
{isDone && (
<span className="text-emerald-700 font-semibold bg-emerald-50 px-1.5 py-0.5 rounded border border-emerald-200 flex items-center gap-0.5">
<CheckCircle2 className="w-2.5 h-2.5 text-emerald-600" />
<span>Done</span>
</span>
)}
</div>
</div>
<h4 className="text-xs font-bold text-slate-900">{module.title}</h4>
<p className="text-[11px] text-slate-500 mt-1 line-clamp-2">{module.description}</p>
</div>
);
})}
</div>
</div>
)}
</div>
</div>
</div>
{/* RIGHT: Syllabus Workbook & Exam Gate (Desktop 4 cols, Sticky) */}
<div className="hidden lg:flex lg:col-span-4 flex-col gap-5 sticky top-20">
{/* Syllabus Playlist Box */}
<div className="bg-white rounded-2xl border border-slate-200/90 p-5 shadow-xs">
<div className="flex items-center justify-between pb-3 border-b border-slate-100 mb-3">
<div>
<span className="text-[10px] uppercase tracking-wider text-slate-500 font-semibold block">
Curriculum Playlist
</span>
<h3 className="text-sm font-bold text-slate-900">Module Lessons</h3>
</div>
<span className="bg-blue-50 text-[#132F50] border border-blue-200/80 text-[10px] px-2.5 py-0.5 rounded-full font-semibold">
{completedModules.size} / {modules.length} Completed
</span>
</div>
{/* Modules List */}
<div className="space-y-2.5 max-h-[420px] overflow-y-auto pr-1">
{modules.map((module, idx) => {
const isDone = completedModules.has(module.id);
const isCurrent = module.id === activeModuleId;
const modYt = module.resources?.find((r) => r.type === 'youtube');
const modVid = modYt ? extractYouTubeVideoId(modYt.url) : null;
const modDur = modVid ? (videoDurations[modVid] || (modYt as any)?.duration || '') : '';
return (
<div
key={module.id}
onClick={() => setActiveModuleId(module.id)}
className={`p-3 rounded-xl border transition-all cursor-pointer ${
isCurrent
? 'border-[#132F50] bg-blue-50/50 shadow-xs ring-1 ring-[#132F50]/20'
: 'border-slate-200/80 hover:border-slate-300 bg-white'
}`}
>
<div className="flex items-center justify-between text-[10px] font-mono mb-1">
<span className={`flex items-center gap-1.5 ${isCurrent ? 'text-blue-700 font-semibold' : 'text-slate-400'}`}>
{isCurrent ? (
<>
<span className="w-1.5 h-1.5 rounded-full bg-blue-600 animate-pulse"></span>
<span>NOW PLAYING</span>
</>
) : (
<span>MODULE {idx + 1}</span>
)}
</span>
{isDone ? (
<span className="text-emerald-700 font-semibold bg-emerald-50 px-1.5 py-0.5 rounded border border-emerald-200 text-[9px] flex items-center gap-0.5">
<CheckCircle2 className="w-2.5 h-2.5 text-emerald-600" />
<span>Done</span>
</span>
) : (
<span className="text-slate-500 font-mono text-[10px] font-medium flex items-center gap-1">
<Clock className="w-2.5 h-2.5 text-slate-400 shrink-0" />
<span>{modDur || (isCurrent && activeDurationFormatted) || 'Loading...'}</span>
</span>
)}
</div>
<h4 className={`text-xs font-bold leading-snug ${isCurrent ? 'text-slate-900' : 'text-slate-700'}`}>
{idx + 1}. {module.title}
</h4>
<p className="text-[11px] text-slate-500 mt-1 leading-tight line-clamp-2">
{module.description}
</p>
</div>
);
})}
</div>
</div>
{/* Certification Exam Gate Card */}
<div id="milestone-gate" className="bg-white rounded-2xl border border-slate-200/90 p-5 shadow-xs">
<h3 className="text-sm font-bold text-slate-900">
Certification Exam Gate
</h3>
<p className="text-xs text-slate-500 mt-1 leading-relaxed font-normal">
Pass mark: 60% or higher. Complete all curriculum modules to unlock your live dynamic proctored assessment.
</p>
<div className="mt-4 pt-3 border-t border-slate-100">
<div className="flex justify-between text-xs mb-1.5">
<span className="text-slate-500 font-medium">Readiness Threshold</span>
<span className="text-[#132F50] font-bold">{completionPercent}%</span>
</div>
<div className="w-full bg-slate-100 rounded-full h-2 overflow-hidden">
<div
className="bg-[#132F50] h-full rounded-full transition-all duration-500"
style={{ width: `${completionPercent}%` }}
/>
</div>
</div>
{allModulesCompleted ? (
<button
onClick={onTakeAssessment}
disabled={generatingAssessment}
className="mt-4 w-full bg-emerald-600 hover:bg-emerald-700 disabled:opacity-50 text-white rounded-xl font-semibold text-xs py-3 transition-all flex items-center justify-center gap-2 shadow-xs cursor-pointer active:scale-[0.98]"
>
{generatingAssessment ? (
<>
<NapkinSpinner size="sm" />
<span>Synthesizing examination sheet...</span>
</>
) : (
<span className="flex items-center gap-1.5">
<GraduationCap className="w-4 h-4" />
<span>Start Certification Exam Now</span>
</span>
)}
</button>
) : (
<button
disabled
className="mt-4 w-full bg-slate-100 text-slate-400 border border-slate-200 rounded-xl py-2.5 text-xs font-medium cursor-not-allowed flex items-center justify-center gap-1.5"
>
<Lock className="w-3.5 h-3.5" />
<span>Complete {modules.length - completedModules.size} More {modules.length - completedModules.size === 1 ? 'Module' : 'Modules'} to Unlock</span>
</button>
)}
</div>
</div>
</div>
</main>
{/* 3. Mobile Docked Bottom Bar */}
<div className="lg:hidden fixed bottom-0 inset-x-0 bg-white/95 backdrop-blur-md border-t border-slate-200 px-4 py-3 z-50 flex items-center justify-between shadow-lg">
<div>
<div className="text-[10px] font-semibold text-slate-500 uppercase">
Module {activeModuleIndex + 1} of {modules.length}
</div>
<div className="text-xs font-bold text-slate-900 truncate max-w-[190px]">
{activeModule?.title}
</div>
</div>
{allModulesCompleted ? (
<button
onClick={onTakeAssessment}
disabled={generatingAssessment}
className="bg-emerald-600 hover:bg-emerald-700 text-white text-xs font-semibold px-4 py-2 rounded-lg flex items-center gap-1.5 shadow-xs cursor-pointer active:scale-[0.98]"
>
{generatingAssessment ? (
<span>Preparing...</span>
) : (
<>
<GraduationCap className="w-3.5 h-3.5" />
<span>Take Exam</span>
<ArrowRight className="w-3.5 h-3.5" />
</>
)}
</button>
) : activeModule && !isActiveCompleted ? (
<button
onClick={() => markModuleComplete(activeModule.id)}
className="bg-[#132F50] hover:bg-[#0e233d] text-white text-xs font-semibold px-4 py-2 rounded-lg flex items-center gap-1.5 shadow-xs cursor-pointer active:scale-[0.98]"
>
<CheckCircle2 className="w-3.5 h-3.5" />
<span>Mark Complete</span>
</button>
) : (
<button
onClick={() => {
// Jump to next uncompleted module
const nextMod = modules.find((m) => !completedModules.has(m.id));
if (nextMod) {
setActiveModuleId(nextMod.id);
window.scrollTo({ top: 0, behavior: 'smooth' });
}
}}
className="bg-blue-600 hover:bg-blue-700 text-white text-xs font-semibold px-4 py-2 rounded-lg flex items-center gap-1.5 shadow-xs cursor-pointer active:scale-[0.98]"
>
<span>Next Module</span>
<ArrowRight className="w-3.5 h-3.5" />
</button>
)}
</div>
</div>
);
}