bug fix
This commit is contained in:
90
src/App.tsx
90
src/App.tsx
@@ -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,47 +78,54 @@ const App = () => {
|
||||
<div className="flex max-lg:flex-col bg-gray-900">
|
||||
{shouldShowSidebar && <Sidebar role={user ? user.role : "admin"} />}
|
||||
|
||||
<Routes>
|
||||
<Route path="/" element={<Navigate to={"/user"} />} />
|
||||
<Route path="/user" element={<UserList />} />
|
||||
<Route path="/login" element={<Login />} />
|
||||
<Route path="/users/create" element={<CreateUser />} />
|
||||
<Route path="/users/:id/edit" element={<EditUser />} />
|
||||
<Route path="/users/:id/" element={<UserDetail />} />
|
||||
<Route path="/agencies" element={<Agencies />} />
|
||||
<Route path="/agencies/:id" element={<AgencyDetail />} />
|
||||
<Route path="/agency/:id/edit" element={<EditAgecy />} />
|
||||
<Route path="/tours/:id" element={<TourDetail />} />
|
||||
<Route path="/employees" element={<Employees />} />
|
||||
<Route
|
||||
path="/finance"
|
||||
element={<FinancePage user={user ? user.role : "moderator"} />}
|
||||
/>
|
||||
<Route path="/purchases/:id/" element={<PurchaseDetailPage />} />
|
||||
<Route path="/travel/booking/:id/" element={<FinanceDetailTour />} />
|
||||
<Route path="/bookings/:id/" element={<FinanceDetailUsers />} />
|
||||
<Route
|
||||
path="/tours"
|
||||
element={<Tours user={user ? user.role : "moderator"} />}
|
||||
/>
|
||||
<Route path="/tours/setting" element={<ToursSetting />} />
|
||||
<Route path="/tours/:id/edit" element={<CreateEditTour />} />
|
||||
<Route path="/tours/create" element={<CreateEditTour />} />
|
||||
<Route path="/bookings" element={<Bookings />} />
|
||||
<Route path="/news" element={<News />} />
|
||||
<Route path="/news/add" element={<AddNews />} />
|
||||
<Route path="/news/edit/:id" element={<AddNews />} />
|
||||
<Route path="/news/categories" element={<NewsCategory />} />
|
||||
<Route path="/faq" element={<Faq />} />
|
||||
<Route path="/faq/categories" element={<FaqCategory />} />
|
||||
<Route path="/support/tours" element={<SupportAgency />} />
|
||||
<Route path="/support/user" element={<SupportTours />} />
|
||||
<Route path="/site-seo" element={<Seo />} />
|
||||
<Route path="/site-pages/" element={<SitePage />} />
|
||||
<Route path="/site-help/" element={<PolicyCrud />} />
|
||||
<Route path="/site-settings/" element={<TourSettings />} />
|
||||
<Route path="/site-banner/" element={<SiteBannerAdmin />} />
|
||||
</Routes>
|
||||
<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 />} />
|
||||
<Route path="/login" element={<Login />} />
|
||||
<Route path="/users/create" element={<CreateUser />} />
|
||||
<Route path="/users/:id/edit" element={<EditUser />} />
|
||||
<Route path="/users/:id/" element={<UserDetail />} />
|
||||
<Route path="/agencies" element={<Agencies />} />
|
||||
<Route path="/agencies/:id" element={<AgencyDetail />} />
|
||||
<Route path="/agency/:id/edit" element={<EditAgecy />} />
|
||||
<Route path="/tours/:id" element={<TourDetail />} />
|
||||
<Route path="/employees" element={<Employees />} />
|
||||
<Route
|
||||
path="/finance"
|
||||
element={<FinancePage user={user ? user.role : "moderator"} />}
|
||||
/>
|
||||
<Route path="/purchases/:id/" element={<PurchaseDetailPage />} />
|
||||
<Route path="/travel/booking/:id/" element={<FinanceDetailTour />} />
|
||||
<Route path="/bookings/:id/" element={<FinanceDetailUsers />} />
|
||||
<Route
|
||||
path="/tours"
|
||||
element={<Tours user={user ? user.role : "moderator"} />}
|
||||
/>
|
||||
<Route path="/tours/setting" element={<ToursSetting />} />
|
||||
<Route path="/tours/:id/edit" element={<CreateEditTour />} />
|
||||
<Route path="/tours/create" element={<CreateEditTour />} />
|
||||
<Route path="/bookings" element={<Bookings />} />
|
||||
<Route path="/news" element={<News />} />
|
||||
<Route path="/news/add" element={<AddNews />} />
|
||||
<Route path="/news/edit/:id" element={<AddNews />} />
|
||||
<Route path="/news/categories" element={<NewsCategory />} />
|
||||
<Route path="/faq" element={<Faq />} />
|
||||
<Route path="/faq/categories" element={<FaqCategory />} />
|
||||
<Route path="/support/tours" element={<SupportAgency />} />
|
||||
<Route path="/support/user" element={<SupportTours />} />
|
||||
<Route path="/site-seo" element={<Seo />} />
|
||||
<Route path="/site-pages/" element={<SitePage />} />
|
||||
<Route path="/site-help/" element={<PolicyCrud />} />
|
||||
<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}]duration`,
|
||||
String(itinerary.duration),
|
||||
);
|
||||
|
||||
// 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_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_image[${f}]image`,
|
||||
l.image,
|
||||
`ticket_itinerary[${i}]ticket_itinerary_destinations[${k}]name`,
|
||||
dest.name,
|
||||
);
|
||||
e.ticket_itinerary_destinations.forEach((e, f) => {
|
||||
formData.append(
|
||||
`ticket_itinerary[${i}]ticket_itinerary_destinations[${f}]name`,
|
||||
String(e.name),
|
||||
);
|
||||
formData.append(
|
||||
`ticket_itinerary[${i}]ticket_itinerary_destinations[${f}]name_ru`,
|
||||
String(e.name_ru),
|
||||
);
|
||||
});
|
||||
}
|
||||
});
|
||||
formData.append(
|
||||
`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,9 +1115,11 @@ 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) =>
|
||||
i === idx ? { ...t, price: num } : t,
|
||||
const currentTarifs =
|
||||
form.getValues("tarif") || [];
|
||||
const updatedTransport = currentTarifs.map(
|
||||
(t, i) =>
|
||||
i === idx ? { ...t, price: num } : t,
|
||||
);
|
||||
|
||||
form.setValue("tarif", updatedTransport);
|
||||
@@ -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,9 +1251,11 @@ 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) =>
|
||||
i === idx ? { ...t, price: num } : t,
|
||||
const currentTransports =
|
||||
form.getValues("transport") || [];
|
||||
const updatedTransport = currentTransports.map(
|
||||
(t, i) =>
|
||||
i === idx ? { ...t, price: num } : t,
|
||||
);
|
||||
|
||||
form.setValue("transport", updatedTransport);
|
||||
@@ -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,14 +294,10 @@ const StepTwo = ({
|
||||
: "all_inclusive";
|
||||
formData.append("meal_plan", mealPlan);
|
||||
|
||||
data.hotelType.forEach((id) => formData.append("hotel_type", id));
|
||||
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);
|
||||
});
|
||||
data.hotelType &&
|
||||
data.hotelType.forEach((id) => formData.append("hotel_type", id));
|
||||
data.hotelFeatures &&
|
||||
data.hotelFeatures.forEach((id) => formData.append("hotel_features", id));
|
||||
|
||||
if (isEditMode && hotelDetail) {
|
||||
edit({
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
interface IconSelectProps {
|
||||
type IconSelectProps = {
|
||||
selectedIcon?: string;
|
||||
defaultIcon?: string;
|
||||
setSelectedIcon: (value: string) => void;
|
||||
}
|
||||
|
||||
const IconSelect: React.FC<IconSelectProps> = ({
|
||||
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("");
|
||||
|
||||
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 },
|
||||
);
|
||||
|
||||
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>
|
||||
</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"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{filteredIcons.map((iconName) => (
|
||||
<SelectItem key={iconName} value={iconName}>
|
||||
<div className="flex items-center gap-2 text-sm">
|
||||
<Icon name={iconName} />
|
||||
{iconName}
|
||||
</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>
|
||||
);
|
||||
};
|
||||
|
||||
export default IconSelect;
|
||||
export default function IconSelect({
|
||||
selectedIcon,
|
||||
setSelectedIcon,
|
||||
}: IconSelectProps) {
|
||||
const [search, setSearch] = useState("");
|
||||
|
||||
const filteredIcons = hotelIcons.filter(
|
||||
(icon) =>
|
||||
icon.uz.toLowerCase().includes(search.toLowerCase()) ||
|
||||
icon.ru.toLowerCase().includes(search.toLowerCase()) ||
|
||||
icon.name.toLowerCase().includes(search.toLowerCase()),
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-2">
|
||||
<Select value={selectedIcon} onValueChange={setSelectedIcon}>
|
||||
<SelectTrigger className="w-full">
|
||||
<SelectValue placeholder="Ikona tanlang" />
|
||||
</SelectTrigger>
|
||||
<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)}
|
||||
/>
|
||||
{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>
|
||||
);
|
||||
})}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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