diff --git a/index.html b/index.html index f0e64d1..1459491 100644 --- a/index.html +++ b/index.html @@ -3,7 +3,27 @@ - JSoD - JS on Demand + + JSoD + + + + + + + + + + diff --git a/package.json b/package.json index 26b0128..a737e3f 100644 --- a/package.json +++ b/package.json @@ -24,6 +24,7 @@ "@codemirror/lint": "^6.8.1", "@fontsource/ibm-plex-mono": "^5.1.0", "@fontsource/ibm-plex-sans": "^5.1.0", + "@hookform/resolvers": "^3.9.1", "@jitl/quickjs-ng-wasmfile-release-sync": "0.31.0", "@lezer/highlight": "^1.2.1", "@radix-ui/react-accordion": "^1.2.1", @@ -60,6 +61,8 @@ "quickjs-emscripten-sync": "^1.5.2", "react": "^18.2.0", "react-dom": "^18.2.0", + "react-helmet-async": "^2.0.5", + "react-hook-form": "^7.53.1", "react-hotkeys-hook": "^4.5.1", "react-resizable-panels": "^2.1.6", "react-virtuoso": "^4.12.0", @@ -67,6 +70,7 @@ "sonner": "^1.5.0", "tailwind-merge": "^2.5.4", "wouter": "^3.3.5", + "zod": "^3.23.8", "zustand": "^5.0.0" }, "devDependencies": { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a3f4cec..0c87f0e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -20,6 +20,9 @@ importers: '@fontsource/ibm-plex-sans': specifier: ^5.1.0 version: 5.1.0 + '@hookform/resolvers': + specifier: ^3.9.1 + version: 3.9.1(react-hook-form@7.53.1(react@18.3.1)) '@jitl/quickjs-ng-wasmfile-release-sync': specifier: 0.31.0 version: 0.31.0 @@ -128,6 +131,12 @@ importers: react-dom: specifier: ^18.2.0 version: 18.3.1(react@18.3.1) + react-helmet-async: + specifier: ^2.0.5 + version: 2.0.5(react@18.3.1) + react-hook-form: + specifier: ^7.53.1 + version: 7.53.1(react@18.3.1) react-hotkeys-hook: specifier: ^4.5.1 version: 4.5.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -149,6 +158,9 @@ importers: wouter: specifier: ^3.3.5 version: 3.3.5(react@18.3.1) + zod: + specifier: ^3.23.8 + version: 3.23.8 zustand: specifier: ^5.0.0 version: 5.0.0(@types/react@18.3.12)(react@18.3.1)(use-sync-external-store@1.2.2(react@18.3.1)) @@ -1231,6 +1243,11 @@ packages: '@fontsource/ibm-plex-sans@5.1.0': resolution: {integrity: sha512-v2aFHGh33ogG+At6dVNUCX6vWlNAhQ6STWj5WrBKPxVWX1SsAnHNq8sXQBa7WHEt29Irmozuk7GTp6GzFlpwdQ==} + '@hookform/resolvers@3.9.1': + resolution: {integrity: sha512-ud2HqmGBM0P0IABqoskKWI6PEf6ZDDBZkFqe2Vnl+mTHCEHzr3ISjjZyCwTjC/qpL25JC9aIDkloQejvMeq0ug==} + peerDependencies: + react-hook-form: ^7.0.0 + '@humanfs/core@0.19.0': resolution: {integrity: sha512-2cbWIHbZVEweE853g8jymffCA+NCMiuqeECeBBLm8dg2oFdjuGJhgN4UAbI+6v0CKbbhvtXA4qV8YR5Ji86nmw==} engines: {node: '>=18.18.0'} @@ -3837,6 +3854,20 @@ packages: peerDependencies: react: ^18.3.1 + react-fast-compare@3.2.2: + resolution: {integrity: sha512-nsO+KSNgo1SbJqJEYRE9ERzo7YtYbou/OqjSQKxV7jcKox7+usiUVZOAC+XnDOABXggQTno0Y1CpVnuWEc1boQ==} + + react-helmet-async@2.0.5: + resolution: {integrity: sha512-rYUYHeus+i27MvFE+Jaa4WsyBKGkL6qVgbJvSBoX8mbsWoABJXdEO0bZyi0F6i+4f0NuIb8AvqPMj3iXFHkMwg==} + peerDependencies: + react: ^16.6.0 || ^17.0.0 || ^18.0.0 + + react-hook-form@7.53.1: + resolution: {integrity: sha512-6aiQeBda4zjcuaugWvim9WsGqisoUk+etmFEsSUMm451/Ic8L/UAb7sRtMj3V+Hdzm6mMjU1VhiSzYUZeBm0Vg==} + engines: {node: '>=18.0.0'} + peerDependencies: + react: ^16.8.0 || ^17 || ^18 || ^19 + react-hotkeys-hook@4.5.1: resolution: {integrity: sha512-scAEJOh3Irm0g95NIn6+tQVf/OICCjsQsC9NBHfQws/Vxw4sfq1tDQut5fhTEvPraXhu/sHxRd9lOtxzyYuNAg==} peerDependencies: @@ -4042,6 +4073,9 @@ packages: resolution: {integrity: sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==} engines: {node: '>= 0.4'} + shallowequal@1.1.0: + resolution: {integrity: sha512-y0m1JoUZSlPAjXVtPPW70aZWfIL/dSP7AFkRnniLCrK/8MDKog3TySTBmckD+RObVxH0v4Tox67+F14PdED2oQ==} + shebang-command@2.0.0: resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} engines: {node: '>=8'} @@ -6046,6 +6080,10 @@ snapshots: '@fontsource/ibm-plex-sans@5.1.0': {} + '@hookform/resolvers@3.9.1(react-hook-form@7.53.1(react@18.3.1))': + dependencies: + react-hook-form: 7.53.1(react@18.3.1) + '@humanfs/core@0.19.0': {} '@humanfs/node@0.16.5': @@ -8765,6 +8803,19 @@ snapshots: react: 18.3.1 scheduler: 0.23.2 + react-fast-compare@3.2.2: {} + + react-helmet-async@2.0.5(react@18.3.1): + dependencies: + invariant: 2.2.4 + react: 18.3.1 + react-fast-compare: 3.2.2 + shallowequal: 1.1.0 + + react-hook-form@7.53.1(react@18.3.1): + dependencies: + react: 18.3.1 + react-hotkeys-hook@4.5.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1): dependencies: react: 18.3.1 @@ -8991,6 +9042,8 @@ snapshots: functions-have-names: 1.2.3 has-property-descriptors: 1.0.2 + shallowequal@1.1.0: {} + shebang-command@2.0.0: dependencies: shebang-regex: 3.0.0 diff --git a/src/app.tsx b/src/app.tsx index 67ae4a4..e038dc9 100644 --- a/src/app.tsx +++ b/src/app.tsx @@ -1,3 +1,5 @@ +import { HelmetProvider } from 'react-helmet-async'; + import { Toaster } from '@/components/ui/sonner'; import { ThemeProvider } from '@/providers/theme-provider'; import Router from '@/routes'; @@ -5,12 +7,14 @@ import Router from '@/routes'; import { PromptProvider } from './providers/prompt-provider'; const App = () => ( - - - - - - + + + + + + + + ); export default App; diff --git a/src/components/ui/form.tsx b/src/components/ui/form.tsx new file mode 100644 index 0000000..69cc7d5 --- /dev/null +++ b/src/components/ui/form.tsx @@ -0,0 +1,171 @@ +import type * as LabelPrimitive from '@radix-ui/react-label'; +import { Slot } from '@radix-ui/react-slot'; +import * as React from 'react'; +import type { ControllerProps, FieldPath, FieldValues } from 'react-hook-form'; +import { Controller, FormProvider, useFormContext } from 'react-hook-form'; + +import { Label } from '@/components/ui/label'; +import { cn } from '@/lib/utils'; + +const Form = FormProvider; + +type FormFieldContextValue< + TFieldValues extends FieldValues = FieldValues, + TName extends FieldPath = FieldPath, +> = { + name: TName; +}; + +const FormFieldContext = React.createContext( + {} as FormFieldContextValue, +); + +const FormField = < + TFieldValues extends FieldValues = FieldValues, + TName extends FieldPath = FieldPath, +>({ + ...props +}: ControllerProps) => { + return ( + + + + ); +}; + +const useFormField = () => { + const fieldContext = React.useContext(FormFieldContext); + const itemContext = React.useContext(FormItemContext); + const { getFieldState, formState } = useFormContext(); + + const fieldState = getFieldState(fieldContext.name, formState); + + if (!fieldContext) { + throw new Error('useFormField should be used within '); + } + + const { id } = itemContext; + + return { + id, + name: fieldContext.name, + formItemId: `${id}-form-item`, + formDescriptionId: `${id}-form-item-description`, + formMessageId: `${id}-form-item-message`, + ...fieldState, + }; +}; + +type FormItemContextValue = { + id: string; +}; + +const FormItemContext = React.createContext( + {} as FormItemContextValue, +); + +const FormItem = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => { + const id = React.useId(); + + return ( + +
+ + ); +}); +FormItem.displayName = 'FormItem'; + +const FormLabel = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => { + const { error, formItemId } = useFormField(); + + return ( +