1 parent
27de078f54
commit
e85e4d74ea
378 files changed
+20710
-22428
No files matched your search
@@ -1,4 +1,4 @@
|
||||
"use client";
|
||||
'use client'
|
||||
|
||||
import {
|
||||
Armchair,
|
||||
@@ -16,102 +16,115 @@ import {
|
||||
Trash2,
|
||||
Users,
|
||||
UserX,
|
||||
} from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { useMemo, useState } from "react";
|
||||
import { bulkDeleteRoomItems, deleteRoomItem, roomRconAction } from "@/actions/rooms";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Card, CardContent } from "@/components/ui/card";
|
||||
import { Checkbox } from "@/components/ui/checkbox";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
|
||||
import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||
import { useServerAction } from "@/hooks/use-server-action";
|
||||
import { EditItemDialog } from "./edit-item-dialog";
|
||||
} from 'lucide-react'
|
||||
import Link from 'next/link'
|
||||
import { useMemo, useState } from 'react'
|
||||
import { bulkDeleteRoomItems, deleteRoomItem, roomRconAction } from '@/actions/rooms'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Card, CardContent } from '@/components/ui/card'
|
||||
import { Checkbox } from '@/components/ui/checkbox'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '@/components/ui/select'
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from '@/components/ui/table'
|
||||
import { Tabs, TabsList, TabsTrigger } from '@/components/ui/tabs'
|
||||
import { useServerAction } from '@/hooks/use-server-action'
|
||||
import { EditItemDialog } from './edit-item-dialog'
|
||||
|
||||
interface FurniRow {
|
||||
id: number;
|
||||
baseId: number;
|
||||
name: string;
|
||||
itemName: string;
|
||||
type: string;
|
||||
x: number;
|
||||
y: number;
|
||||
z: number;
|
||||
rot: number;
|
||||
wallPos: string;
|
||||
userId: number;
|
||||
ownerUsername: string | null;
|
||||
extraData: string;
|
||||
id: number
|
||||
baseId: number
|
||||
name: string
|
||||
itemName: string
|
||||
type: string
|
||||
x: number
|
||||
y: number
|
||||
z: number
|
||||
rot: number
|
||||
wallPos: string
|
||||
userId: number
|
||||
ownerUsername: string | null
|
||||
extraData: string
|
||||
}
|
||||
|
||||
interface Props {
|
||||
room: {
|
||||
id: number;
|
||||
name: string;
|
||||
ownerUsername: string;
|
||||
};
|
||||
items?: FurniRow[];
|
||||
canEdit: boolean;
|
||||
id: number
|
||||
name: string
|
||||
ownerUsername: string
|
||||
}
|
||||
items?: FurniRow[]
|
||||
canEdit: boolean
|
||||
}
|
||||
|
||||
const PER_PAGE = 50;
|
||||
type SortKey = "id" | "name" | "type" | "pos" | "owner";
|
||||
type SortDir = "asc" | "desc";
|
||||
type View = "flat" | "grouped";
|
||||
const PER_PAGE = 50
|
||||
type SortKey = 'id' | 'name' | 'type' | 'pos' | 'owner'
|
||||
type SortDir = 'asc' | 'desc'
|
||||
type View = 'flat' | 'grouped'
|
||||
|
||||
function iconUrl(itemName: string) {
|
||||
if (!itemName) return null;
|
||||
const base = itemName.split("*")[0];
|
||||
return `/swf/dcr/hof_furni/icons/${base}_icon.png`;
|
||||
if (!itemName) return null
|
||||
const base = itemName.split('*')[0]
|
||||
return `/swf/dcr/hof_furni/icons/${base}_icon.png`
|
||||
}
|
||||
|
||||
function isWallType(t: string) {
|
||||
return t === "i";
|
||||
return t === 'i'
|
||||
}
|
||||
|
||||
export function FurniClient({ room, items = [], canEdit }: Props) {
|
||||
const { run, isPending } = useServerAction();
|
||||
const [search, setSearch] = useState("");
|
||||
const [page, setPage] = useState(1);
|
||||
const [view, setView] = useState<View>("flat");
|
||||
const [typeFilter, setTypeFilter] = useState<"all" | "floor" | "wall">("all");
|
||||
const [ownerFilter, setOwnerFilter] = useState<string>("all");
|
||||
const [sortKey, setSortKey] = useState<SortKey>("id");
|
||||
const [sortDir, setSortDir] = useState<SortDir>("desc");
|
||||
const [selected, setSelected] = useState<Set<number>>(new Set());
|
||||
const [editingItem, setEditingItem] = useState<FurniRow | null>(null);
|
||||
const { run, isPending } = useServerAction()
|
||||
const [search, setSearch] = useState('')
|
||||
const [page, setPage] = useState(1)
|
||||
const [view, setView] = useState<View>('flat')
|
||||
const [typeFilter, setTypeFilter] = useState<'all' | 'floor' | 'wall'>('all')
|
||||
const [ownerFilter, setOwnerFilter] = useState<string>('all')
|
||||
const [sortKey, setSortKey] = useState<SortKey>('id')
|
||||
const [sortDir, setSortDir] = useState<SortDir>('desc')
|
||||
const [selected, setSelected] = useState<Set<number>>(new Set())
|
||||
const [editingItem, setEditingItem] = useState<FurniRow | null>(null)
|
||||
|
||||
// ── Stats ──────────────────────────────────────────────────
|
||||
const stats = useMemo(() => {
|
||||
const floor = items.filter((i) => !isWallType(i.type)).length;
|
||||
const wall = items.filter((i) => isWallType(i.type)).length;
|
||||
const uniqueBases = new Set(items.map((i) => i.baseId)).size;
|
||||
const owners = new Set(items.map((i) => i.userId).filter((u) => u > 0)).size;
|
||||
return { total: items.length, floor, wall, uniqueBases, owners };
|
||||
}, [items]);
|
||||
const floor = items.filter((i) => !isWallType(i.type)).length
|
||||
const wall = items.filter((i) => isWallType(i.type)).length
|
||||
const uniqueBases = new Set(items.map((i) => i.baseId)).size
|
||||
const owners = new Set(items.map((i) => i.userId).filter((u) => u > 0)).size
|
||||
return { total: items.length, floor, wall, uniqueBases, owners }
|
||||
}, [items])
|
||||
|
||||
// ── Owner options for filter ───────────────────────────────
|
||||
const ownerOptions = useMemo(() => {
|
||||
const map = new Map<string, number>();
|
||||
const map = new Map<string, number>()
|
||||
for (const i of items) {
|
||||
const key = i.ownerUsername ?? `#${i.userId}`;
|
||||
map.set(key, (map.get(key) ?? 0) + 1);
|
||||
const key = i.ownerUsername ?? `#${i.userId}`
|
||||
map.set(key, (map.get(key) ?? 0) + 1)
|
||||
}
|
||||
return Array.from(map.entries()).sort((a, b) => b[1] - a[1]);
|
||||
}, [items]);
|
||||
return Array.from(map.entries()).sort((a, b) => b[1] - a[1])
|
||||
}, [items])
|
||||
|
||||
// ── Filtering ──────────────────────────────────────────────
|
||||
const filtered = useMemo(() => {
|
||||
const q = search.trim().toLowerCase();
|
||||
const q = search.trim().toLowerCase()
|
||||
return items.filter((i) => {
|
||||
if (typeFilter === "floor" && isWallType(i.type)) return false;
|
||||
if (typeFilter === "wall" && !isWallType(i.type)) return false;
|
||||
if (ownerFilter !== "all") {
|
||||
const key = i.ownerUsername ?? `#${i.userId}`;
|
||||
if (key !== ownerFilter) return false;
|
||||
if (typeFilter === 'floor' && isWallType(i.type)) return false
|
||||
if (typeFilter === 'wall' && !isWallType(i.type)) return false
|
||||
if (ownerFilter !== 'all') {
|
||||
const key = i.ownerUsername ?? `#${i.userId}`
|
||||
if (key !== ownerFilter) return false
|
||||
}
|
||||
if (q) {
|
||||
return (
|
||||
@@ -119,119 +132,119 @@ export function FurniClient({ room, items = [], canEdit }: Props) {
|
||||
i.itemName.toLowerCase().includes(q) ||
|
||||
String(i.baseId).includes(q) ||
|
||||
String(i.id).includes(q)
|
||||
);
|
||||
)
|
||||
}
|
||||
return true;
|
||||
});
|
||||
}, [items, search, typeFilter, ownerFilter]);
|
||||
return true
|
||||
})
|
||||
}, [items, search, typeFilter, ownerFilter])
|
||||
|
||||
// ── Sorting ────────────────────────────────────────────────
|
||||
const sorted = useMemo(() => {
|
||||
const arr = [...filtered];
|
||||
const arr = [...filtered]
|
||||
arr.sort((a, b) => {
|
||||
let cmp = 0;
|
||||
let cmp = 0
|
||||
switch (sortKey) {
|
||||
case "id":
|
||||
cmp = a.id - b.id;
|
||||
break;
|
||||
case "name":
|
||||
cmp = a.name.localeCompare(b.name);
|
||||
break;
|
||||
case "type":
|
||||
cmp = a.type.localeCompare(b.type);
|
||||
break;
|
||||
case "pos":
|
||||
cmp = a.x - b.x || a.y - b.y;
|
||||
break;
|
||||
case "owner":
|
||||
cmp = (a.ownerUsername ?? "").localeCompare(b.ownerUsername ?? "");
|
||||
break;
|
||||
case 'id':
|
||||
cmp = a.id - b.id
|
||||
break
|
||||
case 'name':
|
||||
cmp = a.name.localeCompare(b.name)
|
||||
break
|
||||
case 'type':
|
||||
cmp = a.type.localeCompare(b.type)
|
||||
break
|
||||
case 'pos':
|
||||
cmp = a.x - b.x || a.y - b.y
|
||||
break
|
||||
case 'owner':
|
||||
cmp = (a.ownerUsername ?? '').localeCompare(b.ownerUsername ?? '')
|
||||
break
|
||||
}
|
||||
return sortDir === "asc" ? cmp : -cmp;
|
||||
});
|
||||
return arr;
|
||||
}, [filtered, sortKey, sortDir]);
|
||||
return sortDir === 'asc' ? cmp : -cmp
|
||||
})
|
||||
return arr
|
||||
}, [filtered, sortKey, sortDir])
|
||||
|
||||
// ── Grouped view ───────────────────────────────────────────
|
||||
const grouped = useMemo(() => {
|
||||
const map = new Map<number, { base: FurniRow; count: number; ids: number[] }>();
|
||||
const map = new Map<number, { base: FurniRow; count: number; ids: number[] }>()
|
||||
for (const i of filtered) {
|
||||
const existing = map.get(i.baseId);
|
||||
const existing = map.get(i.baseId)
|
||||
if (existing) {
|
||||
existing.count++;
|
||||
existing.ids.push(i.id);
|
||||
existing.count++
|
||||
existing.ids.push(i.id)
|
||||
} else {
|
||||
map.set(i.baseId, { base: i, count: 1, ids: [i.id] });
|
||||
map.set(i.baseId, { base: i, count: 1, ids: [i.id] })
|
||||
}
|
||||
}
|
||||
return Array.from(map.values()).sort((a, b) => b.count - a.count);
|
||||
}, [filtered]);
|
||||
return Array.from(map.values()).sort((a, b) => b.count - a.count)
|
||||
}, [filtered])
|
||||
|
||||
// ── Pagination (flat view only) ─────────────────────────────
|
||||
const lastPage = Math.max(1, Math.ceil(sorted.length / PER_PAGE));
|
||||
const currentPage = Math.min(page, lastPage);
|
||||
const pageRows = sorted.slice((currentPage - 1) * PER_PAGE, currentPage * PER_PAGE);
|
||||
const lastPage = Math.max(1, Math.ceil(sorted.length / PER_PAGE))
|
||||
const currentPage = Math.min(page, lastPage)
|
||||
const pageRows = sorted.slice((currentPage - 1) * PER_PAGE, currentPage * PER_PAGE)
|
||||
|
||||
// ── Handlers ───────────────────────────────────────────────
|
||||
function toggleSort(key: SortKey) {
|
||||
if (sortKey === key) {
|
||||
setSortDir((d) => (d === "asc" ? "desc" : "asc"));
|
||||
setSortDir((d) => (d === 'asc' ? 'desc' : 'asc'))
|
||||
} else {
|
||||
setSortKey(key);
|
||||
setSortDir("asc");
|
||||
setSortKey(key)
|
||||
setSortDir('asc')
|
||||
}
|
||||
}
|
||||
|
||||
function SortIcon({ k }: { k: SortKey }) {
|
||||
if (sortKey !== k) return <ArrowUpDown className="w-3 h-3 opacity-40" />;
|
||||
return sortDir === "asc" ? <ArrowUp className="w-3 h-3" /> : <ArrowDown className="w-3 h-3" />;
|
||||
if (sortKey !== k) return <ArrowUpDown className="w-3 h-3 opacity-40" />
|
||||
return sortDir === 'asc' ? <ArrowUp className="w-3 h-3" /> : <ArrowDown className="w-3 h-3" />
|
||||
}
|
||||
|
||||
function togglePageSelection(checked: boolean) {
|
||||
const next = new Set(selected);
|
||||
const next = new Set(selected)
|
||||
for (const r of pageRows) {
|
||||
if (checked) next.add(r.id);
|
||||
else next.delete(r.id);
|
||||
if (checked) next.add(r.id)
|
||||
else next.delete(r.id)
|
||||
}
|
||||
setSelected(next);
|
||||
setSelected(next)
|
||||
}
|
||||
|
||||
function toggleRowSelection(id: number, checked: boolean) {
|
||||
const next = new Set(selected);
|
||||
if (checked) next.add(id);
|
||||
else next.delete(id);
|
||||
setSelected(next);
|
||||
const next = new Set(selected)
|
||||
if (checked) next.add(id)
|
||||
else next.delete(id)
|
||||
setSelected(next)
|
||||
}
|
||||
|
||||
function handleRcon(action: "reload" | "kick") {
|
||||
if (action === "kick" && !confirm("Kick all users from this room?")) return;
|
||||
function handleRcon(action: 'reload' | 'kick') {
|
||||
if (action === 'kick' && !confirm('Kick all users from this room?')) return
|
||||
run(() => roomRconAction({ roomId: room.id, action }), {
|
||||
successMessage: action === "reload" ? "Room reloaded" : "All users kicked",
|
||||
});
|
||||
successMessage: action === 'reload' ? 'Room reloaded' : 'All users kicked',
|
||||
})
|
||||
}
|
||||
|
||||
function handleDelete(itemId: number, name: string) {
|
||||
if (!confirm(`Delete "${name}" (#${itemId}) from this room?`)) return;
|
||||
run(() => deleteRoomItem({ roomId: room.id, itemId }), { successMessage: "Item deleted" });
|
||||
if (!confirm(`Delete "${name}" (#${itemId}) from this room?`)) return
|
||||
run(() => deleteRoomItem({ roomId: room.id, itemId }), { successMessage: 'Item deleted' })
|
||||
}
|
||||
|
||||
function handleBulkDelete() {
|
||||
if (selected.size === 0) return;
|
||||
if (!confirm(`Delete ${selected.size} selected item${selected.size > 1 ? "s" : ""}?`)) return;
|
||||
if (selected.size === 0) return
|
||||
if (!confirm(`Delete ${selected.size} selected item${selected.size > 1 ? 's' : ''}?`)) return
|
||||
run(() => bulkDeleteRoomItems({ roomId: room.id, itemIds: Array.from(selected) }), {
|
||||
successMessage: `${selected.size} item(s) deleted`,
|
||||
onSuccess: () => setSelected(new Set()),
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
function handleDeleteGroup(_baseId: number, ids: number[], name: string) {
|
||||
if (!confirm(`Delete all ${ids.length} "${name}" from this room?`)) return;
|
||||
if (!confirm(`Delete all ${ids.length} "${name}" from this room?`)) return
|
||||
run(() => bulkDeleteRoomItems({ roomId: room.id, itemIds: ids }), {
|
||||
successMessage: `${ids.length} item(s) deleted`,
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
const allPageSelected = pageRows.length > 0 && pageRows.every((r) => selected.has(r.id));
|
||||
const allPageSelected = pageRows.length > 0 && pageRows.every((r) => selected.has(r.id))
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
@@ -255,10 +268,20 @@ export function FurniClient({ room, items = [], canEdit }: Props) {
|
||||
</div>
|
||||
{canEdit && (
|
||||
<div className="flex gap-2">
|
||||
<Button variant="outline" size="sm" onClick={() => handleRcon("reload")} disabled={isPending}>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => handleRcon('reload')}
|
||||
disabled={isPending}
|
||||
>
|
||||
<RefreshCw className="w-4 h-4 mr-1" /> Reload
|
||||
</Button>
|
||||
<Button variant="destructive" size="sm" onClick={() => handleRcon("kick")} disabled={isPending}>
|
||||
<Button
|
||||
variant="destructive"
|
||||
size="sm"
|
||||
onClick={() => handleRcon('kick')}
|
||||
disabled={isPending}
|
||||
>
|
||||
<UserX className="w-4 h-4 mr-1" /> Kick All
|
||||
</Button>
|
||||
</div>
|
||||
@@ -270,7 +293,11 @@ export function FurniClient({ room, items = [], canEdit }: Props) {
|
||||
<StatCard icon={<Armchair className="w-4 h-4" />} label="Total" value={stats.total} />
|
||||
<StatCard icon={<Layers className="w-4 h-4" />} label="Floor" value={stats.floor} />
|
||||
<StatCard icon={<Frame className="w-4 h-4" />} label="Wall" value={stats.wall} />
|
||||
<StatCard icon={<Armchair className="w-4 h-4" />} label="Unique furni" value={stats.uniqueBases} />
|
||||
<StatCard
|
||||
icon={<Armchair className="w-4 h-4" />}
|
||||
label="Unique furni"
|
||||
value={stats.uniqueBases}
|
||||
/>
|
||||
<StatCard icon={<Users className="w-4 h-4" />} label="Owners" value={stats.owners} />
|
||||
</div>
|
||||
|
||||
@@ -284,8 +311,8 @@ export function FurniClient({ room, items = [], canEdit }: Props) {
|
||||
placeholder="Search by name or ID..."
|
||||
value={search}
|
||||
onChange={(e) => {
|
||||
setSearch(e.target.value);
|
||||
setPage(1);
|
||||
setSearch(e.target.value)
|
||||
setPage(1)
|
||||
}}
|
||||
className="pl-8"
|
||||
/>
|
||||
@@ -293,9 +320,9 @@ export function FurniClient({ room, items = [], canEdit }: Props) {
|
||||
|
||||
<Select
|
||||
value={typeFilter}
|
||||
onValueChange={(v: "all" | "floor" | "wall") => {
|
||||
setTypeFilter(v);
|
||||
setPage(1);
|
||||
onValueChange={(v: 'all' | 'floor' | 'wall') => {
|
||||
setTypeFilter(v)
|
||||
setPage(1)
|
||||
}}
|
||||
>
|
||||
<SelectTrigger className="w-[140px]">
|
||||
@@ -311,8 +338,8 @@ export function FurniClient({ room, items = [], canEdit }: Props) {
|
||||
<Select
|
||||
value={ownerFilter}
|
||||
onValueChange={(v) => {
|
||||
setOwnerFilter(v);
|
||||
setPage(1);
|
||||
setOwnerFilter(v)
|
||||
setPage(1)
|
||||
}}
|
||||
>
|
||||
<SelectTrigger className="w-[180px]">
|
||||
@@ -337,7 +364,7 @@ export function FurniClient({ room, items = [], canEdit }: Props) {
|
||||
</div>
|
||||
|
||||
{/* Bulk action bar */}
|
||||
{view === "flat" && selected.size > 0 && canEdit && (
|
||||
{view === 'flat' && selected.size > 0 && canEdit && (
|
||||
<div className="flex items-center justify-between bg-muted/50 border rounded-md px-3 py-2">
|
||||
<span className="text-sm">
|
||||
<strong>{selected.size}</strong> selected
|
||||
@@ -346,7 +373,12 @@ export function FurniClient({ room, items = [], canEdit }: Props) {
|
||||
<Button variant="ghost" size="sm" onClick={() => setSelected(new Set())}>
|
||||
Clear
|
||||
</Button>
|
||||
<Button variant="destructive" size="sm" onClick={handleBulkDelete} disabled={isPending}>
|
||||
<Button
|
||||
variant="destructive"
|
||||
size="sm"
|
||||
onClick={handleBulkDelete}
|
||||
disabled={isPending}
|
||||
>
|
||||
<Trash2 className="w-4 h-4 mr-1" /> Delete selected
|
||||
</Button>
|
||||
</div>
|
||||
@@ -354,7 +386,7 @@ export function FurniClient({ room, items = [], canEdit }: Props) {
|
||||
)}
|
||||
|
||||
{/* ── Flat Table ─────────────────────────────────── */}
|
||||
{view === "flat" ? (
|
||||
{view === 'flat' ? (
|
||||
<>
|
||||
<div className="border rounded-md overflow-hidden">
|
||||
<Table>
|
||||
@@ -373,7 +405,7 @@ export function FurniClient({ room, items = [], canEdit }: Props) {
|
||||
<button
|
||||
type="button"
|
||||
className="flex items-center gap-1 hover:text-foreground"
|
||||
onClick={() => toggleSort("name")}
|
||||
onClick={() => toggleSort('name')}
|
||||
>
|
||||
Name <SortIcon k="name" />
|
||||
</button>
|
||||
@@ -382,7 +414,7 @@ export function FurniClient({ room, items = [], canEdit }: Props) {
|
||||
<button
|
||||
type="button"
|
||||
className="flex items-center gap-1 hover:text-foreground"
|
||||
onClick={() => toggleSort("type")}
|
||||
onClick={() => toggleSort('type')}
|
||||
>
|
||||
Type <SortIcon k="type" />
|
||||
</button>
|
||||
@@ -391,7 +423,7 @@ export function FurniClient({ room, items = [], canEdit }: Props) {
|
||||
<button
|
||||
type="button"
|
||||
className="flex items-center gap-1 hover:text-foreground"
|
||||
onClick={() => toggleSort("pos")}
|
||||
onClick={() => toggleSort('pos')}
|
||||
>
|
||||
Position <SortIcon k="pos" />
|
||||
</button>
|
||||
@@ -401,7 +433,7 @@ export function FurniClient({ room, items = [], canEdit }: Props) {
|
||||
<button
|
||||
type="button"
|
||||
className="flex items-center gap-1 hover:text-foreground"
|
||||
onClick={() => toggleSort("owner")}
|
||||
onClick={() => toggleSort('owner')}
|
||||
>
|
||||
Owner <SortIcon k="owner" />
|
||||
</button>
|
||||
@@ -410,7 +442,7 @@ export function FurniClient({ room, items = [], canEdit }: Props) {
|
||||
<button
|
||||
type="button"
|
||||
className="flex items-center gap-1 hover:text-foreground"
|
||||
onClick={() => toggleSort("id")}
|
||||
onClick={() => toggleSort('id')}
|
||||
>
|
||||
ID <SortIcon k="id" />
|
||||
</button>
|
||||
@@ -437,13 +469,13 @@ export function FurniClient({ room, items = [], canEdit }: Props) {
|
||||
</TableRow>
|
||||
) : (
|
||||
pageRows.map((item) => {
|
||||
const icon = iconUrl(item.itemName);
|
||||
const wall = isWallType(item.type);
|
||||
const isSelected = selected.has(item.id);
|
||||
const icon = iconUrl(item.itemName)
|
||||
const wall = isWallType(item.type)
|
||||
const isSelected = selected.has(item.id)
|
||||
return (
|
||||
<TableRow
|
||||
key={item.id}
|
||||
className={isSelected ? "bg-muted/40" : "hover:bg-muted/30"}
|
||||
className={isSelected ? 'bg-muted/40' : 'hover:bg-muted/30'}
|
||||
>
|
||||
{canEdit && (
|
||||
<TableCell>
|
||||
@@ -461,7 +493,7 @@ export function FurniClient({ room, items = [], canEdit }: Props) {
|
||||
alt={item.name}
|
||||
className="w-10 h-10 object-contain"
|
||||
onError={(e) => {
|
||||
(e.target as HTMLImageElement).style.visibility = "hidden";
|
||||
;(e.target as HTMLImageElement).style.visibility = 'hidden'
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
@@ -475,12 +507,14 @@ export function FurniClient({ room, items = [], canEdit }: Props) {
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Badge variant={wall ? "outline" : "secondary"}>
|
||||
{wall ? "Wall" : item.type === "s" ? "Floor" : item.type}
|
||||
<Badge variant={wall ? 'outline' : 'secondary'}>
|
||||
{wall ? 'Wall' : item.type === 's' ? 'Floor' : item.type}
|
||||
</Badge>
|
||||
</TableCell>
|
||||
<TableCell className="font-mono text-xs">
|
||||
{wall ? item.wallPos || "-" : `${item.x},${item.y},${item.z.toFixed(1)}`}
|
||||
{wall
|
||||
? item.wallPos || '-'
|
||||
: `${item.x},${item.y},${item.z.toFixed(1)}`}
|
||||
</TableCell>
|
||||
<TableCell className="font-mono text-xs">{item.rot}</TableCell>
|
||||
<TableCell className="text-sm">
|
||||
@@ -521,7 +555,7 @@ export function FurniClient({ room, items = [], canEdit }: Props) {
|
||||
</TableCell>
|
||||
)}
|
||||
</TableRow>
|
||||
);
|
||||
)
|
||||
})
|
||||
)}
|
||||
</TableBody>
|
||||
@@ -583,8 +617,8 @@ export function FurniClient({ room, items = [], canEdit }: Props) {
|
||||
</TableRow>
|
||||
) : (
|
||||
grouped.map((g) => {
|
||||
const icon = iconUrl(g.base.itemName);
|
||||
const wall = isWallType(g.base.type);
|
||||
const icon = iconUrl(g.base.itemName)
|
||||
const wall = isWallType(g.base.type)
|
||||
return (
|
||||
<TableRow key={g.base.baseId} className="hover:bg-muted/30">
|
||||
<TableCell>
|
||||
@@ -595,7 +629,7 @@ export function FurniClient({ room, items = [], canEdit }: Props) {
|
||||
alt={g.base.name}
|
||||
className="w-10 h-10 object-contain"
|
||||
onError={(e) => {
|
||||
(e.target as HTMLImageElement).style.visibility = "hidden";
|
||||
;(e.target as HTMLImageElement).style.visibility = 'hidden'
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
@@ -609,8 +643,8 @@ export function FurniClient({ room, items = [], canEdit }: Props) {
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Badge variant={wall ? "outline" : "secondary"}>
|
||||
{wall ? "Wall" : g.base.type === "s" ? "Floor" : g.base.type}
|
||||
<Badge variant={wall ? 'outline' : 'secondary'}>
|
||||
{wall ? 'Wall' : g.base.type === 's' ? 'Floor' : g.base.type}
|
||||
</Badge>
|
||||
</TableCell>
|
||||
<TableCell className="text-right">
|
||||
@@ -630,7 +664,7 @@ export function FurniClient({ room, items = [], canEdit }: Props) {
|
||||
</TableCell>
|
||||
)}
|
||||
</TableRow>
|
||||
);
|
||||
)
|
||||
})
|
||||
)}
|
||||
</TableBody>
|
||||
@@ -647,7 +681,7 @@ export function FurniClient({ room, items = [], canEdit }: Props) {
|
||||
onOpenChange={(o) => !o && setEditingItem(null)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
function StatCard({ icon, label, value }: { icon: React.ReactNode; label: string; value: number }) {
|
||||
@@ -663,5 +697,5 @@ function StatCard({ icon, label, value }: { icon: React.ReactNode; label: string
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
)
|
||||
}
|
||||
Reference in new issue
Block a user