diff --git a/.gitignore b/.gitignore index 6704566..dd795b2 100644 --- a/.gitignore +++ b/.gitignore @@ -102,3 +102,5 @@ dist # TernJS port file .tern-port + +/build diff --git a/README.md b/README.md index 2395220..e6ba307 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,134 @@ # remix-hook-form -Open source wrapper for react-hook-form aimed at Remix.run + +remix-hook-form is a lightweight wrapper around [remix-hook-form](https://react-hook-form.com/) that makes it easier to use in your [Remix](https://remix.run) applications. It provides a set of hooks and utilities that simplify the process of working with forms and form data, while leveraging the power and flexibility of remix-hook-form. + +## Installation + +You can install the latest version of remix-hook-form using [npm](https://www.npmjs.com/): + +`npm install remix-hook-form` + +Or, if you prefer [yarn](https://yarnpkg.com/): + +`yarn add remix-hook-form` + +## Usage + +Here is an example usage of remix-hook-form: + +```jsx +import { useRemixForm, getValidatedFormData } from "remix-hook-form"; +import { Form } from "@remix-run/react"; +import { zodResolver } from "@hookform/resolvers/zod"; +import * as zod from "zod"; +import { ActionArgs, json } from "@remix-run/server-runtime"; + +const schema = zod.object({ + name: zod.string().nonempty(), + email: zod.string().email().nonempty(), +}); + +type FormData = zod.infer; + +const resolver = zodResolver(schema); + +export const action = ({ request }: ActionArgs) => { + const { errors, data } = + getValidatedFormData < FormData > (request, resolver); + if (errors) { + return json(errors); + } + // Do something with the data + return json(data); +}; + +export default function MyForm() { + const { + handleSubmit, + formState: { errors }, + register, + } = useRemixForm({ + mode: "onSubmit", + defaultValues: { + name: "", + email: "", + }, + resolver, + }); + + return ( +
+ + + +
+ ); +} +``` + +## Utilities + +### getValidatedFormData + +getValidatedFormData is a utility function that can be used to validate form data in your action. It takes two arguments: the request object and the resolver function. It returns an object with two properties: `errors` and `data`. If there are no errors, `errors` will be `undefined`. If there are errors, `errors` will be an object with the same shape as the `errors` object returned by `useRemixForm`. If there are no errors, `data` will be an object with the same shape as the `data` object returned by `useRemixForm`. + +### validateFormData + +validateFormData is a utility function that can be used to validate form data in your action. It takes two arguments: the request object and the resolver function. It returns an object with two properties: `errors` and `data`. If there are no errors, `errors` will be `undefined`. If there are errors, `errors` will be an object with the same shape as the `errors` object returned by `useRemixForm`. If there are no errors, `data` will be an object with the same shape as the `data` object returned by `useRemixForm`. + +The difference between `validateFormData` and `getValidatedFormData` is that `validateFormData` only validates the data while the `getValidatedFormData` function also extracts the data automatically from the request object assuming you were using the default setup + +### createFormData + +createFormData is a utility function that can be used to create a FormData object from the data returned by the handleSubmit function from `react-hook-form`. It takes two arguments, first one is the `data` from the `handleSubmit` function and the second one is the key that the data will be stored in the FormData object. (default is `formData`). It returns a FormData object. + +### parseFormData + +parseFormData is a utility function that can be used to parse the data submitted to the action by the handleSubmit function from `react-hook-form`. It takes two arguments, first one is the `request` submitted from the frontend and the second one is the key that the data will be stored in the FormData object. (default is `formData`). It returns an object that contains unvalidated `data` submitted from the frontend. + +## Hooks + +### useRemixForm + +useRemixForm is a hook that can be used to create a form in your Remix application. It takes all the same properties as `react-hook-form`'s `useForm` hook, with the addition of 3 properties: +`submitHandlers` - an object containing two properties, `onValid` which can be passed into the function to override the default behavior of the handleSubmit success case provided by the hook, and `onInvalid` which can be passed into the function to override the default behavior of the handleSubmit error case provided by the hook. +`submitConfig` - allows you to pass additional configuration to the `useSubmit` function from remix such as `{ replace: true }` to replace the current history entry instead of pushing a new one., +`submitData` - allows you to pass additional data to the backend when the form is submitted + +The hook acts almost identically to the `react-hook-form` hook, with the exception of the `handleSubmit` function, and the `formState.errors`. + +The `handleSubmit` function uses two thing under the hood to allow you easier usage in Remix, those two things are: + +- The success case is provided by default where when the form is validated by the provided resolver, and it has no errors, it will automatically submit the form to the current route using a POST request. The data will be sent as `formData` to the action function. +- The data that is sent is automatically wrapped into a formData object and passed to the server ready to be used. Easiest way to consume it is by using the `parseFormData` or `getValidatedFormData` function from the `remix-hook-form` package. + +The `formState.errors` object is automatically populated with the errors returned by the server. If the server returns an object with the same shape as the `errors` object returned by `useRemixForm`, it will automatically populate the `formState.errors` object with the errors returned by the server. +This is achieved by using `useActionData` from `@remix-run/react` to get the data returned by the action function. If the data returned by the action function is an object with the same shape as the `errors` object returned by `useRemixForm`, it will automatically populate the `formState.errors` object with the errors returned by the server. To assure this is done properly it is recommended that you use `getValidatedFormData` and then return the errors object from the action function as `json(errors)`. + +### useRemixFormContext + +Exactly the same as `useFormContext` from `react-hook-form` but it also returns the changed `formState.errors` and `handleSubmit` object. + +## RemixFormProvider + +Identical to the `FormProvider` from `react-hook-form`, but it also returns the changed `formState.errors` and `handleSubmit` object. + +## License + +MIT + +## Bugs + +If you find a bug, please file an issue on [our issue tracker on GitHub](https://github.com/Code-Forge-Net/remix-hook-form/issues) + +## Contributing + +We welcome contributions from the community! diff --git a/package.json b/package.json new file mode 100644 index 0000000..4ffd493 --- /dev/null +++ b/package.json @@ -0,0 +1,40 @@ +{ + "name": "remix-hook-form", + "version": "1.0.0", + "description": "Utility wrapper around react-hook-form", + "main": "build/index.js", + "scripts": { + "prepare": "tsc", + "prepublish": "npm run prepare", + "test": "test" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/Code-Forge-Net/remix-hook-form.git" + }, + "keywords": [ + "React", + "Remix", + "Remix.run", + "react-hook-form", + "hooks", + "forms" + ], + "author": "Alem Tuzlak", + "license": "MIT", + "bugs": { + "url": "https://github.com/Code-Forge-Net/remix-hook-form/issues" + }, + "homepage": "https://github.com/Code-Forge-Net/remix-hook-form#readme", + "peerDependencies": { + "@remix-run/react": "^1.15.0", + "react": "^18.2.0", + "react-dom": "^18.2.0", + "react-hook-form": "^7.43.9" + }, + "devDependencies": { + "@hookform/resolvers": "^3.0.1", + "@types/react": "^18.0.34", + "typescript": "^5.0.4" + } +} \ No newline at end of file diff --git a/src/hook/index.tsx b/src/hook/index.tsx new file mode 100644 index 0000000..d3ce8d2 --- /dev/null +++ b/src/hook/index.tsx @@ -0,0 +1,106 @@ +import React from "react"; +import { SubmitFunction, useActionData, useSubmit } from "@remix-run/react"; +import { + SubmitErrorHandler, + SubmitHandler, + useFormContext, +} from "react-hook-form"; +import { useForm, FormProvider } from "react-hook-form"; +import type { + FieldValues, + UseFormHandleSubmit, + UseFormProps, + UseFormReturn, +} from "react-hook-form/dist/types"; +import { createFormData, mergeErrors } from "../utilities"; + +export type SubmitFunctionOptions = Parameters[1]; +interface UseRemixFormOptions extends UseFormProps { + submitHandlers?: { + onValid?: SubmitHandler; + onInvalid?: SubmitErrorHandler; + }; + submitConfig?: SubmitFunctionOptions; + submitData?: FieldValues; +} + +export const useRemixForm = ({ + submitHandlers, + submitConfig, + submitData, + ...formProps +}: UseRemixFormOptions) => { + const submit = useSubmit(); + const data = useActionData(); + const methods = useForm(formProps); + + // Submits the data to the server when form is valid + const onSubmit = (data: T) => { + submit(createFormData({ ...data, ...submitData }), { + method: "post", + ...submitConfig, + }); + }; + + const onInvalid = () => {}; + + const formState = methods.formState; + + const { + dirtyFields, + isDirty, + isSubmitSuccessful, + isSubmitted, + isSubmitting, + isValid, + isValidating, + touchedFields, + submitCount, + errors, + isLoading, + } = formState; + + const formErrors = mergeErrors(errors, data); + + return { + ...methods, + handleSubmit: methods.handleSubmit( + submitHandlers?.onValid ?? onSubmit, + submitHandlers?.onInvalid ?? onInvalid + ), + formState: { + dirtyFields, + isDirty, + isSubmitSuccessful, + isSubmitted, + isSubmitting, + isValid, + isValidating, + touchedFields, + submitCount, + isLoading, + errors: formErrors, + }, + }; +}; +interface RemixFormProviderProps + extends Omit, "handleSubmit"> { + children: React.ReactNode; + handleSubmit: any; +} +export const RemixFormProvider = ({ + children, + ...props +}: RemixFormProviderProps) => { + return {children}; +}; + +export const useRemixFormContext = () => { + const methods = useFormContext(); + return { + ...methods, + handleSubmit: methods.handleSubmit as any as ReturnType< + UseFormHandleSubmit + >, + }; +}; diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 0000000..9fb9b2a --- /dev/null +++ b/src/index.ts @@ -0,0 +1,3 @@ +export * from "./utilities"; +export * from "./hook"; +export * from "./utilities"; diff --git a/src/utilities/index.ts b/src/utilities/index.ts new file mode 100644 index 0000000..d4e08c3 --- /dev/null +++ b/src/utilities/index.ts @@ -0,0 +1,125 @@ +import { + FieldValues, + Resolver, + FieldErrors, + FieldErrorsImpl, + DeepRequired, +} from "react-hook-form"; +/** + * Parses the data from an HTTP request and validates it against a schema. + * + * @async + * @template T + * @param {Request} request - An object that represents an HTTP request. + * @param validator - A function that resolves the schema. + * @returns {Promise<{ errors: any[] | undefined; data: T | undefined }>} - A Promise that resolves to an object containing the validated data or any errors that occurred during validation. + */ +export const getValidatedFormData = async ( + request: Request, + resolver: Resolver +) => { + const data = await parseFormData(request); + const validatedOutput = await validateFormData(data, resolver); + return validatedOutput; +}; + +/** + * Helper method used in actions to validate the form data parsed from the frontend using zod and return a json error if validation fails. + * @param data Data to validate + * @param resolver Schema to validate and cast the data with + * @returns Returns the validated data if successful, otherwise returns the error object + */ +export const validateFormData = async ( + data: any, + resolver: Resolver +) => { + const { errors, values } = await resolver( + data, + {}, + { shouldUseNativeValidation: false, fields: {} } + ); + + if (Object.keys(errors).length > 0) { + return { errors: errors as FieldErrors, data: undefined }; + } + + return { errors: undefined, data: values as T }; +}; +/** + Creates a new instance of FormData with the specified data and key. + @template T - The type of the data parameter. It can be any type of FieldValues. + @param {T} data - The data to be added to the FormData. It can be either an object of type FieldValues. + @param {string} [key="formData"] - The key to be used for adding the data to the FormData. + @returns {FormData} - The FormData object with the data added to it. +*/ +export const createFormData = ( + data: T, + key: string = "formData" +): FormData => { + const formData = new FormData(); + const finalData = JSON.stringify(data); + formData.append(key, finalData); + return formData; +}; +/** + +Parses the specified Request object's FormData to retrieve the data associated with the specified key. +@template T - The type of the data to be returned. +@param {Request} request - The Request object whose FormData is to be parsed. +@param {string} [key="formData"] - The key of the data to be retrieved from the FormData. +@returns {Promise} - A promise that resolves to the data of type T. +@throws {Error} - If no data is found for the specified key, or if the retrieved data is not a string. +*/ +export const parseFormData = async ( + request: Request, + key: string = "formData" +): Promise => { + const formData = await request.formData(); + const data = formData.get(key); + if (!data) { + throw new Error("No data found"); + } + if (!(typeof data === "string")) { + throw new Error("Data is not a string"); + } + return JSON.parse(data); +}; +/** + +Merges two error objects generated by a resolver, where T is the generic type of the object. +The function recursively merges the objects and returns the resulting object. +@template T - The generic type of the object. +@param {Partial>>} frontendErrors - The frontend errors +@param {Partial>>} backendErrors - The backend errors +@returns {Partial>>} - The merged errors of type Partial>>. +*/ +export const mergeErrors = ( + frontendErrors: Partial>>, + backendErrors: Partial>> +) => { + if (!backendErrors) { + return frontendErrors; + } + + for (const [key, rightValue] of Object.entries(backendErrors) as [ + keyof T, + DeepRequired[keyof T] + ][]) { + if (typeof rightValue === "object" && !Array.isArray(rightValue)) { + if (!frontendErrors[key]) { + frontendErrors[key] = {} as DeepRequired[keyof T]; + } + mergeErrors(frontendErrors[key]!, rightValue); + } else { + if (frontendErrors[key]) { + if (rightValue.message) { + frontendErrors[key]!.message = rightValue.message; + } + } else { + frontendErrors[key] = rightValue; + } + } + } + + return frontendErrors; +}; diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..445af61 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,107 @@ +{ + "compilerOptions": { + /* Visit https://aka.ms/tsconfig.json to read more about this file */ + + /* Projects */ + // "incremental": true, /* Enable incremental compilation */ + // "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */ + // "tsBuildInfoFile": "./", /* Specify the folder for .tsbuildinfo incremental compilation files. */ + // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects */ + // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */ + // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ + + /* Language and Environment */ + "target": "ES2020", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */ + // "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */ + "jsx": "preserve", /* Specify what JSX code is generated. */ + // "experimentalDecorators": true, /* Enable experimental support for TC39 stage 2 draft decorators. */ + // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */ + // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h' */ + // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */ + // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using `jsx: react-jsx*`.` */ + // "reactNamespace": "", /* Specify the object invoked for `createElement`. This only applies when targeting `react` JSX emit. */ + // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */ + // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */ + + /* Modules */ + "module": "commonjs", /* Specify what module code is generated. */ + "rootDir": "./src", /* Specify the root folder within your source files. */ + // "moduleResolution": "node", /* Specify how TypeScript looks up a file from a given module specifier. */ + // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */ + // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */ + // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ + // "typeRoots": [], /* Specify multiple folders that act like `./node_modules/@types`. */ + // "types": ["build/index.d.ts"], /* Specify type package names to be included without being referenced in a source file. */ + // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ + // "resolveJsonModule": true, /* Enable importing .json files */ + // "noResolve": true, /* Disallow `import`s, `require`s or ``s from expanding the number of files TypeScript should add to a project. */ + + /* JavaScript Support */ + // "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the `checkJS` option to get errors from these files. */ + // "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */ + // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from `node_modules`. Only applicable with `allowJs`. */ + + /* Emit */ + "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */ + // "declarationMap": true, /* Create sourcemaps for d.ts files. */ + // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */ + // "sourceMap": true, /* Create source map files for emitted JavaScript files. */ + // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If `declaration` is true, also designates a file that bundles all .d.ts output. */ + "outDir": "./build", /* Specify an output folder for all emitted files. */ + // "removeComments": true, /* Disable emitting comments. */ + // "noEmit": true, /* Disable emitting files from a compilation. */ + // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */ + // "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types */ + // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */ + // "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */ + // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ + // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */ + // "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */ + // "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */ + // "newLine": "crlf", /* Set the newline character for emitting files. */ + // "stripInternal": true, /* Disable emitting declarations that have `@internal` in their JSDoc comments. */ + // "noEmitHelpers": true, /* Disable generating custom helper functions like `__extends` in compiled output. */ + // "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */ + // "preserveConstEnums": true, /* Disable erasing `const enum` declarations in generated code. */ + // "declarationDir": "./", /* Specify the output directory for generated declaration files. */ + // "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */ + + /* Interop Constraints */ + // "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */ + // "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */ + "esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables `allowSyntheticDefaultImports` for type compatibility. */ + // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */ + "forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */ + + /* Type Checking */ + "strict": true, /* Enable all strict type-checking options. */ + // "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied `any` type.. */ + // "strictNullChecks": true, /* When type checking, take into account `null` and `undefined`. */ + // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */ + // "strictBindCallApply": true, /* Check that the arguments for `bind`, `call`, and `apply` methods match the original function. */ + // "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */ + // "noImplicitThis": true, /* Enable error reporting when `this` is given the type `any`. */ + // "useUnknownInCatchVariables": true, /* Type catch clause variables as 'unknown' instead of 'any'. */ + // "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */ + // "noUnusedLocals": true, /* Enable error reporting when a local variables aren't read. */ + // "noUnusedParameters": true, /* Raise an error when a function parameter isn't read */ + // "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */ + // "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */ + // "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */ + // "noUncheckedIndexedAccess": true, /* Include 'undefined' in index signature results */ + // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */ + // "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type */ + // "allowUnusedLabels": true, /* Disable error reporting for unused labels. */ + // "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */ + + /* Completeness */ + // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ + "skipLibCheck": true /* Skip type checking all .d.ts files. */ + }, + "exclude": [ + "./bin/**/*.test.ts", + "./build/**/*", + "./bin/mocks/**/*", + "./jest.config.ts", + ] +} \ No newline at end of file diff --git a/yarn.lock b/yarn.lock new file mode 100644 index 0000000..fc970e0 --- /dev/null +++ b/yarn.lock @@ -0,0 +1,110 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +"@hookform/resolvers@^3.0.1": + version "3.0.1" + resolved "https://registry.yarnpkg.com/@hookform/resolvers/-/resolvers-3.0.1.tgz#e030f8ef5c407beb5be9bbafd68059cbdd02e0cb" + integrity sha512-n5oOt0cLw9mQNW3/k9zWaPsNWQcc0k6Jpc7XUrg2Q/AqqsHp3IVa1juqHCxczXI6uXHBa69ILc4pdtsRGyuzsw== + +"@remix-run/react@^1.15.0": + version "1.15.0" + resolved "https://registry.yarnpkg.com/@remix-run/react/-/react-1.15.0.tgz#8f1e5c1abc88ede9a25e8e8ee58a2a7fe16121b2" + integrity sha512-S0RuIeHvQTqryCZ3KVl8EsIWCqL6/ky1/kmDpN2n5Pdjew2BLC6DX7OdrY1ZQjbzOMHAROsZlyaSSVXCItunag== + dependencies: + "@remix-run/router" "1.5.0" + react-router-dom "6.10.0" + use-sync-external-store "1.2.0" + +"@remix-run/router@1.5.0": + version "1.5.0" + resolved "https://registry.yarnpkg.com/@remix-run/router/-/router-1.5.0.tgz#57618e57942a5f0131374a9fdb0167e25a117fdc" + integrity sha512-bkUDCp8o1MvFO+qxkODcbhSqRa6P2GXgrGZVpt0dCXNW2HCSCqYI0ZoAqEOSAjRWmmlKcYgFvN4B4S+zo/f8kg== + +"@types/prop-types@*": + version "15.7.5" + resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.5.tgz#5f19d2b85a98e9558036f6a3cacc8819420f05cf" + integrity sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w== + +"@types/react@^18.0.34": + version "18.0.34" + resolved "https://registry.yarnpkg.com/@types/react/-/react-18.0.34.tgz#e553444a578f023e6e1ac499514688fb80b0a984" + integrity sha512-NO1UO8941541CJl1BeOXi8a9dNKFK09Gnru5ZJqkm4Q3/WoQJtHvmwt0VX0SB9YCEwe7TfSSxDuaNmx6H2BAIQ== + dependencies: + "@types/prop-types" "*" + "@types/scheduler" "*" + csstype "^3.0.2" + +"@types/scheduler@*": + version "0.16.3" + resolved "https://registry.yarnpkg.com/@types/scheduler/-/scheduler-0.16.3.tgz#cef09e3ec9af1d63d2a6cc5b383a737e24e6dcf5" + integrity sha512-5cJ8CB4yAx7BH1oMvdU0Jh9lrEXyPkar6F9G/ERswkCuvP4KQZfZkSjcMbAICCpQTN4OuZn8tz0HiKv9TGZgrQ== + +csstype@^3.0.2: + version "3.1.2" + resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.1.2.tgz#1d4bf9d572f11c14031f0436e1c10bc1f571f50b" + integrity sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ== + +"js-tokens@^3.0.0 || ^4.0.0": + version "4.0.0" + resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" + integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ== + +loose-envify@^1.1.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf" + integrity sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q== + dependencies: + js-tokens "^3.0.0 || ^4.0.0" + +react-dom@^18.2.0: + version "18.2.0" + resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-18.2.0.tgz#22aaf38708db2674ed9ada224ca4aa708d821e3d" + integrity sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g== + dependencies: + loose-envify "^1.1.0" + scheduler "^0.23.0" + +react-hook-form@^7.43.9: + version "7.43.9" + resolved "https://registry.yarnpkg.com/react-hook-form/-/react-hook-form-7.43.9.tgz#84b56ac2f38f8e946c6032ccb760e13a1037c66d" + integrity sha512-AUDN3Pz2NSeoxQ7Hs6OhQhDr6gtF9YRuutGDwPQqhSUAHJSgGl2VeY3qN19MG0SucpjgDiuMJ4iC5T5uB+eaNQ== + +react-router-dom@6.10.0: + version "6.10.0" + resolved "https://registry.yarnpkg.com/react-router-dom/-/react-router-dom-6.10.0.tgz#090ddc5c84dc41b583ce08468c4007c84245f61f" + integrity sha512-E5dfxRPuXKJqzwSe/qGcqdwa18QiWC6f3H3cWXM24qj4N0/beCIf/CWTipop2xm7mR0RCS99NnaqPNjHtrAzCg== + dependencies: + "@remix-run/router" "1.5.0" + react-router "6.10.0" + +react-router@6.10.0: + version "6.10.0" + resolved "https://registry.yarnpkg.com/react-router/-/react-router-6.10.0.tgz#230f824fde9dd0270781b5cb497912de32c0a971" + integrity sha512-Nrg0BWpQqrC3ZFFkyewrflCud9dio9ME3ojHCF/WLsprJVzkq3q3UeEhMCAW1dobjeGbWgjNn/PVF6m46ANxXQ== + dependencies: + "@remix-run/router" "1.5.0" + +react@^18.2.0: + version "18.2.0" + resolved "https://registry.yarnpkg.com/react/-/react-18.2.0.tgz#555bd98592883255fa00de14f1151a917b5d77d5" + integrity sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ== + dependencies: + loose-envify "^1.1.0" + +scheduler@^0.23.0: + version "0.23.0" + resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.23.0.tgz#ba8041afc3d30eb206a487b6b384002e4e61fdfe" + integrity sha512-CtuThmgHNg7zIZWAXi3AsyIzA3n4xx7aNyjwC2VJldO2LMVDhFK+63xGqq6CsJH4rTAt6/M+N4GhZiDYPx9eUw== + dependencies: + loose-envify "^1.1.0" + +typescript@^5.0.4: + version "5.0.4" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.0.4.tgz#b217fd20119bd61a94d4011274e0ab369058da3b" + integrity sha512-cW9T5W9xY37cc+jfEnaUvX91foxtHkza3Nw3wkoF4sSlKn0MONdkdEndig/qPBWXNkmplh3NzayQzCiHM4/hqw== + +use-sync-external-store@1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz#7dbefd6ef3fe4e767a0cf5d7287aacfb5846928a" + integrity sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==