first commit
This commit is contained in:
163
src/shared/ui/iocnSelect.tsx
Normal file
163
src/shared/ui/iocnSelect.tsx
Normal file
@@ -0,0 +1,163 @@
|
||||
"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";
|
||||
|
||||
// 🔹 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 [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="Ikonka tanlang">
|
||||
{selectedIcon ? (
|
||||
<div className="flex items-center gap-2">
|
||||
<LazyIcon name={selectedIcon} />
|
||||
{selectedIcon}
|
||||
</div>
|
||||
) : (
|
||||
"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">Yuklanmoqda...</span>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
);
|
||||
};
|
||||
|
||||
export default IconSelect;
|
||||
Reference in New Issue
Block a user