From c4c8c345793a9a507b6eb5b707597f64d836fe20 Mon Sep 17 00:00:00 2001 From: mehedi-hasan Date: Wed, 24 Apr 2024 00:58:49 +0600 Subject: [PATCH] feat: add phone input field in profile form --- package-lock.json | 54 ++++++ package.json | 2 + .../user-profile-stepper/create-profile.tsx | 26 ++- src/components/phone-input.tsx | 167 ++++++++++++++++++ src/components/task-page.tsx | 2 +- src/components/ui/command.tsx | 155 ++++++++++++++++ src/lib/form-schema.ts | 21 +-- 7 files changed, 409 insertions(+), 18 deletions(-) create mode 100644 src/components/phone-input.tsx create mode 100644 src/components/ui/command.tsx diff --git a/package-lock.json b/package-lock.json index 58f5edb..29182ee 100644 --- a/package-lock.json +++ b/package-lock.json @@ -44,6 +44,7 @@ "axios": "^1.6.8", "class-variance-authority": "^0.7.0", "clsx": "^2.0.0", + "cmdk": "^1.0.0", "contentlayer": "^0.3.4", "eslint": "8.48.0", "eslint-config-next": "^14.0.1", @@ -61,6 +62,7 @@ "react-error-boundary": "^4.0.13", "react-error-overlay": "^6.0.11", "react-hook-form": "^7.47.0", + "react-phone-number-input": "^3.4.0", "react-resizable-panels": "^2.0.16", "rehype-pretty-code": "^0.13.1", "sharp": "^0.32.5", @@ -4414,6 +4416,11 @@ "node": ">=6" } }, + "node_modules/classnames": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.5.1.tgz", + "integrity": "sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==" + }, "node_modules/client-only": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz", @@ -4515,6 +4522,19 @@ "node": ">=6" } }, + "node_modules/cmdk": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/cmdk/-/cmdk-1.0.0.tgz", + "integrity": "sha512-gDzVf0a09TvoJ5jnuPvygTB77+XdOSwEmJ88L6XPFPlv7T3RxbP9jgenfylrAMD0+Le1aO0nVjQUzl2g+vjz5Q==", + "dependencies": { + "@radix-ui/react-dialog": "1.0.5", + "@radix-ui/react-primitive": "1.0.3" + }, + "peerDependencies": { + "react": "^18.0.0", + "react-dom": "^18.0.0" + } + }, "node_modules/color": { "version": "4.2.3", "resolved": "https://registry.npmjs.org/color/-/color-4.2.3.tgz", @@ -4646,6 +4666,11 @@ "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==" }, + "node_modules/country-flag-icons": { + "version": "1.5.11", + "resolved": "https://registry.npmjs.org/country-flag-icons/-/country-flag-icons-1.5.11.tgz", + "integrity": "sha512-B+mvFywunkRJs270k7kCBjhogvIA0uNn6GAXv6m2cPn3rrwqZzZVr2gBWcz+Cz7OGVWlcbERlYRIX0S6OGr8Bw==" + }, "node_modules/cross-spawn": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", @@ -6947,6 +6972,14 @@ "resolved": "https://registry.npmjs.org/inline-style-parser/-/inline-style-parser-0.1.1.tgz", "integrity": "sha512-7NXolsK4CAS5+xvdj5OMMbI962hU/wvwoxk+LWR9Ek9bVtyuuYScDN6eS0rUm6TxApFpw7CX1o4uJzcd4AyD3Q==" }, + "node_modules/input-format": { + "version": "0.3.10", + "resolved": "https://registry.npmjs.org/input-format/-/input-format-0.3.10.tgz", + "integrity": "sha512-5cFv/kOZD7Ch0viprVkuYPDkAU7HBZYBx8QrIpQ6yXUWbAQ0+RQ8IIojDJOf/RO6FDJLL099HDSK2KoVZ2zevg==", + "dependencies": { + "prop-types": "^15.8.1" + } + }, "node_modules/internal-slot": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.7.tgz", @@ -7572,6 +7605,11 @@ "node": ">= 0.8.0" } }, + "node_modules/libphonenumber-js": { + "version": "1.10.61", + "resolved": "https://registry.npmjs.org/libphonenumber-js/-/libphonenumber-js-1.10.61.tgz", + "integrity": "sha512-TsQsyzDttDvvzWNkbp/i0fVbzTGJIG0mUu/uNalIaRQEYeJxVQ/FPg+EJgSqfSXezREjM0V3RZ8cLVsKYhhw0Q==" + }, "node_modules/lilconfig": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.1.0.tgz", @@ -13610,6 +13648,22 @@ "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" }, + "node_modules/react-phone-number-input": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/react-phone-number-input/-/react-phone-number-input-3.4.0.tgz", + "integrity": "sha512-anL8OAqlSnOXd6O+lidkprOO5+OpgW+ODrbfyLc6u8lOX8ghT0nO6ZOPrGjotpZND4cr0xxH+vu3dgbdUB2lBA==", + "dependencies": { + "classnames": "^2.5.1", + "country-flag-icons": "^1.5.11", + "input-format": "^0.3.10", + "libphonenumber-js": "^1.10.61", + "prop-types": "^15.8.1" + }, + "peerDependencies": { + "react": ">=16.8", + "react-dom": ">=16.8" + } + }, "node_modules/react-remove-scroll": { "version": "2.5.5", "resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.5.5.tgz", diff --git a/package.json b/package.json index f180cf8..5be2ba4 100644 --- a/package.json +++ b/package.json @@ -46,6 +46,7 @@ "axios": "^1.6.8", "class-variance-authority": "^0.7.0", "clsx": "^2.0.0", + "cmdk": "^1.0.0", "contentlayer": "^0.3.4", "eslint": "8.48.0", "eslint-config-next": "^14.0.1", @@ -63,6 +64,7 @@ "react-error-boundary": "^4.0.13", "react-error-overlay": "^6.0.11", "react-hook-form": "^7.47.0", + "react-phone-number-input": "^3.4.0", "react-resizable-panels": "^2.0.16", "rehype-pretty-code": "^0.13.1", "sharp": "^0.32.5", diff --git a/src/components/forms/user-profile-stepper/create-profile.tsx b/src/components/forms/user-profile-stepper/create-profile.tsx index 760e744..0e9cc97 100644 --- a/src/components/forms/user-profile-stepper/create-profile.tsx +++ b/src/components/forms/user-profile-stepper/create-profile.tsx @@ -1,4 +1,5 @@ "use client"; +import { PhoneInput } from "@/components/phone-input"; import { Accordion, AccordionContent, @@ -58,6 +59,7 @@ export const CreateProfileOne: React.FC = ({ const delta = currentStep - previousStep; const defaultValues = { + // firstname: "", jobs: [ { jobtitle: "", @@ -96,7 +98,7 @@ export const CreateProfileOne: React.FC = ({ // console.log("product", res); } router.refresh(); - router.push(`/dashboard/products`); + router.push(`/dashboard`); } catch (error: any) { } finally { setLoading(false); @@ -182,8 +184,8 @@ export const CreateProfileOne: React.FC = ({ } }; - const countries = [{ id: "wow", name: "india" }]; - const cities = [{ id: "2", name: "kerala" }]; + const countries = [{ id: "1", name: "india" }]; + const cities = [{ id: "1", name: "kerala" }]; return ( <> @@ -307,11 +309,21 @@ export const CreateProfileOne: React.FC = ({ Contact Number - */} + setPhone(v)} + // value={phone} + placeholder="Enter a phone number..." + defaultCountry="IN" + international + {...field} + /> @@ -341,7 +353,7 @@ export const CreateProfileOne: React.FC = ({ {/* @ts-ignore */} {countries.map((country) => ( - + {country.name} ))} @@ -374,7 +386,7 @@ export const CreateProfileOne: React.FC = ({ {/* @ts-ignore */} {cities.map((city) => ( - + {city.name} ))} diff --git a/src/components/phone-input.tsx b/src/components/phone-input.tsx new file mode 100644 index 0000000..b7d2339 --- /dev/null +++ b/src/components/phone-input.tsx @@ -0,0 +1,167 @@ +import { CheckIcon, ChevronsUpDown } from "lucide-react"; + +import * as React from "react"; + +import * as RPNInput from "react-phone-number-input"; + +import flags from "react-phone-number-input/flags"; + +import { Button } from "@/components/ui/button"; +import { + Command, + CommandEmpty, + CommandGroup, + CommandInput, + CommandItem, + CommandList, +} from "@/components/ui/command"; +import { Input, InputProps } from "@/components/ui/input"; +import { + Popover, + PopoverContent, + PopoverTrigger, +} from "@/components/ui/popover"; + +import { cn } from "@/lib/utils"; +import { ScrollArea } from "./ui/scroll-area"; + +type PhoneInputProps = Omit< + React.InputHTMLAttributes, + "onChange" | "value" +> & + Omit, "onChange"> & { + onChange?: (value: RPNInput.Value) => void; + }; + +const PhoneInput: React.ForwardRefExoticComponent = + React.forwardRef, PhoneInputProps>( + ({ className, onChange, ...props }, ref) => { + return ( + onChange?.(value || "")} + {...props} + /> + ); + }, + ); +PhoneInput.displayName = "PhoneInput"; + +const InputComponent = React.forwardRef( + ({ className, ...props }, ref) => ( + + ), +); +InputComponent.displayName = "InputComponent"; + +type CountrySelectOption = { label: string; value: RPNInput.Country }; + +type CountrySelectProps = { + disabled?: boolean; + value: RPNInput.Country; + onChange: (value: RPNInput.Country) => void; + options: CountrySelectOption[]; +}; + +const CountrySelect = ({ + disabled, + value, + onChange, + options, +}: CountrySelectProps) => { + const handleSelect = React.useCallback( + (country: RPNInput.Country) => { + onChange(country); + }, + [onChange], + ); + + return ( + + + + + + + + + + No country found. + + {options + .filter((x) => x.value) + .map((option) => ( + handleSelect(option.value)} + > + + {option.label} + {option.value && ( + + {`+${RPNInput.getCountryCallingCode(option.value)}`} + + )} + + + ))} + + + + + + + ); +}; + +const FlagComponent = ({ country, countryName }: RPNInput.FlagProps) => { + const Flag = flags[country]; + + return ( + + {Flag && } + + ); +}; +FlagComponent.displayName = "FlagComponent"; + +export { PhoneInput }; diff --git a/src/components/task-page.tsx b/src/components/task-page.tsx index 5d8b574..0111a40 100644 --- a/src/components/task-page.tsx +++ b/src/components/task-page.tsx @@ -59,7 +59,7 @@ const TaskPage = ({ useEffect(() => { if (typeof window !== "undefined") { - console.log(isTaskPageLoading); + // console.log(isTaskPageLoading); updateTaskPageLoading(false); } }, []); diff --git a/src/components/ui/command.tsx b/src/components/ui/command.tsx new file mode 100644 index 0000000..b8c45ea --- /dev/null +++ b/src/components/ui/command.tsx @@ -0,0 +1,155 @@ +"use client" + +import * as React from "react" +import { type DialogProps } from "@radix-ui/react-dialog" +import { MagnifyingGlassIcon } from "@radix-ui/react-icons" +import { Command as CommandPrimitive } from "cmdk" + +import { cn } from "@/lib/utils" +import { Dialog, DialogContent } from "@/components/ui/dialog" + +const Command = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +Command.displayName = CommandPrimitive.displayName + +interface CommandDialogProps extends DialogProps {} + +const CommandDialog = ({ children, ...props }: CommandDialogProps) => { + return ( + + + + {children} + + + + ) +} + +const CommandInput = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( +
+ + +
+)) + +CommandInput.displayName = CommandPrimitive.Input.displayName + +const CommandList = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) + +CommandList.displayName = CommandPrimitive.List.displayName + +const CommandEmpty = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>((props, ref) => ( + +)) + +CommandEmpty.displayName = CommandPrimitive.Empty.displayName + +const CommandGroup = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) + +CommandGroup.displayName = CommandPrimitive.Group.displayName + +const CommandSeparator = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +CommandSeparator.displayName = CommandPrimitive.Separator.displayName + +const CommandItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) + +CommandItem.displayName = CommandPrimitive.Item.displayName + +const CommandShortcut = ({ + className, + ...props +}: React.HTMLAttributes) => { + return ( + + ) +} +CommandShortcut.displayName = "CommandShortcut" + +export { + Command, + CommandDialog, + CommandInput, + CommandList, + CommandEmpty, + CommandGroup, + CommandItem, + CommandShortcut, + CommandSeparator, +} diff --git a/src/lib/form-schema.ts b/src/lib/form-schema.ts index b5d4ca8..2958839 100644 --- a/src/lib/form-schema.ts +++ b/src/lib/form-schema.ts @@ -1,29 +1,30 @@ import * as z from "zod"; +import { isValidPhoneNumber } from "react-phone-number-input"; export const profileSchema = z.object({ firstname: z .string() - .min(3, { message: "Product Name must be at least 3 characters" }), + .min(3, { message: "First name must be at least 3 characters" }), lastname: z .string() - .min(3, { message: "Product Name must be at least 3 characters" }), + .min(3, { message: "Last name must be at least 3 characters" }), email: z .string() - .email({ message: "Product Name must be at least 3 characters" }), - contactno: z.coerce.number(), + .email({ message: "Invalid email" }), + contactno: z.string({required_error: "Number is required"}).refine((value) => isValidPhoneNumber(value), {message: "Invalid number"}), country: z.string().min(1, { message: "Please select a category" }), city: z.string().min(1, { message: "Please select a category" }), // jobs array is for the dynamic fields jobs: z.array( z.object({ - jobcountry: z.string().min(1, { message: "Please select a category" }), - jobcity: z.string().min(1, { message: "Please select a category" }), + jobcountry: z.string().min(1, { message: "Please select a country" }), + jobcity: z.string().min(1, { message: "Please select a city" }), jobtitle: z - .string() - .min(3, { message: "Product Name must be at least 3 characters" }), + .string(), + // .min(3, { message: "First Name must be at least 3 characters" }), employer: z - .string() - .min(3, { message: "Product Name must be at least 3 characters" }), + .string(), + // .min(3, { message: "First Name must be at least 3 characters" }), startdate: z .string() .refine((value) => /^\d{4}-\d{2}-\d{2}$/.test(value), {