coutry crud added

This commit is contained in:
Samandar Turgunboyev
2025-12-04 12:41:01 +05:00
parent 394d158947
commit da46b6cac1
19 changed files with 2193 additions and 163 deletions

View File

@@ -1,10 +1,12 @@
"use client";
import {
countryList,
createTours,
getAllAmenities,
hotelBadge,
hotelTransport,
regionList,
updateTours,
} from "@/pages/tours/lib/api";
import { TourformSchema } from "@/pages/tours/lib/form";
@@ -12,12 +14,14 @@ import { useTicketStore } from "@/pages/tours/lib/store";
import type { GetOneTours } from "@/pages/tours/lib/type";
import TicketsImagesModel from "@/pages/tours/ui/TicketsImagesModel";
import formatPrice from "@/shared/lib/formatPrice";
import { cn } from "@/shared/lib/utils";
import { Badge } from "@/shared/ui/badge";
import { Button } from "@/shared/ui/button";
import { Calendar } from "@/shared/ui/calendar";
import {
Command,
CommandGroup,
CommandInput,
CommandItem,
CommandList,
} from "@/shared/ui/command";
@@ -35,7 +39,16 @@ import { RadioGroup, RadioGroupItem } from "@/shared/ui/radio-group";
import { Textarea } from "@/shared/ui/textarea";
import { zodResolver } from "@hookform/resolvers/zod";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { ChevronDownIcon, Loader2, SquareCheckBig, XIcon } from "lucide-react";
import { AnimatePresence, motion } from "framer-motion";
import {
Check,
ChevronDownIcon,
ChevronsUpDown,
Loader2,
MoveLeft,
SquareCheckBig,
XIcon,
} from "lucide-react";
import { useEffect, useState, type Dispatch, type SetStateAction } from "react";
import { useForm } from "react-hook-form";
import { useTranslation } from "react-i18next";
@@ -72,9 +85,9 @@ const StepOne = ({
departure: "",
// tarif: [],
transport: [],
departure_ru: "",
// departure_ru: "",
destination: "",
destination_ru: "",
// destination_ru: "",
location_name: "",
location_name_ru: "",
departureDateTime: {
@@ -151,10 +164,10 @@ const StepOne = ({
passenger_count: String(tour.passenger_count) ?? "1",
min_person: String(tour.min_person) ?? "1",
max_person: String(tour.max_person) ?? "1",
departure: tour.departure_uz ?? "",
departure_ru: tour.departure_ru ?? "",
destination: tour.destination_uz ?? "",
destination_ru: tour.destination_ru ?? "",
departure: tour.departure?.id.toString() ?? "",
// departure_ru: tour.departure_ru ?? "",
destination: tour.destination?.id.toString() ?? "",
// destination_ru: tour.destination_ru ?? "",
location_name: tour.location_name_uz ?? "",
location_name_ru: tour.location_name_ru ?? "",
hotel_info: tour.hotel_info_uz ?? "",
@@ -227,6 +240,10 @@ const StepOne = ({
// TicketStore uchun id
setId(tour.id);
setSelectedCountry(data.data.departure?.country?.id);
setSearchCity(data.data.departure?.name);
setSelectedCountryDes(data.data.destination?.country.id);
setSearchCityDes(data.data.destination?.name);
}, [isEditMode, data, form, setId]);
const { watch, setValue } = form;
@@ -289,9 +306,9 @@ const StepOne = ({
formData.append("min_person", String(value.min_person));
formData.append("max_person", String(value.max_person));
formData.append("departure", String(value.departure));
formData.append("departure_ru", String(value.departure_ru));
// formData.append("departure_ru", String(value.departure_ru));
formData.append("destination", String(value.destination));
formData.append("destination_ru", String(value.destination_ru));
// formData.append("destination_ru", String(value.destination_ru));
formData.append(
"departure_time",
value.departureDateTime.date?.toISOString(),
@@ -488,11 +505,77 @@ const StepOne = ({
// queryFn: () => hotelTarif({ page: 1, page_size: 10 }),
// });
const [searchCountry, setSearchCountry] = useState<string>("");
const [searchCity, setSearchCity] = useState<string>("");
const [openCountry, setOpenCountry] = useState<boolean>(false);
const [openCity, setOpenCity] = useState<boolean>(false);
const [selectedCountry, setSelectedCountry] = useState<number | null>(null);
const { data: countryData, isLoading: countryLoad } = useQuery({
queryKey: ["all_country", searchCountry],
queryFn: () => countryList({ page: 1, page_size: 10, name: searchCountry }),
select: (res) => res.data.data,
});
const { data: regionData, isLoading: regionLoad } = useQuery({
queryKey: ["all_region", selectedCountry, searchCity],
queryFn: () =>
regionList({
page: 1,
page_size: 10,
country: selectedCountry!,
name: searchCity,
}),
select: (res) => res.data.data,
enabled: !!selectedCountry,
});
//destions select country
const [searchCountryDes, setSearchCountryDes] = useState<string>("");
const [searchCityDes, setSearchCityDes] = useState<string>("");
const [openCountryDes, setOpenCountryDes] = useState<boolean>(false);
const [openCityDes, setOpenCityDes] = useState<boolean>(false);
const [selectedCountryDes, setSelectedCountryDes] = useState<number | null>(
null,
);
const { data: countryDataDes, isLoading: countryLoadDes } = useQuery({
queryKey: ["all_country_des", searchCountryDes],
queryFn: () =>
countryList({ page: 1, page_size: 10, name: searchCountryDes }),
select: (res) => res.data.data,
});
const { data: regionDataDes, isLoading: regionLoadDes } = useQuery({
queryKey: ["all_region_des", selectedCountryDes, searchCityDes],
queryFn: () =>
regionList({
page: 1,
page_size: 10,
country: selectedCountryDes!,
name: searchCityDes,
}),
select: (res) => res.data.data,
enabled: !!selectedCountryDes,
});
const handleCountrySelect = (id: number | null) => {
setSelectedCountry(id);
form.setValue("departure", "");
setOpenCountry(false);
};
const handleCountrySelectDes = (id: number | null) => {
setSelectedCountryDes(id);
form.setValue("destination", "");
setOpenCountryDes(false);
};
const { data: transport } = useQuery({
queryKey: ["all_transport"],
queryFn: () => hotelTransport({ page: 1, page_size: 10 }),
});
console.log(form.formState.errors);
return (
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-6">
@@ -620,24 +703,224 @@ const StepOne = ({
<div className="grid grid-cols-2 gap-4 max-lg:grid-cols-1 items-start">
<FormField
control={form.control}
name="departure"
render={({ field }) => (
<FormItem>
<Label className="text-md">{t("Ketish joyi")}</Label>
<FormControl>
<Input
placeholder="Toshkent"
{...field}
className="h-12 !text-md"
/>
</FormControl>
<FormMessage />
</FormItem>
)}
control={form.control}
render={({ field }) => {
const selectedCity = regionData?.results.find(
(u) => String(u.id) === field.value,
);
return (
<FormItem className="flex flex-col gap-2">
<Label className="text-md">Ketish joyi</Label>
{!selectedCountry && (
<Popover open={openCountry} onOpenChange={setOpenCountry}>
<PopoverTrigger asChild>
<FormControl>
<Button
variant="outline"
className={cn(
"w-full h-12 justify-between transition-all duration-200",
!selectedCountry && "text-muted-foreground",
)}
>
{selectedCountry
? countryData?.results.find(
(c) => c.id === selectedCountry,
)?.name
: "Davlatni tanlang"}
<ChevronsUpDown className="ml-2 h-4 opacity-50" />
</Button>
</FormControl>
</PopoverTrigger>
<AnimatePresence mode="wait">
<motion.div
initial={{ opacity: 0, y: -10 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -10 }}
transition={{ duration: 0.3, ease: "easeOut" }}
>
<PopoverContent className="!w-96 p-0">
<Command shouldFilter={false}>
<CommandInput
placeholder="Qidirish..."
value={searchCountry}
onValueChange={setSearchCountry}
/>
<CommandList>
{countryLoad ? (
<div className="py-6 text-center">
<Loader2 className="animate-spin mx-auto" />
</div>
) : (
<CommandGroup>
{countryData?.results.map((c, index) => (
<motion.div
key={c.id}
initial={{ opacity: 0, x: -20 }}
animate={{ opacity: 1, x: 0 }}
transition={{
duration: 0.2,
delay: index * 0.03,
ease: "easeOut",
}}
>
<CommandItem
onSelect={() => {
handleCountrySelect(c.id);
setOpenCountry(false);
setTimeout(
() => setOpenCity(true),
200,
);
}}
className="cursor-pointer transition-colors"
>
<Check
className={cn(
"mr-2 h-4 transition-opacity duration-200",
selectedCountry === c.id
? "opacity-100"
: "opacity-0",
)}
/>
{c.name}
</CommandItem>
</motion.div>
))}
</CommandGroup>
)}
</CommandList>
</Command>
</PopoverContent>
</motion.div>
</AnimatePresence>
</Popover>
)}
{/* CITY DROPDOWN */}
{selectedCountry && (
<Popover open={openCity} onOpenChange={setOpenCity}>
<PopoverTrigger asChild>
<FormControl>
<Button
variant="outline"
className={cn(
"w-full h-12 justify-between transition-all duration-200",
!field.value && "text-muted-foreground",
)}
>
{selectedCity
? selectedCity.name
: "Shaharni tanlang"}
<ChevronsUpDown className="ml-2 h-4 opacity-50" />
</Button>
</FormControl>
</PopoverTrigger>
<AnimatePresence>
<motion.div
initial={{ opacity: 0, height: 0, y: -10 }}
animate={{
opacity: 1,
height: "auto",
y: 0,
transition: {
height: { duration: 0.3 },
opacity: { duration: 0.3, delay: 0.1 },
y: { duration: 0.3 },
},
}}
exit={{
opacity: 0,
height: 0,
y: -10,
transition: {
height: { duration: 0.25 },
opacity: { duration: 0.15 },
},
}}
style={{ overflow: "hidden" }}
>
<PopoverContent className="!w-96 p-0">
<Command shouldFilter={false}>
<CommandInput
placeholder="Qidirish..."
value={searchCity}
onValueChange={setSearchCity}
/>
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ delay: 0.1 }}
>
<Button
className="w-fit ml-2 p-1 cursor-pointer hover:bg-accent transition-colors"
size={"icon"}
variant={"ghost"}
onClick={() => {
handleCountrySelect(null);
setOpenCity(false);
setTimeout(() => setOpenCountry(true), 200);
}}
>
<MoveLeft className="mr-1" />
<p>Orqaga</p>
</Button>
</motion.div>
<CommandList>
{regionLoad ? (
<div className="py-6 text-center">
<Loader2 className="animate-spin mx-auto" />
</div>
) : (
<CommandGroup>
{regionData?.results.map((r, index) => (
<motion.div
key={r.id}
initial={{ opacity: 0, x: -20 }}
animate={{ opacity: 1, x: 0 }}
transition={{
duration: 0.2,
delay: index * 0.03,
ease: "easeOut",
}}
>
<CommandItem
onSelect={() => {
field.onChange(String(r.id));
setOpenCity(false);
}}
className="cursor-pointer transition-colors"
>
<Check
className={cn(
"mr-2 h-4 transition-opacity duration-200",
field.value === String(r.id)
? "opacity-100"
: "opacity-0",
)}
/>
{r.name}
</CommandItem>
</motion.div>
))}
</CommandGroup>
)}
</CommandList>
</Command>
</PopoverContent>
</motion.div>
</AnimatePresence>
</Popover>
)}
<FormMessage />
</FormItem>
);
}}
/>
<FormField
{/* <FormField
control={form.control}
name="departure_ru"
render={({ field }) => (
@@ -653,27 +936,234 @@ const StepOne = ({
<FormMessage />
</FormItem>
)}
/>
/> */}
<FormField
control={form.control}
name="destination"
render={({ field }) => (
<FormItem>
<Label className="text-md">{t("Borish joyi")}</Label>
<FormControl>
<Input
placeholder="Dubai"
{...field}
className="h-12 !text-md"
/>
</FormControl>
<FormMessage />
</FormItem>
)}
control={form.control}
render={({ field }) => {
const selectedCity = regionDataDes?.results.find(
(u) => String(u.id) === field.value,
);
return (
<FormItem className="flex flex-col gap-2">
<Label className="text-md">Borish joyi</Label>
{/* COUNTRY DROPDOWN */}
{!selectedCountryDes && (
<Popover
open={openCountryDes}
onOpenChange={setOpenCountryDes}
>
<PopoverTrigger asChild>
<FormControl>
<Button
variant="outline"
className={cn(
"w-full h-12 justify-between transition-all duration-200",
!selectedCountryDes && "text-muted-foreground",
)}
>
{selectedCountryDes
? countryDataDes?.results.find(
(c) => c.id === selectedCountryDes,
)?.name
: "Davlatni tanlang"}
<ChevronsUpDown className="ml-2 h-4 opacity-50" />
</Button>
</FormControl>
</PopoverTrigger>
<AnimatePresence mode="wait">
<motion.div
initial={{ opacity: 0, y: -10 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -10 }}
transition={{ duration: 0.3, ease: "easeOut" }}
>
<PopoverContent className="!w-96 p-0">
<Command shouldFilter={false}>
<CommandInput
placeholder="Qidirish..."
value={searchCountryDes}
onValueChange={setSearchCountryDes}
/>
<CommandList>
{countryLoadDes ? (
<div className="py-6 text-center">
<Loader2 className="animate-spin mx-auto" />
</div>
) : (
<CommandGroup>
{countryDataDes?.results.map((c, index) => (
<motion.div
key={c.id}
initial={{ opacity: 0, x: -20 }}
animate={{ opacity: 1, x: 0 }}
transition={{
duration: 0.2,
delay: index * 0.03,
ease: "easeOut",
}}
>
<CommandItem
onSelect={() => {
handleCountrySelectDes(c.id);
setOpenCountryDes(false);
setTimeout(
() => setOpenCityDes(true),
200,
);
}}
className="cursor-pointer transition-colors"
>
<Check
className={cn(
"mr-2 h-4 transition-opacity duration-200",
selectedCountryDes === c.id
? "opacity-100"
: "opacity-0",
)}
/>
{c.name}
</CommandItem>
</motion.div>
))}
</CommandGroup>
)}
</CommandList>
</Command>
</PopoverContent>
</motion.div>
</AnimatePresence>
</Popover>
)}
{selectedCountryDes && (
<Popover open={openCityDes} onOpenChange={setOpenCityDes}>
<PopoverTrigger asChild>
<FormControl>
<Button
variant="outline"
className={cn(
"w-full h-12 justify-between transition-all duration-200",
!field.value && "text-muted-foreground",
)}
>
{selectedCity
? selectedCity.name
: "Shaharni tanlang"}
<ChevronsUpDown className="ml-2 h-4 opacity-50" />
</Button>
</FormControl>
</PopoverTrigger>
<AnimatePresence>
<motion.div
initial={{ opacity: 0, height: 0, y: -10 }}
animate={{
opacity: 1,
height: "auto",
y: 0,
transition: {
height: { duration: 0.3 },
opacity: { duration: 0.3, delay: 0.1 },
y: { duration: 0.3 },
},
}}
exit={{
opacity: 0,
height: 0,
y: -10,
transition: {
height: { duration: 0.25 },
opacity: { duration: 0.15 },
},
}}
style={{ overflow: "hidden" }}
>
<PopoverContent className="!w-96 p-0">
<Command shouldFilter={false}>
<CommandInput
placeholder="Qidirish..."
value={searchCityDes}
onValueChange={setSearchCityDes}
/>
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ delay: 0.1 }}
>
<Button
className="w-fit ml-2 p-1 cursor-pointer hover:bg-accent transition-colors"
size={"icon"}
variant={"ghost"}
onClick={() => {
handleCountrySelectDes(null);
setOpenCityDes(false);
setTimeout(
() => setOpenCountryDes(true),
200,
);
}}
>
<MoveLeft className="mr-1" />
<p>Orqaga</p>
</Button>
</motion.div>
<CommandList>
{regionLoadDes ? (
<div className="py-6 text-center">
<Loader2 className="animate-spin mx-auto" />
</div>
) : (
<CommandGroup>
{regionDataDes?.results.map((r, index) => (
<motion.div
key={r.id}
initial={{ opacity: 0, x: -20 }}
animate={{ opacity: 1, x: 0 }}
transition={{
duration: 0.2,
delay: index * 0.03,
ease: "easeOut",
}}
>
<CommandItem
onSelect={() => {
field.onChange(String(r.id));
setOpenCityDes(false);
}}
className="cursor-pointer transition-colors"
>
<Check
className={cn(
"mr-2 h-4 transition-opacity duration-200",
field.value === String(r.id)
? "opacity-100"
: "opacity-0",
)}
/>
{r.name}
</CommandItem>
</motion.div>
))}
</CommandGroup>
)}
</CommandList>
</Command>
</PopoverContent>
</motion.div>
</AnimatePresence>
</Popover>
)}
<FormMessage />
</FormItem>
);
}}
/>
<FormField
{/* <FormField
control={form.control}
name="destination_ru"
render={({ field }) => (
@@ -689,7 +1179,7 @@ const StepOne = ({
<FormMessage />
</FormItem>
)}
/>
/> */}
<FormField
control={form.control}