@wecodesolutions/shared-react-components-ts 0.17.2 → 0.17.5
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/chart/Chart.d.ts +9 -8
- package/dist/shared-react-components-ts.cjs.development.js +220 -25
- package/dist/shared-react-components-ts.cjs.development.js.map +1 -1
- package/dist/shared-react-components-ts.cjs.production.min.js +1 -1
- package/dist/shared-react-components-ts.cjs.production.min.js.map +1 -1
- package/dist/shared-react-components-ts.esm.js +222 -27
- package/dist/shared-react-components-ts.esm.js.map +1 -1
- package/package.json +2 -1
- package/src/components/chart/Chart.tsx +213 -38
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
import React from 'react';
|
|
1
|
+
import React, { useMemo, useState } from 'react';
|
|
3
2
|
import {
|
|
4
3
|
LineChart,
|
|
5
4
|
Line,
|
|
@@ -7,48 +6,220 @@ import {
|
|
|
7
6
|
YAxis,
|
|
8
7
|
CartesianGrid,
|
|
9
8
|
Tooltip,
|
|
10
|
-
Legend,
|
|
11
9
|
ResponsiveContainer,
|
|
12
10
|
} from 'recharts';
|
|
11
|
+
import { format } from 'date-fns';
|
|
13
12
|
import styles from './Chart.module.css';
|
|
14
13
|
|
|
14
|
+
type CurrencyMap = Record<string, number>;
|
|
15
|
+
|
|
16
|
+
type EarningsDoc = {
|
|
17
|
+
days: Record<string, CurrencyMap>;
|
|
18
|
+
months: Record<string, CurrencyMap>;
|
|
19
|
+
years: Record<string, CurrencyMap>;
|
|
20
|
+
totalAllTime: CurrencyMap;
|
|
21
|
+
updatedAt?: any;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
type ChartPoint = { date: string; profit: number };
|
|
25
|
+
|
|
15
26
|
interface ChartProps {
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
27
|
+
earnings: EarningsDoc | null;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
type ViewMode = 'days' | 'months' | 'years';
|
|
31
|
+
type RangeMode = '1M' | '3M' | '6M' | '1Y' | '2Y' | '5Y' | 'ALL';
|
|
32
|
+
|
|
33
|
+
function parseKeyToDate(mode: ViewMode, key: string): Date | null {
|
|
34
|
+
// keys are UTC-like strings. We'll treat them as UTC.
|
|
35
|
+
if (mode === 'days') return new Date(`${key}T00:00:00Z`);
|
|
36
|
+
if (mode === 'months') return new Date(`${key}-01T00:00:00Z`);
|
|
37
|
+
if (mode === 'years') return new Date(`${key}-01-01T00:00:00Z`);
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function rangeToStart(range: RangeMode): Date | null {
|
|
42
|
+
const end = new Date();
|
|
43
|
+
const d = new Date(end);
|
|
44
|
+
|
|
45
|
+
if (range === 'ALL') return null;
|
|
46
|
+
|
|
47
|
+
if (range === '1M') d.setMonth(d.getMonth() - 1);
|
|
48
|
+
if (range === '3M') d.setMonth(d.getMonth() - 3);
|
|
49
|
+
if (range === '6M') d.setMonth(d.getMonth() - 6);
|
|
50
|
+
if (range === '1Y') d.setFullYear(d.getFullYear() - 1);
|
|
51
|
+
if (range === '2Y') d.setFullYear(d.getFullYear() - 2);
|
|
52
|
+
if (range === '5Y') d.setFullYear(d.getFullYear() - 5);
|
|
53
|
+
|
|
54
|
+
return d;
|
|
19
55
|
}
|
|
20
56
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
57
|
+
function formatXAxis(mode: ViewMode, dt: Date) {
|
|
58
|
+
if (mode === 'days') return format(dt, 'MMM dd');
|
|
59
|
+
if (mode === 'months') return format(dt, 'MMM yyyy');
|
|
60
|
+
return format(dt, 'yyyy');
|
|
25
61
|
}
|
|
26
62
|
|
|
27
|
-
export const Chart: React.FC<ChartProps> = ({
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
63
|
+
export const Chart: React.FC<ChartProps> = ({ earnings }) => {
|
|
64
|
+
const [mode, setMode] = useState<ViewMode>('days');
|
|
65
|
+
const [range, setRange] = useState<RangeMode>('1Y');
|
|
66
|
+
|
|
67
|
+
const currencies = useMemo(() => {
|
|
68
|
+
if (!earnings) return [];
|
|
69
|
+
const set = new Set<string>();
|
|
70
|
+
|
|
71
|
+
for (const v of Object.values(earnings.days ?? {}))
|
|
72
|
+
Object.keys(v).forEach(c => set.add(c));
|
|
73
|
+
for (const v of Object.values(earnings.months ?? {}))
|
|
74
|
+
Object.keys(v).forEach(c => set.add(c));
|
|
75
|
+
for (const v of Object.values(earnings.years ?? {}))
|
|
76
|
+
Object.keys(v).forEach(c => set.add(c));
|
|
77
|
+
Object.keys(earnings.totalAllTime ?? {}).forEach(c => set.add(c));
|
|
78
|
+
|
|
79
|
+
return Array.from(set).sort();
|
|
80
|
+
}, [earnings]);
|
|
81
|
+
|
|
82
|
+
const [currency, setCurrency] = useState<string>('');
|
|
83
|
+
|
|
84
|
+
// Keep currency synced when earnings loads
|
|
85
|
+
React.useEffect(() => {
|
|
86
|
+
if (!currency && currencies.length) setCurrency(currencies[0]);
|
|
87
|
+
}, [currencies, currency]);
|
|
88
|
+
|
|
89
|
+
const data: ChartPoint[] = useMemo(() => {
|
|
90
|
+
if (!earnings || !currency) return [];
|
|
91
|
+
|
|
92
|
+
const bucket =
|
|
93
|
+
mode === 'days'
|
|
94
|
+
? earnings.days
|
|
95
|
+
: mode === 'months'
|
|
96
|
+
? earnings.months
|
|
97
|
+
: earnings.years;
|
|
98
|
+
|
|
99
|
+
const start = rangeToStart(range);
|
|
100
|
+
const items: Array<{ dt: Date; value: number }> = [];
|
|
101
|
+
|
|
102
|
+
for (const [key, map] of Object.entries(bucket ?? {})) {
|
|
103
|
+
const dt = parseKeyToDate(mode, key);
|
|
104
|
+
if (!dt) continue;
|
|
105
|
+
|
|
106
|
+
if (start && dt < start) continue;
|
|
107
|
+
|
|
108
|
+
const val = Number(map?.[currency] ?? 0) || 0;
|
|
109
|
+
items.push({ dt, value: val });
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
items.sort((a, b) => a.dt.getTime() - b.dt.getTime());
|
|
113
|
+
|
|
114
|
+
let cumulative = 0;
|
|
115
|
+
return items.map(({ dt, value }) => {
|
|
116
|
+
cumulative += value;
|
|
117
|
+
return { date: formatXAxis(mode, dt), profit: cumulative };
|
|
118
|
+
});
|
|
119
|
+
}, [earnings, currency, mode, range]);
|
|
120
|
+
|
|
121
|
+
const earnedAmount = useMemo(() => {
|
|
122
|
+
if (!data.length) return 0;
|
|
123
|
+
return data[data.length - 1].profit ?? 0;
|
|
124
|
+
}, [data]);
|
|
125
|
+
|
|
126
|
+
const formatCurrency = (value: number): string =>
|
|
127
|
+
`${currency ? currency + ' ' : '$'}${value.toLocaleString()}`;
|
|
128
|
+
|
|
129
|
+
const rangeOptions: Array<{
|
|
130
|
+
value: RangeMode;
|
|
131
|
+
label: string;
|
|
132
|
+
}> = useMemo(() => {
|
|
133
|
+
if (mode === 'days')
|
|
134
|
+
return [
|
|
135
|
+
{ value: '1M', label: '1M' },
|
|
136
|
+
{ value: '3M', label: '3M' },
|
|
137
|
+
{ value: '6M', label: '6M' },
|
|
138
|
+
{ value: '1Y', label: '1Y' },
|
|
139
|
+
{ value: 'ALL', label: 'ALL' },
|
|
140
|
+
];
|
|
141
|
+
if (mode === 'months')
|
|
142
|
+
return [
|
|
143
|
+
{ value: '6M', label: '6M' },
|
|
144
|
+
{ value: '1Y', label: '1Y' },
|
|
145
|
+
{ value: '2Y', label: '2Y' },
|
|
146
|
+
{ value: '5Y', label: '5Y' },
|
|
147
|
+
{ value: 'ALL', label: 'ALL' },
|
|
148
|
+
];
|
|
149
|
+
return [
|
|
150
|
+
{ value: '1Y', label: '1Y' },
|
|
151
|
+
{ value: '2Y', label: '2Y' },
|
|
152
|
+
{ value: '5Y', label: '5Y' },
|
|
153
|
+
{ value: 'ALL', label: 'ALL' },
|
|
154
|
+
];
|
|
155
|
+
}, [mode]);
|
|
156
|
+
|
|
157
|
+
if (!earnings) {
|
|
158
|
+
return <div className={styles.chartContainer}>No earnings loaded yet.</div>;
|
|
159
|
+
}
|
|
36
160
|
|
|
37
161
|
return (
|
|
38
162
|
<div className={styles.chartContainer}>
|
|
39
|
-
<
|
|
163
|
+
<div
|
|
164
|
+
style={{
|
|
165
|
+
display: 'flex',
|
|
166
|
+
gap: 12,
|
|
167
|
+
alignItems: 'center',
|
|
168
|
+
marginBottom: 12,
|
|
169
|
+
flexWrap: 'wrap',
|
|
170
|
+
}}
|
|
171
|
+
>
|
|
172
|
+
<h2 className={styles.title} style={{ margin: 0 }}>
|
|
173
|
+
Earnings
|
|
174
|
+
</h2>
|
|
175
|
+
|
|
176
|
+
<label>
|
|
177
|
+
View:
|
|
178
|
+
<select
|
|
179
|
+
value={mode}
|
|
180
|
+
onChange={e => setMode(e.target.value as ViewMode)}
|
|
181
|
+
>
|
|
182
|
+
<option value="days">Days</option>
|
|
183
|
+
<option value="months">Months</option>
|
|
184
|
+
<option value="years">Years</option>
|
|
185
|
+
</select>
|
|
186
|
+
</label>
|
|
187
|
+
|
|
188
|
+
<label>
|
|
189
|
+
Range:
|
|
190
|
+
<select
|
|
191
|
+
value={range}
|
|
192
|
+
onChange={e => setRange(e.target.value as RangeMode)}
|
|
193
|
+
>
|
|
194
|
+
{rangeOptions.map(o => (
|
|
195
|
+
<option key={o.value} value={o.value}>
|
|
196
|
+
{o.label}
|
|
197
|
+
</option>
|
|
198
|
+
))}
|
|
199
|
+
</select>
|
|
200
|
+
</label>
|
|
201
|
+
|
|
202
|
+
<label>
|
|
203
|
+
Currency:
|
|
204
|
+
<select value={currency} onChange={e => setCurrency(e.target.value)}>
|
|
205
|
+
{currencies.map(c => (
|
|
206
|
+
<option key={c} value={c}>
|
|
207
|
+
{c}
|
|
208
|
+
</option>
|
|
209
|
+
))}
|
|
210
|
+
</select>
|
|
211
|
+
</label>
|
|
212
|
+
</div>
|
|
213
|
+
|
|
40
214
|
<div className={styles.chartSummary}>
|
|
41
215
|
<div className={styles.summaryItem}>
|
|
42
|
-
<h3>
|
|
43
|
-
<p>{formatCurrency(startingAmount)}</p>
|
|
44
|
-
</div>
|
|
45
|
-
<div className={styles.summaryItem}>
|
|
46
|
-
<h3>Earned Amount</h3>
|
|
216
|
+
<h3>Total Earned</h3>
|
|
47
217
|
<p>{formatCurrency(earnedAmount)}</p>
|
|
48
218
|
</div>
|
|
49
219
|
</div>
|
|
220
|
+
|
|
50
221
|
<div className={styles.chartArea}>
|
|
51
|
-
<ResponsiveContainer width="100%" height={
|
|
222
|
+
<ResponsiveContainer width="100%" height={450}>
|
|
52
223
|
<LineChart
|
|
53
224
|
data={data}
|
|
54
225
|
margin={{ top: 20, right: 20, left: -15, bottom: 20 }}
|
|
@@ -61,26 +232,30 @@ export const Chart: React.FC<ChartProps> = ({
|
|
|
61
232
|
textAnchor="end"
|
|
62
233
|
height={60}
|
|
63
234
|
/>
|
|
64
|
-
<YAxis
|
|
235
|
+
<YAxis
|
|
236
|
+
tick={{ fontSize: 12 }}
|
|
237
|
+
tickFormatter={(v: number) => formatCurrency(v)}
|
|
238
|
+
/>
|
|
65
239
|
<Tooltip formatter={(value: number) => formatCurrency(value)} />
|
|
66
|
-
|
|
240
|
+
|
|
67
241
|
<Line
|
|
68
242
|
type="monotone"
|
|
69
243
|
dataKey="profit"
|
|
70
|
-
name="Cumulative
|
|
71
|
-
stroke="#
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
type="monotone"
|
|
76
|
-
dataKey="depositMoney"
|
|
77
|
-
name="Cumulative Deposited Money"
|
|
78
|
-
stroke="#8884d8"
|
|
79
|
-
strokeDasharray="6 1"
|
|
244
|
+
name="Cumulative Earnings"
|
|
245
|
+
stroke="#4CAF50"
|
|
246
|
+
strokeWidth={3}
|
|
247
|
+
dot={false}
|
|
248
|
+
activeDot={{ r: 6 }}
|
|
80
249
|
/>
|
|
81
250
|
</LineChart>
|
|
82
251
|
</ResponsiveContainer>
|
|
83
252
|
</div>
|
|
253
|
+
|
|
254
|
+
{!data.length && (
|
|
255
|
+
<div style={{ marginTop: 8, opacity: 0.8 }}>
|
|
256
|
+
No data for selected view/range.
|
|
257
|
+
</div>
|
|
258
|
+
)}
|
|
84
259
|
</div>
|
|
85
260
|
);
|
|
86
261
|
};
|