92 lines
2.6 KiB
TypeScript
92 lines
2.6 KiB
TypeScript
"use client";
|
|
|
|
import { useRouter } from "next/navigation";
|
|
import { useTranslations } from "next-intl";
|
|
import { useRef, useState, useTransition } from "react";
|
|
import { toast } from "sonner";
|
|
import type { ActionResult } from "@/lib/safe-action-shared";
|
|
|
|
interface UseServerActionOptions {
|
|
/** Where to redirect after a successful action (optional) */
|
|
redirectOnSuccess?: string;
|
|
}
|
|
|
|
interface RunOptions {
|
|
successMessage?: string;
|
|
errorMessage?: string;
|
|
/** Override redirect path for this specific call */
|
|
redirectTo?: string;
|
|
/** Called on success with the result data. Overrides default router.refresh(). */
|
|
onSuccess?: (data: Record<string, unknown>) => void;
|
|
/** Called when the action fails or throws */
|
|
onError?: () => void;
|
|
}
|
|
|
|
/**
|
|
* Hook that wraps a server action with loading state, toast notifications,
|
|
* and optional redirect/refresh. Drop-in replacement for useAdminForm.
|
|
*
|
|
* @example
|
|
* const { run, isPending } = useServerAction()
|
|
*
|
|
* // Submit
|
|
* run(() => updateNews({ id, ...data }), { successMessage: 'Saved!' })
|
|
*
|
|
* // Delete with redirect
|
|
* run(() => deleteNews(id), { redirectTo: '/admin/news' })
|
|
*/
|
|
export function useServerAction(options: UseServerActionOptions = {}) {
|
|
const t = useTranslations("pages.admin.actions");
|
|
const locked = useRef(false);
|
|
const [fieldErrors, setFieldErrors] = useState<Record<string, string[]>>({});
|
|
const [isPending, startTransition] = useTransition();
|
|
const router = useRouter();
|
|
|
|
function run(action: () => Promise<unknown>, opts: RunOptions = {}) {
|
|
if (locked.current) return;
|
|
locked.current = true;
|
|
setFieldErrors({});
|
|
const {
|
|
successMessage = t("saved"),
|
|
errorMessage = t("error"),
|
|
redirectTo,
|
|
onSuccess,
|
|
onError,
|
|
} = opts;
|
|
|
|
startTransition(async () => {
|
|
try {
|
|
const result = (await action()) as ActionResult | undefined;
|
|
|
|
if (!result || result.ok) {
|
|
toast.success(successMessage);
|
|
if (onSuccess) {
|
|
onSuccess((result?.data ?? {}) as Record<string, unknown>);
|
|
} else {
|
|
const target = redirectTo || options.redirectOnSuccess;
|
|
if (target) router.push(target);
|
|
else router.refresh();
|
|
}
|
|
} else {
|
|
setFieldErrors(result.fieldErrors ?? {});
|
|
toast.error(result.error || errorMessage, {
|
|
description:
|
|
Object.values(result.fieldErrors ?? {})
|
|
.flat()
|
|
.join(" · ") || undefined,
|
|
});
|
|
onError?.();
|
|
}
|
|
} catch (err) {
|
|
console.error("[useServerAction] Unexpected error:", err);
|
|
toast.error(errorMessage);
|
|
onError?.();
|
|
} finally {
|
|
locked.current = false;
|
|
}
|
|
});
|
|
}
|
|
|
|
return { run, isPending, fieldErrors };
|
|
}
|