From 94aca1edec291481e0ca42db6c9723c87613b323 Mon Sep 17 00:00:00 2001 From: dwindown Date: Tue, 30 Dec 2025 21:04:40 +0700 Subject: [PATCH] Add product-level video source toggle and improve curriculum UX MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add video source toggle UI (YouTube/Embed) to product edit form for bootcamps - Remove Bootcamp menu from admin navigation (curriculum managed via Products page) - Remove tabs from product add/edit modal (simplified to single form) - Improve ProductCurriculum layout from 3-column (3|5|4) to 2-column (4|8) - Modules and lessons now in left sidebar with accordion-style expansion - Lesson editor takes 67% width instead of 33% for better content editing UX - Add helpful tip about configuring both video sources for redundancy 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 --- src/components/AppLayout.tsx | 1 - src/pages/admin/AdminProducts.tsx | 209 +++++++------ src/pages/admin/ProductCurriculum.tsx | 423 ++++++++++++++------------ 3 files changed, 343 insertions(+), 290 deletions(-) diff --git a/src/components/AppLayout.tsx b/src/components/AppLayout.tsx index 24e23aa..53b0c45 100644 --- a/src/components/AppLayout.tsx +++ b/src/components/AppLayout.tsx @@ -43,7 +43,6 @@ const userNavItems: NavItem[] = [ const adminNavItems: NavItem[] = [ { label: 'Dashboard', href: '/admin', icon: LayoutDashboard }, { label: 'Produk', href: '/admin/products', icon: Package }, - { label: 'Bootcamp', href: '/admin/bootcamp', icon: BookOpen }, { label: 'Konsultasi', href: '/admin/consulting', icon: Video }, { label: 'Order', href: '/admin/orders', icon: Receipt }, { label: 'Member', href: '/admin/members', icon: Users }, diff --git a/src/pages/admin/AdminProducts.tsx b/src/pages/admin/AdminProducts.tsx index 8e48a2a..c584c6d 100644 --- a/src/pages/admin/AdminProducts.tsx +++ b/src/pages/admin/AdminProducts.tsx @@ -12,13 +12,14 @@ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@ import { Switch } from '@/components/ui/switch'; import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from '@/components/ui/dialog'; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table'; -import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; import { toast } from '@/hooks/use-toast'; import { Skeleton } from '@/components/ui/skeleton'; import { Plus, Pencil, Trash2, Search, X, BookOpen } from 'lucide-react'; -import { CurriculumEditor } from '@/components/admin/CurriculumEditor'; import { RichTextEditor } from '@/components/RichTextEditor'; import { formatIDR } from '@/lib/format'; +import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group'; +import { Alert, AlertDescription } from '@/components/ui/alert'; +import { Info } from 'lucide-react'; interface Product { id: string; @@ -34,6 +35,7 @@ interface Product { price: number; sale_price: number | null; is_active: boolean; + video_source?: string; } const emptyProduct = { @@ -49,6 +51,7 @@ const emptyProduct = { price: 0, sale_price: null as number | null, is_active: true, + video_source: 'youtube' as string, }; export default function AdminProducts() { @@ -60,7 +63,6 @@ export default function AdminProducts() { const [editingProduct, setEditingProduct] = useState(null); const [form, setForm] = useState(emptyProduct); const [saving, setSaving] = useState(false); - const [activeTab, setActiveTab] = useState('details'); const [searchQuery, setSearchQuery] = useState(''); const [filterType, setFilterType] = useState('all'); const [filterStatus, setFilterStatus] = useState('all'); @@ -116,15 +118,14 @@ export default function AdminProducts() { price: product.price, sale_price: product.sale_price, is_active: product.is_active, + video_source: product.video_source || 'youtube', }); - setActiveTab('details'); setDialogOpen(true); }; const handleNew = () => { setEditingProduct(null); setForm(emptyProduct); - setActiveTab('details'); setDialogOpen(true); }; @@ -147,6 +148,7 @@ export default function AdminProducts() { price: form.price, sale_price: form.sale_price || null, is_active: form.is_active, + video_source: form.video_source || 'youtube', }; if (editingProduct) { @@ -420,97 +422,126 @@ export default function AdminProducts() { {editingProduct ? 'Edit Produk' : 'Produk Baru'} - - - Detail - {editingProduct && form.type === 'bootcamp' && Kurikulum} - - +
+
+
+ + setForm({ ...form, title: e.target.value, slug: generateSlug(e.target.value) })} className="border-2" /> +
+
+ + setForm({ ...form, slug: e.target.value })} className="border-2" /> +
+
+
+ + +
+
+ + setForm({ ...form, description: v })} /> +
+
+ + setForm({ ...form, content: v })} /> +
+
+
+ + setForm({ ...form, meeting_link: e.target.value })} placeholder="https://meet.google.com/..." className="border-2" /> +
+
+ + setForm({ ...form, recording_url: e.target.value })} placeholder="https://youtube.com/..." className="border-2" /> +
+
+ {form.type === 'webinar' && (
- - setForm({ ...form, title: e.target.value, slug: generateSlug(e.target.value) })} className="border-2" /> + + setForm({ ...form, event_start: e.target.value || null })} + className="border-2" + />
- - setForm({ ...form, slug: e.target.value })} className="border-2" /> + + setForm({ ...form, duration_minutes: e.target.value ? parseInt(e.target.value) : null })} + placeholder="60" + className="border-2" + />
-
- - -
-
- - setForm({ ...form, description: v })} /> -
-
- - setForm({ ...form, content: v })} /> -
-
-
- - setForm({ ...form, meeting_link: e.target.value })} placeholder="https://meet.google.com/..." className="border-2" /> -
-
- - setForm({ ...form, recording_url: e.target.value })} placeholder="https://youtube.com/..." className="border-2" /> -
-
- {form.type === 'webinar' && ( -
-
- - setForm({ ...form, event_start: e.target.value || null })} - className="border-2" - /> -
-
- - setForm({ ...form, duration_minutes: e.target.value ? parseInt(e.target.value) : null })} - placeholder="60" - className="border-2" - /> -
-
- )} -
-
- - setForm({ ...form, price: parseFloat(e.target.value) || 0 })} className="border-2" /> -
-
- - setForm({ ...form, sale_price: e.target.value ? parseFloat(e.target.value) : null })} placeholder="Kosongkan jika tidak promo" className="border-2" /> -
-
-
- setForm({ ...form, is_active: checked })} /> - -
- - - {editingProduct && form.type === 'bootcamp' && ( - - - )} - + {form.type === 'bootcamp' && ( +
+ + setForm({ ...form, video_source: value })} + > +
+ +
+ +

+ Use YouTube URLs for all lessons +

+
+
+ +
+ +
+ +

+ Use custom embed codes (Adilo, Vimeo, etc.) for all lessons +

+
+
+
+ + + + + This setting affects ALL lessons in this bootcamp. Configure both YouTube URLs and embed codes for each lesson in the curriculum editor. Use this toggle to switch between sources instantly. + + +
+ )} +
+
+ + setForm({ ...form, price: parseFloat(e.target.value) || 0 })} className="border-2" /> +
+
+ + setForm({ ...form, sale_price: e.target.value ? parseFloat(e.target.value) : null })} placeholder="Kosongkan jika tidak promo" className="border-2" /> +
+
+
+ setForm({ ...form, is_active: checked })} /> + +
+ +
diff --git a/src/pages/admin/ProductCurriculum.tsx b/src/pages/admin/ProductCurriculum.tsx index e30790a..9b8e4d3 100644 --- a/src/pages/admin/ProductCurriculum.tsx +++ b/src/pages/admin/ProductCurriculum.tsx @@ -314,226 +314,258 @@ export default function ProductCurriculum() {
- {/* Left: Modules List (3 columns) */} -
+ {/* Left Sidebar: Modules & Lessons (4 columns) */} +
+ {/* Modules Card */} - Modules - +
+ Modules + +
{modules.map((module, index) => { const moduleLessons = getLessonsForModule(module.id); const isSelected = selectedModuleId === module.id; + const isExpanded = expandedModules.has(module.id); return ( -
+ {/* Module Header */} +
{ + setSelectedModuleId(module.id); + toggleModule(module.id); + }} + > +
+
+ + {module.title} +
+
+ + + + +
+
+
+ {moduleLessons.length} lesson{moduleLessons.length !== 1 ? 's' : ''} +
+
+ + {/* Lessons List (expanded) */} + {isExpanded && ( +
+
+ + {moduleLessons.map((lesson, lessonIndex) => { + const isLessonSelected = selectedLessonId === lesson.id; + return ( +
{ + e.stopPropagation(); + handleEditLesson(lesson); + }} + > +
+
+

{lesson.title}

+
+ + {lessonIndex + 1}. + + {lesson.youtube_url && ( + YouTube + )} + {lesson.embed_code && ( + Embed + )} + {lesson.content && ( + ✓ Content + )} +
+
+
+ + + +
+
+
+ ); + })} + {moduleLessons.length === 0 && ( +
+ No lessons yet +
+ )} +
+
)} - > -
-
{ - setSelectedModuleId(module.id); - if (expandedModules.has(module.id)) { - toggleModule(module.id); - } else { - const newExpanded = new Set(expandedModules); - newExpanded.add(module.id); - setExpandedModules(newExpanded); - } - }} - > - - {module.title} -
-
- - - - -
-
-
- {moduleLessons.length} lesson{moduleLessons.length !== 1 ? 's' : ''} -
); })} {modules.length === 0 && (
- No modules yet + No modules yet. Click + to create one.
)}
- {/* Middle: Lessons List (5 columns) */} -
- - - Lessons - {selectedModuleId && ( - - )} - - - {!selectedModuleId ? ( -

- Select a module to view lessons -

- ) : ( -
- {getLessonsForModule(selectedModuleId).map((lesson, index) => { - const isSelected = selectedLessonId === lesson.id; - - return ( -
-
-
handleEditLesson(lesson)} - > -

{lesson.title}

-
- - {index + 1}. {lesson.video_url || lesson.youtube_url || lesson.embed_code ? '✓ Video' : 'No video'} - - {lesson.youtube_url && ( - YouTube - )} - {lesson.embed_code && ( - Embed - )} - {lesson.content && ( - ✓ Content - )} -
-
-
- - - -
-
-
- ); - })} - {getLessonsForModule(selectedModuleId).length === 0 && ( -
- No lessons yet -
- )} -
- )} -
-
-
- - {/* Right: Lesson Editor (4 columns) */} -
+ {/* Right: Lesson Editor (8 columns - full width for better UX) */} +
- Lesson Editor + + {selectedLessonId === 'new' ? 'New Lesson' : editingLesson ? 'Edit Lesson' : 'Lesson Editor'} + {!selectedLessonId ? ( -

- Select a lesson to edit -

+
+

+ Select or create a lesson to start editing +

+

+ Click on a module to expand it, then click "Add Lesson" or select an existing lesson. +

+
) : ( -
+
setLessonForm({ ...lessonForm, title: e.target.value })} placeholder="Lesson title" - className="border-2" + className="border-2 text-base" />
-
- - setLessonForm({ ...lessonForm, youtube_url: e.target.value })} - placeholder="https://www.youtube.com/watch?v=..." - className="border-2" - /> - {lessonForm.youtube_url && ( -

✓ YouTube configured

- )} +
+
+ + setLessonForm({ ...lessonForm, youtube_url: e.target.value })} + placeholder="https://www.youtube.com/watch?v=..." + className="border-2" + /> + {lessonForm.youtube_url && ( +

✓ YouTube configured

+ )} +
+ +
+ + setLessonForm({ ...lessonForm, release_at: e.target.value })} + className="border-2" + /> +
@@ -550,9 +582,9 @@ export default function ProductCurriculum() { )}
-
-

- 💡 Tip: Configure both YouTube URL and embed code for redundancy. Use product settings to toggle between sources. +

+

+ 💡 Tip: Configure both YouTube URL and embed code for redundancy. Use product settings to toggle between sources. This setting affects ALL lessons in the bootcamp.

@@ -562,25 +594,15 @@ export default function ProductCurriculum() { content={lessonForm.content} onChange={(html) => setLessonForm({ ...lessonForm, content: html })} placeholder="Write your lesson content here... Use code blocks for syntax highlighting." - className="min-h-[300px]" + className="min-h-[400px]" />

Supports rich text formatting, code blocks with syntax highlighting, images, and more.

-
- - setLessonForm({ ...lessonForm, release_at: e.target.value })} - className="border-2" - /> -
- -
-