feat: country, state, city select
This commit is contained in:
parent
c4c8c34579
commit
65ea9e5681
|
@ -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",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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<ProfileFormType> = ({
|
|||
const [currentStep, setCurrentStep] = useState(0);
|
||||
const [data, setData] = useState({});
|
||||
const delta = currentStep - previousStep;
|
||||
const [countryGeoId, setCountryGeoId] = useState<null | number>(null);
|
||||
const [stateGeoId, setStateGeoId] = useState<null | number>(null);
|
||||
|
||||
const defaultValues = {
|
||||
// firstname: "",
|
||||
// state: "",
|
||||
jobs: [
|
||||
{
|
||||
jobtitle: "",
|
||||
|
@ -137,6 +140,7 @@ export const CreateProfileOne: React.FC<ProfileFormType> = ({
|
|||
"email",
|
||||
"contactno",
|
||||
"country",
|
||||
"state",
|
||||
"city",
|
||||
],
|
||||
},
|
||||
|
@ -187,6 +191,8 @@ export const CreateProfileOne: React.FC<ProfileFormType> = ({
|
|||
const countries = [{ id: "1", name: "india" }];
|
||||
const cities = [{ id: "1", name: "kerala" }];
|
||||
|
||||
console.log(form.getValues());
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flex items-center justify-between">
|
||||
|
@ -208,18 +214,18 @@ export const CreateProfileOne: React.FC<ProfileFormType> = ({
|
|||
{steps.map((step, index) => (
|
||||
<li key={step.name} className="md:flex-1">
|
||||
{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">
|
||||
<span className="text-sm font-medium text-sky-600 transition-colors ">
|
||||
<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-primary transition-colors ">
|
||||
{step.id}
|
||||
</span>
|
||||
<span className="text-sm font-medium">{step.name}</span>
|
||||
</div>
|
||||
) : currentStep === index ? (
|
||||
<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"
|
||||
>
|
||||
<span className="text-sm font-medium text-sky-600">
|
||||
<span className="text-sm font-medium text-primary">
|
||||
{step.id}
|
||||
</span>
|
||||
<span className="text-sm font-medium">{step.name}</span>
|
||||
|
@ -323,7 +329,6 @@ export const CreateProfileOne: React.FC<ProfileFormType> = ({
|
|||
defaultCountry="IN"
|
||||
international
|
||||
{...field}
|
||||
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
|
@ -335,63 +340,68 @@ export const CreateProfileOne: React.FC<ProfileFormType> = ({
|
|||
name="country"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Country</FormLabel>
|
||||
<Select
|
||||
disabled={loading}
|
||||
onValueChange={field.onChange}
|
||||
value={field.value}
|
||||
defaultValue={field.value}
|
||||
<FormLabel
|
||||
// className="block mb-[6px] mt-[2px]"
|
||||
>
|
||||
<FormControl>
|
||||
<SelectTrigger>
|
||||
<SelectValue
|
||||
defaultValue={field.value}
|
||||
placeholder="Select a country"
|
||||
/>
|
||||
</SelectTrigger>
|
||||
</FormControl>
|
||||
<SelectContent>
|
||||
{/* @ts-ignore */}
|
||||
{countries.map((country) => (
|
||||
<SelectItem key={country.id} value={country.name.toLocaleLowerCase()}>
|
||||
{country.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
Country
|
||||
</FormLabel>
|
||||
<Geolocation
|
||||
geoId={null}
|
||||
onChange={(country, geoId) => {
|
||||
setCountryGeoId(geoId);
|
||||
field.onChange(country);
|
||||
}}
|
||||
isCountry
|
||||
placeholderText="Select a country"
|
||||
searchPlaceholder="Search country..."
|
||||
emptyPlaceholder="No country found"
|
||||
isDisabled={false}
|
||||
/>
|
||||
|
||||
<FormMessage />
|
||||
</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
|
||||
control={form.control}
|
||||
name="city"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>City</FormLabel>
|
||||
<Select
|
||||
disabled={loading}
|
||||
onValueChange={field.onChange}
|
||||
value={field.value}
|
||||
defaultValue={field.value}
|
||||
>
|
||||
<FormControl>
|
||||
<SelectTrigger>
|
||||
<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>
|
||||
<Geolocation
|
||||
geoId={stateGeoId}
|
||||
onChange={(v) => field.onChange(v)}
|
||||
isCountry={false}
|
||||
placeholderText="Select a city"
|
||||
searchPlaceholder="Search city..."
|
||||
emptyPlaceholder="No city found"
|
||||
isDisabled={!stateGeoId}
|
||||
/>
|
||||
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
|
@ -602,7 +612,7 @@ export const CreateProfileOne: React.FC<ProfileFormType> = ({
|
|||
<div>
|
||||
<h1>Completed</h1>
|
||||
<pre className="whitespace-pre-wrap">
|
||||
{JSON.stringify(data)}
|
||||
{JSON.stringify(data, null, 2)}
|
||||
</pre>
|
||||
</div>
|
||||
)}
|
||||
|
@ -616,11 +626,12 @@ export const CreateProfileOne: React.FC<ProfileFormType> = ({
|
|||
{/* Navigation */}
|
||||
<div className="mt-8 pt-5">
|
||||
<div className="flex justify-between">
|
||||
<button
|
||||
<Button
|
||||
type="button"
|
||||
onClick={prev}
|
||||
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
|
||||
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"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
<button
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
onClick={next}
|
||||
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
|
||||
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"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
|
|
|
@ -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;
|
|
@ -117,7 +117,7 @@ const CommandItem = React.forwardRef<
|
|||
<CommandPrimitive.Item
|
||||
ref={ref}
|
||||
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
|
||||
)}
|
||||
{...props}
|
||||
|
|
|
@ -3,28 +3,42 @@ import { isValidPhoneNumber } from "react-phone-number-input";
|
|||
|
||||
export const profileSchema = z.object({
|
||||
firstname: z
|
||||
.string()
|
||||
.min(3, { message: "First name must be at least 3 characters" }),
|
||||
.string({ required_error: "Please enter your first name" })
|
||||
.trim()
|
||||
.min(1, { message: "Please enter your first name" }),
|
||||
lastname: z
|
||||
.string()
|
||||
.min(3, { message: "Last name must be at least 3 characters" }),
|
||||
.string({ required_error: "Please enter your last name" })
|
||||
.trim()
|
||||
.min(1, { message: "Please enter your last name" }),
|
||||
email: z
|
||||
.string()
|
||||
.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" }),
|
||||
.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), {
|
||||
|
|
Loading…
Reference in New Issue