@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.
@@ -1,5 +1,4 @@
1
- // src/components/Chart.tsx
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
- startingAmount: number;
17
- earnedAmount: number;
18
- data: ChartData[];
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
- interface ChartData {
22
- date: string;
23
- profit: number;
24
- depositMoney: number;
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
- startingAmount,
29
- earnedAmount,
30
- data,
31
- }) => {
32
- // Formatter for currency display
33
- const formatCurrency = (value: number): string => {
34
- return `$${value.toLocaleString()}`;
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
- <h2 className={styles.title}>Profit vs. Deposited Money (Daily)</h2>
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:&nbsp;
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:&nbsp;
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:&nbsp;
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>Starting Amount</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={500}>
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 tick={{ fontSize: 12 }} />
235
+ <YAxis
236
+ tick={{ fontSize: 12 }}
237
+ tickFormatter={(v: number) => formatCurrency(v)}
238
+ />
65
239
  <Tooltip formatter={(value: number) => formatCurrency(value)} />
66
- <Legend verticalAlign="top" height={36} />
240
+
67
241
  <Line
68
242
  type="monotone"
69
243
  dataKey="profit"
70
- name="Cumulative Profit"
71
- stroke="#82ca9d"
72
- activeDot={{ r: 8 }}
73
- />
74
- <Line
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
  };