From 2dd9d544eee9b1a73e2a52a44b28f83a37a828df Mon Sep 17 00:00:00 2001 From: dwindown Date: Thu, 25 Dec 2025 23:05:32 +0700 Subject: [PATCH] Add webinar recording page with embedded video player MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Changes: - Create WebinarRecording page with embedded video player - Supports YouTube, Vimeo, Google Drive, and direct MP4 - Check access via user_access or paid orders - Update webinar recording buttons to navigate to page instead of new tab - Add route /webinar/:slug This keeps users on the platform for better UX instead of redirecting to external video sites. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 --- src/App.tsx | 2 + src/pages/Dashboard.tsx | 9 +- src/pages/WebinarRecording.tsx | 188 +++++++++++++++++++++++++++ src/pages/member/MemberAccess.tsx | 9 +- src/pages/member/MemberDashboard.tsx | 5 +- 5 files changed, 202 insertions(+), 11 deletions(-) create mode 100644 src/pages/WebinarRecording.tsx diff --git a/src/App.tsx b/src/App.tsx index bd6f2f9..b656296 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -12,6 +12,7 @@ import Products from "./pages/Products"; import ProductDetail from "./pages/ProductDetail"; import Checkout from "./pages/Checkout"; import Bootcamp from "./pages/Bootcamp"; +import WebinarRecording from "./pages/WebinarRecording"; import Events from "./pages/Events"; import ConsultingBooking from "./pages/ConsultingBooking"; import Calendar from "./pages/Calendar"; @@ -56,6 +57,7 @@ const App = () => ( } /> } /> } /> + } /> } /> } /> } /> diff --git a/src/pages/Dashboard.tsx b/src/pages/Dashboard.tsx index 6e71973..ed834fb 100644 --- a/src/pages/Dashboard.tsx +++ b/src/pages/Dashboard.tsx @@ -97,11 +97,10 @@ export default function Dashboard() { )} {item.product.recording_url && ( - )} diff --git a/src/pages/WebinarRecording.tsx b/src/pages/WebinarRecording.tsx new file mode 100644 index 0000000..7dd7e25 --- /dev/null +++ b/src/pages/WebinarRecording.tsx @@ -0,0 +1,188 @@ +import { useEffect, useState } from 'react'; +import { useParams, useNavigate } from 'react-router-dom'; +import { supabase } from '@/integrations/supabase/client'; +import { useAuth } from '@/hooks/useAuth'; +import { AppLayout } from '@/components/AppLayout'; +import { Button } from '@/components/ui/button'; +import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; +import { Skeleton } from '@/components/ui/skeleton'; +import { toast } from '@/hooks/use-toast'; +import { ChevronLeft, Play } from 'lucide-react'; + +interface Product { + id: string; + title: string; + slug: string; + recording_url: string | null; + description: string | null; +} + +export default function WebinarRecording() { + const { slug } = useParams<{ slug: string }>(); + const navigate = useNavigate(); + const { user, loading: authLoading } = useAuth(); + + const [product, setProduct] = useState(null); + const [loading, setLoading] = useState(true); + + 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, recording_url, description') + .eq('slug', slug) + .eq('type', 'webinar') + .maybeSingle(); + + if (productError || !productData) { + toast({ title: 'Error', description: 'Webinar tidak ditemukan', variant: 'destructive' }); + navigate('/dashboard'); + return; + } + + setProduct(productData); + + if (!productData.recording_url) { + toast({ title: 'Info', description: 'Rekaman webinar belum tersedia', variant: 'destructive' }); + navigate('/dashboard'); + return; + } + + // Check access via user_access or paid orders + const [accessRes, paidOrdersRes] = await Promise.all([ + supabase + .from('user_access') + .select('id') + .eq('user_id', user!.id) + .eq('product_id', productData.id) + .maybeSingle(), + supabase + .from('orders') + .select('order_items!inner(product_id)') + .eq('user_id', user!.id) + .eq('payment_status', 'paid') + ]); + + const hasDirectAccess = !!accessRes.data; + const hasPaidOrderAccess = paidOrdersRes.data?.some((order: any) => + order.order_items?.some((item: any) => item.product_id === productData.id) + ); + + if (!hasDirectAccess && !hasPaidOrderAccess) { + toast({ title: 'Akses ditolak', description: 'Anda tidak memiliki akses ke webinar ini', variant: 'destructive' }); + navigate('/dashboard'); + return; + } + + setLoading(false); + }; + + const getVideoEmbed = (url: string) => { + // YouTube + const youtubeMatch = url.match(/(?:youtube\.com\/(?:watch\?v=|embed\/)|youtu\.be\/)([^&\s]+)/); + if (youtubeMatch) return `https://www.youtube.com/embed/${youtubeMatch[1]}`; + + // Vimeo + const vimeoMatch = url.match(/vimeo\.com\/(\d+)/); + if (vimeoMatch) return `https://player.vimeo.com/video/${vimeoMatch[1]}`; + + // Google Drive + const driveMatch = url.match(/drive\.google\.com\/file\/d\/([^\/]+)/); + if (driveMatch) return `https://drive.google.com/file/d/${driveMatch[1]}/preview`; + + // Direct MP4 or other video files + if (url.match(/\.(mp4|webm|ogg)$/i)) return url; + + return url; + }; + + const isDirectVideo = (url: string) => { + return url.match(/\.(mp4|webm|ogg)$/i) || url.includes('drive.google.com'); + }; + + if (authLoading || loading) { + return ( + +
+ + +
+
+ ); + } + + if (!product) return null; + + const embedUrl = product.recording_url ? getVideoEmbed(product.recording_url) : null; + + return ( + +
+ + + + + {product.title} + + + {/* Video Embed */} + {embedUrl && ( +
+ {isDirectVideo(embedUrl) ? ( + + ) : ( +