From 866796b5de9fb2f1cd1e3c900e07b83a3927f85e Mon Sep 17 00:00:00 2001 From: Oliver Bryan <04oliverbryan@gmail.com> Date: Thu, 1 Jan 2026 03:01:57 +0000 Subject: [PATCH] implemented custom Field component --- .../frontend/src/components/create-issue.tsx | 99 ++++---------- .../src/components/create-organisation.tsx | 128 ++++++------------ .../src/components/create-project.tsx | 116 ++++++---------- .../frontend/src/components/login-form.tsx | 84 ++---------- packages/frontend/src/lib/server/index.ts | 4 +- 5 files changed, 125 insertions(+), 306 deletions(-) diff --git a/packages/frontend/src/components/create-issue.tsx b/packages/frontend/src/components/create-issue.tsx index 7f3ff6b..6b3cd94 100644 --- a/packages/frontend/src/components/create-issue.tsx +++ b/packages/frontend/src/components/create-issue.tsx @@ -1,4 +1,4 @@ -import { type FormEvent, useMemo, useState } from "react"; +import { type FormEvent, useState } from "react"; import { Button } from "@/components/ui/button"; import { Dialog, @@ -8,7 +8,7 @@ import { DialogTitle, DialogTrigger, } from "@/components/ui/dialog"; -import { Input } from "@/components/ui/input"; +import { Field } from "@/components/ui/field"; import { Label } from "@/components/ui/label"; import { issue } from "@/lib/server"; import { cn } from "@/lib/utils"; @@ -27,35 +27,14 @@ export function CreateIssue({ const [open, setOpen] = useState(false); const [title, setTitle] = useState(""); const [description, setDescription] = useState(""); - - const [titleTouched, setTitleTouched] = useState(false); - const [descriptionTouched, setDescriptionTouched] = useState(false); const [submitAttempted, setSubmitAttempted] = useState(false); - const [submitting, setSubmitting] = useState(false); const [error, setError] = useState(null); - const titleInvalid = useMemo( - () => ((titleTouched || submitAttempted) && title.trim() === "" ? "Cannot be empty" : ""), - [titleTouched, submitAttempted, title], - ); - - const descriptionInvalid = useMemo( - () => - (descriptionTouched || submitAttempted) && description.trim().length > 2048 - ? "Too long (2048 character limit)" - : "", - [descriptionTouched, submitAttempted, description], - ); - const reset = () => { setTitle(""); setDescription(""); - - setTitleTouched(false); - setDescriptionTouched(false); setSubmitAttempted(false); - setSubmitting(false); setError(null); }; @@ -72,7 +51,7 @@ export function CreateIssue({ setError(null); setSubmitAttempted(true); - if (title.trim() === "" || descriptionInvalid !== "") { + if (title.trim() === "" || description.trim().length > 2048) { return; } @@ -130,51 +109,25 @@ export function CreateIssue({
-
-
- - { - setTitle(e.target.value); - }} - onBlur={() => setTitleTouched(true)} - aria-invalid={titleInvalid !== ""} - placeholder="Demo Issue" - required - /> -
- {titleInvalid !== "" ? ( - - ) : ( - - )} -
-
- -
- - { - setDescription(e.target.value); - }} - onBlur={() => setDescriptionTouched(true)} - aria-invalid={descriptionInvalid !== ""} - placeholder="Optional details" - /> -
- {descriptionInvalid !== "" ? ( - - ) : ( - - )} -
-
+
+ setTitle(e.target.value)} + validate={(v) => (v.trim() === "" ? "Cannot be empty" : undefined)} + submitAttempted={submitAttempted} + placeholder="Demo Issue" + /> + setDescription(e.target.value)} + validate={(v) => + v.trim().length > 2048 ? "Too long (2048 character limit)" : undefined + } + submitAttempted={submitAttempted} + placeholder="Optional details" + />
{error ? ( @@ -184,7 +137,7 @@ export function CreateIssue({ )}
-
+
diff --git a/packages/frontend/src/components/create-organisation.tsx b/packages/frontend/src/components/create-organisation.tsx index eda3e36..445b488 100644 --- a/packages/frontend/src/components/create-organisation.tsx +++ b/packages/frontend/src/components/create-organisation.tsx @@ -1,4 +1,4 @@ -import { type FormEvent, useMemo, useState } from "react"; +import { type FormEvent, useState } from "react"; import { Button } from "@/components/ui/button"; import { Dialog, @@ -8,7 +8,7 @@ import { DialogTitle, DialogTrigger, } from "@/components/ui/dialog"; -import { Input } from "@/components/ui/input"; +import { Field } from "@/components/ui/field"; import { Label } from "@/components/ui/label"; import { organisation } from "@/lib/server"; import { cn } from "@/lib/utils"; @@ -34,34 +34,17 @@ export function CreateOrganisation({ const [name, setName] = useState(""); const [slug, setSlug] = useState(""); const [description, setDescription] = useState(""); - - const [nameTouched, setNameTouched] = useState(false); - const [slugTouched, setSlugTouched] = useState(false); const [slugManuallyEdited, setSlugManuallyEdited] = useState(false); const [submitAttempted, setSubmitAttempted] = useState(false); - const [submitting, setSubmitting] = useState(false); const [error, setError] = useState(null); - const nameInvalid = useMemo( - () => ((nameTouched || submitAttempted) && name.trim() === "" ? "Cannot be empty" : ""), - [nameTouched, submitAttempted, name], - ); - const slugInvalid = useMemo( - () => ((slugTouched || submitAttempted) && slug.trim() === "" ? "Cannot be empty" : ""), - [slugTouched, submitAttempted, slug], - ); - const reset = () => { setName(""); setSlug(""); setDescription(""); - - setNameTouched(false); - setSlugTouched(false); setSlugManuallyEdited(false); setSubmitAttempted(false); - setSubmitting(false); setError(null); }; @@ -128,69 +111,42 @@ export function CreateOrganisation({ -
-
- - { - const nextName = e.target.value; - setName(nextName); - - if (!slugManuallyEdited) { - setSlug(slugify(nextName)); - } - }} - onBlur={() => setNameTouched(true)} - aria-invalid={nameInvalid !== ""} - placeholder="Demo Organisation" - required - /> -
- {nameInvalid !== "" ? ( - - ) : ( - - )} -
-
- -
- - { - setSlug(slugify(e.target.value)); - setSlugManuallyEdited(true); - }} - onBlur={() => setSlugTouched(true)} - aria-invalid={slugInvalid !== ""} - placeholder="demo-organisation" - required - /> -
- {slugInvalid !== "" ? ( - - ) : ( - - )} -
-
- -
- - setDescription(e.target.value)} - placeholder="What is this organisation for?" - /> -
+
+ { + const nextName = e.target.value; + setName(nextName); + if (!slugManuallyEdited) { + setSlug(slugify(nextName)); + } + }} + validate={(v) => (v.trim() === "" ? "Cannot be empty" : undefined)} + submitAttempted={submitAttempted} + placeholder="Demo Organisation" + /> + { + setSlug(slugify(e.target.value)); + setSlugManuallyEdited(true); + }} + validate={(v) => (v.trim() === "" ? "Cannot be empty" : undefined)} + submitAttempted={submitAttempted} + placeholder="demo-organisation" + /> + setDescription(e.target.value)} + validate={(v) => + v.trim().length > 2048 ? "Too long (2048 character limit)" : undefined + } + submitAttempted={submitAttempted} + placeholder="What is this organisation for?" + />
{error ? ( @@ -200,7 +156,7 @@ export function CreateOrganisation({ )}
-
+
diff --git a/packages/frontend/src/components/create-project.tsx b/packages/frontend/src/components/create-project.tsx index f3248c6..bf2a465 100644 --- a/packages/frontend/src/components/create-project.tsx +++ b/packages/frontend/src/components/create-project.tsx @@ -1,5 +1,5 @@ import type { ProjectRecord } from "@issue/shared"; -import { type FormEvent, useMemo, useState } from "react"; +import { type FormEvent, useState } from "react"; import { Button } from "@/components/ui/button"; import { Dialog, @@ -9,7 +9,7 @@ import { DialogTitle, DialogTrigger, } from "@/components/ui/dialog"; -import { Input } from "@/components/ui/input"; +import { Field } from "@/components/ui/field"; import { Label } from "@/components/ui/label"; import { project } from "@/lib/server"; import { cn } from "@/lib/utils"; @@ -34,36 +34,16 @@ export function CreateProject({ const [open, setOpen] = useState(false); const [name, setName] = useState(""); const [key, setKey] = useState(""); - - const [nameTouched, setNameTouched] = useState(false); - const [keyTouched, setKeyTouched] = useState(false); const [keyManuallyEdited, setKeyManuallyEdited] = useState(false); const [submitAttempted, setSubmitAttempted] = useState(false); - const [submitting, setSubmitting] = useState(false); const [error, setError] = useState(null); - const nameInvalid = useMemo( - () => ((nameTouched || submitAttempted) && name.trim() === "" ? "Cannot be empty" : ""), - [nameTouched, submitAttempted, name], - ); - - const keyInvalid = useMemo(() => { - if (!(keyTouched || submitAttempted)) return ""; - if (key.trim() === "") return "Cannot be empty"; - if (key.length > 4) return "Must be 4 or less characters"; - return ""; - }, [keyTouched, submitAttempted, key]); - const reset = () => { setName(""); setKey(""); - - setNameTouched(false); - setKeyTouched(false); setKeyManuallyEdited(false); setSubmitAttempted(false); - setSubmitting(false); setError(null); }; @@ -80,7 +60,7 @@ export function CreateProject({ setError(null); setSubmitAttempted(true); - if (name.trim() === "" || key.length > 4) { + if (name.trim() === "" || key.trim() === "" || key.length > 4) { return; } @@ -140,58 +120,36 @@ export function CreateProject({ -
-
- - { - const nextName = e.target.value; - setName(nextName); - - if (!keyManuallyEdited) { - setKey(keyify(nextName)); - } - }} - onBlur={() => setNameTouched(true)} - aria-invalid={nameInvalid !== ""} - placeholder="Demo Project" - required - /> -
- {nameInvalid !== "" ? ( - - ) : ( - - )} -
-
- -
- - { - setKey(keyify(e.target.value)); - setKeyManuallyEdited(true); - }} - onBlur={() => setKeyTouched(true)} - aria-invalid={keyInvalid !== ""} - placeholder="DEMO" - required - /> -
- {keyInvalid !== "" ? ( - - ) : ( - - )} -
-
+
+ { + const nextName = e.target.value; + setName(nextName); + if (!keyManuallyEdited) { + setKey(keyify(nextName)); + } + }} + validate={(v) => (v.trim() === "" ? "Cannot be empty" : undefined)} + submitAttempted={submitAttempted} + placeholder="Demo Project" + /> + { + setKey(keyify(e.target.value)); + setKeyManuallyEdited(true); + }} + validate={(v) => { + if (v.trim() === "") return "Cannot be empty"; + if (v.length > 4) return "Must be 4 or less characters"; + return undefined; + }} + submitAttempted={submitAttempted} + placeholder="DEMO" + />
{error ? ( @@ -201,7 +159,7 @@ export function CreateProject({ )}
-
+
diff --git a/packages/frontend/src/components/login-form.tsx b/packages/frontend/src/components/login-form.tsx index d7eb621..afcdbf9 100644 --- a/packages/frontend/src/components/login-form.tsx +++ b/packages/frontend/src/components/login-form.tsx @@ -1,77 +1,19 @@ /** biome-ignore-all lint/correctness/useExhaustiveDependencies: <> */ -import { type ChangeEvent, useEffect, useMemo, useState } from "react"; +import { useEffect, useState } from "react"; import { ServerConfigurationDialog } from "@/components/server-configuration-dialog"; import { Button } from "@/components/ui/button"; -import { Input } from "@/components/ui/input"; +import { Field } from "@/components/ui/field"; import { Label } from "@/components/ui/label"; import { capitalise, cn, getServerURL } from "@/lib/utils"; -function Field({ - label = "label", - onChange = () => {}, - onBlur, - invalidMessage = "", - hidden = false, -}: { - label: string; - onChange?: (e: ChangeEvent) => void; - onBlur?: () => void; - invalidMessage?: string; - hidden?: boolean; -}) { - return ( -
-
- -
- -
- {invalidMessage !== "" ? ( - - ) : ( - - )} -
-
- ); -} - export default function LogInForm() { const [mode, setMode] = useState<"login" | "register">("login"); const [name, setName] = useState(""); const [username, setUsername] = useState(""); const [password, setPassword] = useState(""); - - const [nameTouched, setNameTouched] = useState(false); - const [usernameTouched, setUsernameTouched] = useState(false); - const [passwordTouched, setPasswordTouched] = useState(false); - const [submitAttempted, setSubmitAttempted] = useState(false); - const [error, setError] = useState(""); - - const nameInvalid = useMemo( - () => ((nameTouched || submitAttempted) && name.trim() === "" ? "Cannot be empty" : ""), - [nameTouched, submitAttempted, name], - ); - const usernameInvalid = useMemo( - () => ((usernameTouched || submitAttempted) && username.trim() === "" ? "Cannot be empty" : ""), - [usernameTouched, submitAttempted, username], - ); - const passwordInvalid = useMemo( - () => ((passwordTouched || submitAttempted) && password.trim() === "" ? "Cannot be empty" : ""), - [passwordTouched, submitAttempted, password], - ); + const [submitAttempted, setSubmitAttempted] = useState(false); const logIn = () => { if (username.trim() === "" || password.trim() === "") { @@ -154,11 +96,6 @@ export default function LogInForm() { const resetForm = () => { setError(""); setSubmitAttempted(false); - - setNameTouched(false); - setUsernameTouched(false); - setPasswordTouched(false); - requestAnimationFrame(() => focusFirstInput()); }; @@ -187,23 +124,26 @@ export default function LogInForm() { {mode === "register" && ( setName(e.target.value)} - onBlur={() => setNameTouched(true)} - invalidMessage={nameInvalid} + validate={(v) => (v.trim() === "" ? "Cannot be empty" : undefined)} + submitAttempted={submitAttempted} /> )} setUsername(e.target.value)} - onBlur={() => setUsernameTouched(true)} - invalidMessage={usernameInvalid} + validate={(v) => (v.trim() === "" ? "Cannot be empty" : undefined)} + submitAttempted={submitAttempted} /> setPassword(e.target.value)} - onBlur={() => setPasswordTouched(true)} - invalidMessage={passwordInvalid} + validate={(v) => (v.trim() === "" ? "Cannot be empty" : undefined)} hidden={true} + submitAttempted={submitAttempted} /> {mode === "login" ? ( diff --git a/packages/frontend/src/lib/server/index.ts b/packages/frontend/src/lib/server/index.ts index 067c46c..3b9cd85 100644 --- a/packages/frontend/src/lib/server/index.ts +++ b/packages/frontend/src/lib/server/index.ts @@ -3,6 +3,6 @@ export * as organisation from "./organisation"; export * as project from "./project"; export type ServerQueryInput = { - onSuccess?: (data: unknown, res: Response) => void; - onError?: (error: unknown) => void; + onSuccess?: (data: any, res: Response) => void; + onError?: (error: string) => void; };