feat: country, state, city select

This commit is contained in:
mehedi-hasan 2024-04-24 20:02:12 +06:00
parent c4c8c34579
commit 65ea9e5681
6 changed files with 295 additions and 75 deletions

32
package-lock.json generated
View File

@ -48,6 +48,7 @@
"contentlayer": "^0.3.4", "contentlayer": "^0.3.4",
"eslint": "8.48.0", "eslint": "8.48.0",
"eslint-config-next": "^14.0.1", "eslint-config-next": "^14.0.1",
"geonames.js": "^3.0.6",
"gray-matter": "^4.0.3", "gray-matter": "^4.0.3",
"lucide-react": "^0.291.0", "lucide-react": "^0.291.0",
"monaco-jsx-highlighter": "^2.77.77", "monaco-jsx-highlighter": "^2.77.77",
@ -6177,6 +6178,23 @@
"url": "https://github.com/sponsors/ljharb" "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": { "node_modules/get-caller-file": {
"version": "2.0.5", "version": "2.0.5",
"resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
@ -13521,6 +13539,20 @@
"node": ">=6" "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": { "node_modules/queue-microtask": {
"version": "1.2.3", "version": "1.2.3",
"resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",

View File

@ -50,6 +50,7 @@
"contentlayer": "^0.3.4", "contentlayer": "^0.3.4",
"eslint": "8.48.0", "eslint": "8.48.0",
"eslint-config-next": "^14.0.1", "eslint-config-next": "^14.0.1",
"geonames.js": "^3.0.6",
"gray-matter": "^4.0.3", "gray-matter": "^4.0.3",
"lucide-react": "^0.291.0", "lucide-react": "^0.291.0",
"monaco-jsx-highlighter": "^2.77.77", "monaco-jsx-highlighter": "^2.77.77",

View File

@ -1,4 +1,5 @@
"use client"; "use client";
import Geolocation from "@/components/geolocation";
import { PhoneInput } from "@/components/phone-input"; import { PhoneInput } from "@/components/phone-input";
import { import {
Accordion, Accordion,
@ -57,9 +58,11 @@ export const CreateProfileOne: React.FC<ProfileFormType> = ({
const [currentStep, setCurrentStep] = useState(0); const [currentStep, setCurrentStep] = useState(0);
const [data, setData] = useState({}); const [data, setData] = useState({});
const delta = currentStep - previousStep; const delta = currentStep - previousStep;
const [countryGeoId, setCountryGeoId] = useState<null | number>(null);
const [stateGeoId, setStateGeoId] = useState<null | number>(null);
const defaultValues = { const defaultValues = {
// firstname: "", // state: "",
jobs: [ jobs: [
{ {
jobtitle: "", jobtitle: "",
@ -137,6 +140,7 @@ export const CreateProfileOne: React.FC<ProfileFormType> = ({
"email", "email",
"contactno", "contactno",
"country", "country",
"state",
"city", "city",
], ],
}, },
@ -187,6 +191,8 @@ export const CreateProfileOne: React.FC<ProfileFormType> = ({
const countries = [{ id: "1", name: "india" }]; const countries = [{ id: "1", name: "india" }];
const cities = [{ id: "1", name: "kerala" }]; const cities = [{ id: "1", name: "kerala" }];
console.log(form.getValues());
return ( return (
<> <>
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
@ -208,18 +214,18 @@ export const CreateProfileOne: React.FC<ProfileFormType> = ({
{steps.map((step, index) => ( {steps.map((step, index) => (
<li key={step.name} className="md:flex-1"> <li key={step.name} className="md:flex-1">
{currentStep > index ? ( {currentStep > index ? (
<div className="group flex w-full flex-col border-l-4 border-sky-600 py-2 pl-4 transition-colors md:border-l-0 md:border-t-4 md:pb-0 md:pl-0 md:pt-4"> <div className="group flex w-full flex-col border-l-4 border-primary py-2 pl-4 transition-colors md:border-l-0 md:border-t-4 md:pb-0 md:pl-0 md:pt-4">
<span className="text-sm font-medium text-sky-600 transition-colors "> <span className="text-sm font-medium text-primary transition-colors ">
{step.id} {step.id}
</span> </span>
<span className="text-sm font-medium">{step.name}</span> <span className="text-sm font-medium">{step.name}</span>
</div> </div>
) : currentStep === index ? ( ) : currentStep === index ? (
<div <div
className="flex w-full flex-col border-l-4 border-sky-600 py-2 pl-4 md:border-l-0 md:border-t-4 md:pb-0 md:pl-0 md:pt-4" className="flex w-full flex-col border-l-4 border-primary py-2 pl-4 md:border-l-0 md:border-t-4 md:pb-0 md:pl-0 md:pt-4"
aria-current="step" aria-current="step"
> >
<span className="text-sm font-medium text-sky-600"> <span className="text-sm font-medium text-primary">
{step.id} {step.id}
</span> </span>
<span className="text-sm font-medium">{step.name}</span> <span className="text-sm font-medium">{step.name}</span>
@ -323,7 +329,6 @@ export const CreateProfileOne: React.FC<ProfileFormType> = ({
defaultCountry="IN" defaultCountry="IN"
international international
{...field} {...field}
/> />
</FormControl> </FormControl>
<FormMessage /> <FormMessage />
@ -335,63 +340,68 @@ export const CreateProfileOne: React.FC<ProfileFormType> = ({
name="country" name="country"
render={({ field }) => ( render={({ field }) => (
<FormItem> <FormItem>
<FormLabel>Country</FormLabel> <FormLabel
<Select // className="block mb-[6px] mt-[2px]"
disabled={loading}
onValueChange={field.onChange}
value={field.value}
defaultValue={field.value}
> >
<FormControl> Country
<SelectTrigger> </FormLabel>
<SelectValue <Geolocation
defaultValue={field.value} geoId={null}
placeholder="Select a country" onChange={(country, geoId) => {
/> setCountryGeoId(geoId);
</SelectTrigger> field.onChange(country);
</FormControl> }}
<SelectContent> isCountry
{/* @ts-ignore */} placeholderText="Select a country"
{countries.map((country) => ( searchPlaceholder="Search country..."
<SelectItem key={country.id} value={country.name.toLocaleLowerCase()}> emptyPlaceholder="No country found"
{country.name} isDisabled={false}
</SelectItem> />
))}
</SelectContent>
</Select>
<FormMessage /> <FormMessage />
</FormItem> </FormItem>
)} )}
/> />
<FormField
control={form.control}
name="state"
render={({ field }) => (
<FormItem>
<FormLabel>State</FormLabel>
<Geolocation
geoId={countryGeoId}
onChange={(state, geoId) => {
setStateGeoId(geoId);
field.onChange(state);
}}
isCountry={false}
placeholderText="Select a state"
searchPlaceholder="Search state..."
emptyPlaceholder="No state found"
isDisabled={!countryGeoId}
/>
<FormMessage />
</FormItem>
)}
/>
<FormField <FormField
control={form.control} control={form.control}
name="city" name="city"
render={({ field }) => ( render={({ field }) => (
<FormItem> <FormItem>
<FormLabel>City</FormLabel> <FormLabel>City</FormLabel>
<Select <Geolocation
disabled={loading} geoId={stateGeoId}
onValueChange={field.onChange} onChange={(v) => field.onChange(v)}
value={field.value} isCountry={false}
defaultValue={field.value} placeholderText="Select a city"
> searchPlaceholder="Search city..."
<FormControl> emptyPlaceholder="No city found"
<SelectTrigger> isDisabled={!stateGeoId}
<SelectValue />
defaultValue={field.value}
placeholder="Select a city"
/>
</SelectTrigger>
</FormControl>
<SelectContent>
{/* @ts-ignore */}
{cities.map((city) => (
<SelectItem key={city.id} value={city.name.toLocaleLowerCase()}>
{city.name}
</SelectItem>
))}
</SelectContent>
</Select>
<FormMessage /> <FormMessage />
</FormItem> </FormItem>
)} )}
@ -602,7 +612,7 @@ export const CreateProfileOne: React.FC<ProfileFormType> = ({
<div> <div>
<h1>Completed</h1> <h1>Completed</h1>
<pre className="whitespace-pre-wrap"> <pre className="whitespace-pre-wrap">
{JSON.stringify(data)} {JSON.stringify(data, null, 2)}
</pre> </pre>
</div> </div>
)} )}
@ -616,11 +626,12 @@ export const CreateProfileOne: React.FC<ProfileFormType> = ({
{/* Navigation */} {/* Navigation */}
<div className="mt-8 pt-5"> <div className="mt-8 pt-5">
<div className="flex justify-between"> <div className="flex justify-between">
<button <Button
type="button" type="button"
onClick={prev} onClick={prev}
disabled={currentStep === 0} disabled={currentStep === 0}
className="rounded bg-white px-2 py-1 text-sm font-semibold text-sky-900 shadow-sm ring-1 ring-inset ring-sky-300 hover:bg-sky-50 disabled:cursor-not-allowed disabled:opacity-50" size="icon"
// className="rounded bg-white px-2 py-1 text-sm font-semibold text-sky-900 shadow-sm ring-1 ring-inset ring-sky-300 hover:bg-sky-50 disabled:cursor-not-allowed disabled:opacity-50"
> >
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
@ -636,12 +647,13 @@ export const CreateProfileOne: React.FC<ProfileFormType> = ({
d="M15.75 19.5L8.25 12l7.5-7.5" d="M15.75 19.5L8.25 12l7.5-7.5"
/> />
</svg> </svg>
</button> </Button>
<button <Button
type="button" type="button"
onClick={next} onClick={next}
disabled={currentStep === steps.length - 1} disabled={currentStep === steps.length - 1}
className="rounded bg-white px-2 py-1 text-sm font-semibold text-sky-900 shadow-sm ring-1 ring-inset ring-sky-300 hover:bg-sky-50 disabled:cursor-not-allowed disabled:opacity-50" size="icon"
// className="rounded bg-white px-2 py-1 text-sm font-semibold text-sky-900 shadow-sm ring-1 ring-inset ring-sky-300 hover:bg-sky-50 disabled:cursor-not-allowed disabled:opacity-50"
> >
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
@ -657,7 +669,7 @@ export const CreateProfileOne: React.FC<ProfileFormType> = ({
d="M8.25 4.5l7.5 7.5-7.5 7.5" d="M8.25 4.5l7.5 7.5-7.5 7.5"
/> />
</svg> </svg>
</button> </Button>
</div> </div>
</div> </div>
</> </>

View File

@ -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<Props> = ({
geoId,
onChange,
isCountry,
placeholderText,
searchPlaceholder,
emptyPlaceholder,
isDisabled,
}) => {
const [open, setOpen] = useState(false);
const [currentValue, setCurrentValue] = useState<null | number>(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 (
<Popover open={open} onOpenChange={setOpen}>
<PopoverTrigger asChild disabled={isDisabled}>
<FormControl>
<Button
variant="outline"
role="combobox"
aria-expanded={open}
className="w-full justify-between"
>
{currentPlaceholderText}
<CaretSortIcon className="ml-2 h-4 w-4 shrink-0 opacity-50" />
</Button>
</FormControl>
</PopoverTrigger>
<PopoverContent className="w-[200px] md:w-[400px] p-0">
<Command>
<CommandList>
<CommandInput placeholder={searchPlaceholder} className="h-9" />
<CommandEmpty>{emptyPlaceholder}</CommandEmpty>
<CommandGroup>
{options.map((v: any, index) => (
<CommandItem
key={index}
value={v.geonameId}
onSelect={() => {
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}
<CheckIcon
className={cn(
"ml-auto h-4 w-4",
currentValue === v.geonameId
? "opacity-100"
: "opacity-0",
)}
/>
</CommandItem>
))}
</CommandGroup>
</CommandList>
</Command>
</PopoverContent>
</Popover>
);
};
export default Geolocation;

View File

@ -117,7 +117,7 @@ const CommandItem = React.forwardRef<
<CommandPrimitive.Item <CommandPrimitive.Item
ref={ref} ref={ref}
className={cn( className={cn(
"relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none aria-selected:bg-accent aria-selected:text-accent-foreground data-[disabled='true']:pointer-events-none data-[disabled='true']:opacity-50", "relative flex cursor-pointer select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none aria-selected:bg-accent aria-selected:text-accent-foreground data-[disabled='true']:pointer-events-none data-[disabled='true']:opacity-50",
className className
)} )}
{...props} {...props}

View File

@ -3,28 +3,42 @@ import { isValidPhoneNumber } from "react-phone-number-input";
export const profileSchema = z.object({ export const profileSchema = z.object({
firstname: z firstname: z
.string() .string({ required_error: "Please enter your first name" })
.min(3, { message: "First name must be at least 3 characters" }), .trim()
.min(1, { message: "Please enter your first name" }),
lastname: z lastname: z
.string() .string({ required_error: "Please enter your last name" })
.min(3, { message: "Last name must be at least 3 characters" }), .trim()
.min(1, { message: "Please enter your last name" }),
email: z email: z
.string() .string({ required_error: "Please enter your email" })
.email({ message: "Invalid email" }), .email({ message: "Please enter a valid email" }),
contactno: z.string({required_error: "Number is required"}).refine((value) => isValidPhoneNumber(value), {message: "Invalid number"}), contactno: z
country: z.string().min(1, { message: "Please select a category" }), .string({ required_error: "Please enter your phone number" })
city: z.string().min(1, { message: "Please select a category" }), .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 array is for the dynamic fields
jobs: z.array( jobs: z.array(
z.object({ z.object({
jobcountry: z.string().min(1, { message: "Please select a country" }), jobcountry: z.string().min(1, { message: "Please select a country" }),
jobcity: z.string().min(1, { message: "Please select a city" }), jobcity: z.string().min(1, { message: "Please select a city" }),
jobtitle: z jobtitle: z.string(),
.string(), // .min(3, { message: "First Name must be at least 3 characters" }),
// .min(3, { message: "First Name must be at least 3 characters" }), employer: z.string(),
employer: z // .min(3, { message: "First Name must be at least 3 characters" }),
.string(),
// .min(3, { message: "First Name must be at least 3 characters" }),
startdate: z startdate: z
.string() .string()
.refine((value) => /^\d{4}-\d{2}-\d{2}$/.test(value), { .refine((value) => /^\d{4}-\d{2}-\d{2}$/.test(value), {