@rpg-engine/long-bow 0.8.198 → 0.8.199
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/dist/components/Store/Store.d.ts +11 -1
- package/dist/components/Store/sections/StoreRedeemSection.d.ts +11 -0
- package/dist/index.d.ts +1 -0
- package/dist/long-bow.cjs.development.js +200 -17
- package/dist/long-bow.cjs.development.js.map +1 -1
- package/dist/long-bow.cjs.production.min.js +1 -1
- package/dist/long-bow.cjs.production.min.js.map +1 -1
- package/dist/long-bow.esm.js +201 -19
- package/dist/long-bow.esm.js.map +1 -1
- package/package.json +1 -1
- package/src/components/Store/Store.tsx +25 -2
- package/src/components/Store/__test__/StoreRedeemSection.spec.tsx +232 -0
- package/src/components/Store/sections/StoreRedeemSection.tsx +258 -0
- package/src/index.tsx +1 -0
package/package.json
CHANGED
|
@@ -4,7 +4,7 @@ import { Gift } from 'pixelarticons/react/Gift';
|
|
|
4
4
|
import { Package } from 'pixelarticons/react/Package';
|
|
5
5
|
import { Wallet } from 'pixelarticons/react/Wallet';
|
|
6
6
|
import React, { ReactNode, useMemo, useState } from 'react';
|
|
7
|
-
import { FaHistory, FaShoppingCart, FaWallet } from 'react-icons/fa';
|
|
7
|
+
import { FaHistory, FaShoppingCart, FaTicketAlt, FaWallet } from 'react-icons/fa';
|
|
8
8
|
import styled from 'styled-components';
|
|
9
9
|
import { uiColors } from '../../constants/uiColors';
|
|
10
10
|
import { DraggableContainer } from '../DraggableContainer';
|
|
@@ -18,10 +18,11 @@ import { useStoreCart } from './hooks/useStoreCart';
|
|
|
18
18
|
import { MetadataCollector } from './MetadataCollector';
|
|
19
19
|
import { StoreItemsSection } from './sections/StoreItemsSection';
|
|
20
20
|
import { StorePacksSection } from './sections/StorePacksSection';
|
|
21
|
+
import { StoreRedeemSection } from './sections/StoreRedeemSection';
|
|
21
22
|
import { StoreItemDetails } from './StoreItemDetails';
|
|
22
23
|
|
|
23
24
|
// Define TabId union type for tab identifiers
|
|
24
|
-
type TabId = 'premium' | 'packs' | 'items' | 'wallet' | 'history';
|
|
25
|
+
type TabId = 'premium' | 'packs' | 'items' | 'wallet' | 'history' | 'redeem';
|
|
25
26
|
|
|
26
27
|
// Define IStoreProps locally as a workaround
|
|
27
28
|
export interface IStoreProps {
|
|
@@ -74,6 +75,12 @@ export interface IStoreProps {
|
|
|
74
75
|
onBuyDC?: () => void;
|
|
75
76
|
/** Currency symbol to display (e.g. "$" for USD, "R$" for BRL). Defaults to "$". */
|
|
76
77
|
currencySymbol?: string;
|
|
78
|
+
/** Callback to redeem a voucher code. When provided, the Redeem tab is shown. */
|
|
79
|
+
onRedeem?: (code: string) => Promise<{ success: boolean; dcAmount?: number; error?: string }>;
|
|
80
|
+
/** Called when the voucher code input gains focus. */
|
|
81
|
+
onRedeemInputFocus?: () => void;
|
|
82
|
+
/** Called when the voucher code input loses focus. */
|
|
83
|
+
onRedeemInputBlur?: () => void;
|
|
77
84
|
}
|
|
78
85
|
|
|
79
86
|
export type { IFeaturedItem };
|
|
@@ -116,6 +123,9 @@ export const Store: React.FC<IStoreProps> = ({
|
|
|
116
123
|
onPurchaseError,
|
|
117
124
|
onBuyDC,
|
|
118
125
|
currencySymbol = '$',
|
|
126
|
+
onRedeem,
|
|
127
|
+
onRedeemInputFocus,
|
|
128
|
+
onRedeemInputBlur,
|
|
119
129
|
}) => {
|
|
120
130
|
const [selectedPack, setSelectedPack] = useState<IItemPack | null>(null);
|
|
121
131
|
const [activeTab, setActiveTab] = useState<TabId>(() => {
|
|
@@ -230,6 +240,7 @@ export const Store: React.FC<IStoreProps> = ({
|
|
|
230
240
|
// Build tabs dynamically based on props
|
|
231
241
|
const tabIds: TabId[] = [
|
|
232
242
|
...(tabOrder ?? ['premium', 'packs', 'items']),
|
|
243
|
+
...(onRedeem ? ['redeem' as TabId] : []),
|
|
233
244
|
...((onShowWallet || customWalletContent) ? ['wallet' as TabId] : []),
|
|
234
245
|
...((onShowHistory || customHistoryContent) ? ['history' as TabId] : [])
|
|
235
246
|
];
|
|
@@ -335,6 +346,18 @@ export const Store: React.FC<IStoreProps> = ({
|
|
|
335
346
|
/>
|
|
336
347
|
),
|
|
337
348
|
},
|
|
349
|
+
redeem: {
|
|
350
|
+
id: 'redeem',
|
|
351
|
+
title: 'Redeem',
|
|
352
|
+
icon: <FaTicketAlt size={16} />,
|
|
353
|
+
content: onRedeem ? (
|
|
354
|
+
<StoreRedeemSection
|
|
355
|
+
onRedeem={onRedeem}
|
|
356
|
+
onInputFocus={onRedeemInputFocus}
|
|
357
|
+
onInputBlur={onRedeemInputBlur}
|
|
358
|
+
/>
|
|
359
|
+
) : null,
|
|
360
|
+
},
|
|
338
361
|
wallet: {
|
|
339
362
|
id: 'wallet',
|
|
340
363
|
title: walletLabel ?? 'Wallet',
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @jest-environment jsdom
|
|
3
|
+
*/
|
|
4
|
+
// @ts-nocheck
|
|
5
|
+
import React from 'react';
|
|
6
|
+
import ReactDOM from 'react-dom';
|
|
7
|
+
import { act, Simulate } from 'react-dom/test-utils';
|
|
8
|
+
import { StoreRedeemSection } from '../sections/StoreRedeemSection';
|
|
9
|
+
|
|
10
|
+
jest.mock('../../shared/CTAButton/CTAButton', () => ({
|
|
11
|
+
CTAButton: ({ label, onClick, disabled }) => (
|
|
12
|
+
<button onClick={onClick} disabled={disabled} data-testid="cta-button">
|
|
13
|
+
{label}
|
|
14
|
+
</button>
|
|
15
|
+
),
|
|
16
|
+
}));
|
|
17
|
+
|
|
18
|
+
jest.mock('styled-components', () => {
|
|
19
|
+
const createTag = (tag) => () => {
|
|
20
|
+
if (tag === 'input') return (props) => <input {...props} />;
|
|
21
|
+
if (tag === 'h3') return ({ children }) => <h3>{children}</h3>;
|
|
22
|
+
if (tag === 'p') return ({ children }) => <p>{children}</p>;
|
|
23
|
+
return ({ children, ...props }) => <div {...props}>{children}</div>;
|
|
24
|
+
};
|
|
25
|
+
const styled = new Proxy(() => {}, {
|
|
26
|
+
get: (_target, prop) => {
|
|
27
|
+
if (prop === '__esModule') return true;
|
|
28
|
+
return createTag(prop);
|
|
29
|
+
},
|
|
30
|
+
apply: () => createTag('div'),
|
|
31
|
+
});
|
|
32
|
+
return { __esModule: true, default: styled, keyframes: () => '', css: () => '' };
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
jest.mock('react-icons/fa', () => ({
|
|
36
|
+
FaCheckCircle: () => <span>check</span>,
|
|
37
|
+
FaExclamationCircle: () => <span>error</span>,
|
|
38
|
+
FaTicketAlt: () => <span>ticket</span>,
|
|
39
|
+
}));
|
|
40
|
+
|
|
41
|
+
jest.mock('../../../constants/uiColors', () => ({
|
|
42
|
+
uiColors: {
|
|
43
|
+
white: '#fff',
|
|
44
|
+
lightGray: '#888',
|
|
45
|
+
green: '#4CAF50',
|
|
46
|
+
red: '#D04648',
|
|
47
|
+
},
|
|
48
|
+
}));
|
|
49
|
+
|
|
50
|
+
const flushPromises = () => new Promise((resolve) => setTimeout(resolve, 0));
|
|
51
|
+
|
|
52
|
+
describe('StoreRedeemSection', () => {
|
|
53
|
+
let container;
|
|
54
|
+
|
|
55
|
+
beforeEach(() => {
|
|
56
|
+
container = document.createElement('div');
|
|
57
|
+
document.body.appendChild(container);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
afterEach(() => {
|
|
61
|
+
ReactDOM.unmountComponentAtNode(container);
|
|
62
|
+
document.body.removeChild(container);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it('should render input and redeem button', () => {
|
|
66
|
+
act(() => {
|
|
67
|
+
ReactDOM.render(
|
|
68
|
+
<StoreRedeemSection onRedeem={jest.fn()} />,
|
|
69
|
+
container
|
|
70
|
+
);
|
|
71
|
+
});
|
|
72
|
+
expect(container.querySelector('input')).toBeTruthy();
|
|
73
|
+
expect(container.textContent).toContain('Redeem Code');
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it('should disable button when input is empty', () => {
|
|
77
|
+
act(() => {
|
|
78
|
+
ReactDOM.render(
|
|
79
|
+
<StoreRedeemSection onRedeem={jest.fn()} />,
|
|
80
|
+
container
|
|
81
|
+
);
|
|
82
|
+
});
|
|
83
|
+
const button = container.querySelector('[data-testid="cta-button"]');
|
|
84
|
+
expect(button.disabled).toBe(true);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it('should enable button when input has text', () => {
|
|
88
|
+
act(() => {
|
|
89
|
+
ReactDOM.render(
|
|
90
|
+
<StoreRedeemSection onRedeem={jest.fn()} />,
|
|
91
|
+
container
|
|
92
|
+
);
|
|
93
|
+
});
|
|
94
|
+
const input = container.querySelector('input');
|
|
95
|
+
act(() => {
|
|
96
|
+
const nativeInputValueSetter = Object.getOwnPropertyDescriptor(
|
|
97
|
+
window.HTMLInputElement.prototype,
|
|
98
|
+
'value'
|
|
99
|
+
).set;
|
|
100
|
+
nativeInputValueSetter.call(input, 'ABC-123');
|
|
101
|
+
input.dispatchEvent(new Event('change', { bubbles: true }));
|
|
102
|
+
});
|
|
103
|
+
const button = container.querySelector('[data-testid="cta-button"]');
|
|
104
|
+
expect(button.disabled).toBe(false);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it('should show success with DC amount on valid redeem', async () => {
|
|
108
|
+
const onRedeem = jest.fn().mockResolvedValue({ success: true, dcAmount: 550 });
|
|
109
|
+
|
|
110
|
+
act(() => {
|
|
111
|
+
ReactDOM.render(
|
|
112
|
+
<StoreRedeemSection onRedeem={onRedeem} />,
|
|
113
|
+
container
|
|
114
|
+
);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
const input = container.querySelector('input');
|
|
118
|
+
act(() => {
|
|
119
|
+
const nativeInputValueSetter = Object.getOwnPropertyDescriptor(
|
|
120
|
+
window.HTMLInputElement.prototype,
|
|
121
|
+
'value'
|
|
122
|
+
).set;
|
|
123
|
+
nativeInputValueSetter.call(input, 'CHB-550-ABCDEF1234');
|
|
124
|
+
input.dispatchEvent(new Event('change', { bubbles: true }));
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
const button = container.querySelector('[data-testid="cta-button"]');
|
|
128
|
+
await act(async () => {
|
|
129
|
+
button.click();
|
|
130
|
+
await flushPromises();
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
expect(onRedeem).toHaveBeenCalledWith('CHB-550-ABCDEF1234');
|
|
134
|
+
expect(container.textContent).toContain('Code Redeemed');
|
|
135
|
+
expect(container.textContent).toContain('+550 DC');
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
it('should show error on failed redeem', async () => {
|
|
139
|
+
const onRedeem = jest.fn().mockResolvedValue({ success: false, error: 'Invalid voucher code.' });
|
|
140
|
+
|
|
141
|
+
act(() => {
|
|
142
|
+
ReactDOM.render(
|
|
143
|
+
<StoreRedeemSection onRedeem={onRedeem} />,
|
|
144
|
+
container
|
|
145
|
+
);
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
const input = container.querySelector('input');
|
|
149
|
+
act(() => {
|
|
150
|
+
const nativeInputValueSetter = Object.getOwnPropertyDescriptor(
|
|
151
|
+
window.HTMLInputElement.prototype,
|
|
152
|
+
'value'
|
|
153
|
+
).set;
|
|
154
|
+
nativeInputValueSetter.call(input, 'INVALID-CODE');
|
|
155
|
+
input.dispatchEvent(new Event('change', { bubbles: true }));
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
const button = container.querySelector('[data-testid="cta-button"]');
|
|
159
|
+
await act(async () => {
|
|
160
|
+
button.click();
|
|
161
|
+
await flushPromises();
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
expect(container.textContent).toContain('Invalid voucher code.');
|
|
165
|
+
expect(container.textContent).toContain('Try Again');
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
it('should reset to idle when "Redeem Another" is clicked after success', async () => {
|
|
169
|
+
const onRedeem = jest.fn().mockResolvedValue({ success: true, dcAmount: 100 });
|
|
170
|
+
|
|
171
|
+
act(() => {
|
|
172
|
+
ReactDOM.render(
|
|
173
|
+
<StoreRedeemSection onRedeem={onRedeem} />,
|
|
174
|
+
container
|
|
175
|
+
);
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
const input = container.querySelector('input');
|
|
179
|
+
act(() => {
|
|
180
|
+
const nativeInputValueSetter = Object.getOwnPropertyDescriptor(
|
|
181
|
+
window.HTMLInputElement.prototype,
|
|
182
|
+
'value'
|
|
183
|
+
).set;
|
|
184
|
+
nativeInputValueSetter.call(input, 'CODE123');
|
|
185
|
+
input.dispatchEvent(new Event('change', { bubbles: true }));
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
const submitBtn = container.querySelector('[data-testid="cta-button"]');
|
|
189
|
+
await act(async () => {
|
|
190
|
+
submitBtn.click();
|
|
191
|
+
await flushPromises();
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
expect(container.textContent).toContain('Redeem Another');
|
|
195
|
+
|
|
196
|
+
const redeemAnotherBtn = Array.from(container.querySelectorAll('[data-testid="cta-button"]'))
|
|
197
|
+
.find(b => b.textContent.includes('Redeem Another'));
|
|
198
|
+
|
|
199
|
+
act(() => { redeemAnotherBtn.click(); });
|
|
200
|
+
|
|
201
|
+
expect(container.querySelector('input')).toBeTruthy();
|
|
202
|
+
expect(container.textContent).toContain('Redeem Code');
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
it('should call onInputFocus when input is focused', () => {
|
|
206
|
+
const onInputFocus = jest.fn();
|
|
207
|
+
act(() => {
|
|
208
|
+
ReactDOM.render(
|
|
209
|
+
<StoreRedeemSection onRedeem={jest.fn()} onInputFocus={onInputFocus} />,
|
|
210
|
+
container
|
|
211
|
+
);
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
const input = container.querySelector('input');
|
|
215
|
+
act(() => { Simulate.focus(input); });
|
|
216
|
+
expect(onInputFocus).toHaveBeenCalledTimes(1);
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
it('should call onInputBlur when input loses focus', () => {
|
|
220
|
+
const onInputBlur = jest.fn();
|
|
221
|
+
act(() => {
|
|
222
|
+
ReactDOM.render(
|
|
223
|
+
<StoreRedeemSection onRedeem={jest.fn()} onInputBlur={onInputBlur} />,
|
|
224
|
+
container
|
|
225
|
+
);
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
const input = container.querySelector('input');
|
|
229
|
+
act(() => { Simulate.blur(input); });
|
|
230
|
+
expect(onInputBlur).toHaveBeenCalledTimes(1);
|
|
231
|
+
});
|
|
232
|
+
});
|
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
import React, { useState } from 'react';
|
|
2
|
+
import { FaCheckCircle, FaExclamationCircle, FaTicketAlt } from 'react-icons/fa';
|
|
3
|
+
import styled, { keyframes } from 'styled-components';
|
|
4
|
+
import { uiColors } from '../../../constants/uiColors';
|
|
5
|
+
import { CTAButton } from '../../shared/CTAButton/CTAButton';
|
|
6
|
+
|
|
7
|
+
type RedeemStatus = 'idle' | 'loading' | 'success' | 'error';
|
|
8
|
+
|
|
9
|
+
export interface IStoreRedeemSectionProps {
|
|
10
|
+
onRedeem: (code: string) => Promise<{ success: boolean; dcAmount?: number; error?: string }>;
|
|
11
|
+
onInputFocus?: () => void;
|
|
12
|
+
onInputBlur?: () => void;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export const StoreRedeemSection: React.FC<IStoreRedeemSectionProps> = ({
|
|
16
|
+
onRedeem,
|
|
17
|
+
onInputFocus,
|
|
18
|
+
onInputBlur,
|
|
19
|
+
}) => {
|
|
20
|
+
const [code, setCode] = useState('');
|
|
21
|
+
const [status, setStatus] = useState<RedeemStatus>('idle');
|
|
22
|
+
const [dcAmount, setDcAmount] = useState<number | undefined>();
|
|
23
|
+
const [errorMessage, setErrorMessage] = useState('');
|
|
24
|
+
|
|
25
|
+
const canSubmit = code.trim().length > 0 && status !== 'loading';
|
|
26
|
+
|
|
27
|
+
const handleSubmit = async (): Promise<void> => {
|
|
28
|
+
if (!canSubmit) return;
|
|
29
|
+
|
|
30
|
+
const normalizedCode = code.trim().toUpperCase();
|
|
31
|
+
setStatus('loading');
|
|
32
|
+
setErrorMessage('');
|
|
33
|
+
|
|
34
|
+
try {
|
|
35
|
+
const result = await onRedeem(normalizedCode);
|
|
36
|
+
if (result.success) {
|
|
37
|
+
setStatus('success');
|
|
38
|
+
setDcAmount(result.dcAmount);
|
|
39
|
+
} else {
|
|
40
|
+
setStatus('error');
|
|
41
|
+
setErrorMessage(result.error ?? 'Redemption failed. Please try again.');
|
|
42
|
+
}
|
|
43
|
+
} catch {
|
|
44
|
+
setStatus('error');
|
|
45
|
+
setErrorMessage('Something went wrong. Please try again.');
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
const handleReset = (): void => {
|
|
50
|
+
setCode('');
|
|
51
|
+
setStatus('idle');
|
|
52
|
+
setDcAmount(undefined);
|
|
53
|
+
setErrorMessage('');
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
const handleKeyDown = (e: React.KeyboardEvent): void => {
|
|
57
|
+
if (e.key === 'Enter') {
|
|
58
|
+
void handleSubmit();
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
if (status === 'success') {
|
|
63
|
+
return (
|
|
64
|
+
<Container>
|
|
65
|
+
<ResultContainer>
|
|
66
|
+
<SuccessIcon>
|
|
67
|
+
<FaCheckCircle size={32} />
|
|
68
|
+
</SuccessIcon>
|
|
69
|
+
<SuccessTitle>Code Redeemed!</SuccessTitle>
|
|
70
|
+
{dcAmount != null && (
|
|
71
|
+
<DCAmountDisplay>+{dcAmount.toLocaleString()} DC</DCAmountDisplay>
|
|
72
|
+
)}
|
|
73
|
+
<SuccessHint>Your wallet balance has been updated.</SuccessHint>
|
|
74
|
+
<ButtonWrapper>
|
|
75
|
+
<CTAButton
|
|
76
|
+
icon={<FaTicketAlt />}
|
|
77
|
+
label="Redeem Another"
|
|
78
|
+
onClick={handleReset}
|
|
79
|
+
/>
|
|
80
|
+
</ButtonWrapper>
|
|
81
|
+
</ResultContainer>
|
|
82
|
+
</Container>
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (status === 'error') {
|
|
87
|
+
return (
|
|
88
|
+
<Container>
|
|
89
|
+
<ResultContainer>
|
|
90
|
+
<ErrorIcon>
|
|
91
|
+
<FaExclamationCircle size={32} />
|
|
92
|
+
</ErrorIcon>
|
|
93
|
+
<ErrorTitle>{errorMessage}</ErrorTitle>
|
|
94
|
+
<ButtonWrapper>
|
|
95
|
+
<CTAButton
|
|
96
|
+
icon={<FaTicketAlt />}
|
|
97
|
+
label="Try Again"
|
|
98
|
+
onClick={handleReset}
|
|
99
|
+
/>
|
|
100
|
+
</ButtonWrapper>
|
|
101
|
+
</ResultContainer>
|
|
102
|
+
</Container>
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return (
|
|
107
|
+
<Container>
|
|
108
|
+
<Title>Redeem a Voucher Code</Title>
|
|
109
|
+
<Description>
|
|
110
|
+
Enter your voucher code below to receive Definya Coins.
|
|
111
|
+
</Description>
|
|
112
|
+
<InputRow>
|
|
113
|
+
<CodeInput
|
|
114
|
+
type="text"
|
|
115
|
+
value={code}
|
|
116
|
+
onChange={(e) => setCode(e.target.value)}
|
|
117
|
+
onFocus={onInputFocus}
|
|
118
|
+
onBlur={onInputBlur}
|
|
119
|
+
onKeyDown={handleKeyDown}
|
|
120
|
+
placeholder="Enter code..."
|
|
121
|
+
disabled={status === 'loading'}
|
|
122
|
+
autoComplete="off"
|
|
123
|
+
spellCheck={false}
|
|
124
|
+
/>
|
|
125
|
+
</InputRow>
|
|
126
|
+
<ButtonWrapper>
|
|
127
|
+
<CTAButton
|
|
128
|
+
icon={<FaTicketAlt />}
|
|
129
|
+
label={status === 'loading' ? 'Redeeming...' : 'Redeem Code'}
|
|
130
|
+
onClick={() => { void handleSubmit(); }}
|
|
131
|
+
disabled={!canSubmit}
|
|
132
|
+
fullWidth
|
|
133
|
+
/>
|
|
134
|
+
</ButtonWrapper>
|
|
135
|
+
</Container>
|
|
136
|
+
);
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
const Container = styled.div`
|
|
140
|
+
display: flex;
|
|
141
|
+
flex-direction: column;
|
|
142
|
+
align-items: center;
|
|
143
|
+
justify-content: center;
|
|
144
|
+
padding: 2rem 1.5rem;
|
|
145
|
+
max-width: 420px;
|
|
146
|
+
margin: 0 auto;
|
|
147
|
+
gap: 1rem;
|
|
148
|
+
`;
|
|
149
|
+
|
|
150
|
+
const Title = styled.h3`
|
|
151
|
+
font-family: 'Press Start 2P', cursive;
|
|
152
|
+
font-size: 0.85rem;
|
|
153
|
+
color: ${uiColors.white};
|
|
154
|
+
margin: 0;
|
|
155
|
+
text-align: center;
|
|
156
|
+
`;
|
|
157
|
+
|
|
158
|
+
const Description = styled.p`
|
|
159
|
+
font-family: 'Press Start 2P', cursive;
|
|
160
|
+
font-size: 0.55rem;
|
|
161
|
+
color: ${uiColors.lightGray};
|
|
162
|
+
margin: 0;
|
|
163
|
+
text-align: center;
|
|
164
|
+
line-height: 1.6;
|
|
165
|
+
`;
|
|
166
|
+
|
|
167
|
+
const InputRow = styled.div`
|
|
168
|
+
width: 100%;
|
|
169
|
+
`;
|
|
170
|
+
|
|
171
|
+
const CodeInput = styled.input`
|
|
172
|
+
width: 100%;
|
|
173
|
+
padding: 12px 14px;
|
|
174
|
+
font-family: 'Press Start 2P', cursive;
|
|
175
|
+
font-size: 0.75rem;
|
|
176
|
+
color: ${uiColors.white};
|
|
177
|
+
background: rgba(0, 0, 0, 0.4);
|
|
178
|
+
border: 2px solid #f59e0b;
|
|
179
|
+
border-radius: 4px;
|
|
180
|
+
text-transform: uppercase;
|
|
181
|
+
letter-spacing: 2px;
|
|
182
|
+
text-align: center;
|
|
183
|
+
box-sizing: border-box;
|
|
184
|
+
outline: none;
|
|
185
|
+
|
|
186
|
+
&::placeholder {
|
|
187
|
+
color: ${uiColors.lightGray};
|
|
188
|
+
text-transform: none;
|
|
189
|
+
letter-spacing: 0;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
&:focus {
|
|
193
|
+
border-color: #fbbf24;
|
|
194
|
+
box-shadow: 0 0 8px rgba(251, 191, 36, 0.3);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
&:disabled {
|
|
198
|
+
opacity: 0.5;
|
|
199
|
+
cursor: not-allowed;
|
|
200
|
+
}
|
|
201
|
+
`;
|
|
202
|
+
|
|
203
|
+
const ButtonWrapper = styled.div`
|
|
204
|
+
width: 100%;
|
|
205
|
+
margin-top: 0.5rem;
|
|
206
|
+
`;
|
|
207
|
+
|
|
208
|
+
const ResultContainer = styled.div`
|
|
209
|
+
display: flex;
|
|
210
|
+
flex-direction: column;
|
|
211
|
+
align-items: center;
|
|
212
|
+
gap: 1rem;
|
|
213
|
+
padding: 1rem 0;
|
|
214
|
+
`;
|
|
215
|
+
|
|
216
|
+
const glowPulse = keyframes`
|
|
217
|
+
0%, 100% { opacity: 1; }
|
|
218
|
+
50% { opacity: 0.7; }
|
|
219
|
+
`;
|
|
220
|
+
|
|
221
|
+
const SuccessIcon = styled.div`
|
|
222
|
+
color: ${uiColors.green};
|
|
223
|
+
animation: ${glowPulse} 1.5s ease-in-out infinite;
|
|
224
|
+
`;
|
|
225
|
+
|
|
226
|
+
const SuccessTitle = styled.h3`
|
|
227
|
+
font-family: 'Press Start 2P', cursive;
|
|
228
|
+
font-size: 0.85rem;
|
|
229
|
+
color: ${uiColors.green};
|
|
230
|
+
margin: 0;
|
|
231
|
+
`;
|
|
232
|
+
|
|
233
|
+
const DCAmountDisplay = styled.div`
|
|
234
|
+
font-family: 'Press Start 2P', cursive;
|
|
235
|
+
font-size: 1.2rem;
|
|
236
|
+
color: #fef08a;
|
|
237
|
+
text-shadow: 0 0 10px rgba(254, 240, 138, 0.5);
|
|
238
|
+
`;
|
|
239
|
+
|
|
240
|
+
const SuccessHint = styled.p`
|
|
241
|
+
font-family: 'Press Start 2P', cursive;
|
|
242
|
+
font-size: 0.5rem;
|
|
243
|
+
color: ${uiColors.lightGray};
|
|
244
|
+
margin: 0;
|
|
245
|
+
`;
|
|
246
|
+
|
|
247
|
+
const ErrorIcon = styled.div`
|
|
248
|
+
color: ${uiColors.red};
|
|
249
|
+
`;
|
|
250
|
+
|
|
251
|
+
const ErrorTitle = styled.p`
|
|
252
|
+
font-family: 'Press Start 2P', cursive;
|
|
253
|
+
font-size: 0.65rem;
|
|
254
|
+
color: ${uiColors.red};
|
|
255
|
+
margin: 0;
|
|
256
|
+
text-align: center;
|
|
257
|
+
line-height: 1.6;
|
|
258
|
+
`;
|
package/src/index.tsx
CHANGED
|
@@ -85,6 +85,7 @@ export * from './components/Store/PaymentMethodModal';
|
|
|
85
85
|
export * from './components/Store/PurchaseSuccess';
|
|
86
86
|
export * from './components/Store/Store';
|
|
87
87
|
export * from './components/Store/StoreBadges';
|
|
88
|
+
export * from './components/Store/sections/StoreRedeemSection';
|
|
88
89
|
export * from './components/Store/TrustBar';
|
|
89
90
|
export * from './components/Table/Table';
|
|
90
91
|
export * from './components/TextArea';
|