168 lines
4.8 KiB
TypeScript
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;
|