Compare commits
2 Commits
fa274bd8cc
...
711a5c5d6b
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
711a5c5d6b | ||
|
|
eea3a1f8d8 |
14
package-lock.json
generated
14
package-lock.json
generated
@@ -45,6 +45,7 @@
|
|||||||
"@tiptap/extension-table-cell": "^3.14.0",
|
"@tiptap/extension-table-cell": "^3.14.0",
|
||||||
"@tiptap/extension-table-header": "^3.14.0",
|
"@tiptap/extension-table-header": "^3.14.0",
|
||||||
"@tiptap/extension-table-row": "^3.14.0",
|
"@tiptap/extension-table-row": "^3.14.0",
|
||||||
|
"@tiptap/extension-text-align": "^3.14.0",
|
||||||
"@tiptap/react": "^3.13.0",
|
"@tiptap/react": "^3.13.0",
|
||||||
"@tiptap/starter-kit": "^3.13.0",
|
"@tiptap/starter-kit": "^3.13.0",
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
@@ -3267,6 +3268,19 @@
|
|||||||
"@tiptap/core": "^3.13.0"
|
"@tiptap/core": "^3.13.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@tiptap/extension-text-align": {
|
||||||
|
"version": "3.14.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tiptap/extension-text-align/-/extension-text-align-3.14.0.tgz",
|
||||||
|
"integrity": "sha512-CaxxlbAvfofZZ7KPL28Kg8xuMv8t4rvt5GPwZAqE+jd3rwrucpovpX/SdgclYDc75xs0t8qeoxDFe9HQmG5XZA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/ueberdosis"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@tiptap/core": "^3.14.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@tiptap/extension-underline": {
|
"node_modules/@tiptap/extension-underline": {
|
||||||
"version": "3.13.0",
|
"version": "3.13.0",
|
||||||
"resolved": "https://registry.npmjs.org/@tiptap/extension-underline/-/extension-underline-3.13.0.tgz",
|
"resolved": "https://registry.npmjs.org/@tiptap/extension-underline/-/extension-underline-3.13.0.tgz",
|
||||||
|
|||||||
@@ -48,6 +48,7 @@
|
|||||||
"@tiptap/extension-table-cell": "^3.14.0",
|
"@tiptap/extension-table-cell": "^3.14.0",
|
||||||
"@tiptap/extension-table-header": "^3.14.0",
|
"@tiptap/extension-table-header": "^3.14.0",
|
||||||
"@tiptap/extension-table-row": "^3.14.0",
|
"@tiptap/extension-table-row": "^3.14.0",
|
||||||
|
"@tiptap/extension-text-align": "^3.14.0",
|
||||||
"@tiptap/react": "^3.13.0",
|
"@tiptap/react": "^3.13.0",
|
||||||
"@tiptap/starter-kit": "^3.13.0",
|
"@tiptap/starter-kit": "^3.13.0",
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
|
|||||||
@@ -3,12 +3,13 @@ import StarterKit from '@tiptap/starter-kit';
|
|||||||
import Link from '@tiptap/extension-link';
|
import Link from '@tiptap/extension-link';
|
||||||
import Image from '@tiptap/extension-image';
|
import Image from '@tiptap/extension-image';
|
||||||
import Placeholder from '@tiptap/extension-placeholder';
|
import Placeholder from '@tiptap/extension-placeholder';
|
||||||
|
import TextAlign from '@tiptap/extension-text-align';
|
||||||
import { Node } from '@tiptap/core';
|
import { Node } from '@tiptap/core';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import {
|
import {
|
||||||
Bold, Italic, List, ListOrdered, Quote, Link as LinkIcon,
|
Bold, Italic, List, ListOrdered, Quote, Link as LinkIcon,
|
||||||
Image as ImageIcon, Heading1, Heading2, Undo, Redo,
|
Image as ImageIcon, Heading1, Heading2, Undo, Redo,
|
||||||
Maximize2, Minimize2, MousePointer, Square
|
Maximize2, Minimize2, MousePointer, Square, AlignLeft, AlignCenter, AlignRight, AlignJustify, MoreVertical, Minus
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
import { useCallback, useEffect, useState } from 'react';
|
import { useCallback, useEffect, useState } from 'react';
|
||||||
@@ -243,7 +244,15 @@ export function RichTextEditor({ content, onChange, placeholder = 'Tulis konten.
|
|||||||
|
|
||||||
const editor = useEditor({
|
const editor = useEditor({
|
||||||
extensions: [
|
extensions: [
|
||||||
StarterKit,
|
StarterKit.configure({
|
||||||
|
heading: {
|
||||||
|
levels: [1, 2, 3],
|
||||||
|
},
|
||||||
|
horizontalRule: true,
|
||||||
|
}),
|
||||||
|
TextAlign.configure({
|
||||||
|
types: ['heading', 'paragraph'],
|
||||||
|
}),
|
||||||
Link.configure({
|
Link.configure({
|
||||||
openOnClick: false,
|
openOnClick: false,
|
||||||
HTMLAttributes: {
|
HTMLAttributes: {
|
||||||
@@ -517,6 +526,63 @@ export function RichTextEditor({ content, onChange, placeholder = 'Tulis konten.
|
|||||||
<LinkIcon className="w-4 h-4" />
|
<LinkIcon className="w-4 h-4" />
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
|
{/* Text Align Separator */}
|
||||||
|
<div className="w-px h-6 bg-border mx-1" />
|
||||||
|
|
||||||
|
{/* Text Align Buttons */}
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => editor.chain().focus().setTextAlign('left').run()}
|
||||||
|
className={editor.isActive({ textAlign: 'left' }) ? 'bg-primary text-primary-foreground' : ''}
|
||||||
|
title="Align Left"
|
||||||
|
>
|
||||||
|
<AlignLeft className="w-4 h-4" />
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => editor.chain().focus().setTextAlign('center').run()}
|
||||||
|
className={editor.isActive({ textAlign: 'center' }) ? 'bg-primary text-primary-foreground' : ''}
|
||||||
|
title="Align Center"
|
||||||
|
>
|
||||||
|
<AlignCenter className="w-4 h-4" />
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => editor.chain().focus().setTextAlign('right').run()}
|
||||||
|
className={editor.isActive({ textAlign: 'right' }) ? 'bg-primary text-primary-foreground' : ''}
|
||||||
|
title="Align Right"
|
||||||
|
>
|
||||||
|
<AlignRight className="w-4 h-4" />
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => editor.chain().focus().setTextAlign('justify').run()}
|
||||||
|
className={editor.isActive({ textAlign: 'justify' }) ? 'bg-primary text-primary-foreground' : ''}
|
||||||
|
title="Justify"
|
||||||
|
>
|
||||||
|
<AlignJustify className="w-4 h-4" />
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
{/* Spacer/Separator */}
|
||||||
|
<div className="w-px h-6 bg-border mx-1" />
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => editor.chain().focus().setHorizontalRule().run()}
|
||||||
|
title="Insert Spacer"
|
||||||
|
>
|
||||||
|
<Minus className="w-4 h-4" />
|
||||||
|
</Button>
|
||||||
|
|
||||||
{/* Email Components Separator */}
|
{/* Email Components Separator */}
|
||||||
<div className="w-px h-6 bg-border mx-1" />
|
<div className="w-px h-6 bg-border mx-1" />
|
||||||
|
|
||||||
@@ -628,7 +694,7 @@ export function RichTextEditor({ content, onChange, placeholder = 'Tulis konten.
|
|||||||
<div onPaste={handlePaste}>
|
<div onPaste={handlePaste}>
|
||||||
<EditorContent
|
<EditorContent
|
||||||
editor={editor}
|
editor={editor}
|
||||||
className="prose prose-sm max-w-none p-4 min-h-[200px] focus:outline-none [&_.ProseMirror]:outline-none [&_.ProseMirror]:min-h-[180px] [&_img]:cursor-pointer [&_img.ProseMirror-selectednode]:ring-2 [&_img.ProseMirror-selectednode]:ring-primary [&_h1]:font-bold [&_h1]:text-2xl [&_h1]:mb-4 [&_h1]:mt-6 [&_h2]:font-bold [&_h2]:text-xl [&_h2]:mb-3 [&_h2]:mt-5 [&_blockquote]:border-l-4 [&_blockquote]:border-primary [&_blockquote]:pl-4 [&_blockquote]:italic [&_blockquote]:text-muted-foreground [&_blockquote]:my-4 [&_ul]:list-disc [&_ul]:pl-6 [&_ul]:space-y-1 [&_ol]:list-decimal [&_ol]:pl-6 [&_ol]:space-y-1 [&_li]:marker:text-primary"
|
className="prose prose-sm max-w-none p-4 min-h-[200px] focus:outline-none [&_.ProseMirror]:outline-none [&_.ProseMirror]:min-h-[180px] [&_img]:cursor-pointer [&_img.ProseMirror-selectednode]:ring-2 [&_img.ProseMirror-selectednode]:ring-primary [&_h1]:font-bold [&_h1]:text-2xl [&_h1]:mb-4 [&_h1]:mt-6 [&_h2]:font-bold [&_h2]:text-xl [&_h2]:mb-3 [&_h2]:mt-5 [&_blockquote]:border-l-4 [&_blockquote]:border-primary [&_blockquote]:pl-4 [&_blockquote]:italic [&_blockquote]:text-muted-foreground [&_blockquote]:my-4 [&_ul]:list-disc [&_ul]:pl-6 [&_ul]:space-y-1 [&_ol]:list-decimal [&_ol]:pl-6 [&_ol]:space-y-1 [&_li]:marker:text-primary [&_hr]:border-border [&_hr]:my-4 [&_hr]:border-t-2"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{uploading && (
|
{uploading && (
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ export default function Calendar() {
|
|||||||
// Fetch webinar events
|
// Fetch webinar events
|
||||||
const { data: webinars } = await supabase
|
const { data: webinars } = await supabase
|
||||||
.from('products')
|
.from('products')
|
||||||
.select('id, title, event_start, duration')
|
.select('id, title, event_start, duration_minutes')
|
||||||
.eq('type', 'webinar')
|
.eq('type', 'webinar')
|
||||||
.eq('is_active', true)
|
.eq('is_active', true)
|
||||||
.gte('event_start', start)
|
.gte('event_start', start)
|
||||||
@@ -76,12 +76,16 @@ export default function Calendar() {
|
|||||||
webinars?.forEach(w => {
|
webinars?.forEach(w => {
|
||||||
if (w.event_start) {
|
if (w.event_start) {
|
||||||
const eventDate = new Date(w.event_start);
|
const eventDate = new Date(w.event_start);
|
||||||
|
const durationMs = (w.duration_minutes || 60) * 60 * 1000;
|
||||||
|
const endDate = new Date(eventDate.getTime() + durationMs);
|
||||||
|
|
||||||
allEvents.push({
|
allEvents.push({
|
||||||
id: w.id,
|
id: w.id,
|
||||||
title: w.title,
|
title: w.title,
|
||||||
type: 'webinar',
|
type: 'webinar',
|
||||||
date: format(eventDate, 'yyyy-MM-dd'),
|
date: format(eventDate, 'yyyy-MM-dd'),
|
||||||
start_time: format(eventDate, 'HH:mm'),
|
start_time: format(eventDate, 'HH:mm'),
|
||||||
|
end_time: format(endDate, 'HH:mm'),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -37,6 +37,13 @@ interface ConfirmedSlot {
|
|||||||
end_time: string;
|
end_time: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface Webinar {
|
||||||
|
id: string;
|
||||||
|
title: string;
|
||||||
|
event_start: string;
|
||||||
|
duration_minutes: number | null;
|
||||||
|
}
|
||||||
|
|
||||||
interface TimeSlot {
|
interface TimeSlot {
|
||||||
start: string;
|
start: string;
|
||||||
end: string;
|
end: string;
|
||||||
@@ -54,6 +61,7 @@ export default function ConsultingBooking() {
|
|||||||
const [settings, setSettings] = useState<ConsultingSettings | null>(null);
|
const [settings, setSettings] = useState<ConsultingSettings | null>(null);
|
||||||
const [workhours, setWorkhours] = useState<Workhour[]>([]);
|
const [workhours, setWorkhours] = useState<Workhour[]>([]);
|
||||||
const [confirmedSlots, setConfirmedSlots] = useState<ConfirmedSlot[]>([]);
|
const [confirmedSlots, setConfirmedSlots] = useState<ConfirmedSlot[]>([]);
|
||||||
|
const [webinars, setWebinars] = useState<Webinar[]>([]);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [profile, setProfile] = useState<Profile | null>(null);
|
const [profile, setProfile] = useState<Profile | null>(null);
|
||||||
|
|
||||||
@@ -78,6 +86,7 @@ export default function ConsultingBooking() {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (selectedDate) {
|
if (selectedDate) {
|
||||||
fetchConfirmedSlots(selectedDate);
|
fetchConfirmedSlots(selectedDate);
|
||||||
|
fetchWebinars(selectedDate);
|
||||||
}
|
}
|
||||||
}, [selectedDate]);
|
}, [selectedDate]);
|
||||||
|
|
||||||
@@ -101,10 +110,22 @@ export default function ConsultingBooking() {
|
|||||||
.select('date, start_time, end_time')
|
.select('date, start_time, end_time')
|
||||||
.eq('date', dateStr)
|
.eq('date', dateStr)
|
||||||
.in('status', ['pending_payment', 'confirmed']);
|
.in('status', ['pending_payment', 'confirmed']);
|
||||||
|
|
||||||
if (data) setConfirmedSlots(data);
|
if (data) setConfirmedSlots(data);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const fetchWebinars = async (date: Date) => {
|
||||||
|
const dateStr = format(date, 'yyyy-MM-dd');
|
||||||
|
const { data } = await supabase
|
||||||
|
.from('products')
|
||||||
|
.select('id, title, event_start, duration_minutes')
|
||||||
|
.eq('type', 'webinar')
|
||||||
|
.eq('is_active', true)
|
||||||
|
.like('event_start', `${dateStr}%`);
|
||||||
|
|
||||||
|
if (data) setWebinars(data);
|
||||||
|
};
|
||||||
|
|
||||||
const categories = useMemo(() => {
|
const categories = useMemo(() => {
|
||||||
if (!settings?.consulting_categories) return [];
|
if (!settings?.consulting_categories) return [];
|
||||||
return settings.consulting_categories.split(',').map(c => c.trim()).filter(Boolean);
|
return settings.consulting_categories.split(',').map(c => c.trim()).filter(Boolean);
|
||||||
@@ -131,20 +152,36 @@ export default function ConsultingBooking() {
|
|||||||
const slotStart = format(current, 'HH:mm');
|
const slotStart = format(current, 'HH:mm');
|
||||||
const slotEnd = format(addMinutes(current, duration), 'HH:mm');
|
const slotEnd = format(addMinutes(current, duration), 'HH:mm');
|
||||||
|
|
||||||
// Check if slot conflicts with confirmed/pending slots
|
// Check if slot conflicts with confirmed/pending consulting slots
|
||||||
const isConflict = confirmedSlots.some(cs => {
|
const isConflict = confirmedSlots.some(cs => {
|
||||||
const csStart = cs.start_time.substring(0, 5);
|
const csStart = cs.start_time.substring(0, 5);
|
||||||
const csEnd = cs.end_time.substring(0, 5);
|
const csEnd = cs.end_time.substring(0, 5);
|
||||||
return !(slotEnd <= csStart || slotStart >= csEnd);
|
return !(slotEnd <= csStart || slotStart >= csEnd);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Check if slot conflicts with webinars
|
||||||
|
const webinarConflict = webinars.some(w => {
|
||||||
|
const webinarStart = new Date(w.event_start);
|
||||||
|
const webinarDurationMs = (w.duration_minutes || 60) * 60 * 1000;
|
||||||
|
const webinarEnd = new Date(webinarStart.getTime() + webinarDurationMs);
|
||||||
|
|
||||||
|
const slotStartTime = new Date(selectedDate);
|
||||||
|
slotStartTime.setHours(parseInt(slotStart.split(':')[0]), parseInt(slotStart.split(':')[1]), 0);
|
||||||
|
|
||||||
|
const slotEndTime = new Date(selectedDate);
|
||||||
|
slotEndTime.setHours(parseInt(slotEnd.split(':')[0]), parseInt(slotEnd.split(':')[1]), 0);
|
||||||
|
|
||||||
|
// Block if slot overlaps with webinar time
|
||||||
|
return slotStartTime < webinarEnd && slotEndTime > webinarStart;
|
||||||
|
});
|
||||||
|
|
||||||
// Check if slot is in the past for today
|
// Check if slot is in the past for today
|
||||||
const isPassed = isToday && isBefore(current, now);
|
const isPassed = isToday && isBefore(current, now);
|
||||||
|
|
||||||
slots.push({
|
slots.push({
|
||||||
start: slotStart,
|
start: slotStart,
|
||||||
end: slotEnd,
|
end: slotEnd,
|
||||||
available: !isConflict && !isPassed,
|
available: !isConflict && !webinarConflict && !isPassed,
|
||||||
});
|
});
|
||||||
|
|
||||||
current = addMinutes(current, duration);
|
current = addMinutes(current, duration);
|
||||||
@@ -152,7 +189,7 @@ export default function ConsultingBooking() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return slots;
|
return slots;
|
||||||
}, [selectedDate, workhours, confirmedSlots, settings]);
|
}, [selectedDate, workhours, confirmedSlots, webinars, settings]);
|
||||||
|
|
||||||
// Helper: Get all slots between start and end (inclusive)
|
// Helper: Get all slots between start and end (inclusive)
|
||||||
const getSlotsInRange = useMemo(() => {
|
const getSlotsInRange = useMemo(() => {
|
||||||
@@ -414,6 +451,11 @@ export default function ConsultingBooking() {
|
|||||||
</CardTitle>
|
</CardTitle>
|
||||||
<CardDescription>
|
<CardDescription>
|
||||||
Klik slot awal dan akhir untuk memilih rentang waktu. {settings.consulting_block_duration_minutes} menit per blok.
|
Klik slot awal dan akhir untuk memilih rentang waktu. {settings.consulting_block_duration_minutes} menit per blok.
|
||||||
|
{webinars.length > 0 && (
|
||||||
|
<span className="block mt-1 text-amber-600 dark:text-amber-400">
|
||||||
|
⚠️ {webinars.length} webinar terjadwal - beberapa slot mungkin tidak tersedia
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
</CardDescription>
|
</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
|
|||||||
@@ -345,8 +345,17 @@ export default function ProductDetail() {
|
|||||||
<div className="flex flex-col md:flex-row md:items-start justify-between gap-4 mb-6">
|
<div className="flex flex-col md:flex-row md:items-start justify-between gap-4 mb-6">
|
||||||
<div>
|
<div>
|
||||||
<h1 className="text-4xl font-bold mb-2">{product.title}</h1>
|
<h1 className="text-4xl font-bold mb-2">{product.title}</h1>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2 flex-wrap">
|
||||||
<Badge className="bg-primary text-primary-foreground capitalize">{product.type}</Badge>
|
<Badge className="bg-primary text-primary-foreground capitalize">{product.type}</Badge>
|
||||||
|
{product.type === 'webinar' && product.recording_url && (
|
||||||
|
<Badge className="bg-secondary text-primary">Rekaman Tersedia</Badge>
|
||||||
|
)}
|
||||||
|
{product.type === 'webinar' && !product.recording_url && product.event_start && new Date(product.event_start) > new Date() && (
|
||||||
|
<Badge className="bg-brand-accent text-white">Segera Hadir</Badge>
|
||||||
|
)}
|
||||||
|
{product.type === 'webinar' && !product.recording_url && product.event_start && new Date(product.event_start) <= new Date() && (
|
||||||
|
<Badge className="bg-muted text-primary">Telah Selesai</Badge>
|
||||||
|
)}
|
||||||
{hasAccess && <Badge className="bg-primary text-primary-foreground">Anda memiliki akses</Badge>}
|
{hasAccess && <Badge className="bg-primary text-primary-foreground">Anda memiliki akses</Badge>}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -29,6 +29,8 @@ interface Product {
|
|||||||
content: string;
|
content: string;
|
||||||
meeting_link: string | null;
|
meeting_link: string | null;
|
||||||
recording_url: string | null;
|
recording_url: string | null;
|
||||||
|
event_start: string | null;
|
||||||
|
duration_minutes: number | null;
|
||||||
price: number;
|
price: number;
|
||||||
sale_price: number | null;
|
sale_price: number | null;
|
||||||
is_active: boolean;
|
is_active: boolean;
|
||||||
@@ -42,6 +44,8 @@ const emptyProduct = {
|
|||||||
content: '',
|
content: '',
|
||||||
meeting_link: '',
|
meeting_link: '',
|
||||||
recording_url: '',
|
recording_url: '',
|
||||||
|
event_start: null as string | null,
|
||||||
|
duration_minutes: null as number | null,
|
||||||
price: 0,
|
price: 0,
|
||||||
sale_price: null as number | null,
|
sale_price: null as number | null,
|
||||||
is_active: true,
|
is_active: true,
|
||||||
@@ -84,6 +88,8 @@ export default function AdminProducts() {
|
|||||||
content: product.content || '',
|
content: product.content || '',
|
||||||
meeting_link: product.meeting_link || '',
|
meeting_link: product.meeting_link || '',
|
||||||
recording_url: product.recording_url || '',
|
recording_url: product.recording_url || '',
|
||||||
|
event_start: product.event_start,
|
||||||
|
duration_minutes: product.duration_minutes,
|
||||||
price: product.price,
|
price: product.price,
|
||||||
sale_price: product.sale_price,
|
sale_price: product.sale_price,
|
||||||
is_active: product.is_active,
|
is_active: product.is_active,
|
||||||
@@ -113,6 +119,8 @@ export default function AdminProducts() {
|
|||||||
content: form.content,
|
content: form.content,
|
||||||
meeting_link: form.meeting_link || null,
|
meeting_link: form.meeting_link || null,
|
||||||
recording_url: form.recording_url || null,
|
recording_url: form.recording_url || null,
|
||||||
|
event_start: form.event_start || null,
|
||||||
|
duration_minutes: form.duration_minutes || null,
|
||||||
price: form.price,
|
price: form.price,
|
||||||
sale_price: form.sale_price || null,
|
sale_price: form.sale_price || null,
|
||||||
is_active: form.is_active,
|
is_active: form.is_active,
|
||||||
@@ -322,6 +330,29 @@ export default function AdminProducts() {
|
|||||||
<Input value={form.recording_url} onChange={(e) => setForm({ ...form, recording_url: e.target.value })} placeholder="https://youtube.com/..." className="border-2" />
|
<Input value={form.recording_url} onChange={(e) => setForm({ ...form, recording_url: e.target.value })} placeholder="https://youtube.com/..." className="border-2" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{form.type === 'webinar' && (
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label>Tanggal & Waktu Webinar</Label>
|
||||||
|
<Input
|
||||||
|
type="datetime-local"
|
||||||
|
value={form.event_start || ''}
|
||||||
|
onChange={(e) => setForm({ ...form, event_start: e.target.value || null })}
|
||||||
|
className="border-2"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label>Durasi (menit)</Label>
|
||||||
|
<Input
|
||||||
|
type="number"
|
||||||
|
value={form.duration_minutes || ''}
|
||||||
|
onChange={(e) => setForm({ ...form, duration_minutes: e.target.value ? parseInt(e.target.value) : null })}
|
||||||
|
placeholder="60"
|
||||||
|
className="border-2"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
<div className="grid grid-cols-2 gap-4">
|
<div className="grid grid-cols-2 gap-4">
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label>Harga *</Label>
|
<Label>Harga *</Label>
|
||||||
|
|||||||
Reference in New Issue
Block a user