This commit is contained in:
gpt-engineer-app[bot]
2025-12-19 14:13:40 +00:00
parent 46caf550a6
commit e5d42d2d1b
7 changed files with 986 additions and 119 deletions

View File

@@ -12,6 +12,7 @@ import ProductDetail from "./pages/ProductDetail";
import Checkout from "./pages/Checkout";
import Bootcamp from "./pages/Bootcamp";
import Events from "./pages/Events";
import ConsultingBooking from "./pages/ConsultingBooking";
import NotFound from "./pages/NotFound";
// Member pages
@@ -28,6 +29,7 @@ import AdminOrders from "./pages/admin/AdminOrders";
import AdminMembers from "./pages/admin/AdminMembers";
import AdminEvents from "./pages/admin/AdminEvents";
import AdminSettings from "./pages/admin/AdminSettings";
import AdminConsulting from "./pages/admin/AdminConsulting";
const queryClient = new QueryClient();
@@ -47,6 +49,7 @@ const App = () => (
<Route path="/checkout" element={<Checkout />} />
<Route path="/events" element={<Events />} />
<Route path="/bootcamp/:slug" element={<Bootcamp />} />
<Route path="/consulting" element={<ConsultingBooking />} />
{/* Member routes */}
<Route path="/dashboard" element={<MemberDashboard />} />
@@ -62,6 +65,7 @@ const App = () => (
<Route path="/admin/members" element={<AdminMembers />} />
<Route path="/admin/events" element={<AdminEvents />} />
<Route path="/admin/settings" element={<AdminSettings />} />
<Route path="/admin/consulting" element={<AdminConsulting />} />
<Route path="*" element={<NotFound />} />
</Routes>

View File

@@ -0,0 +1,239 @@
import { useEffect, useState } from 'react';
import { supabase } from '@/integrations/supabase/client';
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { toast } from '@/hooks/use-toast';
import { Palette, Image, Mail } from 'lucide-react';
interface PlatformSettings {
id?: string;
brand_name: string;
brand_tagline: string;
brand_logo_url: string;
brand_favicon_url: string;
brand_primary_color: string;
brand_accent_color: string;
brand_email_from_name: string;
}
const emptySettings: PlatformSettings = {
brand_name: '',
brand_tagline: '',
brand_logo_url: '',
brand_favicon_url: '',
brand_primary_color: '#111827',
brand_accent_color: '#0F766E',
brand_email_from_name: '',
};
export function BrandingTab() {
const [settings, setSettings] = useState<PlatformSettings>(emptySettings);
const [loading, setLoading] = useState(true);
const [saving, setSaving] = useState(false);
useEffect(() => {
fetchSettings();
}, []);
const fetchSettings = async () => {
const { data, error } = await supabase
.from('platform_settings')
.select('*')
.single();
if (data) {
setSettings({
id: data.id,
brand_name: data.brand_name || '',
brand_tagline: data.brand_tagline || '',
brand_logo_url: data.brand_logo_url || '',
brand_favicon_url: data.brand_favicon_url || '',
brand_primary_color: data.brand_primary_color || '#111827',
brand_accent_color: data.brand_accent_color || '#0F766E',
brand_email_from_name: data.brand_email_from_name || '',
});
}
setLoading(false);
};
const saveSettings = async () => {
setSaving(true);
const payload = { ...settings };
delete payload.id;
if (settings.id) {
const { error } = await supabase
.from('platform_settings')
.update(payload)
.eq('id', settings.id);
if (error) toast({ title: 'Error', description: error.message, variant: 'destructive' });
else toast({ title: 'Berhasil', description: 'Pengaturan branding disimpan' });
} else {
const { data, error } = await supabase
.from('platform_settings')
.insert(payload)
.select()
.single();
if (error) toast({ title: 'Error', description: error.message, variant: 'destructive' });
else {
setSettings({ ...settings, id: data.id });
toast({ title: 'Berhasil', description: 'Pengaturan branding disimpan' });
}
}
setSaving(false);
};
if (loading) return <div className="animate-pulse h-64 bg-muted rounded-md" />;
return (
<div className="space-y-6">
<Card className="border-2 border-border">
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Palette className="w-5 h-5" />
Identitas Brand
</CardTitle>
<CardDescription>
Konfigurasi nama, tagline, dan tampilan platform
</CardDescription>
</CardHeader>
<CardContent className="space-y-6">
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div className="space-y-2">
<Label>Nama Platform</Label>
<Input
value={settings.brand_name}
onChange={(e) => setSettings({ ...settings, brand_name: e.target.value })}
placeholder="LearnHub"
className="border-2"
/>
<p className="text-sm text-muted-foreground">
Ditampilkan di sidebar, header, dan email
</p>
</div>
<div className="space-y-2">
<Label>Tagline</Label>
<Input
value={settings.brand_tagline}
onChange={(e) => setSettings({ ...settings, brand_tagline: e.target.value })}
placeholder="Belajar bareng, dari praktisi."
className="border-2"
/>
</div>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div className="space-y-2">
<Label className="flex items-center gap-2">
<Image className="w-4 h-4" />
Logo Utama (URL)
</Label>
<Input
value={settings.brand_logo_url}
onChange={(e) => setSettings({ ...settings, brand_logo_url: e.target.value })}
placeholder="https://example.com/logo.png"
className="border-2"
/>
{settings.brand_logo_url && (
<div className="mt-2 p-2 bg-muted rounded-md">
<img
src={settings.brand_logo_url}
alt="Logo preview"
className="h-12 object-contain"
onError={(e) => (e.currentTarget.style.display = 'none')}
/>
</div>
)}
</div>
<div className="space-y-2">
<Label className="flex items-center gap-2">
<Image className="w-4 h-4" />
Favicon (URL)
</Label>
<Input
value={settings.brand_favicon_url}
onChange={(e) => setSettings({ ...settings, brand_favicon_url: e.target.value })}
placeholder="https://example.com/favicon.ico"
className="border-2"
/>
{settings.brand_favicon_url && (
<div className="mt-2 p-2 bg-muted rounded-md">
<img
src={settings.brand_favicon_url}
alt="Favicon preview"
className="h-8 w-8 object-contain"
onError={(e) => (e.currentTarget.style.display = 'none')}
/>
</div>
)}
</div>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div className="space-y-2">
<Label>Warna Utama (Hex)</Label>
<div className="flex gap-2">
<Input
type="color"
value={settings.brand_primary_color}
onChange={(e) => setSettings({ ...settings, brand_primary_color: e.target.value })}
className="w-16 h-10 p-1 border-2"
/>
<Input
value={settings.brand_primary_color}
onChange={(e) => setSettings({ ...settings, brand_primary_color: e.target.value })}
placeholder="#111827"
className="border-2 flex-1"
/>
</div>
</div>
<div className="space-y-2">
<Label>Warna Aksen / Tombol (Hex)</Label>
<div className="flex gap-2">
<Input
type="color"
value={settings.brand_accent_color}
onChange={(e) => setSettings({ ...settings, brand_accent_color: e.target.value })}
className="w-16 h-10 p-1 border-2"
/>
<Input
value={settings.brand_accent_color}
onChange={(e) => setSettings({ ...settings, brand_accent_color: e.target.value })}
placeholder="#0F766E"
className="border-2 flex-1"
/>
</div>
</div>
</div>
<div className="space-y-2">
<Label className="flex items-center gap-2">
<Mail className="w-4 h-4" />
Nama Pengirim Default Email
</Label>
<Input
value={settings.brand_email_from_name}
onChange={(e) => setSettings({ ...settings, brand_email_from_name: e.target.value })}
placeholder="LearnHub Team"
className="border-2"
/>
<p className="text-sm text-muted-foreground">
Digunakan jika SMTP from_name kosong
</p>
</div>
<Button onClick={saveSettings} disabled={saving} className="shadow-sm">
{saving ? 'Menyimpan...' : 'Simpan Pengaturan'}
</Button>
</CardContent>
</Card>
</div>
);
}

View File

@@ -0,0 +1,280 @@
import { useEffect, useState } from 'react';
import { supabase } from '@/integrations/supabase/client';
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
import { toast } from '@/hooks/use-toast';
import { Puzzle, Webhook, MessageSquare, Calendar, Mail, Link as LinkIcon } from 'lucide-react';
interface IntegrationSettings {
id?: string;
integration_n8n_base_url: string;
integration_whatsapp_number: string;
integration_whatsapp_url: string;
integration_google_calendar_id: string;
integration_email_provider: string;
integration_email_api_base_url: string;
integration_privacy_url: string;
integration_terms_url: string;
}
const emptySettings: IntegrationSettings = {
integration_n8n_base_url: '',
integration_whatsapp_number: '',
integration_whatsapp_url: '',
integration_google_calendar_id: '',
integration_email_provider: 'smtp',
integration_email_api_base_url: '',
integration_privacy_url: '/privacy',
integration_terms_url: '/terms',
};
export function IntegrasiTab() {
const [settings, setSettings] = useState<IntegrationSettings>(emptySettings);
const [loading, setLoading] = useState(true);
const [saving, setSaving] = useState(false);
useEffect(() => {
fetchSettings();
}, []);
const fetchSettings = async () => {
const { data, error } = await supabase
.from('platform_settings')
.select('*')
.single();
if (data) {
setSettings({
id: data.id,
integration_n8n_base_url: data.integration_n8n_base_url || '',
integration_whatsapp_number: data.integration_whatsapp_number || '',
integration_whatsapp_url: data.integration_whatsapp_url || '',
integration_google_calendar_id: data.integration_google_calendar_id || '',
integration_email_provider: data.integration_email_provider || 'smtp',
integration_email_api_base_url: data.integration_email_api_base_url || '',
integration_privacy_url: data.integration_privacy_url || '/privacy',
integration_terms_url: data.integration_terms_url || '/terms',
});
}
setLoading(false);
};
const saveSettings = async () => {
setSaving(true);
const payload = { ...settings };
delete payload.id;
if (settings.id) {
const { error } = await supabase
.from('platform_settings')
.update(payload)
.eq('id', settings.id);
if (error) toast({ title: 'Error', description: error.message, variant: 'destructive' });
else toast({ title: 'Berhasil', description: 'Pengaturan integrasi disimpan' });
} else {
const { data, error } = await supabase
.from('platform_settings')
.insert(payload)
.select()
.single();
if (error) toast({ title: 'Error', description: error.message, variant: 'destructive' });
else {
setSettings({ ...settings, id: data.id });
toast({ title: 'Berhasil', description: 'Pengaturan integrasi disimpan' });
}
}
setSaving(false);
};
if (loading) return <div className="animate-pulse h-64 bg-muted rounded-md" />;
return (
<div className="space-y-6">
{/* n8n / Webhook */}
<Card className="border-2 border-border">
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Webhook className="w-5 h-5" />
n8n / Webhook
</CardTitle>
<CardDescription>
Konfigurasi URL untuk integrasi otomatisasi
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="space-y-2">
<Label>Base URL n8n / Integrasi</Label>
<Input
value={settings.integration_n8n_base_url}
onChange={(e) => setSettings({ ...settings, integration_n8n_base_url: e.target.value })}
placeholder="https://automation.domain.com"
className="border-2"
/>
<p className="text-sm text-muted-foreground">
Digunakan sebagai target default untuk webhook lanjutan. webhook_url per template tetap harus URL lengkap.
</p>
</div>
</CardContent>
</Card>
{/* WhatsApp */}
<Card className="border-2 border-border">
<CardHeader>
<CardTitle className="flex items-center gap-2">
<MessageSquare className="w-5 h-5" />
WhatsApp
</CardTitle>
<CardDescription>
Nomor kontak untuk dukungan dan notifikasi
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="space-y-2">
<Label>Nomor WhatsApp Dukungan</Label>
<Input
value={settings.integration_whatsapp_number}
onChange={(e) => setSettings({ ...settings, integration_whatsapp_number: e.target.value })}
placeholder="+62812xxxx"
className="border-2"
/>
<p className="text-sm text-muted-foreground">
Ditampilkan di konfirmasi konsultasi
</p>
</div>
<div className="space-y-2">
<Label>URL WhatsApp Click-to-Chat (Opsional)</Label>
<Input
value={settings.integration_whatsapp_url}
onChange={(e) => setSettings({ ...settings, integration_whatsapp_url: e.target.value })}
placeholder="https://wa.me/62812..."
className="border-2"
/>
</div>
</div>
</CardContent>
</Card>
{/* Google Calendar */}
<Card className="border-2 border-border">
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Calendar className="w-5 h-5" />
Google Calendar
</CardTitle>
<CardDescription>
Untuk pembuatan event konsultasi otomatis
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="space-y-2">
<Label>ID Kalender Google untuk Konsultasi</Label>
<Input
value={settings.integration_google_calendar_id}
onChange={(e) => setSettings({ ...settings, integration_google_calendar_id: e.target.value })}
placeholder="your-calendar@gmail.com"
className="border-2"
/>
<p className="text-sm text-muted-foreground">
Backend/n8n akan menggunakan ID ini untuk membuat event
</p>
</div>
</CardContent>
</Card>
{/* Email Provider */}
<Card className="border-2 border-border">
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Mail className="w-5 h-5" />
Provider Email (Opsional)
</CardTitle>
<CardDescription>
Konfigurasi alternatif selain SMTP
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="space-y-2">
<Label>Provider Email Eksternal</Label>
<Select
value={settings.integration_email_provider}
onValueChange={(value) => setSettings({ ...settings, integration_email_provider: value })}
>
<SelectTrigger className="border-2">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="smtp">SMTP (Default)</SelectItem>
<SelectItem value="resend">Resend</SelectItem>
<SelectItem value="mailgun">Mailgun</SelectItem>
<SelectItem value="sendgrid">SendGrid</SelectItem>
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<Label>API Base URL Provider Email</Label>
<Input
value={settings.integration_email_api_base_url}
onChange={(e) => setSettings({ ...settings, integration_email_api_base_url: e.target.value })}
placeholder="https://api.resend.com"
className="border-2"
disabled={settings.integration_email_provider === 'smtp'}
/>
</div>
</div>
</CardContent>
</Card>
{/* Public Links */}
<Card className="border-2 border-border">
<CardHeader>
<CardTitle className="flex items-center gap-2">
<LinkIcon className="w-5 h-5" />
Link Publik
</CardTitle>
<CardDescription>
URL untuk halaman legal
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="space-y-2">
<Label>URL Kebijakan Privasi</Label>
<Input
value={settings.integration_privacy_url}
onChange={(e) => setSettings({ ...settings, integration_privacy_url: e.target.value })}
placeholder="/privacy"
className="border-2"
/>
</div>
<div className="space-y-2">
<Label>URL Syarat & Ketentuan</Label>
<Input
value={settings.integration_terms_url}
onChange={(e) => setSettings({ ...settings, integration_terms_url: e.target.value })}
placeholder="/terms"
className="border-2"
/>
</div>
</div>
<p className="text-sm text-muted-foreground">
Default: halaman internal. Bisa diganti dengan URL eksternal.
</p>
</CardContent>
</Card>
<Button onClick={saveSettings} disabled={saving} className="shadow-sm">
{saving ? 'Menyimpan...' : 'Simpan Pengaturan'}
</Button>
</div>
);
}

View File

@@ -41,14 +41,49 @@ const SHORTCODES_HELP = {
event: ['{judul_event}', '{tanggal_event}', '{jam_event}', '{link_event}'],
};
const DEFAULT_TEMPLATES = [
{ key: 'payment_success', name: 'Pembayaran Berhasil' },
{ key: 'access_granted', name: 'Akses Produk Diberikan' },
{ key: 'order_created', name: 'Pesanan Dibuat' },
{ key: 'payment_reminder', name: 'Pengingat Pembayaran' },
{ key: 'consulting_scheduled', name: 'Konsultasi Terjadwal' },
{ key: 'event_reminder', name: 'Reminder Webinar/Bootcamp' },
{ key: 'bootcamp_progress', name: 'Progress Bootcamp' },
const DEFAULT_TEMPLATES: { key: string; name: string; defaultSubject: string; defaultBody: string }[] = [
{
key: 'payment_success',
name: 'Pembayaran Berhasil',
defaultSubject: 'Pembayaran Berhasil - Order #{order_id}',
defaultBody: '<h2>Halo {nama}!</h2><p>Terima kasih, pembayaran Anda sebesar <strong>{total}</strong> telah berhasil dikonfirmasi.</p><p><strong>Detail Pesanan:</strong></p><ul><li>Order ID: {order_id}</li><li>Tanggal: {tanggal_pesanan}</li><li>Metode: {metode_pembayaran}</li></ul><p>Produk: {produk}</p>'
},
{
key: 'access_granted',
name: 'Akses Produk Diberikan',
defaultSubject: 'Akses Anda Sudah Aktif - {produk}',
defaultBody: '<h2>Halo {nama}!</h2><p>Selamat! Akses Anda ke <strong>{produk}</strong> sudah aktif.</p><p><a href="{link_akses}" style="display:inline-block;padding:12px 24px;background:#0066cc;color:white;text-decoration:none;border-radius:6px;">Akses Sekarang</a></p>'
},
{
key: 'order_created',
name: 'Pesanan Dibuat',
defaultSubject: 'Pesanan Anda #{order_id} Sedang Diproses',
defaultBody: '<h2>Halo {nama}!</h2><p>Pesanan Anda dengan nomor <strong>{order_id}</strong> telah kami terima.</p><p>Total: <strong>{total}</strong></p><p>Silakan selesaikan pembayaran sebelum batas waktu.</p>'
},
{
key: 'payment_reminder',
name: 'Pengingat Pembayaran',
defaultSubject: 'Jangan Lupa Bayar - Order #{order_id}',
defaultBody: '<h2>Halo {nama}!</h2><p>Pesanan Anda dengan nomor <strong>{order_id}</strong> menunggu pembayaran.</p><p>Total: <strong>{total}</strong></p><p>Segera selesaikan pembayaran agar tidak kedaluwarsa.</p>'
},
{
key: 'consulting_scheduled',
name: 'Konsultasi Terjadwal',
defaultSubject: 'Konsultasi Anda Sudah Terjadwal - {tanggal_konsultasi}',
defaultBody: '<h2>Halo {nama}!</h2><p>Sesi konsultasi Anda telah dikonfirmasi:</p><ul><li>Tanggal: <strong>{tanggal_konsultasi}</strong></li><li>Jam: <strong>{jam_konsultasi}</strong></li></ul><p>Link meeting: <a href="{link_meet}">{link_meet}</a></p><p>Jika ada pertanyaan, hubungi kami.</p>'
},
{
key: 'event_reminder',
name: 'Reminder Webinar/Bootcamp',
defaultSubject: 'Reminder: {judul_event} Dimulai {tanggal_event}',
defaultBody: '<h2>Halo {nama}!</h2><p>Jangan lupa, <strong>{judul_event}</strong> akan dimulai:</p><ul><li>Tanggal: {tanggal_event}</li><li>Jam: {jam_event}</li></ul><p><a href="{link_event}" style="display:inline-block;padding:12px 24px;background:#0066cc;color:white;text-decoration:none;border-radius:6px;">Bergabung</a></p>'
},
{
key: 'bootcamp_progress',
name: 'Progress Bootcamp',
defaultSubject: 'Update Progress Bootcamp Anda',
defaultBody: '<h2>Halo {nama}!</h2><p>Ini adalah update progress bootcamp Anda.</p><p>Terus semangat belajar!</p>'
},
];
const emptySmtp: SmtpSettings = {
@@ -94,8 +129,8 @@ export function NotifikasiTab() {
key: t.key,
name: t.name,
is_active: false,
email_subject: '',
email_body_html: '',
email_subject: t.defaultSubject,
email_body_html: t.defaultBody,
webhook_url: '',
}));
const { data, error } = await supabase.from('notification_templates').insert(toInsert).select();
@@ -261,8 +296,8 @@ export function NotifikasiTab() {
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="text-sm text-muted-foreground p-3 bg-muted rounded-md">
<p className="font-medium mb-2">Shortcode yang tersedia:</p>
<div className="text-sm text-muted-foreground p-3 bg-muted rounded-md space-y-2">
<p className="font-medium">Shortcode yang tersedia:</p>
<div className="grid grid-cols-1 md:grid-cols-2 gap-2">
<div>
<span className="font-medium">Umum:</span> {SHORTCODES_HELP.common.join(', ')}
@@ -277,6 +312,11 @@ export function NotifikasiTab() {
<span className="font-medium">Event:</span> {SHORTCODES_HELP.event.join(', ')}
</div>
</div>
<p className="text-xs mt-2 p-2 bg-background rounded border">
<strong>Penting:</strong> Toggle "Aktifkan" hanya mengontrol pengiriman email.
Jika <code>webhook_url</code> diisi, sistem tetap akan mengirim payload ke URL tersebut
meskipun email dinonaktifkan.
</p>
</div>
{templates.map((template) => (

View File

@@ -9,6 +9,7 @@ import { useCart } from '@/contexts/CartContext';
import { toast } from '@/hooks/use-toast';
import { Skeleton } from '@/components/ui/skeleton';
import { formatIDR } from '@/lib/format';
import { Video } from 'lucide-react';
interface Product {
id: string;
@@ -21,27 +22,45 @@ interface Product {
is_active: boolean;
}
interface ConsultingSettings {
is_consulting_enabled: boolean;
consulting_block_price: number;
consulting_block_duration_minutes: number;
}
export default function Products() {
const [products, setProducts] = useState<Product[]>([]);
const [consultingSettings, setConsultingSettings] = useState<ConsultingSettings | null>(null);
const [loading, setLoading] = useState(true);
const { addItem, items } = useCart();
useEffect(() => {
fetchProducts();
fetchData();
}, []);
const fetchProducts = async () => {
const { data, error } = await supabase
.from('products')
.select('*')
.eq('is_active', true)
.order('created_at', { ascending: false });
const fetchData = async () => {
const [productsRes, consultingRes] = await Promise.all([
supabase
.from('products')
.select('*')
.eq('is_active', true)
.order('created_at', { ascending: false }),
supabase
.from('consulting_settings')
.select('is_consulting_enabled, consulting_block_price, consulting_block_duration_minutes')
.single(),
]);
if (error) {
if (productsRes.error) {
toast({ title: 'Error', description: 'Gagal memuat produk', variant: 'destructive' });
} else {
setProducts(data || []);
setProducts(productsRes.data || []);
}
if (consultingRes.data) {
setConsultingSettings(consultingRes.data);
}
setLoading(false);
};
@@ -87,12 +106,42 @@ export default function Products() {
</Card>
))}
</div>
) : products.length === 0 ? (
<div className="text-center py-12">
<p className="text-muted-foreground">Belum ada produk tersedia.</p>
</div>
) : (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{/* Consulting Card - Only show when enabled */}
{consultingSettings?.is_consulting_enabled && (
<Card className="border-2 border-primary shadow-sm hover:shadow-md transition-shadow bg-primary/5">
<CardHeader>
<div className="flex justify-between items-start">
<CardTitle className="text-xl flex items-center gap-2">
<Video className="w-5 h-5" />
Konsultasi 1-on-1
</CardTitle>
<Badge className="bg-primary">Konsultasi</Badge>
</div>
<CardDescription className="line-clamp-2">
Sesi konsultasi pribadi dengan mentor. Pilih waktu dan durasi sesuai kebutuhan Anda.
</CardDescription>
</CardHeader>
<CardContent>
<div className="flex items-center gap-2 mb-4">
<span className="text-2xl font-bold">
{formatIDR(consultingSettings.consulting_block_price)}
</span>
<span className="text-muted-foreground">
/ {consultingSettings.consulting_block_duration_minutes} menit
</span>
</div>
<Link to="/consulting">
<Button className="w-full shadow-sm">
Booking Sekarang
</Button>
</Link>
</CardContent>
</Card>
)}
{/* Regular Products */}
{products.map((product) => (
<Card key={product.id} className="border-2 border-border shadow-sm hover:shadow-md transition-shadow">
<CardHeader>
@@ -128,6 +177,12 @@ export default function Products() {
</CardContent>
</Card>
))}
{products.length === 0 && !consultingSettings?.is_consulting_enabled && (
<div className="col-span-full text-center py-12">
<p className="text-muted-foreground">Belum ada produk tersedia.</p>
</div>
)}
</div>
)}
</div>

View File

@@ -7,6 +7,8 @@ import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
import { WorkhoursTab } from '@/components/admin/settings/WorkhoursTab';
import { NotifikasiTab } from '@/components/admin/settings/NotifikasiTab';
import { KonsultasiTab } from '@/components/admin/settings/KonsultasiTab';
import { BrandingTab } from '@/components/admin/settings/BrandingTab';
import { IntegrasiTab } from '@/components/admin/settings/IntegrasiTab';
import { Clock, Bell, Video, Palette, Puzzle } from 'lucide-react';
export default function AdminSettings() {
@@ -51,11 +53,11 @@ export default function AdminSettings() {
<Video className="w-4 h-4" />
<span className="hidden sm:inline">Konsultasi</span>
</TabsTrigger>
<TabsTrigger value="branding" className="flex items-center gap-2" disabled>
<TabsTrigger value="branding" className="flex items-center gap-2">
<Palette className="w-4 h-4" />
<span className="hidden sm:inline">Branding</span>
</TabsTrigger>
<TabsTrigger value="integrasi" className="flex items-center gap-2" disabled>
<TabsTrigger value="integrasi" className="flex items-center gap-2">
<Puzzle className="w-4 h-4" />
<span className="hidden sm:inline">Integrasi</span>
</TabsTrigger>
@@ -74,15 +76,11 @@ export default function AdminSettings() {
</TabsContent>
<TabsContent value="branding">
<div className="text-center py-12 text-muted-foreground">
Fitur Branding akan segera hadir
</div>
<BrandingTab />
</TabsContent>
<TabsContent value="integrasi">
<div className="text-center py-12 text-muted-foreground">
Fitur Integrasi akan segera hadir
</div>
<IntegrasiTab />
</TabsContent>
</Tabs>
</div>