240 lines
7.5 KiB
TypeScript
240 lines
7.5 KiB
TypeScript
"use client";
|
|
|
|
import { Loader2, Search, X } from "lucide-react";
|
|
import Image from "next/image";
|
|
import { useEffect, useRef, useState } from "react";
|
|
import { Badge } from "@/components/ui/badge";
|
|
import { Button } from "@/components/ui/button";
|
|
import { Card, CardContent } from "@/components/ui/card";
|
|
import { Input } from "@/components/ui/input";
|
|
import { cn } from "@/lib/utils";
|
|
|
|
interface Item {
|
|
id: number;
|
|
name: string;
|
|
category?: string;
|
|
image?: string;
|
|
rarity?: string;
|
|
level?: number;
|
|
weight?: number;
|
|
npc_price?: number;
|
|
[key: string]: any;
|
|
}
|
|
|
|
interface ItemSearchProps {
|
|
onItemSelect: (item: Item) => void;
|
|
selectedItem?: Item | null;
|
|
placeholder?: string;
|
|
className?: string;
|
|
}
|
|
|
|
export function ItemSearch({
|
|
onItemSelect,
|
|
selectedItem,
|
|
placeholder = "Pesquisar item...",
|
|
className,
|
|
}: ItemSearchProps) {
|
|
const [searchQuery, setSearchQuery] = useState("");
|
|
const [items, setItems] = useState<Item[]>([]);
|
|
const [loading, setLoading] = useState(false);
|
|
const [showDropdown, setShowDropdown] = useState(false);
|
|
const [previewItem, setPreviewItem] = useState<Item | null>(
|
|
selectedItem || null,
|
|
);
|
|
const searchRef = useRef<HTMLDivElement>(null);
|
|
const debounceRef = useRef<NodeJS.Timeout | undefined>(undefined);
|
|
|
|
// Buscar itens ao digitar
|
|
useEffect(() => {
|
|
if (debounceRef.current) {
|
|
clearTimeout(debounceRef.current);
|
|
}
|
|
|
|
if (searchQuery.trim().length < 2) {
|
|
setItems([]);
|
|
setShowDropdown(false);
|
|
return;
|
|
}
|
|
|
|
setLoading(true);
|
|
debounceRef.current = setTimeout(async () => {
|
|
try {
|
|
const res = await fetch(
|
|
`/api/items?q=${encodeURIComponent(searchQuery)}&pageSize=10`,
|
|
);
|
|
if (res.ok) {
|
|
const data = await res.json();
|
|
setItems(data.items || []);
|
|
setShowDropdown(true);
|
|
}
|
|
} catch (error) {
|
|
console.error("Erro ao buscar itens:", error);
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
}, 300);
|
|
}, [searchQuery]);
|
|
|
|
// Fechar dropdown ao clicar fora
|
|
useEffect(() => {
|
|
const handleClickOutside = (event: MouseEvent) => {
|
|
if (
|
|
searchRef.current &&
|
|
!searchRef.current.contains(event.target as Node)
|
|
) {
|
|
setShowDropdown(false);
|
|
}
|
|
};
|
|
|
|
document.addEventListener("mousedown", handleClickOutside);
|
|
return () => document.removeEventListener("mousedown", handleClickOutside);
|
|
}, []);
|
|
|
|
// Atualizar preview quando item selecionado mudar externamente
|
|
useEffect(() => {
|
|
if (selectedItem) {
|
|
setPreviewItem(selectedItem);
|
|
setSearchQuery(selectedItem.name);
|
|
}
|
|
}, [selectedItem]);
|
|
|
|
const handleItemClick = (item: Item) => {
|
|
setPreviewItem(item);
|
|
setSearchQuery(item.name);
|
|
setShowDropdown(false);
|
|
onItemSelect(item);
|
|
};
|
|
|
|
const handleClear = () => {
|
|
setSearchQuery("");
|
|
setPreviewItem(null);
|
|
setItems([]);
|
|
setShowDropdown(false);
|
|
onItemSelect(null as any);
|
|
};
|
|
|
|
return (
|
|
<div ref={searchRef} className={cn("relative", className)}>
|
|
<div className="relative">
|
|
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
|
|
<Input
|
|
type="text"
|
|
value={searchQuery}
|
|
onChange={(e) => setSearchQuery(e.target.value)}
|
|
onFocus={() => searchQuery.length >= 2 && setShowDropdown(true)}
|
|
placeholder={placeholder}
|
|
className="pl-9 pr-9"
|
|
/>
|
|
{searchQuery && (
|
|
<Button
|
|
type="button"
|
|
variant="ghost"
|
|
size="icon"
|
|
className="absolute right-1 top-1/2 -translate-y-1/2 h-6 w-6"
|
|
onClick={handleClear}
|
|
>
|
|
<X className="h-4 w-4" />
|
|
</Button>
|
|
)}
|
|
{loading && (
|
|
<Loader2 className="absolute right-3 top-1/2 -translate-y-1/2 h-4 w-4 animate-spin text-muted-foreground" />
|
|
)}
|
|
</div>
|
|
|
|
{/* Dropdown de resultados */}
|
|
{showDropdown && items.length > 0 && (
|
|
<div className="absolute z-50 w-full mt-1 bg-background border rounded-md shadow-lg max-h-60 overflow-y-auto">
|
|
{items.map((item) => (
|
|
<div
|
|
key={item.id}
|
|
className="p-2 hover:bg-accent cursor-pointer flex items-center gap-3"
|
|
onClick={() => handleItemClick(item)}
|
|
>
|
|
{item.image && (
|
|
<div className="relative w-10 h-10 flex-shrink-0 rounded bg-muted">
|
|
<Image
|
|
src={item.image}
|
|
alt={item.name}
|
|
fill
|
|
style={{ objectFit: "contain" }}
|
|
sizes="40px"
|
|
/>
|
|
</div>
|
|
)}
|
|
<div className="flex-1 min-w-0">
|
|
<p className="text-sm font-medium truncate">{item.name}</p>
|
|
{item.category && (
|
|
<p className="text-xs text-muted-foreground">
|
|
{item.category}
|
|
</p>
|
|
)}
|
|
</div>
|
|
{item.rarity && (
|
|
<Badge variant="secondary" className="text-xs">
|
|
{item.rarity}
|
|
</Badge>
|
|
)}
|
|
</div>
|
|
))}
|
|
</div>
|
|
)}
|
|
|
|
{/* Preview do item selecionado */}
|
|
{previewItem && (
|
|
<Card className="mt-3">
|
|
<CardContent className="p-4">
|
|
<div className="flex gap-4">
|
|
{previewItem.image && (
|
|
<div className="relative w-20 h-20 flex-shrink-0 rounded-lg bg-muted overflow-hidden">
|
|
<Image
|
|
src={previewItem.image}
|
|
alt={previewItem.name}
|
|
fill
|
|
style={{ objectFit: "contain" }}
|
|
sizes="80px"
|
|
/>
|
|
</div>
|
|
)}
|
|
<div className="flex-1">
|
|
<h4 className="font-semibold text-lg">{previewItem.name}</h4>
|
|
<div className="flex flex-wrap gap-2 mt-2 text-sm text-muted-foreground">
|
|
{previewItem.category && (
|
|
<span className="flex items-center gap-1">
|
|
<span className="font-medium">Categoria:</span>
|
|
{previewItem.category}
|
|
</span>
|
|
)}
|
|
{previewItem.rarity && (
|
|
<Badge variant="secondary">{previewItem.rarity}</Badge>
|
|
)}
|
|
{previewItem.level !== undefined &&
|
|
previewItem.level !== null && (
|
|
<span className="flex items-center gap-1">
|
|
<span className="font-medium">Level:</span>
|
|
{previewItem.level}
|
|
</span>
|
|
)}
|
|
{previewItem.weight !== undefined &&
|
|
previewItem.weight !== null && (
|
|
<span className="flex items-center gap-1">
|
|
<span className="font-medium">Peso:</span>
|
|
{previewItem.weight} oz
|
|
</span>
|
|
)}
|
|
{previewItem.npc_price !== undefined &&
|
|
previewItem.npc_price !== null && (
|
|
<span className="flex items-center gap-1">
|
|
<span className="font-medium">Preço NPC:</span>
|
|
{previewItem.npc_price.toLocaleString()} gp
|
|
</span>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|