ps99-api 2.3.3 → 2.5.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.
Files changed (99) hide show
  1. package/.github/workflows/release-on-main.yml +1 -2
  2. package/.idea/node-ps99-api.iml +1 -0
  3. package/.idea/runConfigurations/test_changing.xml +1 -1
  4. package/README.md +1 -1
  5. package/debug_currency.json +57 -0
  6. package/debug_goals.json +271 -0
  7. package/dist/ps99-api.d.ts +2 -0
  8. package/dist/ps99-api.js +4 -1
  9. package/dist/ps99-api.js.map +1 -1
  10. package/dist/request-client/axios.js +6 -1
  11. package/dist/request-client/axios.js.map +1 -1
  12. package/dist/responses/collection/achievement.d.ts +2 -0
  13. package/dist/responses/collection/guild-battle.d.ts +2 -4
  14. package/dist/responses/collection/index.d.ts +1 -0
  15. package/dist/responses/collection/index.js +15 -0
  16. package/dist/responses/collection/index.js.map +1 -1
  17. package/dist/responses/collection/rank.d.ts +1 -0
  18. package/dist/responses/collection/rarity.d.ts +1 -0
  19. package/dist/responses/collection/seed.d.ts +1 -0
  20. package/dist/responses/exists.d.ts +2 -0
  21. package/example-web/react/package-lock.json +1504 -1470
  22. package/example-web/react2/package-lock.json +3082 -2759
  23. package/example-web/react2/package.json +6 -1
  24. package/example-web/react2/public/assets/gold_variant_icon.png +0 -0
  25. package/example-web/react2/public/assets/hot_cocoa_egg.png +0 -0
  26. package/example-web/react2/public/index.html +34 -31
  27. package/example-web/react2/src/App.tsx +6 -9
  28. package/example-web/react2/src/assets/guild_placeholder.png +0 -0
  29. package/example-web/react2/src/components/AchievementsComponent.tsx +78 -30
  30. package/example-web/react2/src/components/BoostsComponent.tsx +18 -14
  31. package/example-web/react2/src/components/BoothsComponent.tsx +24 -22
  32. package/example-web/react2/src/components/BoxesComponent.tsx +46 -21
  33. package/example-web/react2/src/components/BuffsComponent.tsx +83 -13
  34. package/example-web/react2/src/components/CharmsComponent.tsx +47 -29
  35. package/example-web/react2/src/components/CollectionConfigIndex.tsx +398 -35
  36. package/example-web/react2/src/components/CollectionsIndex.tsx +132 -23
  37. package/example-web/react2/src/components/CollectionsLayout.tsx +50 -0
  38. package/example-web/react2/src/components/CurrencyComponent.tsx +59 -50
  39. package/example-web/react2/src/components/DynamicCollectionConfigData.tsx +178 -11
  40. package/example-web/react2/src/components/EggsComponent.tsx +77 -44
  41. package/example-web/react2/src/components/EnchantsComponent.tsx +84 -34
  42. package/example-web/react2/src/components/FishingRodsComponent.tsx +38 -31
  43. package/example-web/react2/src/components/Footer.tsx +75 -18
  44. package/example-web/react2/src/components/FruitsComponent.tsx +41 -25
  45. package/example-web/react2/src/components/GenericFetchComponent.tsx +40 -22
  46. package/example-web/react2/src/components/GuildBattlesComponent.tsx +93 -65
  47. package/example-web/react2/src/components/Header.tsx +5 -37
  48. package/example-web/react2/src/components/HomePage.tsx +16 -16
  49. package/example-web/react2/src/components/HoverboardsComponent.tsx +25 -37
  50. package/example-web/react2/src/components/ImageComponent.tsx +255 -45
  51. package/example-web/react2/src/components/ItemCard.tsx +240 -0
  52. package/example-web/react2/src/components/LootboxesComponent.tsx +24 -16
  53. package/example-web/react2/src/components/MasteryComponent.tsx +93 -37
  54. package/example-web/react2/src/components/MerchantsComponent.tsx +46 -28
  55. package/example-web/react2/src/components/MiscItemsComponent.tsx +28 -26
  56. package/example-web/react2/src/components/PetsComponent.tsx +115 -46
  57. package/example-web/react2/src/components/PotionsComponent.tsx +53 -36
  58. package/example-web/react2/src/components/RandomEventsComponent.tsx +39 -35
  59. package/example-web/react2/src/components/RanksComponent.tsx +187 -71
  60. package/example-web/react2/src/components/RarityComponent.tsx +124 -13
  61. package/example-web/react2/src/components/RebirthsComponent.tsx +37 -26
  62. package/example-web/react2/src/components/SecretRoomsComponent.tsx +7 -13
  63. package/example-web/react2/src/components/SeedsComponent.tsx +45 -32
  64. package/example-web/react2/src/components/ShovelsComponent.tsx +23 -18
  65. package/example-web/react2/src/components/Sidebar.tsx +105 -0
  66. package/example-web/react2/src/components/SprinklersComponent.tsx +27 -19
  67. package/example-web/react2/src/components/Tooltip.tsx +36 -0
  68. package/example-web/react2/src/components/UltimatesComponent.tsx +29 -24
  69. package/example-web/react2/src/components/UpgradesComponent.tsx +99 -55
  70. package/example-web/react2/src/components/WateringCansComponent.tsx +23 -21
  71. package/example-web/react2/src/components/WorldsComponent.tsx +27 -24
  72. package/example-web/react2/src/components/XPPotionsComponent.tsx +29 -19
  73. package/example-web/react2/src/components/ZoneFlagsComponent.tsx +27 -23
  74. package/example-web/react2/src/components/ZonesComponent.tsx +56 -73
  75. package/example-web/react2/src/constants/collectionIcons.ts +29 -0
  76. package/example-web/react2/src/context/CollectionDataContext.tsx +62 -0
  77. package/example-web/react2/src/hooks/useExpandableList.ts +38 -0
  78. package/example-web/react2/src/hooks/useItemResolution.ts +351 -0
  79. package/example-web/react2/src/index.css +257 -0
  80. package/example-web/react2/src/index.tsx +2 -1
  81. package/example-web/react2/temp_model.rbxm +0 -0
  82. package/example-web/react2/webpack.config.js +103 -47
  83. package/package.json +11 -11
  84. package/ranks.json +1 -0
  85. package/repro_collection_fetch.ts +33 -0
  86. package/repro_image_fetch.ts +50 -0
  87. package/src/__tests__/__snapshots__/ps99-api-changes.ts.snap +34841 -10439
  88. package/src/__tests__/__snapshots__/ps99-api-live.ts.snap +160667 -67217
  89. package/src/ps99-api.ts +9 -5
  90. package/src/request-client/axios.ts +6 -2
  91. package/src/responses/collection/achievement.ts +2 -0
  92. package/src/responses/collection/guild-battle.ts +2 -4
  93. package/src/responses/collection/index.ts +1 -0
  94. package/src/responses/collection/rank.ts +1 -0
  95. package/src/responses/collection/rarity.ts +1 -0
  96. package/src/responses/collection/seed.ts +1 -0
  97. package/src/responses/exists.ts +2 -0
  98. package/tsconfig.json +1 -1
  99. package/example-web/react2/public/service-worker.js +0 -63
