paameterize form using zod to get a type checked fields
This commit is contained in:
parent
a092b4e9c8
commit
1caaa247d8
4 changed files with 303 additions and 29 deletions
48
crawler/frontend/package-lock.json
generated
48
crawler/frontend/package-lock.json
generated
|
|
@ -8,6 +8,7 @@
|
||||||
"name": "wrongmove",
|
"name": "wrongmove",
|
||||||
"version": "0.0.0",
|
"version": "0.0.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@hookform/resolvers": "^5.1.1",
|
||||||
"@radix-ui/react-dialog": "^1.1.14",
|
"@radix-ui/react-dialog": "^1.1.14",
|
||||||
"@radix-ui/react-label": "^2.1.7",
|
"@radix-ui/react-label": "^2.1.7",
|
||||||
"@radix-ui/react-scroll-area": "^1.2.9",
|
"@radix-ui/react-scroll-area": "^1.2.9",
|
||||||
|
|
@ -30,10 +31,12 @@
|
||||||
"oidc-client-ts": "^3.2.1",
|
"oidc-client-ts": "^3.2.1",
|
||||||
"react": "^19.1.0",
|
"react": "^19.1.0",
|
||||||
"react-dom": "^19.1.0",
|
"react-dom": "^19.1.0",
|
||||||
|
"react-hook-form": "^7.58.1",
|
||||||
"react-oidc-context": "^3.3.0",
|
"react-oidc-context": "^3.3.0",
|
||||||
"rivets": "^0.9.6",
|
"rivets": "^0.9.6",
|
||||||
"tailwind-merge": "^3.3.1",
|
"tailwind-merge": "^3.3.1",
|
||||||
"tailwindcss": "^4.1.10"
|
"tailwindcss": "^4.1.10",
|
||||||
|
"zod": "^3.25.67"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/js": "^9.25.0",
|
"@eslint/js": "^9.25.0",
|
||||||
|
|
@ -670,6 +673,18 @@
|
||||||
"integrity": "sha512-MDWhGtE+eHw5JW7lq4qhc5yRLS11ERl1c7Z6Xd0a58DozHES6EnNNwUWbMiG4J9Cgj053Bhk8zvlhFYKVhULwg==",
|
"integrity": "sha512-MDWhGtE+eHw5JW7lq4qhc5yRLS11ERl1c7Z6Xd0a58DozHES6EnNNwUWbMiG4J9Cgj053Bhk8zvlhFYKVhULwg==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/@hookform/resolvers": {
|
||||||
|
"version": "5.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@hookform/resolvers/-/resolvers-5.1.1.tgz",
|
||||||
|
"integrity": "sha512-J/NVING3LMAEvexJkyTLjruSm7aOFx7QX21pzkiJfMoNG0wl5aFEjLTl7ay7IQb9EWY6AkrBy7tHL2Alijpdcg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@standard-schema/utils": "^0.3.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react-hook-form": "^7.55.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@humanfs/core": {
|
"node_modules/@humanfs/core": {
|
||||||
"version": "0.19.1",
|
"version": "0.19.1",
|
||||||
"resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz",
|
"resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz",
|
||||||
|
|
@ -1824,6 +1839,12 @@
|
||||||
"win32"
|
"win32"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"node_modules/@standard-schema/utils": {
|
||||||
|
"version": "0.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@standard-schema/utils/-/utils-0.3.0.tgz",
|
||||||
|
"integrity": "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/@swc/core": {
|
"node_modules/@swc/core": {
|
||||||
"version": "1.12.1",
|
"version": "1.12.1",
|
||||||
"resolved": "https://registry.npmjs.org/@swc/core/-/core-1.12.1.tgz",
|
"resolved": "https://registry.npmjs.org/@swc/core/-/core-1.12.1.tgz",
|
||||||
|
|
@ -5041,6 +5062,22 @@
|
||||||
"react": "^19.1.0"
|
"react": "^19.1.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/react-hook-form": {
|
||||||
|
"version": "7.58.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.58.1.tgz",
|
||||||
|
"integrity": "sha512-Lml/KZYEEFfPhUVgE0RdCVpnC4yhW+PndRhbiTtdvSlQTL8IfVR+iQkBjLIvmmc6+GGoVeM11z37ktKFPAb0FA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/react-hook-form"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": "^16.8.0 || ^17 || ^18 || ^19"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/react-oidc-context": {
|
"node_modules/react-oidc-context": {
|
||||||
"version": "3.3.0",
|
"version": "3.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/react-oidc-context/-/react-oidc-context-3.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/react-oidc-context/-/react-oidc-context-3.3.0.tgz",
|
||||||
|
|
@ -5754,6 +5791,15 @@
|
||||||
"funding": {
|
"funding": {
|
||||||
"url": "https://github.com/sponsors/sindresorhus"
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"node_modules/zod": {
|
||||||
|
"version": "3.25.67",
|
||||||
|
"resolved": "https://registry.npmjs.org/zod/-/zod-3.25.67.tgz",
|
||||||
|
"integrity": "sha512-idA2YXwpCdqUSKRCACDE6ItZD9TZzy3OZMtpfLoh6oPR47lipysRrJfjzMqFxQ3uJuUPyUeWe1r9vLH33xO/Qw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/colinhacks"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@
|
||||||
"preview": "vite preview"
|
"preview": "vite preview"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@hookform/resolvers": "^5.1.1",
|
||||||
"@radix-ui/react-dialog": "^1.1.14",
|
"@radix-ui/react-dialog": "^1.1.14",
|
||||||
"@radix-ui/react-label": "^2.1.7",
|
"@radix-ui/react-label": "^2.1.7",
|
||||||
"@radix-ui/react-scroll-area": "^1.2.9",
|
"@radix-ui/react-scroll-area": "^1.2.9",
|
||||||
|
|
@ -32,10 +33,12 @@
|
||||||
"oidc-client-ts": "^3.2.1",
|
"oidc-client-ts": "^3.2.1",
|
||||||
"react": "^19.1.0",
|
"react": "^19.1.0",
|
||||||
"react-dom": "^19.1.0",
|
"react-dom": "^19.1.0",
|
||||||
|
"react-hook-form": "^7.58.1",
|
||||||
"react-oidc-context": "^3.3.0",
|
"react-oidc-context": "^3.3.0",
|
||||||
"rivets": "^0.9.6",
|
"rivets": "^0.9.6",
|
||||||
"tailwind-merge": "^3.3.1",
|
"tailwind-merge": "^3.3.1",
|
||||||
"tailwindcss": "^4.1.10"
|
"tailwindcss": "^4.1.10",
|
||||||
|
"zod": "^3.25.67"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/js": "^9.25.0",
|
"@eslint/js": "^9.25.0",
|
||||||
|
|
|
||||||
|
|
@ -1,40 +1,100 @@
|
||||||
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
|
import { DialogTitle } from "@radix-ui/react-dialog";
|
||||||
|
import { useForm } from "react-hook-form";
|
||||||
|
import { z } from "zod";
|
||||||
import { Button } from "./ui/button";
|
import { Button } from "./ui/button";
|
||||||
import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle, DialogTrigger } from "./ui/dialog";
|
import { Dialog, DialogContent, DialogTrigger } from "./ui/dialog";
|
||||||
|
import { Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage } from "./ui/form";
|
||||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "./ui/select";
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "./ui/select";
|
||||||
|
|
||||||
|
|
||||||
|
enum Metric {
|
||||||
|
qmprice = 'Price per square meter',
|
||||||
|
rooms = 'Number of rooms',
|
||||||
|
qm = 'Area',
|
||||||
|
price = 'Price',
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
interface ParameterValues {
|
||||||
|
metric: Metric
|
||||||
|
}
|
||||||
|
|
||||||
export function Parameters(
|
export function Parameters(
|
||||||
props: {
|
props: {
|
||||||
isOpen: boolean,
|
isOpen: boolean,
|
||||||
onSubmit: () => void,
|
onSubmit: (fromValues: ParameterValues) => void,
|
||||||
}
|
}
|
||||||
) {
|
) {
|
||||||
|
const {
|
||||||
|
register,
|
||||||
|
watch,
|
||||||
|
formState: { errors },
|
||||||
|
} = useForm<ParameterValues>()
|
||||||
|
// const onSubmit: SubmitHandler<ParameterValues> = (data) => console.log(data)
|
||||||
|
console.log(watch("metric"))
|
||||||
|
console.log(errors)
|
||||||
|
|
||||||
|
const formSchema = z.object({
|
||||||
|
metric: z.nativeEnum(Metric, { required_error: "Metric is required" }),
|
||||||
|
})
|
||||||
|
const form = useForm<z.infer<typeof formSchema>>({
|
||||||
|
resolver: zodResolver(formSchema),
|
||||||
|
defaultValues: {
|
||||||
|
metric: Metric.qmprice,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
// 2. Define a submit handler.
|
||||||
|
function onSubmit(values: z.infer<typeof formSchema>) {
|
||||||
|
// Do something with the form values.
|
||||||
|
// ✅ This will be type-safe and validated.
|
||||||
|
console.log(values)
|
||||||
|
props.onSubmit(values)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
return <>
|
return <>
|
||||||
{/* <Dialog open={props.isOpen}> */}
|
|
||||||
<Dialog >
|
<Dialog >
|
||||||
<form>
|
<DialogTrigger asChild>
|
||||||
<DialogTrigger asChild>
|
<Button variant="outline">Open Parameters</Button>
|
||||||
<Button variant="outline">Open Parameters</Button>
|
</DialogTrigger>
|
||||||
</DialogTrigger>
|
<DialogContent className="sm:max-w-[425px]">
|
||||||
<DialogContent className="sm:max-w-[425px]">
|
<DialogTitle>
|
||||||
<DialogHeader>
|
Visualization Parameters
|
||||||
<DialogTitle>Parameters</DialogTitle>
|
</DialogTitle>
|
||||||
</DialogHeader>
|
|
||||||
<Select>
|
<Form {...form}>
|
||||||
<SelectTrigger className="w-[180px]">
|
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-8">
|
||||||
<SelectValue placeholder="Metric to Visualize" />
|
<FormField
|
||||||
</SelectTrigger>
|
control={form.control}
|
||||||
<SelectContent>
|
name="metric"
|
||||||
<SelectItem value="qmprice">Price per squaremeter</SelectItem>
|
render={({ field }) => (
|
||||||
<SelectItem value="rooms">Number of rooms</SelectItem>
|
<FormItem>
|
||||||
<SelectItem value="qm">Area</SelectItem>
|
<FormLabel>Metric to visualize</FormLabel>
|
||||||
<SelectItem value="total_price">Price</SelectItem>
|
<Select onValueChange={field.onChange} defaultValue={field.value}>
|
||||||
</SelectContent>
|
<FormControl>
|
||||||
</Select>
|
<SelectTrigger className="w-[180px]">
|
||||||
<DialogFooter>
|
<SelectValue placeholder="Metric to Visualize" />
|
||||||
<Button onClick={() => props.onSubmit()}>Fetch Data</Button>
|
</SelectTrigger>
|
||||||
</DialogFooter>
|
</FormControl>
|
||||||
</DialogContent>
|
<SelectContent {...register('metric')} >
|
||||||
</form>
|
<SelectItem value={Metric.qmprice}>Price per squaremeter</SelectItem>
|
||||||
|
<SelectItem value={Metric.rooms}>Number of rooms</SelectItem>
|
||||||
|
<SelectItem value={Metric.qm}>Area</SelectItem>
|
||||||
|
<SelectItem value={Metric.price}>Price</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
<FormDescription>
|
||||||
|
This is your public display name.
|
||||||
|
</FormDescription>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<Button type="submit">Submit</Button>
|
||||||
|
</form>
|
||||||
|
</Form>
|
||||||
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
|
|
|
||||||
165
crawler/frontend/src/components/ui/form.tsx
Normal file
165
crawler/frontend/src/components/ui/form.tsx
Normal file
|
|
@ -0,0 +1,165 @@
|
||||||
|
import * as React from "react"
|
||||||
|
import * as LabelPrimitive from "@radix-ui/react-label"
|
||||||
|
import { Slot } from "@radix-ui/react-slot"
|
||||||
|
import {
|
||||||
|
Controller,
|
||||||
|
FormProvider,
|
||||||
|
useFormContext,
|
||||||
|
useFormState,
|
||||||
|
type ControllerProps,
|
||||||
|
type FieldPath,
|
||||||
|
type FieldValues,
|
||||||
|
} from "react-hook-form"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
import { Label } from "@/components/ui/label"
|
||||||
|
|
||||||
|
const Form = FormProvider
|
||||||
|
|
||||||
|
type FormFieldContextValue<
|
||||||
|
TFieldValues extends FieldValues = FieldValues,
|
||||||
|
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
|
||||||
|
> = {
|
||||||
|
name: TName
|
||||||
|
}
|
||||||
|
|
||||||
|
const FormFieldContext = React.createContext<FormFieldContextValue>(
|
||||||
|
{} as FormFieldContextValue
|
||||||
|
)
|
||||||
|
|
||||||
|
const FormField = <
|
||||||
|
TFieldValues extends FieldValues = FieldValues,
|
||||||
|
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
|
||||||
|
>({
|
||||||
|
...props
|
||||||
|
}: ControllerProps<TFieldValues, TName>) => {
|
||||||
|
return (
|
||||||
|
<FormFieldContext.Provider value={{ name: props.name }}>
|
||||||
|
<Controller {...props} />
|
||||||
|
</FormFieldContext.Provider>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const useFormField = () => {
|
||||||
|
const fieldContext = React.useContext(FormFieldContext)
|
||||||
|
const itemContext = React.useContext(FormItemContext)
|
||||||
|
const { getFieldState } = useFormContext()
|
||||||
|
const formState = useFormState({ name: fieldContext.name })
|
||||||
|
const fieldState = getFieldState(fieldContext.name, formState)
|
||||||
|
|
||||||
|
if (!fieldContext) {
|
||||||
|
throw new Error("useFormField should be used within <FormField>")
|
||||||
|
}
|
||||||
|
|
||||||
|
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<FormItemContextValue>(
|
||||||
|
{} as FormItemContextValue
|
||||||
|
)
|
||||||
|
|
||||||
|
function FormItem({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
|
const id = React.useId()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FormItemContext.Provider value={{ id }}>
|
||||||
|
<div
|
||||||
|
data-slot="form-item"
|
||||||
|
className={cn("grid gap-2", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
</FormItemContext.Provider>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function FormLabel({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof LabelPrimitive.Root>) {
|
||||||
|
const { error, formItemId } = useFormField()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Label
|
||||||
|
data-slot="form-label"
|
||||||
|
data-error={!!error}
|
||||||
|
className={cn("data-[error=true]:text-destructive", className)}
|
||||||
|
htmlFor={formItemId}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function FormControl({ ...props }: React.ComponentProps<typeof Slot>) {
|
||||||
|
const { error, formItemId, formDescriptionId, formMessageId } = useFormField()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Slot
|
||||||
|
data-slot="form-control"
|
||||||
|
id={formItemId}
|
||||||
|
aria-describedby={
|
||||||
|
!error
|
||||||
|
? `${formDescriptionId}`
|
||||||
|
: `${formDescriptionId} ${formMessageId}`
|
||||||
|
}
|
||||||
|
aria-invalid={!!error}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function FormDescription({ className, ...props }: React.ComponentProps<"p">) {
|
||||||
|
const { formDescriptionId } = useFormField()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<p
|
||||||
|
data-slot="form-description"
|
||||||
|
id={formDescriptionId}
|
||||||
|
className={cn("text-muted-foreground text-sm", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function FormMessage({ className, ...props }: React.ComponentProps<"p">) {
|
||||||
|
const { error, formMessageId } = useFormField()
|
||||||
|
const body = error ? String(error?.message ?? "") : props.children
|
||||||
|
|
||||||
|
if (!body) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<p
|
||||||
|
data-slot="form-message"
|
||||||
|
id={formMessageId}
|
||||||
|
className={cn("text-destructive text-sm", className)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{body}
|
||||||
|
</p>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
useFormField,
|
||||||
|
Form,
|
||||||
|
FormItem,
|
||||||
|
FormLabel,
|
||||||
|
FormControl,
|
||||||
|
FormDescription,
|
||||||
|
FormMessage,
|
||||||
|
FormField,
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue