Extract admin dashboard from ghabilee-frontend2 into a dedicated Next.js app for backoffice.ghabilee.ir (no SEO indexing / Clarity).
90 lines
3.1 KiB
TypeScript
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) }
|
|
}
|