From 65ea9e568180431d54bf62fd7bbe0e27a8591660 Mon Sep 17 00:00:00 2001 From: mehedi-hasan Date: Wed, 24 Apr 2024 20:02:12 +0600 Subject: [PATCH] feat: country, state, city select --- package-lock.json | 32 ++++ package.json | 1 + .../user-profile-stepper/create-profile.tsx | 130 +++++++------- src/components/geolocation.tsx | 161 ++++++++++++++++++ src/components/ui/command.tsx | 2 +- src/lib/form-schema.ts | 44 +++-- 6 files changed, 295 insertions(+), 75 deletions(-) create mode 100644 src/components/geolocation.tsx diff --git a/package-lock.json b/package-lock.json index 29182ee..d0cf8a7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -48,6 +48,7 @@ "contentlayer": "^0.3.4", "eslint": "8.48.0", "eslint-config-next": "^14.0.1", + "geonames.js": "^3.0.6", "gray-matter": "^4.0.3", "lucide-react": "^0.291.0", "monaco-jsx-highlighter": "^2.77.77", @@ -6177,6 +6178,23 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/geonames.js": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/geonames.js/-/geonames.js-3.0.6.tgz", + "integrity": "sha512-iDnDEzknKB7RXunjwYNgWg8ZbxjJhO9ChauXjo3njcsqhpHp3rpssqJLhATEp6T6NIybWlT/V3i6dxw+kFcICA==", + "dependencies": { + "axios": "^0.21.0", + "qs": "^6.9.4" + } + }, + "node_modules/geonames.js/node_modules/axios": { + "version": "0.21.4", + "resolved": "https://registry.npmjs.org/axios/-/axios-0.21.4.tgz", + "integrity": "sha512-ut5vewkiu8jjGBdqpM44XxjuCjq9LAKeHVmoVfHVzy8eHgxxq8SbAVQNovDA8mVi05kP0Ea/n/UzcSHcTJQfNg==", + "dependencies": { + "follow-redirects": "^1.14.0" + } + }, "node_modules/get-caller-file": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", @@ -13521,6 +13539,20 @@ "node": ">=6" } }, + "node_modules/qs": { + "version": "6.12.1", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.12.1.tgz", + "integrity": "sha512-zWmv4RSuB9r2mYQw3zxQuHWeU+42aKi1wWig/j4ele4ygELZ7PEO6MM7rim9oAQH2A5MWfsAVf/jPvTPgCbvUQ==", + "dependencies": { + "side-channel": "^1.0.6" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/queue-microtask": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", diff --git a/package.json b/package.json index 5be2ba4..8ab147f 100644 --- a/package.json +++ b/package.json @@ -50,6 +50,7 @@ "contentlayer": "^0.3.4", "eslint": "8.48.0", "eslint-config-next": "^14.0.1", + "geonames.js": "^3.0.6", "gray-matter": "^4.0.3", "lucide-react": "^0.291.0", "monaco-jsx-highlighter": "^2.77.77", diff --git a/src/components/forms/user-profile-stepper/create-profile.tsx b/src/components/forms/user-profile-stepper/create-profile.tsx index 0e9cc97..bbcd989 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 Geolocation from "@/components/geolocation"; import { PhoneInput } from "@/components/phone-input"; import { Accordion, @@ -57,9 +58,11 @@ export const CreateProfileOne: React.FC = ({ const [currentStep, setCurrentStep] = useState(0); const [data, setData] = useState({}); const delta = currentStep - previousStep; + const [countryGeoId, setCountryGeoId] = useState(null); + const [stateGeoId, setStateGeoId] = useState(null); const defaultValues = { - // firstname: "", + // state: "", jobs: [ { jobtitle: "", @@ -137,6 +140,7 @@ export const CreateProfileOne: React.FC = ({ "email", "contactno", "country", + "state", "city", ], }, @@ -187,6 +191,8 @@ export const CreateProfileOne: React.FC = ({ const countries = [{ id: "1", name: "india" }]; const cities = [{ id: "1", name: "kerala" }]; + console.log(form.getValues()); + return ( <>
@@ -208,18 +214,18 @@ export const CreateProfileOne: React.FC = ({ {steps.map((step, index) => (
  • {currentStep > index ? ( -
    - +
    + {step.id} {step.name}
    ) : currentStep === index ? (
    - + {step.id} {step.name} @@ -323,7 +329,6 @@ export const CreateProfileOne: React.FC = ({ defaultCountry="IN" international {...field} - /> @@ -335,63 +340,68 @@ export const CreateProfileOne: React.FC = ({ name="country" render={({ field }) => ( - Country - + Country + + { + setCountryGeoId(geoId); + field.onChange(country); + }} + isCountry + placeholderText="Select a country" + searchPlaceholder="Search country..." + emptyPlaceholder="No country found" + isDisabled={false} + /> + )} /> + ( + + State + { + setStateGeoId(geoId); + field.onChange(state); + }} + isCountry={false} + placeholderText="Select a state" + searchPlaceholder="Search state..." + emptyPlaceholder="No state found" + isDisabled={!countryGeoId} + /> + + + + )} + /> + ( City - + field.onChange(v)} + isCountry={false} + placeholderText="Select a city" + searchPlaceholder="Search city..." + emptyPlaceholder="No city found" + isDisabled={!stateGeoId} + /> + )} @@ -602,7 +612,7 @@ export const CreateProfileOne: React.FC = ({

    Completed

    -                  {JSON.stringify(data)}
    +                  {JSON.stringify(data, null, 2)}
                     
    )} @@ -616,11 +626,12 @@ export const CreateProfileOne: React.FC = ({ {/* Navigation */}
    - - +
    diff --git a/src/components/geolocation.tsx b/src/components/geolocation.tsx new file mode 100644 index 0000000..5b6c4cf --- /dev/null +++ b/src/components/geolocation.tsx @@ -0,0 +1,161 @@ +"use client"; + +import { CaretSortIcon, CheckIcon } from "@radix-ui/react-icons"; + +import { cn } from "@/lib/utils"; +import { Button } from "@/components/ui/button"; +import { + Command, + CommandEmpty, + CommandGroup, + CommandInput, + CommandItem, + CommandList, +} from "@/components/ui/command"; +import { + Popover, + PopoverContent, + PopoverTrigger, +} from "@/components/ui/popover"; +import { FC, useEffect, useState } from "react"; +import Geonames from "geonames.js"; +import { FormControl } from "./ui/form"; + +const frameworks = [ + { + value: "next.js", + label: "Next.js", + }, + { + value: "sveltekit", + label: "SvelteKit", + }, + { + value: "nuxt.js", + label: "Nuxt.js", + }, + { + value: "remix", + label: "Remix", + }, + { + value: "astro", + label: "Astro", + }, +]; + +// @ts-ignore +const geonames = new Geonames({ + username: "thalesandrade", + lan: "en", + encoding: "JSON", +}); + +interface Props { + geoId: number | null; + onChange: (country: string, geoId: number) => void; + isCountry: boolean; + placeholderText: string; + searchPlaceholder: string; + emptyPlaceholder: string; + isDisabled: boolean; +} + +const Geolocation: FC = ({ + geoId, + onChange, + isCountry, + placeholderText, + searchPlaceholder, + emptyPlaceholder, + isDisabled, +}) => { + const [open, setOpen] = useState(false); + const [currentValue, setCurrentValue] = useState(null); + const [options, setOptions] = useState< + { countryName: string; geonameId: number; name: string }[] + >([]); + + useEffect(() => { + try { + const data = () => { + isCountry + ? geonames.countryInfo({}).then((res: any) => { + console.log(res); + setOptions(res.geonames); + }) + : geonames.children({ geonameId: geoId }).then((res: any) => { + if (res.totalResultsCount) setOptions(res.geonames); + }); + }; + data(); + } catch (err) { + console.error(err); + } + }, [geoId, isCountry]); + + const currentPlaceholderText = + currentValue && isCountry + ? options.find((option: any) => option.geonameId === currentValue) + ?.countryName + : currentValue + ? options.find((option: any) => option.geonameId === currentValue)?.name + : placeholderText; + + return ( + + + + + + + + + + + {emptyPlaceholder} + + {options.map((v: any, index) => ( + { + setCurrentValue( + v.geonameId === currentValue ? null : v.geonameId, + ); + const name = isCountry ? v.countryName : v.name; + onChange( + v.geonameId === currentValue ? "" : name, + v.geonameId === currentValue ? null : v.geonameId, + ); + setOpen(false); + }} + > + {isCountry ? v.countryName : v.name} + + + ))} + + + + + + ); +}; + +export default Geolocation; diff --git a/src/components/ui/command.tsx b/src/components/ui/command.tsx index b8c45ea..3a51ec0 100644 --- a/src/components/ui/command.tsx +++ b/src/components/ui/command.tsx @@ -117,7 +117,7 @@ const CommandItem = React.forwardRef< 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" }), + .string({ required_error: "Please enter your email" }) + .email({ message: "Please enter a valid email" }), + contactno: z + .string({ required_error: "Please enter your phone number" }) + .refine((value) => isValidPhoneNumber(value), { + message: "Please enter a valid phone number", + }), + country: z + .string({ required_error: "Please select your country" }) + .trim() + .min(1, { message: "Please select your country" }), + state: z + .string({ required_error: "Please select your state" }) + .trim() + .min(1, { message: "Please select your state" }), + city: z + .string({ required_error: "Please select your city" }) + .trim() + .min(1, { message: "Please select your city" }), // jobs array is for the dynamic fields jobs: z.array( z.object({ 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: "First Name must be at least 3 characters" }), - employer: z - .string(), - // .min(3, { message: "First Name must be at least 3 characters" }), + jobtitle: z.string(), + // .min(3, { message: "First Name must be at least 3 characters" }), + employer: z.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), {