import type { FieldErrors, FieldValues } from 'react-hook-form' import { texts } from '@/texts' import { addToast } from '@/lib/toast' function isPlainObject(value: object): boolean { const proto: object | null = Object.getPrototypeOf(value) as object | null return proto === Object.prototype || proto === null } /** * Walk RHF `FieldErrors` for the first non-empty `message`. * Skips `ref` (DOM / focus handles — often circular) and non-plain objects * so a validation toast can never blow the call stack. */ function findFirstErrorMessage(value: unknown, seen: WeakSet = new WeakSet()): string | undefined { if (!value || typeof value !== 'object') return undefined if (seen.has(value)) return undefined if (Array.isArray(value)) { seen.add(value) for (const item of value) { const message = findFirstErrorMessage(item, seen) if (message) return message } return undefined } // DOM nodes, class instances, etc. — never treat as error trees if (!isPlainObject(value)) return undefined seen.add(value) if ('message' in value && typeof value.message === 'string' && value.message.trim()) { return value.message } for (const [key, nestedValue] of Object.entries(value)) { if (key === 'ref') continue const message = findFirstErrorMessage(nestedValue, seen) if (message) return message } return undefined } /** React Hook Form invalid-submit callback used by user-facing forms. */ export function showFormValidationToast(errors: FieldErrors) { addToast({ title: texts.common.formIncomplete, description: findFirstErrorMessage(errors) ?? texts.common.formValidationDefault, color: 'danger', }) }