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",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"react-hook-form": "^7.51.5",
|
||||
"tailwind-merge": "^2.3.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
@ -2489,6 +2490,21 @@
|
|||
"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": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz",
|
||||
|
|
|
@ -11,6 +11,7 @@
|
|||
"clsx": "^2.1.1",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"react-hook-form": "^7.51.5",
|
||||
"tailwind-merge": "^2.3.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
|
179
src/app.jsx
179
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 Toast from "@radix-ui/react-toast";
|
||||
import { useState } from "react";
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
|
||||
import icon_checkbox_checked from "../assets/icon-checkbox-checked.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 { 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 (
|
||||
<label className="group block cursor-pointer space-y-100">
|
||||
<div className="space-x-100 text-body-sm text-grey-900 group-focus-within:text-green-600">
|
||||
<label className="group block cursor-pointer space-y-100 text-body-sm">
|
||||
<div className="space-x-100 text-grey-900 group-focus-within:text-green-600">
|
||||
<span>{label}</span>
|
||||
{required && <span className="text-green-600">*</span>}
|
||||
</div>
|
||||
<Input
|
||||
name={label.toLowerCase().replace(/ /g, "_")}
|
||||
type={type}
|
||||
required={required}
|
||||
{...register(_label, { required })}
|
||||
aria-invalid={error}
|
||||
{...props}
|
||||
/>
|
||||
{error && <p className="text-red">This field is required</p>}
|
||||
</label>
|
||||
);
|
||||
};
|
||||
|
||||
export default function App() {
|
||||
const {
|
||||
register,
|
||||
handleSubmit,
|
||||
control,
|
||||
formState: { errors },
|
||||
} = useForm();
|
||||
|
||||
const [toastOpen, setToastOpen] = useState(false);
|
||||
|
||||
return (
|
||||
|
@ -35,13 +54,34 @@ export default function App() {
|
|||
<Toast.Provider>
|
||||
<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">
|
||||
<form action="">
|
||||
<form
|
||||
onSubmit={handleSubmit((data) => {
|
||||
console.log(data);
|
||||
setToastOpen(true);
|
||||
})}
|
||||
>
|
||||
<h1 className="text-heading">Contact Us</h1>
|
||||
|
||||
<div className="mt-400 space-y-300">
|
||||
<InputField label="First Name" required />
|
||||
<InputField label="Last Name" required />
|
||||
<InputField label="Email Address" type="email" required />
|
||||
<InputField
|
||||
register={register}
|
||||
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-x-100">
|
||||
|
@ -49,53 +89,92 @@ export default function App() {
|
|||
<span className="text-body-sm text-green-600">*</span>
|
||||
</div>
|
||||
|
||||
<RadioGroup.Root className="space-y-200" name="query_type">
|
||||
{["General Enquiry", "Support Request"].map((label) => (
|
||||
<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"
|
||||
value={label.toLowerCase().replace(/ /g, "_")}
|
||||
key={label}
|
||||
<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}
|
||||
>
|
||||
<label className="flex cursor-pointer items-center justify-start gap-150 hover:text-green-600 group-data-[state='checked']:text-green-600">
|
||||
<span className="relative size-300 rounded-full border-2 border-solid border-grey-500 bg-transparent group-data-[state='checked']:border-green-600">
|
||||
<span className="absolute left-1/2 top-1/2 size-150 -translate-x-1/2 -translate-y-1/2 transform rounded-full bg-transparent group-data-[state='checked']:bg-green-600"></span>
|
||||
</span>
|
||||
<span className="text-body-md text-grey-900 group-hover:text-green-600 group-data-[state='checked']:text-green-600">
|
||||
{label}
|
||||
</span>
|
||||
</label>
|
||||
</RadioGroup.Item>
|
||||
))}
|
||||
</RadioGroup.Root>
|
||||
{["General Enquiry", "Support Request"].map((label) => (
|
||||
<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"
|
||||
value={label.toLowerCase().replace(/ /g, "_")}
|
||||
key={label}
|
||||
>
|
||||
<label className="flex cursor-pointer items-center justify-start gap-150 hover:text-green-600 group-data-[state='checked']:text-green-600">
|
||||
<span className="relative size-300 rounded-full border-2 border-solid border-grey-500 bg-transparent group-data-[state='checked']:border-green-600">
|
||||
<span className="absolute left-1/2 top-1/2 size-150 -translate-x-1/2 -translate-y-1/2 transform rounded-full bg-transparent group-data-[state='checked']:bg-green-600"></span>
|
||||
</span>
|
||||
<span className="text-body-md text-grey-900 group-hover:text-green-600 group-data-[state='checked']:text-green-600">
|
||||
{label}
|
||||
</span>
|
||||
</label>
|
||||
</RadioGroup.Item>
|
||||
))}
|
||||
{invalid && (
|
||||
<p className="text-body-sm text-red">
|
||||
Please select a query type
|
||||
</p>
|
||||
)}
|
||||
</RadioGroup.Root>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<InputField label="Message" type="textarea" rows="4" required />
|
||||
<InputField
|
||||
register={register}
|
||||
errors={errors}
|
||||
label="Message"
|
||||
type="textarea"
|
||||
rows="4"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<label className="my-500 flex cursor-pointer items-center">
|
||||
<Checkbox.Root
|
||||
className="size-300 flex-shrink-0"
|
||||
style={{ backgroundImage: `url('${icon_checkbox_unchecked}')` }}
|
||||
required
|
||||
>
|
||||
<Checkbox.Indicator>
|
||||
<img src={icon_checkbox_checked} />
|
||||
</Checkbox.Indicator>
|
||||
</Checkbox.Root>
|
||||
<div className="mx-200 space-x-100 text-body-sm text-grey-900">
|
||||
<span>I consent to being contacted by the team</span>
|
||||
<span className="text-green-600">*</span>
|
||||
</div>
|
||||
</label>
|
||||
<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>
|
||||
<img src={icon_checkbox_checked} />
|
||||
</Checkbox.Indicator>
|
||||
</Checkbox.Root>
|
||||
<div className="mx-200 space-x-100 text-body-sm text-grey-900">
|
||||
<span>I consent to being contacted by the team</span>
|
||||
<span className="text-green-600">*</span>
|
||||
</div>
|
||||
</label>
|
||||
{invalid && (
|
||||
<p className="text-body-sm text-red">
|
||||
To submit this form, please consent to being contacted
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
|
||||
<Button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setToastOpen((toastOpen) => !toastOpen);
|
||||
}}
|
||||
>
|
||||
Submit
|
||||
</Button>
|
||||
<Button type="submit">Submit</Button>
|
||||
</form>
|
||||
</div>
|
||||
<Toast.Root
|
||||
|
|
|
@ -6,7 +6,7 @@ const Input = React.forwardRef(({ type, className, ...props }, ref) => {
|
|||
return (
|
||||
<Component
|
||||
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,
|
||||
)}
|
||||
type={type}
|
||||
|
|
Reference in a new issue