feat: enhance error handling in forms by introducing FieldError component for consistent error display

This commit is contained in:
Yoga Pangestu 2026-04-22 22:07:58 +07:00
parent beafccf974
commit 703ceefddf
21 changed files with 186 additions and 100 deletions

View File

@ -1,17 +1,64 @@
import type { HTMLAttributes } from 'react';
import { cn } from '@/lib/utils';
import { useMemo } from 'react';
interface InputErrorProps extends HTMLAttributes<HTMLParagraphElement> {
message?: string;
label?: string;
}
export default function InputError({
message,
label,
className = '',
...props
}: HTMLAttributes<HTMLParagraphElement> & { message?: string }) {
return message ? (
}: InputErrorProps) {
const formattedError = useMemo(() => {
if (!message) return null;
if (!label) return message;
// Preserve all-caps labels (like NIK), otherwise capitalize first letter and lowercase the rest
const isAllOptionsCaps = label === label.toUpperCase() && label.length > 1;
const formattedLabel = isAllOptionsCaps
? label
: label.charAt(0).toUpperCase() + label.slice(1).toLowerCase();
const words = message.split(' ');
// List of common Indonesian and English validation verbs/connectors that follow the attribute
const verbs = [
'wajib', 'harus', 'berupa', 'adalah', 'minimal', 'maksimal', 'tidak', 'kurang', 'lebih', 'antara', 'sudah',
'is', 'must', 'field', 'has', 'was', 'should', 'cannot', 'required', 'invalid'
];
// Find the first occurrence of a verb
let verbIndex = -1;
for (let i = 0; i < words.length; i++) {
if (verbs.includes(words[i].toLowerCase())) {
verbIndex = i;
break;
}
}
if (verbIndex !== -1) {
// Replace everything before the verb with the label
return `${formattedLabel} ${words.slice(verbIndex).join(' ')}`;
}
// Fallback to replacing only the first word if no verb found
if (words.length > 0) {
return `${formattedLabel} ${words.slice(1).join(' ')}`;
}
return message;
}, [message, label]);
return formattedError ? (
<p
{...props}
className={cn('text-sm text-red-600 dark:text-red-400', className)}
>
{message}
{formattedError}
</p>
) : null;
}

View File

