From 963160d16539c567f1a5934e9cf1f3a8c76abd77 Mon Sep 17 00:00:00 2001 From: dwindown Date: Sun, 4 Jan 2026 10:47:59 +0700 Subject: [PATCH] Improve Bootcamp timeline with collapsible modules and HTML support MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add collapsible structure: Module → Lesson → Timeline (as sublesson) - Support HTML in timeline titles with DOMPurify sanitization - Add inline code styling for timeline content - Fix vertical alignment (items-start instead of items-center) - Add soft borders between timeline items - Change layout from 2-column grid to single column (video full width, timeline below) - Align Bootcamp page layout with WebinarRecording page 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 --- src/components/TimelineChapters.tsx | 31 ++-- src/index.css | 6 + src/pages/Bootcamp.tsx | 225 ++++++++++++++++++++++------ 3 files changed, 204 insertions(+), 58 deletions(-) diff --git a/src/components/TimelineChapters.tsx b/src/components/TimelineChapters.tsx index 694ff3f..d36805f 100644 --- a/src/components/TimelineChapters.tsx +++ b/src/components/TimelineChapters.tsx @@ -1,6 +1,7 @@ import { Clock } from 'lucide-react'; import { Button } from '@/components/ui/button'; import { Card } from '@/components/ui/card'; +import DOMPurify from 'dompurify'; interface VideoChapter { time: number; // Time in seconds @@ -58,9 +59,10 @@ export function TimelineChapters({ {/* Scrollable chapter list with max-height */} -
+
{chapters.map((chapter, index) => { const active = isChapterActive(index); + const isLast = index === chapters.length - 1; return ( - ); - })} + return ( +
+ {/* Module Header - Collapsible */} + + + {/* Lessons - Hidden if module is collapsed */} + {!isModuleCollapsed && ( +
+ {module.lessons.map((lesson) => { + const isCompleted = isLessonCompleted(lesson.id); + const isSelected = selectedLesson?.id === lesson.id; + const isReleased = !lesson.release_at || new Date(lesson.release_at) <= new Date(); + const isLessonCollapsed = collapsedLessons.has(lesson.id); + const hasChapters = lesson.chapters && lesson.chapters.length > 0; + + return ( +
+ {/* Lesson Button - Collapsible if has chapters */} + + + {/* Timeline Chapters - Shown if lesson is expanded */} + {hasChapters && !isLessonCollapsed && ( +
+ {lesson.chapters?.map((chapter, index) => { + const isChapterActive = currentTime >= chapter.time && + (!lesson.chapters?.[index + 1] || currentTime < lesson.chapters[index + 1].time); + + return ( + + ); + })} +
+ )} +
+ ); + })} +
+ )}
-
- ))} + ); + })}
);