@rpg-engine/long-bow 0.8.161 → 0.8.163

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,11 +1,16 @@
1
1
  import { IItemPack, IPurchase, IProductBlueprint, ItemRarities, ItemSubType, ItemType, UserAccountTypes, PaymentCurrency, PurchaseType } from '@rpg-engine/shared';
2
+ import { Box } from 'pixelarticons/react/Box';
3
+ import { Crown } from 'pixelarticons/react/Crown';
4
+ import { Gift } from 'pixelarticons/react/Gift';
5
+ import { Wallet } from 'pixelarticons/react/Wallet';
2
6
  import React, { ReactNode, useMemo, useState } from 'react';
3
7
  import { FaHistory, FaShoppingCart, FaWallet } from 'react-icons/fa';
4
8
  import styled from 'styled-components';
5
9
  import { uiColors } from '../../constants/uiColors';
6
10
  import { DraggableContainer } from '../DraggableContainer';
7
- import { InternalTabs } from '../InternalTabs/InternalTabs';
8
11
  import { RPGUIContainerTypes } from '../RPGUI/RPGUIContainer';
12
+ import { Tabs } from '../shared/Tabs';
13
+ import { LabelPill } from '../shared/LabelPill/LabelPill';
9
14
  import { CTAButton } from '../shared/CTAButton/CTAButton';
10
15
  import { CartView } from './CartView';
11
16
  import { useStoreCart } from './hooks/useStoreCart';
@@ -38,6 +43,7 @@ export interface IStoreProps {
38
43
  textInputItemKeys?: string[];
39
44
  customPacksContent?: React.ReactNode;
40
45
  customWalletContent?: React.ReactNode;
46
+ packsBadge?: string;
41
47
  }
42
48
 
