Files
simple-admin/src/shared/ui/iocnSelect.tsx
Samandar Turgunboyev 05b752daf2 api ulandi
2025-10-25 18:42:01 +05:00

168 lines
4.8 KiB
TypeScript

"use client";
import Icon from "@/shared/ui/icon";
import { Input } from "@/shared/ui/input";
import {
Select,
SelectContent,
SelectItem,
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";
// 🔹 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 {
selectedIcon?: string;
setSelectedIcon: (value: string) => void;
}
const IconSelect: React.FC<IconSelectProps> = ({
selectedIcon,
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>
) : (
t("Ikonka tanlang")
)}
</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;