@ -184,37 +184,59 @@ function FieldSeparator({
function FieldError({
className,
children,
errors,
error,
label,
...props
}: React.ComponentProps<"div"> & {
errors?: Array<{ message?: string } | undefined>
error?: string | string[]
label?: string
}) {
const content = useMemo(() => {
if (children) {
return children
}
if (!errors?.length) {
const rawError = Array.isArray(error) ? error[0] : error
if (!rawError) {
return null
}
const uniqueErrors = [
...new Map(errors.map((error) => [error?.message, error])).values(),
]
if (label) {
const isAllUppercase = label === label.toUpperCase() && label.length > 1
const formattedLabel = isAllUppercase
? label
: label.charAt(0).toUpperCase() + label.slice(1).toLowerCase()
const words = rawError.split(" ")
// List of common Indonesian and English validation verbs/connectors that follow the attribute
const verbs = [
"wajib", "harus", "berupa", "adalah", "minimal", "maksimal", "tidak", "kurang", "lebih", "antara", "sudah",
"is", "must", "field", "has", "was", "should", "cannot", "required", "invalid"
]
// Find the first occurrence of a verb
let verbIndex = -1
for (let i = 0; i < words.length; i++) {
if (verbs.includes(words[i].toLowerCase())) {
verbIndex = i
break
}
}
if (verbIndex !== -1) {
// Replace everything before the verb with the label
// If the first word was "The" or "Isian", we replace them too
return `${formattedLabel} ${words.slice(verbIndex).join(" ")}`
}
if (uniqueErrors?.length == 1) {
return uniqueErrors[0]?.message
// Fallback to replacing only the first word if no verb found
return `${formattedLabel} ${words.slice(1).join(" ")}`
}
return (
<ul className="ml-4 flex list-disc flex-col gap-1">
{uniqueErrors.map(
(error, index) =>
error?.message && <li key={index}>{error.message}</li>
)}
</ul>
)
}, [children, errors])
return rawError
}, [children, error, label])
if (!content) {
return null

View File

@ -6,7 +6,7 @@ import {
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog"
import { Field, FieldGroup } from "@/components/ui/field"
import { Field, FieldError, FieldGroup } from "@/components/ui/field"
import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"
import { Expense } from '@/types';
@ -122,7 +122,7 @@ export function ExpenseFormModal({ isOpen, onClose, expense }: ExpenseFormModalP
placeholder='Contoh: Bayar Listrik'
maxLength={100}
/>
{errors.name && <p className="text-xs text-red-500">{errors.name}</p>}
<FieldError error={errors.name} label="Nama" className="text-xs" />
</Field>
<Field>
@ -140,7 +140,7 @@ export function ExpenseFormModal({ isOpen, onClose, expense }: ExpenseFormModalP
placeholder="Rp 0"
autoComplete='off'
/>
{errors.amount && <p className="text-xs text-red-500">{errors.amount}</p>}
<FieldError error={errors.amount} label="Nominal" className="text-xs" />
</Field>
<Field>
@ -179,7 +179,7 @@ export function ExpenseFormModal({ isOpen, onClose, expense }: ExpenseFormModalP
onChange={onImageChange}
/>
</div>
{errors.image && <p className="text-xs text-red-500 mt-1">{errors.image}</p>}
<FieldError error={errors.image} label="Bukti" className="text-xs mt-1" />
</Field>
</FieldGroup>
<DialogFooter className="mt-6">

View File

@ -1,7 +1,7 @@
import { Head, Link, useForm, router } from '@inertiajs/react';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { Field } from "@/components/ui/field";
import { Field, FieldError } from "@/components/ui/field";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import * as orderRoutes from '@/routes/order';
@ -268,7 +268,7 @@ export default function OrderCreate({ products, cartItems, orderStatus, orderCha
<Field>
<Label htmlFor='customer_name' className="text-xs text-muted-foreground" required>Pelanggan</Label>
<Input id="customer_name" className="h-8 text-xs" value={data.customer_name} onChange={e => setData('customer_name', e.target.value)} placeholder="Nama Pelanggan" />
{errors.customer_name && <p className="text-xs text-red-500">{errors.customer_name}</p>}
<FieldError error={errors.customer_name} label="Pelanggan" className="text-xs" />
</Field>
<Field>
<Label className="text-xs text-muted-foreground" required>Channel</Label>
@ -282,7 +282,7 @@ export default function OrderCreate({ products, cartItems, orderStatus, orderCha
))}
</SelectContent>
</Select>
{errors.order_channel && <p className="text-xs text-red-500">{errors.order_channel}</p>}
<FieldError error={errors.order_channel} label="Channel" className="text-xs" />
</Field>
</div>
@ -299,7 +299,7 @@ export default function OrderCreate({ products, cartItems, orderStatus, orderCha
))}
</SelectContent>
</Select>
{errors.payment_method && <p className="text-xs text-red-500">{errors.payment_method}</p>}
<FieldError error={errors.payment_method} label="Metode Bayar" className="text-xs" />
</Field>
<Field>
<Label className="text-xs text-muted-foreground" required>Status</Label>
@ -313,7 +313,7 @@ export default function OrderCreate({ products, cartItems, orderStatus, orderCha
))}
</SelectContent>
</Select>
{errors.order_status && <p className="text-xs text-red-500">{errors.order_status}</p>}
<FieldError error={errors.order_status} label="Status" className="text-xs" />
</Field>
</div>
@ -338,7 +338,7 @@ export default function OrderCreate({ products, cartItems, orderStatus, orderCha
placeholder="Rp 0"
autoComplete='off'
/>
{errors.discount && <p className="text-xs text-red-500">{errors.discount}</p>}
<FieldError error={errors.discount} label="Potongan / Diskon" className="text-xs" />
</div>
</div>

View File

@ -1,7 +1,7 @@
import { Head, Link, useForm, router } from '@inertiajs/react';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { Field } from "@/components/ui/field";
import { Field, FieldError } from "@/components/ui/field";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import * as orderRoutes from '@/routes/order';
@ -270,7 +270,7 @@ export default function OrderEdit({ order, products, orderStatus, orderChannels,
<Field>
<Label htmlFor='customer_name' className="text-xs text-muted-foreground" required>Pelanggan</Label>
<Input id='customer_name' className="h-8 text-xs" value={data.customer_name} onChange={e => setData('customer_name', e.target.value)} />
{errors.customer_name && <p className="text-xs text-red-500">{errors.customer_name}</p>}
<FieldError error={errors.customer_name} label="Pelanggan" className="text-xs" />
</Field>
<Field>
<Label className="text-xs text-muted-foreground" required>Channel</Label>
@ -282,7 +282,7 @@ export default function OrderEdit({ order, products, orderStatus, orderChannels,
))}
</SelectContent>
</Select>
{errors.order_channel && <p className="text-xs text-red-500">{errors.order_channel}</p>}
<FieldError error={errors.order_channel} label="Channel" className="text-xs" />
</Field>
</div>
@ -297,7 +297,7 @@ export default function OrderEdit({ order, products, orderStatus, orderChannels,
))}
</SelectContent>
</Select>
{errors.payment_method && <p className="text-xs text-red-500">{errors.payment_method}</p>}
<FieldError error={errors.payment_method} label="Metode Bayar" className="text-xs" />
</Field>
<Field>
<Label className="text-xs text-muted-foreground" required>Status</Label>
@ -309,7 +309,7 @@ export default function OrderEdit({ order, products, orderStatus, orderChannels,
))}
</SelectContent>
</Select>
{errors.order_status && <p className="text-xs text-red-500">{errors.order_status}</p>}
<FieldError error={errors.order_status} label="Status" className="text-xs" />
</Field>
</div>
@ -335,7 +335,7 @@ export default function OrderEdit({ order, products, orderStatus, orderChannels,
autoComplete='off'
/>
</div>
{errors.discount && <p className="text-xs text-red-500">{errors.discount}</p>}
<FieldError error={errors.discount} label="Potongan / Diskon" className="text-xs" />
</div>
</div>

View File

@ -1,7 +1,7 @@
import { Head, Link, useForm, router } from '@inertiajs/react';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { Field } from "@/components/ui/field";
import { Field, FieldError } from "@/components/ui/field";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import purchaseRoutes from '@/routes/purchase';
@ -272,11 +272,13 @@ export default function PurchaseCreate({ products, cartItems }: { products: Prod
/>
</PopoverContent>
</Popover>
<FieldError error={errors.purchase_date} label="Tanggal" className="text-xs" />
</Field>
<Field>
<Label htmlFor='note' className="text-xs text-muted-foreground">Catatan</Label>
<Input id='note' className="h-8 text-xs" value={data.note} onChange={e => setData('note', e.target.value)} placeholder="...." />
<FieldError error={errors.note} label="Catatan" className="text-xs" />
</Field>
<div className="rounded-xl bg-primary/10 border border-primary/20 p-3 flex items-center justify-between">
@ -577,6 +579,7 @@ export default function PurchaseCreate({ products, cartItems }: { products: Prod
<Field>
<Label htmlFor="note" className="text-xs text-muted-foreground">Catatan</Label>
<Input id="note" className="h-8 text-xs" value={data.note} onChange={e => setData('note', e.target.value)} placeholder="...." />
<FieldError error={errors.note} label="Catatan" className="text-xs" />
</Field>
<div className="rounded-xl bg-primary/10 border border-primary/20 p-3 flex items-center justify-between">
@ -752,6 +755,7 @@ export default function PurchaseCreate({ products, cartItems }: { products: Prod
<Field>
<Label htmlFor="note_mobile" className="text-xs text-muted-foreground">Catatan</Label>
<Input id="note_mobile" className="h-8 text-xs" value={data.note} onChange={e => setData('note', e.target.value)} placeholder="...." />
<FieldError error={errors.note} label="Catatan" className="text-xs" />
</Field>
<div className="rounded-xl bg-primary/10 border border-primary/20 p-3 flex items-center justify-between">

View File

@ -1,7 +1,7 @@
import { Head, Link, useForm, router } from '@inertiajs/react';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { Field } from "@/components/ui/field";
import { Field, FieldError } from "@/components/ui/field";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import purchaseRoutes from '@/routes/purchase';
@ -276,11 +276,13 @@ export default function PurchaseEdit({ purchase, products }: { purchase: Purchas
/>
</PopoverContent>
</Popover>
<FieldError error={errors.purchase_date} label="Tanggal" className="text-xs" />
</Field>
<Field>
<Label htmlFor='note' className="text-xs text-muted-foreground">Catatan</Label>
<Input id='note' className="h-8 text-xs" value={data.note} onChange={e => setData('note', e.target.value)} placeholder="...." />
<FieldError error={errors.note} label="Catatan" className="text-xs" />
</Field>
<div className="rounded-xl bg-primary/10 border border-primary/20 p-3 flex items-center justify-between">
@ -576,11 +578,13 @@ export default function PurchaseEdit({ purchase, products }: { purchase: Purchas
/>
</PopoverContent>
</Popover>
<FieldError error={errors.purchase_date} label="Tanggal" className="text-xs" />
</Field>
<Field>
<Label htmlFor="note" className="text-xs text-muted-foreground">Catatan</Label>
<Input id="note" className="h-8 text-xs" value={data.note} onChange={e => setData('note', e.target.value)} placeholder="...." />
<FieldError error={errors.note} label="Catatan" className="text-xs" />
</Field>
<div className="rounded-xl bg-primary/10 border border-primary/20 p-3 flex items-center justify-between">
@ -756,6 +760,7 @@ export default function PurchaseEdit({ purchase, products }: { purchase: Purchas
<Field>
<Label htmlFor="note_mobile" className="text-xs text-muted-foreground">Catatan</Label>
<Input id="note_mobile" className="h-8 text-xs" value={data.note} onChange={e => setData('note', e.target.value)} placeholder="...." />
<FieldError error={errors.note} label="Catatan" className="text-xs" />
</Field>
<div className="rounded-xl bg-primary/10 border border-primary/20 p-3 flex items-center justify-between">

View File

@ -6,7 +6,7 @@ import {
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog"
import { Field, FieldGroup } from "@/components/ui/field"
import { Field, FieldError, FieldGroup } from "@/components/ui/field"
import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"
import { Category } from '@/types';
@ -85,7 +85,7 @@ export function CategoryFormModal({ isOpen, onClose, category }: CategoryFormMod
placeholder='Contoh: Gamis'
maxLength={50}
/>
{errors.name && <p className="text-xs text-red-500">{errors.name}</p>}
<FieldError error={errors.name} label="Nama" className="text-xs" />
</Field>
</FieldGroup>
<DialogFooter className="mt-6">

View File

@ -2,7 +2,7 @@ import { Head, Link } from '@inertiajs/react';
import { useForm } from '@inertiajs/react';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { Field } from "@/components/ui/field"
import { Field, FieldError } from "@/components/ui/field"
import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"
import productRoutes from '@/routes/product';
@ -143,7 +143,7 @@ export default function ProductCreate({ categories }: { categories: Category[] }
placeholder='Contoh: Gamis Wanita'
maxLength={100}
/>
{errors.name && <p className="text-xs text-red-500">{errors.name}</p>}
<FieldError error={errors.name} label="Nama" className="text-xs" />
</Field>
<Field>
@ -185,7 +185,7 @@ export default function ProductCreate({ categories }: { categories: Category[] }
</ComboboxList>
</ComboboxContent>
</Combobox>
{errors.category_ids && <p className="text-xs text-red-500 mt-1">{errors.category_ids}</p>}
<FieldError error={errors.category_ids} label="Kategori" className="text-xs mt-1" />
</Field>
<Field>
@ -194,7 +194,7 @@ export default function ProductCreate({ categories }: { categories: Category[] }
value={data.description}
onChange={(val) => setData('description', val)}
/>
{errors.description && <p className="text-xs text-red-500 mt-1">{errors.description}</p>}
<FieldError error={errors.description} label="Deskripsi" className="text-xs mt-1" />
</Field>
</CardContent>
</Card>
@ -223,7 +223,7 @@ export default function ProductCreate({ categories }: { categories: Category[] }
placeholder="Rp 0"
autoComplete='off'
/>
{errors['prices.purchase'] && <p className="text-xs text-red-500">{errors['prices.purchase']}</p>}
<FieldError error={errors['prices.purchase']} label="Harga Beli" className="text-xs" />
</Field>
<Field>
@ -244,7 +244,7 @@ export default function ProductCreate({ categories }: { categories: Category[] }
placeholder="Rp 0"
autoComplete='off'
/>
{errors['prices.distributor'] && <p className="text-xs text-red-500">{errors['prices.distributor']}</p>}
<FieldError error={errors['prices.distributor']} label="Harga Distributor" className="text-xs" />
</Field>
<Field>
@ -265,7 +265,7 @@ export default function ProductCreate({ categories }: { categories: Category[] }
placeholder="Rp 0"
autoComplete='off'
/>
{errors['prices.agent'] && <p className="text-xs text-red-500">{errors['prices.agent']}</p>}
<FieldError error={errors['prices.agent']} label="Harga Agen" className="text-xs" />
</Field>
<Field>
@ -286,7 +286,7 @@ export default function ProductCreate({ categories }: { categories: Category[] }
placeholder="Rp 0"
autoComplete='off'
/>
{errors['prices.reseller'] && <p className="text-xs text-red-500">{errors['prices.reseller']}</p>}
<FieldError error={errors['prices.reseller']} label="Harga Reseller" className="text-xs" />
</Field>
<Field className="md:col-span-2">
@ -307,7 +307,7 @@ export default function ProductCreate({ categories }: { categories: Category[] }
placeholder="Rp 0"
autoComplete='off'
/>
{errors['prices.retail'] && <p className="text-xs text-red-500">{errors['prices.retail']}</p>}
<FieldError error={errors['prices.retail']} label="Harga Retail" className="text-xs" />
</Field>
</div>
</CardContent>
@ -353,7 +353,7 @@ export default function ProductCreate({ categories }: { categories: Category[] }
className="hidden"
onChange={handleThumbnailChange}
/>
{errors.thumbnail && <p className="text-xs text-red-500 mt-2">{errors.thumbnail}</p>}
<FieldError error={errors.thumbnail} label="Thumbnail" className="text-xs mt-2" />
</CardContent>
</Card>
@ -397,7 +397,7 @@ export default function ProductCreate({ categories }: { categories: Category[] }
className="hidden"
onChange={handleImagesChange}
/>
{errors['images'] && <p className="text-xs text-red-500">{errors['images']}</p>}
<FieldError error={errors['images']} label="Galeri Gambar" className="text-xs" />
</CardContent>
</Card>

View File

@ -3,7 +3,7 @@ import type { Product, ProductPrice } from '@/types';
import { Category } from '@/types/category';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { Field } from "@/components/ui/field"
import { Field, FieldError } from "@/components/ui/field"
import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"
import { toast } from 'sonner';
@ -194,7 +194,7 @@ export default function ProductEdit({ product, categories }: { product: Product,
placeholder='Contoh: Gamis Wanita'
maxLength={100}
/>
{errors.name && <p className="text-xs text-red-500">{errors.name}</p>}
<FieldError error={errors.name} label="Nama" className="text-xs" />
</Field>
<Field>
@ -209,7 +209,7 @@ export default function ProductEdit({ product, categories }: { product: Product,
placeholder="0"
autoComplete='off'
/>
{errors.stock && <p className="text-xs text-red-500">{errors.stock}</p>}
<FieldError error={errors.stock} label="Stok" className="text-xs" />
</Field>
</div>
@ -252,7 +252,7 @@ export default function ProductEdit({ product, categories }: { product: Product,
</ComboboxList>
</ComboboxContent>
</Combobox>
{errors.category_ids && <p className="text-xs text-red-500 mt-1">{errors.category_ids}</p>}
<FieldError error={errors.category_ids} label="Kategori" className="text-xs mt-1" />
</Field>
<Field>
@ -261,7 +261,7 @@ export default function ProductEdit({ product, categories }: { product: Product,
value={data.description}
onChange={(val) => setData('description', val)}
/>
{errors.description && <p className="text-xs text-red-500 mt-1">{errors.description}</p>}
<FieldError error={errors.description} label="Deskripsi" className="text-xs mt-1" />
</Field>
</CardContent>
</Card>
@ -290,7 +290,7 @@ export default function ProductEdit({ product, categories }: { product: Product,
placeholder="Rp 0"
autoComplete='off'
/>
{errors['prices.purchase'] && <p className="text-xs text-red-500">{errors['prices.purchase']}</p>}
<FieldError error={errors['prices.purchase']} label="Harga Beli" className="text-xs" />
</Field>
<Field>
@ -311,7 +311,7 @@ export default function ProductEdit({ product, categories }: { product: Product,
placeholder="Rp 0"
autoComplete='off'
/>
{errors['prices.distributor'] && <p className="text-xs text-red-500">{errors['prices.distributor']}</p>}
<FieldError error={errors['prices.distributor']} label="Harga Distributor" className="text-xs" />
</Field>
<Field>
@ -332,7 +332,7 @@ export default function ProductEdit({ product, categories }: { product: Product,
placeholder="Rp 0"
autoComplete='off'
/>
{errors['prices.agent'] && <p className="text-xs text-red-500">{errors['prices.agent']}</p>}
<FieldError error={errors['prices.agent']} label="Harga Agen" className="text-xs" />
</Field>
<Field>
@ -353,7 +353,7 @@ export default function ProductEdit({ product, categories }: { product: Product,
placeholder="Rp 0"
autoComplete='off'
/>
{errors['prices.reseller'] && <p className="text-xs text-red-500">{errors['prices.reseller']}</p>}
<FieldError error={errors['prices.reseller']} label="Harga Reseller" className="text-xs" />
</Field>
<Field className="md:col-span-2">
@ -374,7 +374,7 @@ export default function ProductEdit({ product, categories }: { product: Product,
placeholder="Rp 0"
autoComplete='off'
/>
{errors['prices.retail'] && <p className="text-xs text-red-500">{errors['prices.retail']}</p>}
<FieldError error={errors['prices.retail']} label="Harga Retail" className="text-xs" />
</Field>
</div>
</CardContent>
@ -420,7 +420,7 @@ export default function ProductEdit({ product, categories }: { product: Product,
className="hidden"
onChange={handleThumbnailChange}
/>
{errors.thumbnail && <p className="text-xs text-red-500 mt-2">{errors.thumbnail}</p>}
<FieldError error={errors.thumbnail} label="Thumbnail" className="text-xs mt-2" />
</CardContent>
</Card>
@ -464,7 +464,7 @@ export default function ProductEdit({ product, categories }: { product: Product,
className="hidden"
onChange={handleImagesChange}
/>
{errors['images'] && <p className="text-xs text-red-500">{errors['images']}</p>}
<FieldError error={errors['images']} label="Galeri Gambar" className="text-xs" />
</CardContent>
</Card>

View File

@ -2,7 +2,7 @@ import { Head, Link } from '@inertiajs/react';
import { useForm } from '@inertiajs/react';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { Field } from "@/components/ui/field"
import { Field, FieldError } from "@/components/ui/field"
import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"
import userRoutes from '@/routes/user';
@ -77,7 +77,7 @@ export default function UserCreate() {
autoComplete='off'
placeholder='Contoh: John Doe'
/>
{errors.full_name && <p className="text-xs text-red-500">{errors.full_name}</p>}
<FieldError error={errors.full_name} label="Nama Lengkap" className="text-xs" />
</Field>
<Field>
@ -91,7 +91,7 @@ export default function UserCreate() {
placeholder='Contoh: 3213051307900001'
maxLength={16}
/>
{errors.nik && <p className="text-xs text-red-500">{errors.nik}</p>}
<FieldError error={errors.nik} label="NIK" className="text-xs" />
</Field>
<Field>
@ -104,7 +104,7 @@ export default function UserCreate() {
autoComplete='off'
placeholder='Contoh: 08123456789'
/>
{errors.phone_number && <p className="text-xs text-red-500">{errors.phone_number}</p>}
<FieldError error={errors.phone_number} label="Nomor Telepon" className="text-xs" />
</Field>
<Field>
@ -122,7 +122,7 @@ export default function UserCreate() {
placeholder="Rp 0"
autoComplete='off'
/>
{errors.base_salary && <p className="text-xs text-red-500">{errors.base_salary}</p>}
<FieldError error={errors.base_salary} label="Gaji Pokok" className="text-xs" />
</Field>
<Field>
@ -135,7 +135,7 @@ export default function UserCreate() {
autoComplete='off'
placeholder='Contoh: Jakarta'
/>
{errors.birth_place && <p className="text-xs text-red-500">{errors.birth_place}</p>}
<FieldError error={errors.birth_place} label="Tempat Lahir" className="text-xs" />
</Field>
<Field>
@ -175,14 +175,14 @@ export default function UserCreate() {
/>
</PopoverContent>
</Popover>
{errors.birth_date && <p className="text-xs text-red-500">{errors.birth_date}</p>}
<FieldError error={errors.birth_date} label="Tanggal Lahir" className="text-xs" />
</Field>
</div>
<Field>
<Label htmlFor="address" required>Alamat</Label>
<Textarea id='address' name='address' value={data.address} onChange={e => setData('address', e.target.value)} placeholder='Contoh: Kp. Bakan Sampeu' />
{errors.address && <p className="text-xs text-red-500">{errors.address}</p>}
<FieldError error={errors.address} label="Alamat" className="text-xs" />
</Field>
</CardContent>
</Card>
@ -205,7 +205,7 @@ export default function UserCreate() {
autoComplete='off'
placeholder='Contoh: john@example.com'
/>
{errors.email && <p className="text-xs text-red-500">{errors.email}</p>}
<FieldError error={errors.email} label="Alamat Surel" className="text-xs" />
</Field>
<Field>
@ -218,7 +218,7 @@ export default function UserCreate() {
autoComplete='off'
placeholder='Contoh: johndoe'
/>
{errors.username && <p className="text-xs text-red-500">{errors.username}</p>}
<FieldError error={errors.username} label="Nama Pengguna" className="text-xs" />
</Field>
</CardContent>
</Card>

View File

@ -2,7 +2,7 @@ import { Head, Link } from '@inertiajs/react';
import { useForm } from '@inertiajs/react';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { Field } from "@/components/ui/field"
import { Field, FieldError } from "@/components/ui/field"
import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"
import userRoutes from '@/routes/user';
@ -79,7 +79,7 @@ export default function UserEdit({ user }: { user: any }) {
placeholder='Contoh: 3213051307900001'
maxLength={16}
/>
{errors.nik && <p className="text-xs text-red-500">{errors.nik}</p>}
<FieldError error={errors.nik} label="NIK" className="text-xs" />
</Field>
<Field>
@ -92,7 +92,7 @@ export default function UserEdit({ user }: { user: any }) {
autoComplete='off'
placeholder='Contoh: John Doe'
/>
{errors.full_name && <p className="text-xs text-red-500">{errors.full_name}</p>}
<FieldError error={errors.full_name} label="Nama Lengkap" className="text-xs" />
</Field>
<Field>
@ -105,7 +105,7 @@ export default function UserEdit({ user }: { user: any }) {
autoComplete='off'
placeholder='Contoh: 08123456789'
/>
{errors.phone_number && <p className="text-xs text-red-500">{errors.phone_number}</p>}
<FieldError error={errors.phone_number} label="Nomor Telepon" className="text-xs" />
</Field>
<Field>
@ -123,7 +123,7 @@ export default function UserEdit({ user }: { user: any }) {
placeholder="Rp 0"
autoComplete='off'
/>
{errors.base_salary && <p className="text-xs text-red-500">{errors.base_salary}</p>}
<FieldError error={errors.base_salary} label="Gaji Pokok" className="text-xs" />
</Field>
<Field>
@ -136,7 +136,7 @@ export default function UserEdit({ user }: { user: any }) {
autoComplete='off'
placeholder='Contoh: Jakarta'
/>
{errors.birth_place && <p className="text-xs text-red-500">{errors.birth_place}</p>}
<FieldError error={errors.birth_place} label="Tempat Lahir" className="text-xs" />
</Field>
<Field>
@ -176,14 +176,14 @@ export default function UserEdit({ user }: { user: any }) {
/>
</PopoverContent>
</Popover>
{errors.birth_date && <p className="text-xs text-red-500">{errors.birth_date}</p>}
<FieldError error={errors.birth_date} label="Tanggal Lahir" className="text-xs" />
</Field>
</div>
<Field>
<Label htmlFor="address" required>Alamat</Label>
<Textarea id='address' name='address' value={data.address} onChange={e => setData('address', e.target.value)} placeholder='Contoh: Kp. Bakan Sampeu' />
{errors.address && <p className="text-xs text-red-500">{errors.address}</p>}
<FieldError error={errors.address} label="Alamat" className="text-xs" />
</Field>
</CardContent>
</Card>
@ -206,7 +206,7 @@ export default function UserEdit({ user }: { user: any }) {
autoComplete='off'
placeholder='Contoh: john@example.com'
/>
{errors.email && <p className="text-xs text-red-500">{errors.email}</p>}
<FieldError error={errors.email} label="Alamat Surel" className="text-xs" />
</Field>
<Field>
@ -219,7 +219,7 @@ export default function UserEdit({ user }: { user: any }) {
autoComplete='off'
placeholder='Contoh: johndoe'
/>
{errors.username && <p className="text-xs text-red-500">{errors.username}</p>}
<FieldError error={errors.username} label="Nama Pengguna" className="text-xs" />
</Field>
</CardContent>
</Card>

View File

@ -1,7 +1,7 @@
import { Head, useForm } from '@inertiajs/react';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { Field } from "@/components/ui/field"
import { Field, FieldError } from "@/components/ui/field"
import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"
import { Textarea } from '@/components/ui/textarea';
@ -96,7 +96,7 @@ export default function SettingIndex({ setting }: { setting: GeneralSetting | nu
placeholder='Contoh: VN Grup Dress'
maxLength={100}
/>
{errors.name && <p className="text-xs text-red-500">{errors.name}</p>}
<FieldError error={errors.name} label="Nama Aplikasi" className="text-xs" />
</Field>
<Field>
@ -108,7 +108,7 @@ export default function SettingIndex({ setting }: { setting: GeneralSetting | nu
placeholder="Deskripsi singkat aplikasi"
rows={4}
/>
{errors.description && <p className="text-xs text-red-500 mt-1">{errors.description}</p>}
<FieldError error={errors.description} label="Deskripsi" className="text-xs mt-1" />
</Field>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
@ -120,7 +120,7 @@ export default function SettingIndex({ setting }: { setting: GeneralSetting | nu
onChange={(e) => setData('phone', e.target.value)}
placeholder="0812xxxx"
/>
{errors.phone && <p className="text-xs text-red-500">{errors.phone}</p>}
<FieldError error={errors.phone} label="No. Telepon" className="text-xs" />
</Field>
</div>
@ -133,7 +133,7 @@ export default function SettingIndex({ setting }: { setting: GeneralSetting | nu
placeholder="Alamat lengkap toko"
rows={3}
/>
{errors.address && <p className="text-xs text-red-500 mt-1">{errors.address}</p>}
<FieldError error={errors.address} label="Alamat" className="text-xs mt-1" />
</Field>
</CardContent>
</Card>
@ -178,7 +178,7 @@ export default function SettingIndex({ setting }: { setting: GeneralSetting | nu
className="hidden"
onChange={handleLogoChange}
/>
{errors.logo && <p className="text-xs text-red-500 mt-2">{errors.logo}</p>}
<FieldError error={errors.logo} label="Logo Aplikasi" className="text-xs mt-2" />
</CardContent>
</Card>
@ -222,7 +222,7 @@ export default function SettingIndex({ setting }: { setting: GeneralSetting | nu
className="hidden"
onChange={handleIconChange}
/>
{errors.icon && <p className="text-xs text-red-500 mt-2">{errors.icon}</p>}
<FieldError error={errors.icon} label="Favicon / Icon" className="text-xs mt-2" />
</CardContent>
</Card>

View File

@ -24,7 +24,7 @@ export default function ConfirmPassword() {
autoFocus
/>
<InputError message={errors.password} />
<InputError message={errors.password} label="Password" />
</div>
<div className="flex items-center">

View File

@ -35,7 +35,7 @@ export default function ForgotPassword({ status }: { status?: string }) {
placeholder="email@example.com"
/>
<InputError message={errors.email} />
<InputError message={errors.email} label="Email address" />
</div>
<div className="my-6 flex items-center justify-start">

View File

@ -39,7 +39,7 @@ export default function Login({
autoComplete="off"
placeholder="johndoe@example.com atau johndoe"
/>
<InputError message={errors.login} />
<InputError message={errors.login} label="Alamat surel atau nama pengguna" />
</div>
<div className="grid gap-2">
@ -54,7 +54,7 @@ export default function Login({
autoComplete="off"
placeholder="********"
/>
<InputError message={errors.password} />
<InputError message={errors.password} label="Kata sandi" />
</div>
<div className="flex items-center space-x-3">

View File

@ -36,6 +36,7 @@ export default function Register() {
/>
<InputError
message={errors.name}
label="Name"
className="mt-2"
/>
</div>
@ -51,7 +52,7 @@ export default function Register() {
name="email"
placeholder="email@example.com"
/>
<InputError message={errors.email} />
<InputError message={errors.email} label="Email address" />
</div>
<div className="grid gap-2">
@ -64,7 +65,7 @@ export default function Register() {
name="password"
placeholder="Password"
/>
<InputError message={errors.password} />
<InputError message={errors.password} label="Password" />
</div>
<div className="grid gap-2">
@ -81,6 +82,7 @@ export default function Register() {
/>
<InputError
message={errors.password_confirmation}
label="Confirm password"
/>
</div>

View File

@ -37,6 +37,7 @@ export default function ResetPassword({ token, email }: Props) {
/>
<InputError
message={errors.email}
label="Email"
className="mt-2"
/>
</div>
@ -51,7 +52,7 @@ export default function ResetPassword({ token, email }: Props) {
autoFocus
placeholder="Password"
/>
<InputError message={errors.password} />
<InputError message={errors.password} label="Password" />
</div>
<div className="grid gap-2">
@ -67,6 +68,7 @@ export default function ResetPassword({ token, email }: Props) {
/>
<InputError
message={errors.password_confirmation}
label="Confirm password"
className="mt-2"
/>
</div>

View File

@ -73,6 +73,7 @@ export default function TwoFactorChallenge() {
/>
<InputError
message={errors.recovery_code}
label="Recovery code"
/>
</>
) : (
@ -99,7 +100,7 @@ export default function TwoFactorChallenge() {
</InputOTPGroup>
</InputOTP>
</div>
<InputError message={errors.code} />
<InputError message={errors.code} label="Code" />
</div>
)}

View File

@ -56,6 +56,7 @@ export default function Profile({
<InputError
className="mt-2"
message={errors.name}
label="Name"
/>
</div>
@ -76,6 +77,7 @@ export default function Profile({
<InputError
className="mt-2"
message={errors.email}
label="Email address"
/>
</div>

View File

@ -100,7 +100,7 @@ export default function Security({
placeholder="Current password"
/>
<InputError message={errors.current_password} />
<InputError message={errors.current_password} label="Current password" />
</div>
<div className="grid gap-2">
@ -115,7 +115,7 @@ export default function Security({
placeholder="New password"
/>
<InputError message={errors.password} />
<InputError message={errors.password} label="New password" />
</div>
<div className="grid gap-2">
@ -133,6 +133,7 @@ export default function Security({
<InputError
message={errors.password_confirmation}
label="Confirm password"
/>
</div>