"use client"; import { useVirtualizer } from "@tanstack/react-virtual"; import { GripVertical, LayoutGrid } from "lucide-react"; import { useTranslations } from "next-intl"; import { useRef } from "react"; import type { CatalogLayout } from "@/lib/catalog-layouts"; import { getLocalIconUrl } from "@/lib/furni/classname"; import { cn } from "@/lib/utils"; export interface OrganizeImportItem { itemId: number; catalogItemId: number | null; alreadyPlaced: boolean; itemName: string; publicName: string; spriteId: number; type: string; interactionType: string; sourcePageCaption: string | null; } export interface OrganizeImportGroup { name: string; icon: number; layout: CatalogLayout | null; total: number; moved: number; added: number; items: OrganizeImportItem[]; } export const LAYOUT_GRIDS: Record = { default_3x3: { cols: 3, rows: 3 }, sold_ltd_items: { cols: 3, rows: 3 }, room_bundle: { cols: 3, rows: 2 }, badge_display: { cols: 3, rows: 3 }, spaces_new: { cols: 4, rows: 2 }, single_bundle: { cols: 1, rows: 1 }, }; export function FurniThumb({ item, size = 24, }: { item: { itemName: string }; size?: number; }) { const primary = getLocalIconUrl(item.itemName); return (
{item.itemName} { const img = e.target as HTMLImageElement; img.style.opacity = "0"; }} loading="lazy" />
); } /** Schematic grid of how a layout places the first furniture items. */ export function LayoutPreview({ layout, items, compact = false, }: { layout: string; items: OrganizeImportItem[]; compact?: boolean; }) { const grid = LAYOUT_GRIDS[layout]; if (!grid) { return (
{layout}
); } const cells = grid.cols * grid.rows; return (
{Array.from({ length: cells }).map((_, index) => { const item = items[index]; return item ? (
) : (
); })}
); } interface GroupItemListProps { items: OrganizeImportItem[]; moveTargets: OrganizeImportGroup[]; selection: Set; destDupes: Set; onToggleSelect: (itemId: number, checked: boolean) => void; onMoveItem: (item: OrganizeImportItem, toName: string) => void; onDragStart: (item: OrganizeImportItem) => void; onDragEnd: (item: OrganizeImportItem) => void; } // Virtualized row list, scrolled inside its own container so groups with // hundreds of furniture items stay smooth. export function GroupItemList({ items, moveTargets, selection, destDupes, onToggleSelect, onMoveItem, onDragStart, onDragEnd, }: GroupItemListProps) { const t = useTranslations("pages.admin.catalog.workspace.organizeImports"); const scrollRef = useRef(null); const rowVirtualizer = useVirtualizer({ count: items.length, getScrollElement: () => scrollRef.current, estimateSize: () => 34, overscan: 14, }); return (
8 ? "max-h-64" : "max-h-none", )} style={{ height: items.length > 8 ? undefined : "auto" }} > {items.length === 0 ? (

{t("emptyCategory")}

) : (
{rowVirtualizer.getVirtualItems().map((vItem) => { const item = items[vItem.index]; const checked = selection.has(item.itemId); const isDupe = destDupes.has(item.itemName); return ( // biome-ignore lint/a11y/noStaticElementInteractions: draggable row with native HTML5 DnD
{ e.dataTransfer.effectAllowed = "move"; e.dataTransfer.setData("text/plain", String(item.itemId)); onDragStart(item); }} onDragEnd={() => onDragEnd(item)} > onToggleSelect(item.itemId, e.target.checked) } />

{item.publicName || item.itemName}

{item.itemName}

{item.alreadyPlaced ? ( {item.sourcePageCaption ? item.sourcePageCaption.replace( new RegExp(`^${t("impPrefix")}`), "", ) : t("isMoved")} ) : ( {t("isNew")} )} {isDupe && ( {t("dupeOnDestShort")} )} {moveTargets.length > 0 && ( )}
); })}
)}
); }