import { useCallback, useEffect, useState } from "react";
import {
Alert,
AlertDescription,
type BlockEditorProps,
Button,
type ReactNode,
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
Tabs,
TabsContent,
TabsList,
TabsTrigger,
Input,
Label,
Textarea,
Switch,
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
Settings,
Edit3,
CheckCircle2,
XCircle,
Loader2,
Key,
AlertCircle,
ExternalLink,
RefreshCw,
t,
} from "@block-ninja/ui";
type EventTypeSummary = {
id: number;
slug: string;
title: string;
lengthInMinutes: number;
};
function renderConnectedAccount(apiKeyConfigured: boolean | null, calcomUsername: string) {
if (apiKeyConfigured === false) {
return
{t("calcom.editor.connectedAs.noKey", "Configure the API key in plugin settings to connect an account.")}
;
}
if (calcomUsername) {
return {calcomUsername}
;
}
return {t("calcom.editor.connectedAs.resolving", "Resolving account from API key…")}
;
}
function CalcomBlockEditor({ content, onChange }: BlockEditorProps) {
const [activeTab, setActiveTab] = useState("config");
const [apiKeyConfigured, setApiKeyConfigured] = useState(null);
const [calcomUsername, setCalcomUsername] = useState("");
const [eventTypes, setEventTypes] = useState([]);
const [loadingEventTypes, setLoadingEventTypes] = useState(false);
const [fetchFailed, setFetchFailed] = useState(false);
const [retryCounter, setRetryCounter] = useState(0);
const handleFieldChange = useCallback(
(field: string, value: unknown) => {
onChange({ ...content, [field]: value });
},
[content, onChange],
);
const getString = (field: string, defaultValue = "") => {
return (content[field] as string) || defaultValue;
};
const getNumber = (field: string, defaultValue = 0) => {
const val = content[field];
if (typeof val === "number") return val;
if (typeof val === "string") return parseInt(val, 10) || defaultValue;
return defaultValue;
};
const getBool = (field: string, defaultValue = false) => {
const val = content[field];
if (typeof val === "boolean") return val;
return defaultValue;
};
const eventTypeSlug = getString("eventTypeSlug");
useEffect(() => {
fetch(`/api/plugins/calcomblock/settings`)
.then((r) => r.json())
.then((d: { api_key_configured?: boolean; username?: string }) => {
setApiKeyConfigured(!!d.api_key_configured);
setCalcomUsername(d.username ?? "");
})
.catch(() => setApiKeyConfigured(false));
}, []);
// Stamp the resolved Cal.com username into block content so the public
// render path (block.go BookingConfig.Username) keeps working without any
// backend rewiring. Existing blocks with a stale username get refreshed
// automatically the next time the editor opens.
useEffect(() => {
if (calcomUsername && content.username !== calcomUsername) {
onChange({ ...content, username: calcomUsername });
}
}, [calcomUsername, content, onChange]);
useEffect(() => {
if (!apiKeyConfigured) {
setEventTypes([]);
setFetchFailed(false);
return;
}
const timer = setTimeout(() => {
setLoadingEventTypes(true);
setFetchFailed(false);
fetch(`/api/plugins/calcomblock/event-types?username=${encodeURIComponent(calcomUsername)}`)
.then((r) => r.json())
.then((d: { success?: boolean; event_types?: EventTypeSummary[] }) => {
if (d.success) {
setEventTypes(d.event_types ?? []);
} else {
setEventTypes([]);
setFetchFailed(true);
}
})
.catch(() => {
setEventTypes([]);
setFetchFailed(true);
})
.finally(() => setLoadingEventTypes(false));
}, 300);
return () => clearTimeout(timer);
}, [apiKeyConfigured, calcomUsername, retryCounter]);
const handleRetry = useCallback(() => {
setRetryCounter((n) => n + 1);
}, []);
const selectPlaceholder = (() => {
if (apiKeyConfigured === false) {
return (
{t("calcom.editor.eventType.placeholder.noApiKey", "Configure API key first")}
);
}
if (loadingEventTypes) {
return (
{t("calcom.editor.eventType.placeholder.loading", "Loading…")}
);
}
if (!calcomUsername) {
return (
{t("calcom.editor.eventType.placeholder.resolvingUser", "Resolving account…")}
);
}
if (eventTypes.length === 0) {
return (
{t("calcom.editor.eventType.placeholder.noEventTypes", "No event types found")}
);
}
return t("calcom.editor.eventType.placeholder.select", "Select event type");
})();
const apiKeyReady = apiKeyConfigured === true;
const usernameReady = calcomUsername.length > 0;
const eventTypeReady = eventTypeSlug.length > 0;
const canOpenInCalcom = usernameReady && eventTypeReady;
const renderStatusRow = (ok: boolean, label: string, action?: ReactNode) => (
{ok ?
: }
{label}
{action ?
{action}
: null}
);
return (
{t("calcom.editor.tab.configuration", "Configuration")}
{t("calcom.editor.tab.content", "Content")}
{/* Configuration Tab */}
{apiKeyConfigured === false && (
{t("calcom.editor.alert.apiKeyMissing.prefix", "Configure your Cal.com API key in")}{" "}
{t("calcom.editor.alert.apiKeyMissing.linkLabel", "plugin settings")}
{" "}
{t("calcom.editor.alert.apiKeyMissing.suffix", "to enable the event-type dropdown.")}
)}
{t("calcom.editor.section.config.title", "Cal.com Settings")}
{t("calcom.editor.section.config.description", "Connect to your Cal.com account and select an event type")}
{t("calcom.editor.connectedAs.label", "Connected Account")}
{renderConnectedAccount(apiKeyConfigured, calcomUsername)}
{t("calcom.editor.eventType.label", "Event Type")} *
{fetchFailed ?
<>
handleFieldChange("eventTypeSlug", e.target.value)}
placeholder={t("calcom.editor.eventType.fallbackPlaceholder", "30min")}
className="flex-1"
/>
{t("calcom.editor.eventType.retryLabel", "Retry")}
{t("calcom.editor.eventType.fetchFailed", "Couldn't load event types from Cal.com. Enter the slug manually or retry.")}
>
: <>
handleFieldChange("eventTypeSlug", v)}
disabled={loadingEventTypes || !apiKeyConfigured || eventTypes.length === 0}>
{eventTypes.map((et) => (
{et.title}
{" "}
({et.slug} · {et.lengthInMinutes}m)
))}
{t("calcom.editor.eventType.help", "Fetched from your Cal.com account")}
>
}
{t("calcom.editor.weeksToShow.label", "Weeks to Display")}
handleFieldChange("weeksToShow", parseInt(v, 10))}>
{[1, 2, 3, 4, 5, 6, 7, 8].map((n) => (
{n} {n > 1 ? t("calcom.editor.weeksToShow.unitPlural", "weeks") : t("calcom.editor.weeksToShow.unitSingular", "week")}
))}
{t("calcom.editor.weeksToShow.help", "How many weeks of dates to show in the calendar")}
{t("calcom.editor.showTimezone.label", "Show Timezone Selector")}
{t("calcom.editor.showTimezone.help", "Show a label above the date grid indicating which timezone times are displayed in.")}
handleFieldChange("showTimezone", v)}
aria-label={t("calcom.editor.showTimezone.label", "Show Timezone Selector")}
/>
{t("calcom.editor.status.title", "Configuration status")}
{t("calcom.editor.status.description", "Quick check of the values needed before this block renders bookings.")}
{renderStatusRow(
apiKeyReady,
t("calcom.editor.status.apiKey", "API key configured"),
!apiKeyReady ?
{t("calcom.editor.status.apiKey.configureLink", "Configure")}
: null,
)}
{renderStatusRow(usernameReady, t("calcom.editor.status.username", "Account resolved"))}
{renderStatusRow(eventTypeReady, t("calcom.editor.status.eventType", "Event type selected"))}
·
{t("calcom.editor.status.eventTypesAvailable", "{count} event types available").replace("{count}", String(eventTypes.length))}
{/* Content Tab */}
{t("calcom.editor.section.content.title", "Widget Content")}
{t("calcom.editor.section.content.description", "Customize the text shown in the booking widget")}
{t("calcom.editor.description.label", "Description")}
{t("calcom.editor.unavailableMessage.label", "Booking unavailable message")}
);
}
export default CalcomBlockEditor;