import { useState, useCallback, useEffect } from "react"; import { Card, CardContent, CardDescription, CardHeader, CardTitle, Input, Label, Button, Alert, AlertDescription, Badge, ScrollArea, Loader2, CheckCircle2, XCircle, AlertCircle, Eye, EyeOff, Key, Zap, Settings, Send, Copy, RefreshCw, toast, t, useAlertDialog, } from "@block-ninja/ui"; type PluginSettingsProps = { pluginName: string; }; type SettingsData = { api_key_configured: boolean; api_key_masked?: string; webhook_url?: string; webhook_secret_configured?: boolean; webhook_secret_masked?: string; last_event_at?: string; last_event_type?: string; }; type TestResult = { success: boolean; error?: string; event_types?: { id: number; title: string; slug: string }[]; }; const copyToClipboard = async (text: string) => { if (!text) return; try { await navigator.clipboard.writeText(text); toast.success(t("calcom.settings.copied", "Copied to clipboard")); } catch (_error) { // Clipboard API may be unavailable (non-secure context). toast.error(t("calcom.settings.copyFailed", "Could not copy to clipboard")); } }; const formatRelativeTime = (iso: string): string => { const parsed = Date.parse(iso); if (Number.isNaN(parsed)) return iso; const diffMs = Date.now() - parsed; const minutes = Math.round(diffMs / 60000); if (minutes < 1) return t("calcom.settings.relative.justNow", "just now"); if (minutes < 60) { return minutes === 1 ? t("calcom.settings.relative.minute", "1 minute ago") : t("calcom.settings.relative.minutes", "{count} minutes ago").replace("{count}", String(minutes)); } const hours = Math.round(minutes / 60); if (hours < 24) { return hours === 1 ? t("calcom.settings.relative.hour", "1 hour ago") : t("calcom.settings.relative.hours", "{count} hours ago").replace("{count}", String(hours)); } const days = Math.round(hours / 24); return days === 1 ? t("calcom.settings.relative.day", "1 day ago") : t("calcom.settings.relative.days", "{count} days ago").replace("{count}", String(days)); }; function CalcomSettings({ pluginName }: PluginSettingsProps) { const { confirm } = useAlertDialog(); const [apiKey, setApiKey] = useState(""); const [showApiKey, setShowApiKey] = useState(false); const [loading, setLoading] = useState(true); const [saving, setSaving] = useState(false); const [testing, setTesting] = useState(false); const [rotating, setRotating] = useState(false); const [showSecret, setShowSecret] = useState(false); const [settings, setSettings] = useState(null); const [testResult, setTestResult] = useState(null); const [saveMessage, setSaveMessage] = useState<{ type: "success" | "error"; text: string } | null>(null); const baseUrl = `/api/plugins/${pluginName}`; // Follow-up: when @block-ninja/ui re-exports useQuery from connect-query // AND this plugin's settings endpoints move to proto-backed RPCs, swap this // useEffect + fetch pair for useQuery with a staleTime so re-opening the // settings panel does not refetch. Today neither precondition holds (no // .proto for this plugin, no tanstack-query primitives re-exported), so // the raw fetch stays. const loadSettings = useCallback(async () => { try { const res = await fetch(`${baseUrl}/settings`); if (res.ok) { const data = await res.json(); setSettings(data); } } catch (_error) { console.error("Failed to load settings"); } finally { setLoading(false); } }, [baseUrl]); useEffect(() => { loadSettings(); }, [loadSettings]); // Save API key const handleSave = async () => { if (!apiKey.trim()) { setSaveMessage({ type: "error", text: t("calcom.settings.apiKey.errorEmpty", "Please enter an API key") }); return; } setSaving(true); setSaveMessage(null); setTestResult(null); try { const res = await fetch(`${baseUrl}/settings`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ api_key: apiKey }), }); if (res.ok) { // Reload settings to pick up the auto-generated webhook secret. await loadSettings(); setApiKey(""); setSaveMessage({ type: "success", text: t("calcom.settings.apiKey.saved", "API key saved successfully") }); } else { const text = await res.text(); setSaveMessage({ type: "error", text: text || t("calcom.settings.apiKey.saveFailed", "Failed to save API key") }); } } catch (_error) { setSaveMessage({ type: "error", text: t("calcom.settings.serverError", "Failed to connect to server") }); } finally { setSaving(false); } }; // Test connection const handleTest = async () => { setTesting(true); setTestResult(null); try { const res = await fetch(`${baseUrl}/test`, { method: "POST" }); const data = await res.json(); setTestResult(data); } catch (_error) { setTestResult({ success: false, error: t("calcom.settings.serverError", "Failed to connect to server") }); } finally { setTesting(false); } }; // Clear API key const handleClear = async () => { setSaving(true); setSaveMessage(null); try { const res = await fetch(`${baseUrl}/settings`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ api_key: "" }), }); if (res.ok) { await loadSettings(); setTestResult(null); setSaveMessage({ type: "success", text: t("calcom.settings.apiKey.removed", "API key removed") }); } } catch (_error) { setSaveMessage({ type: "error", text: t("calcom.settings.apiKey.clearFailed", "Failed to clear API key") }); } finally { setSaving(false); } }; // Rotate webhook secret const handleRotate = async () => { const confirmed = await confirm({ title: t("calcom.settings.webhook.rotateConfirmTitle", "Rotate webhook secret?"), message: t("calcom.settings.webhook.rotateConfirmMessage", "This invalidates the existing secret immediately. You'll need to update Cal.com to keep webhooks flowing."), confirmLabel: t("calcom.settings.webhook.rotateConfirmLabel", "Rotate"), variant: "destructive", }); if (!confirmed) return; setRotating(true); setSaveMessage(null); try { const res = await fetch(`${baseUrl}/settings/rotate-webhook-secret`, { method: "POST" }); if (res.ok) { const data = await res.json(); setSettings((prev) => (prev ? { ...prev, webhook_secret_masked: data.webhook_secret_masked, webhook_secret_configured: true } : prev)); const successMsg = t("calcom.settings.webhook.rotated", "Webhook secret rotated. Update it in Cal.com."); setSaveMessage({ type: "success", text: successMsg }); toast.success(successMsg); } else { setSaveMessage({ type: "error", text: t("calcom.settings.webhook.rotateFailed", "Failed to rotate webhook secret") }); } } catch (_error) { setSaveMessage({ type: "error", text: t("calcom.settings.serverError", "Failed to connect to server") }); } finally { setRotating(false); } }; if (loading) { return (
); } const apiKeyVisibilityLabel = showApiKey ? t("calcom.settings.toggleVisibility.hide", "Hide") : t("calcom.settings.toggleVisibility.show", "Show"); const secretVisibilityLabel = showSecret ? t("calcom.settings.toggleVisibility.hide", "Hide") : t("calcom.settings.toggleVisibility.show", "Show"); return (
{/* Header */}

{t("calcom.settings.heading", "Cal.com Integration")}

{t("calcom.settings.headingDescription", "Configure your Cal.com API key to enable booking functionality")}

{/* Current Status */} {t("calcom.settings.apiKey.title", "API Key Status")} {t("calcom.settings.apiKey.description", "Your Cal.com API key is required for the booking widget to function")} {settings?.api_key_configured ?
{t("calcom.settings.apiKey.configuredLabel", "Configured")}

{settings.api_key_masked || t("calcom.settings.apiKey.maskedFallback", "***configured***")}

:
{t("calcom.settings.apiKey.notConfiguredLabel", "Not configured")}

{t("calcom.settings.apiKey.notConfiguredHelp", "Enter your Cal.com API key below to enable booking")}

} {/* API Key Input */}
setApiKey(e.target.value)} placeholder={t("calcom.settings.apiKey.placeholder", "cal_live_...")} className="pr-10" />

