admin/components/forms/SeoCharCounterHint.tsx
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

38 lines
1.6 KiB
TypeScript

import type { FieldValues, Path } from 'react-hook-form'
import { useFormContext } from 'react-hook-form'
import { texts, format } from '@/texts'
interface SeoCharCounterHintProps<T extends FieldValues> {
name: Path<T>
max: number
min?: number
}
/**
* Live `n/max` counter for an SEO/char-limited text field, rendered as the
* field's `description` — shared across the admin CRUD form modals
* (tags, event categories, blog articles) so the display logic and its
* `max`/`min` thresholds live in one place instead of being hand-defined
* per modal. `max`/`min` should always be passed from the exported
* constants next to the corresponding zod schema, not bare literals.
*/
export function SeoCharCounterHint<T extends FieldValues>({ name, max, min }: SeoCharCounterHintProps<T>) {
const { watch } = useFormContext<T>()
// `watch(name)`'s return type doesn't fully resolve for a naked generic
// `T extends FieldValues` (react-hook-form's conditional path-value type
// needs a concrete field-map to compute against) — widen explicitly to
// `unknown` and narrow via `typeof` rather than let it flow as `any`.
const value: unknown = watch(name)
const length = typeof value === 'string' ? value.length : 0
const tooShort = min !== undefined && length > 0 && length < min
return (
<span className={length > max || tooShort ? 'text-fourth' : undefined}>
{`${length.toLocaleString('fa-IR')}/${max.toLocaleString('fa-IR')}`}
{min !== undefined ? format(texts.common.seoCharMinHint, { min: min.toLocaleString('fa-IR') }) : ''}
</span>
)
}