Skip to content

Commit 0e177e8

Browse files
committed
feat(subscription-tiers): improve data limit input UX
- Replace single text input with structured number + unit selector - Add data parsing and formatting to maintain backend compatibility - Fix React Hook dependency warning with functional update pattern - Ensure consistent formatting of storage values ('X MB/GB per month') - Improve error prevention by separating numeric value from unit - Fix TypeScript type errors related to InputNumber and Select components This change makes it easier for users to correctly set storage limits while preventing format errors and maintaining the expected backend format.
1 parent a2c81bf commit 0e177e8

1 file changed

Lines changed: 225 additions & 66 deletions

File tree

src/components/SubscriptionTiersManager/SubscriptionTiersManager.tsx

Lines changed: 225 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,33 @@
11
import React, { useState, useEffect } from 'react';
2-
import { Input, Switch, Tooltip } from 'antd';
2+
import { Input, Switch, Tooltip, Select, InputNumber, Space } from 'antd';
33
import { BaseButton } from '@app/components/common/BaseButton/BaseButton';
4-
import { PlusOutlined, DollarOutlined, DatabaseOutlined, DeleteOutlined, ThunderboltOutlined } from '@ant-design/icons';
4+
import { PlusOutlined, DatabaseOutlined, DeleteOutlined, ThunderboltOutlined } from '@ant-design/icons';
55
import * as S from '@app/pages/uiComponentsPages/UIComponentsPage.styles';
66
import type { SubscriptionTier } from '@app/constants/relaySettings';
77
import styled from 'styled-components';
88