@@ -1,46 +1,34 @@
1
1
  import React from "react";
2
2
  import { CollectionConfigData } from "ps99-api";
3
- import { GenericFetchComponent } from "./GenericFetchComponent";
4
- import ImageComponent from "./ImageComponent";
3
+ import ItemCard from "./ItemCard";
4
+ import { useItemResolution } from "../hooks/useItemResolution";
5
5
 
6
6
  const HoverboardsComponent: React.FC<{
7
- configData?: CollectionConfigData<"Hoverboards">;
7
+ configData: CollectionConfigData<"Hoverboards">;
8
8
  }> = ({ configData }) => {
9
+ const { getRarityColor } = useItemResolution();
10
+ const rarityColor = configData.Rarity ? getRarityColor(configData.Rarity) : null;
11
+
9
12
  return (
10
- <GenericFetchComponent<CollectionConfigData<"Hoverboards">>
11
- collectionName="Hoverboards"
12
- configData={configData}
13
- render={(data) => (
14
- <div>
15
- <h2>{data.DisplayName}</h2>
16
- <ImageComponent src={data.Icon} alt={data.DisplayName} />
17
- <p>Description: {data.Desc}</p>
18
- <p>Rarity: {data.Rarity.DisplayName}</p>
19
- <p>Rarity Number: {data.Rarity.RarityNumber}</p>
20
- {data.Tradable && <p>Tradable: Yes</p>}
21
- {data.CanBeShiny && <p>Can Be Shiny: Yes</p>}
22
- {data.HoverHeight && <p>Hover Height: {data.HoverHeight}</p>}
23
- {data.RotationLimit && <p>Rotation Limit: {data.RotationLimit}</p>}
24
- {data.ProductId && <p>Product ID: {data.ProductId}</p>}
25
- {data.Animation && <p>Animation: {data.Animation}</p>}
26
- {data.BobRate && <p>Bob Rate: {data.BobRate}</p>}
27
- {data.PitchScale && <p>Pitch Scale: {data.PitchScale}</p>}
28
- {data.MaxRoll && <p>Max Roll: {data.MaxRoll}</p>}
29
- {data.DefaultJumpSpeedBoost && (
30
- <p>Default Jump Speed Boost: {data.DefaultJumpSpeedBoost}</p>
31
- )}
32
- {data.IdleVolumeSpeedScale && (
33
- <p>Idle Volume Speed Scale: {data.IdleVolumeSpeedScale}</p>
34
- )}
35
- {data.IdlePitchScale && (
36
- <p>Idle Pitch Scale: {data.IdlePitchScale}</p>
37
- )}
38
- {data.BlockcastScale && <p>Blockcast Scale: {data.BlockcastScale}</p>}
39
- {data.SkateMode && <p>Skate Mode: Yes</p>}
40
- {data.IdleVolume && <p>Idle Volume: {data.IdleVolume}</p>}
41
- </div>
42
- )}
43
- />
13
+ <div style={{ width: '100%', height: '100%', boxSizing: 'border-box' }}>
14
+ <ItemCard
15
+ id={configData.DisplayName}
16
+ amount={1}
17
+ label={configData.DisplayName}
18
+ itemData={{
19
+ icon: configData.Icon,
20
+ rarity: configData.Rarity,
21
+ name: configData.DisplayName
22
+ }}
23
+ rarityColor={rarityColor}
24
+ />
25
+ <div style={{ marginTop: '10px', fontSize: '0.9em', color: '#666', textAlign: 'center' }}>
26
+ <p>{configData.Desc}</p>
27
+ {configData.ProductId && <p>ID: {configData.ProductId}</p>}
28
+ {configData.HoverHeight && <p>Height: {configData.HoverHeight}</p>}
29
+ <p>Speed: {configData.DefaultJumpSpeedBoost || 'Normal'}</p>
30
+ </div>
31
+ </div>
44
32
  );
45
33
  };
