ps99-api 2.4.0 → 2.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/workflows/release-on-main.yml +1 -2
- package/.idea/runConfigurations/test_changing.xml +1 -1
- package/debug_currency.json +57 -0
- package/debug_goals.json +271 -0
- package/dist/ps99-api.d.ts +2 -0
- package/dist/ps99-api.js +4 -1
- package/dist/ps99-api.js.map +1 -1
- package/dist/request-client/axios.js +6 -1
- package/dist/request-client/axios.js.map +1 -1
- package/dist/responses/collection/index.d.ts +1 -0
- package/dist/responses/collection/index.js +15 -0
- package/dist/responses/collection/index.js.map +1 -1
- package/dist/responses/collection/rarity.d.ts +1 -0
- package/example-web/react/package-lock.json +1504 -1470
- package/example-web/react2/package-lock.json +3089 -2766
- package/example-web/react2/package.json +6 -1
- package/example-web/react2/public/assets/gold_variant_icon.png +0 -0
- package/example-web/react2/public/assets/hot_cocoa_egg.png +0 -0
- package/example-web/react2/public/index.html +34 -31
- package/example-web/react2/src/App.tsx +15 -15
- package/example-web/react2/src/assets/guild_placeholder.png +0 -0
- package/example-web/react2/src/components/AchievementsComponent.tsx +74 -19
- package/example-web/react2/src/components/AutoSizer.tsx +49 -0
- package/example-web/react2/src/components/BoostsComponent.tsx +16 -5
- package/example-web/react2/src/components/BoothsComponent.tsx +22 -52
- package/example-web/react2/src/components/BoxesComponent.tsx +48 -16
- package/example-web/react2/src/components/BuffsComponent.tsx +82 -34
- package/example-web/react2/src/components/CharmsComponent.tsx +84 -24
- package/example-web/react2/src/components/CollectionConfigIndex.tsx +867 -33
- package/example-web/react2/src/components/CollectionsIndex.tsx +380 -27
- package/example-web/react2/src/components/CollectionsLayout.tsx +60 -0
- package/example-web/react2/src/components/CurrencyComponent.tsx +57 -39
- package/example-web/react2/src/components/DynamicCollectionConfigData.tsx +172 -15
- package/example-web/react2/src/components/EggsComponent.tsx +50 -12
- package/example-web/react2/src/components/EnchantsComponent.tsx +88 -42
- package/example-web/react2/src/components/FishingRodsComponent.tsx +36 -22
- package/example-web/react2/src/components/Footer.tsx +18 -8
- package/example-web/react2/src/components/FruitsComponent.tsx +40 -17
- package/example-web/react2/src/components/GenericFetchComponent.tsx +9 -1
- package/example-web/react2/src/components/GuildBattlesComponent.tsx +41 -34
- package/example-web/react2/src/components/Header.tsx +39 -52
- package/example-web/react2/src/components/HomePage.tsx +15 -17
- package/example-web/react2/src/components/HoverboardsComponent.tsx +23 -99
- package/example-web/react2/src/components/ImageComponent.tsx +255 -45
- package/example-web/react2/src/components/ItemCard.tsx +240 -0
- package/example-web/react2/src/components/LootboxesComponent.tsx +22 -7
- package/example-web/react2/src/components/MasteryComponent.tsx +165 -30
- package/example-web/react2/src/components/MerchantsComponent.tsx +41 -16
- package/example-web/react2/src/components/MiscItemsComponent.tsx +26 -31
- package/example-web/react2/src/components/PetsComponent.tsx +100 -61
- package/example-web/react2/src/components/PotionsComponent.tsx +121 -27
- package/example-web/react2/src/components/RandomEventsComponent.tsx +32 -23
- package/example-web/react2/src/components/RanksComponent.tsx +187 -62
- package/example-web/react2/src/components/RarityComponent.tsx +123 -5
- package/example-web/react2/src/components/ReactWindowMock.tsx +73 -0
- package/example-web/react2/src/components/RebirthsComponent.tsx +36 -19
- package/example-web/react2/src/components/SecretRoomsComponent.tsx +5 -4
- package/example-web/react2/src/components/SeedsComponent.tsx +41 -21
- package/example-web/react2/src/components/ShovelsComponent.tsx +21 -9
- package/example-web/react2/src/components/Sidebar.tsx +105 -0
- package/example-web/react2/src/components/SprinklersComponent.tsx +25 -10
- package/example-web/react2/src/components/Tooltip.tsx +36 -0
- package/example-web/react2/src/components/UltimatesComponent.tsx +28 -16
- package/example-web/react2/src/components/UpgradesComponent.tsx +97 -47
- package/example-web/react2/src/components/WateringCansComponent.tsx +20 -14
- package/example-web/react2/src/components/WorldsComponent.tsx +21 -11
- package/example-web/react2/src/components/XPPotionsComponent.tsx +28 -11
- package/example-web/react2/src/components/ZoneFlagsComponent.tsx +25 -14
- package/example-web/react2/src/components/ZonesComponent.tsx +43 -60
- package/example-web/react2/src/constants/collectionIcons.ts +29 -0
- package/example-web/react2/src/context/CollectionDataContext.tsx +62 -0
- package/example-web/react2/src/context/ScrollContext.tsx +35 -0
- package/example-web/react2/src/hooks/useCollapsibleHeader.ts +69 -0
- package/example-web/react2/src/hooks/useExpandableList.ts +38 -0
- package/example-web/react2/src/hooks/useItemResolution.ts +351 -0
- package/example-web/react2/src/index.css +257 -0
- package/example-web/react2/src/index.tsx +2 -1
- package/example-web/react2/src/utils/gigantix.ts +40 -0
- package/example-web/react2/temp_model.rbxm +0 -0
- package/example-web/react2/webpack.config.js +103 -47
- package/package.json +11 -11
- package/ranks.json +1 -0
- package/repro_collection_fetch.ts +33 -0
- package/repro_image_fetch.ts +50 -0
- package/src/__tests__/__snapshots__/ps99-api-changes.ts.snap +34841 -10439
- package/src/__tests__/__snapshots__/ps99-api-live.ts.snap +160667 -67217
- package/src/ps99-api.ts +9 -5
- package/src/request-client/axios.ts +6 -2
- package/src/responses/collection/index.ts +1 -0
- package/src/responses/collection/rarity.ts +1 -0
- package/tsconfig.json +1 -1
- package/example-web/react2/public/service-worker.js +0 -63
|
@@ -1,50 +1,884 @@
|
|
|
1
|
-
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
1
|
+
// @ts-nocheck
|
|
2
|
+
import React, { useEffect, useState, useRef } from "react";
|
|
3
|
+
import { useParams, Link, useNavigate } from "react-router-dom";
|
|
4
|
+
import Tooltip from "./Tooltip";
|
|
5
|
+
import { PetSimulator99API, Collections, CollectionName } from "ps99-api";
|
|
6
|
+
import ItemCard from "./ItemCard";
|
|
7
|
+
import DynamicCollectionConfigData from "./DynamicCollectionConfigData";
|
|
8
|
+
import ImageComponent from "./ImageComponent";
|
|
4
9
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
10
|
+
import { useItemResolution } from "../hooks/useItemResolution";
|
|
11
|
+
import { useCollectionData } from "../context/CollectionDataContext";
|
|
12
|
+
import { FixedSizeGrid, FixedSizeList } from "./ReactWindowMock";
|
|
13
|
+
import AutoSizer from "./AutoSizer";
|
|
14
|
+
import { useScrollPersistence } from "../context/ScrollContext";
|
|
15
|
+
import { useCollapsibleHeader } from '../hooks/useCollapsibleHeader';
|
|
16
|
+
import { formatGigantix } from "../utils/gigantix";
|
|
9
17
|
|
|
18
|
+
// const FixedSizeGrid = Grid;
|
|
19
|
+
// const FixedSizeList = List;
|
|
20
|
+
|
|
21
|
+
// Collections that need prefix stripping
|
|
22
|
+
const collectionsToClean = new Set([
|
|
23
|
+
"Achievement", "Achievements",
|
|
24
|
+
"Boost", "Boosts",
|
|
25
|
+
"Booth", "Booths",
|
|
26
|
+
"Box", "Boxes",
|
|
27
|
+
"Charms",
|
|
28
|
+
"Currency",
|
|
29
|
+
"Enchants",
|
|
30
|
+
"FishingRods",
|
|
31
|
+
"Fruit", "Fruits",
|
|
32
|
+
"Hoverboards",
|
|
33
|
+
"Mastery", "Masteries",
|
|
34
|
+
"Potions",
|
|
35
|
+
"RandomEvents",
|
|
36
|
+
"Rebirths",
|
|
37
|
+
"SecretRooms",
|
|
38
|
+
"Seeds",
|
|
39
|
+
"Shovels",
|
|
40
|
+
"Sprinklers",
|
|
41
|
+
"Ultimates",
|
|
42
|
+
"Upgrades",
|
|
43
|
+
"WateringCans",
|
|
44
|
+
"ZoneFlags",
|
|
45
|
+
"XPPotions"
|
|
46
|
+
]);
|
|
47
|
+
|
|
48
|
+
// Collections that should default to Compact List View
|
|
49
|
+
const COMPACT_COLLECTIONS = new Set([
|
|
50
|
+
"ZoneFlags",
|
|
51
|
+
"Enchants",
|
|
52
|
+
"Fruit",
|
|
53
|
+
"Boosts",
|
|
54
|
+
"Charms",
|
|
55
|
+
"Hoverboards",
|
|
56
|
+
"XPPotions",
|
|
57
|
+
"Seeds",
|
|
58
|
+
"Sprinklers",
|
|
59
|
+
"WateringCans",
|
|
60
|
+
"Shovels",
|
|
61
|
+
"FishingRods",
|
|
62
|
+
"Booths",
|
|
63
|
+
"Boxes",
|
|
64
|
+
"Rebirths",
|
|
65
|
+
"RandomEvents",
|
|
66
|
+
"SecretRooms",
|
|
67
|
+
"Ultimates"
|
|
68
|
+
]);
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
function getCleanName(name: string, collectionName: string): string {
|
|
72
|
+
if (!name || !collectionsToClean.has(collectionName)) return name;
|
|
73
|
+
|
|
74
|
+
const prefixes = [collectionName];
|
|
75
|
+
if (collectionName.endsWith('s')) prefixes.push(collectionName.slice(0, -1));
|
|
76
|
+
if (collectionName === 'Boxes') prefixes.push('Box');
|
|
77
|
+
|
|
78
|
+
for (const prefix of prefixes) {
|
|
79
|
+
if (name.startsWith(prefix)) {
|
|
80
|
+
const remainder = name.slice(prefix.length);
|
|
81
|
+
if (remainder.length === 0) return ""; // Exact match returns empty to allow fallback
|
|
82
|
+
if (remainder.startsWith(" | ")) return remainder.slice(3);
|
|
83
|
+
if (remainder.startsWith(" - ")) return remainder.slice(3);
|
|
84
|
+
if (remainder.startsWith(" ")) return remainder.slice(1);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
return name;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// --- Grid Cell Renderer ---
|
|
91
|
+
const GridCellRenderer = ({ columnIndex, rowIndex, style, data }: any) => {
|
|
92
|
+
const { items, columnCount, navigate, collectionName, variantFilter, shinyFilter, resolveIcon, GAP } = data;
|
|
93
|
+
const index = rowIndex * columnCount + columnIndex;
|
|
94
|
+
if (index >= items.length) return null;
|
|
95
|
+
const item = items[index];
|
|
96
|
+
const itemConfig = (item as any).configData || item;
|
|
97
|
+
const icon = resolveIcon(itemConfig);
|
|
98
|
+
const itemDataWithIcon = { ...itemConfig, icon };
|
|
99
|
+
|
|
100
|
+
const rawName = itemConfig.DisplayName || itemConfig.name || item.configName;
|
|
101
|
+
let label = getCleanName(rawName, collectionName);
|
|
102
|
+
if (!label && item.configName) {
|
|
103
|
+
label = getCleanName(item.configName, collectionName);
|
|
104
|
+
}
|
|
105
|
+
if (!label) label = rawName;
|
|
106
|
+
|
|
107
|
+
return (
|
|
108
|
+
<div style={style}>
|
|
109
|
+
<div style={{
|
|
110
|
+
position: 'absolute',
|
|
111
|
+
top: GAP / 2,
|
|
112
|
+
left: GAP / 2,
|
|
113
|
+
right: GAP / 2,
|
|
114
|
+
bottom: GAP / 2,
|
|
115
|
+
}}>
|
|
116
|
+
<div
|
|
117
|
+
onClick={() => navigate(`/collections/${collectionName}/${item.configName}`)}
|
|
118
|
+
style={{ cursor: "pointer", height: '100%' }}
|
|
119
|
+
>
|
|
120
|
+
<ItemCard
|
|
121
|
+
id={item.configName}
|
|
122
|
+
amount={""}
|
|
123
|
+
label={label}
|
|
124
|
+
itemData={itemDataWithIcon}
|
|
125
|
+
rarityColor={itemConfig.rarity?.Color || (itemConfig.Rarity?.Color)}
|
|
126
|
+
variant={collectionName === "Pets" ? (variantFilter as any) : undefined}
|
|
127
|
+
shiny={collectionName === "Pets" ? shinyFilter : undefined}
|
|
128
|
+
/>
|
|
129
|
+
</div>
|
|
130
|
+
</div>
|
|
131
|
+
</div>
|
|
132
|
+
);
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
// --- List Row Renderer ---
|
|
136
|
+
const ListRowRenderer = ({ index, style, data }: any) => {
|
|
137
|
+
const { items, navigate, collectionName, resolveIcon, getRarityColor, variantFilter, shinyFilter } = data;
|
|
138
|
+
const item = items[index];
|
|
139
|
+
if (!item) return null;
|
|
140
|
+
|
|
141
|
+
const itemConfig = (item as any).configData || item;
|
|
142
|
+
if (collectionName === "Enchants" && index < 3) {
|
|
143
|
+
console.log(`[Enchants Debug] Item ${index}:`, itemConfig);
|
|
144
|
+
console.log(`[Enchants Debug] Diminish:`, itemConfig.DiminishPowerThreshold);
|
|
145
|
+
console.log(`[Enchants Debug] Tiers:`, itemConfig.Tiers);
|
|
146
|
+
}
|
|
147
|
+
const icon = resolveIcon(itemConfig);
|
|
148
|
+
const rarityColor = itemConfig.rarity?.Color || itemConfig.Rarity?.Color || getRarityColor(itemConfig.rarity || itemConfig.Rarity) || "#ccc";
|
|
149
|
+
|
|
150
|
+
const rawName = itemConfig.DisplayName || itemConfig.name || item.configName;
|
|
151
|
+
let name = getCleanName(rawName, collectionName);
|
|
152
|
+
|
|
153
|
+
// If cleaning resulted in empty string (e.g. "Hoverboard" -> ""), try cleaning the configName ("Hoverboard | Original" -> "Original")
|
|
154
|
+
if (!name && item.configName) {
|
|
155
|
+
name = getCleanName(item.configName, collectionName);
|
|
156
|
+
}
|
|
157
|
+
// If still empty, revert to rawName
|
|
158
|
+
if (!name) name = rawName;
|
|
159
|
+
|
|
160
|
+
const rawSubtext = itemConfig.Description || itemConfig.Desc || (itemConfig.Tiers && itemConfig.Tiers[0]?.Desc) || item.configName;
|
|
161
|
+
const cleanSubtext = (itemConfig.Description || itemConfig.Desc || (itemConfig.Tiers && itemConfig.Tiers[0]?.Desc)) ? (itemConfig.Description || itemConfig.Desc || (itemConfig.Tiers && itemConfig.Tiers[0]?.Desc)) : getCleanName(item.configName, collectionName);
|
|
162
|
+
|
|
163
|
+
// Visual Styles for Pets
|
|
164
|
+
let rowBorder = `2px solid #e0e0e0`;
|
|
165
|
+
let rowBg = "#f9f9f9";
|
|
166
|
+
let iconFilter = "none";
|
|
167
|
+
let rainbowBackground = "";
|
|
168
|
+
|
|
169
|
+
if (collectionName === "Pets") {
|
|
170
|
+
if (variantFilter === "Golden") {
|
|
171
|
+
iconFilter = "sepia(100%) saturate(300%) hue-rotate(10deg)";
|
|
172
|
+
rowBorder = "2px solid #FFD700";
|
|
173
|
+
rowBg = "#FFFDF0";
|
|
174
|
+
} else if (variantFilter === "Rainbow") {
|
|
175
|
+
iconFilter = "saturate(200%) hue-rotate(30deg) contrast(120%)";
|
|
176
|
+
// Gradient border trick -> requires setting background to padding-box + border-box
|
|
177
|
+
rainbowBackground = "linear-gradient(#f9f9f9, #f9f9f9) padding-box, linear-gradient(45deg, #ff0000, #ff7f00, #ffff00, #00ff00, #0000ff, #4b0082, #9400d3) border-box";
|
|
178
|
+
rowBorder = "2px solid transparent";
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
return (
|
|
183
|
+
<div style={{ ...style, padding: "5px 10px", boxSizing: "border-box" }}>
|
|
184
|
+
<div
|
|
185
|
+
onClick={() => navigate(`/collections/${collectionName}/${item.configName}`)}
|
|
186
|
+
style={{
|
|
187
|
+
display: "flex",
|
|
188
|
+
alignItems: "center",
|
|
189
|
+
backgroundColor: rowBg,
|
|
190
|
+
background: rainbowBackground || rowBg,
|
|
191
|
+
borderRadius: "12px",
|
|
192
|
+
padding: "8px 15px",
|
|
193
|
+
height: "calc(100% - 10px)", // Account for padding
|
|
194
|
+
cursor: "pointer",
|
|
195
|
+
border: rowBorder,
|
|
196
|
+
boxShadow: "0 2px 5px rgba(0,0,0,0.05)",
|
|
197
|
+
borderLeft: (collectionName !== "Pets" || variantFilter === "Normal") ? `6px solid ${rarityColor}` : undefined,
|
|
198
|
+
transition: "transform 0.1s active",
|
|
199
|
+
position: "relative",
|
|
200
|
+
overflow: "hidden"
|
|
201
|
+
}}
|
|
202
|
+
onMouseDown={(e) => e.currentTarget.style.transform = "scale(0.98)"}
|
|
203
|
+
onMouseUp={(e) => e.currentTarget.style.transform = "scale(1)"}
|
|
204
|
+
onMouseLeave={(e) => e.currentTarget.style.transform = "scale(1)"}
|
|
205
|
+
>
|
|
206
|
+
{/* Shiny Overlay */}
|
|
207
|
+
{collectionName === "Pets" && shinyFilter && (
|
|
208
|
+
<div style={{
|
|
209
|
+
position: "absolute", top: 0, left: 0, right: 0, bottom: 0,
|
|
210
|
+
pointerEvents: "none", zIndex: 10,
|
|
211
|
+
backgroundImage: "url('data:image/svg+xml;utf8,%3Csvg width=\"20\" height=\"20\" viewBox=\"0 0 20 20\" xmlns=\"http://www.w3.org/2000/svg\"%3E%3Cpath d=\"M10 0L12 8L20 10L12 12L10 20L8 12L0 10L8 8L10 0Z\" fill=\"%23FFD700\" opacity=\"0.4\"/%3E%3C/svg%3E')",
|
|
212
|
+
backgroundSize: "40px 40px", opacity: 0.5,
|
|
213
|
+
}} />
|
|
214
|
+
)}
|
|
215
|
+
|
|
216
|
+
{/* Icon (if available) */}
|
|
217
|
+
{icon ? (
|
|
218
|
+
<div style={{
|
|
219
|
+
width: "48px",
|
|
220
|
+
height: "48px",
|
|
221
|
+
marginRight: "15px",
|
|
222
|
+
flexShrink: 0,
|
|
223
|
+
borderRadius: "8px",
|
|
224
|
+
overflow: "hidden",
|
|
225
|
+
backgroundColor: "#fff",
|
|
226
|
+
border: "1px solid #eee",
|
|
227
|
+
display: "flex", alignItems: "center", justifyContent: "center",
|
|
228
|
+
zIndex: 2
|
|
229
|
+
}}>
|
|
230
|
+
<ImageComponent
|
|
231
|
+
src={icon}
|
|
232
|
+
alt={name}
|
|
233
|
+
style={{ width: "100%", height: "100%", objectFit: "contain", filter: iconFilter }}
|
|
234
|
+
/>
|
|
235
|
+
</div>
|
|
236
|
+
) : (
|
|
237
|
+
<div style={{
|
|
238
|
+
width: "48px", height: "48px", marginRight: "15px", flexShrink: 0,
|
|
239
|
+
display: "flex", alignItems: "center", justifyContent: "center",
|
|
240
|
+
fontSize: "1.5rem", backgroundColor: "#eee", borderRadius: "8px", color: "#aaa",
|
|
241
|
+
zIndex: 2
|
|
242
|
+
}}>
|
|
243
|
+
?
|
|
244
|
+
</div>
|
|
245
|
+
)}
|
|
246
|
+
|
|
247
|
+
{/* Text Content */}
|
|
248
|
+
<div style={{ flex: 1, overflow: "hidden", zIndex: 2 }}>
|
|
249
|
+
<div style={{
|
|
250
|
+
fontSize: "1.1rem",
|
|
251
|
+
fontWeight: "700",
|
|
252
|
+
color: "#333",
|
|
253
|
+
whiteSpace: "nowrap",
|
|
254
|
+
overflow: "hidden",
|
|
255
|
+
textOverflow: "ellipsis",
|
|
256
|
+
fontFamily: "'Fredoka One', cursive, sans-serif",
|
|
257
|
+
}}>
|
|
258
|
+
{name}
|
|
259
|
+
</div>
|
|
260
|
+
{/* Optional Subtext - Only show if different from name */}
|
|
261
|
+
{(cleanSubtext && cleanSubtext !== name) && (
|
|
262
|
+
<div style={{
|
|
263
|
+
fontSize: "0.85rem",
|
|
264
|
+
color: "#666",
|
|
265
|
+
marginTop: "2px",
|
|
266
|
+
display: "-webkit-box",
|
|
267
|
+
WebkitLineClamp: 2,
|
|
268
|
+
WebkitBoxOrient: "vertical",
|
|
269
|
+
overflow: "hidden",
|
|
270
|
+
lineHeight: "1.2em",
|
|
271
|
+
maxHeight: "2.4em"
|
|
272
|
+
}}>
|
|
273
|
+
{cleanSubtext}
|
|
274
|
+
</div>
|
|
275
|
+
)}
|
|
276
|
+
|
|
277
|
+
{/* Generic Stats Rendering */}
|
|
278
|
+
<div style={{ display: 'flex', gap: '15px', marginTop: '2px', alignItems: 'center', flexWrap: 'wrap' }}>
|
|
279
|
+
{/* Duration / Time / ZoneFlags */}
|
|
280
|
+
{(itemConfig.Duration || itemConfig.Time) && (
|
|
281
|
+
<div style={{ fontSize: "0.85rem", color: "#1976d2", fontWeight: "600" }}>
|
|
282
|
+
⏱ {formatGigantix(itemConfig.Duration || itemConfig.Time)}s
|
|
283
|
+
</div>
|
|
284
|
+
)}
|
|
285
|
+
|
|
286
|
+
{/* Power (Enchants/Potions) */}
|
|
287
|
+
{(itemConfig.Power || (itemConfig.Tiers && itemConfig.Tiers[0]?.Power)) && (
|
|
288
|
+
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
|
|
289
|
+
<div style={{ fontSize: "0.85rem", color: "#E65100", fontWeight: "600" }}>
|
|
290
|
+
⚡ {formatGigantix(itemConfig.Power || itemConfig.Tiers[0]?.Power)}
|
|
291
|
+
</div>
|
|
292
|
+
|
|
293
|
+
{/* Enchant Diminish Cap Calculator */}
|
|
294
|
+
{collectionName === "Enchants" && itemConfig.DiminishPowerThreshold && itemConfig.Tiers && itemConfig.Tiers.length > 0 && (
|
|
295
|
+
(() => {
|
|
296
|
+
const maxTier = itemConfig.Tiers[itemConfig.Tiers.length - 1];
|
|
297
|
+
const power = maxTier.Power;
|
|
298
|
+
if (!power) return null;
|
|
299
|
+
|
|
300
|
+
const countToCap = Math.ceil(itemConfig.DiminishPowerThreshold / power);
|
|
301
|
+
|
|
302
|
+
return (
|
|
303
|
+
<div style={{
|
|
304
|
+
fontSize: "0.75rem",
|
|
305
|
+
backgroundColor: "#fff3e0",
|
|
306
|
+
color: "#e65100",
|
|
307
|
+
border: "1px solid #ffe0b2",
|
|
308
|
+
padding: "2px 6px",
|
|
309
|
+
borderRadius: "6px",
|
|
310
|
+
display: "flex",
|
|
311
|
+
alignItems: "center",
|
|
312
|
+
gap: "4px",
|
|
313
|
+
fontWeight: "bold"
|
|
314
|
+
}} title={`Diminish Threshold: ${itemConfig.DiminishPowerThreshold} | Max Tier Power: ${power}`}>
|
|
315
|
+
<span>🛑 Cap: {countToCap}x</span>
|
|
316
|
+
</div>
|
|
317
|
+
);
|
|
318
|
+
})()
|
|
319
|
+
)}
|
|
320
|
+
</div>
|
|
321
|
+
)}
|
|
322
|
+
|
|
323
|
+
{/* Speed (Hoverboards) */}
|
|
324
|
+
{(collectionName === "Hoverboards" || itemConfig.Speed || itemConfig.DefaultJumpSpeedBoost) && (
|
|
325
|
+
<div style={{ fontSize: "0.85rem", color: "#2E7D32", fontWeight: "600" }}>
|
|
326
|
+
💨 {itemConfig.Speed ? formatGigantix(itemConfig.Speed) : (itemConfig.DefaultJumpSpeedBoost || 'Normal')}
|
|
327
|
+
</div>
|
|
328
|
+
)}
|
|
329
|
+
|
|
330
|
+
{/* Boost (Boosts) */}
|
|
331
|
+
{(collectionName === "Boosts" || itemConfig.Boost || itemConfig.MaximumPercent) && (
|
|
332
|
+
<div style={{ fontSize: "0.85rem", color: "#9C27B0", fontWeight: "600" }}>
|
|
333
|
+
🚀 {formatGigantix(itemConfig.Boost || itemConfig.MaximumPercent || 0)}%
|
|
334
|
+
</div>
|
|
335
|
+
)}
|
|
336
|
+
|
|
337
|
+
{/* Multiplier */}
|
|
338
|
+
{itemConfig.Multiplier && (
|
|
339
|
+
<div style={{ fontSize: "0.85rem", color: "#C2185B", fontWeight: "600" }}>
|
|
340
|
+
✖ {itemConfig.Multiplier}x
|
|
341
|
+
</div>
|
|
342
|
+
)}
|
|
343
|
+
</div>
|
|
344
|
+
</div>
|
|
345
|
+
|
|
346
|
+
{/* Chevron > */}
|
|
347
|
+
<div style={{ marginLeft: "10px", color: "rgba(0,0,0,0.2)", fontWeight: "bold", fontSize: "1.2rem", zIndex: 2 }}>
|
|
348
|
+
›
|
|
349
|
+
</div>
|
|
350
|
+
</div>
|
|
351
|
+
</div>
|
|
352
|
+
);
|
|
353
|
+
};
|
|
354
|
+
|
|
355
|
+
|
|
356
|
+
interface CollectionConfigIndexProps { }
|
|
357
|
+
|
|
358
|
+
const CollectionConfigIndex: React.FC<CollectionConfigIndexProps> = () => {
|
|
359
|
+
const { collectionName: rawCollectionName } = useParams<{ collectionName: string }>();
|
|
360
|
+
// Normalize collection name to Title Case (e.g. "pets" -> "Pets") to match logic checks
|
|
361
|
+
const collectionName = React.useMemo(() => {
|
|
362
|
+
if (!rawCollectionName) return "";
|
|
363
|
+
return rawCollectionName.charAt(0).toUpperCase() + rawCollectionName.slice(1);
|
|
364
|
+
}, [rawCollectionName]);
|
|
365
|
+
|
|
366
|
+
const navigate = useNavigate();
|
|
367
|
+
// Call the hook at the top level so we can use it
|
|
368
|
+
const { resolveIcon, getRarityColor } = useItemResolution();
|
|
369
|
+
|
|
370
|
+
const { data, fetchCollection, isLoading } = useCollectionData();
|
|
371
|
+
|
|
372
|
+
const items = (data[collectionName || ""] || []) as Collections[];
|
|
373
|
+
const loading = isLoading(collectionName || "");
|
|
374
|
+
|
|
375
|
+
const [searchTerm, setSearchTerm] = useState<string>("");
|
|
376
|
+
const [windowWidth, setWindowWidth] = useState(window.innerWidth);
|
|
377
|
+
|
|
378
|
+
// State for Pets Filter
|
|
379
|
+
const [specialFilter, setSpecialFilter] = useState<"H" | "T" | "G" | null>(null);
|
|
380
|
+
const [variantFilter, setVariantFilter] = useState<"Normal" | "Golden" | "Rainbow">("Normal");
|
|
381
|
+
const [shinyFilter, setShinyFilter] = useState<boolean>(false);
|
|
382
|
+
const [showFiltersMobile, setShowFiltersMobile] = useState(false); // Mobile Toggle
|
|
383
|
+
|
|
384
|
+
// View Mode: 'grid' or 'list'
|
|
385
|
+
const [viewMode, setViewMode] = useState<'grid' | 'list'>('grid');
|
|
386
|
+
|
|
387
|
+
// Responsive check
|
|
388
|
+
useEffect(() => {
|
|
389
|
+
const handleResize = () => {
|
|
390
|
+
setWindowWidth(window.innerWidth);
|
|
391
|
+
};
|
|
392
|
+
window.addEventListener("resize", handleResize);
|
|
393
|
+
return () => window.removeEventListener("resize", handleResize);
|
|
394
|
+
}, []);
|
|
395
|
+
|
|
396
|
+
const isMobile = windowWidth < 768;
|
|
397
|
+
|
|
398
|
+
// Scroll Persistence
|
|
399
|
+
const { saveScrollPosition, getScrollPosition } = useScrollPersistence();
|
|
400
|
+
const scrollKey = `collection_config_${collectionName || 'default'}_${viewMode}`;
|
|
401
|
+
const initialScrollOffset = getScrollPosition(scrollKey);
|
|
402
|
+
|
|
403
|
+
// Save on unmount
|
|
10
404
|
useEffect(() => {
|
|
11
|
-
|
|
12
|
-
|
|
405
|
+
return () => {
|
|
406
|
+
saveScrollPosition(scrollKey, scrollRef.current);
|
|
407
|
+
};
|
|
408
|
+
}, [saveScrollPosition, scrollKey]);
|
|
13
409
|
|
|
14
|
-
|
|
15
|
-
|
|
410
|
+
useEffect(() => {
|
|
411
|
+
if (collectionName) {
|
|
412
|
+
fetchCollection(collectionName as CollectionName);
|
|
413
|
+
}
|
|
414
|
+
}, [collectionName, fetchCollection]);
|
|
16
415
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
416
|
+
// Process items for rendering
|
|
417
|
+
const processedItems = items.map((item) => {
|
|
418
|
+
const configData: any = (item as any).configData || item;
|
|
419
|
+
return {
|
|
420
|
+
...item,
|
|
421
|
+
configData,
|
|
422
|
+
};
|
|
423
|
+
});
|
|
424
|
+
|
|
425
|
+
const filteredItems = processedItems.filter((item) => {
|
|
426
|
+
const config = item.configData;
|
|
427
|
+
const name =
|
|
428
|
+
config.DisplayName || config.name || item.configName || "";
|
|
429
|
+
if (
|
|
430
|
+
searchTerm &&
|
|
431
|
+
!name.toLowerCase().includes(searchTerm.toLowerCase())
|
|
432
|
+
) {
|
|
433
|
+
return false;
|
|
434
|
+
}
|
|
435
|
+
return true;
|
|
436
|
+
});
|
|
437
|
+
|
|
438
|
+
|
|
439
|
+
// Apply Pet Filters
|
|
440
|
+
const finalItems = filteredItems.filter((item) => {
|
|
441
|
+
if (collectionName !== "Pets") return true;
|
|
442
|
+
|
|
443
|
+
const config = item.configData;
|
|
444
|
+
const name = config.DisplayName || config.name || item.configName || "";
|
|
445
|
+
|
|
446
|
+
// Special Filter
|
|
447
|
+
if (specialFilter === "H") {
|
|
448
|
+
if (!name.startsWith("Huge ")) return false;
|
|
449
|
+
} else if (specialFilter === "T") {
|
|
450
|
+
if (!name.startsWith("Titanic ")) return false;
|
|
451
|
+
} else if (specialFilter === "G") {
|
|
452
|
+
if (!name.startsWith("Gargantuan ")) return false;
|
|
453
|
+
} else {
|
|
454
|
+
// None selected: Exclude Huge, Titanic, Gargantuan
|
|
455
|
+
if (name.startsWith("Huge ") || name.startsWith("Titanic ") || name.startsWith("Gargantuan ")) return false;
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
return true;
|
|
459
|
+
});
|
|
460
|
+
|
|
461
|
+
// Determine View Mode
|
|
462
|
+
useEffect(() => {
|
|
463
|
+
if (isMobile) {
|
|
464
|
+
setViewMode('list');
|
|
465
|
+
return;
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
// Heuristic: If first 20 items have NO icons, default to list
|
|
469
|
+
// Only check if we have items
|
|
470
|
+
if (finalItems.length > 0) {
|
|
471
|
+
const sample = finalItems.slice(0, 20);
|
|
472
|
+
const hasImages = sample.some(item => !!resolveIcon(item.configData || item));
|
|
473
|
+
const hasImages = sample.some(item => !!resolveIcon(item.configData || item));
|
|
474
|
+
if (!hasImages || COMPACT_COLLECTIONS.has(collectionName)) {
|
|
475
|
+
setViewMode('list');
|
|
20
476
|
} else {
|
|
21
|
-
|
|
477
|
+
setViewMode('grid');
|
|
22
478
|
}
|
|
23
|
-
}
|
|
479
|
+
}
|
|
480
|
+
}, [isMobile, collectionName, finalItems.length]); // Re-evaluate on collection change or mobile resize, or items load
|
|
481
|
+
|
|
24
482
|
|
|
25
|
-
|
|
26
|
-
|
|
483
|
+
// Scroll Direction // Header Logic
|
|
484
|
+
const { showHeader, handleScroll, scrollRef, headerRef, headerHeight, contentPadding } = useCollapsibleHeader({ deps: [loading] });
|
|
27
485
|
|
|
28
|
-
|
|
29
|
-
|
|
486
|
+
// Loading State
|
|
487
|
+
if (loading) {
|
|
488
|
+
return (
|
|
489
|
+
<div
|
|
490
|
+
style={{
|
|
491
|
+
display: "flex",
|
|
492
|
+
justifyContent: "center",
|
|
493
|
+
alignItems: "center",
|
|
494
|
+
height: "50vh",
|
|
495
|
+
fontSize: "1.5rem",
|
|
496
|
+
fontWeight: "bold",
|
|
497
|
+
color: "#666",
|
|
498
|
+
}}
|
|
499
|
+
>
|
|
500
|
+
Loading {collectionName}...
|
|
501
|
+
</div>
|
|
502
|
+
);
|
|
30
503
|
}
|
|
31
504
|
|
|
32
|
-
|
|
33
|
-
<div
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
505
|
+
const renderFilters = () => (
|
|
506
|
+
<div style={{
|
|
507
|
+
display: "flex",
|
|
508
|
+
gap: "8px",
|
|
509
|
+
alignItems: "center",
|
|
510
|
+
flexWrap: isMobile ? "wrap" : "nowrap",
|
|
511
|
+
justifyContent: isMobile ? "center" : "flex-start",
|
|
512
|
+
padding: isMobile ? "10px 0" : "0",
|
|
513
|
+
backgroundColor: isMobile ? "#f5f5f5" : "transparent",
|
|
514
|
+
width: isMobile ? "100%" : "auto",
|
|
515
|
+
borderRadius: isMobile ? "12px" : "0",
|
|
516
|
+
}}>
|
|
517
|
+
{/* Specials: H, T, G */}
|
|
518
|
+
<div style={{ display: 'flex', gap: '5px' }}>
|
|
519
|
+
{['H', 'T', 'G'].map((symbol) => (
|
|
520
|
+
<button
|
|
521
|
+
key={symbol}
|
|
522
|
+
onClick={() => setSpecialFilter(specialFilter === symbol ? null : symbol as any)}
|
|
523
|
+
style={{
|
|
524
|
+
width: "36px",
|
|
525
|
+
height: "36px",
|
|
526
|
+
borderRadius: "50%",
|
|
527
|
+
border: "3px solid #333",
|
|
528
|
+
backgroundColor: specialFilter === symbol ? "#333" : "#fff",
|
|
529
|
+
color: specialFilter === symbol ? "#fff" : "#333",
|
|
530
|
+
fontSize: "1rem",
|
|
531
|
+
fontWeight: "bold",
|
|
532
|
+
cursor: "pointer",
|
|
533
|
+
display: "flex",
|
|
534
|
+
alignItems: "center",
|
|
535
|
+
justifyContent: "center",
|
|
536
|
+
boxShadow: "0 2px 0 #ccc",
|
|
537
|
+
}}
|
|
538
|
+
>
|
|
539
|
+
{symbol}
|
|
540
|
+
</button>
|
|
44
541
|
))}
|
|
45
|
-
</
|
|
542
|
+
</div>
|
|
543
|
+
|
|
544
|
+
<div style={{ width: "1px", height: "30px", backgroundColor: "#ddd", margin: "0 5px" }}></div>
|
|
545
|
+
|
|
546
|
+
{/* Variants: Normal, Golden, Rainbow */}
|
|
547
|
+
<div style={{ display: 'flex', gap: '5px' }}>
|
|
548
|
+
{[
|
|
549
|
+
{ symbol: '🐾', mode: 'Normal', color: '#fff' },
|
|
550
|
+
{ symbol: '★', mode: 'Golden', color: '#FFD700' },
|
|
551
|
+
{ symbol: '🌈', mode: 'Rainbow', color: 'inherit' }
|
|
552
|
+
].map(({ symbol, mode, color: iconColor }) => {
|
|
553
|
+
const isActive = variantFilter === mode;
|
|
554
|
+
const isRainbow = mode === 'Rainbow';
|
|
555
|
+
const isGold = mode === 'Golden';
|
|
556
|
+
|
|
557
|
+
return (
|
|
558
|
+
<button
|
|
559
|
+
key={mode}
|
|
560
|
+
onClick={() => setVariantFilter(mode as any)}
|
|
561
|
+
className={(isActive && isRainbow) ? "sheen-effect" : ""}
|
|
562
|
+
style={{
|
|
563
|
+
width: "40px", // slightly smaller for mobile fit
|
|
564
|
+
height: "40px",
|
|
565
|
+
borderRadius: "50%",
|
|
566
|
+
border: "3px solid #333",
|
|
567
|
+
backgroundColor: isActive
|
|
568
|
+
? (mode === 'Normal' ? "#e0e0e0" : "#333")
|
|
569
|
+
: "#fff",
|
|
570
|
+
color: isActive
|
|
571
|
+
? (mode === 'Normal' ? "#333" : "#fff")
|
|
572
|
+
: "#333",
|
|
573
|
+
fontSize: "1.2rem",
|
|
574
|
+
fontWeight: "bold",
|
|
575
|
+
cursor: "pointer",
|
|
576
|
+
display: "flex",
|
|
577
|
+
alignItems: "center",
|
|
578
|
+
justifyContent: "center",
|
|
579
|
+
boxShadow: "0 2px 0 #ccc",
|
|
580
|
+
filter: "none",
|
|
581
|
+
position: 'relative',
|
|
582
|
+
overflow: 'hidden',
|
|
583
|
+
padding: 0
|
|
584
|
+
}}
|
|
585
|
+
title={mode}
|
|
586
|
+
>
|
|
587
|
+
{isGold ? (
|
|
588
|
+
<img
|
|
589
|
+
src="/node-ps99-api/assets/gold_variant_icon.png"
|
|
590
|
+
alt="Gold"
|
|
591
|
+
style={{
|
|
592
|
+
width: '70%',
|
|
593
|
+
height: '70%',
|
|
594
|
+
objectFit: 'contain',
|
|
595
|
+
filter: isActive ? 'none' : 'grayscale(1) opacity(0.5)'
|
|
596
|
+
}}
|
|
597
|
+
/>
|
|
598
|
+
) : (
|
|
599
|
+
<span style={{
|
|
600
|
+
filter: mode === 'Golden' && !isActive ? "grayscale(1)" : "none",
|
|
601
|
+
color: (isActive && mode !== 'Rainbow' && mode !== 'Normal') ? iconColor : 'inherit'
|
|
602
|
+
}}>
|
|
603
|
+
{symbol}
|
|
604
|
+
</span>
|
|
605
|
+
)}
|
|
606
|
+
</button>
|
|
607
|
+
)
|
|
608
|
+
})}
|
|
609
|
+
</div>
|
|
610
|
+
|
|
611
|
+
{/* Shiny Toggle */}
|
|
612
|
+
<button
|
|
613
|
+
onClick={() => setShinyFilter(!shinyFilter)}
|
|
614
|
+
className={shinyFilter ? "sheen-effect" : ""}
|
|
615
|
+
style={{
|
|
616
|
+
width: "40px",
|
|
617
|
+
height: "40px",
|
|
618
|
+
borderRadius: "50%",
|
|
619
|
+
border: "3px solid #333",
|
|
620
|
+
backgroundColor: shinyFilter ? "#333" : "#fff",
|
|
621
|
+
color: shinyFilter ? "#fff" : "#333",
|
|
622
|
+
fontSize: "1.2rem",
|
|
623
|
+
fontWeight: "bold",
|
|
624
|
+
cursor: "pointer",
|
|
625
|
+
display: "flex",
|
|
626
|
+
alignItems: "center",
|
|
627
|
+
justifyContent: "center",
|
|
628
|
+
boxShadow: "0 2px 0 #ccc",
|
|
629
|
+
position: 'relative',
|
|
630
|
+
overflow: 'hidden',
|
|
631
|
+
marginLeft: "5px"
|
|
632
|
+
}}
|
|
633
|
+
>
|
|
634
|
+
✨
|
|
635
|
+
</button>
|
|
46
636
|
</div>
|
|
47
637
|
);
|
|
638
|
+
|
|
639
|
+
return (
|
|
640
|
+
<div
|
|
641
|
+
style={{
|
|
642
|
+
flex: 1,
|
|
643
|
+
display: "flex",
|
|
644
|
+
flexDirection: "column",
|
|
645
|
+
overflow: "hidden",
|
|
646
|
+
backgroundColor: "#ffffff",
|
|
647
|
+
height: "100%", // ensure it fills parent
|
|
648
|
+
position: 'relative' // Context for absolute header
|
|
649
|
+
}}
|
|
650
|
+
>
|
|
651
|
+
{/* Header Bar inside Window */}
|
|
652
|
+
<div
|
|
653
|
+
ref={headerRef}
|
|
654
|
+
style={{
|
|
655
|
+
padding: isMobile ? "10px 15px" : "15px 20px",
|
|
656
|
+
borderBottom: "4px solid #333",
|
|
657
|
+
display: "flex",
|
|
658
|
+
flexDirection: "column",
|
|
659
|
+
gap: isMobile ? "10px" : "0px",
|
|
660
|
+
backgroundColor: "#fff",
|
|
661
|
+
position: "absolute",
|
|
662
|
+
top: 0,
|
|
663
|
+
left: 0,
|
|
664
|
+
right: 0,
|
|
665
|
+
zIndex: 10,
|
|
666
|
+
transition: "transform 0.3s ease-in-out",
|
|
667
|
+
transform: showHeader ? "translateY(0)" : "translateY(-100%)",
|
|
668
|
+
}}
|
|
669
|
+
>
|
|
670
|
+
<div style={{ display: "flex", alignItems: "center", justifyContent: "space-between", width: "100%" }}>
|
|
671
|
+
<h2
|
|
672
|
+
style={{
|
|
673
|
+
margin: 0,
|
|
674
|
+
fontSize: isMobile ? "1.8rem" : "2.5rem",
|
|
675
|
+
fontWeight: "900",
|
|
676
|
+
color: "#333",
|
|
677
|
+
textShadow: isMobile ? "2px 2px 0px #eee" : "3px 3px 0px #eee",
|
|
678
|
+
fontFamily: "'Fredoka One', cursive, sans-serif",
|
|
679
|
+
letterSpacing: "1px",
|
|
680
|
+
overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap",
|
|
681
|
+
maxWidth: isMobile ? "60%" : "auto"
|
|
682
|
+
}}
|
|
683
|
+
>
|
|
684
|
+
{collectionName}!
|
|
685
|
+
</h2>
|
|
686
|
+
|
|
687
|
+
{/* Mobile Filter Toggle (Pets Only) */}
|
|
688
|
+
{collectionName === "Pets" && isMobile && (
|
|
689
|
+
<button
|
|
690
|
+
onClick={() => setShowFiltersMobile(!showFiltersMobile)}
|
|
691
|
+
style={{
|
|
692
|
+
borderRadius: "12px",
|
|
693
|
+
border: "2px solid #333", background: showFiltersMobile ? "#333" : "#fff",
|
|
694
|
+
color: showFiltersMobile ? "#fff" : "#333",
|
|
695
|
+
fontSize: "1rem", display: "flex", alignItems: "center", justifyContent: "center",
|
|
696
|
+
marginRight: "10px",
|
|
697
|
+
padding: "8px 12px",
|
|
698
|
+
fontWeight: "bold",
|
|
699
|
+
boxShadow: "0 2px 0 #ccc",
|
|
700
|
+
}}
|
|
701
|
+
>
|
|
702
|
+
{showFiltersMobile ? "Hide Filters" : "Filters"}
|
|
703
|
+
</button>
|
|
704
|
+
)}
|
|
705
|
+
|
|
706
|
+
{/* Desktop Filter Ribbon (Pets Only) & Search */}
|
|
707
|
+
{!isMobile && (
|
|
708
|
+
<div style={{ display: 'flex', alignItems: 'center', flex: 1, justifyContent: 'flex-end', marginRight: '20px' }}>
|
|
709
|
+
{collectionName === "Pets" && renderFilters()}
|
|
710
|
+
<div style={{ position: "relative", marginLeft: collectionName === "Pets" ? "20px" : "0" }}>
|
|
711
|
+
<input
|
|
712
|
+
type="text"
|
|
713
|
+
placeholder="Search"
|
|
714
|
+
value={searchTerm}
|
|
715
|
+
onChange={(e) => setSearchTerm(e.target.value)}
|
|
716
|
+
style={{
|
|
717
|
+
padding: "10px 20px",
|
|
718
|
+
borderRadius: "50px",
|
|
719
|
+
border: "3px solid #ccc",
|
|
720
|
+
outline: "none",
|
|
721
|
+
fontSize: "1.2rem",
|
|
722
|
+
width: "200px",
|
|
723
|
+
fontWeight: "800",
|
|
724
|
+
backgroundColor: "#f9f9f9",
|
|
725
|
+
color: "#333",
|
|
726
|
+
}}
|
|
727
|
+
/>
|
|
728
|
+
</div>
|
|
729
|
+
</div>
|
|
730
|
+
)}
|
|
731
|
+
|
|
732
|
+
{/* Red Close Button */}
|
|
733
|
+
<button
|
|
734
|
+
onClick={() => navigate("/collections")}
|
|
735
|
+
style={{
|
|
736
|
+
width: isMobile ? "40px" : "48px",
|
|
737
|
+
height: isMobile ? "40px" : "48px",
|
|
738
|
+
borderRadius: "12px",
|
|
739
|
+
backgroundColor: "#ff0055",
|
|
740
|
+
color: "white",
|
|
741
|
+
border: isMobile ? "3px solid #900" : "4px solid #900",
|
|
742
|
+
fontSize: "20px",
|
|
743
|
+
fontWeight: "900",
|
|
744
|
+
display: "flex",
|
|
745
|
+
alignItems: "center",
|
|
746
|
+
justifyContent: "center",
|
|
747
|
+
cursor: "pointer",
|
|
748
|
+
boxShadow: "inset 0 4px 4px rgba(255,255,255,0.4), 0 4px 0 #500",
|
|
749
|
+
flexShrink: 0
|
|
750
|
+
}}
|
|
751
|
+
>
|
|
752
|
+
X
|
|
753
|
+
</button>
|
|
754
|
+
</div>
|
|
755
|
+
|
|
756
|
+
{/* Mobile Search Row */}
|
|
757
|
+
{isMobile && (
|
|
758
|
+
<div style={{ width: "100%" }}>
|
|
759
|
+
<input
|
|
760
|
+
type="text"
|
|
761
|
+
placeholder={`Search ${collectionName}...`}
|
|
762
|
+
value={searchTerm}
|
|
763
|
+
onChange={(e) => setSearchTerm(e.target.value)}
|
|
764
|
+
style={{
|
|
765
|
+
padding: "10px 15px",
|
|
766
|
+
borderRadius: "12px",
|
|
767
|
+
border: "3px solid #ccc",
|
|
768
|
+
outline: "none",
|
|
769
|
+
fontSize: "1rem",
|
|
770
|
+
width: "100%",
|
|
771
|
+
fontWeight: "700",
|
|
772
|
+
backgroundColor: "#f5f5f5",
|
|
773
|
+
color: "#333",
|
|
774
|
+
boxSizing: "border-box"
|
|
775
|
+
}}
|
|
776
|
+
/>
|
|
777
|
+
</div>
|
|
778
|
+
)}
|
|
779
|
+
|
|
780
|
+
|
|
781
|
+
{/* Mobile Filters Dropdown/Area */}
|
|
782
|
+
{collectionName === "Pets" && isMobile && showFiltersMobile && (
|
|
783
|
+
<div style={{ paddingTop: "10px", borderTop: "1px dashed #eee" }}>
|
|
784
|
+
{renderFilters()}
|
|
785
|
+
</div>
|
|
786
|
+
)}
|
|
787
|
+
|
|
788
|
+
</div>
|
|
789
|
+
|
|
790
|
+
{/* Virtualized and AutoSized Content */}
|
|
791
|
+
<div style={{ height: `calc(100% - ${contentPadding})`, width: "100%", flex: 1, marginTop: contentPadding, transition: "margin-top 0.3s ease-in-out, height 0.3s ease-in-out" }}>
|
|
792
|
+
{/* @ts-ignore */}
|
|
793
|
+
<AutoSizer style={{ width: "100%", height: "100%" }} renderProp={({ height, width }: { height: number; width: number }) => {
|
|
794
|
+
const GAP = 10;
|
|
795
|
+
// Adjustment for mobile header spacer
|
|
796
|
+
// If mobile, we are absolute positioning the header. The list needs to have padding TOP to account for it, OR we just let the list flow and use `paddingTop` on the container.
|
|
797
|
+
// However, AutoSizer gives us 'height' of the parent. We should subtract the header height IF it were static, but it's absolute.
|
|
798
|
+
// So we should just use the full height, but ensure the first items aren't hidden behind the header.
|
|
799
|
+
// A cleaner way for "Scroll to Hide" is to have the list occupy 100% height, and simply add a "Header Spacer" as the very first item in the list?
|
|
800
|
+
// OR, simpler: Use `paddingTop` on the container div above, which we are animating.
|
|
801
|
+
// Let's use `paddingTop` on the wrapper div. But wait, AutoSizer measures the `flex: 1` div.
|
|
802
|
+
// If we animate `paddingTop`, the `height` passed to `AutoSizer` will change (because flex child shrinks).
|
|
803
|
+
// That causes Re-renders of the list. That might be jerky.
|
|
804
|
+
// BETTER APPROACH: Keep the list static full screen, and rely on `contentContainerStyle` padding?
|
|
805
|
+
// React-window doesn't support dynamic contentContainerStyle easily without remounting.
|
|
806
|
+
// Let's try the simpler approach first: `paddingTop` on the container. resizing might be performant enough.
|
|
807
|
+
// actually, wait. If we use `paddingTop` on the flex container, the available height for AutoSizer shrinks.
|
|
808
|
+
// This is GOOD. The list gets smaller, but stays at the bottom.
|
|
809
|
+
// When header hides, padding becomes 0, list gets taller.
|
|
810
|
+
// The issue is: scrolling DOWN triggers hide -> list grows -> potential scroll jump?
|
|
811
|
+
// Let's test it.
|
|
812
|
+
|
|
813
|
+
return (
|
|
814
|
+
<>
|
|
815
|
+
{viewMode === 'list' ? (
|
|
816
|
+
/* @ts-ignore */
|
|
817
|
+
<FixedSizeList
|
|
818
|
+
height={height}
|
|
819
|
+
itemCount={finalItems.length}
|
|
820
|
+
itemSize={80} // List view row height
|
|
821
|
+
width={width}
|
|
822
|
+
initialScrollOffset={initialScrollOffset}
|
|
823
|
+
onScroll={handleScroll}
|
|
824
|
+
itemData={{
|
|
825
|
+
items: finalItems,
|
|
826
|
+
navigate,
|
|
827
|
+
collectionName,
|
|
828
|
+
resolveIcon,
|
|
829
|
+
getRarityColor,
|
|
830
|
+
variantFilter,
|
|
831
|
+
shinyFilter
|
|
832
|
+
}}
|
|
833
|
+
>
|
|
834
|
+
{/* @ts-ignore */}
|
|
835
|
+
{ListRowRenderer}
|
|
836
|
+
</FixedSizeList>
|
|
837
|
+
) : (
|
|
838
|
+
/* @ts-ignore */
|
|
839
|
+
/* @ts-ignore */
|
|
840
|
+
(() => {
|
|
841
|
+
const SCROLLBAR_WIDTH = 40;
|
|
842
|
+
const effectiveWidth = width - SCROLLBAR_WIDTH;
|
|
843
|
+
const colCount = Math.floor(effectiveWidth / 150) || 1;
|
|
844
|
+
const colWidth = effectiveWidth / colCount;
|
|
845
|
+
|
|
846
|
+
return (
|
|
847
|
+
<FixedSizeGrid
|
|
848
|
+
columnCount={colCount}
|
|
849
|
+
columnWidth={colWidth}
|
|
850
|
+
height={height}
|
|
851
|
+
rowCount={Math.ceil(finalItems.length / colCount)}
|
|
852
|
+
rowHeight={220}
|
|
853
|
+
width={width}
|
|
854
|
+
initialScrollOffset={initialScrollOffset}
|
|
855
|
+
onScroll={handleScroll}
|
|
856
|
+
style={{ overflowX: "hidden" }}
|
|
857
|
+
itemData={{
|
|
858
|
+
items: finalItems,
|
|
859
|
+
columnCount: colCount,
|
|
860
|
+
navigate,
|
|
861
|
+
collectionName,
|
|
862
|
+
variantFilter,
|
|
863
|
+
shinyFilter,
|
|
864
|
+
resolveIcon,
|
|
865
|
+
GAP
|
|
866
|
+
}}
|
|
867
|
+
>
|
|
868
|
+
{/* @ts-ignore */}
|
|
869
|
+
{GridCellRenderer}
|
|
870
|
+
</FixedSizeGrid>
|
|
871
|
+
);
|
|
872
|
+
})()
|
|
873
|
+
|
|
874
|
+
)}
|
|
875
|
+
</>
|
|
876
|
+
);
|
|
877
|
+
}} />
|
|
878
|
+
</div>
|
|
879
|
+
|
|
880
|
+
</div >
|
|
881
|
+
);
|
|
48
882
|
};
|
|
49
883
|
|
|
50
884
|
export default CollectionConfigIndex;
|