Files
minecraft-account-manager/apps/web/src/components/admin-modal-form.tsx
T
dmg d4afb71798
CI / validate (push) Successful in 4m16s
Release / release (push) Failing after 9m14s
feat(admin): streamline group management
2026-08-02 11:06:55 -04:00

106 lines
3.4 KiB
TypeScript

"use client";
import type { ReactNode, RefObject } from "react";
import { useEffect, useId, useRef, useState } from "react";
import { useFormStatus } from "react-dom";
export function AdminModalForm({
action,
children,
description,
intent = "default",
submitLabel,
title,
triggerClassName,
triggerLabel,
triggerPressed,
}: {
action: (formData: FormData) => Promise<void>;
children?: ReactNode;
description: string;
intent?: "default" | "danger";
submitLabel: string;
title: string;
triggerClassName?: string;
triggerLabel: string;
triggerPressed?: boolean;
}) {
const dialogRef = useRef<HTMLDialogElement>(null);
const titleId = useId();
const descriptionId = useId();
const [submitting, setSubmitting] = useState(false);
return (
<>
<button
aria-haspopup="dialog"
aria-pressed={triggerPressed}
className={triggerClassName ?? "font-mono text-[10px] font-bold uppercase underline underline-offset-4"}
disabled={submitting}
onClick={() => dialogRef.current?.showModal()}
type="button"
>
{triggerLabel}
</button>
<dialog
aria-describedby={descriptionId}
aria-labelledby={titleId}
className="admin-modal m-auto w-[min(92vw,36rem)] border border-ink bg-panel p-0 text-ink shadow-[10px_10px_0_var(--color-shadow)] backdrop:bg-ink/70"
onCancel={(event) => { if (submitting) event.preventDefault(); }}
ref={dialogRef}
>
<form action={action} className="p-6 sm:p-8" onSubmit={() => setSubmitting(true)}>
<p className="font-mono text-[9px] font-bold uppercase tracking-[0.2em] text-accent">Confirm operation</p>
<h2 className="mt-3 font-display text-3xl font-black uppercase" id={titleId}>{title}</h2>
<p className="mt-3 text-sm leading-6 text-muted" id={descriptionId}>{description}</p>
{children && <div className="mt-6">{children}</div>}
<ModalActions dialogRef={dialogRef} intent={intent} onPendingChange={setSubmitting} submitLabel={submitLabel} />
</form>
</dialog>
</>
);
}
function ModalActions({
dialogRef,
intent,
onPendingChange,
submitLabel,
}: {
dialogRef: RefObject<HTMLDialogElement | null>;
intent: "default" | "danger";
onPendingChange: (pending: boolean) => void;
submitLabel: string;
}) {
const { pending } = useFormStatus();
const observedPending = useRef(false);
useEffect(() => {
if (pending) {
observedPending.current = true;
onPendingChange(true);
} else if (observedPending.current) {
observedPending.current = false;
onPendingChange(false);
}
}, [onPendingChange, pending]);
return (
<div className="mt-8 flex flex-wrap justify-end gap-3 border-t border-line pt-5">
<button
className="border border-line px-5 py-3 font-mono text-[10px] font-bold uppercase tracking-wider disabled:opacity-50"
disabled={pending}
onClick={() => dialogRef.current?.close()}
type="button"
>
Cancel
</button>
<button
className={`px-5 py-3 font-mono text-[10px] font-bold uppercase tracking-wider text-canvas disabled:cursor-wait disabled:opacity-60 ${intent === "danger" ? "bg-accent" : "bg-ink"}`}
disabled={pending}
type="submit"
>
{pending ? "Applying…" : submitLabel}
</button>
<span aria-live="polite" className="sr-only">{pending ? "Operation in progress." : ""}</span>
</div>
);
}