46
34
 
@@ -1,62 +1,272 @@
1
- import React, { useEffect, useState } from "react";
1
+ import React, { useEffect, useState, useRef } from "react";
2
2
  import { PetSimulator99API } from "ps99-api";
3
+ import { get, set, del } from "idb-keyval";
3
4
 
4
5
  interface ImageProps {
5
- src: string;
6
- alt: string;
6
+ src: string;
7
+ alt: string;
8
+ style?: React.CSSProperties;
7
9
  }
8
10
 
11
+ // Global Queue State
9
12
  const MAX_CONCURRENT_REQUESTS = 5;
10
- const requestQueue: Array<() => void> = [];
13
+ const MIN_CONCURRENT_REQUESTS = 1;
14
+ let currentConcurrencyLimit = MAX_CONCURRENT_REQUESTS;
15
+
16
+ interface QueueItem {
17
+ src: string;
18
+ resolve: (blob: Blob) => void;
19
+ reject: (err: any) => void;
20
+ retries: number;
21
+ }
22
+
23
+ const requestQueue: QueueItem[] = [];
11
24
  let activeRequests = 0;
25
+ let isPaused = false;
26
+ let pauseTimeout: NodeJS.Timeout | null = null;
27
+ let backoffDelay = 2000; // Start at 2s
28
+ let consecutiveErrors = 0;
29
+
30
+ // Map to dedupe requests: src -> Promise<Blob>
31
+ const pendingRequests = new Map<string, Promise<Blob>>();
12
32
 
