@@ -32,16 +32,18 @@ const STACK_ORDER: string[] = [
3232] as const ;
3333
3434type StackKey = ( typeof STACK_ORDER ) [ number ] ;
35-
36- function createRoundedTopShape ( currentKey : StackKey ) {
37- const currentIndex = STACK_ORDER . indexOf ( currentKey ) ;
35+ function createRoundedTopShape (
36+ currentKey : StackKey ,
37+ visibleStackOrder : StackKey [ ] ,
38+ ) {
39+ const currentIndex = visibleStackOrder . indexOf ( currentKey ) ;
3840 return ( props : any ) => {
3941 const payload = props . payload as
4042 | Partial < Record < StackKey , number > >
4143 | undefined ;
4244 let hasAbove = false ;
43- for ( let i = currentIndex + 1 ; i < STACK_ORDER . length ; i ++ ) {
44- const key = STACK_ORDER [ i ] ;
45+ for ( let i = currentIndex + 1 ; i < visibleStackOrder . length ; i ++ ) {
46+ const key = visibleStackOrder [ i ] ;
4547 const val = key ? ( payload ?. [ key ] ?? 0 ) : 0 ;
4648 if ( val > 0 ) {
4749 hasAbove = true ;
@@ -55,6 +57,7 @@ function createRoundedTopShape(currentKey: StackKey) {
5557}
5658
5759export default function EmailChart ( { days, domain } : EmailChartProps ) {
60+ const [ selectedMetrics , setSelectedMetrics ] = React . useState < StackKey [ ] > ( [ ] ) ;
5861 const domainId = domain ? Number ( domain ) : undefined ;
5962 const statusQuery = api . dashboard . emailTimeSeries . useQuery ( {
6063 days : days ,
@@ -63,6 +66,32 @@ export default function EmailChart({ days, domain }: EmailChartProps) {
6366
6467 const currentColors = useColors ( ) ;
6568
69+ const metricMeta : Record < StackKey , { label : string ; color : string } > = {
70+ delivered : { label : "Delivered" , color : currentColors . delivered } ,
71+ bounced : { label : "Bounced" , color : currentColors . bounced } ,
72+ complained : { label : "Complained" , color : currentColors . complained } ,
73+ opened : { label : "Opened" , color : currentColors . opened } ,
74+ clicked : { label : "Clicked" , color : currentColors . clicked } ,
75+ } ;
76+
77+ const visibleMetrics =
78+ selectedMetrics . length === 0
79+ ? STACK_ORDER
80+ : STACK_ORDER . filter ( ( key ) => selectedMetrics . includes ( key ) ) ;
81+
82+ const toggleMetric = ( metric : StackKey ) => {
83+ setSelectedMetrics ( ( prev ) => {
84+ const exists = prev . includes ( metric ) ;
85+
86+ if ( exists ) {
87+ return prev . filter ( ( key ) => key !== metric ) ;
88+ }
89+
90+ const nextSet = new Set ( [ ...prev , metric ] ) ;
91+ return STACK_ORDER . filter ( ( key ) => nextSet . has ( key ) ) ;
92+ } ) ;
93+ } ;
94+
6695 return (
6796 < div className = "flex flex-col gap-16" >
6897 { ! statusQuery . isLoading && statusQuery . data ? (
@@ -75,6 +104,8 @@ export default function EmailChart({ days, domain }: EmailChartProps) {
75104 status = { "total" }
76105 count = { statusQuery . data . totalCounts . sent }
77106 percentage = { 100 }
107+ isActive = { selectedMetrics . length === 0 }
108+ isClickable = { false }
78109 />
79110 < EmailChartItem
80111 status = { EmailStatus . DELIVERED }
@@ -83,6 +114,11 @@ export default function EmailChart({ days, domain }: EmailChartProps) {
83114 statusQuery . data . totalCounts . delivered /
84115 statusQuery . data . totalCounts . sent
85116 }
117+ isActive = {
118+ selectedMetrics . length === 0 ||
119+ selectedMetrics . includes ( "delivered" )
120+ }
121+ onClick = { ( ) => toggleMetric ( "delivered" ) }
86122 />
87123 < EmailChartItem
88124 status = { EmailStatus . BOUNCED }
@@ -91,6 +127,11 @@ export default function EmailChart({ days, domain }: EmailChartProps) {
91127 statusQuery . data . totalCounts . bounced /
92128 statusQuery . data . totalCounts . sent
93129 }
130+ isActive = {
131+ selectedMetrics . length === 0 ||
132+ selectedMetrics . includes ( "bounced" )
133+ }
134+ onClick = { ( ) => toggleMetric ( "bounced" ) }
94135 />
95136 < EmailChartItem
96137 status = { EmailStatus . COMPLAINED }
@@ -99,6 +140,11 @@ export default function EmailChart({ days, domain }: EmailChartProps) {
99140 statusQuery . data . totalCounts . complained /
100141 statusQuery . data . totalCounts . sent
101142 }
143+ isActive = {
144+ selectedMetrics . length === 0 ||
145+ selectedMetrics . includes ( "complained" )
146+ }
147+ onClick = { ( ) => toggleMetric ( "complained" ) }
102148 />
103149 < EmailChartItem
104150 status = { EmailStatus . CLICKED }
@@ -107,6 +153,11 @@ export default function EmailChart({ days, domain }: EmailChartProps) {
107153 statusQuery . data . totalCounts . clicked /
108154 statusQuery . data . totalCounts . sent
109155 }
156+ isActive = {
157+ selectedMetrics . length === 0 ||
158+ selectedMetrics . includes ( "clicked" )
159+ }
160+ onClick = { ( ) => toggleMetric ( "clicked" ) }
110161 />
111162 < EmailChartItem
112163 status = { EmailStatus . OPENED }
@@ -115,6 +166,11 @@ export default function EmailChart({ days, domain }: EmailChartProps) {
115166 statusQuery . data . totalCounts . opened /
116167 statusQuery . data . totalCounts . sent
117168 }
169+ isActive = {
170+ selectedMetrics . length === 0 ||
171+ selectedMetrics . includes ( "opened" )
172+ }
173+ onClick = { ( ) => toggleMetric ( "opened" ) }
118174 />
119175 </ div >
120176 </ div >
@@ -157,11 +213,10 @@ export default function EmailChart({ days, domain }: EmailChartProps) {
157213 if ( ! data ) return null ;
158214
159215 const hasAnyData =
160- ( data . delivered || 0 ) > 0 ||
161- ( data . bounced || 0 ) > 0 ||
162- ( data . complained || 0 ) > 0 ||
163- ( data . opened || 0 ) > 0 ||
164- ( data . clicked || 0 ) > 0 ;
216+ visibleMetrics . reduce (
217+ ( sum , key ) => sum + ( data [ key ] || 0 ) ,
218+ 0 ,
219+ ) > 0 ;
165220
166221 if ( ! hasAnyData ) return null ;
167222
@@ -170,105 +225,43 @@ export default function EmailChart({ days, domain }: EmailChartProps) {
170225 < p className = "text-sm text-muted-foreground" >
171226 { data . date }
172227 </ p >
173- { data . delivered ? (
174- < div className = "flex gap-2 items-center" >
175- < div
176- className = "w-2.5 h-2.5 rounded-[2px]"
177- style = { { backgroundColor : currentColors . delivered } }
178- > </ div >
179- < p className = "text-xs text-muted-foreground w-[70px]" >
180- Delivered
181- </ p >
182- < p className = "text-xs font-mono" > { data . delivered } </ p >
183- </ div >
184- ) : null }
185- { data . bounced ? (
186- < div className = "flex gap-2 items-center" >
187- < div
188- className = "w-2.5 h-2.5 rounded-[2px]"
189- style = { { backgroundColor : currentColors . bounced } }
190- > </ div >
191- < p className = "text-xs text-muted-foreground w-[70px]" >
192- Bounced
193- </ p >
194- < p className = "text-xs font-mono" > { data . bounced } </ p >
195- </ div >
196- ) : null }
197- { data . complained ? (
198- < div className = "flex gap-2 items-center" >
199- < div
200- className = "w-2.5 h-2.5 rounded-[2px]"
201- style = { {
202- backgroundColor : currentColors . complained ,
203- } }
204- > </ div >
205- < p className = "text-xs text-muted-foreground w-[70px]" >
206- Complained
207- </ p >
208- < p className = "text-xs font-mono" > { data . complained } </ p >
209- </ div >
210- ) : null }
211- { data . opened ? (
212- < div className = "flex gap-2 items-center" >
213- < div
214- className = "w-2.5 h-2.5 rounded-[2px]"
215- style = { { backgroundColor : currentColors . opened } }
216- > </ div >
217- < p className = "text-xs text-muted-foreground w-[70px]" >
218- Opened
219- </ p >
220- < p className = "text-xs font-mono" > { data . opened } </ p >
221- </ div >
222- ) : null }
223- { data . clicked ? (
224- < div className = "flex gap-2 items-center" >
228+ { visibleMetrics . map ( ( metricKey ) => {
229+ const metricValue = data [ metricKey ] || 0 ;
230+ if ( ! metricValue ) return null ;
231+
232+ return (
225233 < div
226- className = "w-2.5 h-2.5 rounded-[2px]"
227- style = { { backgroundColor : currentColors . clicked } }
228- > </ div >
229- < p className = "text-xs text-muted-foreground w-[70px]" >
230- Clicked
231- </ p >
232- < p className = "text-xs font-mono" > { data . clicked } </ p >
233- </ div >
234- ) : null }
234+ key = { metricKey }
235+ className = "flex gap-2 items-center"
236+ >
237+ < div
238+ className = "w-2.5 h-2.5 rounded-[2px]"
239+ style = { {
240+ backgroundColor : metricMeta [ metricKey ] . color ,
241+ } }
242+ > </ div >
243+ < p className = "text-xs text-muted-foreground w-[70px]" >
244+ { metricMeta [ metricKey ] . label }
245+ </ p >
246+ < p className = "text-xs font-mono" > { metricValue } </ p >
247+ </ div >
248+ ) ;
249+ } ) }
235250 </ div >
236251 ) ;
237252 } }
238253 cursor = { false }
239254 />
240- { /* <Legend /> */ }
241- < Bar
242- barSize = { 20 }
243- dataKey = "delivered"
244- stackId = "a"
245- fill = { currentColors . delivered }
246- shape = { createRoundedTopShape ( "delivered" ) }
247- />
248- < Bar
249- dataKey = "bounced"
250- stackId = "a"
251- fill = { currentColors . bounced }
252- shape = { createRoundedTopShape ( "bounced" ) }
253- />
254- < Bar
255- dataKey = "complained"
256- stackId = "a"
257- fill = { currentColors . complained }
258- shape = { createRoundedTopShape ( "complained" ) }
259- />
260- < Bar
261- dataKey = "opened"
262- stackId = "a"
263- fill = { currentColors . opened }
264- shape = { createRoundedTopShape ( "opened" ) }
265- />
266- < Bar
267- dataKey = "clicked"
268- stackId = "a"
269- fill = { currentColors . clicked }
270- shape = { createRoundedTopShape ( "clicked" ) }
271- />
255+ { visibleMetrics . map ( ( metricKey ) => (
256+ < Bar
257+ key = { metricKey }
258+ barSize = { 20 }
259+ dataKey = { metricKey }
260+ stackId = "a"
261+ fill = { metricMeta [ metricKey ] . color }
262+ shape = { createRoundedTopShape ( metricKey , visibleMetrics ) }
263+ />
264+ ) ) }
272265 </ BarChart >
273266 </ ResponsiveContainer >
274267 </ div >
@@ -283,6 +276,9 @@ type DashboardItemCardProps = {
283276 status : EmailStatus | "total" ;
284277 count : number ;
285278 percentage : number ;
279+ onClick ?: ( ) => void ;
280+ isActive ?: boolean ;
281+ isClickable ?: boolean ;
286282} ;
287283
288284const DashboardItemCard : React . FC < DashboardItemCardProps > = ( {
@@ -314,6 +310,9 @@ const EmailChartItem: React.FC<DashboardItemCardProps> = ({
314310 status,
315311 count,
316312 percentage,
313+ onClick,
314+ isActive = false ,
315+ isClickable = true ,
317316} ) => {
318317 const currentColors = useColors ( ) ;
319318
@@ -336,7 +335,17 @@ const EmailChartItem: React.FC<DashboardItemCardProps> = ({
336335 } ;
337336
338337 return (
339- < div className = "flex gap-3 items-stretch font-mono" >
338+ < button
339+ type = "button"
340+ onClick = { onClick }
341+ disabled = { ! isClickable }
342+ aria-pressed = { isClickable ? isActive : undefined }
343+ className = { `flex gap-3 items-stretch font-mono transition-opacity ${
344+ isClickable ? "cursor-pointer" : "cursor-default"
345+ } ${ isActive ? "opacity-100" : "opacity-45 hover:opacity-100" } ${
346+ isClickable ? "" : "pointer-events-none"
347+ } `}
348+ >
340349 < div >
341350 < div className = " flex items-center gap-2" >
342351 < div
@@ -357,6 +366,6 @@ const EmailChartItem: React.FC<DashboardItemCardProps> = ({
357366 </ span >
358367 </ div >
359368 </ div >
360- </ div >
369+ </ button >
361370 ) ;
362371} ;
0 commit comments