feat(admin): streamline group management
This commit is contained in:
@@ -0,0 +1,105 @@
|
||||
"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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user