From b6d9c1cec3ba057413c8daa29a7b299bf4d68630 Mon Sep 17 00:00:00 2001 From: Kalle <38327916+Sendouc@users.noreply.github.com> Date: Sun, 18 Jan 2026 22:22:31 +0200 Subject: [PATCH] Form with all fields to /components --- .../form-examples-schema.ts | 157 ++++++++++++++++++ .../components-showcase/routes/components.tsx | 149 +++++++++++++++++ 2 files changed, 306 insertions(+) create mode 100644 app/features/components-showcase/form-examples-schema.ts diff --git a/app/features/components-showcase/form-examples-schema.ts b/app/features/components-showcase/form-examples-schema.ts new file mode 100644 index 000000000..892903c5b --- /dev/null +++ b/app/features/components-showcase/form-examples-schema.ts @@ -0,0 +1,157 @@ +import { z } from "zod"; +import { + checkboxGroup, + customField, + datetimeOptional, + datetimeRequired, + dayMonthYearRequired, + dualSelectOptional, + numberFieldOptional, + radioGroup, + select, + selectDynamicOptional, + selectOptional, + stageSelect, + textAreaOptional, + textAreaRequired, + textFieldOptional, + textFieldRequired, + timeRangeOptional, + toggle, + userSearchOptional, + weaponPool, + weaponSelectOptional, +} from "~/form/fields"; + +export const formFieldsShowcaseSchema = z.object({ + // Text fields + requiredText: textFieldRequired({ + label: "labels.name", + maxLength: 100, + }), + optionalText: textFieldOptional({ + label: "labels.bio", + maxLength: 200, + }), + optionalNumber: numberFieldOptional({ + label: "labels.vodTeamSize", + }), + + // Text areas + requiredTextArea: textAreaRequired({ + label: "labels.description", + maxLength: 500, + }), + optionalTextArea: textAreaOptional({ + label: "labels.text", + maxLength: 1000, + }), + + // Toggles + isPublic: toggle({ + label: "labels.buildPrivate", + }), + enableNotifications: toggle({ + label: "labels.isEstablished", + }), + + // Selects + requiredSelect: select({ + label: "labels.voiceChat", + items: [ + { label: "options.voiceChat.yes", value: "YES" }, + { label: "options.voiceChat.no", value: "NO" }, + { label: "options.voiceChat.listenOnly", value: "LISTEN_ONLY" }, + ], + }), + optionalSelect: selectOptional({ + label: "labels.vodType", + items: [ + { label: "vodTypes.TOURNAMENT", value: "TOURNAMENT" }, + { label: "vodTypes.CAST", value: "CAST" }, + { label: "vodTypes.SCRIM", value: "SCRIM" }, + ], + }), + dynamicSelect: selectDynamicOptional({ + label: "labels.orgSeries", + }), + divisionRange: dualSelectOptional({ + fields: [ + { + label: "labels.scrimMaxDiv", + items: [ + { label: () => "S+", value: "S+" }, + { label: () => "S", value: "S" }, + { label: () => "A", value: "A" }, + { label: () => "B", value: "B" }, + ], + }, + { + label: "labels.scrimMinDiv", + items: [ + { label: () => "S+", value: "S+" }, + { label: () => "S", value: "S" }, + { label: () => "A", value: "A" }, + { label: () => "B", value: "B" }, + ], + }, + ], + }), + + // Radio & Checkbox groups + matchType: radioGroup({ + label: "labels.scrimMaps", + items: [ + { label: "options.scrimMaps.noPreference", value: "NO_PREFERENCE" }, + { label: "options.scrimMaps.szOnly", value: "SZ_ONLY" }, + { label: "options.scrimMaps.rankedOnly", value: "RANKED_ONLY" }, + ], + }), + selectedModes: checkboxGroup({ + label: "labels.buildModes", + items: [ + { label: "modes.SZ", value: "SZ" }, + { label: "modes.TC", value: "TC" }, + { label: "modes.RM", value: "RM" }, + { label: "modes.CB", value: "CB" }, + ], + }), + + // Date & Time + requiredDatetime: datetimeRequired({ + label: "labels.startTime", + }), + optionalDatetime: datetimeOptional({ + label: "labels.vodDate", + }), + birthDate: dayMonthYearRequired({ + label: "labels.banUserExpiresAt", + }), + availableTime: timeRangeOptional({ + label: "labels.weekdayTimes", + startLabel: "labels.start", + endLabel: "labels.end", + }), + + // Game-specific fields + weapons: weaponPool({ + label: "labels.weaponPool", + maxCount: 5, + minCount: 0, + }), + stage: stageSelect({ + label: "labels.vodStage", + }), + weapon: weaponSelectOptional({ + label: "labels.vodWeapon", + }), + user: userSearchOptional({ + label: "labels.banUserPlayer", + }), + + // Custom field + customValue: customField( + { initialValue: "custom initial value" }, + z.string().optional(), + ), +}); diff --git a/app/features/components-showcase/routes/components.tsx b/app/features/components-showcase/routes/components.tsx index f173d3d07..0ebdc0130 100644 --- a/app/features/components-showcase/routes/components.tsx +++ b/app/features/components-showcase/routes/components.tsx @@ -47,8 +47,11 @@ import { SubmitButton } from "~/components/SubmitButton"; import { SubNav, SubNavLink } from "~/components/SubNav"; import { Table } from "~/components/Table"; import { WeaponSelect } from "~/components/WeaponSelect"; +import type { CustomFieldRenderProps } from "~/form/FormField"; +import { SendouForm } from "~/form/SendouForm"; import type { MainWeaponId, StageId } from "~/modules/in-game-lists/types"; import styles from "../components-showcase.module.css"; +import { formFieldsShowcaseSchema } from "../form-examples-schema"; export const SECTIONS = [ { title: "Buttons", id: "buttons", component: ButtonsSection }, @@ -90,6 +93,7 @@ export const SECTIONS = [ { title: "Placements", id: "placements", component: PlacementSection }, { title: "Badges", id: "badges", component: BadgeSection }, { title: "Game Selects", id: "game-selects", component: GameSelectSection }, + { title: "Form Fields", id: "form-fields", component: FormFieldsSection }, { title: "Miscellaneous", id: "miscellaneous", component: MiscSection }, ] as const; @@ -2039,6 +2043,151 @@ function GameSelectSection({ id }: { id: string }) { ); } +const DYNAMIC_SELECT_OPTIONS = [ + { value: "1", label: "Tournament A" }, + { value: "2", label: "Tournament B" }, + { value: "3", label: "Tournament C" }, +]; + +function FormFieldsSection({ id }: { id: string }) { + return ( +
+ Form Fields +

+ Schema-based form fields using SendouForm. Each field type is defined + with Zod schemas that generate both UI and validation. +

+ + + {({ FormField }) => ( +
+ Text Fields + + + + + + + + + + + + + + Text Areas + + + + + + + + + + Toggle (Switch) + + +
+ + +
+
+ + Select Fields + + + + + + + + + + + + + + + + + + Radio & Checkbox Groups + + + + + + + + + + Date & Time + + + + + + + + + + + + + + + + + + Game-specific Fields + + + + + + + + + + + + + + + + + + Custom Field + + + + {(props: CustomFieldRenderProps) => ( +
+ + props.onChange(e.target.value)} + aria-invalid={Boolean(props.error)} + /> + {props.error ? ( + {props.error} + ) : null} +
+ )} +
+
+
+ )} +
+
+ ); +} + function MiscSection({ id }: { id: string }) { const [rangeValue, setRangeValue] = useState(50); const [colorValue, setColorValue] = useState("#3b82f6");