@@ -3,16 +3,16 @@ import { useState, useEffect } from "react";
33import { useRouter } from "next/navigation" ;
44import { addToast } from "@heroui/toast" ;
55import React from "react" ;
6+ import { Sankey , ResponsiveContainer , Tooltip } from "recharts" ;
67
78import JobApplicationsDashboard , { Application } from "@/components/JobApplicationsDashboard" ;
89import ResponseRateCard from "@/components/response_rate_card" ;
910import UniqueOpenRateChart from "@/components/response_rate_chart" ;
1011import { checkAuth } from "@/utils/auth" ;
11- import { Sankey , ResponsiveContainer , Tooltip } from "recharts" ;
1212
1313interface SankeyData {
14- nodes : { name : string } [ ] ;
15- links : { source : number ; target : number ; value : number } [ ] ;
14+ nodes : { name : string } [ ] ;
15+ links : { source : number ; target : number ; value : number } [ ] ;
1616}
1717
1818export default function Dashboard ( ) {
@@ -245,148 +245,149 @@ export default function Dashboard() {
245245 </ div >
246246 </ >
247247 ) ;
248-
249-
250- const sankeyChartContent = sankeyData && sankeyData . nodes && sankeyData . nodes . length > 0 ? (
251- < div className = "bg-gray-100 dark:bg-gray-800 p-6 shadow-md rounded-lg" >
252- < div className = "flex items-center justify-between mb-4" >
253- < h2 className = "text-gray-700 dark:text-gray-300 text-base md:text-xl font-semibold" > My Job Search</ h2 >
254- < button
255- onClick = { downloadSankey }
256- disabled = { downloading }
257- className = "px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700 disabled:opacity-50 disabled:cursor-not-allowed text-sm"
258- >
259- { downloading ? "Downloading..." : "Download PNG" }
260- </ button >
261- </ div >
262- < div className = "h-[450px] w-full relative" >
263- < ResponsiveContainer width = "100%" height = "100%" >
264- < Sankey
265- data = { sankeyData }
266- nodePadding = { 24 }
267- margin = { { top : 10 , bottom : 30 , left : 10 , right : 10 } } // Smaller margins
268- link = { { stroke : "#cbd5e1" , strokeOpacity : 0.4 } }
269- node = { ( props ) => {
270- const isSource = props . x < 50 ;
271- const nodeName = props . payload . name ;
272- const status = nodeName . split ( ' (' ) [ 0 ] ;
273-
274- const getStatusColor = ( status : string ) => {
275- const normalized = status . toLowerCase ( ) ;
276- switch ( normalized ) {
277- case "applications" :
278- return "#3b82f6" ; // Blue for source
279- case "offer made" :
280- return "#10b981" ;
281- case "rejection" :
282- return "#ef4444" ;
283- case "interview invitation" :
284- return "#06b6d4" ;
285- case "assessment sent" :
286- return "#8b5cf6" ;
287- case "availability request" :
288- return "#f59e0b" ;
289- case "application confirmation" :
290- return "#3b82f6" ;
291- case "information request" :
292- return "#6b7280" ;
293- case "inbound request" :
294- return "#10b981" ;
295- case "action required" :
296- return "#84cc16" ;
297- case "hiring freeze" :
298- return "#8b5cf6" ;
299- case "withdrew application" :
300- return "#ec4899" ;
301- default :
302- return "#8884d8" ;
303- }
304- } ;
305-
306- return (
307- < g >
308- < rect
309- x = { props . x }
310- y = { props . y }
311- width = { props . width }
312- height = { props . height }
313- fill = { getStatusColor ( status ) }
314- fillOpacity = { 0.8 }
315- stroke = { getStatusColor ( status ) }
316- />
317- < text
318- x = { isSource ? props . x + props . width + 8 : props . x - 8 }
319- y = { props . y + props . height / 2 }
320- textAnchor = { isSource ? "start" : "end" }
321- dominantBaseline = "middle"
322- fill = "#333"
323- fontSize = { 12 }
324- >
325- { props . payload . name }
326- </ text >
327- </ g >
328- ) ;
329- } }
248+
249+ const sankeyChartContent =
250+ sankeyData && sankeyData . nodes && sankeyData . nodes . length > 0 ? (
251+ < div className = "bg-gray-100 dark:bg-gray-800 p-6 shadow-md rounded-lg" >
252+ < div className = "flex items-center justify-between mb-4" >
253+ < h2 className = "text-gray-700 dark:text-gray-300 text-base md:text-xl font-semibold" >
254+ My Job Search
255+ </ h2 >
256+ < button
257+ className = "px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700 disabled:opacity-50 disabled:cursor-not-allowed text-sm"
258+ disabled = { downloading }
259+ onClick = { downloadSankey }
330260 >
331- < Tooltip
332- content = { ( { active, payload } ) => {
333- console . log ( "test " , payload ) ;
334- if ( active && payload && payload . length ) {
335-
336- const { sourceNode, targetNode, value } = payload [ 0 ] . payload ;
337- return (
338- < div className = "bg-white p-2 rounded shadow" >
339- < div >
340- < strong >
341- { sourceNode ?. name } → { targetNode ?. name }
342- </ strong >
343- </ div >
344- < div > Count: { value } </ div >
345- </ div >
346- ) ;
347- }
348- return null ;
349- } }
350- />
351- </ Sankey >
352- </ ResponsiveContainer >
353- < div className = "absolute bottom-2 left-2 text-xs text-gray-500 opacity-70" >
354- { ( ( ) => {
355- // Calculate date range from the data
356- if ( data && data . length > 0 ) {
357- const dates = data
358- . map ( item => item . received_at ? new Date ( item . received_at ) : null )
359- . filter ( date => date !== null ) ;
360-
361- if ( dates . length > 0 ) {
362- const minDate = new Date ( Math . min ( ...dates . map ( d => d . getTime ( ) ) ) ) ;
363- const maxDate = new Date ( Math . max ( ...dates . map ( d => d . getTime ( ) ) ) ) ;
364-
365- const formatDate = ( date : Date ) => {
366- const month = date . toLocaleDateString ( 'en-US' , { month : 'long' } ) ;
367- const year = date . getFullYear ( ) ;
368- return { month, year } ;
261+ { downloading ? "Downloading..." : "Download PNG" }
262+ </ button >
263+ </ div >
264+ < div className = "h-[450px] w-full relative" >
265+ < ResponsiveContainer height = "100%" width = "100%" >
266+ < Sankey
267+ data = { sankeyData }
268+ link = { { stroke : "#cbd5e1" , strokeOpacity : 0.4 } }
269+ margin = { { top : 10 , bottom : 30 , left : 10 , right : 10 } } // Smaller margins
270+ node = { ( props ) => {
271+ const isSource = props . x < 50 ;
272+ const nodeName = props . payload . name ;
273+ const status = nodeName . split ( " (" ) [ 0 ] ;
274+
275+ const getStatusColor = ( status : string ) => {
276+ const normalized = status . toLowerCase ( ) ;
277+ switch ( normalized ) {
278+ case "applications" :
279+ return "#3b82f6" ; // Blue for source
280+ case "offer made" :
281+ return "#10b981" ;
282+ case "rejection" :
283+ return "#ef4444" ;
284+ case "interview invitation" :
285+ return "#06b6d4" ;
286+ case "assessment sent" :
287+ return "#8b5cf6" ;
288+ case "availability request" :
289+ return "#f59e0b" ;
290+ case "application confirmation" :
291+ return "#3b82f6" ;
292+ case "information request" :
293+ return "#6b7280" ;
294+ case "inbound request" :
295+ return "#10b981" ;
296+ case "action required" :
297+ return "#84cc16" ;
298+ case "hiring freeze" :
299+ return "#8b5cf6" ;
300+ case "withdrew application" :
301+ return "#ec4899" ;
302+ default :
303+ return "#8884d8" ;
304+ }
369305 } ;
370-
371- const minFormatted = formatDate ( minDate ) ;
372- const maxFormatted = formatDate ( maxDate ) ;
373-
374- let dateRange ;
375- if ( minFormatted . year === maxFormatted . year ) {
376- dateRange = `${ minFormatted . month } - ${ maxFormatted . month } ${ maxFormatted . year } ` ;
377- } else {
378- dateRange = `${ minFormatted . month } ${ minFormatted . year } - ${ maxFormatted . month } ${ maxFormatted . year } ` ;
306+
307+ return (
308+ < g >
309+ < rect
310+ fill = { getStatusColor ( status ) }
311+ fillOpacity = { 0.8 }
312+ height = { props . height }
313+ stroke = { getStatusColor ( status ) }
314+ width = { props . width }
315+ x = { props . x }
316+ y = { props . y }
317+ />
318+ < text
319+ dominantBaseline = "middle"
320+ fill = "#333"
321+ fontSize = { 12 }
322+ textAnchor = { isSource ? "start" : "end" }
323+ x = { isSource ? props . x + props . width + 8 : props . x - 8 }
324+ y = { props . y + props . height / 2 }
325+ >
326+ { props . payload . name }
327+ </ text >
328+ </ g >
329+ ) ;
330+ } }
331+ nodePadding = { 24 }
332+ >
333+ < Tooltip
334+ content = { ( { active, payload } ) => {
335+ console . log ( "test " , payload ) ;
336+ if ( active && payload && payload . length ) {
337+ const { sourceNode, targetNode, value } = payload [ 0 ] . payload ;
338+ return (
339+ < div className = "bg-white p-2 rounded shadow" >
340+ < div >
341+ < strong >
342+ { sourceNode ?. name } → { targetNode ?. name }
343+ </ strong >
344+ </ div >
345+ < div > Count: { value } </ div >
346+ </ div >
347+ ) ;
348+ }
349+ return null ;
350+ } }
351+ />
352+ </ Sankey >
353+ </ ResponsiveContainer >
354+ < div className = "absolute bottom-2 left-2 text-xs text-gray-500 opacity-70" >
355+ { ( ( ) => {
356+ // Calculate date range from the data
357+ if ( data && data . length > 0 ) {
358+ const dates = data
359+ . map ( ( item ) => ( item . received_at ? new Date ( item . received_at ) : null ) )
360+ . filter ( ( date ) => date !== null ) ;
361+
362+ if ( dates . length > 0 ) {
363+ const minDate = new Date ( Math . min ( ...dates . map ( ( d ) => d . getTime ( ) ) ) ) ;
364+ const maxDate = new Date ( Math . max ( ...dates . map ( ( d ) => d . getTime ( ) ) ) ) ;
365+
366+ const formatDate = ( date : Date ) => {
367+ const month = date . toLocaleDateString ( "en-US" , { month : "long" } ) ;
368+ const year = date . getFullYear ( ) ;
369+ return { month, year } ;
370+ } ;
371+
372+ const minFormatted = formatDate ( minDate ) ;
373+ const maxFormatted = formatDate ( maxDate ) ;
374+
375+ let dateRange ;
376+ if ( minFormatted . year === maxFormatted . year ) {
377+ dateRange = `${ minFormatted . month } - ${ maxFormatted . month } ${ maxFormatted . year } ` ;
378+ } else {
379+ dateRange = `${ minFormatted . month } ${ minFormatted . year } - ${ maxFormatted . month } ${ maxFormatted . year } ` ;
380+ }
381+
382+ return `JustAJobApp.com • ${ dateRange } ` ;
379383 }
380-
381- return `JustAJobApp.com • ${ dateRange } ` ;
382384 }
383- }
384- return "JustAJobApp.com" ;
385- } ) ( ) }
385+ return "JustAJobApp.com" ;
386+ } ) ( ) }
387+ </ div >
386388 </ div >
387389 </ div >
388- </ div >
389- ) : null ;
390+ ) : null ;
390391
391392 return (
392393 < JobApplicationsDashboard
0 commit comments