admin/hooks/useAdminCrudFormModal.ts
alisaza e1eaf5eff5 feat: initial ghabilee-admin backoffice app
Extract admin dashboard from ghabilee-frontend2 into a dedicated Next.js
app for backoffice.ghabilee.ir (no SEO indexing / Clarity).
2026-09-05 13:12:59 +03:30

90 lines
3.1 KiB
TypeScript

'use client'
import { useEffect, useState } from 'react'
import { type DefaultValues, type FieldValues, type Resolver, useForm, type UseFormReturn } from 'react-hook-form'
import { addToast } from '@/lib/toast'
import type { ServiceResult } from '@/services/errorHandler'
interface UseAdminCrudFormModalOptions<TEntity, TFormValues extends FieldValues, TPayload, TId> {
/** Present when editing; absent (undefined) when creating a new entity. */
entity: TEntity | undefined
isOpen: boolean
emptyValues: TFormValues
toFormValues: (entity: TEntity) => TFormValues
resolver: Resolver<TFormValues>
buildPayload: (values: TFormValues) => TPayload
create: (payload: TPayload) => Promise<ServiceResult<unknown>>
update: (id: TId, payload: TPayload) => Promise<ServiceResult<unknown>>
getId: (entity: TEntity) => TId
successMessage: { create: string; edit: string }
onOpenChange: (isOpen: boolean) => void
onSuccess: () => void
}
interface UseAdminCrudFormModalResult<TFormValues extends FieldValues> {
form: UseFormReturn<TFormValues>
isEdit: boolean
submitting: boolean
// react-hook-form's form.handleSubmit(...) is Promise-returning, not
// void — declaring void here would be a lying type that trips
// @typescript-eslint/no-misused-promises at the return statement below.
handleSubmit: () => Promise<void>
}
/**
* Owns the repeated *shape* behind the admin create/edit form modals (tags,
* event categories, blog articles, …): a reset-on-open effect plus a
* submitting-flag/create-or-update/toast/close/refresh submit flow. Each
* modal keeps its own field set (`emptyValues`, `toFormValues`) and payload
* shape (`buildPayload`) — this hook never inspects either.
*/
export default function useAdminCrudFormModal<TEntity, TFormValues extends FieldValues, TPayload, TId>({
entity,
isOpen,
emptyValues,
toFormValues,
resolver,
buildPayload,
create,
update,
getId,
successMessage,
onOpenChange,
onSuccess,
}: UseAdminCrudFormModalOptions<TEntity, TFormValues, TPayload, TId>): UseAdminCrudFormModalResult<TFormValues> {
const isEdit = Boolean(entity)
const [submitting, setSubmitting] = useState(false)
const form = useForm<TFormValues>({
resolver,
defaultValues: (entity ? toFormValues(entity) : emptyValues) as DefaultValues<TFormValues>,
})
const { reset } = form
// Modal instance is reused across opens, so form state needs an explicit
// reset whenever the target entity changes or the modal re-opens.
useEffect(() => {
if (isOpen) {
reset(entity ? toFormValues(entity) : emptyValues)
}
}, [entity, isOpen, reset, toFormValues, emptyValues])
const onSubmit = async (values: TFormValues) => {
setSubmitting(true)
const payload = buildPayload(values)
const result = isEdit && entity ? await update(getId(entity), payload) : await create(payload)
setSubmitting(false)
if (!result.ok) return
addToast({ title: isEdit ? successMessage.edit : successMessage.create, color: 'success' })
onOpenChange(false)
onSuccess()
}
return { form, isEdit, submitting, handleSubmit: form.handleSubmit(onSubmit) }
}