diff --git a/frontend/src/components/ConfirmDialog.tsx b/frontend/src/components/ConfirmDialog.tsx new file mode 100644 index 0000000..4785cd1 --- /dev/null +++ b/frontend/src/components/ConfirmDialog.tsx @@ -0,0 +1,151 @@ +import { For, Show, createEffect, createSignal } from "solid-js" +import Modal from "@/components/Modal" + +type ConfirmDialogProps = { + open: boolean + title: string + description: string + /** Optional bullet points shown in a warning box below the description */ + details?: string[] + confirmLabel: string + busyLabel?: string + busy?: boolean + tone?: "danger" | "primary" + onConfirm: () => void | Promise + onClose: () => void +} + +const TONE_STYLES: Record, string> = { + danger: "bg-red-600 text-white hover:bg-red-700", + primary: "bg-blue-600 text-white hover:bg-blue-700", +} + +const DETAIL_BOX_STYLES: Record, string> = { + danger: "border-amber-200 bg-amber-50 text-amber-800", + primary: "border-blue-200 bg-blue-50 text-blue-800", +} + +type ConfirmDialogSnapshot = { + title: string + description: string + details?: string[] + confirmLabel: string + busyLabel?: string + busy: boolean + tone: NonNullable +} + +export default function ConfirmDialog(props: ConfirmDialogProps) { + const [snapshot, setSnapshot] = createSignal({ + title: props.title, + description: props.description, + details: props.details ? [...props.details] : undefined, + confirmLabel: props.confirmLabel, + busyLabel: props.busyLabel, + busy: props.busy ?? false, + tone: props.tone ?? "primary", + }) + + createEffect(() => { + if (!props.open) return + + setSnapshot({ + title: props.title, + description: props.description, + details: props.details ? [...props.details] : undefined, + confirmLabel: props.confirmLabel, + busyLabel: props.busyLabel, + busy: props.busy ?? false, + tone: props.tone ?? "primary", + }) + }) + + const content = () => props.open + ? { + title: props.title, + description: props.description, + details: props.details, + confirmLabel: props.confirmLabel, + busyLabel: props.busyLabel, + busy: props.busy ?? false, + tone: props.tone ?? "primary", + } + : snapshot() + const tone = () => content().tone + const confirmText = () => content().busy ? (content().busyLabel ?? content().confirmLabel) : content().confirmLabel + + function handleClose() { + if (props.busy) return + props.onClose() + } + + function handleConfirm() { + if (props.busy) return + void props.onConfirm() + } + + return ( + +
+
+
+

{content().title}

+
+ +
+
+ +
+
+

{content().description}

+ 0}> +
    + + {(item) => ( +
  • + + {item} +
  • + )} +
    +
+
+
+
+ +
+ + +
+
+ ) +} \ No newline at end of file diff --git a/frontend/src/components/RelayDetailCard.tsx b/frontend/src/components/RelayDetailCard.tsx index 360c0f9..0ccf91b 100644 --- a/frontend/src/components/RelayDetailCard.tsx +++ b/frontend/src/components/RelayDetailCard.tsx @@ -2,6 +2,7 @@ import { A } from "@solidjs/router" import { Show, createEffect, createSignal, onCleanup } from "solid-js" import type { Relay, PlanId } from "@/lib/api" import menuDotsIcon from "@/assets/menu-dots-2.svg" +import ConfirmDialog from "@/components/ConfirmDialog" import Field from "@/components/Field" import PricingTable from "@/components/PricingTable" import ToggleButton from "@/components/ToggleButton" @@ -51,8 +52,8 @@ type RelayDetailCardProps = { currentMembers?: number showTenant?: boolean editHref?: string - onDeactivate?: () => void - onReactivate?: () => void + onDeactivate?: () => void | Promise + onReactivate?: () => void | Promise deactivating?: boolean reactivating?: boolean onTogglePublicJoin?: () => void @@ -76,6 +77,7 @@ export default function RelayDetailCard(props: RelayDetailCardProps) { } const [menuOpen, setMenuOpen] = createSignal(false) const [plan, setPlan] = createSignal(props.relay.plan) + const [pendingAction, setPendingAction] = createSignal<"deactivate" | "reactivate" | null>(null) let menuContainerRef: HTMLDivElement | undefined @@ -86,6 +88,24 @@ export default function RelayDetailCard(props: RelayDetailCardProps) { } const planLimited = () => (props.enforcePlanLimits ?? true) && r().plan === "free" const showPlanActions = () => props.showPlanActions ?? true + const actionBusy = () => pendingAction() === "deactivate" ? !!props.deactivating : pendingAction() === "reactivate" ? !!props.reactivating : false + const relayLabel = () => r().info_name || r().subdomain + const confirmTitle = () => pendingAction() === "deactivate" ? "Deactivate relay?" : "Reactivate relay?" + const confirmDescription = () => pendingAction() === "deactivate" + ? `${relayLabel()} will be taken offline immediately.` + : `${relayLabel()} will come back online and start accepting connections.` + const confirmDetails = () => pendingAction() === "deactivate" + ? [ + "All client connections will be dropped immediately.", + "Members will be unable to read from or publish to the relay.", + "Scheduled and automated tasks (billing, syncing) will be paused.", + "All relay data, settings, and members are preserved, nothing is deleted.", + "You can reactivate at any time from this page.", + ] + : undefined + const confirmLabel = () => pendingAction() === "deactivate" ? "Yes, deactivate" : "Yes, reactivate" + const confirmBusyLabel = () => pendingAction() === "deactivate" ? "Deactivating..." : "Reactivating..." + const confirmTone = () => pendingAction() === "deactivate" ? "danger" : "primary" async function changePlan(plan: PlanId) { setPlan(plan) @@ -97,6 +117,29 @@ export default function RelayDetailCard(props: RelayDetailCardProps) { } } + function openActionDialog(action: "deactivate" | "reactivate") { + setMenuOpen(false) + setPendingAction(action) + } + + function closeActionDialog() { + if (actionBusy()) return + setPendingAction(null) + } + + async function confirmAction() { + const action = pendingAction() + if (!action) return + + if (action === "deactivate") { + await props.onDeactivate?.() + } else { + await props.onReactivate?.() + } + + setPendingAction(null) + } + createEffect(() => { if (!menuOpen()) return @@ -128,7 +171,7 @@ export default function RelayDetailCard(props: RelayDetailCardProps) {
- +
@@ -148,7 +191,7 @@ export default function RelayDetailCard(props: RelayDetailCardProps) {
-
+
) }