ps99-api 2.4.0 → 2.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (92) hide show
  1. package/.github/workflows/release-on-main.yml +1 -2
  2. package/.idea/runConfigurations/test_changing.xml +1 -1
  3. package/debug_currency.json +57 -0
  4. package/debug_goals.json +271 -0
  5. package/dist/ps99-api.d.ts +2 -0
  6. package/dist/ps99-api.js +4 -1
  7. package/dist/ps99-api.js.map +1 -1
  8. package/dist/request-client/axios.js +6 -1
  9. package/dist/request-client/axios.js.map +1 -1
  10. package/dist/responses/collection/index.d.ts +1 -0
  11. package/dist/responses/collection/index.js +15 -0
  12. package/dist/responses/collection/index.js.map +1 -1
  13. package/dist/responses/collection/rarity.d.ts +1 -0
  14. package/example-web/react/package-lock.json +1504 -1470
  15. package/example-web/react2/package-lock.json +3089 -2766
  16. package/example-web/react2/package.json +6 -1
  17. package/example-web/react2/public/assets/gold_variant_icon.png +0 -0
  18. package/example-web/react2/public/assets/hot_cocoa_egg.png +0 -0
  19. package/example-web/react2/public/index.html +34 -31
  20. package/example-web/react2/src/App.tsx +15 -15
  21. package/example-web/react2/src/assets/guild_placeholder.png +0 -0
  22. package/example-web/react2/src/components/AchievementsComponent.tsx +74 -19
  23. package/example-web/react2/src/components/AutoSizer.tsx +49 -0
  24. package/example-web/react2/src/components/BoostsComponent.tsx +16 -5
  25. package/example-web/react2/src/components/BoothsComponent.tsx +22 -52
  26. package/example-web/react2/src/components/BoxesComponent.tsx +48 -16
  27. package/example-web/react2/src/components/BuffsComponent.tsx +82 -34
  28. package/example-web/react2/src/components/CharmsComponent.tsx +84 -24
  29. package/example-web/react2/src/components/CollectionConfigIndex.tsx +867 -33
  30. package/example-web/react2/src/components/CollectionsIndex.tsx +380 -27
  31. package/example-web/react2/src/components/CollectionsLayout.tsx +60 -0
  32. package/example-web/react2/src/components/CurrencyComponent.tsx +57 -39
  33. package/example-web/react2/src/components/DynamicCollectionConfigData.tsx +172 -15
  34. package/example-web/react2/src/components/EggsComponent.tsx +50 -12
  35. package/example-web/react2/src/components/EnchantsComponent.tsx +88 -42
  36. package/example-web/react2/src/components/FishingRodsComponent.tsx +36 -22
  37. package/example-web/react2/src/components/Footer.tsx +18 -8
  38. package/example-web/react2/src/components/FruitsComponent.tsx +40 -17
  39. package/example-web/react2/src/components/GenericFetchComponent.tsx +9 -1
  40. package/example-web/react2/src/components/GuildBattlesComponent.tsx +41 -34
  41. package/example-web/react2/src/components/Header.tsx +39 -52
  42. package/example-web/react2/src/components/HomePage.tsx +15 -17
  43. package/example-web/react2/src/components/HoverboardsComponent.tsx +23 -99
  44. package/example-web/react2/src/components/ImageComponent.tsx +255 -45
  45. package/example-web/react2/src/components/ItemCard.tsx +240 -0
  46. package/example-web/react2/src/components/LootboxesComponent.tsx +22 -7
  47. package/example-web/react2/src/components/MasteryComponent.tsx +165 -30
  48. package/example-web/react2/src/components/MerchantsComponent.tsx +41 -16
  49. package/example-web/react2/src/components/MiscItemsComponent.tsx +26 -31
  50. package/example-web/react2/src/components/PetsComponent.tsx +100 -61
  51. package/example-web/react2/src/components/PotionsComponent.tsx +121 -27
  52. package/example-web/react2/src/components/RandomEventsComponent.tsx +32 -23
  53. package/example-web/react2/src/components/RanksComponent.tsx +187 -62
  54. package/example-web/react2/src/components/RarityComponent.tsx +123 -5
  55. package/example-web/react2/src/components/ReactWindowMock.tsx +73 -0
  56. package/example-web/react2/src/components/RebirthsComponent.tsx +36 -19
  57. package/example-web/react2/src/components/SecretRoomsComponent.tsx +5 -4
  58. package/example-web/react2/src/components/SeedsComponent.tsx +41 -21
  59. package/example-web/react2/src/components/ShovelsComponent.tsx +21 -9
  60. package/example-web/react2/src/components/Sidebar.tsx +105 -0
  61. package/example-web/react2/src/components/SprinklersComponent.tsx +25 -10
  62. package/example-web/react2/src/components/Tooltip.tsx +36 -0
  63. package/example-web/react2/src/components/UltimatesComponent.tsx +28 -16
  64. package/example-web/react2/src/components/UpgradesComponent.tsx +97 -47
  65. package/example-web/react2/src/components/WateringCansComponent.tsx +20 -14
  66. package/example-web/react2/src/components/WorldsComponent.tsx +21 -11
  67. package/example-web/react2/src/components/XPPotionsComponent.tsx +28 -11
  68. package/example-web/react2/src/components/ZoneFlagsComponent.tsx +25 -14
  69. package/example-web/react2/src/components/ZonesComponent.tsx +43 -60
  70. package/example-web/react2/src/constants/collectionIcons.ts +29 -0
  71. package/example-web/react2/src/context/CollectionDataContext.tsx +62 -0
  72. package/example-web/react2/src/context/ScrollContext.tsx +35 -0
  73. package/example-web/react2/src/hooks/useCollapsibleHeader.ts +69 -0
  74. package/example-web/react2/src/hooks/useExpandableList.ts +38 -0
  75. package/example-web/react2/src/hooks/useItemResolution.ts +351 -0
  76. package/example-web/react2/src/index.css +257 -0
  77. package/example-web/react2/src/index.tsx +2 -1
  78. package/example-web/react2/src/utils/gigantix.ts +40 -0
  79. package/example-web/react2/temp_model.rbxm +0 -0
  80. package/example-web/react2/webpack.config.js +103 -47
  81. package/package.json +11 -11
  82. package/ranks.json +1 -0
  83. package/repro_collection_fetch.ts +33 -0
  84. package/repro_image_fetch.ts +50 -0
  85. package/src/__tests__/__snapshots__/ps99-api-changes.ts.snap +34841 -10439
  86. package/src/__tests__/__snapshots__/ps99-api-live.ts.snap +160667 -67217
  87. package/src/ps99-api.ts +9 -5
  88. package/src/request-client/axios.ts +6 -2
  89. package/src/responses/collection/index.ts +1 -0
  90. package/src/responses/collection/rarity.ts +1 -0
  91. package/tsconfig.json +1 -1
  92. package/example-web/react2/public/service-worker.js +0 -63
