import { useEffect, useState, useRef } from 'react'; import { useParams, useNavigate } from 'react-router-dom'; import { supabase } from '@/integrations/supabase/client'; import { useAuth } from '@/hooks/useAuth'; import { Button } from '@/components/ui/button'; import { Card, CardContent } from '@/components/ui/card'; import { Badge } from '@/components/ui/badge'; import { Skeleton } from '@/components/ui/skeleton'; import { toast } from '@/hooks/use-toast'; import { formatDuration } from '@/lib/format'; import { ChevronLeft, ChevronRight, Check, Play, BookOpen, Clock, Menu, Star, CheckCircle } from 'lucide-react'; import { cn } from '@/lib/utils'; import { Sheet, SheetContent, SheetTrigger } from '@/components/ui/sheet'; import { ReviewModal } from '@/components/reviews/ReviewModal'; import { VideoPlayerWithChapters, VideoPlayerRef } from '@/components/VideoPlayerWithChapters'; import { TimelineChapters } from '@/components/TimelineChapters'; import DOMPurify from 'dompurify'; interface VideoChapter { time: number; title: string; } interface Product { id: string; title: string; slug: string; video_source?: string; } interface Module { id: string; title: string; position: number; lessons: Lesson[]; } interface Lesson { id: string; title: string; content: string | null; video_url: string | null; youtube_url: string | null; embed_code: string | null; duration_seconds: number | null; position: number; release_at: string | null; chapters?: VideoChapter[]; } interface Progress { lesson_id: string; completed_at: string; } interface UserReview { id: string; rating: number; title: string; body: string; is_approved: boolean; created_at: string; } export default function Bootcamp() { const { slug, lessonId } = useParams<{ slug: string; lessonId?: string }>(); const navigate = useNavigate(); const { user, loading: authLoading } = useAuth(); const [product, setProduct] = useState(null); const [modules, setModules] = useState([]); const [progress, setProgress] = useState([]); const [selectedLesson, setSelectedLesson] = useState(null); const [loading, setLoading] = useState(true); const [sidebarOpen, setSidebarOpen] = useState(true); const [mobileMenuOpen, setMobileMenuOpen] = useState(false); const [userReview, setUserReview] = useState(null); const [reviewModalOpen, setReviewModalOpen] = useState(false); const [currentTime, setCurrentTime] = useState(0); const [accentColor, setAccentColor] = useState('#f97316'); const playerRef = useRef(null); useEffect(() => { if (!authLoading && !user) { navigate('/auth'); } else if (user && slug) { checkAccessAndFetch(); } }, [user, authLoading, slug]); const checkAccessAndFetch = async () => { const { data: productData, error: productError } = await supabase .from('products') .select('id, title, slug, video_source') .eq('slug', slug) .eq('type', 'bootcamp') .maybeSingle(); if (productError || !productData) { toast({ title: 'Error', description: 'Bootcamp tidak ditemukan', variant: 'destructive' }); navigate('/dashboard'); return; } setProduct(productData); // Fetch accent color from settings const { data: settings } = await supabase .from('site_settings') .select('brand_accent_color') .single(); if (settings?.brand_accent_color) { setAccentColor(settings.brand_accent_color); } const { data: accessData } = await supabase .from('user_access') .select('id') .eq('user_id', user!.id) .eq('product_id', productData.id) .maybeSingle(); if (!accessData) { toast({ title: 'Akses ditolak', description: 'Anda tidak memiliki akses ke bootcamp ini', variant: 'destructive' }); navigate('/dashboard'); return; } const { data: modulesData } = await supabase .from('bootcamp_modules') .select(` id, title, position, bootcamp_lessons ( id, title, content, video_url, youtube_url, embed_code, duration_seconds, position, release_at, chapters ) `) .eq('product_id', productData.id) .order('position'); if (modulesData) { const sortedModules = modulesData.map(m => ({ ...m, lessons: (m.bootcamp_lessons as Lesson[]).sort((a, b) => a.position - b.position) })); setModules(sortedModules); // Select lesson based on URL parameter or default to first lesson const allLessons = sortedModules.flatMap(m => m.lessons); if (lessonId) { // Find the lesson by ID from URL const lessonFromUrl = allLessons.find(l => l.id === lessonId); if (lessonFromUrl) { setSelectedLesson(lessonFromUrl); } else if (allLessons.length > 0) { // If lesson not found, default to first lesson setSelectedLesson(allLessons[0]); } } else if (allLessons.length > 0 && sortedModules[0].lessons.length > 0) { // No lessonId in URL, select first lesson setSelectedLesson(sortedModules[0].lessons[0]); } } const { data: progressData } = await supabase .from('lesson_progress') .select('lesson_id, completed_at') .eq('user_id', user!.id); if (progressData) { setProgress(progressData); } // Check if user has already reviewed this bootcamp const { data: reviewData } = await supabase .from('reviews') .select('id, rating, title, body, is_approved, created_at') .eq('user_id', user!.id) .eq('product_id', productData.id) .order('created_at', { ascending: false }) .limit(1); if (reviewData && reviewData.length > 0) { setUserReview(reviewData[0] as UserReview); } else { setUserReview(null); } setLoading(false); }; const isLessonCompleted = (lessonId: string) => { return progress.some(p => p.lesson_id === lessonId); }; const handleSelectLesson = (lesson: Lesson) => { setSelectedLesson(lesson); // Update URL without full page reload navigate(`/bootcamp/${slug}/${lesson.id}`); }; const markAsCompleted = async () => { if (!selectedLesson || !user || !product) return; const { error } = await supabase .from('lesson_progress') .insert({ user_id: user.id, lesson_id: selectedLesson.id }); if (error) { if (error.code === '23505') { toast({ title: 'Info', description: 'Pelajaran sudah ditandai selesai' }); } else { toast({ title: 'Error', description: 'Gagal menandai selesai', variant: 'destructive' }); } return; } const newProgress = [...progress, { lesson_id: selectedLesson.id, completed_at: new Date().toISOString() }]; setProgress(newProgress); // Calculate completion percentage for notification const completedCount = newProgress.length; const completionPercent = Math.round((completedCount / totalLessons) * 100); // Trigger progress notification at milestones if (completionPercent === 25 || completionPercent === 50 || completionPercent === 75 || completionPercent === 100) { try { await supabase.functions.invoke('send-notification', { body: { template_key: 'bootcamp_progress', recipient_email: user.email, recipient_name: user.user_metadata?.name || 'Peserta', variables: { bootcamp_title: product.title, progress_percent: completionPercent.toString(), completed_lessons: completedCount.toString(), total_lessons: totalLessons.toString(), }, }, }); } catch (err) { console.log('Progress notification skipped:', err); } } toast({ title: 'Selesai!', description: 'Pelajaran ditandai selesai' }); goToNextLesson(); }; const goToNextLesson = () => { if (!selectedLesson) return; const allLessons = modules.flatMap(m => m.lessons); const currentIndex = allLessons.findIndex(l => l.id === selectedLesson.id); if (currentIndex < allLessons.length - 1) { setSelectedLesson(allLessons[currentIndex + 1]); } }; const goToPrevLesson = () => { if (!selectedLesson) return; const allLessons = modules.flatMap(m => m.lessons); const currentIndex = allLessons.findIndex(l => l.id === selectedLesson.id); if (currentIndex > 0) { setSelectedLesson(allLessons[currentIndex - 1]); } }; const getYouTubeEmbedUrl = (url: string): string => { const match = url.match(/(?:youtube\.com\/(?:watch\?v=|embed\/)|youtu\.be\/)([^&\s]+)/); return match ? `https://www.youtube.com/embed/${match[1]}` : url; }; const VideoPlayer = ({ lesson }: { lesson: Lesson }) => { const activeSource = product?.video_source || 'youtube'; const hasChapters = lesson.chapters && lesson.chapters.length > 0; // Get video based on product's active source const getVideoSource = () => { if (activeSource === 'youtube') { if (lesson.youtube_url && lesson.youtube_url.trim()) { return { type: 'youtube', url: lesson.youtube_url, embedUrl: getYouTubeEmbedUrl(lesson.youtube_url) }; } else if (lesson.video_url && lesson.video_url.trim()) { // Fallback to old video_url for backward compatibility return { type: 'youtube', url: lesson.video_url, embedUrl: getYouTubeEmbedUrl(lesson.video_url) }; } else { // Fallback to embed if YouTube not available return lesson.embed_code && lesson.embed_code.trim() ? { type: 'embed', html: lesson.embed_code } : null; } } else { if (lesson.embed_code && lesson.embed_code.trim()) { return { type: 'embed', html: lesson.embed_code }; } else { // Fallback to YouTube if embed not available return lesson.youtube_url && lesson.youtube_url.trim() ? { type: 'youtube', url: lesson.youtube_url, embedUrl: getYouTubeEmbedUrl(lesson.youtube_url) } : lesson.video_url && lesson.video_url.trim() ? { type: 'youtube', url: lesson.video_url, embedUrl: getYouTubeEmbedUrl(lesson.video_url) } : null; } } }; const video = getVideoSource(); // Show warning if no video available if (!video) { return (

Konten tidak tersedia

Video belum dikonfigurasi untuk pelajaran ini.

); } // Render based on video type if (video.type === 'embed') { return (
{hasChapters && (
)}
); } // YouTube with chapters support const isYouTube = video.type === 'youtube'; return (
{hasChapters && (
{ if (playerRef.current) { playerRef.current.jumpToTime(time); } }} currentTime={currentTime} accentColor={accentColor} />
)}
); }; const completedCount = progress.length; const totalLessons = modules.reduce((sum, m) => sum + m.lessons.length, 0); const isBootcampCompleted = totalLessons > 0 && completedCount >= totalLessons; const renderSidebarContent = () => (
{modules.map((module) => (

{module.title}

{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(); return ( ); })}
))}
); if (authLoading || loading) { return (
{[...Array(5)].map((_, i) => ( ))}
); } return (
{/* Header */}

{product?.title}

{completedCount} / {totalLessons} selesai
0 ? (completedCount / totalLessons) * 100 : 0}%` }} />
{/* Mobile menu trigger */}
Kurikulum
{renderSidebarContent()}
{/* Desktop Sidebar */} {/* Toggle sidebar button */} {/* Main content */}
{selectedLesson ? (

{selectedLesson.title}

{selectedLesson.duration_seconds && ( {formatDuration(selectedLesson.duration_seconds)} )}
{selectedLesson.content && (
)}
{isBootcampCompleted ? ( ) : ( )}
{/* Bootcamp completion review prompt */} {isBootcampCompleted && ( {userReview ? ( userReview.is_approved ? ( // Approved review - celebratory display

Ulasan Anda Terbit!

Disetujui

Terima kasih telah berbagi pengalaman Anda. Ulasan Anda membantu peserta lain!

{/* User's review display */}
{[1, 2, 3, 4, 5].map((i) => ( ))}

{userReview.title}

{userReview.body && (

{userReview.body}

)}
Diterbitkan pada {new Date(userReview.created_at).toLocaleDateString('id-ID', { day: 'numeric', month: 'long', year: 'numeric' })}
) : ( // Pending review

Ulasan Anda sedang ditinjau

Terima kasih! Ulasan akan muncul setelah disetujui admin.

) ) : ( // No review yet - prompt to review

🎉 Selamat menyelesaikan bootcamp!

Bagikan pengalaman Anda

)}
)}
) : (

{modules.length === 0 ? 'Belum ada pelajaran tersedia' : 'Pilih pelajaran untuk memulai'}

)}
{/* Review Modal */} {user && product && ( { // Refresh review data const refreshReview = async () => { const { data } = await supabase .from('reviews') .select('id, rating, title, body, is_approved, created_at') .eq('user_id', user.id) .eq('product_id', product.id) .order('created_at', { ascending: false }) .limit(1); if (data && data.length > 0) { setUserReview(data[0] as UserReview); } }; refreshReview(); }} /> )}
); }