diff --git a/src/components/AppLayout.tsx b/src/components/AppLayout.tsx
index 4ee5392..48e9eab 100644
--- a/src/components/AppLayout.tsx
+++ b/src/components/AppLayout.tsx
@@ -20,6 +20,7 @@ import {
Home,
MoreHorizontal,
X,
+ Video,
} from 'lucide-react';
interface NavItem {
@@ -40,6 +41,7 @@ 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 },
{ label: 'Kalender', href: '/admin/events', icon: Calendar },
@@ -152,15 +154,17 @@ export function AppLayout({ children }: AppLayoutProps) {
-
-
- Keranjang
- {items.length > 0 && (
-
- {items.length}
-
- )}
-
+ {!isAdmin && (
+
+
+ Keranjang
+ {items.length > 0 && (
+
+ {items.length}
+
+ )}
+
+ )}
diff --git a/src/pages/Admin.tsx b/src/pages/Admin.tsx
index 752723d..e1ec578 100644
--- a/src/pages/Admin.tsx
+++ b/src/pages/Admin.tsx
@@ -20,7 +20,7 @@ import { CurriculumEditor } from '@/components/admin/CurriculumEditor';
interface Product { id: string; title: string; slug: string; type: string; description: string; content: string; meeting_link: string | null; recording_url: string | null; price: number; sale_price: number | null; is_active: boolean; }
-const emptyProduct = { title: '', slug: '', type: 'consulting', description: '', content: '', meeting_link: '', recording_url: '', price: 0, sale_price: null as number | null, is_active: true };
+const emptyProduct = { title: '', slug: '', type: 'webinar', description: '', content: '', meeting_link: '', recording_url: '', price: 0, sale_price: null as number | null, is_active: true };
export default function Admin() {
const { user, isAdmin, loading: authLoading } = useAuth();
@@ -102,7 +102,7 @@ export default function Admin() {
setForm({ ...form, title: e.target.value, slug: generateSlug(e.target.value) })} className="border-2" />
setForm({ ...form, slug: e.target.value })} className="border-2" />
-
+
diff --git a/src/pages/ConsultingBooking.tsx b/src/pages/ConsultingBooking.tsx
index 7ac1450..0df2128 100644
--- a/src/pages/ConsultingBooking.tsx
+++ b/src/pages/ConsultingBooking.tsx
@@ -13,7 +13,7 @@ import { Skeleton } from '@/components/ui/skeleton';
import { toast } from '@/hooks/use-toast';
import { formatIDR } from '@/lib/format';
import { Video, Clock, Calendar as CalendarIcon, MessageSquare } from 'lucide-react';
-import { format, addMinutes, parse, isAfter, isBefore, startOfDay, addDays } from 'date-fns';
+import { format, addMinutes, parse, isAfter, isBefore, startOfDay, addDays, isSameDay } from 'date-fns';
import { id } from 'date-fns/locale';
interface ConsultingSettings {
@@ -106,6 +106,8 @@ export default function ConsultingBooking() {
const slots: TimeSlot[] = [];
const duration = settings.consulting_block_duration_minutes;
+ const now = new Date();
+ const isToday = isSameDay(selectedDate, now);
for (const wh of dayWorkhours) {
let current = parse(wh.start_time, 'HH:mm:ss', selectedDate);
@@ -122,10 +124,13 @@ export default function ConsultingBooking() {
return !(slotEnd <= csStart || slotStart >= csEnd);
});
+ // Check if slot is in the past for today
+ const isPassed = isToday && isBefore(current, now);
+
slots.push({
start: slotStart,
end: slotEnd,
- available: !isConflict,
+ available: !isConflict && !isPassed,
});
current = addMinutes(current, duration);
diff --git a/src/pages/admin/AdminProducts.tsx b/src/pages/admin/AdminProducts.tsx
index 3dc304f..078a78a 100644
--- a/src/pages/admin/AdminProducts.tsx
+++ b/src/pages/admin/AdminProducts.tsx
@@ -32,13 +32,12 @@ interface Product {
price: number;
sale_price: number | null;
is_active: boolean;
- consulting_duration_minutes: number | null;
}
const emptyProduct = {
title: '',
slug: '',
- type: 'consulting',
+ type: 'webinar',
description: '',
content: '',
meeting_link: '',
@@ -46,7 +45,6 @@ const emptyProduct = {
price: 0,
sale_price: null as number | null,
is_active: true,
- consulting_duration_minutes: 60,
};
export default function AdminProducts() {
@@ -89,7 +87,6 @@ export default function AdminProducts() {
price: product.price,
sale_price: product.sale_price,
is_active: product.is_active,
- consulting_duration_minutes: product.consulting_duration_minutes || 60,
});
setActiveTab('details');
setDialogOpen(true);
@@ -119,7 +116,6 @@ export default function AdminProducts() {
price: form.price,
sale_price: form.sale_price || null,
is_active: form.is_active,
- consulting_duration_minutes: form.type === 'consulting' ? form.consulting_duration_minutes : null,
};
if (editingProduct) {
@@ -246,23 +242,11 @@ export default function AdminProducts() {
- {form.type === 'consulting' && (
-
-
- setForm({ ...form, consulting_duration_minutes: parseInt(e.target.value) || 60 })}
- className="border-2"
- />
-
- )}
setForm({ ...form, description: v })} />
diff --git a/supabase/config.toml b/supabase/config.toml
new file mode 100644
index 0000000..f771d52
--- /dev/null
+++ b/supabase/config.toml
@@ -0,0 +1,22 @@
+project_id = "lovable"
+
+[api]
+enabled = true
+port = 54321
+schemas = ["public", "graphql_public"]
+extra_search_path = ["public", "extensions"]
+max_rows = 1000
+
+[db]
+port = 54322
+major_version = 15
+
+[studio]
+enabled = true
+port = 54323
+
+[functions.pakasir-webhook]
+verify_jwt = false
+
+[functions.send-test-email]
+verify_jwt = false
diff --git a/supabase/functions/send-test-email/index.ts b/supabase/functions/send-test-email/index.ts
new file mode 100644
index 0000000..0749d37
--- /dev/null
+++ b/supabase/functions/send-test-email/index.ts
@@ -0,0 +1,179 @@
+import { serve } from "https://deno.land/std@0.190.0/http/server.ts";
+
+const corsHeaders = {
+ "Access-Control-Allow-Origin": "*",
+ "Access-Control-Allow-Headers": "authorization, x-client-info, apikey, content-type",
+};
+
+interface TestEmailRequest {
+ to: string;
+ smtp_host: string;
+ smtp_port: number;
+ smtp_username: string;
+ smtp_password: string;
+ smtp_from_name: string;
+ smtp_from_email: string;
+ smtp_use_tls: boolean;
+}
+
+async function sendEmail(config: TestEmailRequest): Promise<{ success: boolean; message: string }> {
+ const { to, smtp_host, smtp_port, smtp_username, smtp_password, smtp_from_name, smtp_from_email, smtp_use_tls } = config;
+
+ // Build email content
+ const boundary = "----=_Part_" + Math.random().toString(36).substr(2, 9);
+ const emailContent = [
+ `From: "${smtp_from_name}" <${smtp_from_email}>`,
+ `To: ${to}`,
+ `Subject: =?UTF-8?B?${btoa("Email Uji Coba - Konfigurasi SMTP Berhasil")}?=`,
+ `MIME-Version: 1.0`,
+ `Content-Type: multipart/alternative; boundary="${boundary}"`,
+ ``,
+ `--${boundary}`,
+ `Content-Type: text/plain; charset=UTF-8`,
+ ``,
+ `Ini adalah email uji coba dari sistem notifikasi Anda.`,
+ `Jika Anda menerima email ini, konfigurasi SMTP Anda sudah benar.`,
+ ``,
+ `--${boundary}`,
+ `Content-Type: text/html; charset=UTF-8`,
+ ``,
+ `
+
+
+
+
+
Email Uji Coba Berhasil! ✓
+
Ini adalah email uji coba dari sistem notifikasi Anda.
+
Jika Anda menerima email ini, konfigurasi SMTP Anda sudah benar.
+
+
+ Dikirim dari: ${smtp_from_email}
+ Server: ${smtp_host}:${smtp_port}
+
+
+
+`,
+ `--${boundary}--`,
+ ].join("\r\n");
+
+ // Connect to SMTP server
+ const conn = smtp_use_tls
+ ? await Deno.connectTls({ hostname: smtp_host, port: smtp_port })
+ : await Deno.connect({ hostname: smtp_host, port: smtp_port });
+
+ const encoder = new TextEncoder();
+ const decoder = new TextDecoder();
+
+ async function readResponse(): Promise {
+ const buffer = new Uint8Array(1024);
+ const n = await conn.read(buffer);
+ if (n === null) return "";
+ return decoder.decode(buffer.subarray(0, n));
+ }
+
+ async function sendCommand(cmd: string): Promise {
+ await conn.write(encoder.encode(cmd + "\r\n"));
+ return await readResponse();
+ }
+
+ try {
+ // Read greeting
+ await readResponse();
+
+ // EHLO
+ let response = await sendCommand(`EHLO localhost`);
+ console.log("EHLO response:", response);
+
+ // For non-TLS connection on port 587, we may need STARTTLS
+ if (!smtp_use_tls && response.includes("STARTTLS")) {
+ await sendCommand("STARTTLS");
+ // Upgrade to TLS - not supported in basic Deno.connect
+ // For now, recommend using TLS directly
+ }
+
+ // AUTH LOGIN
+ response = await sendCommand("AUTH LOGIN");
+ console.log("AUTH response:", response);
+
+ // Username (base64)
+ response = await sendCommand(btoa(smtp_username));
+ console.log("Username response:", response);
+
+ // Password (base64)
+ response = await sendCommand(btoa(smtp_password));
+ console.log("Password response:", response);
+
+ if (!response.includes("235") && !response.includes("Authentication successful")) {
+ throw new Error("Authentication failed: " + response);
+ }
+
+ // MAIL FROM
+ response = await sendCommand(`MAIL FROM:<${smtp_from_email}>`);
+ if (!response.includes("250")) {
+ throw new Error("MAIL FROM failed: " + response);
+ }
+
+ // RCPT TO
+ response = await sendCommand(`RCPT TO:<${to}>`);
+ if (!response.includes("250")) {
+ throw new Error("RCPT TO failed: " + response);
+ }
+
+ // DATA
+ response = await sendCommand("DATA");
+ if (!response.includes("354")) {
+ throw new Error("DATA failed: " + response);
+ }
+
+ // Send email content
+ await conn.write(encoder.encode(emailContent + "\r\n.\r\n"));
+ response = await readResponse();
+ if (!response.includes("250")) {
+ throw new Error("Email send failed: " + response);
+ }
+
+ // QUIT
+ await sendCommand("QUIT");
+ conn.close();
+
+ return { success: true, message: "Email uji coba berhasil dikirim ke " + to };
+ } catch (error) {
+ conn.close();
+ throw error;
+ }
+}
+
+serve(async (req: Request): Promise => {
+ // Handle CORS preflight
+ if (req.method === "OPTIONS") {
+ return new Response(null, { headers: corsHeaders });
+ }
+
+ try {
+ const body: TestEmailRequest = await req.json();
+
+ // Validate required fields
+ if (!body.to || !body.smtp_host || !body.smtp_username || !body.smtp_password) {
+ return new Response(
+ JSON.stringify({ success: false, message: "Missing required fields" }),
+ { status: 400, headers: { ...corsHeaders, "Content-Type": "application/json" } }
+ );
+ }
+
+ console.log("Attempting to send test email to:", body.to);
+ console.log("SMTP config:", { host: body.smtp_host, port: body.smtp_port, user: body.smtp_username });
+
+ const result = await sendEmail(body);
+
+ return new Response(
+ JSON.stringify(result),
+ { status: 200, headers: { ...corsHeaders, "Content-Type": "application/json" } }
+ );
+ } catch (error: any) {
+ console.error("Error sending test email:", error);
+ return new Response(
+ JSON.stringify({ success: false, message: error.message || "Failed to send email" }),
+ { status: 500, headers: { ...corsHeaders, "Content-Type": "application/json" } }
+ );
+ }
+});