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.
@@ -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 { Grid as FixedSizeGrid } from "react-window";
13
- import { AutoSizer } from "react-virtualized-auto-sizer/dist/react-virtualized-auto-sizer.cjs";
14
- import ItemCard from "./ItemCard";
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
- const GridCellRenderer = ({ columnIndex, rowIndex, style, items, columnCount, navigate, collectionName, variantFilter, shinyFilter, resolveIcon, GAP }: any) => {
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={itemConfig.DisplayName || itemConfig.name || item.configName}
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
- interface ItemData {
158
- items: typeof finalItems;
159
- columnCount?: number;
160
- navigate: any;
161
- collectionName: string | undefined;
162
- variantFilter: string;
163
- shinyFilter: boolean;
164
- resolveIcon: (item: any) => string | null;
165
- GAP: number;
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
- alignItems: "center",
186
- gap: "15px",
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
- <h2
191
- style={{
192
- margin: 0,
193
- fontSize: "2.5rem",
194
- fontWeight: "900",
195
- color: "#333",
196
- textShadow: "3px 3px 0px #eee",
197
- marginRight: "auto",
198
- fontFamily: "'Fredoka One', cursive, sans-serif",
199
- letterSpacing: "1px",
200
- }}
201
- >
202
- {collectionName}!
203
- </h2>
204
-
205
- {/* Filter Ribbon (Pets Only) */}
206
- {collectionName === "Pets" && (
207
- <div style={{ display: "flex", gap: "8px", marginRight: "20px" }}>
208
- {/* Specials: H, T, G */}
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={() => setShinyFilter(!shinyFilter)}
300
- className={shinyFilter ? "sheen-effect" : ""}
690
+ onClick={() => setShowFiltersMobile(!showFiltersMobile)}
301
691
  style={{
302
- width: "48px",
303
- height: "48px",
304
- borderRadius: "50%",
305
- border: "3px solid #333",
306
- backgroundColor: shinyFilter ? "#333" : "#fff",
307
- color: shinyFilter ? "#fff" : "#333",
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
- cursor: "pointer",
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: "calc(100vh - 80px)", width: "100%" }}>
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
- const MIN_COL_WIDTH = 150;
378
-
379
- const columnCount = Math.floor(width / MIN_COL_WIDTH) || 1;
380
- const columnWidth = width / columnCount;
381
- const rowHeight = 220; // Increased to ensure no cutoff
382
- const rowCount = Math.ceil(finalItems.length / columnCount);
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
- // @ts-ignore
386
- <FixedSizeGrid
387
- columnCount={columnCount}
388
- columnWidth={columnWidth}
389
- height={height}
390
- rowCount={rowCount}
391
- rowHeight={rowHeight}
392
- width={width}
393
- cellComponent={GridCellRenderer}
394
- cellProps={{
395
- items: finalItems,
396
- columnCount,
397
- navigate,
398
- collectionName,
399
- variantFilter,
400
- shinyFilter,
401
- resolveIcon,
402
- GAP
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>