diff --git a/frontend/src/ts/components/modals/SimpleModal.tsx b/frontend/src/ts/components/modals/SimpleModal.tsx index 42303e5f0f53..6aac8bea6270 100644 --- a/frontend/src/ts/components/modals/SimpleModal.tsx +++ b/frontend/src/ts/components/modals/SimpleModal.tsx @@ -1,5 +1,4 @@ import { AnyFieldApi, createForm } from "@tanstack/solid-form"; -import { format as dateFormat } from "date-fns/format"; import { Accessor, For, @@ -10,14 +9,7 @@ import { Switch, untrack, } from "solid-js"; -import { - z, - ZodDate, - ZodDefault, - ZodFirstPartyTypeKind, - ZodNumber, - ZodTypeAny, -} from "zod"; +import { z, ZodDefault, ZodFirstPartyTypeKind, ZodTypeAny } from "zod"; import { hideLoaderBar, showLoaderBar } from "../../states/loader-bar"; import { @@ -38,6 +30,7 @@ import { AnimatedModal } from "../common/AnimatedModal"; import { Checkbox } from "../ui/form/Checkbox"; import { InputField } from "../ui/form/InputField"; import { SubmitButton } from "../ui/form/SubmitButton"; +import { TextareaField } from "../ui/form/TextareaField"; import { fieldMandatory, fromSchema, handleResult } from "../ui/form/utils"; type SyncValidator = (opts: { @@ -116,19 +109,13 @@ function FieldInput(props: { input: GenericSimpleModalInput; schema: z.ZodTypeAny; }): JSXElement { - const formatDate = (date: Date | undefined) => - date === undefined - ? undefined - : dateFormat( - date, - props.input.type === "date" ? "yyyy-MM-dd" : "yyyy-MM-dd'T'HH:mm:ss", - ); return ( } > @@ -156,66 +149,30 @@ function FieldInput(props: { /> - + />
- { - props.field().handleChange(e.currentTarget.value); - props.input.oninput?.(e); - }} - onBlur={() => props.field().handleBlur()} /> {props.field().state.value as string}
- - - { - props.field().handleChange(e.currentTarget.value); - props.input.oninput?.(e); - }} - onBlur={() => props.field().handleBlur()} - /> -
); } @@ -443,35 +400,6 @@ export function convertFn( } } -function getMinAndMax(schema: ZodTypeAny): { - min?: number; - max?: number; -} { - if (getZodType(schema) !== ZodFirstPartyTypeKind.ZodNumber) return {}; - - return { - min: (schema as ZodNumber).minValue ?? undefined, - max: (schema as ZodNumber).maxValue ?? undefined, - }; -} -function getDateMinAndMax( - schema: ZodTypeAny, - format: (val: Date | undefined) => string | undefined, -): { - min?: string; - max?: string; -} { - if (getZodType(schema) !== ZodFirstPartyTypeKind.ZodDate) return {}; - - const applyFormat = (it: Date | null) => - it === null ? undefined : format(it); - - return { - min: applyFormat((schema as ZodDate).minDate), - max: applyFormat((schema as ZodDate).maxDate), - }; -} - function getZodType(schema: ZodTypeAny): ZodFirstPartyTypeKind { // oxlint-disable-next-line typescript/no-unsafe-assignment typescript/no-unsafe-member-access return schema._def["typeName"] as ZodFirstPartyTypeKind; diff --git a/frontend/src/ts/components/pages/settings/SettingsPage.tsx b/frontend/src/ts/components/pages/settings/SettingsPage.tsx index fe7b77e8c3f2..16da1cc15277 100644 --- a/frontend/src/ts/components/pages/settings/SettingsPage.tsx +++ b/frontend/src/ts/components/pages/settings/SettingsPage.tsx @@ -324,7 +324,7 @@ function AutoSetting(props: { [props.key]: getConfig[props.key], }, onSubmit: ({ value }) => { - const val = parseInt(String(value[props.key])); + const val = parseFloat(String(value[props.key])); if (val === getConfig[props.key]) return; savedIndicator.flash(); setConfig(props.key, val as Config[T]); @@ -350,7 +350,7 @@ function AutoSetting(props: { name={props.key} validators={{ onChange: ({ value }) => { - const val = parseInt(String(value)); + const val = parseFloat(String(value)); if (isNaN(val)) { return "Must be a number"; } @@ -368,6 +368,7 @@ function AutoSetting(props: {
{ - const val = parseInt(String(value.fpsLimit)); + const val = parseFloat(String(value.fpsLimit)); if (val === getfpsLimit()) return; setfpsLimit(val); savedIndicator.flash(); @@ -54,7 +54,7 @@ export function AnimationFpsLimit(): JSXElement { name="fpsLimit" validators={{ onChange: ({ value }) => { - const val = parseInt(String(value)); + const val = parseFloat(String(value)); if (isNaN(val)) { return "Must be a number"; } @@ -72,6 +72,7 @@ export function AnimationFpsLimit(): JSXElement { field={field} placeholder={"custom limit"} type="number" + schema={fpsLimitSchema} resetToDefaultIfEmptyOnBlur /> diff --git a/frontend/src/ts/components/pages/settings/custom-setting/MaxLineWidth.tsx b/frontend/src/ts/components/pages/settings/custom-setting/MaxLineWidth.tsx index f69258ea38cc..822523b1cb95 100644 --- a/frontend/src/ts/components/pages/settings/custom-setting/MaxLineWidth.tsx +++ b/frontend/src/ts/components/pages/settings/custom-setting/MaxLineWidth.tsx @@ -19,7 +19,7 @@ export function MaxLineWidth(): JSXElement { maxLineWidth: getConfig.maxLineWidth, }, onSubmit: ({ value }) => { - const val = parseInt(String(value.maxLineWidth)); + const val = parseFloat(String(value.maxLineWidth)); if (val === getConfig.maxLineWidth) return; flash(); setConfig("maxLineWidth", val); @@ -45,7 +45,7 @@ export function MaxLineWidth(): JSXElement { name="maxLineWidth" validators={{ onChange: ({ value }) => { - const val = parseInt(String(value)); + const val = parseFloat(String(value)); if (isNaN(val)) { return "Must be a number"; } @@ -61,6 +61,7 @@ export function MaxLineWidth(): JSXElement {
{ - const val = parseInt(String(value.minAccCustom)); + const val = parseFloat(String(value.minAccCustom)); if (val === getConfig.minAccCustom) return; if (getConfig.minAcc === "custom") { // @@ -51,7 +51,7 @@ export function MinAcc(): JSXElement { name="minAccCustom" validators={{ onChange: ({ value }) => { - const val = parseInt(String(value)); + const val = parseFloat(String(value)); if (isNaN(val)) { return "Must be a number"; } @@ -67,6 +67,7 @@ export function MinAcc(): JSXElement {
{ - const val = parseInt(String(value.minBurstCustomSpeed)); + const val = parseFloat(String(value.minBurstCustomSpeed)); if (val === getConfig.minBurstCustomSpeed) return; if (getConfig.minBurst !== "off") { // @@ -51,7 +51,7 @@ export function MinBurst(): JSXElement { name="minBurstCustomSpeed" validators={{ onChange: ({ value }) => { - const val = parseInt(String(value)); + const val = parseFloat(String(value)); if (isNaN(val)) { return "Must be a number"; } @@ -67,6 +67,7 @@ export function MinBurst(): JSXElement {
{ - const val = parseInt(String(value.minWpmCustomSpeed)); + const val = parseFloat(String(value.minWpmCustomSpeed)); if (val === getConfig.minWpmCustomSpeed) return; if (getConfig.minWpm === "custom") { // @@ -51,7 +51,7 @@ export function MinSpeed(): JSXElement { name="minWpmCustomSpeed" validators={{ onChange: ({ value }) => { - const val = parseInt(String(value)); + const val = parseFloat(String(value)); if (isNaN(val)) { return "Must be a number"; } @@ -65,6 +65,7 @@ export function MinSpeed(): JSXElement {
{ - const val = parseInt(String(value.paceCaretCustomSpeed)); + const val = parseFloat(String(value.paceCaretCustomSpeed)); if (val === getConfig.paceCaretCustomSpeed) return; if (getConfig.paceCaret !== "off") { // @@ -54,7 +54,7 @@ export function PaceCaret(): JSXElement { name="paceCaretCustomSpeed" validators={{ onChange: ({ value }) => { - const val = parseInt(String(value)); + const val = parseFloat(String(value)); if (isNaN(val)) { return "Must be a number"; } @@ -70,6 +70,7 @@ export function PaceCaret(): JSXElement {
+ date === undefined + ? undefined + : dateFormat( + date, + props.type === "date" ? "yyyy-MM-dd" : "yyyy-MM-dd'T'HH:mm:ss", + ); + return (
props.field().handleChange(e.target.value)} + onInput={(e) => { + props.field().handleChange(e.target.value); + }} onKeyDown={(e) => { if (e.key === "Enter") { shakeItIfYouWantIt(); @@ -84,8 +97,11 @@ export function InputField(props: { onFocus={() => props.onFocus?.()} dir={props.dir} maxLength={props.maxLength} + {...getNumberOptions(props.schema)} + {...getDateOptions(props.schema, formatDate)} min={props.min} max={props.max} + step={props.step?.toString()} /> @@ -93,3 +109,43 @@ export function InputField(props: {
); } + +function getNumberOptions(schema: ZodTypeAny | undefined): { + min?: number; + max?: number; + step?: string; +} { + if (schema === undefined) return {}; + if (getZodType(schema) !== ZodFirstPartyTypeKind.ZodNumber) return {}; + const numberSchema = schema as ZodNumber; + + return { + min: numberSchema.minValue ?? undefined, + max: numberSchema.maxValue ?? undefined, + step: numberSchema.isInt ? "1" : "any", + }; +} + +function getDateOptions( + schema: ZodTypeAny | undefined, + format: (val: Date | undefined) => string | undefined, +): { + min?: string; + max?: string; +} { + if (schema === undefined) return {}; + if (getZodType(schema) !== ZodFirstPartyTypeKind.ZodDate) return {}; + + const applyFormat = (it: Date | null) => + it === null ? undefined : format(it); + + return { + min: applyFormat((schema as ZodDate).minDate), + max: applyFormat((schema as ZodDate).maxDate), + }; +} + +function getZodType(schema: ZodTypeAny): ZodFirstPartyTypeKind { + // oxlint-disable-next-line typescript/no-unsafe-assignment typescript/no-unsafe-member-access + return schema._def["typeName"] as ZodFirstPartyTypeKind; +} diff --git a/frontend/src/ts/components/ui/form/TextareaField.tsx b/frontend/src/ts/components/ui/form/TextareaField.tsx index 394069d40645..555027c597ce 100644 --- a/frontend/src/ts/components/ui/form/TextareaField.tsx +++ b/frontend/src/ts/components/ui/form/TextareaField.tsx @@ -8,6 +8,7 @@ export function TextareaField(props: { field: Accessor; ref?: HTMLTextAreaElement | ((el: HTMLTextAreaElement) => void); placeholder?: string; + autocomplete?: string; disabled?: boolean; class?: string; maxLength?: number; @@ -28,6 +29,7 @@ export function TextareaField(props: { id={props.field().name as string} name={props.field().name as string} placeholder={props.placeholder ?? ""} + autocomplete={props.autocomplete} value={props.field().state.value as string} onBlur={() => props.field().handleBlur()} onInput={(e) => props.field().handleChange(e.currentTarget.value)}