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,35 +1,38 @@
1
1
  import React from "react";
2
2
  import { CollectionConfigData } from "ps99-api";
3
- import { GenericFetchComponent } from "./GenericFetchComponent";
4
3
 
5
4
  const WorldComponent: React.FC<{
6
- configData?: CollectionConfigData<"Worlds">;
5
+ configData: CollectionConfigData<"Worlds">;
7
6
  }> = ({ configData }) => {
8
7
  return (
9
- <GenericFetchComponent<CollectionConfigData<"Worlds">>
10
- collectionName="Worlds"
11
- configData={configData}
12
- render={(data) => (
13
- <div>
14
- <h2>World</h2>
15
- <h3>{data.MapName}</h3>
16
- <p>Spawn ID: {data.SpawnId}</p>
17
- <p>World Currency: {data.WorldCurrency}</p>
18
- <p>Place ID: {data.PlaceId}</p>
19
- <p>World Number: {data.WorldNumber}</p>
20
- {data.AdditionalMusic && data.AdditionalMusic.length > 0 && (
21
- <div>
22
- <h4>Additional Music</h4>
23
- <ul>
24
- {data.AdditionalMusic.map((music, index) => (
25
- <li key={index}>{music}</li>
26
- ))}
27
- </ul>
28
- </div>
29
- )}
8
+ <div style={{ width: '100%', height: '100%', boxSizing: 'border-box' }}>
9
+ <div style={{ textAlign: 'left', width: '100%' }}>
10
+ <p><strong>World #:</strong> {configData.WorldNumber}</p>
11
+ <p><strong>Spawn ID:</strong> {configData.SpawnId}</p>
12
+ <p><strong>Currency:</strong> {configData.WorldCurrency}</p>
13
+ <p><strong>Place ID:</strong> {configData.PlaceId}</p>
14
+ </div>
15
+
16
+ {configData.AdditionalMusic && configData.AdditionalMusic.length > 0 && (
17
+ <div style={{ marginTop: '15px', width: '100%' }}>
18
+ <h4>Additional Music</h4>
19
+ <div style={{ display: 'flex', flexWrap: 'wrap', gap: '8px', justifyContent: 'center' }}>
20
+ {configData.AdditionalMusic.map((music, index) => (
21
+ <span key={index} style={{
22
+ background: '#e0f7fa',
23
+ color: '#006064',
24
+ padding: '5px 10px',
25
+ borderRadius: '15px',
26
+ fontSize: '0.9em',
27
+ fontWeight: 'bold'
28
+ }}>
29
+ {music}
30
+ </span>
31
+ ))}
32
+ </div>
30
33
  </div>
31
34
  )}
32
- />
35
+ </div>
33
36
  );
34
37
  };
35
38
 
@@ -1,29 +1,39 @@
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 XPPotionsComponent: React.FC<{
7
- configData?: CollectionConfigData<"XPPotions">;
7
+ configData: CollectionConfigData<"XPPotions">;
8
8
  }> = ({ configData }) => {
9
+ const { getRarityColor } = useItemResolution();
10
+ const rarityColor = configData.Rarity ? getRarityColor(configData.Rarity) : null;
11
+
9
12
  return (
10
- <GenericFetchComponent<CollectionConfigData<"XPPotions">>
11
- collectionName="XPPotions"
12
- configData={configData}
13
- render={(data) => (
14
- <div>
15
- <h2>{data.DisplayName}</h2>
16
- <ImageComponent src={data.Icon} alt={data.DisplayName} />
17
- <p>Amount: {data.Amount}</p>
18
- <p>Description: {data.Desc}</p>
19
- <p>Item ID: {data.ItemId}</p>
20
- <h3>Rarity</h3>
21
- <p>Display Name: {data.Rarity.DisplayName}</p>
22
- <p>Rarity Number: {data.Rarity.RarityNumber}</p>
23
- <p>Announce: {data.Rarity.Announce ? "Yes" : "No"}</p>
13
+ <div style={{ width: '100%', height: '100%', boxSizing: 'border-box' }}>
14
+
15
+ <div style={{ maxWidth: '300px', margin: '0 auto 15px auto' }}>
16
+ <ItemCard
17
+ id={configData.DisplayName}
18
+ amount={1}
19
+ label={configData.DisplayName}
20
+ itemData={{
21
+ icon: configData.Icon,
22
+ rarity: configData.Rarity,
23
+ name: configData.DisplayName
24
+ }}
25
+ rarityColor={rarityColor}
26
+ />
27
+ </div>
28
+
29
+ <div style={{ fontSize: '0.9em', color: '#666', textAlign: 'center' }}>
30
+ <p style={{ marginBottom: '10px' }}>{configData.Desc}</p>
31
+ <div style={{ display: 'flex', gap: '10px', justifyContent: 'center' }}>
32
+ <span className="badge">Amount: {configData.Amount}</span>
33
+ <span className="badge" style={{ background: '#eee', color: '#333' }}>ID: {configData.ItemId}</span>
24
34
  </div>
25
- )}
26
- />
35
+ </div>
36
+ </div>
27
37
  );
28
38
  };
29
39
 
@@ -1,32 +1,36 @@
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 ZoneFlagComponent: React.FC<{
7
- configData?: CollectionConfigData<"ZoneFlags">;
7
+ configData: CollectionConfigData<"ZoneFlags">;
8
8
  }> = ({ configData }) => {
9
+ const { getRarityColor } = useItemResolution();
10
+ const rarityColor = configData.Rarity ? getRarityColor(configData.Rarity) : null;
11
+
9
12
  return (
10
- <GenericFetchComponent<CollectionConfigData<"ZoneFlags">>
11
- collectionName="ZoneFlags"
12
- configData={configData}
13
- render={(data) => (
14
- <div>
15
- <h2>Zone Flag</h2>
16
- <h3>{data.Name}</h3>
17
- <p>Description: {data.Desc}</p>
18
- <p>Duration: {data.Duration} seconds</p>
19
- <p>Color: {data.Color}</p>
20
- <p>
21
- Icon: <ImageComponent src={data.Icon} alt={data.Name} />
22
- </p>
23
- <h4>Rarity</h4>
24
- <p>Rarity: {data.Rarity.DisplayName}</p>
25
- <p>Rarity Number: {data.Rarity.RarityNumber}</p>
26
- <p>Announce: {data.Rarity.Announce.toString()}</p>
27
- </div>
28
- )}
29
- />
13
+ <div style={{ width: '100%', height: '100%', boxSizing: 'border-box' }}>
14
+
15
+ <div style={{ maxWidth: '300px', margin: '0 auto 15px 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
+ />
27
+ </div>
28
+
29
+ <div style={{ fontSize: '0.9em', color: '#666', textAlign: 'center' }}>
30
+ <p>{configData.Desc}</p>
31
+ <p style={{ fontWeight: 'bold', marginTop: '5px' }}>Duration: {configData.Duration}s</p>
32
+ </div>
33
+ </div>
30
34
  );
31
35
  };
32
36
 
@@ -1,88 +1,71 @@
1
1
  import React from "react";
2
2
  import { CollectionConfigData } from "ps99-api";
3
- import { GenericFetchComponent } from "./GenericFetchComponent";
4
3
 
5
4
  const ZoneComponent: React.FC<{
6
- configData?: CollectionConfigData<"Zones">;
5
+ configData: CollectionConfigData<"Zones">;
7
6
  }> = ({ configData }) => {
8
7
  return (
9
- <GenericFetchComponent<CollectionConfigData<"Zones">>
10
- collectionName="Zones"
11
- configData={configData}
12
- render={(data) => (
13
- <div>
14
- <h2>Zone</h2>
15
- <h3>{data.ZoneName}</h3>
16
- <p>Zone Number: {data.ZoneNumber}</p>
17
- <p>World Number: {data.WorldNumber}</p>
18
- <p>Currency: {data.Currency}</p>
19
- <p>Maximum Available Egg: {data.MaximumAvailableEgg}</p>
20
- {data.Price && <p>Price: {data.Price}</p>}
21
- {data.GateHealth && <p>Gate Health: {data.GateHealth}</p>}
22
- {data.TeleportToZoneOnFall && (
23
- <p>
24
- Teleport to Zone on Fall: {data.TeleportToZoneOnFall.toString()}
25
- </p>
26
- )}
27
- {data.Ambience && <p>Ambience Sound ID: {data.Ambience.SoundId}</p>}
28
- {data.QuestsRequired && data.QuestsRequired.length > 0 && (
29
- <div>
30
- <h4>Quests Required</h4>
31
- <ul>
32
- {data.QuestsRequired.map((quest, index) => (
33
- <li key={index}>
34
- Type: {quest.Type}, Amount: {quest.Amount}
35
- </li>
36
- ))}
37
- </ul>
8
+ <div style={{ width: '100%', height: '100%', boxSizing: 'border-box' }}>
9
+ <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '10px', width: '100%', textAlign: 'left' }}>
10
+ <p><strong>Zone #:</strong> {configData.ZoneNumber}</p>
11
+ <p><strong>World #:</strong> {configData.WorldNumber}</p>
12
+ <p><strong>Currency:</strong> {configData.Currency}</p>
13
+ <p><strong>Max Egg:</strong> {configData.MaximumAvailableEgg}</p>
14
+ {configData.Price && <p><strong>Price:</strong> {configData.Price}</p>}
15
+ {configData.GateHealth && <p><strong>Gate Health:</strong> {configData.GateHealth}</p>}
16
+ {configData.TeleportToZoneOnFall && (
17
+ <p><strong>Teleport on Fall:</strong> Yes</p>
18
+ )}
19
+ {configData.Ambience && (
20
+ <p><strong>Ambience ID:</strong> {configData.Ambience.SoundId}</p>
21
+ )}
22
+ </div>
23
+
24
+ {configData.QuestsRequired && configData.QuestsRequired.length > 0 && (
25
+ <div style={{ width: '100%', marginTop: '20px' }}>
26
+ <h4>Quests Required</h4>
27
+ <div style={{ display: 'flex', flexWrap: 'wrap', gap: '10px', justifyContent: 'center' }}>
28
+ {configData.QuestsRequired.map((quest, index) => (
29
+ <div key={index} style={{
30
+ background: '#f0f0f0',
31
+ padding: '10px',
32
+ borderRadius: '8px',
33
+ border: '1px solid #ddd'
34
+ }}>
35
+ <strong>{quest.Type}</strong>: {quest.Amount}
36
+ </div>
37
+ ))}
38
+ </div>
39
+ </div>
40
+ )}
41
+
42
+ {configData.Lighting && (
43
+ <div style={{ width: '100%', marginTop: '20px', textAlign: 'left', background: '#f9f9f9', padding: '15px', borderRadius: '8px' }}>
44
+ <h4 style={{ textAlign: 'center' }}>Lighting Settings</h4>
45
+ <p>Brightness: {configData.Lighting.Brightness}</p>
46
+ <p>Clock Time: {configData.Lighting.ClockTime}</p>
47
+
48
+ {configData.Lighting.Bloom && (
49
+ <div style={{ marginTop: '10px' }}>
50
+ <strong>Bloom:</strong>
51
+ <span style={{ fontSize: '0.9em', marginLeft: '10px' }}>
52
+ Enabled: {configData.Lighting.Bloom.Enabled.toString()},
53
+ Intensity: {configData.Lighting.Bloom.Intensity}
54
+ </span>
38
55
  </div>
39
56
  )}
40
- {data.Lighting && (
41
- <div>
42
- <h4>Lighting</h4>
43
- <p>Brightness: {data.Lighting.Brightness}</p>
44
- <p>Clock Time: {data.Lighting.ClockTime}</p>
45
- {data.Lighting.Bloom && (
46
- <div>
47
- <h5>Bloom</h5>
48
- <p>Enabled: {data.Lighting.Bloom.Enabled.toString()}</p>
49
- <p>Intensity: {data.Lighting.Bloom.Intensity}</p>
50
- <p>Size: {data.Lighting.Bloom.Size}</p>
51
- <p>Threshold: {data.Lighting.Bloom.Threshold}</p>
52
- </div>
53
- )}
54
- {data.Lighting.ColorCorrection && (
55
- <div>
56
- <h5>Color Correction</h5>
57
- <p>
58
- Enabled: {data.Lighting.ColorCorrection.Enabled.toString()}
59
- </p>
60
- <p>Saturation: {data.Lighting.ColorCorrection.Saturation}</p>
61
- <p>Contrast: {data.Lighting.ColorCorrection.Contrast}</p>
62
- <p>Brightness: {data.Lighting.ColorCorrection.Brightness}</p>
63
- </div>
64
- )}
65
- {data.Lighting.Sky && (
66
- <div>
67
- <h5>Sky</h5>
68
- <p>Star Count: {data.Lighting.Sky.StarCount}</p>
69
- <p>
70
- Celestial Bodies Shown:{" "}
71
- {data.Lighting.Sky.CelestialBodiesShown.toString()}
72
- </p>
73
- <p>Skybox Up: {data.Lighting.Sky.SkyboxUp}</p>
74
- <p>Skybox Bk: {data.Lighting.Sky.SkyboxBk}</p>
75
- <p>Skybox Dn: {data.Lighting.Sky.SkyboxDn}</p>
76
- <p>Skybox Lf: {data.Lighting.Sky.SkyboxLf}</p>
77
- <p>Skybox Rt: {data.Lighting.Sky.SkyboxRt}</p>
78
- <p>Skybox Ft: {data.Lighting.Sky.SkyboxFt}</p>
79
- </div>
80
- )}
57
+
58
+ {configData.Lighting.Sky && (
59
+ <div style={{ marginTop: '10px' }}>
60
+ <strong>Sky:</strong>
61
+ <span style={{ fontSize: '0.9em', marginLeft: '10px' }}>
62
+ Stars: {configData.Lighting.Sky.StarCount}
63
+ </span>
81
64
  </div>
82
65
  )}
83
66
  </div>
84
67
  )}
85
- />
68
+ </div>
86
69
  );
87
70
  };
88
71
 
@@ -0,0 +1,29 @@
1
+ export const COLLECTION_ICONS: Record<string, string> = {
2
+ Pets: "๐Ÿฑ",
3
+ Eggs: "๐Ÿฅš",
4
+ Enchants: "๐Ÿ“˜",
5
+ Potions: "๐Ÿงช",
6
+ Items: "๐ŸŽ’",
7
+ Hoverboards: "๐Ÿ›น",
8
+ Booths: "๐Ÿช",
9
+ Ultimates: "๐ŸŒ€",
10
+ MiscItems: "๐Ÿ“ฆ",
11
+ Keys: "๐Ÿ—๏ธ",
12
+ Tickets: "๐ŸŽŸ๏ธ",
13
+ Charms: "๐Ÿงฟ",
14
+ Fruits: "๐ŸŽ",
15
+ Seeds: "๐ŸŒฑ",
16
+ Flags: "๐Ÿšฉ",
17
+ Glitch: "๐Ÿ‘พ",
18
+ Achievements: "๐Ÿ†",
19
+ Boosts: "๐Ÿš€",
20
+ Upgrades: "โฌ†๏ธ",
21
+ Relics: "๐Ÿ—ฟ",
22
+ Ranks: "๐Ÿ“ˆ",
23
+ RandomEvents: "๐ŸŽฒ",
24
+ SecretRooms: "๐Ÿ”’",
25
+ Zones: "uq",
26
+ World: "๐ŸŒ",
27
+ Clan: "๐Ÿ›ก๏ธ",
28
+ Mastery: "๐ŸŽ“",
29
+ };
@@ -0,0 +1,62 @@
1
+ import React, { createContext, useContext, useState, useCallback, ReactNode } from 'react';
2
+ import { PetSimulator99API, Collections, CollectionName } from "ps99-api";
3
+
4
+ const api = new PetSimulator99API();
5
+
6
+ type CollectionDataCache = Partial<Record<string, Collections[]>>;
7
+
8
+ interface CollectionDataContextType {
9
+ data: CollectionDataCache;
10
+ fetchCollection: (collectionName: CollectionName) => Promise<void>;
11
+ isLoading: (collectionName: string) => boolean;
12
+ }
13
+
14
+ const CollectionDataContext = createContext<CollectionDataContextType | undefined>(undefined);
15
+
16
+ export const CollectionDataProvider: React.FC<{ children: ReactNode }> = ({ children }) => {
17
+ const [data, setData] = useState<CollectionDataCache>({});
18
+ const [loadingStates, setLoadingStates] = useState<Record<string, boolean>>({});
19
+
20
+ const fetchCollection = useCallback(async (collectionName: CollectionName) => {
21
+ // If data already exists, don't fetch again unless force refresh is needed (not implemented yet)
22
+ if (data[collectionName]) {
23
+ return;
24
+ }
25
+
26
+ if (loadingStates[collectionName]) return; // Already loading
27
+
28
+ setLoadingStates(prev => ({ ...prev, [collectionName]: true }));
29
+
30
+ try {
31
+ console.log(`[CollectionDataContext] Fetching ${collectionName}...`);
32
+ const result = await api.getCollection(collectionName);
33
+ if (result.status === "ok") {
34
+ setData(prev => ({ ...prev, [collectionName]: result.data as Collections[] }));
35
+ } else {
36
+ console.error(`Status error fetching ${collectionName}:`, result);
37
+ }
38
+ } catch (error) {
39
+ console.error(`Failed to fetch collection ${collectionName}`, error);
40
+ } finally {
41
+ setLoadingStates(prev => ({ ...prev, [collectionName]: false }));
42
+ }
43
+ }, [data, loadingStates]);
44
+
45
+ const isLoading = useCallback((collectionName: string) => {
46
+ return !!loadingStates[collectionName];
47
+ }, [loadingStates]);
48
+
49
+ return (
50
+ <CollectionDataContext.Provider value={{ data, fetchCollection, isLoading }}>
51
+ {children}
52
+ </CollectionDataContext.Provider>
53
+ );
54
+ };
55
+
56
+ export const useCollectionData = () => {
57
+ const context = useContext(CollectionDataContext);
58
+ if (!context) {
59
+ throw new Error('useCollectionData must be used within a CollectionDataProvider');
60
+ }
61
+ return context;
62
+ };
@@ -0,0 +1,38 @@
1
+ import { useState, useCallback, useEffect } from 'react';
2
+
3
+ export const useExpandableList = (totalItems: number, initialExpanded: boolean = false) => {
4
+ const [expandedIndices, setExpandedIndices] = useState<Set<number>>(new Set());
5
+
6
+ // Initialize state when totalItems changes or on mount
7
+ useEffect(() => {
8
+ if (initialExpanded) {
9
+ setExpandedIndices(new Set(Array.from({ length: totalItems }, (_, i) => i)));
10
+ } else {
11
+ setExpandedIndices(new Set());
12
+ }
13
+ }, [totalItems, initialExpanded]);
14
+
15
+ const toggle = useCallback((index: number) => {
16
+ setExpandedIndices(prev => {
17
+ const next = new Set(prev);
18
+ if (next.has(index)) {
19
+ next.delete(index);
20
+ } else {
21
+ next.add(index);
22
+ }
23
+ return next;
24
+ });
25
+ }, []);
26
+
27
+ const expandAll = useCallback(() => {
28
+ setExpandedIndices(new Set(Array.from({ length: totalItems }, (_, i) => i)));
29
+ }, [totalItems]);
30
+
31
+ const collapseAll = useCallback(() => {
32
+ setExpandedIndices(new Set());
33
+ }, []);
34
+
35
+ const isExpanded = useCallback((index: number) => expandedIndices.has(index), [expandedIndices]);
36
+
37
+ return { expandedIndices, toggle, expandAll, collapseAll, isExpanded };
38
+ };