From 31fc0d41e62a22fa4cbadcd1755e4909146f0bb0 Mon Sep 17 00:00:00 2001 From: Oliver Bryan <04oliverbryan@gmail.com> Date: Thu, 1 Jan 2026 03:01:31 +0000 Subject: [PATCH] custom Field component with error display --- packages/frontend/src/components/ui/field.tsx | 58 +++++++++++++++++++ 1 file changed, 58 insertions(+) create mode 100644 packages/frontend/src/components/ui/field.tsx diff --git a/packages/frontend/src/components/ui/field.tsx b/packages/frontend/src/components/ui/field.tsx new file mode 100644 index 0000000..2fe9140 --- /dev/null +++ b/packages/frontend/src/components/ui/field.tsx @@ -0,0 +1,58 @@ +import { type ChangeEvent, useMemo, useState } from "react"; +import { Input } from "./input"; +import { Label } from "./label"; + +export function Field({ + label, + value = "", + onChange = () => {}, + validate, + hidden = false, + submitAttempted, + placeholder, +}: { + label: string; + value?: string; + onChange?: (e: ChangeEvent) => void; + validate?: (value: string) => string | undefined; + hidden?: boolean; + submitAttempted?: boolean; + placeholder?: string; +}) { + const [internalTouched, setInternalTouched] = useState(false); + const isTouched = submitAttempted || internalTouched; + + const invalidMessage = useMemo(() => { + if (!isTouched) { + return ""; + } + return validate?.(value) ?? ""; + }, [isTouched, validate, value]); + + return ( +
+
+ +
+ setInternalTouched(true)} + name={label} + aria-invalid={invalidMessage !== ""} + type={hidden ? "password" : "text"} + /> +
+ {invalidMessage !== "" ? ( + + ) : ( + + )} +
+
+ ); +}