bug fix
This commit is contained in:
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user