9+
// Helper functions for data limit parsing and formatting
10+
interface DataLimit {
11+
amount: number;
12+
unit: 'MB' | 'GB';
13+
}
14+
15+
const parseDataLimit = (dataLimitString: string): DataLimit => {
16+
const match = dataLimitString.match(/^(\d+)\s*(MB|GB)/i);
17+
if (match) {
18+
return {
19+
amount: parseInt(match[1], 10),
20+
unit: match[2].toUpperCase() as 'MB' | 'GB'
21+
};
22+
}
23+
// Default fallback
24+
return { amount: 1, unit: 'GB' };
25+
};
26+
27+
const formatDataLimit = (amount: number, unit: 'MB' | 'GB'): string => {
28+
return `${amount} ${unit} per month`;
29+
};
30+
931
// Styled components for better UI
1032
const TierCard = styled.div`
1133
background: linear-gradient(145deg, #1b1b38 0%, #161632 100%);
@@ -128,6 +150,52 @@ const InputIcon = styled.div`
128150
}
129151
`;
130152

153+
const DataLimitInputGroup = styled.div`
154+
display: flex;
155+
gap: 8px;
156+
align-items: flex-start;
157+
`;
158+
159+
const StyledInputNumber = styled(InputNumber)`
160+
flex: 1;
161+
background-color: #1b1b38 !important;
162+
border-color: #313131 !important;
163+
color: white !important;
164+
border-radius: 8px !important;
165+
height: 48px !important;
166+
167+
.ant-input-number-input {
168+
color: white !important;
169+
}
170+
171+
&.ant-input-number-focused {
172+
border-color: #4e4e8b !important;
173+
box-shadow: 0 0 0 2px rgba(78, 78, 139, 0.2) !important;
174+
}
175+
`;
176+
177+
const StyledSelect = styled(Select)`
178+
width: 120px !important;
179+
180+
.ant-select-selector {
181+
background-color: #1b1b38 !important;
182+
border-color: #313131 !important;
183+
height: 48px !important;
184+
display: flex !important;
185+
align-items: center !important;
186+
border-radius: 8px !important;
187+
}
188+
189+
.ant-select-selection-item {
190+
color: white !important;
191+
}
192+
193+
&.ant-select-focused .ant-select-selector {
194+
border-color: #4e4e8b !important;
195+
box-shadow: 0 0 0 2px rgba(78, 78, 139, 0.2) !important;
196+
}
197+
`;
198+
131199
interface SubscriptionTiersManagerProps {
132200
tiers?: SubscriptionTier[];
133201
onChange: (tiers: SubscriptionTier[]) => void;
@@ -149,73 +217,160 @@ const SubscriptionTiersManager: React.FC<SubscriptionTiersManagerProps> = ({
149217
{ data_limit: '10 GB per month', price: '15000' }
150218
];
151219

152-
// Initialize with properly formatted tiers from props or default
153-
const [currentTiers, setCurrentTiers] = useState<SubscriptionTier[]>(() => {
154-
return tiers.length > 0 ? tiers.map(tier => ({
155-
data_limit: tier.data_limit.includes('per month') ? tier.data_limit : `${tier.data_limit} per month`,
156-
price: tier.price
157-
})) : defaultTiers;
220+
// Initialize tiers with data_limit parsed into amount and unit
221+
const [currentTiers, setCurrentTiers] = useState<(SubscriptionTier & { amount: number; unit: 'MB' | 'GB' })[]>(() => {
222+
return tiers.length > 0 ? tiers.map(tier => {
223+
const dataLimit = parseDataLimit(tier.data_limit);
224+
return {
225+
data_limit: tier.data_limit.includes('per month') ? tier.data_limit : `${tier.data_limit} per month`,
226+
price: tier.price,
227+
amount: dataLimit.amount,
228+
unit: dataLimit.unit
229+
};
230+
}) : defaultTiers.map(tier => {
231+
const dataLimit = parseDataLimit(tier.data_limit);
232+
return {
233+
data_limit: tier.data_limit,
234+
price: tier.price,
235+
amount: dataLimit.amount,
236+
unit: dataLimit.unit
237+
};
238+
});
158239
});
159240

241+
// Parse free tier limit into amount and unit
242+
const parsedFreeTierLimit = parseDataLimit(freeTierLimit);
243+
const [freeTierAmount, setFreeTierAmount] = useState<number>(parsedFreeTierLimit.amount);
244+
const [freeTierUnit, setFreeTierUnit] = useState<'MB' | 'GB'>(parsedFreeTierLimit.unit);
245+
160246
// Update current tiers when props change
161247
useEffect(() => {
162248
if (tiers.length > 0) {
163-
const formattedTiers = tiers.map(tier => ({
164-
data_limit: tier.data_limit.includes('per month') ? tier.data_limit : `${tier.data_limit} per month`,
165-
price: tier.price
166-
}));
167-
168-
// Only update if the formatted tiers are different from current
169-
const currentTiersString = JSON.stringify(currentTiers);
170-
const formattedTiersString = JSON.stringify(formattedTiers);
171-
172-
if (currentTiersString !== formattedTiersString) {
173-
setCurrentTiers(formattedTiers);
174-
}
249+
// Use functional update pattern to avoid dependency on currentTiers
250+
setCurrentTiers(prevTiers => {
251+
const formattedTiers = tiers.map(tier => {
252+
const dataLimit = parseDataLimit(tier.data_limit);
253+
return {
254+
data_limit: tier.data_limit.includes('per month') ? tier.data_limit : `${tier.data_limit} per month`,
255+
price: tier.price,
256+
amount: dataLimit.amount,
257+
unit: dataLimit.unit
258+
};
259+
});
260+
261+
// Only update if the formatted tiers are different from current
262+
const currentTierDataOnly = prevTiers.map(({ data_limit, price }) => ({ data_limit, price }));
263+
const formattedTierDataOnly = formattedTiers.map(({ data_limit, price }) => ({ data_limit, price }));
264+
265+
if (JSON.stringify(currentTierDataOnly) !== JSON.stringify(formattedTierDataOnly)) {
266+
return formattedTiers;
267+
}
268+
return prevTiers;
269+
});
175270
}
176-
}, [tiers, currentTiers]);
271+
}, [tiers]);
177272

178-
const handleUpdateTier = (index: number, field: keyof SubscriptionTier, value: string) => {
273+
// Update free tier amount and unit when freeTierLimit prop changes
274+
useEffect(() => {
275+
const parsed = parseDataLimit(freeTierLimit);
276+
setFreeTierAmount(parsed.amount);
277+
setFreeTierUnit(parsed.unit);
278+
}, [freeTierLimit]);
279+
280+
const handleUpdateTierPrice = (index: number, value: string) => {
179281
const newTiers = currentTiers.map((tier, i) => {
180282
if (i === index) {
181-
if (field === 'data_limit') {
182-
const formattedValue = value.includes('per month') ? value : `${value} per month`;
183-
return { ...tier, [field]: formattedValue };
184-
}
185-
return { ...tier, [field]: value };
283+
return { ...tier, price: value };
284+
}
285+
return tier;
286+
});
287+
288+
setCurrentTiers(newTiers);
289+
onChange(newTiers.map(({ data_limit, price }) => ({ data_limit, price })));
290+
};
291+
292+
// Fixed type signature for InputNumber's onChange
293+
const handleUpdateTierAmount = (index: number, value: string | number | null) => {
294+
if (value === null) return;
295+
296+
const numValue = typeof value === 'string' ? parseInt(value, 10) : value;
297+
298+
const newTiers = currentTiers.map((tier, i) => {
299+
if (i === index) {
300+
const newDataLimit = formatDataLimit(numValue, tier.unit);
301+
return {
302+
...tier,
303+
amount: numValue,
304+
data_limit: newDataLimit
305+
};
306+
}
307+
return tier;
308+
});
309+
310+
setCurrentTiers(newTiers);
311+
onChange(newTiers.map(({ data_limit, price }) => ({ data_limit, price })));
312+
};
313+
314+
// Fixed type signature for Select's onChange
315+
const handleUpdateTierUnit = (index: number, value: unknown) => {
316+
const unit = value as 'MB' | 'GB';
317+
318+
const newTiers = currentTiers.map((tier, i) => {
319+
if (i === index) {
320+
const newDataLimit = formatDataLimit(tier.amount, unit);
321+
return {
322+
...tier,
323+
unit,
324+
data_limit: newDataLimit
325+
};
186326
}
187327
return tier;
188328
});
189329

190330
setCurrentTiers(newTiers);
191-
onChange(newTiers);
331+
onChange(newTiers.map(({ data_limit, price }) => ({ data_limit, price })));
192332
};
193333

194334
const addTier = () => {
195335
if (currentTiers.length < 3) {
196-
const newTier: SubscriptionTier = {
336+
const newTier = {
197337
data_limit: '1 GB per month',
198-
price: '10000'
338+
price: '10000',
339+
amount: 1,
340+
unit: 'GB' as 'MB' | 'GB'
199341
};
200342
const updatedTiers = [...currentTiers, newTier];
201343
setCurrentTiers(updatedTiers);
202-
onChange(updatedTiers);
344+
onChange(updatedTiers.map(({ data_limit, price }) => ({ data_limit, price })));
203345
}
204346
};
205347

206348
const removeTier = (index: number) => {
207-
const newTier = currentTiers.filter((_, i) => i !== index);
208-
setCurrentTiers(newTier);
209-
onChange(newTier);
349+
const newTiers = currentTiers.filter((_, i) => i !== index);
350+
setCurrentTiers(newTiers);
351+
onChange(newTiers.map(({ data_limit, price }) => ({ data_limit, price })));
210352
};
211353

212354
const toggleFreeTier = (checked: boolean) => {
213355
onFreeTierChange(checked, checked ? freeTierLimit : '100 MB per month');
214356
};
215357

216-
const updateFreeTierLimit = (value: string) => {
217-
const formattedValue = value.includes('per month') ? value : `${value} per month`;
218-
onFreeTierChange(freeTierEnabled, formattedValue);
358+
// Fixed type signature for InputNumber's onChange
359+
const updateFreeTierAmount = (value: string | number | null) => {
360+
if (value === null) return;
361+
362+
const numValue = typeof value === 'string' ? parseInt(value, 10) : value;
363+
setFreeTierAmount(numValue);
364+
const newLimit = formatDataLimit(numValue, freeTierUnit);
365+
onFreeTierChange(freeTierEnabled, newLimit);
366+
};
367+
368+
// Fixed type signature for Select's onChange
369+
const updateFreeTierUnit = (value: unknown) => {
370+
const unit = value as 'MB' | 'GB';
371+
setFreeTierUnit(unit);
372+
const newLimit = formatDataLimit(freeTierAmount, unit);
373+
onFreeTierChange(freeTierEnabled, newLimit);
219374
};
220375

221376
return (
@@ -248,20 +403,22 @@ const SubscriptionTiersManager: React.FC<SubscriptionTiersManagerProps> = ({
248403
<DatabaseOutlined />
249404
<InputLabel>Data Limit</InputLabel>
250405
</InputIcon>
251-
<Input
252-
value={freeTierLimit}
253-
onChange={(e) => updateFreeTierLimit(e.target.value)}
254-
placeholder="e.g., 100 MB per month"
255-
style={{
256-
width: '100%',
257-
backgroundColor: '#1b1b38',
258-
borderColor: '#313131',
259-
color: 'white',
260-
height: '48px',
261-
borderRadius: '8px'
262-
}}
263-
prefix={<DatabaseOutlined style={{ color: '#a9a9c8' }} />}
264-
/>
406+
<DataLimitInputGroup>
407+
<StyledInputNumber
408+
min={1}
409+
value={freeTierAmount}
410+
onChange={updateFreeTierAmount}
411+
prefix={<DatabaseOutlined style={{ color: '#a9a9c8' }} />}
412+
/>
413+
<StyledSelect
414+
value={freeTierUnit}
415+
onChange={updateFreeTierUnit}
416+
options={[
417+
{ value: 'MB', label: 'MB' },
418+
{ value: 'GB', label: 'GB' }
419+
]}
420+
/>
421+
</DataLimitInputGroup>
265422
</InputGroup>
266423

267424
<InputGroup style={{ flex: 1 }}>
@@ -311,20 +468,22 @@ const SubscriptionTiersManager: React.FC<SubscriptionTiersManagerProps> = ({
311468
<DatabaseOutlined />
312469
<InputLabel>Data Limit</InputLabel>
313470
</InputIcon>
314-
<Input
315-
value={tier.data_limit}
316-
onChange={(e) => handleUpdateTier(index, 'data_limit', e.target.value)}
317-
placeholder="e.g., 1 GB per month"
318-
style={{
319-
width: '100%',
320-
backgroundColor: '#1b1b38',
321-
borderColor: '#313131',
322-
color: 'white',
323-
height: '48px',
324-
borderRadius: '8px'
325-
}}
326-
prefix={<DatabaseOutlined style={{ color: '#a9a9c8' }} />}
327-
/>
471+
<DataLimitInputGroup>
472+
<StyledInputNumber
473+
min={1}
474+
value={tier.amount}
475+
onChange={(value) => handleUpdateTierAmount(index, value)}
476+
prefix={<DatabaseOutlined style={{ color: '#a9a9c8' }} />}
477+
/>
478+
<StyledSelect
479+
value={tier.unit}
480+
onChange={(value) => handleUpdateTierUnit(index, value)}
481+
options={[
482+
{ value: 'MB', label: 'MB' },
483+
{ value: 'GB', label: 'GB' }
484+
]}
485+
/>
486+
</DataLimitInputGroup>
328487
</InputGroup>
329488

330489
<InputGroup style={{ flex: 1 }}>
@@ -335,7 +494,7 @@ const SubscriptionTiersManager: React.FC<SubscriptionTiersManagerProps> = ({
335494
<Input
336495
type="number"
337496
value={tier.price}
338-
onChange={(e) => handleUpdateTier(index, 'price', e.target.value)}
497+
onChange={(e) => handleUpdateTierPrice(index, e.target.value)}
339498
placeholder="Price in sats"
340499
style={{
341500
width: '100%',

0 commit comments

Comments
 (0)