Fix bootcamp video reloading issue and duplicate component error
- Moved VideoPlayer component outside main Bootcamp component to prevent re-creation on every render - Memoized video source object and URL values to ensure stability - Removed duplicate Bootcamp component declaration that caused build failure - Video player now persists across Bootcamp re-renders, fixing continuous reload from 00:00 - Timeline clicking now works correctly without triggering video reload 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -64,149 +64,6 @@ interface UserReview {
|
|||||||
created_at: string;
|
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<Product | null>(null);
|
|
||||||
const [modules, setModules] = useState<Module[]>([]);
|
|
||||||
const [progress, setProgress] = useState<Progress[]>([]);
|
|
||||||
const [selectedLesson, setSelectedLesson] = useState<Lesson | null>(null);
|
|
||||||
const [loading, setLoading] = useState(true);
|
|
||||||
const [sidebarOpen, setSidebarOpen] = useState(true);
|
|
||||||
const [mobileMenuOpen, setMobileMenuOpen] = useState(false);
|
|
||||||
const [userReview, setUserReview] = useState<UserReview | null>(null);
|
|
||||||
const [reviewModalOpen, setReviewModalOpen] = useState(false);
|
|
||||||
const [currentTime, setCurrentTime] = useState(0);
|
|
||||||
const [accentColor, setAccentColor] = useState<string>('');
|
|
||||||
const playerRef = useRef<VideoPlayerRef>(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')
|
|
||||||
.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('platform_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,
|
|
||||||
m3u8_url,
|
|
||||||
mp4_url,
|
|
||||||
video_host,
|
|
||||||
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);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// Helper function to get YouTube embed URL
|
// Helper function to get YouTube embed URL
|
||||||
const getYouTubeEmbedUrl = (url: string): string => {
|
const getYouTubeEmbedUrl = (url: string): string => {
|
||||||
const match = url.match(/(?:youtube\.com\/(?:watch\?v=|embed\/)|youtu\.be\/)([^&\s]+)/);
|
const match = url.match(/(?:youtube\.com\/(?:watch\?v=|embed\/)|youtu\.be\/)([^&\s]+)/);
|
||||||
|
|||||||
Reference in New Issue
Block a user