43
49
  export const Store: React.FC<IStoreProps> = ({
@@ -60,6 +66,7 @@ export const Store: React.FC<IStoreProps> = ({
60
66
  customPacksContent,
61
67
  customWalletContent,
62
68
  packsTabLabel = 'Packs',
69
+ packsBadge,
63
70
  }) => {
64
71
  const [selectedPack, setSelectedPack] = useState<IItemPack | null>(null);
65
72
  const [activeTab, setActiveTab] = useState<TabId>(() => {
@@ -83,7 +90,7 @@ export const Store: React.FC<IStoreProps> = ({
83
90
  const [isCollectingMetadata, setIsCollectingMetadata] = useState(false);
84
91
  const [currentMetadataItem, setCurrentMetadataItem] = useState<IProductBlueprint | null>(null);
85
92
 
86
- const handleAddPackToCart = (pack: IItemPack) => {
93
+ const handleAddPackToCart = (pack: IItemPack, quantity: number = 1) => {
87
94
  const packItem: IProductBlueprint = {
88
95
  key: pack.key,
89
96
  name: pack.title,
@@ -101,7 +108,7 @@ export const Store: React.FC<IStoreProps> = ({
101
108
  maxStackSize: 1,
102
109
  isUsable: false,
103
110
  };
104
- handleAddToCart(packItem, 1);
111
+ handleAddToCart(packItem, quantity);
105
112
  };
106
113
 
107
114
  const filterItems = (
@@ -159,10 +166,11 @@ export const Store: React.FC<IStoreProps> = ({
159
166
  const tabIds: TabId[] = tabOrder ?? ['premium', 'packs', 'items'];
160
167
  const availableTabIds: TabId[] = tabIds.filter(id => !(hidePremiumTab && id === 'premium'));
161
168
 
162
- const tabsMap: Record<string, { id: TabId; title: string; content: ReactNode }> = {
169
+ const tabsMap: Record<string, { id: TabId; title: ReactNode; icon: ReactNode; content: ReactNode }> = {
163
170
  premium: {
164
171
  id: 'premium',
165
172
  title: 'Premium',
173
+ icon: <Crown width={18} height={18} />,
166
174
  content: (
167
175
  <StorePacksSection
168
176
  packs={packs.filter(pack => pack.priceUSD >= 9.99)}
@@ -175,7 +183,13 @@ export const Store: React.FC<IStoreProps> = ({
175
183
  },
176
184
  packs: {
177
185
  id: 'packs',
178
- title: packsTabLabel,
186
+ title: packsBadge ? (
187
+ <TabLabelWithBadge>
188
+ {packsTabLabel}
189
+ <LabelPill background="#f59e0b" borderColor="#f59e0b" color="#000">{packsBadge}</LabelPill>
190
+ </TabLabelWithBadge>
191
+ ) : packsTabLabel,
192
+ icon: <Gift width={18} height={18} />,
179
193
  content: customPacksContent ?? (
180
194
  <StorePacksSection
181
195
  packs={hidePremiumTab ? packs : packs.filter(pack => pack.priceUSD < 9.99)}
@@ -189,6 +203,7 @@ export const Store: React.FC<IStoreProps> = ({
189
203
  items: {
190
204
  id: 'items',
191
205
  title: 'Items',
206
+ icon: <Box width={18} height={18} />,
192
207
  content: (
193
208
  <StoreItemsSection
194
209
  items={filteredItems.items}
@@ -203,12 +218,11 @@ export const Store: React.FC<IStoreProps> = ({
203
218
  wallet: {
204
219
  id: 'wallet',
205
220
  title: 'Wallet',
221
+ icon: <Wallet width={18} height={18} />,
206
222
  content: customWalletContent ?? null,
207
223
  },
208
224
  };
209
225
 
210
- const tabs = availableTabIds.map(id => tabsMap[id]);
211
-
212
226
  return (
213
227
  <DraggableContainer
214
228
  title="Store"
@@ -276,16 +290,14 @@ export const Store: React.FC<IStoreProps> = ({
276
290
  </CartButton>
277
291
  </TopBar>
278
292
  <MainContent>
279
- <InternalTabs
280
- tabs={tabs}
281
- activeTextColor="#000000"
282
- activeColor="#fef08a"
283
- inactiveColor="#6b7280"
284
- borderColor="#f59e0b"
285
- hoverColor="#fef3c7"
286
- activeTab={activeTab}
287
- onTabChange={(tabId: string) => setActiveTab(tabId as TabId)}
293
+ <Tabs
294
+ options={availableTabIds.map(id => ({ id, label: tabsMap[id].title, icon: tabsMap[id].icon }))}
295
+ activeTabId={activeTab}
296
+ onTabChange={(tabId) => setActiveTab(tabId as TabId)}
288
297
  />
298
+ <TabContent>
299
+ {tabsMap[activeTab]?.content}
300
+ </TabContent>
289
301
  </MainContent>
290
302
  {cartItems.length > 0 && (
291
303
  <Footer>
@@ -349,12 +361,12 @@ const MainContent = styled.div`
349
361
  flex-direction: column;
350
362
  min-height: 0;
351
363
  overflow: hidden;
364
+ `;
352
365
 
353
- .rpgui-tabs-content {
354
- flex: 1;
355
- overflow-y: auto;
356
- padding-right: 0.5rem;
357
- }
366
+ const TabContent = styled.div`
367
+ flex: 1;
368
+ overflow-y: auto;
369
+ padding-right: 0.5rem;
358
370
  `;
359
371
 
360
372
  const Footer = styled.div`
@@ -386,6 +398,13 @@ const CartInfo = styled.div`
386
398
  }
387
399
  `;
388
400
 
401
+ const TabLabelWithBadge = styled.span`
402
+ display: inline-flex;
403
+ align-items: center;
404
+ gap: 5px;
405
+ `;
406
+
407
+
389
408
  const LoadingMessage = styled.div`
390
409
  text-align: center;
391
410
  color: ${uiColors.white};
@@ -4,6 +4,7 @@ import { FaCartPlus } from 'react-icons/fa';
4
4
  import styled from 'styled-components';
5
5
  import { SelectArrow } from '../Arrow/SelectArrow';
6
6
  import { CTAButton } from '../shared/CTAButton/CTAButton';
7
+ import { ItemRowWrapper } from '../shared/ItemRowWrapper';
7
8
  import { SpriteFromAtlas } from '../shared/SpriteFromAtlas';
8
9
  import { useQuantityControl } from '../../hooks/useQuantityControl';
9
10
 
@@ -56,27 +57,29 @@ export const StoreItemRow: React.FC<IStoreItemRowProps> = ({
56
57
  };
57
58
 
58
59
  return (
59
- <ItemWrapper $isHighlighted={item.store?.isHighlighted || false}>
60
- <ItemIconContainer>
61
- <SpriteFromAtlas
62
- atlasJSON={atlasJSON}
63
- atlasIMG={atlasIMG}
64
- spriteKey={item.texturePath}
65
- width={32}
66
- height={32}
67
- imgScale={2}
68
- centered
69
- />
70
- </ItemIconContainer>
71
-
72
- <ItemDetails>
73
- <ItemName>{item.name}</ItemName>
74
- <ItemPrice>
75
- ${item.price}
76
- {(item as any).dcPrice ? ` · ${((item as any).dcPrice as number).toLocaleString()} DC` : ''}
77
- </ItemPrice>
78
- <ItemDescription>{item.description}</ItemDescription>
79
- </ItemDetails>
60
+ <ItemRowWrapper $isHighlighted={item.store?.isHighlighted || false}>
61
+ <LeftSection>
62
+ <ItemIconContainer>
63
+ <SpriteFromAtlas
64
+ atlasJSON={atlasJSON}
65
+ atlasIMG={atlasIMG}
66
+ spriteKey={item.texturePath}
67
+ width={32}
68
+ height={32}
69
+ imgScale={2}
70
+ centered
71
+ />
72
+ </ItemIconContainer>
73
+
74
+ <ItemDetails>
75
+ <ItemName>{item.name}</ItemName>
76
+ <ItemPrice>
77
+ ${item.price}
78
+ {(item as any).dcPrice ? ` · ${((item as any).dcPrice as number).toLocaleString()} DC` : ''}
79
+ </ItemPrice>
80
+ <ItemDescription>{item.description}</ItemDescription>
81
+ </ItemDetails>
82
+ </LeftSection>
80
83
 
81
84
  <Controls>
82
85
  {/* Show text input if configured, else show arrows only for stackable items */}
@@ -121,24 +124,16 @@ export const StoreItemRow: React.FC<IStoreItemRowProps> = ({
121
124
  disabled={!hasRequiredAccount}
122
125
  />
123
126
  </Controls>
124
- </ItemWrapper>
127
+ </ItemRowWrapper>
125
128
  );
126
129
  };
127
130
 
128
- const ItemWrapper = styled.div<{ $isHighlighted: boolean }>`
131
+ const LeftSection = styled.div`
129
132
  display: flex;
130
133
  align-items: center;
131
134
  gap: 0.75rem;
132
- padding: 0.5rem 1rem;
133
- border-bottom: 1px solid rgba(255, 255, 255, 0.1);
134
- background: ${props =>
135
- props.$isHighlighted ? 'rgba(255, 215, 0, 0.1)' : 'transparent'};
136
- border-left: ${props =>
137
- props.$isHighlighted ? '3px solid #ffd700' : '3px solid transparent'};
138
-
139
- &:last-child {
140
- border-bottom: none;
141
- }
135
+ flex: 1;
136
+ min-width: 0;
142
137
  `;
143
138
 
144
139
  const ItemIconContainer = styled.div`
@@ -9,7 +9,7 @@ import styled from 'styled-components';
9
9
  import { ScrollableContent } from '../../shared/ScrollableContent/ScrollableContent';
10
10
  import { StoreCharacterSkinRow } from '../StoreCharacterSkinRow';
11
11
  import { StoreItemRow } from '../StoreItemRow';
12
- import { Dropdown } from '../../Dropdown';
12
+ import { SegmentedToggle } from '../../shared/SegmentedToggle';
13
13
  import { SearchBar } from '../../shared/SearchBar/SearchBar';
14
14
  import { useStoreFiltering } from '../../../hooks/useStoreFiltering';
15
15
 
@@ -37,6 +37,7 @@ export const StoreItemsSection: React.FC<IStoreItemsSectionProps> = ({
37
37
  const {
38
38
  searchQuery,
39
39
  setSearchQuery,
40
+ selectedCategory,
40
41
  setSelectedCategory,
41
42
  categoryOptions,
42
43
  filteredItems,
@@ -93,13 +94,11 @@ export const StoreItemsSection: React.FC<IStoreItemsSectionProps> = ({
93
94
  placeholder="Search items..."
94
95
  />
95
96
  </SearchBarContainer>
96
- <DropdownContainer>
97
- <Dropdown
98
- options={categoryOptions}
99
- onChange={value => setSelectedCategory(value as ItemType | 'all')}
100
- width="100%"
101
- />
102
- </DropdownContainer>
97
+ <SegmentedToggle
98
+ options={categoryOptions.map(opt => ({ id: opt.value, label: opt.option }))}
99
+ activeId={selectedCategory}
100
+ onChange={id => setSelectedCategory(id as ItemType | 'all')}
101
+ />
103
102
  </SearchHeader>
104
103
 
105
104
  <ScrollableContent
@@ -130,8 +129,3 @@ const SearchHeader = styled.div`
130
129
  const SearchBarContainer = styled.div`
131
130
  flex: 0.75;
132
131
  `;
133
-
134
- const DropdownContainer = styled.div`
135
- flex: 0.25;
136
- min-width: 140px;
137
- `;
@@ -3,22 +3,76 @@ import React, { useCallback } from 'react';
3
3
  import { FaCartPlus } from 'react-icons/fa';
4
4
  import styled from 'styled-components';
5
5
  import { CTAButton } from '../../shared/CTAButton/CTAButton';
6
+ import { ItemRowWrapper } from '../../shared/ItemRowWrapper';
6
7
  import { SpriteFromAtlas } from '../../shared/SpriteFromAtlas';
7
8
  import { ScrollableContent } from '../../shared/ScrollableContent/ScrollableContent';
9
+ import { SelectArrow } from '../../Arrow/SelectArrow';
8
10
  import { usePackFiltering } from '../../../hooks/usePackFiltering';
11
+ import { useQuantityControl } from '../../../hooks/useQuantityControl';
9
12
 
10
13
  interface IStorePacksSectionProps {
11
14
  packs: IItemPack[];
12
- onAddToCart: (pack: IItemPack) => void;
15
+ onAddToCart: (pack: IItemPack, quantity: number) => void;
13
16
  onSelectPack?: (pack: IItemPack) => void;
14
17
  atlasJSON?: any;
15
18
  atlasIMG?: string;
16
19
  }
17
20
 
21
+ interface IPackRowItemProps {
22
+ pack: IItemPack;
23
+ onAddToCart: (pack: IItemPack, quantity: number) => void;
24
+ renderPackIcon: (pack: IItemPack) => React.ReactNode;
25
+ }
26
+
27
+ const PackRowItem: React.FC<IPackRowItemProps> = ({ pack, onAddToCart, renderPackIcon }) => {
28
+ const { quantity, handleQuantityChange, handleBlur, incrementQuantity, decrementQuantity, resetQuantity } = useQuantityControl();
29
+
30
+ const handleAdd = () => {
31
+ onAddToCart(pack, quantity);
32
+ resetQuantity();
33
+ };
34
+
35
+ return (
36
+ <PackRow>
37
+ <LeftSection>
38
+ <PackIconContainer>
39
+ {renderPackIcon(pack)}
40
+ </PackIconContainer>
41
+
42
+ <PackDetails>
43
+ <PackName>{pack.title}</PackName>
44
+ <PackPrice>${pack.priceUSD}</PackPrice>
45
+ {pack.description && <PackDescription>{pack.description}</PackDescription>}
46
+ </PackDetails>
47
+ </LeftSection>
48
+
49
+ <Controls>
50
+ <ArrowsContainer>
51
+ <SelectArrow direction="left" onPointerDown={decrementQuantity} size={24} />
52
+ <QuantityInput
53
+ type="number"
54
+ value={quantity}
55
+ onChange={handleQuantityChange}
56
+ onBlur={handleBlur}
57
+ min={1}
58
+ max={99}
59
+ className="rpgui-input"
60
+ />
61
+ <SelectArrow direction="right" onPointerDown={incrementQuantity} size={24} />
62
+ </ArrowsContainer>
63
+ <CTAButton
64
+ icon={<FaCartPlus />}
65
+ label="Add"
66
+ onClick={handleAdd}
67
+ />
68
+ </Controls>
69
+ </PackRow>
70
+ );
71
+ };
72
+
18
73
  export const StorePacksSection: React.FC<IStorePacksSectionProps> = ({
19
74
  packs,
20
75
  onAddToCart,
21
- onSelectPack,
22
76
  atlasJSON,
23
77
  atlasIMG,
24
78
  }) => {
@@ -42,30 +96,14 @@ export const StorePacksSection: React.FC<IStorePacksSectionProps> = ({
42
96
 
43
97
  const renderPack = useCallback(
44
98
  (pack: IItemPack) => (
45
- <PackRow key={pack.key} onClick={() => onSelectPack?.(pack)}>
46
- <PackIconContainer>
47
- {renderPackIcon(pack)}
48
- </PackIconContainer>
49
-
50
- <PackDetails>
51
- <PackName>{pack.title}</PackName>
52
- <PackPrice>${pack.priceUSD}</PackPrice>
53
- {pack.description && <PackDescription>{pack.description}</PackDescription>}
54
- </PackDetails>
55
-
56
- <Controls>
57
- <CTAButton
58
- icon={<FaCartPlus />}
59
- label="Add"
60
- onClick={e => {
61
- e.stopPropagation();
62
- onAddToCart(pack);
63
- }}
64
- />
65
- </Controls>
66
- </PackRow>
99
+ <PackRowItem
100
+ key={pack.key}
101
+ pack={pack}
102
+ onAddToCart={onAddToCart}
103
+ renderPackIcon={renderPackIcon}
104
+ />
67
105
  ),
68
- [onSelectPack, onAddToCart]
106
+ [onAddToCart, renderPackIcon]
69
107
  );
70
108
 
71
109
  return (
@@ -84,21 +122,14 @@ export const StorePacksSection: React.FC<IStorePacksSectionProps> = ({
84
122
  );
85
123
  };
86
124
 
87
- const PackRow = styled.div`
125
+ const PackRow = styled(ItemRowWrapper)``;
126
+
127
+ const LeftSection = styled.div`
88
128
  display: flex;
89
129
  align-items: center;
90
130
  gap: 0.75rem;
91
- padding: 0.5rem 1rem;
92
- border-bottom: 1px solid rgba(255, 255, 255, 0.1);
93
- cursor: pointer;
94
-
95
- &:last-child {
96
- border-bottom: none;
97
- }
98
-
99
- &:hover {
100
- background: rgba(255, 255, 255, 0.04);
101
- }
131
+ flex: 1;
132
+ min-width: 0;
102
133
  `;
103
134
 
104
135
  const PackIconContainer = styled.div`
@@ -146,5 +177,32 @@ const PackDescription = styled.div`
146
177
  const Controls = styled.div`
147
178
  display: flex;
148
179
  align-items: center;
180
+ gap: 0.5rem;
149
181
  flex-shrink: 0;
150
182
  `;
183
+
184
+ const ArrowsContainer = styled.div`
185
+ position: relative;
186
+ display: flex;
187
+ align-items: center;
188
+ width: 120px;
189
+ height: 42px;
190
+ justify-content: space-between;
191
+ `;
192
+
193
+ const QuantityInput = styled.input`
194
+ width: 40px;
195
+ text-align: center;
196
+ margin: 0 auto;
197
+ font-size: 0.875rem;
198
+ background: rgba(0, 0, 0, 0.2);
199
+ color: #ffffff;
200
+ border: none;
201
+ padding: 0.25rem;
202
+
203
+ &::-webkit-inner-spin-button,
204
+ &::-webkit-outer-spin-button {
205
+ -webkit-appearance: none;
206
+ margin: 0;
207
+ }
208
+ `;
@@ -0,0 +1,22 @@
1
+ import styled from 'styled-components';
2
+
3
+ export const ItemRowWrapper = styled.div<{ $isHighlighted?: boolean }>`
4
+ display: flex;
5
+ align-items: center;
6
+ justify-content: space-between;
7
+ padding: 0.6rem 1rem;
8
+ margin-bottom: 4px;
9
+ background: ${p => p.$isHighlighted ? 'rgba(255, 215, 0, 0.08)' : 'rgba(0, 0, 0, 0.25)'};
10
+ border: 1px solid rgba(255, 255, 255, 0.05);
11
+ border-radius: 6px;
12
+ border-left: 4px solid ${p => p.$isHighlighted ? '#ffd700' : 'transparent'};
13
+ transition: all 0.2s ease-in-out;
14
+
15
+ &:hover {
16
+ background: rgba(245, 158, 11, 0.08);
17
+ border-color: rgba(245, 158, 11, 0.2);
18
+ border-left-color: #f59e0b;
19
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
20
+ transform: translateY(-1px);
21
+ }
22
+ `;
@@ -237,8 +237,9 @@ export const Default: Story = {
237
237
  atlasIMG={itemsAtlasIMG}
238
238
  hidePremiumTab={true}
239
239
  tabOrder={['items', 'packs']}
240
- defaultActiveTab="items"
240
+ defaultActiveTab="packs"
241
241
  textInputItemKeys={['original-greater-life-potion-2', 'original-angelic-sword-1', 'character-name-change']}
242
+ packsBadge="SAVE"
242
243
  />
243
244
  ),
244
245
  };
@@ -43,13 +43,26 @@ const mockYourBuyOrders: IMarketplaceBuyOrderItem[] = [
43
43
  ];
44
44
 
45
45
  const mockOpenBuyOrders: IMarketplaceBuyOrderItem[] = [
46
+ // Abyssal Tide Staff — single request
46
47
  { _id: 'obo-1', owner: 'player-2', itemBlueprintKey: 'items/abyssal-tide-staff', itemRarity: 'Epic', maxPrice: 1500, escrowedGold: 1500, fee: 75, status: MarketplaceBuyOrderStatus.Active, createdAt: daysAgo(1), updatedAt: daysAgo(1) },
48
+ // Wooden Shield — single request
47
49
  { _id: 'obo-2', owner: 'player-3', itemBlueprintKey: 'items/wooden-shield', maxPrice: 200, escrowedGold: 200, fee: 10, status: MarketplaceBuyOrderStatus.Active, createdAt: daysAgo(5), updatedAt: daysAgo(5) },
48
- { _id: 'obo-3', owner: 'player-4', itemBlueprintKey: 'items/fire-wand', itemRarity: 'Rare', maxPrice: 800, escrowedGold: 800, fee: 40, status: MarketplaceBuyOrderStatus.Active, createdAt: daysAgo(2), updatedAt: daysAgo(2) },
49
- { _id: 'obo-4', owner: 'player-5', itemBlueprintKey: 'items/broad-sword', maxPrice: 350, escrowedGold: 350, fee: 17, status: MarketplaceBuyOrderStatus.Active, createdAt: daysAgo(3), updatedAt: daysAgo(3) },
50
+ // Fire Wand 2 requests (grouped)
51
+ { _id: 'obo-3a', owner: 'player-4', itemBlueprintKey: 'items/fire-wand', itemRarity: 'Rare', maxPrice: 800, escrowedGold: 800, fee: 40, status: MarketplaceBuyOrderStatus.Active, createdAt: daysAgo(2), updatedAt: daysAgo(2) },
52
+ { _id: 'obo-3b', owner: 'player-10', itemBlueprintKey: 'items/fire-wand', maxPrice: 550, escrowedGold: 550, fee: 27, status: MarketplaceBuyOrderStatus.Active, createdAt: daysAgo(4), updatedAt: daysAgo(4) },
53
+ // Broad Sword — 3 requests (grouped)
54
+ { _id: 'obo-4a', owner: 'player-5', itemBlueprintKey: 'items/broad-sword', maxPrice: 350, escrowedGold: 350, fee: 17, status: MarketplaceBuyOrderStatus.Active, createdAt: daysAgo(3), updatedAt: daysAgo(3) },
55
+ { _id: 'obo-4b', owner: 'player-11', itemBlueprintKey: 'items/broad-sword', itemRarity: 'Rare', maxPrice: 480, escrowedGold: 480, fee: 24, status: MarketplaceBuyOrderStatus.Active, createdAt: daysAgo(1), updatedAt: daysAgo(1) },
56
+ { _id: 'obo-4c', owner: 'player-12', itemBlueprintKey: 'items/broad-sword', maxPrice: 270, escrowedGold: 270, fee: 13, status: MarketplaceBuyOrderStatus.Active, createdAt: daysAgo(6), updatedAt: daysAgo(6) },
57
+ // Barbarian Helmet — single request
50
58
  { _id: 'obo-5', owner: 'player-6', itemBlueprintKey: 'items/barbarian-helmet', itemRarity: 'Uncommon', maxPrice: 600, escrowedGold: 600, fee: 30, status: MarketplaceBuyOrderStatus.Active, createdAt: daysAgo(1), updatedAt: daysAgo(1) },
51
- { _id: 'obo-6', owner: 'player-7', itemBlueprintKey: 'items/leather-armor', maxPrice: 420, escrowedGold: 420, fee: 21, status: MarketplaceBuyOrderStatus.Active, createdAt: daysAgo(4), updatedAt: daysAgo(4) },
52
- { _id: 'obo-7', owner: 'player-8', itemBlueprintKey: 'items/angelic-sword', itemRarity: 'Legendary', maxPrice: 5000, escrowedGold: 5000, fee: 250, status: MarketplaceBuyOrderStatus.Active, createdAt: daysAgo(6), updatedAt: daysAgo(6) },
59
+ // Leather Armor 2 requests (grouped)
60
+ { _id: 'obo-6a', owner: 'player-7', itemBlueprintKey: 'items/leather-armor', maxPrice: 420, escrowedGold: 420, fee: 21, status: MarketplaceBuyOrderStatus.Active, createdAt: daysAgo(4), updatedAt: daysAgo(4) },
61
+ { _id: 'obo-6b', owner: 'player-13', itemBlueprintKey: 'items/leather-armor', itemRarity: 'Uncommon', maxPrice: 380, escrowedGold: 380, fee: 19, status: MarketplaceBuyOrderStatus.Active, createdAt: daysAgo(2), updatedAt: daysAgo(2) },
62
+ // Angelic Sword — 2 requests (grouped)
63
+ { _id: 'obo-7a', owner: 'player-8', itemBlueprintKey: 'items/angelic-sword', itemRarity: 'Legendary', maxPrice: 5000, escrowedGold: 5000, fee: 250, status: MarketplaceBuyOrderStatus.Active, createdAt: daysAgo(6), updatedAt: daysAgo(6) },
64
+ { _id: 'obo-7b', owner: 'player-14', itemBlueprintKey: 'items/angelic-sword', itemRarity: 'Epic', maxPrice: 3500, escrowedGold: 3500, fee: 175, status: MarketplaceBuyOrderStatus.Active, createdAt: daysAgo(3), updatedAt: daysAgo(3) },
65
+ // Greater Life Potion — single request
53
66
  { _id: 'obo-8', owner: 'player-9', itemBlueprintKey: 'items/greater-life-potion', maxPrice: 90, stackQty: 10, escrowedGold: 900, fee: 45, status: MarketplaceBuyOrderStatus.Active, createdAt: daysAgo(2), updatedAt: daysAgo(2) },
54
67
  ];
55
68
 
@@ -169,7 +182,7 @@ const Template: Story = () => {
169
182
  onYourBuyOrdersPageChange={p => console.log('your orders page:', p)}
170
183
  onCancelBuyOrder={id => setYourBuyOrders(prev => prev.filter(o => o._id !== id))}
171
184
  openBuyOrders={mockOpenBuyOrders}
172
- openBuyOrdersTotal={mockOpenBuyOrders.length} // 8 items → pagination shows (page size = 5)
185
+ openBuyOrdersTotal={mockOpenBuyOrders.length}
173
186
  openBuyOrdersPage={1}
174
187
  onOpenBuyOrdersPageChange={(p: number) => console.log('open orders page:', p)}
175
188
  // Blueprint Search props