{t("calcom.settings.apiKey.helpPrefix", "Get your API key from")}{" "} {t("calcom.settings.apiKey.helpLink", "Cal.com API Settings")}

{/* Save Message */} {saveMessage && ( {saveMessage.text} )}
{/* Test Connection */} {settings?.api_key_configured && ( {t("calcom.settings.test.title", "Test Connection")} {t("calcom.settings.test.description", "Verify your API key works and see available event types")} {testResult && (
{testResult.success ? <> {t("calcom.settings.test.successPrefix", "Connection successful! Found")} {testResult.event_types?.length || 0}{" "} {t("calcom.settings.test.successSuffix", "event types.")} {testResult.event_types && testResult.event_types.length > 0 && ( {testResult.event_types.map((et) => ( ))}
{t("calcom.settings.test.tableEventType", "Event Type")} {t("calcom.settings.test.tableSlug", "Slug")}
{et.title} {et.slug}
)} : {testResult.error || t("calcom.settings.test.failure", "Connection failed")} }
)}
)} {/* Webhook */} {settings?.api_key_configured && ( {t("calcom.settings.webhook.title", "Webhook")} {t("calcom.settings.webhook.description", "Configure this URL in Cal.com → Settings → Developer → Webhooks to receive booking events.")}

{t( "calcom.settings.webhook.secretHelp", "Cal.com displays the secret only once. Regenerating invalidates the previous value immediately — update it in Cal.com after rotating.", )}

{settings?.last_event_at ?
{t("calcom.settings.webhook.lastEventPrefix", "Last event")} {formatRelativeTime(settings.last_event_at)} {settings.last_event_type && ( <> {" — "} {settings.last_event_type} )}
:
{t("calcom.settings.webhook.waitingForEvent", "Status: Waiting for first event")}
}
)} {/* Documentation */} {t("calcom.settings.docs.title", "Documentation")} {t("calcom.settings.docs.description", "Reference material for working with the Cal.com plugin.")} {t("calcom.settings.docs.calcomApi", "Cal.com API documentation")} {/* Follow-up: confirm /admin/docs/calcom-plugin route exists; fallback to the FEATURES/docs entry once published. */} {t("calcom.settings.docs.pluginGuide", "BlockNinja plugin guide")}
); } export default CalcomSettings;