1
1
import { FaUsers } from "react-icons/fa6" ;
2
2
import { Bar , BarChart , CartesianGrid , ResponsiveContainer , Tooltip , TooltipProps , XAxis , YAxis } from "recharts" ;
3
3
import { NameType , ValueType } from "recharts/types/component/DefaultTooltipContent" ;
4
- import { useMemo } from "react" ;
4
+ import { useMemo , useState } from "react" ;
5
5
import Card from "components/atoms/Card/card" ;
6
6
import SkeletonWrapper from "components/atoms/SkeletonLoader/skeleton-wrapper" ;
7
7
import humanizeNumber from "lib/utils/humanizeNumber" ;
@@ -11,34 +11,82 @@ type RossChartProps = {
11
11
isLoading : boolean ;
12
12
error : Error | undefined ;
13
13
range : number ;
14
- rangedTotal : number ;
15
14
className ?: string ;
16
15
} ;
17
16
18
- export default function RossChart ( { stats, rangedTotal, isLoading, error, range, className } : RossChartProps ) {
19
- const rangedAverage = useMemo ( ( ) => ( rangedTotal / range ) . toPrecision ( 2 ) , [ rangedTotal , range ] ) ;
17
+ export default function RossChart ( { stats, isLoading, error, range, className } : RossChartProps ) {
18
+ const [ filterOutside , setFilterOutside ] = useState ( true ) ;
19
+ const [ filterRecurring , setFilterRecurring ] = useState ( true ) ;
20
+ const [ filterInternal , setFilterInternal ] = useState ( true ) ;
21
+
22
+ const filteredTotal = useMemo ( ( ) => {
23
+ return (
24
+ stats ?. contributors . reduce ( ( prev , curr ) => {
25
+ return ( prev +=
26
+ ( filterOutside ? curr . new : 0 ) +
27
+ ( filterRecurring ? curr . recurring : 0 ) +
28
+ ( filterInternal ? curr . internal : 0 ) ) ;
29
+ } , 0 ) || 0
30
+ ) ;
31
+ } , [ stats , filterOutside , filterRecurring , filterInternal ] ) ;
32
+
33
+ const rangedAverage = useMemo (
34
+ ( ) => ( filteredTotal / ( stats ? stats . contributors . length : 1 ) ) . toPrecision ( 2 ) ,
35
+ [ filteredTotal , stats ]
36
+ ) ;
20
37
21
38
const weeklyData = useMemo ( ( ) => {
22
- const result = stats ?. contributors . reverse ( ) . map ( ( week ) => {
39
+ return stats ?. contributors . reverse ( ) . map ( ( week ) => {
23
40
return {
24
- ...week ,
41
+ new : filterOutside ? week . new : 0 ,
42
+ recurring : filterRecurring ? week . recurring : 0 ,
43
+ internal : filterInternal ? week . internal : 0 ,
25
44
bucket : new Date ( week . bucket ) . toLocaleDateString ( undefined , { month : "numeric" , day : "numeric" } ) ,
26
45
} ;
27
46
} ) ;
28
-
29
- return result ;
30
- } , [ stats ] ) ;
47
+ } , [ stats , filterOutside , filterRecurring , filterInternal ] ) ;
31
48
32
49
const bucketTicks = useMemo ( ( ) => {
33
- const result = stats ?. contributors . reverse ( ) . map ( ( week ) => {
50
+ return stats ?. contributors . reverse ( ) . map ( ( week ) => {
34
51
return new Date ( week . bucket ) . toLocaleDateString ( undefined , { month : "numeric" , day : "numeric" } ) ;
35
52
} ) ;
36
-
37
- return result ;
38
53
} , [ stats ] ) ;
39
54
55
+ const CONTRIBUTOR_COLORS : Record < string , string > = {
56
+ internal : "#1E3A8A" ,
57
+ recurring : "#2563EB" ,
58
+ new : "#60A5FA" ,
59
+ } ;
60
+
61
+ function CustomTooltip ( { active, payload } : TooltipProps < ValueType , NameType > ) {
62
+ if ( active && payload ) {
63
+ const legend = payload . reverse ( ) ;
64
+ return (
65
+ < figcaption className = "flex flex-col gap-1 text-sm bg-white px-4 py-3 rounded-lg border w-fit" >
66
+ < section className = "flex gap-2 font-medium text-slate-500 items-center text-xs w-fit" >
67
+ < FaUsers className = "fill-slate-500" />
68
+ < p > Contributors</ p >
69
+ < p > { payload [ 0 ] ?. payload . bucket } </ p >
70
+ </ section >
71
+
72
+ { legend . map ( ( data ) => (
73
+ < section key = { `${ data . payload . bucket } _${ data . name } ` } className = "flex justify-between" >
74
+ < p className = "flex gap-2 items-center px-1 text-slate-500 capitalize" >
75
+ < span
76
+ className = { `w-2 h-2 rounded-full bg-[${ CONTRIBUTOR_COLORS [ data . name || "new" ] } ] inline-block` }
77
+ > </ span >
78
+ { data . name === "new" ? "Outside" : data . name } :
79
+ </ p >
80
+ < p className = "font-medium pl-2" > { data . value } </ p >
81
+ </ section >
82
+ ) ) }
83
+ </ figcaption >
84
+ ) ;
85
+ }
86
+ }
87
+
40
88
return (
41
- < Card className = { `${ className ?? "" } flex flex-col gap-8 w-full h-full items-center !px-6 !py-8` } >
89
+ < Card className = { `${ className ?? "" } flex flex-col gap-6 w-full h-full items-center !px-6 !py-8` } >
42
90
< section className = "flex flex-col lg:flex-row w-full items-start lg:items-start gap-4 lg:justify-between px-2" >
43
91
{ isLoading ? (
44
92
< SkeletonWrapper width = { 100 } height = { 24 } />
@@ -54,10 +102,10 @@ export default function RossChart({ stats, rangedTotal, isLoading, error, range,
54
102
< aside className = "flex gap-8" >
55
103
< div >
56
104
< h3 className = "text-xs xl:text-sm text-slate-500" > Total { range } days</ h3 >
57
- < p className = "font-semibold text-xl xl:text-3xl" > { rangedTotal } </ p >
105
+ < p className = "font-semibold text-xl xl:text-3xl" > { filteredTotal } </ p >
58
106
</ div >
59
107
< div >
60
- < h3 className = "text-xs xl:text-sm text-slate-500" > Average per day </ h3 >
108
+ < h3 className = "text-xs xl:text-sm text-slate-500" > Average per week </ h3 >
61
109
< p className = "font-semibold text-xl xl:text-3xl" > { humanizeNumber ( rangedAverage ) } </ p >
62
110
</ div >
63
111
</ aside >
@@ -79,57 +127,48 @@ export default function RossChart({ stats, rangedTotal, isLoading, error, range,
79
127
/>
80
128
< Tooltip content = { CustomTooltip } filterNull = { false } />
81
129
< CartesianGrid vertical = { false } strokeDasharray = "4" stroke = "#E2E8F0" />
82
- < Bar dataKey = "internal" stackId = "a" fill = "#1E3A8A" />
83
- < Bar dataKey = "recurring" stackId = "a" fill = "#2563EB" />
84
- < Bar dataKey = "new" stackId = "a" fill = "#60A5FA" />
130
+ { filterInternal && < Bar dataKey = "internal" stackId = "a" fill = { CONTRIBUTOR_COLORS [ "internal" ] } /> }
131
+ { filterRecurring && < Bar dataKey = "recurring" stackId = "a" fill = { CONTRIBUTOR_COLORS [ "recurring" ] } /> }
132
+ { filterOutside && < Bar dataKey = "new" stackId = "a" fill = { CONTRIBUTOR_COLORS [ "new" ] } /> }
85
133
</ BarChart >
86
134
) }
87
135
</ ResponsiveContainer >
136
+
137
+ < fieldset className = "flex flex-row gap-4 w-fit text-sm mx-auto p-0" >
138
+ < button
139
+ onClick = { ( ) => setFilterOutside ( ! filterOutside ) }
140
+ className = { `flex gap-2 h-full items-center text-slate-700 ${
141
+ ! filterOutside && "opacity-60"
142
+ } transition-all duration-300 hover:bg-slate-100 rounded-lg px-2 py-1`}
143
+ >
144
+ < span className = { `w-4 h-4 rounded-sm bg-[#60A5FA] inline-block` } />
145
+ Outside
146
+ </ button >
147
+
148
+ < button
149
+ onClick = { ( ) => setFilterRecurring ( ! filterRecurring ) }
150
+ className = { `flex gap-2 h-full items-center text-slate-700 ${
151
+ ! filterRecurring && "opacity-60"
152
+ } transition-all duration-300 hover:bg-slate-100 rounded-lg px-2 py-1`}
153
+ >
154
+ < span className = { `w-4 h-4 rounded-sm bg-[#2563EB] inline-block` } />
155
+ Recurring
156
+ </ button >
157
+
158
+ < button
159
+ onClick = { ( ) => setFilterInternal ( ! filterInternal ) }
160
+ className = { `flex gap-2 h-full items-center text-slate-700 ${
161
+ ! filterInternal && "opacity-60"
162
+ } transition-all duration-300 hover:bg-slate-100 rounded-lg px-2 py-1`}
163
+ >
164
+ < span className = { `w-4 h-4 rounded-sm bg-[#1E3A8A] inline-block` } />
165
+ Internal
166
+ </ button >
167
+ </ fieldset >
88
168
</ Card >
89
169
) ;
90
170
}
91
171
92
- function CustomTooltip ( { active, payload } : TooltipProps < ValueType , NameType > ) {
93
- if ( active && payload ) {
94
- return (
95
- < figcaption className = "flex flex-col gap-1 text-sm bg-white px-4 py-3 rounded-lg border w-fit" >
96
- < section className = "flex gap-2 font-medium text-slate-500 items-center text-xs w-fit" >
97
- < FaUsers className = "fill-slate-500" />
98
- < p > Contributors</ p >
99
- < p > { payload [ 0 ] ?. payload . bucket } </ p >
100
- </ section >
101
- { payload [ 2 ] ?. value && (
102
- < section className = "flex justify-between" >
103
- < p className = "flex gap-2 items-center px-1 text-slate-500" >
104
- < span className = { `w-2 h-2 rounded-full bg-[#60A5FA] inline-block` } > </ span >
105
- New:
106
- </ p >
107
- < p className = "font-medium pl-2" > { payload [ 2 ] ?. value } </ p >
108
- </ section >
109
- ) }
110
- { payload [ 1 ] ?. value && (
111
- < section className = "flex justify-between" >
112
- < p className = "flex gap-2 items-center px-1 text-slate-500" >
113
- < span className = { `w-2 h-2 rounded-full bg-[#2563EB] inline-block` } > </ span >
114
- Recurring:
115
- </ p >
116
- < p className = "font-medium pl-2" > { payload [ 1 ] ?. value } </ p >
117
- </ section >
118
- ) }
119
- { payload [ 0 ] ?. value && (
120
- < section className = "flex justify-between" >
121
- < p className = "flex gap-2 items-center px-1 text-slate-500" >
122
- < span className = { `w-2 h-2 rounded-full bg-[#1E3A8A] inline-block` } > </ span >
123
- Internal:
124
- </ p >
125
- < p className = "font-medium pl-2" > { payload [ 0 ] ?. value } </ p >
126
- </ section >
127
- ) }
128
- </ figcaption >
129
- ) ;
130
- }
131
- }
132
-
133
172
function CustomTick ( { x, y, payload } : { x : number ; y : number ; payload : { value : string } } ) {
134
173
return (
135
174
< g transform = { `translate(${ x } ,${ y } )` } >
0 commit comments