| | | 1 | | import { useState } from "react"; |
| | | 2 | | import type { ItemResponse, LocationResponse, RoomResponse } from "~/api/client"; |
| | | 3 | | |
| | 0 | 4 | | export function Win98Tree({ |
| | | 5 | | locations, rooms, items, categories, |
| | | 6 | | filterRoomId, filterCategory, |
| | | 7 | | onSelectRoom, onSelectCategory, onAddRoom, |
| | | 8 | | }: { |
| | | 9 | | locations: LocationResponse[]; |
| | | 10 | | rooms: RoomResponse[]; |
| | | 11 | | items: ItemResponse[]; |
| | | 12 | | categories: string[]; |
| | | 13 | | filterRoomId: number | null; |
| | | 14 | | filterCategory: string | null; |
| | | 15 | | onSelectRoom: (id: number) => void; |
| | | 16 | | onSelectCategory: (name: string) => void; |
| | | 17 | | onAddRoom: (locationId: number) => void; |
| | | 18 | | }) { |
| | 0 | 19 | | const [collapsed, setCollapsed] = useState<Set<number>>(new Set()); |
| | 0 | 20 | | const [rootCollapsed, setRootCollapsed] = useState(false); |
| | 0 | 21 | | const [categoriesCollapsed, setCategoriesCollapsed] = useState(false); |
| | | 22 | | |
| | 0 | 23 | | const toggle = (id: number) => { |
| | 0 | 24 | | setCollapsed(prev => { |
| | 0 | 25 | | const next = new Set(prev); |
| | 0 | 26 | | if (next.has(id)) next.delete(id); |
| | 0 | 27 | | else next.add(id); |
| | 0 | 28 | | return next; |
| | | 29 | | }); |
| | | 30 | | }; |
| | | 31 | | |
| | 0 | 32 | | return ( |
| | | 33 | | <div className="win98-tree"> |
| | | 34 | | <Row |
| | | 35 | | chevron={rootCollapsed ? "▶" : "▼"} |
| | | 36 | | icon={<img src="/brand/icon.svg" alt="" width={16} height={16} style={{ display: "block" }} />} |
| | | 37 | | label={<>clutter<span style={{ color: "#c4502a" }}>:stock</span></>} |
| | | 38 | | bold |
| | 0 | 39 | | onChevronClick={() => setRootCollapsed(v => !v)} |
| | | 40 | | /> |
| | | 41 | | |
| | | 42 | | {!rootCollapsed && ( |
| | | 43 | | <div className="win98-tree-indent"> |
| | 0 | 44 | | {locations.map(loc => { |
| | 0 | 45 | | const locRooms = rooms.filter(r => r.locationId === loc.id); |
| | 0 | 46 | | const isCollapsed = loc.id != null && collapsed.has(loc.id); |
| | 0 | 47 | | return ( |
| | | 48 | | <div key={loc.id}> |
| | | 49 | | <Row |
| | | 50 | | chevron={locRooms.length > 0 ? (isCollapsed ? "▶" : "▼") : ""} |
| | | 51 | | icon="🏠" |
| | | 52 | | label={loc.name ?? "Location"} |
| | 0 | 53 | | onChevronClick={() => loc.id != null && toggle(loc.id)} |
| | 0 | 54 | | onIconClick={() => loc.id != null && toggle(loc.id)} |
| | | 55 | | trailing={ |
| | | 56 | | <button |
| | | 57 | | type="button" |
| | 0 | 58 | | onClick={(e) => { e.stopPropagation(); if (loc.id != null) onAddRoom(loc.id); }} |
| | | 59 | | title="New room" |
| | | 60 | | className="win98-tree-add" |
| | | 61 | | >+</button> |
| | | 62 | | } |
| | | 63 | | /> |
| | | 64 | | {!isCollapsed && ( |
| | | 65 | | <div className="win98-tree-indent"> |
| | 0 | 66 | | {locRooms.map(room => { |
| | 0 | 67 | | const count = items.filter(i => i.roomId === room.id).length; |
| | 0 | 68 | | const selected = filterRoomId === room.id; |
| | 0 | 69 | | return ( |
| | | 70 | | <Row |
| | | 71 | | key={room.id} |
| | | 72 | | icon="📁" |
| | | 73 | | label={`${room.name ?? "Room"} (${count})`} |
| | | 74 | | selected={selected} |
| | 0 | 75 | | onRowClick={() => room.id != null && onSelectRoom(room.id)} |
| | | 76 | | /> |
| | | 77 | | ); |
| | | 78 | | })} |
| | | 79 | | {locRooms.length === 0 && ( |
| | | 80 | | <div className="win98-tree-empty">No rooms yet</div> |
| | | 81 | | )} |
| | | 82 | | </div> |
| | | 83 | | )} |
| | | 84 | | </div> |
| | | 85 | | ); |
| | | 86 | | })} |
| | | 87 | | </div> |
| | | 88 | | )} |
| | | 89 | | |
| | | 90 | | {categories.length > 0 && ( |
| | | 91 | | <> |
| | | 92 | | <div className="win98-tree-divider"> |
| | | 93 | | <button |
| | | 94 | | type="button" |
| | 0 | 95 | | onClick={() => setCategoriesCollapsed(v => !v)} |
| | | 96 | | className="win98-tree-divider-btn" |
| | | 97 | | > |
| | | 98 | | {categoriesCollapsed ? "▶" : "▼"} Categories |
| | | 99 | | </button> |
| | | 100 | | </div> |
| | | 101 | | {!categoriesCollapsed && ( |
| | | 102 | | <div className="win98-tree-indent"> |
| | 0 | 103 | | {categories.map(cat => { |
| | 0 | 104 | | const count = items.filter(i => i.category === cat).length; |
| | 0 | 105 | | const selected = filterCategory === cat; |
| | 0 | 106 | | return ( |
| | | 107 | | <Row |
| | | 108 | | key={cat} |
| | | 109 | | icon="🏷" |
| | | 110 | | label={`${cat} (${count})`} |
| | | 111 | | selected={selected} |
| | 0 | 112 | | onRowClick={() => onSelectCategory(cat)} |
| | | 113 | | /> |
| | | 114 | | ); |
| | | 115 | | })} |
| | | 116 | | </div> |
| | | 117 | | )} |
| | | 118 | | </> |
| | | 119 | | )} |
| | | 120 | | |
| | | 121 | | </div> |
| | | 122 | | ); |
| | | 123 | | } |
| | | 124 | | |
| | 0 | 125 | | function Row({ |
| | | 126 | | chevron, icon, label, selected, bold, |
| | | 127 | | onRowClick, onChevronClick, onIconClick, trailing, |
| | | 128 | | }: { |
| | | 129 | | chevron?: string; |
| | | 130 | | icon: React.ReactNode; |
| | | 131 | | label: React.ReactNode; |
| | | 132 | | selected?: boolean; |
| | | 133 | | bold?: boolean; |
| | | 134 | | onRowClick?: () => void; |
| | | 135 | | onChevronClick?: () => void; |
| | | 136 | | onIconClick?: () => void; |
| | | 137 | | trailing?: React.ReactNode; |
| | | 138 | | }) { |
| | 0 | 139 | | const interactive = !!onRowClick; |
| | 0 | 140 | | return ( |
| | | 141 | | <div |
| | | 142 | | className={`win98-tree-row${selected ? " win98-tree-row--selected" : ""}`} |
| | | 143 | | onClick={interactive ? onRowClick : undefined} |
| | | 144 | | role={interactive ? "button" : undefined} |
| | | 145 | | tabIndex={interactive ? 0 : undefined} |
| | | 146 | | style={{ cursor: interactive ? "pointer" : "default" }} |
| | | 147 | | > |
| | | 148 | | <span |
| | | 149 | | className="win98-tree-chevron" |
| | 0 | 150 | | onClick={onChevronClick ? (e) => { e.stopPropagation(); onChevronClick(); } : undefined} |
| | | 151 | | style={{ cursor: onChevronClick ? "pointer" : "default" }} |
| | | 152 | | > |
| | | 153 | | {chevron ?? ""} |
| | | 154 | | </span> |
| | | 155 | | <span |
| | | 156 | | className="win98-tree-icon" |
| | 0 | 157 | | onClick={onIconClick ? (e) => { e.stopPropagation(); onIconClick(); } : undefined} |
| | | 158 | | > |
| | | 159 | | {icon} |
| | | 160 | | </span> |
| | | 161 | | <span className="win98-tree-label" style={bold ? { fontWeight: 700 } : undefined}> |
| | | 162 | | {label} |
| | | 163 | | </span> |
| | | 164 | | {trailing && <span className="win98-tree-trailing">{trailing}</span>} |
| | | 165 | | </div> |
| | | 166 | | ); |
| | | 167 | | } |