ps99-api 2.5.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/example-web/react2/package-lock.json +10 -10
- package/example-web/react2/src/App.tsx +13 -10
- package/example-web/react2/src/components/AutoSizer.tsx +49 -0
- package/example-web/react2/src/components/CharmsComponent.tsx +39 -2
- package/example-web/react2/src/components/CollectionConfigIndex.tsx +707 -236
- package/example-web/react2/src/components/CollectionsIndex.tsx +346 -102
- package/example-web/react2/src/components/CollectionsLayout.tsx +17 -7
- package/example-web/react2/src/components/CurrencyComponent.tsx +3 -1
- package/example-web/react2/src/components/DynamicCollectionConfigData.tsx +10 -1
- package/example-web/react2/src/components/EnchantsComponent.tsx +45 -40
- package/example-web/react2/src/components/Footer.tsx +17 -0
- package/example-web/react2/src/components/Header.tsx +40 -21
- package/example-web/react2/src/components/MasteryComponent.tsx +105 -33
- package/example-web/react2/src/components/PotionsComponent.tsx +96 -18
- package/example-web/react2/src/components/ReactWindowMock.tsx +73 -0
- package/example-web/react2/src/components/UpgradesComponent.tsx +4 -3
- 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/utils/gigantix.ts +40 -0
- package/package.json +1 -1
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// @ts-nocheck
|
|
2
|
-
import React, { useEffect, useState } from "react";
|
|
2
|
+
import React, { useEffect, useState, useRef } from "react";
|
|
3
3
|
import { useParams, Link, useNavigate } from "react-router-dom";
|
|
4
4
|
import Tooltip from "./Tooltip";
|
|
5
5
|
import { PetSimulator99API, Collections, CollectionName } from "ps99-api";
|
|
@@ -9,11 +9,87 @@ import ImageComponent from "./ImageComponent";
|
|
|
9
9
|
|
|
10
10
|
import { useItemResolution } from "../hooks/useItemResolution";
|
|
11
11
|
import { useCollectionData } from "../context/CollectionDataContext";
|
|
12
|
-
import {
|
|
13
|
-
import
|
|
14
|
-
import
|
|
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";
|
|
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
|
+
}
|
|
15
89
|
|
|
16
|
-
|
|
90
|
+
// --- Grid Cell Renderer ---
|
|
91
|
+
const GridCellRenderer = ({ columnIndex, rowIndex, style, data }: any) => {
|
|
92
|
+
const { items, columnCount, navigate, collectionName, variantFilter, shinyFilter, resolveIcon, GAP } = data;
|
|
17
93
|
const index = rowIndex * columnCount + columnIndex;
|
|
18
94
|
if (index >= items.length) return null;
|
|
19
95
|
const item = items[index];
|
|
@@ -21,6 +97,13 @@ const GridCellRenderer = ({ columnIndex, rowIndex, style, items, columnCount, na
|
|
|
21
97
|
const icon = resolveIcon(itemConfig);
|
|
22
98
|
const itemDataWithIcon = { ...itemConfig, icon };
|
|
23
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
|
+
|
|
24
107
|
return (
|
|
25
108
|
<div style={style}>
|
|
26
109
|
<div style={{
|
|
@@ -29,7 +112,6 @@ const GridCellRenderer = ({ columnIndex, rowIndex, style, items, columnCount, na
|
|
|
29
112
|
left: GAP / 2,
|
|
30
113
|
right: GAP / 2,
|
|
31
114
|
bottom: GAP / 2,
|
|
32
|
-
// Using explicit specific size if needed, but absolute positioning with insets works well for gaps
|
|
33
115
|
}}>
|
|
34
116
|
<div
|
|
35
117
|
onClick={() => navigate(`/collections/${collectionName}/${item.configName}`)}
|
|
@@ -38,7 +120,7 @@ const GridCellRenderer = ({ columnIndex, rowIndex, style, items, columnCount, na
|
|
|
38
120
|
<ItemCard
|
|
39
121
|
id={item.configName}
|
|
40
122
|
amount={""}
|
|
41
|
-
label={
|
|
123
|
+
label={label}
|
|
42
124
|
itemData={itemDataWithIcon}
|
|
43
125
|
rarityColor={itemConfig.rarity?.Color || (itemConfig.Rarity?.Color)}
|
|
44
126
|
variant={collectionName === "Pets" ? (variantFilter as any) : undefined}
|
|
@@ -50,15 +132,240 @@ const GridCellRenderer = ({ columnIndex, rowIndex, style, items, columnCount, na
|
|
|
50
132
|
);
|
|
51
133
|
};
|
|
52
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
|
+
};
|
|
53
354
|
|
|
54
355
|
|
|
55
356
|
interface CollectionConfigIndexProps { }
|
|
56
357
|
|
|
57
358
|
const CollectionConfigIndex: React.FC<CollectionConfigIndexProps> = () => {
|
|
58
|
-
const { collectionName } = useParams<{ collectionName: string }>();
|
|
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
|
+
|
|
59
366
|
const navigate = useNavigate();
|
|
60
367
|
// Call the hook at the top level so we can use it
|
|
61
|
-
const { resolveIcon } = useItemResolution();
|
|
368
|
+
const { resolveIcon, getRarityColor } = useItemResolution();
|
|
62
369
|
|
|
63
370
|
const { data, fetchCollection, isLoading } = useCollectionData();
|
|
64
371
|
|
|
@@ -72,6 +379,10 @@ const CollectionConfigIndex: React.FC<CollectionConfigIndexProps> = () => {
|
|
|
72
379
|
const [specialFilter, setSpecialFilter] = useState<"H" | "T" | "G" | null>(null);
|
|
73
380
|
const [variantFilter, setVariantFilter] = useState<"Normal" | "Golden" | "Rainbow">("Normal");
|
|
74
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');
|
|
75
386
|
|
|
76
387
|
// Responsive check
|
|
77
388
|
useEffect(() => {
|
|
@@ -84,31 +395,24 @@ const CollectionConfigIndex: React.FC<CollectionConfigIndexProps> = () => {
|
|
|
84
395
|
|
|
85
396
|
const isMobile = windowWidth < 768;
|
|
86
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
|
|
404
|
+
useEffect(() => {
|
|
405
|
+
return () => {
|
|
406
|
+
saveScrollPosition(scrollKey, scrollRef.current);
|
|
407
|
+
};
|
|
408
|
+
}, [saveScrollPosition, scrollKey]);
|
|
409
|
+
|
|
87
410
|
useEffect(() => {
|
|
88
411
|
if (collectionName) {
|
|
89
412
|
fetchCollection(collectionName as CollectionName);
|
|
90
413
|
}
|
|
91
414
|
}, [collectionName, fetchCollection]);
|
|
92
415
|
|
|
93
|
-
// Loading State
|
|
94
|
-
if (loading) {
|
|
95
|
-
return (
|
|
96
|
-
<div
|
|
97
|
-
style={{
|
|
98
|
-
display: "flex",
|
|
99
|
-
justifyContent: "center",
|
|
100
|
-
alignItems: "center",
|
|
101
|
-
height: "50vh",
|
|
102
|
-
fontSize: "1.5rem",
|
|
103
|
-
fontWeight: "bold",
|
|
104
|
-
color: "#666",
|
|
105
|
-
}}
|
|
106
|
-
>
|
|
107
|
-
Loading {collectionName}...
|
|
108
|
-
</div>
|
|
109
|
-
);
|
|
110
|
-
}
|
|
111
|
-
|
|
112
416
|
// Process items for rendering
|
|
113
417
|
const processedItems = items.map((item) => {
|
|
114
418
|
const configData: any = (item as any).configData || item;
|
|
@@ -154,17 +458,184 @@ const CollectionConfigIndex: React.FC<CollectionConfigIndexProps> = () => {
|
|
|
154
458
|
return true;
|
|
155
459
|
});
|
|
156
460
|
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
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');
|
|
476
|
+
} else {
|
|
477
|
+
setViewMode('grid');
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
}, [isMobile, collectionName, finalItems.length]); // Re-evaluate on collection change or mobile resize, or items load
|
|
481
|
+
|
|
482
|
+
|
|
483
|
+
// Scroll Direction // Header Logic
|
|
484
|
+
const { showHeader, handleScroll, scrollRef, headerRef, headerHeight, contentPadding } = useCollapsibleHeader({ deps: [loading] });
|
|
485
|
+
|
|
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
|
+
);
|
|
166
503
|
}
|
|
167
504
|
|
|
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>
|
|
541
|
+
))}
|
|
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>
|
|
636
|
+
</div>
|
|
637
|
+
);
|
|
638
|
+
|
|
168
639
|
return (
|
|
169
640
|
<div
|
|
170
641
|
style={{
|
|
@@ -174,234 +645,234 @@ const CollectionConfigIndex: React.FC<CollectionConfigIndexProps> = () => {
|
|
|
174
645
|
overflow: "hidden",
|
|
175
646
|
backgroundColor: "#ffffff",
|
|
176
647
|
height: "100%", // ensure it fills parent
|
|
648
|
+
position: 'relative' // Context for absolute header
|
|
177
649
|
}}
|
|
178
650
|
>
|
|
179
651
|
{/* Header Bar inside Window */}
|
|
180
652
|
<div
|
|
653
|
+
ref={headerRef}
|
|
181
654
|
style={{
|
|
182
|
-
padding: "15px 20px",
|
|
655
|
+
padding: isMobile ? "10px 15px" : "15px 20px",
|
|
183
656
|
borderBottom: "4px solid #333",
|
|
184
657
|
display: "flex",
|
|
185
|
-
|
|
186
|
-
gap: "
|
|
658
|
+
flexDirection: "column",
|
|
659
|
+
gap: isMobile ? "10px" : "0px",
|
|
187
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%)",
|
|
188
668
|
}}
|
|
189
669
|
>
|
|
190
|
-
<
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
{['H', 'T', 'G'].map((symbol) => (
|
|
210
|
-
<button
|
|
211
|
-
key={symbol}
|
|
212
|
-
onClick={() => setSpecialFilter(specialFilter === symbol ? null : symbol as any)}
|
|
213
|
-
style={{
|
|
214
|
-
width: "36px",
|
|
215
|
-
height: "36px",
|
|
216
|
-
borderRadius: "50%",
|
|
217
|
-
border: "3px solid #333",
|
|
218
|
-
backgroundColor: specialFilter === symbol ? "#333" : "#fff",
|
|
219
|
-
color: specialFilter === symbol ? "#fff" : "#333",
|
|
220
|
-
fontSize: "1.2rem",
|
|
221
|
-
fontWeight: "bold",
|
|
222
|
-
cursor: "pointer",
|
|
223
|
-
display: "flex",
|
|
224
|
-
alignItems: "center",
|
|
225
|
-
justifyContent: "center",
|
|
226
|
-
boxShadow: "0 3px 0 #ccc",
|
|
227
|
-
}}
|
|
228
|
-
>
|
|
229
|
-
{symbol}
|
|
230
|
-
</button>
|
|
231
|
-
))}
|
|
232
|
-
|
|
233
|
-
{/* Variants: Normal, Golden, Rainbow */}
|
|
234
|
-
{[
|
|
235
|
-
{ symbol: '🐾', mode: 'Normal', color: '#fff' },
|
|
236
|
-
{ symbol: '★', mode: 'Golden', color: '#FFD700' },
|
|
237
|
-
{ symbol: '🌈', mode: 'Rainbow', color: 'inherit' }
|
|
238
|
-
].map(({ symbol, mode, color: iconColor }) => {
|
|
239
|
-
const isActive = variantFilter === mode;
|
|
240
|
-
const isRainbow = mode === 'Rainbow';
|
|
241
|
-
const isGold = mode === 'Golden';
|
|
242
|
-
|
|
243
|
-
return (
|
|
244
|
-
<button
|
|
245
|
-
key={mode}
|
|
246
|
-
onClick={() => setVariantFilter(mode as any)}
|
|
247
|
-
className={(isActive && isRainbow) ? "sheen-effect" : ""}
|
|
248
|
-
style={{
|
|
249
|
-
width: "48px",
|
|
250
|
-
height: "48px",
|
|
251
|
-
borderRadius: "50%",
|
|
252
|
-
border: "3px solid #333",
|
|
253
|
-
backgroundColor: isActive
|
|
254
|
-
? (mode === 'Normal' ? "#e0e0e0" : "#333")
|
|
255
|
-
: "#fff",
|
|
256
|
-
color: isActive
|
|
257
|
-
? (mode === 'Normal' ? "#333" : "#fff")
|
|
258
|
-
: "#333",
|
|
259
|
-
fontSize: "1.2rem",
|
|
260
|
-
fontWeight: "bold",
|
|
261
|
-
cursor: "pointer",
|
|
262
|
-
display: "flex",
|
|
263
|
-
alignItems: "center",
|
|
264
|
-
justifyContent: "center",
|
|
265
|
-
boxShadow: "0 3px 0 #ccc",
|
|
266
|
-
filter: "none",
|
|
267
|
-
position: 'relative',
|
|
268
|
-
overflow: 'hidden',
|
|
269
|
-
padding: 0
|
|
270
|
-
}}
|
|
271
|
-
title={mode}
|
|
272
|
-
>
|
|
273
|
-
{isGold ? (
|
|
274
|
-
<img
|
|
275
|
-
src="./assets/gold_variant_icon.png"
|
|
276
|
-
alt="Gold"
|
|
277
|
-
style={{
|
|
278
|
-
width: '70%',
|
|
279
|
-
height: '70%',
|
|
280
|
-
objectFit: 'contain',
|
|
281
|
-
filter: isActive ? 'none' : 'grayscale(1) opacity(0.5)'
|
|
282
|
-
}}
|
|
283
|
-
/>
|
|
284
|
-
) : (
|
|
285
|
-
<span style={{
|
|
286
|
-
filter: mode === 'Golden' && !isActive ? "grayscale(1)" : "none",
|
|
287
|
-
color: (isActive && mode !== 'Rainbow' && mode !== 'Normal') ? iconColor : 'inherit'
|
|
288
|
-
}}>
|
|
289
|
-
{symbol}
|
|
290
|
-
</span>
|
|
291
|
-
)}
|
|
292
|
-
</button>
|
|
293
|
-
)
|
|
294
|
-
})}
|
|
295
|
-
|
|
296
|
-
{/* Shiny Toggle */}
|
|
297
|
-
{/* Shiny Toggle */}
|
|
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 && (
|
|
298
689
|
<button
|
|
299
|
-
onClick={() =>
|
|
300
|
-
className={shinyFilter ? "sheen-effect" : ""}
|
|
690
|
+
onClick={() => setShowFiltersMobile(!showFiltersMobile)}
|
|
301
691
|
style={{
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
fontSize: "1.2rem",
|
|
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",
|
|
309
698
|
fontWeight: "bold",
|
|
310
|
-
|
|
311
|
-
display: "flex",
|
|
312
|
-
alignItems: "center",
|
|
313
|
-
justifyContent: "center",
|
|
314
|
-
boxShadow: "0 3px 0 #ccc",
|
|
315
|
-
position: 'relative',
|
|
316
|
-
overflow: 'hidden'
|
|
699
|
+
boxShadow: "0 2px 0 #ccc",
|
|
317
700
|
}}
|
|
318
701
|
>
|
|
319
|
-
|
|
702
|
+
{showFiltersMobile ? "Hide Filters" : "Filters"}
|
|
320
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
|
+
/>
|
|
321
777
|
</div>
|
|
322
778
|
)}
|
|
323
779
|
|
|
324
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
|
+
)}
|
|
325
787
|
|
|
326
|
-
{/* Search Bar */}
|
|
327
|
-
<div style={{ position: "relative" }}>
|
|
328
|
-
<input
|
|
329
|
-
type="text"
|
|
330
|
-
placeholder="Search"
|
|
331
|
-
value={searchTerm}
|
|
332
|
-
onChange={(e) => setSearchTerm(e.target.value)}
|
|
333
|
-
style={{
|
|
334
|
-
padding: "10px 20px",
|
|
335
|
-
borderRadius: "50px",
|
|
336
|
-
border: "3px solid #ccc",
|
|
337
|
-
outline: "none",
|
|
338
|
-
fontSize: "1.2rem",
|
|
339
|
-
width: "200px",
|
|
340
|
-
fontWeight: "800",
|
|
341
|
-
backgroundColor: "#fff",
|
|
342
|
-
color: "#ccc",
|
|
343
|
-
textAlign: "right"
|
|
344
|
-
}}
|
|
345
|
-
/>
|
|
346
|
-
</div>
|
|
347
|
-
|
|
348
|
-
{/* Red Close Button */}
|
|
349
|
-
<button
|
|
350
|
-
onClick={() => navigate("/collections")}
|
|
351
|
-
style={{
|
|
352
|
-
width: "48px",
|
|
353
|
-
height: "48px",
|
|
354
|
-
borderRadius: "12px",
|
|
355
|
-
backgroundColor: "#ff0055", // Hot pink/red
|
|
356
|
-
color: "white",
|
|
357
|
-
border: "4px solid #900", // Dark red border
|
|
358
|
-
fontSize: "24px",
|
|
359
|
-
fontWeight: "900",
|
|
360
|
-
display: "flex",
|
|
361
|
-
alignItems: "center",
|
|
362
|
-
justifyContent: "center",
|
|
363
|
-
cursor: "pointer",
|
|
364
|
-
boxShadow: "inset 0 4px 4px rgba(255,255,255,0.4), 0 4px 0 #500",
|
|
365
|
-
}}
|
|
366
|
-
>
|
|
367
|
-
X
|
|
368
|
-
</button>
|
|
369
788
|
</div>
|
|
370
789
|
|
|
371
790
|
{/* Virtualized and AutoSized Content */}
|
|
372
|
-
<div style={{ height:
|
|
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" }}>
|
|
373
792
|
{/* @ts-ignore */}
|
|
374
793
|
<AutoSizer style={{ width: "100%", height: "100%" }} renderProp={({ height, width }: { height: number; width: number }) => {
|
|
375
|
-
if (!height || !width) return <div style={{ color: 'red' }}>AutoSizer returned 0 dimensions</div>;
|
|
376
794
|
const GAP = 10;
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
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.
|
|
383
812
|
|
|
384
813
|
return (
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
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
|
+
</>
|
|
405
876
|
);
|
|
406
877
|
}} />
|
|
407
878
|
</div>
|