@@ -1,5 +1,5 @@
1
- import React, { lazy, Suspense } from "react";
2
- import { useParams } from "react-router-dom";
1
+ import React, { lazy, Suspense, useState } from "react";
2
+ import { useParams, useNavigate } from "react-router-dom";
3
3
  import { CollectionName, CollectionConfigData } from "ps99-api";
4
4
  import { GenericFetchComponent } from "./GenericFetchComponent";
5
5
 
@@ -7,6 +7,7 @@ interface DynamicCollectionConfigDataProps {
7
7
  collectionName?: CollectionName;
8
8
  configName?: string;
9
9
  render?: (configData: CollectionConfigData<any>) => React.ReactNode; // Add render prop
10
+ additionalProps?: any; // Add additional props to pass to the rendered component
10
11
  }
11
12
 
12
13
  const DynamicCollectionConfigData: React.FC<
@@ -17,26 +18,182 @@ const DynamicCollectionConfigData: React.FC<
17
18
  configName: string;
18
19
  }>();
19
20
 
21
+ const navigate = useNavigate();
20
22
  const collectionName = props.collectionName || params.collectionName;
21
23
  const configName = props.configName || params.configName;
22
24
 
23
- if (!collectionName || !configName) {
25
+ const [viewMode, setViewMode] = useState<'render' | 'schema'>('render');
26
+
27
+ /*
28
+ * We need to use useMemo here to prevent the component from being re-created
29
+ * on every render. This usage of useMemo is safe because the import path
30
+ * depends only on collectionName, and we want to re-create the component
31
+ * only when collectionName changes.
32
+ */
33
+ /*
34
+ * We need to use useMemo here to prevent the component from being re-created
35
+ * on every render. This usage of useMemo is safe because the import path
36
+ * depends only on collectionName, and we want to re-create the component
37
+ * only when collectionName changes.
38
+ */
39
+ const Component = React.useMemo(() => {
40
+ if (!collectionName) return null;
41
+ // Capitalize first letter to match file naming convention (e.g. "pets" -> "Pets")
42
+ // This fixes issues where navigation uses lowercase but files are Capitalized
43
+ const formattedName = collectionName.charAt(0).toUpperCase() + collectionName.slice(1);
44
+ return lazy(() => import(`./${formattedName}Component`));
45
+ }, [collectionName]);
46
+
47
+ if (!collectionName || !configName || !Component) {
24
48
  return <div>Invalid collection or config name</div>;
25
49
  }
26
50
 
27
- const Component = lazy(() => import(`./${collectionName}Component`));
28
-
29
51
  return (
30
- <Suspense fallback={<div>Loading...</div>}>
31
- <GenericFetchComponent
32
- collectionName={collectionName}
33
- configName={configName}
34
- render={
35
- props.render ||
36
- ((configData) => <Component configData={configData} />)
37
- } // Use render prop if provided
38
- />
39
- </Suspense>
52
+ <div style={{ display: "flex", flexDirection: "column", height: "100%", width: "100%" }}>
53
+ {/* Header with Close Button */}
54
+ <div style={{
55
+ padding: "15px 20px",
56
+ borderBottom: "4px solid #333",
57
+ display: "flex",
58
+ alignItems: "center",
59
+ justifyContent: "space-between",
60
+ backgroundColor: "#fff",
61
+ flexShrink: 0,
62
+ }}>
63
+ <div
64
+ style={{ display: "flex", alignItems: "center", gap: "10px", cursor: "pointer" }}
65
+ onClick={() => {
66
+ navigator.clipboard.writeText(window.location.href);
67
+ // visual feedback could be added here, e.g. a small alert or changing the icon color briefly
68
+ const icon = document.getElementById('link-icon-path');
69
+ if (icon) icon.style.fill = '#4caf50';
70
+ setTimeout(() => { if (icon) icon.style.fill = '#ccc'; }, 1000);
71
+ }}
72
+ title="Click to copy link to this item"
73
+ onMouseEnter={(e) => {
74
+ const icon = e.currentTarget.querySelector('.link-icon');
75
+ if (icon) (icon as HTMLElement).style.opacity = '1';
76
+ }}
77
+ onMouseLeave={(e) => {
78
+ const icon = e.currentTarget.querySelector('.link-icon');
79
+ if (icon) (icon as HTMLElement).style.opacity = '0';
80
+ }}
81
+ >
82
+ <h2 style={{
83
+ margin: 0,
84
+ fontSize: "2rem",
85
+ fontWeight: "900",
86
+ color: "#333",
87
+ textShadow: "2px 2px 0px #eee",
88
+ fontFamily: "'Fredoka One', cursive, sans-serif",
89
+ textTransform: "capitalize",
90
+ }}>
91
+ {configName}
92
+ </h2>
93
+ <svg
94
+ className="link-icon"
95
+ xmlns="http://www.w3.org/2000/svg"
96
+ width="24"
97
+ height="24"
98
+ viewBox="0 0 24 24"
99
+ fill="none"
100
+ stroke="#ccc"
101
+ strokeWidth="2"
102
+ strokeLinecap="round"
103
+ strokeLinejoin="round"
104
+ style={{ opacity: 0, transition: 'opacity 0.2s' }}
105
+ >
106
+ <path id="link-icon-path" d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path>
107
+ <path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path>
108
+ </svg>
109
+ </div>
110
+
111
+ <div style={{ display: 'flex', gap: '10px' }}>
112
+ <button
113
+ onClick={() => setViewMode(prev => prev === 'render' ? 'schema' : 'render')}
114
+ style={{
115
+ width: "48px",
116
+ height: "48px",
117
+ borderRadius: "12px",
118
+ backgroundColor: viewMode === 'render' ? "#2196f3" : "#4caf50",
119
+ color: "white",
120
+ border: `4px solid ${viewMode === 'render' ? "#1565c0" : "#2e7d32"}`,
121
+ fontSize: "20px",
122
+ fontWeight: "900",
123
+ display: "flex",
124
+ alignItems: "center",
125
+ justifyContent: "center",
126
+ cursor: "pointer",
127
+ boxShadow: `inset 0 4px 4px rgba(255,255,255,0.4), 0 4px 0 ${viewMode === 'render' ? "#0d47a1" : "#1b5e20"}`,
128
+ fontFamily: "monospace"
129
+ }}
130
+ title={viewMode === 'render' ? "View Raw Schema" : "View Rendered Item"}
131
+ >
132
+ {viewMode === 'render' ? "{}" : "👁️"}
133
+ </button>
134
+
135
+ <button
136
+ onClick={() => navigate(`/collections/${collectionName}`)}
137
+ style={{
138
+ width: "48px",
139
+ height: "48px",
140
+ borderRadius: "12px",
141
+ backgroundColor: "#ff0055",
142
+ color: "white",
143
+ border: "4px solid #900",
144
+ fontSize: "24px",
145
+ fontWeight: "900",
146
+ display: "flex",
147
+ alignItems: "center",
148
+ justifyContent: "center",
149
+ cursor: "pointer",
150
+ boxShadow: "inset 0 4px 4px rgba(255,255,255,0.4), 0 4px 0 #500",
151
+ }}
152
+ >
153
+ X
154
+ </button>
155
+ </div>
156
+ </div>
157
+
158
+ <div style={{ flex: 1, overflowY: "auto", padding: "20px" }}>
159
+ <Suspense fallback={<div>Loading...</div>}>
160
+ <GenericFetchComponent
161
+ // We use the key prop to force a re-mount of the component when the
162
+ // collectionName or configName changes. This ensures that the internal
163
+ // state of the GenericFetchComponent is reset and the new data is
164
+ // fetched correctly, preventing stale data from being passed to the
165
+ // child component.
166
+ key={`${collectionName}-${configName}`}
167
+ collectionName={collectionName}
168
+ configName={configName}
169
+ render={
170
+ (configData: CollectionConfigData<any>) => {
171
+ if (viewMode === 'schema') {
172
+ return (
173
+ <div style={{
174
+ padding: '20px',
175
+ background: '#1e1e1e',
176
+ color: '#d4d4d4',
177
+ borderRadius: '12px',
178
+ overflow: 'auto',
179
+ height: '100%',
180
+ fontFamily: 'Consolas, Monaco, "Andale Mono", "Ubuntu Mono", monospace',
181
+ fontSize: '0.9rem',
182
+ textAlign: 'left',
183
+ boxShadow: 'inset 0 0 20px rgba(0,0,0,0.5)',
184
+ border: '2px solid #333'
185
+ }}>
186
+ <pre style={{ margin: 0 }}>{JSON.stringify(configData, null, 2)}</pre>
187
+ </div>
188
+ );
189
+ }
190
+ return props.render ? props.render(configData) : <Component configData={configData} {...props.additionalProps} />;
191
+ }
192
+ } // Use render prop if provided
193
+ />
194
+ </Suspense>
195
+ </div>
196
+ </div>
40
197
  );
41
198
  };
