Finish mobile form
This commit is contained in:
parent
0dc2abfdca
commit
012eeb3b8e
4 changed files with 147 additions and 51 deletions
16
package-lock.json
generated
16
package-lock.json
generated
|
@ -11,6 +11,7 @@
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"react": "^18.3.1",
|
"react": "^18.3.1",
|
||||||
"react-dom": "^18.3.1",
|
"react-dom": "^18.3.1",
|
||||||
|
"react-hook-form": "^7.51.5",
|
||||||
"tailwind-merge": "^2.3.0"
|
"tailwind-merge": "^2.3.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
@ -2489,6 +2490,21 @@
|
||||||
"react": "^18.3.1"
|
"react": "^18.3.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/react-hook-form": {
|
||||||
|
"version": "7.51.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.51.5.tgz",
|
||||||
|
"integrity": "sha512-J2ILT5gWx1XUIJRETiA7M19iXHlG74+6O3KApzvqB/w8S5NQR7AbU8HVZrMALdmDgWpRPYiZJl0zx8Z4L2mP6Q==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12.22.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/react-hook-form"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": "^16.8.0 || ^17 || ^18"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/read-cache": {
|
"node_modules/read-cache": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz",
|
||||||
|
|
|
@ -11,6 +11,7 @@
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"react": "^18.3.1",
|
"react": "^18.3.1",
|
||||||
"react-dom": "^18.3.1",
|
"react-dom": "^18.3.1",
|
||||||
|
"react-hook-form": "^7.51.5",
|
||||||
"tailwind-merge": "^2.3.0"
|
"tailwind-merge": "^2.3.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|
129
src/app.jsx
129
src/app.jsx
|
@ -2,6 +2,7 @@ import * as Checkbox from "@radix-ui/react-checkbox";
|
||||||
import * as RadioGroup from "@radix-ui/react-radio-group";
|
import * as RadioGroup from "@radix-ui/react-radio-group";
|
||||||
import * as Toast from "@radix-ui/react-toast";
|
import * as Toast from "@radix-ui/react-toast";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
|
import { Controller, useForm } from "react-hook-form";
|
||||||
|
|
||||||
import icon_checkbox_checked from "../assets/icon-checkbox-checked.svg";
|
import icon_checkbox_checked from "../assets/icon-checkbox-checked.svg";
|
||||||
import icon_checkbox_unchecked from "../assets/icon-checkbox-unchecked.svg";
|
import icon_checkbox_unchecked from "../assets/icon-checkbox-unchecked.svg";
|
||||||
|
@ -10,24 +11,42 @@ import icon_check from "../assets/check.svg";
|
||||||
import { Button } from "./components/button";
|
import { Button } from "./components/button";
|
||||||
import { Input } from "./components/input";
|
import { Input } from "./components/input";
|
||||||
|
|
||||||
const InputField = ({ label, type = "text", required = false, ...props }) => {
|
const InputField = ({
|
||||||
|
label,
|
||||||
|
type = "text",
|
||||||
|
required = false,
|
||||||
|
register,
|
||||||
|
errors,
|
||||||
|
...props
|
||||||
|
}) => {
|
||||||
|
const _label = label.toLowerCase().replace(/ /g, "_");
|
||||||
|
const error = errors[_label]?.type == "required";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<label className="group block cursor-pointer space-y-100">
|
<label className="group block cursor-pointer space-y-100 text-body-sm">
|
||||||
<div className="space-x-100 text-body-sm text-grey-900 group-focus-within:text-green-600">
|
<div className="space-x-100 text-grey-900 group-focus-within:text-green-600">
|
||||||
<span>{label}</span>
|
<span>{label}</span>
|
||||||
{required && <span className="text-green-600">*</span>}
|
{required && <span className="text-green-600">*</span>}
|
||||||
</div>
|
</div>
|
||||||
<Input
|
<Input
|
||||||
name={label.toLowerCase().replace(/ /g, "_")}
|
|
||||||
type={type}
|
type={type}
|
||||||
required={required}
|
{...register(_label, { required })}
|
||||||
|
aria-invalid={error}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
|
{error && <p className="text-red">This field is required</p>}
|
||||||
</label>
|
</label>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function App() {
|
export default function App() {
|
||||||
|
const {
|
||||||
|
register,
|
||||||
|
handleSubmit,
|
||||||
|
control,
|
||||||
|
formState: { errors },
|
||||||
|
} = useForm();
|
||||||
|
|
||||||
const [toastOpen, setToastOpen] = useState(false);
|
const [toastOpen, setToastOpen] = useState(false);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -35,13 +54,34 @@ export default function App() {
|
||||||
<Toast.Provider>
|
<Toast.Provider>
|
||||||
<Toast.Viewport className="left-0 top-0 fixed flex w-full justify-center p-300" />
|
<Toast.Viewport className="left-0 top-0 fixed flex w-full justify-center p-300" />
|
||||||
<div className="rounded-2xl bg-white p-300 text-grey-900">
|
<div className="rounded-2xl bg-white p-300 text-grey-900">
|
||||||
<form action="">
|
<form
|
||||||
|
onSubmit={handleSubmit((data) => {
|
||||||
|
console.log(data);
|
||||||
|
setToastOpen(true);
|
||||||
|
})}
|
||||||
|
>
|
||||||
<h1 className="text-heading">Contact Us</h1>
|
<h1 className="text-heading">Contact Us</h1>
|
||||||
|
|
||||||
<div className="mt-400 space-y-300">
|
<div className="mt-400 space-y-300">
|
||||||
<InputField label="First Name" required />
|
<InputField
|
||||||
<InputField label="Last Name" required />
|
register={register}
|
||||||
<InputField label="Email Address" type="email" required />
|
errors={errors}
|
||||||
|
label="First Name"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
<InputField
|
||||||
|
register={register}
|
||||||
|
errors={errors}
|
||||||
|
label="Last Name"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
<InputField
|
||||||
|
register={register}
|
||||||
|
errors={errors}
|
||||||
|
label="Email Address"
|
||||||
|
type="email"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
|
||||||
<div className="space-y-200">
|
<div className="space-y-200">
|
||||||
<div className="space-x-100">
|
<div className="space-x-100">
|
||||||
|
@ -49,7 +89,19 @@ export default function App() {
|
||||||
<span className="text-body-sm text-green-600">*</span>
|
<span className="text-body-sm text-green-600">*</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<RadioGroup.Root className="space-y-200" name="query_type">
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="query_type"
|
||||||
|
rules={{ required: true }}
|
||||||
|
render={({
|
||||||
|
field: { onChange, value },
|
||||||
|
fieldState: { invalid },
|
||||||
|
}) => (
|
||||||
|
<RadioGroup.Root
|
||||||
|
className="space-y-200"
|
||||||
|
value={value}
|
||||||
|
onValueChange={onChange}
|
||||||
|
>
|
||||||
{["General Enquiry", "Support Request"].map((label) => (
|
{["General Enquiry", "Support Request"].map((label) => (
|
||||||
<RadioGroup.Item
|
<RadioGroup.Item
|
||||||
className="group w-full rounded-lg border-[1px] border-grey-500 px-300 py-150 text-left hover:border-green-600 data-[state='checked']:border-green-600 data-[state='checked']:bg-green-200"
|
className="group w-full rounded-lg border-[1px] border-grey-500 px-300 py-150 text-left hover:border-green-600 data-[state='checked']:border-green-600 data-[state='checked']:bg-green-200"
|
||||||
|
@ -66,17 +118,43 @@ export default function App() {
|
||||||
</label>
|
</label>
|
||||||
</RadioGroup.Item>
|
</RadioGroup.Item>
|
||||||
))}
|
))}
|
||||||
|
{invalid && (
|
||||||
|
<p className="text-body-sm text-red">
|
||||||
|
Please select a query type
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
</RadioGroup.Root>
|
</RadioGroup.Root>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<InputField label="Message" type="textarea" rows="4" required />
|
<InputField
|
||||||
</div>
|
register={register}
|
||||||
|
errors={errors}
|
||||||
<label className="my-500 flex cursor-pointer items-center">
|
label="Message"
|
||||||
<Checkbox.Root
|
type="textarea"
|
||||||
className="size-300 flex-shrink-0"
|
rows="4"
|
||||||
style={{ backgroundImage: `url('${icon_checkbox_unchecked}')` }}
|
|
||||||
required
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="consent_given"
|
||||||
|
rules={{ validate: (value) => value }}
|
||||||
|
render={({
|
||||||
|
field: { onChange, value },
|
||||||
|
fieldState: { invalid },
|
||||||
|
}) => (
|
||||||
|
<div className="my-500 space-y-100 ">
|
||||||
|
<label className="flex cursor-pointer items-center">
|
||||||
|
<Checkbox.Root
|
||||||
|
checked={value}
|
||||||
|
onCheckedChange={onChange}
|
||||||
|
className="size-300 flex-shrink-0"
|
||||||
|
style={{
|
||||||
|
backgroundImage: `url('${icon_checkbox_unchecked}')`,
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<Checkbox.Indicator>
|
<Checkbox.Indicator>
|
||||||
<img src={icon_checkbox_checked} />
|
<img src={icon_checkbox_checked} />
|
||||||
|
@ -87,15 +165,16 @@ export default function App() {
|
||||||
<span className="text-green-600">*</span>
|
<span className="text-green-600">*</span>
|
||||||
</div>
|
</div>
|
||||||
</label>
|
</label>
|
||||||
|
{invalid && (
|
||||||
|
<p className="text-body-sm text-red">
|
||||||
|
To submit this form, please consent to being contacted
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
<Button
|
<Button type="submit">Submit</Button>
|
||||||
type="button"
|
|
||||||
onClick={() => {
|
|
||||||
setToastOpen((toastOpen) => !toastOpen);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Submit
|
|
||||||
</Button>
|
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
<Toast.Root
|
<Toast.Root
|
||||||
|
|
|
@ -6,7 +6,7 @@ const Input = React.forwardRef(({ type, className, ...props }, ref) => {
|
||||||
return (
|
return (
|
||||||
<Component
|
<Component
|
||||||
className={cn(
|
className={cn(
|
||||||
"block w-full cursor-pointer rounded-lg border-[1px] border-grey-500 px-300 py-150 text-body-md focus:border-green-600 group-hover:border-green-600",
|
"block w-full cursor-pointer rounded-lg border-[1px] border-grey-500 px-300 py-150 text-body-md focus:border-green-600 group-hover:border-green-600 aria-[invalid='true']:border-red",
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
type={type}
|
type={type}
|
||||||
|
|
Reference in a new issue