bug fix
This commit is contained in:
@@ -2,7 +2,7 @@
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" href="/Logo_blue.png" />
|
||||
<link rel="icon" href="/Logo.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Simple Travel</title>
|
||||
</head>
|
||||
|
||||
4
public/Logo.svg
Normal file
4
public/Logo.svg
Normal file
@@ -0,0 +1,4 @@
|
||||
<svg width="219" height="224" viewBox="0 0 219 224" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M75.0372 0.24821C65.181 2.14821 52.5935 7.01696 45.706 11.6482C42.9747 13.4295 40.2435 14.9732 39.6497 14.9732C39.056 14.9732 33.7122 19.6045 28.0122 25.3045C12.8122 40.3857 5.33097 54.042 1.05597 75.1795C-1.08153 85.0357 0.224719 109.498 3.19347 119.473C7.70597 134.198 15.0685 145.836 27.656 158.542C41.5497 172.317 52.7122 178.967 70.881 183.598C93.3247 189.536 122.893 185.142 141.775 173.267C144.268 171.842 146.762 170.536 147.356 170.536C147.831 170.536 159.112 181.104 172.412 193.929C203.525 224.329 206.256 226.229 213.856 222.192C216.943 220.648 218.725 216.729 218.725 211.386C218.725 210.436 206.493 197.492 191.412 182.529C176.331 167.567 164.1 154.623 164.1 153.673C164.1 152.723 166.237 149.636 168.731 146.904C171.343 144.054 175.618 137.761 178.112 132.654C200.793 88.9545 181.912 31.8357 137.618 9.74821C122.418 2.14821 115.531 0.60446 95.2247 0.12946C85.7247 -0.10804 76.6997 0.0107099 75.0372 0.24821ZM105.318 19.9607C118.856 21.8607 133.581 29.5795 144.981 40.8607C160.537 56.2982 166.95 71.9732 166.95 95.1295C166.95 107.123 166.593 109.023 163.15 117.692C153.412 142.036 133.343 158.304 107.575 162.817C94.5122 165.073 89.0497 165.073 77.7685 162.698C51.7622 157.354 30.8622 136.573 23.9747 109.023C19.6997 92.042 21.1247 79.217 29.1997 61.8795C38.8185 41.3357 56.5122 26.492 77.7685 20.9107C87.1497 18.417 92.731 18.1795 105.318 19.9607Z" fill="#084FE3"/>
|
||||
<path d="M122.537 62.5919C118.975 64.7294 113.631 68.0544 110.662 70.1919C101.043 76.9607 101.518 76.8419 87.981 73.6357C81.2122 72.0919 70.5247 69.7169 64.3497 68.4107L53.0685 66.0357L51.0497 68.5294C47.3685 72.9232 51.406 77.0794 67.556 85.7482C72.306 88.2419 76.4622 91.0919 76.8185 91.9232C77.6497 94.0607 70.0497 101.661 67.1997 101.661C65.8935 101.661 61.2622 100.473 56.8685 99.0482C50.8122 97.1482 48.1997 96.7919 46.7747 97.6232C44.3997 99.1669 43.4497 104.748 45.231 106.886C47.4872 109.498 65.0622 119.473 67.556 119.473C70.406 119.473 82.5185 111.992 112.562 91.5669C136.668 75.1794 136.906 75.0607 139.043 71.0232C140.706 67.9357 139.993 65.0857 136.906 61.4044C133.937 57.9607 130.256 58.3169 122.537 62.5919Z" fill="#084FE3"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.2 KiB |
@@ -33,6 +33,7 @@ import UserDetail from "@/pages/users/ui/UserDetail";
|
||||
import { getMe } from "@/shared/config/api/auth/api";
|
||||
import "@/shared/config/i18n";
|
||||
import { getAuthToken } from "@/shared/lib/authCookies";
|
||||
import { cn } from "@/shared/lib/utils";
|
||||
import { Sidebar } from "@/widgets/sidebar/ui/Sidebar";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { useEffect } from "react";
|
||||
@@ -77,6 +78,12 @@ const App = () => {
|
||||
<div className="flex max-lg:flex-col bg-gray-900">
|
||||
{shouldShowSidebar && <Sidebar role={user ? user.role : "admin"} />}
|
||||
|
||||
<main
|
||||
className={cn(
|
||||
"flex-1 min-h-screen bg-gray-900 transition-all",
|
||||
shouldShowSidebar ? "lg:ml-64" : "ml-0"
|
||||
)}
|
||||
>
|
||||
<Routes>
|
||||
<Route path="/" element={<Navigate to={"/user"} />} />
|
||||
<Route path="/user" element={<UserList />} />
|
||||
@@ -118,6 +125,7 @@ const App = () => {
|
||||
<Route path="/site-settings/" element={<TourSettings />} />
|
||||
<Route path="/site-banner/" element={<SiteBannerAdmin />} />
|
||||
</Routes>
|
||||
</main>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import type {
|
||||
AllAmenitiesData,
|
||||
CreateTourRes,
|
||||
DetailAmenitiesData,
|
||||
GetAllTours,
|
||||
GetDetailTours,
|
||||
GetHotelRes,
|
||||
@@ -19,6 +21,7 @@ import type {
|
||||
} from "@/pages/tours/lib/type";
|
||||
import httpClient from "@/shared/config/api/httpClient";
|
||||
import {
|
||||
AMENITIES,
|
||||
GET_TICKET,
|
||||
HOTEL,
|
||||
HOTEL_BADGE,
|
||||
@@ -444,13 +447,71 @@ const hotelFeatureTypeDelete = async ({ id }: { id: number }) => {
|
||||
return response;
|
||||
};
|
||||
|
||||
//Amenities
|
||||
|
||||
const getAllAmenities = async ({
|
||||
page,
|
||||
page_size,
|
||||
}: {
|
||||
page_size: number;
|
||||
page: number;
|
||||
}): Promise<AxiosResponse<AllAmenitiesData>> => {
|
||||
const response = await httpClient.get(AMENITIES, {
|
||||
params: { page, page_size },
|
||||
});
|
||||
return response;
|
||||
};
|
||||
|
||||
const amenitiesCreate = async ({
|
||||
body,
|
||||
}: {
|
||||
body: {
|
||||
name: string;
|
||||
name_ru: string;
|
||||
icon_name: string;
|
||||
};
|
||||
}) => {
|
||||
const response = await httpClient.post(AMENITIES, body);
|
||||
return response;
|
||||
};
|
||||
|
||||
const deleteAmenities = async ({ id }: { id: number }) => {
|
||||
const response = await httpClient.delete(`${AMENITIES}${id}/`);
|
||||
return response;
|
||||
};
|
||||
|
||||
const getDetailAmenities = async ({
|
||||
id,
|
||||
}: {
|
||||
id: number;
|
||||
}): Promise<AxiosResponse<DetailAmenitiesData>> => {
|
||||
const response = await httpClient.get(`${AMENITIES}${id}/`);
|
||||
return response;
|
||||
};
|
||||
|
||||
const amenitiesUpdate = async ({
|
||||
id,
|
||||
body,
|
||||
}: {
|
||||
id: number;
|
||||
body: { name: string; name_ru: string; icon_name: string };
|
||||
}) => {
|
||||
const response = await httpClient.patch(`${AMENITIES}${id}/`, body);
|
||||
return response;
|
||||
};
|
||||
|
||||
export {
|
||||
addedPopularTours,
|
||||
amenitiesCreate,
|
||||
amenitiesUpdate,
|
||||
createHotel,
|
||||
createTours,
|
||||
deleteAmenities,
|
||||
deleteTours,
|
||||
editHotel,
|
||||
getAllAmenities,
|
||||
getAllTours,
|
||||
getDetailAmenities,
|
||||
getDetailToursId,
|
||||
getHotel,
|
||||
getOneTours,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import type {
|
||||
AllAmenitiesDataRes,
|
||||
Badge,
|
||||
HotelFeatures,
|
||||
HotelFeaturesType,
|
||||
@@ -10,6 +11,8 @@ import type {
|
||||
} from "@/pages/tours/lib/type";
|
||||
import { Button } from "@/shared/ui/button";
|
||||
import { type ColumnDef } from "@tanstack/react-table";
|
||||
import * as LucideIcons from "lucide-react";
|
||||
import { XIcon } from "lucide-react";
|
||||
import type { Dispatch, SetStateAction } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
@@ -305,3 +308,59 @@ export const FeatureTypeColumns = (
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
export const AmenitiesColumns = (
|
||||
onEdit: (id: number) => void,
|
||||
onDelete: (id: number) => void,
|
||||
t: (key: string) => string,
|
||||
): ColumnDef<AllAmenitiesDataRes>[] => [
|
||||
{
|
||||
accessorKey: "id",
|
||||
header: "ID",
|
||||
cell: ({ row }) => {
|
||||
return <span>{row.original.id}</span>;
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: "name",
|
||||
header: t("Nomi"),
|
||||
cell: ({ row }) => <span>{row.original.name}</span>,
|
||||
},
|
||||
{
|
||||
accessorKey: "icon_name",
|
||||
header: t("Icon"),
|
||||
cell: ({ row }) => {
|
||||
const Icon = (LucideIcons as any)[row.original.icon_name] || XIcon;
|
||||
return (
|
||||
<span>
|
||||
<Icon className="size-4" />
|
||||
</span>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "actions",
|
||||
header: () => <div className="text-right">{t("Harakatlar")}</div>,
|
||||
cell: ({ row }) => {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<div className="flex justify-end gap-2">
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={() => onEdit(row.original.id)}
|
||||
>
|
||||
{t("Tahrirlash")}
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="destructive"
|
||||
onClick={() => onDelete(row.original.id)}
|
||||
>
|
||||
{t("O'chirish")}
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
@@ -127,15 +127,7 @@ export const TourformSchema = z.object({
|
||||
images: z
|
||||
.array(z.union([z.instanceof(File), z.string()]))
|
||||
.min(1, { message: "Kamida bitta rasm yuklang." }),
|
||||
amenities: z
|
||||
.array(
|
||||
z.object({
|
||||
name: z.string().min(1, { message: "Majburiy maydon" }),
|
||||
name_ru: z.string().min(1, { message: "Majburiy maydon" }),
|
||||
icon_name: z.string().min(1, { message: "Majburiy maydon" }),
|
||||
}),
|
||||
)
|
||||
.min(1, { message: "Kamida bitta qulaylik kiriting." }),
|
||||
amenities: z.array(z.number()).optional(),
|
||||
|
||||
// 🔹 Quyidagilar endi ixtiyoriy (required emas)
|
||||
hotel_services: z
|
||||
|
||||
@@ -1,36 +1,41 @@
|
||||
import { create } from "zustand";
|
||||
|
||||
interface Amenity {
|
||||
name: string;
|
||||
name_ru: string;
|
||||
icon_name: string;
|
||||
}
|
||||
|
||||
interface TicketStore {
|
||||
amenities: Amenity[];
|
||||
amenities: number[];
|
||||
id: number | null;
|
||||
setId: (id: number) => void;
|
||||
setAmenities: (amenities: Amenity[]) => void;
|
||||
addAmenity: (amenity: Amenity) => void;
|
||||
removeAmenity: (index: number) => void;
|
||||
updateAmenity: (index: number, updated: Partial<Amenity>) => void;
|
||||
setAmenities: (amenities: number[]) => void;
|
||||
addAmenity: (amenity: number) => void;
|
||||
removeAmenity: (id: number) => void;
|
||||
updateAmenity: (index: number, updated: number) => void;
|
||||
}
|
||||
|
||||
export const useTicketStore = create<TicketStore>((set) => ({
|
||||
amenities: [],
|
||||
id: null,
|
||||
|
||||
setId: (id) => set({ id }),
|
||||
|
||||
setAmenities: (amenities) => set({ amenities }),
|
||||
|
||||
addAmenity: (amenity) =>
|
||||
set((state) => ({ amenities: [...state.amenities, amenity] })),
|
||||
removeAmenity: (index) =>
|
||||
set((state) => {
|
||||
// agar qulaylik allaqachon mavjud bo‘lsa, qo‘shmaydi
|
||||
if (state.amenities.includes(amenity)) return state;
|
||||
return { amenities: [...state.amenities, amenity] };
|
||||
}),
|
||||
|
||||
removeAmenity: (id) =>
|
||||
set((state) => ({
|
||||
amenities: state.amenities.filter((_, i) => i !== index),
|
||||
amenities: state.amenities.filter((a) => a !== id),
|
||||
})),
|
||||
|
||||
updateAmenity: (index, updated) =>
|
||||
set((state) => ({
|
||||
amenities: state.amenities.map((a, i) =>
|
||||
i === index ? { ...a, ...updated } : a,
|
||||
),
|
||||
})),
|
||||
set((state) => {
|
||||
const newAmenities = [...state.amenities];
|
||||
if (index >= 0 && index < newAmenities.length) {
|
||||
newAmenities[index] = updated;
|
||||
}
|
||||
return { amenities: newAmenities };
|
||||
}),
|
||||
}));
|
||||
|
||||
@@ -75,14 +75,7 @@ export interface GetOneTours {
|
||||
image: string;
|
||||
},
|
||||
];
|
||||
ticket_amenities: [
|
||||
{
|
||||
name: string;
|
||||
name_ru: string;
|
||||
name_uz: string;
|
||||
icon_name: string;
|
||||
},
|
||||
];
|
||||
ticket_amenities: number[];
|
||||
ticket_included_services: [
|
||||
{
|
||||
image: string;
|
||||
@@ -530,3 +523,37 @@ export interface GetHotelRes {
|
||||
];
|
||||
};
|
||||
}
|
||||
|
||||
export interface AllAmenitiesData {
|
||||
status: boolean;
|
||||
data: {
|
||||
links: {
|
||||
previous: string;
|
||||
next: string;
|
||||
};
|
||||
total_items: number;
|
||||
total_pages: number;
|
||||
page_size: number;
|
||||
current_page: number;
|
||||
results: AllAmenitiesDataRes[];
|
||||
};
|
||||
}
|
||||
|
||||
export interface AllAmenitiesDataRes {
|
||||
id: number;
|
||||
name: string;
|
||||
name_ru: string;
|
||||
name_uz: string;
|
||||
icon_name: string;
|
||||
}
|
||||
|
||||
export interface DetailAmenitiesData {
|
||||
status: boolean;
|
||||
data: {
|
||||
id: number;
|
||||
name: string;
|
||||
name_ru: string;
|
||||
name_uz: string;
|
||||
icon_name: string;
|
||||
};
|
||||
}
|
||||
|
||||
357
src/pages/tours/ui/Amenities.tsx
Normal file
357
src/pages/tours/ui/Amenities.tsx
Normal file
@@ -0,0 +1,357 @@
|
||||
import {
|
||||
amenitiesCreate,
|
||||
amenitiesUpdate,
|
||||
deleteAmenities,
|
||||
getDetailAmenities,
|
||||
} from "@/pages/tours/lib/api";
|
||||
import { AmenitiesColumns } from "@/pages/tours/lib/column";
|
||||
import type { AllAmenitiesDataRes } from "@/pages/tours/lib/type";
|
||||
import { Button } from "@/shared/ui/button";
|
||||
import { Dialog, DialogContent } from "@/shared/ui/dialog";
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from "@/shared/ui/form";
|
||||
import { Input } from "@/shared/ui/input";
|
||||
import IconSelect from "@/shared/ui/iocnSelect";
|
||||
import { Label } from "@/shared/ui/label";
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "@/shared/ui/table";
|
||||
import RealPagination from "@/widgets/real-pagination/ui/RealPagination";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import {
|
||||
flexRender,
|
||||
getCoreRowModel,
|
||||
useReactTable,
|
||||
} from "@tanstack/react-table";
|
||||
import { Loader, PlusIcon } from "lucide-react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { toast } from "sonner";
|
||||
import z from "zod";
|
||||
|
||||
const formSchema = z.object({
|
||||
name: z.string().min(1, { message: "Majburiy maydon" }),
|
||||
name_ru: z.string().min(1, { message: "Majburiy maydon" }),
|
||||
icon_name: z.string().min(1, { message: "Majburiy maydon" }),
|
||||
});
|
||||
|
||||
const Amenities = ({
|
||||
data,
|
||||
page,
|
||||
pageSize,
|
||||
}: {
|
||||
page: number;
|
||||
pageSize: number;
|
||||
data:
|
||||
| {
|
||||
links: {
|
||||
previous: string;
|
||||
next: string;
|
||||
};
|
||||
total_items: number;
|
||||
total_pages: number;
|
||||
page_size: number;
|
||||
current_page: number;
|
||||
results: AllAmenitiesDataRes[];
|
||||
}
|
||||
| undefined;
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const [open, setOpen] = useState<boolean>(false);
|
||||
const [editId, setEditId] = useState<number | null>(null);
|
||||
const queryClient = useQueryClient();
|
||||
const [types, setTypes] = useState<"edit" | "create">("create");
|
||||
|
||||
const handleEdit = (id: number) => {
|
||||
setTypes("edit");
|
||||
setOpen(true);
|
||||
setEditId(id);
|
||||
};
|
||||
|
||||
const { data: badgeDetail } = useQuery({
|
||||
queryKey: ["detail_amenities", editId],
|
||||
queryFn: () => getDetailAmenities({ id: editId! }),
|
||||
enabled: !!editId,
|
||||
});
|
||||
|
||||
const { mutate: deleteMutate } = useMutation({
|
||||
mutationFn: ({ id }: { id: number }) => deleteAmenities({ id }),
|
||||
onSuccess: () => {
|
||||
queryClient.refetchQueries({ queryKey: ["detail_amenities"] });
|
||||
queryClient.refetchQueries({ queryKey: ["all_amenities"] });
|
||||
setOpen(false);
|
||||
form.reset();
|
||||
},
|
||||
onError: () => {
|
||||
toast.error(t("Xatolik yuz berdi"), {
|
||||
position: "top-center",
|
||||
richColors: true,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
const handleDelete = (id: number) => {
|
||||
deleteMutate({ id });
|
||||
};
|
||||
|
||||
const columns = AmenitiesColumns(handleEdit, handleDelete, t);
|
||||
|
||||
const { mutate: create, isPending } = useMutation({
|
||||
mutationFn: ({
|
||||
body,
|
||||
}: {
|
||||
body: { name: string; name_ru: string; icon_name: string };
|
||||
}) => amenitiesCreate({ body }),
|
||||
onSuccess: () => {
|
||||
queryClient.refetchQueries({ queryKey: ["detail_amenities"] });
|
||||
queryClient.refetchQueries({ queryKey: ["all_amenities"] });
|
||||
setOpen(false);
|
||||
form.reset();
|
||||
},
|
||||
onError: () => {
|
||||
toast.error(t("Xatolik yuz berdi"), {
|
||||
position: "top-center",
|
||||
richColors: true,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
const { mutate: update, isPending: updatePending } = useMutation({
|
||||
mutationFn: ({
|
||||
body,
|
||||
id,
|
||||
}: {
|
||||
id: number;
|
||||
body: { name: string; name_ru: string; icon_name: string };
|
||||
}) => amenitiesUpdate({ body, id }),
|
||||
onSuccess: () => {
|
||||
queryClient.refetchQueries({ queryKey: ["detail_amenities"] });
|
||||
queryClient.refetchQueries({ queryKey: ["all_amenities"] });
|
||||
setOpen(false);
|
||||
form.reset();
|
||||
},
|
||||
onError: () => {
|
||||
toast.error(t("Xatolik yuz berdi"), {
|
||||
position: "top-center",
|
||||
richColors: true,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
const form = useForm<z.infer<typeof formSchema>>({
|
||||
resolver: zodResolver(formSchema),
|
||||
defaultValues: {
|
||||
name: "",
|
||||
name_ru: "",
|
||||
},
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (badgeDetail) {
|
||||
form.setValue("icon_name", badgeDetail.data.data.icon_name);
|
||||
form.setValue("name", badgeDetail.data.data.name_uz);
|
||||
form.setValue("name_ru", badgeDetail.data.data.name_ru);
|
||||
}
|
||||
}, [editId, badgeDetail]);
|
||||
|
||||
function onSubmit(values: z.infer<typeof formSchema>) {
|
||||
if (types === "create") {
|
||||
create({
|
||||
body: {
|
||||
icon_name: values.icon_name,
|
||||
name: values.name,
|
||||
name_ru: values.name_ru,
|
||||
},
|
||||
});
|
||||
} else if (types === "edit" && editId) {
|
||||
update({
|
||||
id: editId,
|
||||
body: {
|
||||
icon_name: values.icon_name,
|
||||
name: values.name,
|
||||
name_ru: values.name_ru,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const table = useReactTable({
|
||||
data: data?.results ?? [],
|
||||
columns,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
manualPagination: true,
|
||||
pageCount: data?.total_pages ?? 0,
|
||||
state: {
|
||||
pagination: {
|
||||
pageIndex: page - 1,
|
||||
pageSize: pageSize,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flex justify-end">
|
||||
<Button
|
||||
variant="default"
|
||||
onClick={() => {
|
||||
setOpen(true);
|
||||
setTypes("create");
|
||||
form.reset();
|
||||
}}
|
||||
>
|
||||
<PlusIcon className="mr-2" />
|
||||
{t("Qo‘shish")}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="border border-gray-700 rounded-md overflow-hidden mt-4">
|
||||
<Table key={data?.current_page}>
|
||||
<TableHeader>
|
||||
{table.getHeaderGroups().map((headerGroup) => (
|
||||
<TableRow key={headerGroup.id}>
|
||||
{headerGroup.headers.map((header) => (
|
||||
<TableHead key={header.id}>
|
||||
{header.isPlaceholder
|
||||
? null
|
||||
: flexRender(
|
||||
header.column.columnDef.header,
|
||||
header.getContext(),
|
||||
)}
|
||||
</TableHead>
|
||||
))}
|
||||
</TableRow>
|
||||
))}
|
||||
</TableHeader>
|
||||
|
||||
<TableBody>
|
||||
{table.getRowModel().rows?.length ? (
|
||||
table.getRowModel().rows.map((row) => (
|
||||
<TableRow key={row.id}>
|
||||
{row.getVisibleCells().map((cell) => (
|
||||
<TableCell key={cell.id}>
|
||||
{flexRender(
|
||||
cell.column.columnDef.cell,
|
||||
cell.getContext(),
|
||||
)}
|
||||
</TableCell>
|
||||
))}
|
||||
</TableRow>
|
||||
))
|
||||
) : (
|
||||
<TableRow>
|
||||
<TableCell
|
||||
colSpan={columns.length}
|
||||
className="h-24 text-center text-gray-400"
|
||||
>
|
||||
{t("Ma'lumot topilmadi")}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
|
||||
<RealPagination
|
||||
table={table}
|
||||
totalPages={data?.total_pages}
|
||||
namePage="pageAmenities"
|
||||
namePageSize="pageSizeAmenities"
|
||||
/>
|
||||
|
||||
<Dialog open={open} onOpenChange={setOpen}>
|
||||
<DialogContent>
|
||||
<p className="text-xl">
|
||||
{types === "create" ? t("Saqlash") : t("Tahrirlash")}
|
||||
</p>
|
||||
<Form {...form}>
|
||||
<form
|
||||
onSubmit={form.handleSubmit(onSubmit)}
|
||||
className="space-y-6 p-2"
|
||||
>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="icon_name"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<Label className="text-md">{t("Qulayliklar")}</Label>
|
||||
|
||||
<div className="flex flex-col gap-4 w-full">
|
||||
<IconSelect
|
||||
selectedIcon={field.value ?? ""}
|
||||
setSelectedIcon={(val: string) => field.onChange(val)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="name"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>{t("Nomi")}</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder={t("Nomi")} {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="name_ru"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>{t("Nomi (ru)")}</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder={t("Nomi (ru)")} {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<div className="flex justify-end gap-4">
|
||||
<Button
|
||||
type="button"
|
||||
onClick={() => setOpen(false)}
|
||||
className="border-slate-600 text-white hover:bg-gray-600 bg-gray-600"
|
||||
>
|
||||
{t("Bekor qilish")}
|
||||
</Button>
|
||||
<Button type="submit">
|
||||
{isPending || updatePending ? (
|
||||
<Loader className="animate-spin" />
|
||||
) : types === "create" ? (
|
||||
t("Saqlash")
|
||||
) : (
|
||||
t("Tahrirlash")
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</Form>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default Amenities;
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
import {
|
||||
createTours,
|
||||
getAllAmenities,
|
||||
hotelBadge,
|
||||
hotelTarif,
|
||||
hotelTransport,
|
||||
@@ -29,14 +30,12 @@ import {
|
||||
FormMessage,
|
||||
} from "@/shared/ui/form";
|
||||
import { Input } from "@/shared/ui/input";
|
||||
import IconSelect from "@/shared/ui/iocnSelect";
|
||||
import { Label } from "@/shared/ui/label";
|
||||
import { Popover, PopoverContent, PopoverTrigger } from "@/shared/ui/popover";
|
||||
import { RadioGroup, RadioGroupItem } from "@/shared/ui/radio-group";
|
||||
import { Textarea } from "@/shared/ui/textarea";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { useMutation, useQuery } from "@tanstack/react-query";
|
||||
import * as LucideIcons from "lucide-react";
|
||||
import { ChevronDownIcon, SquareCheckBig, XIcon } from "lucide-react";
|
||||
import { useEffect, useState, type Dispatch, type SetStateAction } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
@@ -105,7 +104,7 @@ const StepOne = ({
|
||||
},
|
||||
});
|
||||
|
||||
const { addAmenity, setId } = useTicketStore();
|
||||
const { addAmenity, setId, removeAmenity } = useTicketStore();
|
||||
|
||||
useEffect(() => {
|
||||
if (!isEditMode || !data?.data) return;
|
||||
@@ -158,14 +157,7 @@ const StepOne = ({
|
||||
}
|
||||
|
||||
// 🔹 Qulayliklar (amenities)
|
||||
form.setValue(
|
||||
"amenities",
|
||||
tour.ticket_amenities?.map((a) => ({
|
||||
name: a.name ?? "",
|
||||
name_ru: a.name_ru ?? "",
|
||||
icon_name: a.icon_name ?? "",
|
||||
})) ?? [],
|
||||
);
|
||||
form.setValue("amenities", tour.ticket_amenities);
|
||||
|
||||
// 🔹 Xizmatlar (hotel_services)
|
||||
form.setValue(
|
||||
@@ -258,7 +250,6 @@ const StepOne = ({
|
||||
const { watch, setValue } = form;
|
||||
const selectedDate = watch("departureDateTime.date");
|
||||
const selectedDateTravel = watch("travelDateTime.date");
|
||||
const [selectedIcon, setSelectedIcon] = useState("");
|
||||
|
||||
const { mutate: create } = useMutation({
|
||||
mutationFn: (body: FormData) => {
|
||||
@@ -341,24 +332,20 @@ const StepOne = ({
|
||||
value.badges?.forEach((e, i) => {
|
||||
formData.append(`badge[${i}]`, String(e));
|
||||
});
|
||||
value.amenities?.forEach((e, i) => {
|
||||
formData.append(`ticket_amenities[${i}]`, String(e));
|
||||
});
|
||||
value.images.forEach((e) => {
|
||||
if (e instanceof File) {
|
||||
formData.append("ticket_images", e);
|
||||
}
|
||||
});
|
||||
value.amenities.forEach((e, i) => {
|
||||
formData.append(`ticket_amenities[${i}]name`, e.name);
|
||||
formData.append(`ticket_amenities[${i}]name_ru`, e.name_ru);
|
||||
formData.append(`ticket_amenities[${i}]icon_name`, e.icon_name);
|
||||
addAmenity({
|
||||
icon_name: e.icon_name,
|
||||
name: e.name,
|
||||
name_ru: e.name_ru,
|
||||
});
|
||||
value.amenities?.forEach((e, i) => {
|
||||
formData.append(`badge[${i}]`, String(e));
|
||||
});
|
||||
value.hotel_services &&
|
||||
value.hotel_services.forEach((e, i) => {
|
||||
if (e instanceof File) {
|
||||
if (e.image instanceof File) {
|
||||
formData.append(`ticket_included_services[${i}]image`, e.image);
|
||||
formData.append(`ticket_included_services[${i}]title`, e.title);
|
||||
formData.append(`ticket_included_services[${i}]title_ru`, e.title_ru);
|
||||
@@ -366,31 +353,43 @@ const StepOne = ({
|
||||
formData.append(`ticket_included_services[${i}]desc`, e.description);
|
||||
}
|
||||
});
|
||||
value.ticket_itinerary.forEach((e, i) => {
|
||||
e.ticket_itinerary_image.forEach((l, f) => {
|
||||
if (e instanceof File) {
|
||||
formData.append(`ticket_itinerary[${i}]title`, e.title);
|
||||
formData.append(`ticket_itinerary[${i}]title_ru`, e.title_ru);
|
||||
formData.append(`ticket_itinerary[${i}]duration`, String(e.duration));
|
||||
value.ticket_itinerary?.forEach((itinerary, i) => {
|
||||
formData.append(`ticket_itinerary[${i}]title`, itinerary.title);
|
||||
formData.append(`ticket_itinerary[${i}]title_ru`, itinerary.title_ru);
|
||||
formData.append(
|
||||
`ticket_itinerary[${i}]ticket_itinerary_image[${f}]image`,
|
||||
l.image,
|
||||
`ticket_itinerary[${i}]duration`,
|
||||
String(itinerary.duration),
|
||||
);
|
||||
e.ticket_itinerary_destinations.forEach((e, f) => {
|
||||
|
||||
// Rasmlar
|
||||
if (Array.isArray(itinerary.ticket_itinerary_image)) {
|
||||
itinerary.ticket_itinerary_image.forEach((img, j) => {
|
||||
const file = img instanceof File ? img : img.image;
|
||||
if (file) {
|
||||
formData.append(
|
||||
`ticket_itinerary[${i}]ticket_itinerary_destinations[${f}]name`,
|
||||
String(e.name),
|
||||
`ticket_itinerary[${i}]ticket_itinerary_image[${j}]image`,
|
||||
file,
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Destinations
|
||||
if (Array.isArray(itinerary.ticket_itinerary_destinations)) {
|
||||
itinerary.ticket_itinerary_destinations.forEach((dest, k) => {
|
||||
formData.append(
|
||||
`ticket_itinerary[${i}]ticket_itinerary_destinations[${k}]name`,
|
||||
dest.name,
|
||||
);
|
||||
formData.append(
|
||||
`ticket_itinerary[${i}]ticket_itinerary_destinations[${f}]name_ru`,
|
||||
String(e.name_ru),
|
||||
`ticket_itinerary[${i}]ticket_itinerary_destinations[${k}]name_ru`,
|
||||
dest.name_ru,
|
||||
);
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
value.hotel_meals.forEach((e, i) => {
|
||||
if (e instanceof File) {
|
||||
if (e.image instanceof File) {
|
||||
formData.append(`ticket_hotel_meals[${i}]image`, e.image);
|
||||
formData.append(`ticket_hotel_meals[${i}]name`, e.title);
|
||||
formData.append(`ticket_hotel_meals[${i}]name_ru`, e.title_ru);
|
||||
@@ -419,13 +418,16 @@ const StepOne = ({
|
||||
}
|
||||
}
|
||||
|
||||
console.log(form.formState.errors);
|
||||
|
||||
const { data: badge } = useQuery({
|
||||
queryKey: ["all_badge"],
|
||||
queryFn: () => hotelBadge({ page: 1, page_size: 10 }),
|
||||
});
|
||||
|
||||
const { data: amenitiesData } = useQuery({
|
||||
queryKey: ["all_amenities"],
|
||||
queryFn: () => getAllAmenities({ page: 1, page_size: 10 }),
|
||||
});
|
||||
|
||||
const { data: tariff } = useQuery({
|
||||
queryKey: ["all_tarif"],
|
||||
queryFn: () => hotelTarif({ page: 1, page_size: 10 }),
|
||||
@@ -973,6 +975,114 @@ const StepOne = ({
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="amenities"
|
||||
render={() => (
|
||||
<FormItem>
|
||||
<Label className="text-md">{t("Qulayliklar")}</Label>
|
||||
|
||||
<div className="flex flex-wrap gap-2 mb-3">
|
||||
{(form.watch("amenities") ?? []).length > 0 &&
|
||||
(form.watch("amenities") ?? []).map((badgeId: number) => {
|
||||
const badgeItem =
|
||||
amenitiesData?.data?.data?.results?.find(
|
||||
(b: any) => b.id === badgeId,
|
||||
);
|
||||
|
||||
return (
|
||||
<Badge
|
||||
key={badgeId}
|
||||
variant="secondary"
|
||||
className="px-3 py-1 text-sm flex items-center gap-2"
|
||||
>
|
||||
{badgeItem?.name || `Qulaylilar #${badgeId}`}
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
const current = form.getValues("amenities") ?? [];
|
||||
form.setValue(
|
||||
"amenities",
|
||||
current.filter((b: number) => b !== badgeId),
|
||||
);
|
||||
}}
|
||||
className="ml-1 text-muted-foreground hover:text-destructive"
|
||||
>
|
||||
<XIcon className="size-4" />
|
||||
</button>
|
||||
</Badge>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
className="w-full h-12 justify-between font-normal"
|
||||
>
|
||||
{t("Qulaylik tanlang")}
|
||||
<ChevronDownIcon />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
|
||||
<PopoverContent className="p-0 w-[250px]" align="start">
|
||||
<Command>
|
||||
<CommandList>
|
||||
<CommandGroup heading={t("Mavjud qulayliklar")}>
|
||||
{badge?.data?.data?.results?.map((item: any) => {
|
||||
const currentBadges =
|
||||
form.getValues("amenities") ?? [];
|
||||
const selected = currentBadges.includes(item.id);
|
||||
|
||||
return (
|
||||
<CommandItem
|
||||
key={item.id}
|
||||
onSelect={() => {
|
||||
const selected = currentBadges.includes(
|
||||
item.id,
|
||||
);
|
||||
|
||||
if (selected) {
|
||||
removeAmenity(item.id);
|
||||
form.setValue(
|
||||
"amenities",
|
||||
currentBadges.filter(
|
||||
(b: number) => b !== item.id,
|
||||
),
|
||||
);
|
||||
} else {
|
||||
addAmenity(item.id);
|
||||
form.setValue("amenities", [
|
||||
...currentBadges,
|
||||
item.id,
|
||||
]);
|
||||
}
|
||||
}}
|
||||
className="flex items-center gap-2"
|
||||
>
|
||||
<SquareCheckBig
|
||||
className={`size-4 ${
|
||||
selected
|
||||
? "text-green-500"
|
||||
: "text-muted-foreground"
|
||||
}`}
|
||||
/>
|
||||
{item.name}
|
||||
</CommandItem>
|
||||
);
|
||||
})}
|
||||
</CommandGroup>
|
||||
</CommandList>
|
||||
</Command>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="tarif"
|
||||
@@ -1005,8 +1115,10 @@ const StepOne = ({
|
||||
onChange={(e) => {
|
||||
const raw = e.target.value.replace(/\D/g, "");
|
||||
const num = Number(raw);
|
||||
const currentTarifs = form.getValues("tarif") || [];
|
||||
const updatedTransport = currentTarifs.map((t, i) =>
|
||||
const currentTarifs =
|
||||
form.getValues("tarif") || [];
|
||||
const updatedTransport = currentTarifs.map(
|
||||
(t, i) =>
|
||||
i === idx ? { ...t, price: num } : t,
|
||||
);
|
||||
|
||||
@@ -1057,7 +1169,9 @@ const StepOne = ({
|
||||
<CommandGroup heading={t("Mavjud tariflar")}>
|
||||
{tariff?.data?.data?.results?.map((item: any) => {
|
||||
const currentTarifs = form.getValues("tarif") || [];
|
||||
const selected = currentTarifs.some((t) => t.tariff === item.id);
|
||||
const selected = currentTarifs.some(
|
||||
(t) => t.tariff === item.id,
|
||||
);
|
||||
|
||||
return (
|
||||
<CommandItem
|
||||
@@ -1137,8 +1251,10 @@ const StepOne = ({
|
||||
onChange={(e) => {
|
||||
const raw = e.target.value.replace(/\D/g, "");
|
||||
const num = Number(raw);
|
||||
const currentTransports = form.getValues("transport") || [];
|
||||
const updatedTransport = currentTransports.map((t, i) =>
|
||||
const currentTransports =
|
||||
form.getValues("transport") || [];
|
||||
const updatedTransport = currentTransports.map(
|
||||
(t, i) =>
|
||||
i === idx ? { ...t, price: num } : t,
|
||||
);
|
||||
|
||||
@@ -1190,14 +1306,18 @@ const StepOne = ({
|
||||
<CommandList>
|
||||
<CommandGroup heading={t("Mavjud transportlar")}>
|
||||
{transport?.data?.data?.results?.map((item: any) => {
|
||||
const currentTransports = form.getValues("transport") || [];
|
||||
const selected = currentTransports.some((t) => t.transport === item.id);
|
||||
const currentTransports =
|
||||
form.getValues("transport") || [];
|
||||
const selected = currentTransports.some(
|
||||
(t) => t.transport === item.id,
|
||||
);
|
||||
|
||||
return (
|
||||
<CommandItem
|
||||
key={item.id}
|
||||
onSelect={() => {
|
||||
const current = form.getValues("transport") || [];
|
||||
const current =
|
||||
form.getValues("transport") || [];
|
||||
if (selected) {
|
||||
form.setValue(
|
||||
"transport",
|
||||
@@ -1279,99 +1399,6 @@ const StepOne = ({
|
||||
imageUrl={data?.data.ticket_images?.map((img) => img.image)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="amenities"
|
||||
render={() => (
|
||||
<FormItem>
|
||||
<Label className="text-md">{t("Qulayliklar")}</Label>
|
||||
|
||||
<div className="flex flex-col gap-4">
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{form.watch("amenities").map((item, idx) => {
|
||||
const Icon = (LucideIcons as any)[item.icon_name] || XIcon;
|
||||
return (
|
||||
<Badge
|
||||
key={idx}
|
||||
variant="secondary"
|
||||
className="px-3 py-1 text-sm flex items-center gap-2"
|
||||
>
|
||||
<Icon className="size-4" />
|
||||
<span>{item.name}</span>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
const current = form.getValues("amenities");
|
||||
form.setValue(
|
||||
"amenities",
|
||||
current.filter((_, i: number) => i !== idx),
|
||||
);
|
||||
}}
|
||||
className="ml-1 text-muted-foreground hover:text-destructive"
|
||||
>
|
||||
<XIcon className="size-4" />
|
||||
</button>
|
||||
</Badge>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<div className="flex gap-3 items-end flex-wrap">
|
||||
<div className="flex-1 min-w-[200px]">
|
||||
<IconSelect
|
||||
setSelectedIcon={setSelectedIcon}
|
||||
selectedIcon={selectedIcon}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Input
|
||||
id="amenity_name"
|
||||
placeholder={t("Qulaylik nomi (masalan: Wi-Fi)")}
|
||||
className="h-12 !text-md flex-1 min-w-[200px]"
|
||||
/>
|
||||
|
||||
<Input
|
||||
id="amenity_name_ru"
|
||||
placeholder={t("Qulaylik nomi (ru)")}
|
||||
className="h-12 !text-md flex-1 min-w-[200px]"
|
||||
/>
|
||||
|
||||
<Button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
const nameInput = document.getElementById(
|
||||
"amenity_name",
|
||||
) as HTMLInputElement;
|
||||
const nameRuInput = document.getElementById(
|
||||
"amenity_name_ru",
|
||||
) as HTMLInputElement;
|
||||
|
||||
if (selectedIcon && nameInput.value) {
|
||||
const current = form.getValues("amenities");
|
||||
form.setValue("amenities", [
|
||||
...current,
|
||||
{
|
||||
icon_name: selectedIcon,
|
||||
name: nameInput.value,
|
||||
name_ru: nameRuInput.value,
|
||||
},
|
||||
]);
|
||||
nameInput.value = "";
|
||||
nameRuInput.value = "";
|
||||
setSelectedIcon("");
|
||||
}
|
||||
}}
|
||||
className="h-12"
|
||||
>
|
||||
{t("Qo'shish")}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<FormMessage />
|
||||
</div>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="extra_service"
|
||||
|
||||
@@ -47,15 +47,9 @@ const formSchema = z.object({
|
||||
}),
|
||||
rating: z.string().min(1).max(5),
|
||||
mealPlan: z.string().min(1, { message: "Taom rejasi tanlanishi majburiy" }),
|
||||
hotelType: z
|
||||
.array(z.string())
|
||||
.min(1, { message: "Kamida 1 ta mehmonxona turi tanlang" }),
|
||||
hotelFeatures: z
|
||||
.array(z.string())
|
||||
.min(1, { message: "Kamida 1 ta xususiyat tanlang" }),
|
||||
hotelFeaturesType: z
|
||||
.array(z.string())
|
||||
.min(1, { message: "Kamida 1 ta tur tanlang" }),
|
||||
hotelType: z.array(z.string()).optional(),
|
||||
hotelFeatures: z.array(z.string()).optional(),
|
||||
hotelFeaturesType: z.array(z.string()).optional(),
|
||||
});
|
||||
|
||||
const StepTwo = ({
|
||||
@@ -179,18 +173,20 @@ const StepTwo = ({
|
||||
|
||||
// 🔹 Feature type'larni yuklash (tanlangan feature bo‘yicha)
|
||||
useEffect(() => {
|
||||
if (selectedHotelFeatures.length === 0) {
|
||||
if (selectedHotelFeatures && selectedHotelFeatures.length === 0) {
|
||||
setAllHotelFeatureType([]);
|
||||
setFeatureTypeMapping({});
|
||||
return;
|
||||
}
|
||||
|
||||
const loadFeatureTypes = async () => {
|
||||
const selectedIds = selectedHotelFeatures.map(Number).filter(Boolean);
|
||||
const selectedIds =
|
||||
selectedHotelFeatures &&
|
||||
selectedHotelFeatures.map(Number).filter(Boolean);
|
||||
let allResults: HotelFeaturesType[] = [];
|
||||
const mapping: Record<string, string[]> = {};
|
||||
|
||||
for (const id of selectedIds) {
|
||||
for (const id of selectedIds!) {
|
||||
let page = 1;
|
||||
let hasNext = true;
|
||||
const featureTypes: string[] = [];
|
||||
@@ -253,12 +249,12 @@ const StepTwo = ({
|
||||
const removeHotelType = (id: string) =>
|
||||
form.setValue(
|
||||
"hotelType",
|
||||
form.getValues("hotelType").filter((v) => v !== id),
|
||||
(form.getValues("hotelType") ?? []).filter((v) => v !== id),
|
||||
);
|
||||
|
||||
const removeHotelFeature = (id: string) => {
|
||||
const current = form.getValues("hotelFeatures");
|
||||
const types = form.getValues("hotelFeaturesType");
|
||||
const current = form.getValues("hotelFeatures") ?? [];
|
||||
const types = form.getValues("hotelFeaturesType") ?? [];
|
||||
const toRemove = featureTypeMapping[id] || [];
|
||||
|
||||
form.setValue(
|
||||
@@ -274,7 +270,7 @@ const StepTwo = ({
|
||||
const removeFeatureType = (id: string) =>
|
||||
form.setValue(
|
||||
"hotelFeaturesType",
|
||||
form.getValues("hotelFeaturesType").filter((v) => v !== id),
|
||||
(form.getValues("hotelFeaturesType") ?? []).filter((v) => v !== id),
|
||||
);
|
||||
|
||||
// 🧩 Submit
|
||||
@@ -284,6 +280,9 @@ const StepTwo = ({
|
||||
formData.append("ticket", ticketId ? String(ticketId) : "");
|
||||
formData.append("name", data.title);
|
||||
formData.append("rating", data.rating);
|
||||
amenities.forEach((e, i) => {
|
||||
formData.append(`hotel_amenities[${i}]`, String(e));
|
||||
});
|
||||
|
||||
const mealPlan =
|
||||
data.mealPlan === "Breakfast Only"
|
||||
@@ -295,15 +294,11 @@ const StepTwo = ({
|
||||
: "all_inclusive";
|
||||
formData.append("meal_plan", mealPlan);
|
||||
|
||||
data.hotelType &&
|
||||
data.hotelType.forEach((id) => formData.append("hotel_type", id));
|
||||
data.hotelFeatures &&
|
||||
data.hotelFeatures.forEach((id) => formData.append("hotel_features", id));
|
||||
|
||||
amenities.forEach((e, i) => {
|
||||
formData.append(`hotel_amenities[${i}]name`, e.name);
|
||||
formData.append(`hotel_amenities[${i}]name_ru`, e.name_ru);
|
||||
formData.append(`hotel_amenities[${i}]icon_name`, e.icon_name);
|
||||
});
|
||||
|
||||
if (isEditMode && hotelDetail) {
|
||||
edit({
|
||||
body: formData,
|
||||
@@ -416,7 +411,7 @@ const StepTwo = ({
|
||||
<Label>{t("Mehmonxona turlari")}</Label>
|
||||
<FormControl>
|
||||
<div className="space-y-2">
|
||||
{field.value.length > 0 && (
|
||||
{field.value && field.value.length > 0 && (
|
||||
<div className="flex flex-wrap gap-2 p-2 border rounded-md">
|
||||
{field.value.map((selectedValue) => {
|
||||
const selectedItem = allHotelTypes.find(
|
||||
@@ -444,7 +439,7 @@ const StepTwo = ({
|
||||
<Select
|
||||
value=""
|
||||
onValueChange={(value) => {
|
||||
if (!field.value.includes(value)) {
|
||||
if (field.value && !field.value.includes(value)) {
|
||||
field.onChange([...field.value, value]);
|
||||
}
|
||||
}}
|
||||
@@ -452,7 +447,7 @@ const StepTwo = ({
|
||||
<SelectTrigger className="!h-12 w-full">
|
||||
<SelectValue
|
||||
placeholder={
|
||||
field.value.length > 0
|
||||
field.value && field.value.length > 0
|
||||
? t("Yana tanlang...")
|
||||
: t("Tanlang")
|
||||
}
|
||||
@@ -461,7 +456,9 @@ const StepTwo = ({
|
||||
<SelectContent>
|
||||
{allHotelTypes
|
||||
.filter(
|
||||
(type) => !field.value.includes(String(type.id)),
|
||||
(type) =>
|
||||
field.value &&
|
||||
!field.value.includes(String(type.id)),
|
||||
)
|
||||
.map((type) => (
|
||||
<SelectItem key={type.id} value={String(type.id)}>
|
||||
@@ -486,7 +483,7 @@ const StepTwo = ({
|
||||
<Label>{t("Mehmonxona xususiyatlari")}</Label>
|
||||
<FormControl>
|
||||
<div className="space-y-2">
|
||||
{field.value.length > 0 && (
|
||||
{field.value && field.value.length > 0 && (
|
||||
<div className="flex flex-wrap gap-2 p-2 border rounded-md">
|
||||
{field.value.map((selectedValue) => {
|
||||
const selectedItem = allHotelFeature.find(
|
||||
@@ -520,7 +517,7 @@ const StepTwo = ({
|
||||
<Select
|
||||
value=""
|
||||
onValueChange={(value) => {
|
||||
if (!field.value.includes(value)) {
|
||||
if (field.value && !field.value.includes(value)) {
|
||||
field.onChange([...field.value, value]);
|
||||
}
|
||||
}}
|
||||
@@ -528,7 +525,7 @@ const StepTwo = ({
|
||||
<SelectTrigger className="!h-12 w-full">
|
||||
<SelectValue
|
||||
placeholder={
|
||||
field.value.length > 0
|
||||
field.value && field.value.length > 0
|
||||
? t("Yana tanlang...")
|
||||
: t("Tanlang")
|
||||
}
|
||||
@@ -537,7 +534,9 @@ const StepTwo = ({
|
||||
<SelectContent>
|
||||
{allHotelFeature
|
||||
.filter(
|
||||
(type) => !field.value.includes(String(type.id)),
|
||||
(type) =>
|
||||
field.value &&
|
||||
!field.value.includes(String(type.id)),
|
||||
)
|
||||
.map((type) => (
|
||||
<SelectItem key={type.id} value={String(type.id)}>
|
||||
@@ -562,7 +561,7 @@ const StepTwo = ({
|
||||
<Label>{t("Xususiyat turlari")}</Label>
|
||||
<FormControl>
|
||||
<div className="space-y-2">
|
||||
{field.value.length > 0 && (
|
||||
{field.value && field.value.length > 0 && (
|
||||
<div className="flex flex-wrap gap-2 p-2 border rounded-md">
|
||||
{field.value.map((selectedValue) => {
|
||||
const selectedItem = allHotelFeatureType.find(
|
||||
@@ -590,7 +589,7 @@ const StepTwo = ({
|
||||
<Select
|
||||
value=""
|
||||
onValueChange={(value) => {
|
||||
if (!field.value.includes(value)) {
|
||||
if (field.value && !field.value.includes(value)) {
|
||||
field.onChange([...field.value, value]);
|
||||
}
|
||||
}}
|
||||
@@ -598,9 +597,10 @@ const StepTwo = ({
|
||||
<SelectTrigger className="!h-12 w-full">
|
||||
<SelectValue
|
||||
placeholder={
|
||||
selectedHotelFeatures &&
|
||||
selectedHotelFeatures.length === 0
|
||||
? t("Avval xususiyat tanlang")
|
||||
: field.value.length > 0
|
||||
: field.value && field.value.length > 0
|
||||
? t("Yana tanlang...")
|
||||
: t("Tanlang")
|
||||
}
|
||||
@@ -614,7 +614,9 @@ const StepTwo = ({
|
||||
) : (
|
||||
allHotelFeatureType
|
||||
.filter(
|
||||
(type) => !field.value.includes(String(type.id)),
|
||||
(type) =>
|
||||
field.value &&
|
||||
!field.value.includes(String(type.id)),
|
||||
)
|
||||
.map((type) => (
|
||||
<SelectItem key={type.id} value={String(type.id)}>
|
||||
|
||||
@@ -26,6 +26,7 @@ import { useNavigate, useParams } from "react-router-dom";
|
||||
|
||||
export default function TourDetailPage() {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const params = useParams();
|
||||
const router = useNavigate();
|
||||
const {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import {
|
||||
getAllAmenities,
|
||||
hotelBadge,
|
||||
hotelFeature,
|
||||
hotelFeatureType,
|
||||
@@ -8,6 +9,7 @@ import {
|
||||
hotelTransport,
|
||||
hotelType,
|
||||
} from "@/pages/tours/lib/api";
|
||||
import Amenities from "@/pages/tours/ui/Amenities";
|
||||
import BadgeTable from "@/pages/tours/ui/BadgeTable";
|
||||
import FeaturesTable from "@/pages/tours/ui/FeaturesTable";
|
||||
import FeaturesTableType from "@/pages/tours/ui/FeaturesTableType";
|
||||
@@ -120,13 +122,31 @@ const ToursSetting: React.FC = () => {
|
||||
enabled: !!featureId,
|
||||
});
|
||||
|
||||
const pageAmenities = parseInt(searchParams.get("pageAmenities") || "1", 10);
|
||||
const pageSizeAmenities = parseInt(
|
||||
searchParams.get("pageSizeAmenities") || "10",
|
||||
10,
|
||||
);
|
||||
|
||||
const {
|
||||
data: amenitiesData,
|
||||
isLoading: amenitiesLoad,
|
||||
isError: amenitiesError,
|
||||
refetch: amenitiesRef,
|
||||
} = useQuery({
|
||||
queryKey: ["all_amenities", page, pageSize],
|
||||
queryFn: () => getAllAmenities({ page, page_size: pageSize }),
|
||||
select: (res) => res.data.data,
|
||||
});
|
||||
|
||||
if (
|
||||
isLoading ||
|
||||
tarifLoad ||
|
||||
transportLoad ||
|
||||
typeLoad ||
|
||||
featureLoad ||
|
||||
featureTypeLoad
|
||||
featureTypeLoad ||
|
||||
amenitiesLoad
|
||||
) {
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center min-h-screen bg-slate-900 text-white gap-4 w-full">
|
||||
@@ -142,7 +162,8 @@ const ToursSetting: React.FC = () => {
|
||||
transportError ||
|
||||
typeError ||
|
||||
featureError ||
|
||||
featureTypeError
|
||||
featureTypeError ||
|
||||
amenitiesError
|
||||
) {
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center min-h-screen bg-slate-900 w-full text-center text-white gap-4">
|
||||
@@ -158,6 +179,7 @@ const ToursSetting: React.FC = () => {
|
||||
typeRef();
|
||||
featureRef();
|
||||
featureTypeRef();
|
||||
amenitiesRef();
|
||||
}}
|
||||
className="bg-gradient-to-r from-blue-600 to-cyan-600 text-white rounded-lg px-5 py-2 hover:opacity-90"
|
||||
>
|
||||
@@ -187,7 +209,7 @@ const ToursSetting: React.FC = () => {
|
||||
<TabsTrigger value="badge">{t("Belgilar (Badge)")}</TabsTrigger>
|
||||
<TabsTrigger value="tarif">{t("Tariflar")}</TabsTrigger>
|
||||
<TabsTrigger value="transport">{t("Transport")}</TabsTrigger>
|
||||
{/* <TabsTrigger value="meal">{t("Ovqatlanish")}</TabsTrigger> */}
|
||||
<TabsTrigger value="meal">{t("Qulayliklar")}</TabsTrigger>
|
||||
<TabsTrigger value="hotel_type">{t("Otel turlari")}</TabsTrigger>
|
||||
<TabsTrigger value="hotel_features">
|
||||
{t("Otel sharoitlari")}
|
||||
@@ -211,6 +233,13 @@ const ToursSetting: React.FC = () => {
|
||||
pageSize={pageSizeTransport}
|
||||
/>
|
||||
</TabsContent>
|
||||
<TabsContent value="meal" className="space-y-4">
|
||||
<Amenities
|
||||
data={amenitiesData}
|
||||
page={pageAmenities}
|
||||
pageSize={pageSizeAmenities}
|
||||
/>
|
||||
</TabsContent>
|
||||
<TabsContent value="hotel_type" className="space-y-4">
|
||||
<MealTable
|
||||
data={typeData}
|
||||
|
||||
@@ -15,6 +15,7 @@ const HOTEL_FEATURES_TYPE = "dashboard/dashboard-ticket-hotel-feature/";
|
||||
const HOTEL_TARIF = "dashboard/dashboard-tickets-settings-tariff/";
|
||||
const TOUR_TRANSPORT = "dashboard/dashboard-tickets-settings-transport/";
|
||||
const HPTEL_TYPES = "dashboard/dashboard-tickets-settings-hotel-type/";
|
||||
const AMENITIES = "dashboard/dashboard-ticket-settings-amenities/";
|
||||
const NEWS = "dashboard/dashboard-post/";
|
||||
const NEWS_CATEGORY = "dashboard/dashboard-category/";
|
||||
const HOTEL = "dashboard/dashboard-hotel/";
|
||||
@@ -34,6 +35,7 @@ const TOUR_ADMIN = "dashboard/dashboard-tour-admin/";
|
||||
|
||||
export {
|
||||
AGENCY_ORDERS,
|
||||
AMENITIES,
|
||||
AUTH_LOGIN,
|
||||
BANNER,
|
||||
BASE_URL,
|
||||
|
||||
@@ -491,5 +491,6 @@
|
||||
"Booking Date": "Дата бронирования",
|
||||
"Yangi foydalanuvchi ma'lumotlari": "Данные нового пользователя",
|
||||
"Agentlik uchun tizimga kirish ma'lumotlari": "Входные данные для агентства",
|
||||
"Ikona tanlang": "Выберите иконку0",
|
||||
"Haqiqatan ham bu foydalanuvchini o'chirmoqchimisiz? Bu amalni qaytarib bo'lmaydi.": "Вы уверены, что хотите удалить этого пользователя? Это действие необратимо."
|
||||
}
|
||||
|
||||
@@ -491,6 +491,7 @@
|
||||
"Travel Date": "Sayohat sanasi",
|
||||
"Booking Date": "Bandlov sanasi",
|
||||
"Yangi foydalanuvchi ma'lumotlari": "Yangi foydalanuvchi ma'lumotlari",
|
||||
"Ikona tanlang": "Ikona tanlang",
|
||||
"Agentlik uchun tizimga kirish ma'lumotlari": "Agentlik uchun tizimga kirish ma'lumotlari",
|
||||
"Haqiqatan ham bu foydalanuvchini o'chirmoqchimisiz? Bu amalni qaytarib bo'lmaydi.": "Haqiqatan ham bu foydalanuvchini o'chirmoqchimisiz? Bu amalni qaytarib bo'lmaydi."
|
||||
}
|
||||
|
||||
70
src/shared/lib/iconTranslations.ts
Normal file
70
src/shared/lib/iconTranslations.ts
Normal file
@@ -0,0 +1,70 @@
|
||||
export const hotelIcons = [
|
||||
{ name: "Wifi", uz: "Wi-Fi", ru: "Wi-Fi" },
|
||||
{ name: "Bus", uz: "Avtobus", ru: "Автобус" },
|
||||
{ name: "CarFront", uz: "Auto", ru: "Авто" },
|
||||
{ name: "PlaneTakeoff", uz: "Avia", ru: "Авиа" },
|
||||
{ name: "WifiOff", uz: "Wi-Fi mavjud emas", ru: "Нет Wi-Fi" },
|
||||
{ name: "Tv", uz: "Televizor", ru: "Телевизор" },
|
||||
{ name: "TvMinimalPlay", uz: "Smart TV", ru: "Смарт ТВ" },
|
||||
{ name: "AirVent", uz: "Ventilyatsiya", ru: "Вентиляция" },
|
||||
{ name: "Wind", uz: "Konditsioner", ru: "Кондиционер" },
|
||||
{ name: "Thermometer", uz: "Isitish tizimi", ru: "Отопление" },
|
||||
{ name: "Snowflake", uz: "Sovutgich", ru: "Холодильник" },
|
||||
{ name: "Fan", uz: "Ventilyator", ru: "Вентилятор" },
|
||||
{ name: "Lightbulb", uz: "Yoritish", ru: "Освещение" },
|
||||
{ name: "Plug", uz: "Rozetka", ru: "Розетка" },
|
||||
{ name: "BatteryFull", uz: "Zaryadlash", ru: "Зарядка" },
|
||||
{ name: "Bed", uz: "Yotoq", ru: "Кровать" },
|
||||
{ name: "BedSingle", uz: "Yagona yotoq", ru: "Односпальная кровать" },
|
||||
{ name: "BedDouble", uz: "Ikki kishilik yotoq", ru: "Двуспальная кровать" },
|
||||
{ name: "Bath", uz: "Vanna", ru: "Ванна" },
|
||||
{ name: "ShowerHead", uz: "Dush", ru: "Душ" },
|
||||
{ name: "Toilet", uz: "Hojatxona", ru: "Туалет" },
|
||||
{ name: "Towel", uz: "Sochiqlar", ru: "Полотенца" },
|
||||
{ name: "Soap", uz: "Gigiyena vositalari", ru: "Средства гигиены" },
|
||||
{ name: "CupSoda", uz: "Ichimliklar", ru: "Напитки" },
|
||||
{ name: "Coffee", uz: "Kofe / nonushta", ru: "Кофе / завтрак" },
|
||||
{ name: "Utensils", uz: "Restoran", ru: "Ресторан" },
|
||||
{ name: "UtensilsCrossed", uz: "Oshxona", ru: "Кухня" },
|
||||
{ name: "Wine", uz: "Ichimliklar", ru: "Вино" },
|
||||
{ name: "Beer", uz: "Pivo", ru: "Пиво" },
|
||||
{ name: "Salad", uz: "Salatlar", ru: "Салаты" },
|
||||
{ name: "Cake", uz: "Shirinliklar", ru: "Десерты" },
|
||||
{ name: "Gift", uz: "Sovg‘a do‘koni", ru: "Сувенирный магазин" },
|
||||
{ name: "Dumbbell", uz: "Sport zali", ru: "Тренажёрный зал" },
|
||||
{ name: "Swim", uz: "Basseyn", ru: "Бассейн" },
|
||||
{ name: "Spa", uz: "Spa markazi", ru: "Спа центр" },
|
||||
{ name: "Heart", uz: "Masaj xizmati", ru: "Массаж" },
|
||||
{ name: "ShieldCheck", uz: "Xavfsizlik", ru: "Безопасность" },
|
||||
{ name: "Camera", uz: "Kuzatuv kamerasi", ru: "Видеонаблюдение" },
|
||||
{ name: "Lock", uz: "Saqlash joyi", ru: "Сейф" },
|
||||
{ name: "ConciergeBell", uz: "Resepshn", ru: "Ресепшн" },
|
||||
{ name: "BaggageClaim", uz: "Bagaj saqlash", ru: "Хранение багажа" },
|
||||
{ name: "Elevator", uz: "Lift", ru: "Лифт" },
|
||||
{ name: "CarFront", uz: "Avtoturargoh", ru: "Парковка" },
|
||||
{ name: "ParkingSquare", uz: "Parkovka", ru: "Стоянка" },
|
||||
{ name: "Taxi", uz: "Taksi xizmati", ru: "Такси" },
|
||||
{ name: "MapPin", uz: "Joylashuv", ru: "Расположение" },
|
||||
{ name: "Mountain", uz: "Tog‘ manzarasi", ru: "Горный вид" },
|
||||
{ name: "TreePalm", uz: "Bog‘ / palma", ru: "Пальмы" },
|
||||
{ name: "Sun", uz: "Quyoshli joy", ru: "Солнечная сторона" },
|
||||
{ name: "Moon", uz: "Tun xizmati", ru: "Ночная смена" },
|
||||
{ name: "Users", uz: "Ko‘p o‘rinli xona", ru: "Многоместный номер" },
|
||||
{ name: "Baby", uz: "Bolalar uchun", ru: "Для детей" },
|
||||
{
|
||||
name: "Wheelchair",
|
||||
uz: "Nogironlar uchun qulay",
|
||||
ru: "Доступ для инвалидов",
|
||||
},
|
||||
{ name: "Dog", uz: "Uy hayvoni mumkin", ru: "Можно с животными" },
|
||||
{ name: "CigaretteOff", uz: "Chekish taqiqlangan", ru: "Курение запрещено" },
|
||||
{ name: "Cigarette", uz: "Chekish joyi", ru: "Место для курения" },
|
||||
{ name: "Shirt", uz: "Tozalash xizmati", ru: "Химчистка" },
|
||||
{ name: "WashingMachine", uz: "Kir yuvish mashinasi", ru: "Прачечная" },
|
||||
{ name: "Iron", uz: "Dazmol", ru: "Утюг" },
|
||||
{ name: "Phone", uz: "Telefon", ru: "Телефон" },
|
||||
{ name: "CreditCard", uz: "Kartali to‘lov", ru: "Оплата картой" },
|
||||
{ name: "Wallet", uz: "Naqd to‘lov", ru: "Наличные" },
|
||||
{ name: "AlarmClock", uz: "Budilnik", ru: "Будильник" },
|
||||
{ name: "Clock", uz: "Soat", ru: "Часы" },
|
||||
];
|
||||
@@ -1,7 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import Icon from "@/shared/ui/icon";
|
||||
import { Input } from "@/shared/ui/input";
|
||||
import { hotelIcons } from "@/shared/lib/iconTranslations";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
@@ -9,164 +8,54 @@ import {
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/shared/ui/select";
|
||||
import { HelpCircle, Search } from "lucide-react";
|
||||
import React, {
|
||||
Suspense,
|
||||
useDeferredValue,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
type ComponentType,
|
||||
type LazyExoticComponent,
|
||||
} from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import * as Icons from "lucide-react";
|
||||
import { useState } from "react";
|
||||
|
||||
// 🔹 Lazy icon faqat tanlangan icon uchun
|
||||
const LazyIcon: React.FC<{ name: string }> = ({ name }) => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const IconComp: LazyExoticComponent<ComponentType<any>> = React.lazy(
|
||||
async () => {
|
||||
const icons = await import("lucide-react");
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
return { default: (icons as any)[name] || HelpCircle };
|
||||
},
|
||||
);
|
||||
|
||||
return (
|
||||
<Suspense fallback={<div className="w-4 h-4" />}>
|
||||
<IconComp className="w-4 h-4" />
|
||||
</Suspense>
|
||||
);
|
||||
type IconSelectProps = {
|
||||
selectedIcon?: string;
|
||||
setSelectedIcon: (value: string) => void;
|
||||
};
|
||||
|
||||
interface IconSelectProps {
|
||||
selectedIcon?: string;
|
||||
defaultIcon?: string;
|
||||
setSelectedIcon: (value: string) => void;
|
||||
}
|
||||
|
||||
const IconSelect: React.FC<IconSelectProps> = ({
|
||||
export default function IconSelect({
|
||||
selectedIcon,
|
||||
defaultIcon = "HelpCircle",
|
||||
setSelectedIcon,
|
||||
}) => {
|
||||
const [icons, setIcons] = useState<string[]>([]);
|
||||
const { t } = useTranslation();
|
||||
const [visibleIcons, setVisibleIcons] = useState<string[]>([]);
|
||||
const [chunkSize] = useState(100);
|
||||
const [index, setIndex] = useState(1);
|
||||
const [containerEl, setContainerEl] = useState<HTMLDivElement | null>(null);
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [searchTerm, setSearchTerm] = useState("");
|
||||
}: IconSelectProps) {
|
||||
const [search, setSearch] = useState("");
|
||||
|
||||
const loaderRef = useRef<HTMLDivElement | null>(null);
|
||||
const deferredSearch = useDeferredValue(searchTerm);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isOpen) return;
|
||||
const loadIcons = async () => {
|
||||
const mod = await import("lucide-react");
|
||||
const allIcons = Object.keys(mod).filter((k) => /^[A-Z]/.test(k));
|
||||
setIcons(allIcons);
|
||||
setVisibleIcons(allIcons.slice(0, chunkSize));
|
||||
setIndex(1);
|
||||
};
|
||||
loadIcons();
|
||||
}, [isOpen, chunkSize]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!containerEl || !loaderRef.current || !isOpen) return;
|
||||
|
||||
const observer = new IntersectionObserver(
|
||||
(entries) => {
|
||||
if (entries[0].isIntersecting) {
|
||||
const start = index * chunkSize;
|
||||
const end = start + chunkSize;
|
||||
const next = icons.slice(start, end);
|
||||
if (next.length > 0) {
|
||||
setVisibleIcons((p) => [...p, ...next]);
|
||||
setIndex((p) => p + 1);
|
||||
}
|
||||
}
|
||||
},
|
||||
{ root: containerEl, threshold: 1.0 },
|
||||
const filteredIcons = hotelIcons.filter(
|
||||
(icon) =>
|
||||
icon.uz.toLowerCase().includes(search.toLowerCase()) ||
|
||||
icon.ru.toLowerCase().includes(search.toLowerCase()) ||
|
||||
icon.name.toLowerCase().includes(search.toLowerCase()),
|
||||
);
|
||||
|
||||
observer.observe(loaderRef.current);
|
||||
return () => observer.disconnect();
|
||||
}, [containerEl, icons, index, chunkSize, isOpen]);
|
||||
|
||||
const filteredIcons = useMemo(() => {
|
||||
const term = deferredSearch.trim().toLowerCase();
|
||||
if (!term) return visibleIcons;
|
||||
return icons.filter((n) => n.toLowerCase().includes(term));
|
||||
}, [icons, visibleIcons, deferredSearch]);
|
||||
|
||||
const handleOpenChange = (open: boolean) => {
|
||||
setIsOpen(open);
|
||||
if (!open) {
|
||||
setVisibleIcons([]);
|
||||
setIcons([]);
|
||||
setIndex(1);
|
||||
setSearchTerm("");
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Select
|
||||
value={selectedIcon}
|
||||
onValueChange={setSelectedIcon}
|
||||
onOpenChange={handleOpenChange}
|
||||
>
|
||||
<SelectTrigger className="!h-12 w-[220px] text-md">
|
||||
<SelectValue placeholder={t("Ikonka tanlang")}>
|
||||
{selectedIcon ? (
|
||||
<div className="flex items-center gap-2">
|
||||
<LazyIcon name={selectedIcon} />
|
||||
{selectedIcon}
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex items-center gap-2 text-gray-500">
|
||||
<LazyIcon name={defaultIcon} />
|
||||
{defaultIcon}
|
||||
</div>
|
||||
)}
|
||||
</SelectValue>
|
||||
<div className="flex flex-col gap-2">
|
||||
<Select value={selectedIcon} onValueChange={setSelectedIcon}>
|
||||
<SelectTrigger className="w-full">
|
||||
<SelectValue placeholder="Ikona tanlang" />
|
||||
</SelectTrigger>
|
||||
|
||||
<SelectContent ref={setContainerEl} className="max-h-80 overflow-y-auto">
|
||||
<div className="sticky top-0 bg-white dark:bg-neutral-900 z-10 p-2 border-b flex items-center gap-2">
|
||||
<Search className="w-4 h-4 text-gray-500" />
|
||||
<Input
|
||||
placeholder="Qidiruv..."
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
className="h-8 text-sm"
|
||||
<SelectContent className="max-h-64 overflow-y-auto p-2">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Qidirish..."
|
||||
className="w-full border px-2 py-1 text-sm mb-2 rounded-md outline-none"
|
||||
onChange={(e) => setSearch(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{filteredIcons.map((iconName) => (
|
||||
<SelectItem key={iconName} value={iconName}>
|
||||
<div className="flex items-center gap-2 text-sm">
|
||||
<Icon name={iconName} />
|
||||
{iconName}
|
||||
{filteredIcons.map(({ name, uz, ru }) => {
|
||||
const LucideIcon = (Icons as any)[name];
|
||||
if (!LucideIcon) return null;
|
||||
return (
|
||||
<SelectItem key={name} value={name}>
|
||||
<div className="flex items-center gap-2">
|
||||
<LucideIcon className="w-4 h-4" />
|
||||
{uz} <span className="text-gray-400">({ru})</span>
|
||||
</div>
|
||||
</SelectItem>
|
||||
))}
|
||||
|
||||
{!searchTerm && isOpen && (
|
||||
<div ref={loaderRef} className="h-6 flex justify-center items-center">
|
||||
{visibleIcons.length < icons.length && (
|
||||
<span className="text-xs text-gray-400">
|
||||
{t("Yuklanmoqda...")}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
);
|
||||
})}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default IconSelect;
|
||||
}
|
||||
|
||||
@@ -268,7 +268,7 @@ export function Sidebar({ role }: SidebarProps) {
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="lg:border">
|
||||
<div className="lg:border fixed">
|
||||
{/* Mobil versiya */}
|
||||
<div className="lg:hidden flex items-center justify-end bg-gray-900 p-4 sticky top-0 z-50">
|
||||
<Sheet open={isSheetOpen} onOpenChange={setIsSheetOpen}>
|
||||
|
||||
Reference in New Issue
Block a user