42
199
 
@@ -1,14 +1,31 @@
1
1
  import React from "react";
2
2
  import { CollectionConfigData } from "ps99-api";
3
3
  import ImageComponent from "./ImageComponent";
4
+ import ItemCard from "./ItemCard";
5
+ import { useItemResolution } from "../hooks/useItemResolution";
4
6
 
5
7
  const EggsComponent: React.FC<{
6
8
  configData: CollectionConfigData<"Eggs">;
7
9
  }> = ({ configData }) => {
10
+ const { getRarityColor, resolveItem } = useItemResolution();
11
+ const rarityColor = configData.rarity ? getRarityColor(configData.rarity) : null;
12
+
8
13
  return (
9
- <div>
10
- <h2>{configData.name}</h2>
11
- <ImageComponent src={configData.icon} alt={configData.name} />
14
+ <div style={{ width: "100%", height: "100%", boxSizing: "border-box" }}>
15
+ <div style={{ width: '300px', margin: '0 auto 20px auto' }}>
16
+ <ItemCard
17
+ id={configData.name}
18
+ amount={1}
19
+ label={configData.name}
20
+ itemData={{
21
+ icon: configData.icon,
22
+ rarity: configData.rarity,
23
+ name: configData.name
24
+ }}
25
+ rarityColor={rarityColor}
26
+ typeId={(configData as any)._index}
27
+ />
28
+ </div>
12
29
  <p>Currency: {configData.currency}</p>
13
30
  <p>Override Cost: {configData.overrideCost}</p>
14
31
  {configData.isCustomEgg && <p>Custom Egg: Yes</p>}
@@ -29,15 +46,36 @@ const EggsComponent: React.FC<{
29
46
  </div>
30
47
  )}
31
48
  <h3>Pets:</h3>
32
- <ul>
33
- {configData.pets.map((pet, index) => (
34
- <li key={index}>
35
- <p>Name: {pet[0]}</p>
36
- <p>Chance: {pet[1]}</p>
37
- {pet[2] && <p>Tier: {pet[2]}</p>}
38
- </li>
39
- ))}
40
- </ul>
49
+ <div style={{ marginTop: '15px' }}>
50
+ <h3 style={{ borderBottom: '1px solid #eee', paddingBottom: '5px' }}>Pets</h3>
51
+ <div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(120px, 1fr))', gap: '16px' }}>
52
+ {configData.pets.map((pet, index) => {
53
+ const petName = pet[0];
54
+ const chance = pet[1];
55
+ // The 3rd element appears to be the tier (tn), based on existing code usage
56
+ const tn = pet[2];
57
+ const resolvedItem = resolveItem(petName, tn);
58
+ const rarityColor = resolvedItem?.rarity ? getRarityColor(resolvedItem.rarity) : null;
59
+
60
+ return (
61
+ <div key={index} style={{
62
+ // Auto-sizing handled by grid
63
+ }}>
64
+ <ItemCard
65
+ id={petName}
66
+ // Formatting chance as string to include %
67
+ amount={`${chance}%`}
68
+ label={resolvedItem?.name || petName}
69
+ itemData={resolvedItem}
70
+ rarityColor={rarityColor}
71
+ tn={tn}
72
+ // weight or typeId not strictly needed here unless we want debug info
73
+ />
74
+ </div>
75
+ );
76
+ })}
77
+ </div>
78
+ </div>
41
79
  {configData.productIds && (
42
80
  <div>
43
81
  <h3>Product IDs:</h3>
@@ -1,52 +1,98 @@
1
1
  import React from "react";
2
2
  import { CollectionConfigData } from "ps99-api";
3
- import ImageComponent from "./ImageComponent";
3
+ import ItemCard from "./ItemCard";
4
+ import { useItemResolution } from "../hooks/useItemResolution";
4
5
 
5
6
  const EnchantsComponent: React.FC<{
6
7
  configData: CollectionConfigData<"Enchants">;
7
8
  }> = ({ configData }) => {
9
+ const { getRarityColor } = useItemResolution();
10
+
11
+
12
+ const isSingleTier = configData.Tiers.length === 1;
13
+
14
+ if (isSingleTier) {
15
+ const tier = configData.Tiers[0];
16
+ const rarityColor = tier.Rarity ? getRarityColor(tier.Rarity) : null;
17
+
18
+ return (
19
+ <div style={{ width: '100%', height: '100%', boxSizing: 'border-box', display: 'flex', flexDirection: 'column', alignItems: 'center', paddingTop: '20px' }}>
20
+ <div style={{
21
+ width: '120px',
22
+ height: '120px',
23
+ borderRadius: '16px',
24
+ border: rarityColor ? `4px solid ${rarityColor}` : '4px solid #eee',
25
+ padding: '10px',
26
+ backgroundColor: '#fff',
27
+ boxShadow: '0 4px 10px rgba(0,0,0,0.1)',
28
+ display: 'flex',
29
+ alignItems: 'center',
30
+ justifyContent: 'center',
31
+ marginBottom: '20px'
32
+ }}>
33
+ <img src={tier.Icon} alt={tier.DisplayName} style={{ width: '100%', height: '100%', objectFit: 'contain' }} />
34
+ </div>
35
+
36
+ <div style={{ textAlign: 'center', maxWidth: '400px' }}>
37
+ {configData.BaseTier !== 1 && <p><strong>Base Tier:</strong> {configData.BaseTier}</p>}
38
+ {configData.MaxTier !== 1 && <p><strong>Max Tier:</strong> {configData.MaxTier}</p>}
39
+ {configData.DiminishPowerThreshold && <p><strong>Diminish:</strong> {configData.DiminishPowerThreshold}</p>}
40
+ {configData.EmpoweredBoost && <p><strong>Empowered Boost:</strong> {configData.EmpoweredBoost}</p>}
41
+
42
+ <h3 style={{ margin: '10px 0', fontSize: '1.5em', color: '#333' }}>{tier.DisplayName}</h3>
43
+ <p style={{ color: '#666', fontSize: '1.1em', marginBottom: '15px' }}>{tier.Desc}</p>
44
+ <p style={{ fontWeight: '900', fontSize: '1.2em', color: '#333' }}>Power: {tier.Power}</p>
45
+ </div>
46
+ </div>
47
+ );
48
+ }
49
+
8
50
  return (
9
- <div>
10
- <h2>Enchantment</h2>
11
- <p>Base Tier: {configData.BaseTier}</p>
12
- <p>Max Tier: {configData.MaxTier}</p>
13
- <p>Max Page: {configData.MaxPage}</p>
14
- {configData.DiminishPowerThreshold && (
15
- <p>Diminish Power Threshold: {configData.DiminishPowerThreshold}</p>
16
- )}
17
- {configData.EmpoweredBoost && (
18
- <p>Empowered Boost: {configData.EmpoweredBoost}</p>
19
- )}
20
- {configData.ProductId && <p>Product ID: {configData.ProductId}</p>}
21
- <h3>Tiers:</h3>
22
- <div style={{ display: "flex", flexWrap: "wrap", gap: "1em" }}>
23
- {configData.Tiers.map((tier, index) => (
24
- <div
25
- key={index}
26
- style={{
27
- border: "1px solid #ccc",
28
- borderRadius: "8px",
29
- padding: "1em",
30
- width: "calc(33% - 1em)",
31
- boxSizing: "border-box",
32
- }}
33
- >
34
- <ImageComponent src={tier.Icon} alt={tier.DisplayName} />
35
- <h4>{tier.DisplayName}</h4>
36
- <p>
37
- <strong>Description:</strong> {tier.Desc}
38
- </p>
39
- <p>
40
- <strong>Power:</strong> {tier.Power}
41
- </p>
42
- <p>
43
- <strong>Rarity:</strong> {tier.Rarity.DisplayName}
44
- </p>
45
- <p>
46
- <strong>Rarity Number:</strong> {tier.Rarity.RarityNumber}
47
- </p>
48
- </div>
49
- ))}
51
+ <div style={{ width: '100%', height: '100%', boxSizing: 'border-box' }}>
52
+ <div style={{ textAlign: 'left', marginBottom: '20px', display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(200px, 1fr))', gap: '10px' }}>
53
+ <p><strong>Base Tier:</strong> {configData.BaseTier}</p>
54
+ <p><strong>Max Tier:</strong> {configData.MaxTier}</p>
55
+ <p><strong>Max Page:</strong> {configData.MaxPage}</p>
56
+ {configData.DiminishPowerThreshold && (
57
+ <p><strong>Diminish Threshold:</strong> {configData.DiminishPowerThreshold}</p>
58
+ )}
59
+ {configData.EmpoweredBoost && (
60
+ <p><strong>Empowered Boost:</strong> {configData.EmpoweredBoost}</p>
61
+ )}
62
+ {configData.ProductId && <p><strong>Product ID:</strong> {configData.ProductId}</p>}
63
+ </div>
64
+ <h3>Tiers</h3>
65
+ <div style={{ display: "grid", gridTemplateColumns: 'repeat(auto-fill, minmax(150px, 1fr))', gap: "15px" }}>
66
+ {configData.Tiers.map((tier, index) => {
67
+ const rarityColor = tier.Rarity ? getRarityColor(tier.Rarity) : null;
68
+ return (
69
+ <div
70
+ key={index}
71
+ style={{
72
+ display: 'flex',
73
+ flexDirection: 'column',
74
+ height: '100%'
75
+ }}
76
+ >
77
+ <ItemCard
78
+ id={tier.DisplayName}
79
+ amount={1}
80
+ label={tier.DisplayName}
81
+ tn={index + 1}
82
+ itemData={{
83
+ icon: tier.Icon,
84
+ rarity: tier.Rarity,
85
+ name: tier.DisplayName
86
+ }}
87
+ rarityColor={rarityColor}
88
+ />
89
+ <div style={{ marginTop: '10px', fontSize: '0.9em', color: '#666', textAlign: 'center' }}>
90
+ <p>{tier.Desc}</p>
91
+ <p style={{ fontWeight: 'bold', color: '#333' }}>Power: {tier.Power}</p>
92
+ </div>
93
+ </div>
94
+ );
95
+ })}
50
96
  </div>
51
97
  </div>
52
98
  );
@@ -1,35 +1,49 @@
1
1
  import React from "react";
2
2
  import { CollectionConfigData } from "ps99-api";
3
- import ImageComponent from "./ImageComponent";
3
+ import ItemCard from "./ItemCard";
4
+ import { useItemResolution } from "../hooks/useItemResolution";
4
5
 
5
6
  const FishingRodsComponent: React.FC<{
6
7
  configData: CollectionConfigData<"FishingRods">;
7
8
  }> = ({ configData }) => {
8
9
  return (
9
- <div>
10
+ <div className="game-card" style={{ maxWidth: '500px', margin: '0 auto' }}>
10
11
  <h2>{configData.DisplayName}</h2>
11
- <ImageComponent src={configData.Icon} alt={configData.DisplayName} />
12
- <p>Fishing Chance: {configData.FishingChance}</p>
13
- <p>Fishing Currency Multiplier: {configData.FishingCurrencyMultiplier}</p>
14
- <p>Minimum Fishing Time: {configData.MinFishingTime} seconds</p>
15
- <p>
16
- Fishing Game Speed Multiplier: {configData.FishingGameSpeedMultiplier}
17
- </p>
18
- <p>Bar Size: {configData.BarSize}</p>
19
- <p>Associated Item ID: {configData.AssociatedItemID}</p>
20
- {configData.MerchantSalePrice && (
21
- <p>Merchant Sale Price: {configData.MerchantSalePrice}</p>
22
- )}
23
- <h3>Fishing Odds:</h3>
24
- <ul>
25
- {configData.FishingOdds.map((odds, index) => (
26
- <li key={index}>
27
- {odds[0]}: {odds[1]}%
28
- </li>
29
- ))}
30
- </ul>
12
+
13
+ <div style={{ maxWidth: '300px', margin: '0 auto 15px auto' }}>
14
+ <ItemCard
15
+ id={configData.DisplayName}
16
+ amount={1}
17
+ label={configData.DisplayName}
18
+ itemData={{
19
+ icon: configData.Icon,
20
+ rarity: undefined,
21
+ name: configData.DisplayName
22
+ }}
23
+ rarityColor={null}
24
+ />
25
+ </div>
26
+
27
+ <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '10px', marginBottom: '20px', textAlign: 'left' }}>
28
+ <p><strong>Chance:</strong> {configData.FishingChance}%</p>
29
+ <p><strong>Currency:</strong> {configData.FishingCurrencyMultiplier}x</p>
30
+ <p><strong>Speed:</strong> {configData.FishingGameSpeedMultiplier}x</p>
31
+ <p><strong>Bar Size:</strong> {configData.BarSize}</p>
32
+ </div>
33
+
34
+ <div style={{ borderTop: '1px solid #eee', paddingTop: '15px' }}>
35
+ <h3 style={{ fontSize: '1.1em', marginBottom: '10px' }}>Fishing Odds</h3>
36
+ <div style={{ display: 'flex', flexWrap: 'wrap', gap: '8px', justifyContent: 'center' }}>
37
+ {configData.FishingOdds.map((odds, index) => (
38
+ <span key={index} className="badge" style={{ background: '#e3f2fd', color: '#1565c0', border: '1px solid #90caf9' }}>
39
+ {odds[0]}: {odds[1]}%
40
+ </span>
41
+ ))}
42
+ </div>
43
+ </div>
31
44
  </div>
32
45
  );
33
46
  };
34
47
 
48
+
35
49
  export default FishingRodsComponent;
@@ -1,4 +1,5 @@
1
1
  import React, { useState, useEffect } from "react";
2
+ import { useLocation } from "react-router-dom";
2
3
  import { useOnlineStatus } from "../hooks/useOnlineStatus";
3
4
 
4
5
  const Footer: React.FC = () => {
@@ -6,6 +7,18 @@ const Footer: React.FC = () => {
6
7
  const [lastUpdate, setLastUpdate] = useState<string | null>(null);
7
8
  const [loading, setLoading] = useState(false);
8
9
 
10
+ const location = useLocation();
11
+ const isCollectionsRoute = location.pathname.startsWith("/collections");
12
+ const [isMobile, setIsMobile] = useState(window.innerWidth < 768);
13
+
14
+ useEffect(() => {
15
+ const handleResize = () => setIsMobile(window.innerWidth < 768);
16
+ window.addEventListener("resize", handleResize);
17
+ return () => window.removeEventListener("resize", handleResize);
18
+ }, []);
19
+
20
+
21
+
9
22
  const updateLastUpdate = () => {
10
23
  setLastUpdate(new Date().toLocaleString());
11
24
  };
@@ -30,15 +43,12 @@ const Footer: React.FC = () => {
30
43
  };
31
44
  }, []);
32
45
 
46
+ if (isMobile && isCollectionsRoute) {
47
+ return null;
48
+ }
49
+
33
50
  return (
34
- <footer
35
- style={{
36
- textAlign: "center",
37
- padding: "1em",
38
- background: "#f8f9fa",
39
- borderTop: "1px solid #ccc",
40
- }}
41
- >
51
+ <footer className="game-footer">
42
52
  <div
43
53
  style={{
44
54
  display: "flex",
@@ -1,27 +1,50 @@
1
1
  import React from "react";
2
2
  import { CollectionConfigData } from "ps99-api";
3
- import ImageComponent from "./ImageComponent";
3
+ import ItemCard from "./ItemCard";
4
+ import { useItemResolution } from "../hooks/useItemResolution";
4
5
 
5
6
  const FruitsComponent: React.FC<{
6
7
  configData: CollectionConfigData<"Fruits">;
7
8
  }> = ({ configData }) => {
9
+ const { getRarityColor } = useItemResolution();
10
+ const rarityColor = configData.Rarity ? getRarityColor(configData.Rarity) : null;
11
+
8
12
  return (
9
- <div>
10
- <h2>{configData.DisplayName}</h2>
11
- <ImageComponent src={configData.Icon} alt={configData.DisplayName} />
12
- <p>Duration: {configData.Duration} seconds</p>
13
- <p>Rarity: {configData.Rarity.DisplayName}</p>
14
- <p>Rarity Number: {configData.Rarity.RarityNumber}</p>
15
- {configData.Desc && <p>Description: {configData.Desc}</p>}
16
- {configData.IgnoreFruitMachine && <p>Ignore Fruit Machine: Yes</p>}
17
- <h3>Boosts:</h3>
18
- <ul>
19
- {configData.Boost.map((boost, index) => (
20
- <li key={index}>
21
- {boost.Type}: {boost.Amount}
22
- </li>
23
- ))}
24
- </ul>
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>Duration: {configData.Duration}s</p>
27
+ {configData.Desc && <p>{configData.Desc}</p>}
28
+ {configData.IgnoreFruitMachine && <p>Ignore Fruit Machine</p>}
29
+ </div>
30
+ <div style={{ marginTop: '15px' }}>
31
+ <h3 style={{ textAlign: 'center', fontSize: '1.1em' }}>Boosts</h3>
32
+ <div style={{ display: 'flex', flexWrap: 'wrap', gap: '8px', justifyContent: 'center' }}>
33
+ {configData.Boost.map((boost, index) => (
34
+ <span key={index} style={{
35
+ background: '#e8f5e9',
36
+ color: '#2e7d32',
37
+ padding: '5px 10px',
38
+ borderRadius: '12px',
39
+ fontSize: '0.9em',
40
+ fontWeight: 'bold',
41
+ border: '1px solid #a5d6a7'
42
+ }}>
43
+ {boost.Type}: {boost.Amount}
44
+ </span>
45
+ ))}
46
+ </div>
47
+ </div>
25
48
  </div>
26
49
  );
27
50
  };
@@ -59,5 +59,13 @@ export const GenericFetchComponent = <T,>({
59
59
  return <div>Loading...</div>;
60
60
  }
61
61
 
62
- return <div>{render(data)}</div>;
62
+ return (
63
+ <div style={{
64
+ width: '100%',
65
+ padding: '20px',
66
+ boxSizing: 'border-box'
67
+ }}>
68
+ {render(data)}
69
+ </div>
70
+ );
63
71
  };