This commit is contained in:
Samandar Turgunboyev
2025-11-01 16:18:36 +05:00
parent 0a61399e3d
commit 4e9b2f3bd8
19 changed files with 959 additions and 424 deletions

View File

@@ -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,

View File

@@ -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.": "Вы уверены, что хотите удалить этого пользователя? Это действие необратимо."
}

View File

@@ -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."
}

View 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: "Sovga dokoni", 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: "Kop orinli 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 tolov", ru: "Оплата картой" },
{ name: "Wallet", uz: "Naqd tolov", ru: "Наличные" },
{ name: "AlarmClock", uz: "Budilnik", ru: "Будильник" },
{ name: "Clock", uz: "Soat", ru: "Часы" },
];

View File

@@ -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>
);
}