Display bootcamp lesson chapters on Product Detail page as marketing content
This commit implements displaying lesson chapters/timeline as marketing content on the Product Detail page for bootcamp products, helping potential buyers understand the detailed breakdown of what they'll learn. ## Changes ### Product Detail Page (src/pages/ProductDetail.tsx) - Updated Lesson interface to include optional chapters property - Modified fetchCurriculum to fetch chapters along with lessons - Enhanced renderCurriculumPreview to display chapters as nested content under lessons - Chapters shown with timestamps and titles, clickable to navigate to bootcamp access page - Visual hierarchy: Module → Lesson → Chapters with proper indentation and styling ### Review System Fixes - Fixed review prompt re-appearing after submission (before admin approval) - Added hasSubmittedReview check to prevent showing prompt when review exists - Fixed edit review functionality to pre-populate form with existing data - ReviewModal now handles both INSERT (new) and UPDATE (edit) operations - Edit resets is_approved to false requiring re-approval ### Video Player Enhancements - Implemented Adilo/Video.js integration for M3U8/HLS playback - Added video progress tracking with refs pattern for reliability - Implemented chapter navigation for both Adilo and YouTube players - Added keyboard shortcuts (Space, Arrows, F, M, J, L) - Resume prompt for returning users with saved progress ### Database Migrations - Added Adilo video support fields (m3u8_url, mp4_url, video_host) - Created video_progress table for tracking user watch progress - Fixed consulting slots user_id foreign key - Added chapters support to products and bootcamp_lessons tables ### Documentation - Added Adilo implementation plan and quick reference docs - Cleaned up transcript analysis files 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
37
supabase/migrations/20250101000001_adilo_video_support.sql
Normal file
37
supabase/migrations/20250101000001_adilo_video_support.sql
Normal file
@@ -0,0 +1,37 @@
|
||||
-- Add Adilo video columns to products table (webinars)
|
||||
ALTER TABLE products
|
||||
ADD COLUMN IF NOT EXISTS m3u8_url TEXT,
|
||||
ADD COLUMN IF NOT EXISTS mp4_url TEXT,
|
||||
ADD COLUMN IF NOT EXISTS video_host TEXT DEFAULT 'youtube',
|
||||
ADD COLUMN IF NOT EXISTS adilo_video_id TEXT;
|
||||
|
||||
-- Add Adilo video columns to bootcamp_lessons table
|
||||
ALTER TABLE bootcamp_lessons
|
||||
ADD COLUMN IF NOT EXISTS m3u8_url TEXT,
|
||||
ADD COLUMN IF NOT EXISTS mp4_url TEXT,
|
||||
ADD COLUMN IF NOT EXISTS video_host TEXT DEFAULT 'youtube',
|
||||
ADD COLUMN IF NOT EXISTS adilo_video_id TEXT;
|
||||
|
||||
-- Add constraint to ensure valid video hosts
|
||||
ALTER TABLE products
|
||||
ADD CONSTRAINT products_video_host_check
|
||||
CHECK (video_host IN ('youtube', 'adilo'));
|
||||
|
||||
ALTER TABLE bootcamp_lessons
|
||||
ADD CONSTRAINT bootcamp_lessons_video_host_check
|
||||
CHECK (video_host IN ('youtube', 'adilo'));
|
||||
|
||||
-- Create indexes for faster queries
|
||||
CREATE INDEX IF NOT EXISTS idx_products_video_host ON products(video_host);
|
||||
CREATE INDEX IF NOT EXISTS idx_bootcamp_lessons_video_host ON bootcamp_lessons(video_host);
|
||||
|
||||
-- Comments for documentation
|
||||
COMMENT ON COLUMN products.m3u8_url IS 'M3U8 streaming URL from Adilo for HLS playback';
|
||||
COMMENT ON COLUMN products.mp4_url IS 'MP4 fallback URL from Adilo for direct download/legacy browsers';
|
||||
COMMENT ON COLUMN products.video_host IS 'Video hosting platform: youtube or adilo';
|
||||
COMMENT ON COLUMN products.adilo_video_id IS 'Adilo video identifier for API reference';
|
||||
|
||||
COMMENT ON COLUMN bootcamp_lessons.m3u8_url IS 'M3U8 streaming URL from Adilo for HLS playback';
|
||||
COMMENT ON COLUMN bootcamp_lessons.mp4_url IS 'MP4 fallback URL from Adilo for direct download/legacy browsers';
|
||||
COMMENT ON COLUMN bootcamp_lessons.video_host IS 'Video hosting platform: youtube or adilo';
|
||||
COMMENT ON COLUMN bootcamp_lessons.adilo_video_id IS 'Adilo video identifier for API reference';
|
||||
@@ -0,0 +1,94 @@
|
||||
-- Fix consulting_slots table: ensure user_id column exists, backfill from orders, and add RLS policies
|
||||
-- This fixes the 400 error when members try to fetch their consulting slots
|
||||
|
||||
-- Add user_id column if it doesn't exist
|
||||
DO $$
|
||||
BEGIN
|
||||
IF NOT EXISTS (
|
||||
SELECT 1 FROM information_schema.columns
|
||||
WHERE table_name = 'consulting_slots'
|
||||
AND column_name = 'user_id'
|
||||
) THEN
|
||||
ALTER TABLE consulting_slots
|
||||
ADD COLUMN user_id UUID REFERENCES auth.users(id) ON DELETE CASCADE;
|
||||
|
||||
RAISE NOTICE 'user_id column added to consulting_slots';
|
||||
ELSE
|
||||
RAISE NOTICE 'user_id column already exists in consulting_slots';
|
||||
END IF;
|
||||
END $$;
|
||||
|
||||
-- Backfill user_id from orders for existing records
|
||||
DO $$
|
||||
DECLARE
|
||||
backfill_count INTEGER;
|
||||
null_count INTEGER;
|
||||
BEGIN
|
||||
-- Count NULL user_ids before backfill
|
||||
SELECT COUNT(*) INTO null_count FROM consulting_slots WHERE user_id IS NULL;
|
||||
RAISE NOTICE 'Found % consulting_slots with NULL user_id', null_count;
|
||||
|
||||
-- Backfill from orders
|
||||
UPDATE consulting_slots cs
|
||||
SET user_id = o.user_id
|
||||
FROM orders o
|
||||
WHERE cs.order_id = o.id
|
||||
AND cs.user_id IS NULL;
|
||||
|
||||
GET DIAGNOSTICS backfill_count = ROW_COUNT;
|
||||
RAISE NOTICE 'Backfilled user_id for % consulting_slots from orders', backfill_count;
|
||||
|
||||
-- Check remaining NULLs
|
||||
SELECT COUNT(*) INTO null_count FROM consulting_slots WHERE user_id IS NULL;
|
||||
RAISE NOTICE 'Remaining consulting_slots with NULL user_id: %', null_count;
|
||||
END $$;
|
||||
|
||||
-- Create index for faster queries
|
||||
CREATE INDEX IF NOT EXISTS idx_consulting_slots_user_id ON consulting_slots(user_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_consulting_slots_user_status ON consulting_slots(user_id, status);
|
||||
|
||||
-- Enable RLS on consulting_slots (if not already enabled)
|
||||
ALTER TABLE consulting_slots ENABLE ROW LEVEL SECURITY;
|
||||
|
||||
-- Drop ALL existing policies first to avoid conflicts
|
||||
DROP POLICY IF EXISTS "consulting_slots_select_own" ON consulting_slots;
|
||||
DROP POLICY IF EXISTS "consulting_slots_insert_own" ON consulting_slots;
|
||||
DROP POLICY IF EXISTS "consulting_slots_update_own" ON consulting_slots;
|
||||
DROP POLICY IF EXISTS "consulting_slots_select_all" ON consulting_slots;
|
||||
DROP POLICY IF EXISTS "consulting_slots_service_role" ON consulting_slots;
|
||||
|
||||
-- Create RLS policies for consulting_slots
|
||||
-- Policy for users to see their own slots
|
||||
CREATE POLICY "consulting_slots_select_own"
|
||||
ON consulting_slots
|
||||
FOR SELECT
|
||||
TO authenticated
|
||||
USING (auth.uid() = user_id);
|
||||
|
||||
-- Policy for users to insert their own slots
|
||||
CREATE POLICY "consulting_slots_insert_own"
|
||||
ON consulting_slots
|
||||
FOR INSERT
|
||||
TO authenticated
|
||||
WITH CHECK (auth.uid() = user_id);
|
||||
|
||||
-- Policy for users to update their own slots
|
||||
CREATE POLICY "consulting_slots_update_own"
|
||||
ON consulting_slots
|
||||
FOR UPDATE
|
||||
TO authenticated
|
||||
USING (auth.uid() = user_id)
|
||||
WITH CHECK (auth.uid() = user_id);
|
||||
|
||||
-- Policy for service role (admins) to access all slots
|
||||
CREATE POLICY "consulting_slots_service_role"
|
||||
ON consulting_slots
|
||||
FOR ALL
|
||||
TO service_role
|
||||
USING (true)
|
||||
WITH CHECK (true);
|
||||
|
||||
-- Grant permissions
|
||||
GRANT USAGE ON SCHEMA public TO service_role;
|
||||
GRANT ALL ON consulting_slots TO service_role;
|
||||
GRANT SELECT, INSERT, UPDATE, DELETE ON consulting_slots TO authenticated;
|
||||
@@ -0,0 +1,50 @@
|
||||
-- Clean up ALL consulting_slots RLS policies and recreate with simple working policies
|
||||
-- This fixes the 400 error caused by conflicting policies using has_role() function
|
||||
|
||||
-- Drop ALL existing policies (including the problematic ones with has_role)
|
||||
DROP POLICY IF EXISTS "Users see own slots" ON consulting_slots;
|
||||
DROP POLICY IF EXISTS "Admin manage slots" ON consulting_slots;
|
||||
DROP POLICY IF EXISTS "Users create own slots" ON consulting_slots;
|
||||
DROP POLICY IF EXISTS "consulting_slots_select_own" ON consulting_slots;
|
||||
DROP POLICY IF EXISTS "consulting_slots_insert_own" ON consulting_slots;
|
||||
DROP POLICY IF EXISTS "consulting_slots_update_own" ON consulting_slots;
|
||||
DROP POLICY IF EXISTS "consulting_slots_select_all" ON consulting_slots;
|
||||
DROP POLICY IF EXISTS "consulting_slots_service_role" ON consulting_slots;
|
||||
|
||||
-- Create simple, working policies
|
||||
-- Users can see their own consulting slots
|
||||
CREATE POLICY "Users can view own consulting slots"
|
||||
ON consulting_slots
|
||||
FOR SELECT
|
||||
TO authenticated
|
||||
USING (auth.uid() = user_id);
|
||||
|
||||
-- Users can insert their own consulting slots
|
||||
CREATE POLICY "Users can insert own consulting slots"
|
||||
ON consulting_slots
|
||||
FOR INSERT
|
||||
TO authenticated
|
||||
WITH CHECK (auth.uid() = user_id);
|
||||
|
||||
-- Users can update their own consulting slots
|
||||
CREATE POLICY "Users can update own consulting slots"
|
||||
ON consulting_slots
|
||||
FOR UPDATE
|
||||
TO authenticated
|
||||
USING (auth.uid() = user_id)
|
||||
WITH CHECK (auth.uid() = user_id);
|
||||
|
||||
-- Users can delete their own consulting slots
|
||||
CREATE POLICY "Users can delete own consulting slots"
|
||||
ON consulting_slots
|
||||
FOR DELETE
|
||||
TO authenticated
|
||||
USING (auth.uid() = user_id);
|
||||
|
||||
-- Service role (for edge functions and admin operations) can do everything
|
||||
CREATE POLICY "Service role full access"
|
||||
ON consulting_slots
|
||||
FOR ALL
|
||||
TO service_role
|
||||
USING (true)
|
||||
WITH CHECK (true);
|
||||
59
supabase/migrations/20250101000004_video_progress.sql
Normal file
59
supabase/migrations/20250101000004_video_progress.sql
Normal file
@@ -0,0 +1,59 @@
|
||||
-- Video progress tracking table
|
||||
-- Stores user's playback position for lessons and webinars
|
||||
|
||||
CREATE TABLE IF NOT EXISTS video_progress (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
user_id UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE,
|
||||
video_id TEXT NOT NULL, -- lesson_id or webinar/product_id
|
||||
video_type TEXT NOT NULL CHECK (video_type IN ('lesson', 'webinar')),
|
||||
last_position DECIMAL(10,2) NOT NULL DEFAULT 0, -- seconds
|
||||
total_duration DECIMAL(10,2), -- total video duration in seconds
|
||||
completed BOOLEAN DEFAULT FALSE,
|
||||
last_watched_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
||||
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
||||
UNIQUE(user_id, video_id, video_type)
|
||||
);
|
||||
|
||||
-- Indexes for fast queries
|
||||
CREATE INDEX IF NOT EXISTS idx_video_progress_user ON video_progress(user_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_video_progress_user_type ON video_progress(user_id, video_type);
|
||||
CREATE INDEX IF NOT EXISTS idx_video_progress_completed ON video_progress(user_id, completed) WHERE completed = TRUE;
|
||||
|
||||
-- Enable RLS
|
||||
ALTER TABLE video_progress ENABLE ROW LEVEL SECURITY;
|
||||
|
||||
-- Drop existing policies if any
|
||||
DROP POLICY IF EXISTS "Users manage own progress" ON video_progress;
|
||||
DROP POLICY IF EXISTS "Service role full access" ON video_progress;
|
||||
|
||||
-- Users can manage their own progress
|
||||
CREATE POLICY "Users manage own progress"
|
||||
ON video_progress
|
||||
FOR ALL
|
||||
TO authenticated
|
||||
USING (auth.uid() = user_id)
|
||||
WITH CHECK (auth.uid() = user_id);
|
||||
|
||||
-- Service role has full access
|
||||
CREATE POLICY "Service role full access"
|
||||
ON video_progress
|
||||
FOR ALL
|
||||
TO service_role
|
||||
USING (true)
|
||||
WITH CHECK (true);
|
||||
|
||||
-- Function to update updated_at timestamp
|
||||
CREATE OR REPLACE FUNCTION update_video_progress_updated_at()
|
||||
RETURNS TRIGGER AS $$
|
||||
BEGIN
|
||||
NEW.updated_at = NOW();
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
-- Trigger to auto-update updated_at
|
||||
DROP TRIGGER IF EXISTS update_video_progress_updated_at ON video_progress;
|
||||
CREATE TRIGGER update_video_progress_updated_at
|
||||
BEFORE UPDATE ON video_progress
|
||||
FOR EACH ROW
|
||||
EXECUTE FUNCTION update_video_progress_updated_at();
|
||||
Reference in New Issue
Block a user