refactor: memoize Zod schemas and internationalize form labels and messages in demo components

This commit is contained in:
Firman Ramdhani
2026-06-22 11:34:25 +07:00
parent 1ceceb6759
commit 4d2558cf71
5 changed files with 217 additions and 76 deletions
@@ -109,7 +109,7 @@ export default function AllFieldsDemo() {
<Stack gap="xl"> <Stack gap="xl">
{/* --- Text & Numbers --- */} {/* --- Text & Numbers --- */}
<div> <div>
<Title order={5} mb="sm" c="brand">Text & Numbers</Title> <Title order={5} mb="sm" c="brand">{t.sections.textAndNumbers}</Title>
<Divider mb="md" /> <Divider mb="md" />
<Group grow align="flex-start" mb="md"> <Group grow align="flex-start" mb="md">
<FieldTextInput name="customerName" control={control} label={t.fields.customerName} /> <FieldTextInput name="customerName" control={control} label={t.fields.customerName} />
@@ -131,7 +131,7 @@ export default function AllFieldsDemo() {
{/* --- Selections --- */} {/* --- Selections --- */}
<div> <div>
<Title order={5} mb="sm" c="brand">Selections</Title> <Title order={5} mb="sm" c="brand">{t.sections.selections}</Title>
<Divider mb="md" /> <Divider mb="md" />
<Group grow align="flex-start" mb="md"> <Group grow align="flex-start" mb="md">
<FieldSelect <FieldSelect
@@ -151,13 +151,13 @@ export default function AllFieldsDemo() {
<FieldAutocomplete <FieldAutocomplete
name="country" name="country"
control={control} control={control}
label="Country" label={t.fields.country}
data={['Indonesia', 'Singapore', 'Malaysia']} data={['Indonesia', 'Singapore', 'Malaysia']}
/> />
<FieldMultiSelect <FieldMultiSelect
name="categories" name="categories"
control={control} control={control}
label="Categories" label={t.fields.categories}
data={['Electronics', 'Fashion', 'Food']} data={['Electronics', 'Fashion', 'Food']}
/> />
</Group> </Group>
@@ -165,8 +165,8 @@ export default function AllFieldsDemo() {
<FieldLocalSelect <FieldLocalSelect
name="localSelect" name="localSelect"
control={control} control={control}
label="Local Select" label={t.fields.localSelect}
placeholder="Select a complex object" placeholder={t.placeholders.selectComplexObject}
options={[ options={[
{ id: 1, name: 'Apple', type: 'Fruit' }, { id: 1, name: 'Apple', type: 'Fruit' },
{ id: 2, name: 'Carrot', type: 'Vegetable' }, { id: 2, name: 'Carrot', type: 'Vegetable' },
@@ -180,8 +180,8 @@ export default function AllFieldsDemo() {
multiple multiple
name="multiLocalSelect" name="multiLocalSelect"
control={control} control={control}
label="Multi Local Select" label={t.fields.multiLocalSelect}
placeholder="Select multiple objects" placeholder={t.placeholders.selectMultipleObjects}
options={[ options={[
{ id: 1, name: 'Red', hex: '#f00' }, { id: 1, name: 'Red', hex: '#f00' },
{ id: 2, name: 'Green', hex: '#0f0' }, { id: 2, name: 'Green', hex: '#0f0' },
@@ -196,8 +196,8 @@ export default function AllFieldsDemo() {
<FieldAsyncSelect <FieldAsyncSelect
name="asyncSelect" name="asyncSelect"
control={control} control={control}
label="Async Select (Mock API)" label={t.fields.asyncSelectMock}
placeholder="Search pokemon..." placeholder={t.placeholders.searchPokemon}
loadOptions={loadMockPokemonOptions} loadOptions={loadMockPokemonOptions}
valueKey="id" valueKey="id"
labelKey="name" labelKey="name"
@@ -207,8 +207,8 @@ export default function AllFieldsDemo() {
multiple multiple
name="multiAsyncSelect" name="multiAsyncSelect"
control={control} control={control}
label="Multi Async Select" label={t.fields.multiAsyncSelect}
placeholder="Select multiple pokemon..." placeholder={t.placeholders.selectMultiplePokemon}
loadOptions={loadMockPokemonOptions} loadOptions={loadMockPokemonOptions}
valueKey="id" valueKey="id"
labelKey="name" labelKey="name"
@@ -219,8 +219,8 @@ export default function AllFieldsDemo() {
<FieldAsyncSelect <FieldAsyncSelect
name="realPokeSelect" name="realPokeSelect"
control={control} control={control}
label="Real PokeAPI (Single - Tests Deduplication)" label={t.fields.realPokeSingle}
placeholder="Scroll to test deduplication..." placeholder={t.placeholders.scrollDeduplication}
loadOptions={loadRealPokemonOptions} loadOptions={loadRealPokemonOptions}
valueKey="id" valueKey="id"
labelKey="name" labelKey="name"
@@ -230,8 +230,8 @@ export default function AllFieldsDemo() {
multiple multiple
name="multiRealPokeSelect" name="multiRealPokeSelect"
control={control} control={control}
label="Real PokeAPI (Multi - Tests Deduplication)" label={t.fields.realPokeMulti}
placeholder="Scroll to test deduplication..." placeholder={t.placeholders.scrollDeduplication}
loadOptions={loadRealPokemonOptions} loadOptions={loadRealPokemonOptions}
valueKey="id" valueKey="id"
labelKey="name" labelKey="name"
@@ -240,13 +240,13 @@ export default function AllFieldsDemo() {
</Group> </Group>
<FieldTagsInput name="tags" control={control} label={t.fields.tags} /> <FieldTagsInput name="tags" control={control} label={t.fields.tags} />
<Title order={5} mb="sm" mt="lg" c="brand">Advanced Object Selects (Custom Labels & Default Values)</Title> <Title order={5} mb="sm" mt="lg" c="brand">{t.sections.advancedObjectSelects}</Title>
<Divider mb="md" /> <Divider mb="md" />
<Group grow align="flex-start" mb="md"> <Group grow align="flex-start" mb="md">
<FieldLocalSelect <FieldLocalSelect
name="localSelectEmpty" name="localSelectEmpty"
control={control} control={control}
label="Local Empty" label={t.fields.localEmpty}
options={MOCK_VENDORS} options={MOCK_VENDORS}
valueKey="id" valueKey="id"
renderLabel={(item) => `[${item.code}] ${item.name}`} renderLabel={(item) => `[${item.code}] ${item.name}`}
@@ -255,7 +255,7 @@ export default function AllFieldsDemo() {
<FieldLocalSelect <FieldLocalSelect
name="localSelectPrefilled" name="localSelectPrefilled"
control={control} control={control}
label="Local Prefilled" label={t.fields.localPrefilled}
options={MOCK_VENDORS} options={MOCK_VENDORS}
valueKey="id" valueKey="id"
renderLabel={(item) => `[${item.code}] ${item.name}`} renderLabel={(item) => `[${item.code}] ${item.name}`}
@@ -266,7 +266,7 @@ export default function AllFieldsDemo() {
<FieldAsyncSelect <FieldAsyncSelect
name="asyncSelectEmpty" name="asyncSelectEmpty"
control={control} control={control}
label="Async Empty" label={t.fields.asyncEmpty}
loadOptions={loadMockVendorsOptions} loadOptions={loadMockVendorsOptions}
valueKey="id" valueKey="id"
renderLabel={(item) => `[${item.code}] ${item.name}`} renderLabel={(item) => `[${item.code}] ${item.name}`}
@@ -275,7 +275,7 @@ export default function AllFieldsDemo() {
<FieldAsyncSelect <FieldAsyncSelect
name="asyncSelectPrefilled" name="asyncSelectPrefilled"
control={control} control={control}
label="Async Prefilled (Edit Mode)" label={t.fields.asyncPrefilled}
loadOptions={loadMockVendorsOptions} loadOptions={loadMockVendorsOptions}
valueKey="id" valueKey="id"
renderLabel={(item) => `[${item.code}] ${item.name}`} renderLabel={(item) => `[${item.code}] ${item.name}`}
@@ -284,14 +284,14 @@ export default function AllFieldsDemo() {
/> />
</Group> </Group>
<Title order={5} mb="sm" mt="lg" c="brand">Multi-Select Edit Mode (No defaultOptions fallback)</Title> <Title order={5} mb="sm" mt="lg" c="brand">{t.sections.multiSelectEditMode}</Title>
<Divider mb="md" /> <Divider mb="md" />
<Group grow align="flex-start" mb="md"> <Group grow align="flex-start" mb="md">
<FieldLocalSelect <FieldLocalSelect
multiple multiple
name="localMultiPrefilled" name="localMultiPrefilled"
control={control} control={control}
label="Local Multi Prefilled" label={t.fields.localMultiPrefilled}
options={MOCK_VENDORS} options={MOCK_VENDORS}
valueKey="id" valueKey="id"
renderLabel={(item) => `[${item.code}] ${item.name}`} renderLabel={(item) => `[${item.code}] ${item.name}`}
@@ -301,7 +301,7 @@ export default function AllFieldsDemo() {
multiple multiple
name="asyncMultiPrefilled" name="asyncMultiPrefilled"
control={control} control={control}
label="Async Multi Prefilled (Ghost Items)" label={t.fields.asyncMultiPrefilled}
loadOptions={loadMockVendorsOptions} loadOptions={loadMockVendorsOptions}
valueKey="id" valueKey="id"
renderLabel={(item) => `[${item.code}] ${item.name}`} renderLabel={(item) => `[${item.code}] ${item.name}`}
@@ -309,27 +309,27 @@ export default function AllFieldsDemo() {
/> />
</Group> </Group>
<Title order={5} mb="sm" mt="lg" c="brand">Rich Text Editor (TipTap)</Title> <Title order={5} mb="sm" mt="lg" c="brand">{t.sections.richTextEditor}</Title>
<Divider mb="md" /> <Divider mb="md" />
<FieldRichTextEditor <FieldRichTextEditor
name="richTextEmpty" name="richTextEmpty"
control={control} control={control}
label="Rich Text (Empty)" label={t.fields.richTextEmpty}
description="A fresh TipTap editor instance" description={t.descriptions.freshTipTap}
/> />
<div style={{ marginTop: '16px' }}> <div style={{ marginTop: '16px' }}>
<FieldRichTextEditor <FieldRichTextEditor
name="richTextPrefilled" name="richTextPrefilled"
control={control} control={control}
label="Rich Text (Prefilled / Edit Mode)" label={t.fields.richTextPrefilled}
description="HTML string successfully loaded from default values" description={t.descriptions.htmlStringLoaded}
/> />
</div> </div>
</div> </div>
{/* --- Toggles & Choices --- */} {/* --- Toggles & Choices --- */}
<div> <div>
<Title order={5} mb="sm" c="brand">Toggles & Choices</Title> <Title order={5} mb="sm" c="brand">{t.sections.togglesAndChoices}</Title>
<Divider mb="md" /> <Divider mb="md" />
<Group mb="md"> <Group mb="md">
<FieldCheckbox name="terms" control={control} label={t.fields.terms} /> <FieldCheckbox name="terms" control={control} label={t.fields.terms} />
@@ -372,11 +372,11 @@ export default function AllFieldsDemo() {
{/* --- Ranges & Specialized --- */} {/* --- Ranges & Specialized --- */}
<div> <div>
<Title order={5} mb="sm" c="brand">Ranges & Specialized</Title> <Title order={5} mb="sm" c="brand">{t.sections.rangesAndSpecialized}</Title>
<Divider mb="md" /> <Divider mb="md" />
<Group grow align="flex-start" mb="md"> <Group grow align="flex-start" mb="md">
<FieldSlider name="satisfaction" control={control} label={t.fields.rating} /> <FieldSlider name="satisfaction" control={control} label={t.fields.rating} />
<FieldRangeSlider name="priceRange" control={control} label="Price Range" /> <FieldRangeSlider name="priceRange" control={control} label={t.fields.priceRange} />
</Group> </Group>
<Group grow align="flex-start" mb="md"> <Group grow align="flex-start" mb="md">
<FieldColorInput name="themeColor" control={control} label={t.fields.themeColor} /> <FieldColorInput name="themeColor" control={control} label={t.fields.themeColor} />
@@ -7,7 +7,7 @@ import { useConditionalField } from '@repo/ui/hooks';
import { compose, required, emailValidator } from '@repo/ui/validators'; import { compose, required, emailValidator } from '@repo/ui/validators';
import { useFormDemoTranslation } from '../i18n/useFormDemoTranslation'; import { useFormDemoTranslation } from '../i18n/useFormDemoTranslation';
import { Info } from 'lucide-react'; import { Info } from 'lucide-react';
import { useEffect, useCallback, useRef } from 'react'; import { useEffect, useCallback, useRef, useMemo } from 'react';
interface Region { interface Region {
id: string; id: string;
@@ -52,10 +52,10 @@ export default function ReactiveWatchDemo() {
// Define atomic validators for conditional fields // Define atomic validators for conditional fields
const taxIdValidator = compose(z.string(), required(t.watch.corporateTaxId)); const taxIdValidator = compose(z.string(), required(t.watch.corporateTaxId));
const spouseNameValidator = compose(z.string(), required(t.watch.spouseName)); const spouseNameValidator = compose(z.string(), required(t.watch.spouseName));
const newsletterEmailValidator = compose(z.string(), required('Newsletter Email'), emailValidator()); const newsletterEmailValidator = compose(z.string(), required(t.fields.email), emailValidator());
const roleValidator = compose(z.string(), required('Role')); const roleValidator = compose(z.string(), required(t.fields.role));
const reactiveSchema = z const reactiveSchema = useMemo(() => z
.object({ .object({
userType: z.enum(['PERSONAL', 'CORPORATE']), userType: z.enum(['PERSONAL', 'CORPORATE']),
corporateTaxId: z.string().optional(), corporateTaxId: z.string().optional(),
@@ -92,7 +92,7 @@ export default function ReactiveWatchDemo() {
z.object({ department: z.string().min(1), role: roleValidator }), z.object({ department: z.string().min(1), role: roleValidator }),
z.object({ department: z.union([z.literal(''), z.undefined(), z.null()]).optional() }), z.object({ department: z.union([z.literal(''), z.undefined(), z.null()]).optional() }),
]), ]),
); ), [t, taxIdValidator, spouseNameValidator, newsletterEmailValidator, roleValidator]);
const { control, handleSubmit, setValue, unregister, clearErrors } = useForm<any>({ const { control, handleSubmit, setValue, unregister, clearErrors } = useForm<any>({
resolver: zodResolver(reactiveSchema as any), resolver: zodResolver(reactiveSchema as any),
@@ -214,7 +214,7 @@ export default function ReactiveWatchDemo() {
<form onSubmit={handleSubmit(onSubmit)}> <form onSubmit={handleSubmit(onSubmit)}>
<Stack gap="md"> <Stack gap="md">
<Title order={5} mb="sm" c="brand"> <Title order={5} mb="sm" c="brand">
Dynamic Fields & Validation {t.sections.reactiveWatchCascading}
</Title> </Title>
<Divider mb="sm" /> <Divider mb="sm" />
@@ -264,8 +264,8 @@ export default function ReactiveWatchDemo() {
<FieldSelect <FieldSelect
name="department" name="department"
control={control} control={control}
label="Department" label={t.fields.department}
placeholder="Select a department" placeholder={t.placeholders.selectComplexObject}
data={[ data={[
{ value: 'IT', label: 'Information Technology' }, { value: 'IT', label: 'Information Technology' },
{ value: 'HR', label: 'Human Resources' }, { value: 'HR', label: 'Human Resources' },
@@ -286,15 +286,15 @@ export default function ReactiveWatchDemo() {
key={`role-select-${department}`} key={`role-select-${department}`}
name="role" name="role"
control={control} control={control}
label="Role" label={t.fields.role}
placeholder="Select a role" placeholder={t.placeholders.selectComplexObject}
disabled={!department} disabled={!department}
data={currentRoleOptions} data={currentRoleOptions}
withAsterisk={!!department} withAsterisk={!!department}
/> />
<Title order={5} mb="sm" c="brand" mt="lg"> <Title order={5} mb="sm" c="brand" mt="lg">
Cascading Object Selects {t.sections.reactiveWatchCascading}
</Title> </Title>
<Divider mb="sm" /> <Divider mb="sm" />
@@ -302,7 +302,7 @@ export default function ReactiveWatchDemo() {
multiple multiple
name="regions" name="regions"
control={control as any} control={control as any}
label="Regions" label={t.fields.regions}
options={REGIONS} options={REGIONS}
valueKey="id" valueKey="id"
labelKey="code" labelKey="code"
@@ -314,7 +314,7 @@ export default function ReactiveWatchDemo() {
key={`warehouse-select-${regions?.map((r: any) => r.id).join(',')}`} key={`warehouse-select-${regions?.map((r: any) => r.id).join(',')}`}
name="warehouses" name="warehouses"
control={control as any} control={control as any}
label="Warehouses" label={t.fields.warehouses}
disabled={!regions || regions.length === 0} disabled={!regions || regions.length === 0}
loadOptions={useCallback(async (search, page) => { loadOptions={useCallback(async (search, page) => {
if (!regions || regions.length === 0) return { options: [], hasMore: false }; if (!regions || regions.length === 0) return { options: [], hasMore: false };
@@ -327,31 +327,31 @@ export default function ReactiveWatchDemo() {
{regions && regions.length > 0 && ( {regions && regions.length > 0 && (
<Alert mt="sm" color="teal"> <Alert mt="sm" color="teal">
Selected regions tax rates: {regions.map((r: any) => `${r.code} (${(r.taxRate * 100).toFixed(0)}%)`).join(', ')} {t.descriptions.selectedRegionsTax} {regions.map((r: any) => `${r.code} (${(r.taxRate * 100).toFixed(0)}%)`).join(', ')}
</Alert> </Alert>
)} )}
<Title order={5} mb="sm" c="brand" mt="lg"> <Title order={5} mb="sm" c="brand" mt="lg">
Reactive Rich Text Preview {t.sections.reactiveRichTextPreview}
</Title> </Title>
<Divider mb="sm" /> <Divider mb="sm" />
<FieldRichTextEditor <FieldRichTextEditor
name="richTextLive" name="richTextLive"
control={control as any} control={control as any}
label="Live Editor" label={t.fields.liveEditor}
description="Type to see instantaneous reactive rendering below" description={t.descriptions.typeToSeePreview}
/> />
<Paper p="md" withBorder radius="md" mt="sm"> <Paper p="md" withBorder radius="md" mt="sm">
<Title order={6} mb="xs">Live HTML Preview Render</Title> <Title order={6} mb="xs">{t.sections.liveHtmlPreview}</Title>
<TypographyStylesProvider> <TypographyStylesProvider>
<div dangerouslySetInnerHTML={{ __html: watchedRichTextLive }} /> <div dangerouslySetInnerHTML={{ __html: watchedRichTextLive }} />
</TypographyStylesProvider> </TypographyStylesProvider>
</Paper> </Paper>
<Button type="submit" mt="md"> <Button type="submit" mt="md">
{t.common?.submit || 'Submit Reactive Form'} {t.common.submitReactive}
</Button> </Button>
</Stack> </Stack>
</form> </form>
@@ -1,3 +1,4 @@
import { useMemo } from 'react';
import { useForm } from 'react-hook-form'; import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod'; import { zodResolver } from '@hookform/resolvers/zod';
import { z } from 'zod'; import { z } from 'zod';
@@ -63,20 +64,20 @@ export default function ValidationBankDemo() {
const t = useFormDemoTranslation(); const t = useFormDemoTranslation();
// Compose the Zod schema using the atomic validators // Compose the Zod schema using the atomic validators
const validationSchema = z.object({ const validationSchema = useMemo(() => z.object({
username: compose(z.string(), required(t.fields.customerName), rangeLength(3, 15, t.fields.customerName)), username: compose(z.string(), required(t.fields.customerName), rangeLength(3, 15, t.fields.customerName)),
simplePass: compose(z.string(), required(t.validation.simplePassword), simplePassword(6)), simplePass: compose(z.string(), required(t.validation.simplePassword), simplePassword(6)),
complexPass: compose(z.string(), required(t.validation.complexPassword), complexPassword(8)), complexPass: compose(z.string(), required(t.validation.complexPassword), complexPassword(8)),
age: compose(z.number(), required(t.fields.age), rangeValue(18, 65, t.fields.age)), age: compose(z.number(), required(t.fields.age), rangeValue(18, 65, t.fields.age)),
score: compose(z.number(), required(t.validation.score), positiveNumber(t.validation.score)), score: compose(z.number(), required(t.validation.score), positiveNumber(t.validation.score)),
phone: compose(z.string(), required(t.validation.phone), phoneValidator()), phone: compose(z.string(), required(t.validation.phone), phoneValidator()),
department: z.object({ code: z.string(), name: z.string() }, { required_error: 'Department is required' }), department: z.object({ code: z.string(), name: z.string() }, { required_error: t.errors.departmentRequired }),
assignees: z.array(z.object({ id: z.number(), email: z.string() })).min(2, "Select at least 2 assignees"), assignees: z.array(z.object({ id: z.number(), email: z.string() })).min(2, t.errors.min2Assignees),
prefilledVendor: z.object({ id: z.union([z.string(), z.number()]), code: z.string(), name: z.string() }, { required_error: 'Vendor is required' }), prefilledVendor: z.object({ id: z.union([z.string(), z.number()]), code: z.string(), name: z.string() }, { required_error: t.errors.vendorRequired }),
emptyVendor: z.object({ id: z.union([z.string(), z.number()]), code: z.string(), name: z.string() }, { required_error: 'Vendor is required' }), emptyVendor: z.object({ id: z.union([z.string(), z.number()]), code: z.string(), name: z.string() }, { required_error: t.errors.vendorRequired }),
prefilledAsyncMulti: z.array(z.object({ id: z.union([z.string(), z.number()]), code: z.string(), name: z.string() })).min(1, "Select at least 1 vendor"), prefilledAsyncMulti: z.array(z.object({ id: z.union([z.string(), z.number()]), code: z.string(), name: z.string() })).min(1, t.errors.min1Vendor),
richTextNotes: z.string().min(15, "Notes must be at least 15 characters long (including HTML tags)"), richTextNotes: z.string().min(15, t.errors.notesMin15),
}); }), [t]);
type ValidationFormValues = z.infer<typeof validationSchema>; type ValidationFormValues = z.infer<typeof validationSchema>;
@@ -109,7 +110,7 @@ export default function ValidationBankDemo() {
<Paper p="xl" withBorder radius="md"> <Paper p="xl" withBorder radius="md">
<form onSubmit={handleSubmit(onSubmit)}> <form onSubmit={handleSubmit(onSubmit)}>
<Stack gap="md"> <Stack gap="md">
<Title order={5} c="brand">Validation Bank (Atomic Registry)</Title> <Title order={5} c="brand">{t.sections.validationBankTitle}</Title>
<Divider mb="sm" /> <Divider mb="sm" />
<FieldTextInput <FieldTextInput
@@ -125,14 +126,14 @@ export default function ValidationBankDemo() {
name="simplePass" name="simplePass"
control={control} control={control}
label={t.validation.simplePassword} label={t.validation.simplePassword}
description="Min 6 chars" description={t.descriptions.min6Chars}
withAsterisk withAsterisk
/> />
<FieldPasswordInput <FieldPasswordInput
name="complexPass" name="complexPass"
control={control} control={control}
label={t.validation.complexPassword} label={t.validation.complexPassword}
description="Min 8, 1 uppercase, 1 number, 1 special" description={t.descriptions.min8Complex}
withAsterisk withAsterisk
/> />
</Group> </Group>
@@ -149,7 +150,7 @@ export default function ValidationBankDemo() {
name="score" name="score"
control={control} control={control}
label={t.validation.score} label={t.validation.score}
description="Must be > 0" description={t.descriptions.mustBePositive}
withAsterisk withAsterisk
/> />
</Group> </Group>
@@ -158,17 +159,17 @@ export default function ValidationBankDemo() {
name="phone" name="phone"
control={control} control={control}
label={t.validation.phone} label={t.validation.phone}
description="Format: +62..." description={t.descriptions.formatPhone}
withAsterisk withAsterisk
/> />
<Title order={5} c="brand" mt="md">Object Level Validations (Local & Async)</Title> <Title order={5} c="brand" mt="md">{t.sections.objectLevelValidations}</Title>
<Divider mb="sm" /> <Divider mb="sm" />
<FieldLocalSelect<Department> <FieldLocalSelect<Department>
name="department" name="department"
control={control as any} control={control as any}
label="Department" label={t.fields.department}
options={MOCK_DEPARTMENTS} options={MOCK_DEPARTMENTS}
valueKey="code" valueKey="code"
renderLabel={(item) => `[${item.code}] ${item.name}`} renderLabel={(item) => `[${item.code}] ${item.name}`}
@@ -180,7 +181,7 @@ export default function ValidationBankDemo() {
multiple multiple
name="assignees" name="assignees"
control={control as any} control={control as any}
label="Assignees" label={t.fields.assignees}
loadOptions={mockFetchUsers} loadOptions={mockFetchUsers}
valueKey="id" valueKey="id"
labelKey="email" labelKey="email"
@@ -189,14 +190,14 @@ export default function ValidationBankDemo() {
withAsterisk withAsterisk
/> />
<Title order={5} c="brand" mt="lg">Validated Prefilled Objects</Title> <Title order={5} c="brand" mt="lg">{t.sections.validatedPrefilledObjects}</Title>
<Divider mb="sm" /> <Divider mb="sm" />
<Group grow align="flex-start"> <Group grow align="flex-start">
<FieldAsyncSelect <FieldAsyncSelect
name="emptyVendor" name="emptyVendor"
control={control as any} control={control as any}
label="Empty Vendor" label={t.fields.emptyVendor}
loadOptions={mockFetchVendors} loadOptions={mockFetchVendors}
valueKey="id" valueKey="id"
renderLabel={(item) => `[${item.code}] ${item.name}`} renderLabel={(item) => `[${item.code}] ${item.name}`}
@@ -206,7 +207,7 @@ export default function ValidationBankDemo() {
<FieldAsyncSelect <FieldAsyncSelect
name="prefilledVendor" name="prefilledVendor"
control={control as any} control={control as any}
label="Prefilled Vendor" label={t.fields.prefilledVendor}
loadOptions={mockFetchVendors} loadOptions={mockFetchVendors}
valueKey="id" valueKey="id"
renderLabel={(item) => `[${item.code}] ${item.name}`} renderLabel={(item) => `[${item.code}] ${item.name}`}
@@ -220,7 +221,7 @@ export default function ValidationBankDemo() {
multiple multiple
name="prefilledAsyncMulti" name="prefilledAsyncMulti"
control={control as any} control={control as any}
label="Prefilled Async Multi (No Fallback)" label={t.fields.prefilledAsyncMulti}
loadOptions={mockFetchVendors} loadOptions={mockFetchVendors}
valueKey="id" valueKey="id"
renderLabel={(item) => `[${item.code}] ${item.name}`} renderLabel={(item) => `[${item.code}] ${item.name}`}
@@ -228,14 +229,14 @@ export default function ValidationBankDemo() {
withAsterisk withAsterisk
/> />
<Title order={5} c="brand" mt="lg">Rich Text Editor Validations</Title> <Title order={5} c="brand" mt="lg">{t.sections.richTextValidations}</Title>
<Divider mb="sm" /> <Divider mb="sm" />
<FieldRichTextEditor <FieldRichTextEditor
name="richTextNotes" name="richTextNotes"
control={control as any} control={control as any}
label="Important Notes" label={t.fields.importantNotes}
description="This uses Zod minimum length string validation" description={t.descriptions.zodMinLengthString}
withAsterisk withAsterisk
/> />
@@ -6,9 +6,26 @@
}, },
"common": { "common": {
"submit": "Submit Data", "submit": "Submit Data",
"submitReactive": "Submit Reactive Form",
"reset": "Reset Form", "reset": "Reset Form",
"submittedData": "Submitted Data" "submittedData": "Submitted Data"
}, },
"sections": {
"validationBankTitle": "Validation Bank (Atomic Registry)",
"objectLevelValidations": "Object Level Validations (Local & Async)",
"validatedPrefilledObjects": "Validated Prefilled Objects",
"richTextValidations": "Rich Text Editor Validations",
"textAndNumbers": "Text & Numbers",
"selections": "Selections",
"advancedObjectSelects": "Advanced Object Selects (Custom Labels & Default Values)",
"multiSelectEditMode": "Multi-Select Edit Mode (No defaultOptions fallback)",
"richTextEditor": "Rich Text Editor (TipTap)",
"togglesAndChoices": "Toggles & Choices",
"rangesAndSpecialized": "Ranges & Specialized",
"reactiveWatchCascading": "Reactive Watch (Cascading)",
"reactiveRichTextPreview": "Reactive Rich Text Preview",
"liveHtmlPreview": "Live HTML Preview Render"
},
"fields": { "fields": {
"customerName": "Customer Name", "customerName": "Customer Name",
"email": "Email Address", "email": "Email Address",
@@ -26,7 +43,60 @@
"orderType": "Order Type", "orderType": "Order Type",
"quantity": "Quantity", "quantity": "Quantity",
"fabricColor": "Fabric Color", "fabricColor": "Fabric Color",
"pin": "Security PIN" "pin": "Security PIN",
"department": "Department",
"assignees": "Assignees",
"emptyVendor": "Empty Vendor",
"prefilledVendor": "Prefilled Vendor",
"prefilledAsyncMulti": "Prefilled Async Multi (No Fallback)",
"importantNotes": "Important Notes",
"country": "Country",
"categories": "Categories",
"localSelect": "Local Select",
"multiLocalSelect": "Multi Local Select",
"asyncSelectMock": "Async Select (Mock API)",
"multiAsyncSelect": "Multi Async Select",
"realPokeSingle": "Real PokeAPI (Single - Tests Deduplication)",
"realPokeMulti": "Real PokeAPI (Multi - Tests Deduplication)",
"localEmpty": "Local Empty",
"localPrefilled": "Local Prefilled",
"asyncEmpty": "Async Empty",
"asyncPrefilled": "Async Prefilled (Edit Mode)",
"localMultiPrefilled": "Local Multi Prefilled",
"asyncMultiPrefilled": "Async Multi Prefilled (Ghost Items)",
"richTextEmpty": "Rich Text (Empty)",
"richTextPrefilled": "Rich Text (Prefilled / Edit Mode)",
"priceRange": "Price Range",
"role": "Role",
"regions": "Regions",
"warehouses": "Warehouses",
"liveEditor": "Live Editor"
},
"placeholders": {
"selectComplexObject": "Select a complex object",
"selectMultipleObjects": "Select multiple objects",
"searchPokemon": "Search pokemon...",
"selectMultiplePokemon": "Select multiple pokemon...",
"scrollDeduplication": "Scroll to test deduplication..."
},
"descriptions": {
"min6Chars": "Min 6 chars",
"min8Complex": "Min 8, 1 uppercase, 1 number, 1 special",
"mustBePositive": "Must be > 0",
"formatPhone": "Format: +62...",
"zodMinLengthString": "This uses Zod minimum length string validation",
"freshTipTap": "A fresh TipTap editor instance",
"htmlStringLoaded": "HTML string successfully loaded from default values",
"typeToSeePreview": "Type to see instantaneous reactive rendering below",
"selectedRegionsTax": "Selected regions tax rates:"
},
"errors": {
"departmentRequired": "Department is required",
"vendorRequired": "Vendor is required",
"min2Assignees": "Select at least 2 assignees",
"min1Vendor": "Select at least 1 vendor",
"notesMin15": "Notes must be at least 15 characters long (including HTML tags)",
"selectRegionFirst": "Select a region first to load warehouses"
}, },
"validation": { "validation": {
"simplePassword": "Simple Password", "simplePassword": "Simple Password",
@@ -6,9 +6,26 @@
}, },
"common": { "common": {
"submit": "Kirim Data", "submit": "Kirim Data",
"submitReactive": "Kirim Form Reaktif",
"reset": "Reset Form", "reset": "Reset Form",
"submittedData": "Data Terkirim" "submittedData": "Data Terkirim"
}, },
"sections": {
"validationBankTitle": "Bank Validasi (Registri Atomik)",
"objectLevelValidations": "Validasi Tingkat Objek (Lokal & Async)",
"validatedPrefilledObjects": "Objek Terisi yang Divalidasi",
"richTextValidations": "Validasi Rich Text Editor",
"textAndNumbers": "Teks & Angka",
"selections": "Pilihan",
"advancedObjectSelects": "Pemilihan Objek Tingkat Lanjut (Label Kustom & Nilai Default)",
"multiSelectEditMode": "Mode Edit Multi-Select (Tanpa fallback defaultOptions)",
"richTextEditor": "Rich Text Editor (TipTap)",
"togglesAndChoices": "Tombol Sakelar & Pilihan",
"rangesAndSpecialized": "Rentang & Khusus",
"reactiveWatchCascading": "Reactive Watch (Berjenjang)",
"reactiveRichTextPreview": "Pratinjau Rich Text Reaktif",
"liveHtmlPreview": "Render Pratinjau HTML Langsung"
},
"fields": { "fields": {
"customerName": "Nama Pelanggan", "customerName": "Nama Pelanggan",
"email": "Alamat Email", "email": "Alamat Email",
@@ -26,7 +43,60 @@
"orderType": "Tipe Pesanan", "orderType": "Tipe Pesanan",
"quantity": "Jumlah", "quantity": "Jumlah",
"fabricColor": "Warna Kain", "fabricColor": "Warna Kain",
"pin": "PIN Keamanan" "pin": "PIN Keamanan",
"department": "Departemen",
"assignees": "Penerima Tugas",
"emptyVendor": "Vendor Kosong",
"prefilledVendor": "Vendor Terisi",
"prefilledAsyncMulti": "Multi Async Terisi (Tanpa Fallback)",
"importantNotes": "Catatan Penting",
"country": "Negara",
"categories": "Kategori",
"localSelect": "Pilihan Lokal",
"multiLocalSelect": "Pilihan Lokal Multi",
"asyncSelectMock": "Pilihan Async (Mock API)",
"multiAsyncSelect": "Pilihan Async Multi",
"realPokeSingle": "API Pokemon Asli (Tunggal - Uji Deduplikasi)",
"realPokeMulti": "API Pokemon Asli (Multi - Uji Deduplikasi)",
"localEmpty": "Lokal Kosong",
"localPrefilled": "Lokal Terisi",
"asyncEmpty": "Async Kosong",
"asyncPrefilled": "Async Terisi (Mode Edit)",
"localMultiPrefilled": "Multi Lokal Terisi",
"asyncMultiPrefilled": "Multi Async Terisi (Item Hantu)",
"richTextEmpty": "Rich Text (Kosong)",
"richTextPrefilled": "Rich Text (Terisi / Mode Edit)",
"priceRange": "Rentang Harga",
"role": "Peran",
"regions": "Wilayah",
"warehouses": "Gudang",
"liveEditor": "Editor Langsung"
},
"placeholders": {
"selectComplexObject": "Pilih objek yang kompleks",
"selectMultipleObjects": "Pilih beberapa objek",
"searchPokemon": "Cari pokemon...",
"selectMultiplePokemon": "Pilih beberapa pokemon...",
"scrollDeduplication": "Gulir untuk menguji deduplikasi..."
},
"descriptions": {
"min6Chars": "Minimal 6 karakter",
"min8Complex": "Min 8, 1 huruf besar, 1 angka, 1 karakter khusus",
"mustBePositive": "Harus > 0",
"formatPhone": "Format: +62...",
"zodMinLengthString": "Ini menggunakan validasi panjang string minimum Zod",
"freshTipTap": "Instance editor TipTap yang baru",
"htmlStringLoaded": "String HTML berhasil dimuat dari nilai default",
"typeToSeePreview": "Ketik untuk melihat render reaktif seketika di bawah",
"selectedRegionsTax": "Tarif pajak wilayah yang dipilih:"
},
"errors": {
"departmentRequired": "Departemen wajib diisi",
"vendorRequired": "Vendor wajib diisi",
"min2Assignees": "Pilih minimal 2 penerima tugas",
"min1Vendor": "Pilih minimal 1 vendor",
"notesMin15": "Catatan minimal harus terdiri dari 15 karakter (termasuk tag HTML)",
"selectRegionFirst": "Pilih wilayah terlebih dahulu untuk memuat gudang"
}, },
"validation": { "validation": {
"simplePassword": "Sandi Sederhana", "simplePassword": "Sandi Sederhana",