13
33
  const processQueue = () => {
14
- if (activeRequests < MAX_CONCURRENT_REQUESTS && requestQueue.length > 0) {
15
- const nextRequest = requestQueue.shift();
16
- if (nextRequest) {
17
- activeRequests++;
18
- nextRequest();
34
+ if (isPaused || activeRequests >= currentConcurrencyLimit || requestQueue.length === 0) {
35
+ return;
19
36
  }
20
- }
37
+
38
+ const request = requestQueue.shift();
39
+ if (!request) return;
40
+
41
+ activeRequests++;
42
+ const { src, resolve, reject, retries } = request;
43
+
44
+ // Use empty string as baseUrl to force relative paths (which hit the webpack proxy)
45
+ // This is the "shim" layer making it seamless for the component
46
+ const api = new PetSimulator99API({ baseUrl: "" });
47
+
48
+ api.getImage(src)
49
+ .then((data: any) => {
50
+ const blob = new Blob([data], { type: "image/png" });
51
+ resolve(blob);
52
+ set(`img-cache-${src}`, blob).catch(err => console.error("Cache set failed", err));
53
+
54
+ // Success: Reset backoff and slowly ramp up concurrency
55
+ consecutiveErrors = 0;
56
+ backoffDelay = 2000;
57
+ if (currentConcurrencyLimit < MAX_CONCURRENT_REQUESTS) {
58
+ currentConcurrencyLimit++;
59
+ }
60
+ })
61
+ .catch((error: any) => {
62
+ console.error(`[ImageComponent] Failed ${src}`, error);
63
+
64
+ // Check for 429 or other rate limit errors
65
+ const isRateLimit = error?.response?.status === 429 || error?.status === 429;
66
+
67
+ if (isRateLimit) {
68
+ consecutiveErrors++;
69
+ console.warn(`Rate limit hit (429). Backoff: ${backoffDelay}ms. Concurrency: ${currentConcurrencyLimit}`);
70
+
71
+ // Throttle concurrency
72
+ currentConcurrencyLimit = MIN_CONCURRENT_REQUESTS;
73
+
74
+ // Backoff logic
75
+ handleRateLimit();
76
+
77
+ // Retry if under limit
78
+ if (retries < 3) {
79
+ requestQueue.unshift({ ...request, retries: retries + 1 });
80
+ return;
81
+ }
82
+ }
83
+ // If not rate limit, or max retries exceeded, reject
84
+ reject(error);
85
+ })
86
+ .finally(() => {
87
+ activeRequests--;
88
+ processQueue();
89
+ });
21
90
  };
22
91
 
23
- const ImageComponent: React.FC<ImageProps> = ({ src, alt }) => {
24
- const [imageUrl, setImageUrl] = useState<string | null>(null);
92
+ const handleRateLimit = () => {
93
+ if (isPaused) return;
94
+ isPaused = true;
25
95
 
26
- useEffect(() => {
27
- const fetchImage = async () => {
28
- const api = new PetSimulator99API();
29
- try {
30
- const imageBlob = await api.getImage(src);
31
- const url = URL.createObjectURL(
32
- new Blob([imageBlob], { type: "image/png" }),
33
- );
34
- setImageUrl(url);
35
- } catch (error) {
36
- console.error("Error fetching image:", error);
37
- } finally {
38
- activeRequests--;
96
+ if (pauseTimeout) clearTimeout(pauseTimeout);
97
+
98
+ pauseTimeout = setTimeout(() => {
99
+ isPaused = false;
100
+ // Increase backoff for next time (Exponential)
101
+ backoffDelay = Math.min(backoffDelay * 2, 60000); // Cap at 60s
39
102
  processQueue();
40
- }
41
- };
42
-
43
- const requestImage = () => {
44
- fetchImage();
45
- };
46
-
47
- requestQueue.push(requestImage);
48
- processQueue();
49
-
50
- return () => {
51
- if (imageUrl) {
52
- URL.revokeObjectURL(imageUrl);
53
- }
54
- };
55
- }, [src]);
56
-
57
- return (
58
- <div>{imageUrl ? <img src={imageUrl} alt={alt} /> : <p>Loading...</p>}</div>
59
- );
103
+ }, backoffDelay);
104
+ };
105
+
106
+
107
+ // Helper to fetch blob with deduping
108
+ const fetchImageBlob = (src: string): Promise<Blob> => {
109
+ if (pendingRequests.has(src)) {
110
+ return pendingRequests.get(src)!;
111
+ }
112
+
113
+ const promise = new Promise<Blob>((resolve, reject) => {
114
+ requestQueue.push({ src, resolve, reject, retries: 0 });
115
+ processQueue();
116
+ }).finally(() => {
117
+ pendingRequests.delete(src);
118
+ });
119
+
120
+ pendingRequests.set(src, promise);
121
+ return promise;
122
+ };
123
+
124
+ const ImageComponent: React.FC<ImageProps> = ({ src, alt, style }) => {
125
+ // console.log(`[ImageComponent] Mounting for ${src}`); // Commented out to reduce noise, enable if needed
126
+ const [imageUrl, setImageUrl] = useState<string | null>(null);
127
+ const [isLoading, setIsLoading] = useState<boolean>(true);
128
+ const [hasError, setHasError] = useState<boolean>(false);
129
+ const mountedRef = useRef(true);
130
+
131
+ useEffect(() => {
132
+ mountedRef.current = true;
133
+ // Reset state on src change
134
+ setImageUrl(null);
135
+ setIsLoading(true);
136
+ setHasError(false);
137
+
138
+ // Check for local assets or full URLs (non-api)
139
+ if (src.startsWith("/") || src.startsWith("http")) {
140
+ setImageUrl(src);
141
+ setIsLoading(false);
142
+ return;
143
+ }
144
+
145
+ const load = async () => {
146
+ try {
147
+ // 1. Try Cache
148
+ try {
149
+ const cached = await get<Blob>(`img-cache-${src}`);
150
+ if (cached && cached instanceof Blob) {
151
+ if (mountedRef.current) {
152
+ const url = URL.createObjectURL(cached);
153
+ setImageUrl(url);
154
+ setIsLoading(false);
155
+ return;
156
+ }
157
+ } else if (cached) {
158
+ await del(`img-cache-${src}`);
159
+ }
160
+ } catch (idbErr) {
161
+ // console.error(`[ImageComponent] IDB Error for ${src}:`, idbErr);
162
+ }
163
+
164
+ if (!mountedRef.current) return;
165
+
166
+ // 2. Fetch Network
167
+ const blob = await fetchImageBlob(src);
168
+ if (mountedRef.current) {
169
+ if (blob instanceof Blob) {
170
+ const url = URL.createObjectURL(blob);
171
+ setImageUrl(url);
172
+ setIsLoading(false);
173
+ } else {
174
+ console.error("Fetched data is not a Blob:", blob);
175
+ setHasError(true);
176
+ setIsLoading(false);
177
+ }
178
+ }
179
+ } catch (err: any) {
180
+ if (mountedRef.current) {
181
+ // console.error(`[ImageComponent] Load failed for ${src}:`, err);
182
+ setHasError(true);
183
+ setIsLoading(false);
184
+ }
185
+ }
186
+ };
187
+
188
+ load();
189
+
190
+ return () => {
191
+ mountedRef.current = false;
192
+ };
193
+ }, [src]);
194
+
195
+ if (hasError) {
196
+ return (
197
+ <div style={{
198
+ width: '100%',
199
+ height: '100%',
200
+ display: 'flex',
201
+ alignItems: 'center',
202
+ justifyContent: 'center',
203
+ backgroundColor: 'rgba(0,0,0,0.05)',
204
+ borderRadius: '8px',
205
+ color: '#aaa',
206
+ fontSize: '1.5rem',
207
+ fontWeight: 'bold',
208
+ ...style // Preserve layout styles
209
+ }}>
210
+ ?
211
+ </div>
212
+ );
213
+ }
214
+
215
+ if (imageUrl) {
216
+ return (
217
+ <div style={{
218
+ display: 'flex',
219
+ justifyContent: 'center',
220
+ alignItems: 'center',
221
+ width: '100%',
222
+ height: '100%',
223
+ position: 'relative',
224
+ animation: "fadeIn 0.3s ease-in-out", // Smooth fade-in
225
+ }}>
226
+ <style>{`
227
+ @keyframes fadeIn {
228
+ from { opacity: 0; transform: scale(0.95); }
229
+ to { opacity: 1; transform: scale(1); }
230
+ }
231
+ `}</style>
232
+ <img
233
+ src={imageUrl}
234
+ alt={alt}
235
+ style={{
236
+ maxWidth: "100%",
237
+ maxHeight: "100%",
238
+ objectFit: "contain",
239
+ borderRadius: "8px",
240
+ ...style
241
+ }}
242
+ onError={() => {
243
+ setImageUrl(null);
244
+ setHasError(true);
245
+ }}
246
+ />
247
+ </div>
248
+ );
249
+ }
250
+
251
+ // Loading State (Skeleton)
252
+ return (
253
+ <div style={{
254
+ width: '100%',
255
+ height: '100%',
256
+ backgroundColor: '#eee',
257
+ borderRadius: '8px',
258
+ animation: 'pulse 1.5s infinite ease-in-out',
259
+ ...style
260
+ }}>
261
+ <style>{`
262
+ @keyframes pulse {
263
+ 0% { opacity: 0.6; }
264
+ 50% { opacity: 0.8; }
265
+ 100% { opacity: 0.6; }
266
+ }
267
+ `}</style>
268
+ </div>
269
+ );
60
270
  };
61
271
 
62
272
  export default ImageComponent;
@@ -0,0 +1,240 @@
1
+ import React, { useState } from "react";
2
+ import ImageComponent from "./ImageComponent";
3
+
4
+ interface ItemCardProps {
5
+ id: string;
6
+ amount: string | number;
7
+ label: string;
8
+ itemData?: any; // Pass full item data for customization
9
+ rarityColor?: string;
10
+ content?: React.ReactNode;
11
+ children?: React.ReactNode;
12
+ // New props for visual variants
13
+ variant?: "Normal" | "Golden" | "Rainbow";
14
+ shiny?: boolean;
15
+ // Legacy/Other props
16
+ tn?: any;
17
+ weight?: any;
18
+ typeId?: any;
19
+ }
20
+
21
+ const ItemCard: React.FC<ItemCardProps> = ({
22
+ id,
23
+ amount,
24
+ label,
25
+ itemData,
26
+ rarityColor = "#e0e0e0", // Default gray
27
+ content,
28
+ children,
29
+ variant = "Normal",
30
+ shiny = false,
31
+ tn, // Destructure but ignore
32
+ weight, // Destructure but ignore
33
+ typeId // Destructure but ignore
34
+ }) => {
35
+ const [isHovered, setIsHovered] = useState(false);
36
+
37
+ // If custom content is provided, render that (for list view etc)
38
+ if (content || children) {
39
+ return <>{content || children}</>;
40
+ }
41
+
42
+ // Determine visual styles based on variant
43
+ // Determine visual styles based on variant
44
+ const filters: string[] = [];
45
+ if (isHovered) {
46
+ filters.push("drop-shadow(0 5px 10px rgba(0,0,0,0.2))");
47
+ }
48
+
49
+ let cardBorder = "2px solid #e0e0e0";
50
+ let bgStyle = "#ffffff";
51
+
52
+ if (variant === "Golden") {
53
+ filters.push("sepia(100%) saturate(300%) hue-rotate(10deg)");
54
+ cardBorder = "2px solid #FFD700";
55
+ bgStyle = "#FFFDF0";
56
+ } else if (variant === "Rainbow") {
57
+ // Rainbow effect usually implies vibrant changing colors or just a rainbow gradient border
58
+ // For item image, maybe a slight hue rotate or saturation boost?
59
+ // Actual PS99 rainbow pets are distinct, but a filter approximation:
60
+ filters.push("saturate(200%) hue-rotate(30deg) contrast(120%)");
61
+ cardBorder = "2px solid #a29bfe"; // Placeholder for rainbow, usually gradient
62
+ }
63
+
64
+ const imageFilter = filters.length > 0 ? filters.join(" ") : "none";
65
+
66
+ // Shiny Effect
67
+ // Shiny acts as an overlay or texture
68
+
69
+ // Clean White Square Style (Reference Match)
70
+ return (
71
+ <div
72
+ onMouseEnter={() => setIsHovered(true)}
73
+ onMouseLeave={() => setIsHovered(false)}
74
+ style={{
75
+ display: "flex",
76
+ flexDirection: "column",
77
+ alignItems: "center",
78
+ justifyContent: "space-between",
79
+ // border handled below
80
+ borderRadius: "16px",
81
+ backgroundColor: bgStyle,
82
+ boxShadow: isHovered
83
+ ? "0 8px 20px rgba(0,0,0,0.1)"
84
+ : "0 2px 5px rgba(0,0,0,0.05)",
85
+ transition: "all 0.1s ease-out",
86
+ // transform: isHovered ? "translateY(-4px)" : "translateY(0)", // User requested no movement
87
+ cursor: "pointer",
88
+ width: "100%",
89
+ aspectRatio: "1 / 1",
90
+ position: "relative",
91
+ overflow: "hidden",
92
+ padding: "20px",
93
+ boxSizing: "border-box", // CRITICAL: Ensure padding is included in width to prevent overlap
94
+ // Rainbow Border Trick
95
+ background: variant === "Rainbow"
96
+ ? "linear-gradient(#fff, #fff) padding-box, linear-gradient(45deg, #ff0000, #ff7f00, #ffff00, #00ff00, #0000ff, #4b0082, #9400d3) border-box"
97
+ : undefined,
98
+ border: variant === "Rainbow" ? "3px solid transparent" : cardBorder,
99
+ zIndex: isHovered ? 100 : 1, // Ensure it pops over neighbors on hover
100
+ }}
101
+ >
102
+ {/* Shiny Sparkles Overlay */}
103
+ {shiny && (
104
+ <div style={{
105
+ position: "absolute",
106
+ top: 0,
107
+ left: 0,
108
+ right: 0,
109
+ bottom: 0,
110
+ pointerEvents: "none",
111
+ zIndex: 10,
112
+ 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')",
113
+ backgroundSize: "40px 40px",
114
+ opacity: 0.5,
115
+ }} />
116
+ )}
117
+
118
+ {/* Rarity Indicator REMOVED based on user feedback */}
119
+
120
+ {/* Item Image */}
121
+ <div
122
+ style={{
123
+ flex: 1,
124
+ width: "90%",
125
+ display: "flex",
126
+ justifyContent: "center",
127
+ alignItems: "center",
128
+ position: "relative",
129
+ marginTop: "10px",
130
+ }}
131
+ >
132
+ {(() => {
133
+ // Use a simple local resolver if hook isn't available or just use the passed data
134
+ // actually we can't easily use the hook inside a map or conditional without refactoring parent
135
+ // BUT, we can just use the same logic here or expect the parent to pass a resolved/normalized object.
136
+ // However, to be "Universal" and "Clean", let's replicate the safe check here or expect `itemData` to be rich.
137
+
138
+ const iconSrc =
139
+ itemData?.icon ||
140
+ itemData?.Icon ||
141
+ itemData?.thumbnail ||
142
+ itemData?.image ||
143
+ itemData?.texture ||
144
+ itemData?.orbImage ||
145
+ itemData?.titanicIcon ||
146
+ itemData?.petIcon ||
147
+ itemData?.eggIcon ||
148
+ itemData?.enchantIcon ||
149
+ itemData?.potionIcon ||
150
+ itemData?.fruitIcon ||
151
+ itemData?.toyIcon ||
152
+ itemData?.charmIcon ||
153
+ itemData?.boothIcon ||
154
+ itemData?.flagIcon ||
155
+ itemData?.keyIcon ||
156
+ itemData?.seedIcon ||
157
+ itemData?.bookIcon ||
158
+ itemData?.giftIcon ||
159
+ itemData?.currencyIcon ||
160
+ itemData?.miscIcon;
161
+
162
+ if (iconSrc) {
163
+ return (
164
+ <ImageComponent
165
+ src={iconSrc}
166
+ alt={label}
167
+ style={{
168
+ width: "100%",
169
+ height: "100%",
170
+ objectFit: "contain",
171
+ filter: imageFilter,
172
+ transition: "filter 0.2s ease",
173
+ transform: isHovered ? "scale(1.1)" : "scale(1)",
174
+ }}
175
+ />
176
+ );
177
+ } else {
178
+ return <span style={{ fontSize: "3.5rem", opacity: 0.3 }}>?</span>;
179
+ }
180
+ })()}
181
+ </div>
182
+
183
+ {/* Quantity Badge (Top Right) */}
184
+ {amount && amount !== 1 && amount !== "1" && (
185
+ <div
186
+ style={{
187
+ position: "absolute",
188
+ top: "12px",
189
+ right: "12px",
190
+ backgroundColor: "#333",
191
+ color: "#fff",
192
+ padding: "2px 8px",
193
+ borderRadius: "8px",
194
+ fontWeight: "800",
195
+ fontSize: "0.8rem",
196
+ zIndex: 5,
197
+ boxShadow: "0 2px 4px rgba(0,0,0,0.1)",
198
+ }}
199
+ >
200
+ x{amount.toLocaleString()}
201
+ </div>
202
+ )}
203
+
204
+ {/* Label (Hover Only Overlay) */}
205
+ <div
206
+ style={{
207
+ position: "absolute",
208
+ bottom: 0,
209
+ left: 0,
210
+ width: "100%",
211
+ padding: "8px 4px",
212
+ backgroundColor: "rgba(255, 255, 255, 0.95)",
213
+ textAlign: "center",
214
+ opacity: isHovered ? 1 : 0,
215
+ transform: isHovered ? "translateY(0)" : "translateY(100%)",
216
+ transition: "all 0.2s cubic-bezier(0.4, 0, 0.2, 1)",
217
+ zIndex: 20,
218
+ borderTop: "2px solid rgba(0,0,0,0.05)",
219
+ }}
220
+ >
221
+ <span
222
+ style={{
223
+ fontSize: "0.85rem",
224
+ fontWeight: "800",
225
+ color: "#2d3436",
226
+ lineHeight: "1.1",
227
+ display: "-webkit-box",
228
+ WebkitLineClamp: 2,
229
+ WebkitBoxOrient: "vertical",
230
+ overflow: "hidden",
231
+ }}
232
+ >
233
+ {label}
234
+ </span>
235
+ </div>
236
+ </div>
237
+ );
238
+ };
239
+
240
+ export default ItemCard;
@@ -1,25 +1,33 @@
1
1
  import React from "react";
2
2
  import { CollectionConfigData } from "ps99-api";
3
- import { GenericFetchComponent } from "./GenericFetchComponent";
4
- import ImageComponent from "./ImageComponent";
3
+ import ItemCard from "./ItemCard";
4
+ import { useItemResolution } from "../hooks/useItemResolution";
5
5
 
6
6
  const LootboxesComponent: React.FC<{
7
- configData?: CollectionConfigData<"Lootboxes">;
7
+ configData: CollectionConfigData<"Lootboxes">;
8
8
  }> = ({ configData }) => {
9
+ const { getRarityColor } = useItemResolution();
10
+ const rarityColor = (configData as any).Rarity ? getRarityColor((configData as any).Rarity) : null;
11
+
9
12
  return (
10
- <GenericFetchComponent<CollectionConfigData<"Lootboxes">>
11
- collectionName="Lootboxes"
12
- configData={configData}
13
- render={(data) => (
14
- <div>
15
- <h2>{data.DisplayName}</h2>
16
- <ImageComponent src={data.Icon} alt={data.DisplayName} />
17
- <p>Description: {data.Desc}</p>
18
- <p>Rarity: {data.Rarity.DisplayName}</p>
19
- <p>Rarity Number: {data.Rarity.RarityNumber}</p>
20
- </div>
21
- )}
22
- />
13
+ <div style={{ width: '100%', height: '100%', boxSizing: 'border-box' }}>
14
+ <ItemCard
15
+ id={configData.DisplayName}
16
+ amount={1}
17
+ label={configData.DisplayName}
18
+ itemData={{
19
+ icon: configData.Icon,
20
+ rarity: (configData as any).Rarity,
21
+ name: configData.DisplayName
22
+ }}
23
+ rarityColor={rarityColor}
24
+ typeId={(configData as any)._index}
25
+ />
26
+ {/* Additional details provided by ItemCard, but we can add more if needed below */}
27
+ <div style={{ marginTop: '10px', fontSize: '0.9em', color: '#666', textAlign: 'center' }}>
28
+ <p>{configData.Desc}</p>
29
+ </div>
30
+ </div>
23
31
  );
24
32
  };
25
33