@rpg-engine/long-bow 0.8.136 → 0.8.137

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rpg-engine/long-bow",
3
- "version": "0.8.136",
3
+ "version": "0.8.137",
4
4
  "license": "MIT",
5
5
  "main": "dist/index.js",
6
6
  "typings": "dist/index.d.ts",
@@ -1,7 +1,6 @@
1
1
  import React from 'react';
2
2
  import styled from 'styled-components';
3
3
  import { uiColors } from '../../constants/uiColors';
4
- import { Dropdown, IOptionsProps } from '../Dropdown';
5
4
  import { Pagination } from '../shared/Pagination/Pagination';
6
5
 
7
6
  export interface IDCTransaction {
@@ -17,24 +16,24 @@ export interface IDCTransaction {
17
16
  const TRANSACTION_TYPE_LABELS: Record<string, string> = {
18
17
  Purchase: 'Purchase',
19
18
  Transfer: 'Transfer',
20
- MarketplaceSale: 'Marketplace Sale',
21
- MarketplacePurchase: 'Marketplace Buy',
22
- StorePurchase: 'Store Purchase',
19
+ MarketplaceSale: 'Mkt Sale',
20
+ MarketplacePurchase: 'Mkt Buy',
21
+ StorePurchase: 'Store',
23
22
  Fee: 'Fee',
24
23
  Refund: 'Refund',
25
- AdminAdjustment: 'Admin Adjustment',
24
+ AdminAdjustment: 'Admin',
26
25
  };
27
26
 
28
- const TRANSACTION_TYPE_OPTIONS: IOptionsProps[] = [
29
- { id: 0, value: '', option: 'All Types' },
30
- { id: 1, value: 'Purchase', option: 'Purchase' },
31
- { id: 2, value: 'Transfer', option: 'Transfer' },
32
- { id: 3, value: 'MarketplaceSale', option: 'Marketplace Sale' },
33
- { id: 4, value: 'MarketplacePurchase', option: 'Marketplace Buy' },
34
- { id: 5, value: 'StorePurchase', option: 'Store Purchase' },
35
- { id: 6, value: 'Fee', option: 'Fee' },
36
- { id: 7, value: 'Refund', option: 'Refund' },
37
- { id: 8, value: 'AdminAdjustment', option: 'Admin Adjustment' },
27
+ const TRANSACTION_TYPE_OPTIONS = [
28
+ { value: '', label: 'All Types' },
29
+ { value: 'Purchase', label: 'Purchase' },
30
+ { value: 'Transfer', label: 'Transfer' },
31
+ { value: 'MarketplaceSale', label: 'Marketplace Sale' },
32
+ { value: 'MarketplacePurchase', label: 'Marketplace Buy' },
33
+ { value: 'StorePurchase', label: 'Store Purchase' },
34
+ { value: 'Fee', label: 'Fee' },
35
+ { value: 'Refund', label: 'Refund' },
36
+ { value: 'AdminAdjustment', label: 'Admin Adjustment' },
38
37
  ];
39
38
 
40
39
  export interface IDCHistoryPanelProps {
@@ -54,7 +53,14 @@ export const DCHistoryPanel: React.FC<IDCHistoryPanelProps> = ({
54
53
  }) => {
55
54
  const [selectedType, setSelectedType] = React.useState<string>('');
56
55
 
57
- const handleTypeChange = (value: string): void => {
56
+ // Auto-load on first mount (when History tab is opened)
57
+ React.useEffect(() => {
58
+ onRequestHistory(1);
59
+ // eslint-disable-next-line react-hooks/exhaustive-deps
60
+ }, []);
61
+
62
+ const handleTypeChange = (e: React.ChangeEvent<HTMLSelectElement>): void => {
63
+ const value = e.target.value;
58
64
  setSelectedType(value);
59
65
  onRequestHistory(1, value || undefined);
60
66
  };
@@ -65,15 +71,18 @@ export const DCHistoryPanel: React.FC<IDCHistoryPanelProps> = ({
65
71
 
66
72
  const formatDate = (dateStr: string): string => {
67
73
  const d = new Date(dateStr);
68
- const months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
69
- return `${months[d.getMonth()]} ${d.getDate()}, ${d.getFullYear()}`;
74
+ return `${d.getMonth() + 1}/${d.getDate()}/${String(d.getFullYear()).slice(-2)}`;
70
75
  };
71
76
 
72
77
  return (
73
78
  <PanelContainer>
74
79
  <FilterRow>
75
80
  <FilterLabel>Filter:</FilterLabel>
76
- <Dropdown options={TRANSACTION_TYPE_OPTIONS} onChange={handleTypeChange} width="200px" />
81
+ <TypeSelect value={selectedType} onChange={handleTypeChange}>
82
+ {TRANSACTION_TYPE_OPTIONS.map((opt) => (
83
+ <option key={opt.value} value={opt.value}>{opt.label}</option>
84
+ ))}
85
+ </TypeSelect>
77
86
  </FilterRow>
78
87
 
79
88
  {loading && (
@@ -92,18 +101,20 @@ export const DCHistoryPanel: React.FC<IDCHistoryPanelProps> = ({
92
101
  {transactions.map((tx) => {
93
102
  const isCredit = tx.amount > 0;
94
103
  const label = TRANSACTION_TYPE_LABELS[tx.type] ?? tx.type;
104
+ const subtitle = tx.note ?? (tx.relatedCharacterName ? tx.relatedCharacterName : '');
95
105
 
96
106
  return (
97
107
  <TransactionRow key={tx._id}>
98
- <TxType>{label}</TxType>
99
- <TxAmount $credit={isCredit}>
100
- {isCredit ? '+' : ''}{tx.amount} DC
101
- </TxAmount>
102
- <TxNote>
103
- {tx.note ?? (tx.relatedCharacterName ? `With: ${tx.relatedCharacterName}` : '')}
104
- </TxNote>
105
- <TxDate>{formatDate(tx.createdAt)}</TxDate>
106
- <TxBalance>Balance: {tx.balanceAfter} DC</TxBalance>
108
+ <TxLeft>
109
+ <TxType>{label}</TxType>
110
+ {subtitle ? <TxNote>{subtitle}</TxNote> : null}
111
+ </TxLeft>
112
+ <TxRight>
113
+ <TxAmount $credit={isCredit}>
114
+ {isCredit ? '+' : ''}{tx.amount} DC
115
+ </TxAmount>
116
+ <TxDate>{formatDate(tx.createdAt)}</TxDate>
117
+ </TxRight>
107
118
  </TransactionRow>
108
119
  );
109
120
  })}
@@ -132,76 +143,115 @@ const FilterRow = styled.div`
132
143
  display: flex;
133
144
  align-items: center;
134
145
  gap: 8px;
135
- margin-bottom: 4px;
146
+ margin-bottom: 2px;
136
147
  `;
137
148
 
138
149
  const FilterLabel = styled.span`
139
- font-size: 8px;
150
+ font-size: 7px;
140
151
  color: #f59e0b;
141
152
  font-family: 'Press Start 2P', cursive;
142
153
  white-space: nowrap;
143
154
  `;
144
155
 
156
+ const TypeSelect = styled.select`
157
+ background: rgba(0, 0, 0, 0.7);
158
+ border: 1px solid rgba(245, 158, 11, 0.4);
159
+ border-radius: 3px;
160
+ color: #f59e0b;
161
+ font-size: 7px;
162
+ font-family: 'Press Start 2P', cursive;
163
+ padding: 3px 5px;
164
+ cursor: pointer;
165
+ outline: none;
166
+ flex: 1;
167
+
168
+ option {
169
+ background: #1a1a2e;
170
+ color: #f59e0b;
171
+ }
172
+
173
+ &:hover {
174
+ border-color: #f59e0b;
175
+ }
176
+
177
+ &:focus {
178
+ border-color: #f59e0b;
179
+ box-shadow: 0 0 0 1px rgba(245, 158, 11, 0.3);
180
+ }
181
+ `;
182
+
145
183
  const TransactionList = styled.div`
146
184
  display: flex;
147
185
  flex-direction: column;
148
- gap: 4px;
149
- max-height: 280px;
186
+ gap: 3px;
187
+ max-height: 260px;
150
188
  overflow-y: auto;
189
+
190
+ &::-webkit-scrollbar {
191
+ width: 4px;
192
+ }
193
+ &::-webkit-scrollbar-track {
194
+ background: rgba(0, 0, 0, 0.3);
195
+ }
196
+ &::-webkit-scrollbar-thumb {
197
+ background: rgba(245, 158, 11, 0.4);
198
+ border-radius: 2px;
199
+ }
151
200
  `;
152
201
 
153
202
  const TransactionRow = styled.div`
154
- background: rgba(0, 0, 0, 0.4);
155
- border: 1px solid rgba(255, 215, 0, 0.2);
156
- padding: 8px;
157
- display: grid;
158
- grid-template-columns: 1fr auto;
159
- grid-template-rows: auto auto auto;
203
+ background: rgba(0, 0, 0, 0.35);
204
+ border: 1px solid rgba(245, 158, 11, 0.15);
205
+ border-radius: 2px;
206
+ padding: 5px 7px;
207
+ display: flex;
208
+ align-items: center;
209
+ gap: 8px;
210
+ `;
211
+
212
+ const TxLeft = styled.div`
213
+ flex: 1;
214
+ min-width: 0;
215
+ display: flex;
216
+ flex-direction: column;
217
+ gap: 2px;
218
+ `;
219
+
220
+ const TxRight = styled.div`
221
+ display: flex;
222
+ flex-direction: column;
223
+ align-items: flex-end;
160
224
  gap: 2px;
225
+ flex-shrink: 0;
161
226
  `;
162
227
 
163
228
  const TxType = styled.span`
164
- font-size: 8px;
229
+ font-size: 7px;
165
230
  color: #f59e0b;
166
231
  font-family: 'Press Start 2P', cursive;
167
- grid-column: 1;
168
- grid-row: 1;
169
232
  `;
170
233
 
171
234
  const TxAmount = styled.span<{ $credit: boolean }>`
172
- font-size: 10px;
235
+ font-size: 8px;
173
236
  font-family: 'Press Start 2P', cursive;
174
237
  color: ${({ $credit }) => ($credit ? uiColors.green : uiColors.red)};
175
- font-weight: bold;
176
- grid-column: 2;
177
- grid-row: 1;
178
- text-align: right;
238
+ white-space: nowrap;
179
239
  `;
180
240
 
181
241
  const TxNote = styled.span`
182
- font-size: 7px;
242
+ font-size: 6px;
183
243
  color: ${uiColors.lightGray};
184
244
  font-family: 'Press Start 2P', cursive;
185
- grid-column: 1;
186
- grid-row: 2;
245
+ white-space: nowrap;
246
+ overflow: hidden;
247
+ text-overflow: ellipsis;
187
248
  `;
188
249
 
189
250
  const TxDate = styled.span`
190
- font-size: 7px;
191
- color: ${uiColors.lightGray};
251
+ font-size: 6px;
252
+ color: rgba(255, 255, 255, 0.4);
192
253
  font-family: 'Press Start 2P', cursive;
193
- grid-column: 2;
194
- grid-row: 2;
195
- text-align: right;
196
- `;
197
-
198
- const TxBalance = styled.span`
199
- font-size: 7px;
200
- color: ${uiColors.white};
201
- font-family: 'Press Start 2P', cursive;
202
- grid-column: 1 / 3;
203
- grid-row: 3;
204
- opacity: 0.7;
254
+ white-space: nowrap;
205
255
  `;
206
256
 
207
257
  const LoadingRow = styled.div`
@@ -219,8 +269,8 @@ const Spinner = styled.div`
219
269
  border: 3px solid rgba(255, 255, 255, 0.2);
220
270
  border-radius: 50%;
221
271
  border-top: 3px solid #f59e0b;
222
- width: 20px;
223
- height: 20px;
272
+ width: 16px;
273
+ height: 16px;
224
274
  animation: spin 0.8s linear infinite;
225
275
 
226
276
  @keyframes spin {
@@ -230,7 +280,7 @@ const Spinner = styled.div`
230
280
  `;
231
281
 
232
282
  const EmptyMessage = styled.div`
233
- font-size: 9px;
283
+ font-size: 8px;
234
284
  color: ${uiColors.lightGray};
235
285
  font-family: 'Press Start 2P', cursive;
236
286
  text-align: center;
@@ -1,4 +1,5 @@
1
- import React, { useCallback, useEffect, useRef, useState } from 'react';
1
+ import { debounce } from 'lodash';
2
+ import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
2
3
  import { FaPaperPlane } from 'react-icons/fa';
3
4
  import styled from 'styled-components';
4
5
  import { uiColors } from '../../constants/uiColors';
@@ -6,6 +7,11 @@ import { ConfirmModal } from '../ConfirmModal';
6
7
  import { Input } from '../Input';
7
8
  import { CTAButton } from '../shared/CTAButton/CTAButton';
8
9
 
10
+ export interface IDCTransferCharacterResult {
11
+ _id: string;
12
+ name: string;
13
+ }
14
+
9
15
  export interface IDCTransferPanelProps {
10
16
  dcBalance: number;
11
17
  transferLoading: boolean;
@@ -15,6 +21,8 @@ export interface IDCTransferPanelProps {
15
21
  characterName?: string;
16
22
  onInputFocus?: () => void;
17
23
  onInputBlur?: () => void;
24
+ onSearchCharacter?: (name: string) => void;
25
+ searchResults?: IDCTransferCharacterResult[];
18
26
  }
19
27
 
20
28
  export const DCTransferPanel: React.FC<IDCTransferPanelProps> = ({
@@ -26,13 +34,33 @@ export const DCTransferPanel: React.FC<IDCTransferPanelProps> = ({
26
34
  characterName,
27
35
  onInputFocus,
28
36
  onInputBlur,
37
+ onSearchCharacter,
38
+ searchResults,
29
39
  }) => {
30
40
  const [recipientName, setRecipientName] = useState<string>('');
31
41
  const [amount, setAmount] = useState<string>('');
32
42
  const [validationError, setValidationError] = useState<string>('');
33
43
  const [showConfirm, setShowConfirm] = useState<boolean>(false);
44
+ const [showDropdown, setShowDropdown] = useState<boolean>(false);
45
+ const dropdownRef = useRef<HTMLDivElement>(null);
34
46
  const clearResultTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
35
47
 
48
+ const debouncedSearch = useMemo(
49
+ () =>
50
+ debounce((name: string) => {
51
+ if (name.trim().length >= 2) {
52
+ onSearchCharacter?.(name.trim());
53
+ }
54
+ }, 300),
55
+ [onSearchCharacter]
56
+ );
57
+
58
+ useEffect(() => {
59
+ return () => {
60
+ debouncedSearch.cancel();
61
+ };
62
+ }, [debouncedSearch]);
63
+
36
64
  useEffect(() => {
37
65
  if (transferResult) {
38
66
  if (clearResultTimerRef.current) {
@@ -93,15 +121,46 @@ export const DCTransferPanel: React.FC<IDCTransferPanelProps> = ({
93
121
  return (
94
122
  <PanelContainer>
95
123
  <FieldLabel>Recipient Character Name</FieldLabel>
96
- <StyledInput
97
- type="text"
98
- value={recipientName}
99
- onChange={(e) => setRecipientName(e.target.value)}
100
- onFocus={onInputFocus}
101
- onBlur={onInputBlur}
102
- placeholder="Enter character name"
103
- disabled={transferLoading}
104
- />
124
+ <AutocompleteWrapper ref={dropdownRef}>
125
+ <StyledInput
126
+ type="text"
127
+ value={recipientName}
128
+ onChange={(e) => {
129
+ setRecipientName(e.target.value);
130
+ debouncedSearch(e.target.value);
131
+ setShowDropdown(true);
132
+ }}
133
+ onFocus={() => {
134
+ onInputFocus?.();
135
+ if (recipientName.trim().length >= 2 && searchResults && searchResults.length > 0) {
136
+ setShowDropdown(true);
137
+ }
138
+ }}
139
+ onBlur={() => {
140
+ // Delay to allow dropdown click to register
141
+ setTimeout(() => setShowDropdown(false), 150);
142
+ onInputBlur?.();
143
+ }}
144
+ placeholder="Enter character name"
145
+ disabled={transferLoading}
146
+ autoComplete="off"
147
+ />
148
+ {showDropdown && searchResults && searchResults.length > 0 && (
149
+ <DropdownList>
150
+ {searchResults.map((char) => (
151
+ <DropdownItem
152
+ key={char._id}
153
+ onPointerDown={() => {
154
+ setRecipientName(char.name);
155
+ setShowDropdown(false);
156
+ }}
157
+ >
158
+ {char.name}
159
+ </DropdownItem>
160
+ ))}
161
+ </DropdownList>
162
+ )}
163
+ </AutocompleteWrapper>
105
164
 
106
165
  <FieldLabel>Amount (DC)</FieldLabel>
107
166
  <StyledInput
@@ -215,3 +274,36 @@ const ErrorMessage = styled.div`
215
274
  color: ${uiColors.red};
216
275
  font-family: 'Press Start 2P', cursive;
217
276
  `;
277
+
278
+ const AutocompleteWrapper = styled.div`
279
+ position: relative;
280
+ width: 100%;
281
+ `;
282
+
283
+ const DropdownList = styled.ul`
284
+ position: absolute;
285
+ top: 100%;
286
+ left: 0;
287
+ right: 0;
288
+ background: rgba(10, 10, 30, 0.95);
289
+ border: 1px solid #f59e0b;
290
+ border-top: none;
291
+ list-style: none;
292
+ padding: 0;
293
+ margin: 0;
294
+ max-height: 120px;
295
+ overflow-y: auto;
296
+ z-index: 10;
297
+ `;
298
+
299
+ const DropdownItem = styled.li`
300
+ padding: 6px 8px;
301
+ font-size: 11px;
302
+ font-family: 'Press Start 2P', cursive;
303
+ color: ${uiColors.white};
304
+ cursor: pointer;
305
+
306
+ &:hover {
307
+ background: rgba(245, 158, 11, 0.3);
308
+ }
309
+ `;
@@ -1,11 +1,11 @@
1
1
  import React, { useCallback, useState } from 'react';
2
- import { FaTimes } from 'react-icons/fa';
2
+ import { FaShoppingCart, FaTimes } from 'react-icons/fa';
3
3
  import styled from 'styled-components';
4
4
  import { uiColors } from '../../constants/uiColors';
5
5
  import ModalPortal from '../Abstractions/ModalPortal';
6
6
  import { InternalTabs } from '../InternalTabs/InternalTabs';
7
7
  import { DCHistoryPanel, IDCTransaction } from './DCHistoryPanel';
8
- import { DCTransferPanel } from './DCTransferPanel';
8
+ import { DCTransferPanel, IDCTransferCharacterResult } from './DCTransferPanel';
9
9
 
10
10
  type WalletTabId = 'balance' | 'transfer' | 'history';
11
11
 
@@ -22,6 +22,9 @@ export interface IDCWalletModalProps {
22
22
  characterName?: string;
23
23
  onInputFocus?: () => void;
24
24
  onInputBlur?: () => void;
25
+ onBuyDC?: () => void;
26
+ onSearchCharacter?: (name: string) => void;
27
+ searchResults?: IDCTransferCharacterResult[];
25
28
  }
26
29
 
27
30
  export const DCWalletModal: React.FC<IDCWalletModalProps> = ({
@@ -37,6 +40,9 @@ export const DCWalletModal: React.FC<IDCWalletModalProps> = ({
37
40
  characterName,
38
41
  onInputFocus,
39
42
  onInputBlur,
43
+ onBuyDC,
44
+ onSearchCharacter,
45
+ searchResults,
40
46
  }) => {
41
47
  const [activeTab, setActiveTab] = useState<WalletTabId>('balance');
42
48
 
@@ -55,7 +61,12 @@ export const DCWalletModal: React.FC<IDCWalletModalProps> = ({
55
61
  <BalanceContent>
56
62
  <BalanceLabel>Your DC Balance</BalanceLabel>
57
63
  <BalanceAmount>{dcBalance.toLocaleString()} DC</BalanceAmount>
58
- <BalanceHint>Buy DC packs from the Store &gt; Packs tab.</BalanceHint>
64
+ {onBuyDC && (
65
+ <BuyButton onPointerDown={onBuyDC} title="Buy Definya Coins">
66
+ <FaShoppingCart />
67
+ <BuyButtonLabel>Buy DC</BuyButtonLabel>
68
+ </BuyButton>
69
+ )}
59
70
  </BalanceContent>
60
71
  ),
61
72
  },
@@ -72,6 +83,8 @@ export const DCWalletModal: React.FC<IDCWalletModalProps> = ({
72
83
  characterName={characterName}
73
84
  onInputFocus={onInputFocus}
74
85
  onInputBlur={onInputBlur}
86
+ onSearchCharacter={onSearchCharacter}
87
+ searchResults={searchResults}
75
88
  />
76
89
  ),
77
90
  },
@@ -110,7 +123,12 @@ export const DCWalletModal: React.FC<IDCWalletModalProps> = ({
110
123
  <InternalTabs
111
124
  tabs={tabs}
112
125
  activeTab={activeTab}
113
- onTabChange={(tabId: string) => setActiveTab(tabId as WalletTabId)}
126
+ onTabChange={(tabId: string) => {
127
+ setActiveTab(tabId as WalletTabId);
128
+ if (tabId === 'history') {
129
+ onRequestHistory(1);
130
+ }
131
+ }}
114
132
  activeTextColor="#000000"
115
133
  activeColor="#fef08a"
116
134
  inactiveColor="#6b7280"
@@ -218,8 +236,29 @@ const BalanceAmount = styled.div`
218
236
  letter-spacing: 2px;
219
237
  `;
220
238
 
221
- const BalanceHint = styled.span`
239
+ const BuyButton = styled.button`
240
+ display: flex;
241
+ align-items: center;
242
+ gap: 8px;
243
+ margin-top: 8px;
244
+ padding: 10px 20px;
245
+ background: #f59e0b;
246
+ border: none;
247
+ border-radius: 6px;
248
+ color: #000;
249
+ cursor: pointer;
250
+ font-size: 1rem;
251
+
252
+ &:hover {
253
+ background: #fbbf24;
254
+ }
255
+
256
+ &:active {
257
+ background: #d97706;
258
+ }
259
+ `;
260
+
261
+ const BuyButtonLabel = styled.span`
222
262
  font-family: 'Press Start 2P', cursive;
223
- font-size: 7px;
224
- color: ${uiColors.lightGray};
263
+ font-size: 8px;
225
264
  `;