@@ -3,19 +3,11 @@ import { useState, useEffect, useMemo } from "react";
33import { useRouter } from "next/navigation" ;
44import { addToast } from "@heroui/toast" ;
55import React from "react" ;
6- import { Sankey , ResponsiveContainer , Tooltip } from "recharts" ;
76import posthog from "posthog-js" ;
87
98import JobApplicationsDashboard , { Application } from "@/components/JobApplicationsDashboard" ;
10- import ResponseRateCard from "@/components/response_rate_card" ;
11- import UniqueOpenRateChart from "@/components/response_rate_chart" ;
129import { checkAuth } from "@/utils/auth" ;
1310
14- interface SankeyData {
15- nodes : { name : string } [ ] ;
16- links : { source : number ; target : number ; value : number } [ ] ;
17- }
18-
1911export default function Dashboard ( ) {
2012 const router = useRouter ( ) ;
2113 const [ data , setData ] = useState < Application [ ] > ( [ ] ) ;
@@ -24,7 +16,6 @@ export default function Dashboard() {
2416 const [ error , setError ] = useState < string | null > ( null ) ;
2517 const [ currentPage , setCurrentPage ] = useState ( 1 ) ;
2618 const [ totalPages , setTotalPages ] = useState ( 1 ) ;
27- const [ sankeyData , setSankeyData ] = useState < SankeyData | null > ( null ) ;
2819 const [ searchTerm , setSearchTerm ] = useState ( "" ) ;
2920 const [ statusFilter , setStatusFilter ] = useState ( "" ) ;
3021 const [ companyFilter , setCompanyFilter ] = useState ( "" ) ;
@@ -101,25 +92,6 @@ export default function Dashboard() {
10192 fetchData ( ) ;
10293 } , [ apiUrl , router , currentPage ] ) ;
10394
104- useEffect ( ( ) => {
105- // Fetch Sankey data
106- const fetchSankey = async ( ) => {
107- try {
108- const response = await fetch ( `${ apiUrl } /get-sankey-data` , {
109- method : "GET" ,
110- credentials : "include"
111- } ) ;
112- if ( response . ok ) {
113- const result = await response . json ( ) ;
114- setSankeyData ( result ) ;
115- }
116- } catch {
117- // ignore for now
118- }
119- } ;
120- fetchSankey ( ) ;
121- } , [ apiUrl , router , currentPage ] ) ;
122-
12395 // Filter data based on search term, status, company, and hide options
12496 const filteredData = useMemo ( ( ) => {
12597 return data . filter ( ( item ) => {
@@ -231,53 +203,6 @@ export default function Dashboard() {
231203 ) ;
232204 }
233205
234- async function downloadSankey ( ) {
235- setDownloading ( true ) ;
236- try {
237- const response = await fetch ( `${ apiUrl } /process-sankey` , {
238- method : "GET" ,
239- credentials : "include"
240- } ) ;
241-
242- if ( ! response . ok ) {
243- let description = "Something went wrong. Please try again." ;
244-
245- if ( response . status === 429 ) {
246- description = "Download limit reached. Please wait before trying again." ;
247- } else {
248- description = "Please try again or contact help@justajobapp.com if the issue persists." ;
249- }
250-
251- addToast ( {
252- title : "Failed to download Sankey Diagram" ,
253- description,
254- color : "danger"
255- } ) ;
256-
257- return ;
258- }
259-
260- // Create a download link to trigger the file download
261- const blob = await response . blob ( ) ;
262- const link = document . createElement ( "a" ) ;
263- const url = URL . createObjectURL ( blob ) ;
264- link . href = url ;
265- link . download = `sankey_diagram_${ new Date ( ) . toISOString ( ) . split ( "T" ) [ 0 ] } .png` ;
266- document . body . appendChild ( link ) ;
267- link . click ( ) ;
268- document . body . removeChild ( link ) ;
269- URL . revokeObjectURL ( url ) ;
270- } catch {
271- addToast ( {
272- title : "Something went wrong" ,
273- description : "Please try again" ,
274- color : "danger"
275- } ) ;
276- } finally {
277- setDownloading ( false ) ;
278- }
279- }
280-
281206 const handleRemoveItem = async ( id : string ) => {
282207 try {
283208 // Make a DELETE request to the backend
@@ -306,161 +231,6 @@ export default function Dashboard() {
306231 }
307232 } ;
308233
309- const responseRateContent = (
310- < >
311- < div className = "flex flex-col gap-4 mt-4 mb-6 md:flex-row" >
312- < div className = "w-full md:w-[30%]" >
313- < ResponseRateCard />
314- </ div >
315- < div className = "md:w-[70%]" >
316- < UniqueOpenRateChart />
317- </ div >
318- </ div >
319- </ >
320- ) ;
321-
322- const sankeyChartContent =
323- sankeyData && sankeyData . nodes && sankeyData . nodes . length > 0 ? (
324- < div className = "bg-gray-100 dark:bg-gray-800 p-6 shadow-md rounded-lg" >
325- < div className = "flex items-center justify-between mb-4" >
326- < h2 className = "text-gray-700 dark:text-gray-300 text-base md:text-xl font-semibold" >
327- My Job Search
328- </ h2 >
329- < button
330- className = "px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700 disabled:opacity-50 disabled:cursor-not-allowed text-sm"
331- disabled = { downloading }
332- onClick = { downloadSankey }
333- >
334- { downloading ? "Downloading..." : "Download PNG" }
335- </ button >
336- </ div >
337- < div className = "h-[450px] w-full relative" >
338- < ResponsiveContainer height = "100%" width = "100%" >
339- < Sankey
340- data = { sankeyData }
341- link = { { stroke : "#cbd5e1" , strokeOpacity : 0.4 } }
342- margin = { { top : 10 , bottom : 30 , left : 10 , right : 10 } } // Smaller margins
343- node = { ( props ) => {
344- const isSource = props . x < 50 ;
345- const nodeName = props . payload . name ;
346- const status = nodeName . split ( " (" ) [ 0 ] ;
347-
348- const getStatusColor = ( status : string ) => {
349- const normalized = status . toLowerCase ( ) ;
350- switch ( normalized ) {
351- case "applications" :
352- return "#3b82f6" ; // Blue for source
353- case "offer made" :
354- return "#10b981" ;
355- case "rejection" :
356- return "#ef4444" ;
357- case "interview invitation" :
358- return "#06b6d4" ;
359- case "assessment sent" :
360- return "#8b5cf6" ;
361- case "availability request" :
362- return "#f59e0b" ;
363- case "application confirmation" :
364- return "#3b82f6" ;
365- case "information request" :
366- return "#6b7280" ;
367- case "inbound request" :
368- return "#10b981" ;
369- case "action required" :
370- return "#84cc16" ;
371- case "hiring freeze" :
372- return "#8b5cf6" ;
373- case "withdrew application" :
374- return "#ec4899" ;
375- default :
376- return "#8884d8" ;
377- }
378- } ;
379-
380- return (
381- < g >
382- < rect
383- fill = { getStatusColor ( status ) }
384- fillOpacity = { 0.8 }
385- height = { props . height }
386- stroke = { getStatusColor ( status ) }
387- width = { props . width }
388- x = { props . x }
389- y = { props . y }
390- />
391- < text
392- dominantBaseline = "middle"
393- fill = "#333"
394- fontSize = { 12 }
395- textAnchor = { isSource ? "start" : "end" }
396- x = { isSource ? props . x + props . width + 8 : props . x - 8 }
397- y = { props . y + props . height / 2 }
398- >
399- { props . payload . name }
400- </ text >
401- </ g >
402- ) ;
403- } }
404- nodePadding = { 24 }
405- >
406- < Tooltip
407- content = { ( { active, payload } ) => {
408- if ( active && payload && payload . length ) {
409- const { sourceNode, targetNode, value } = payload [ 0 ] . payload ;
410- return (
411- < div className = "bg-white p-2 rounded shadow" >
412- < div >
413- < strong >
414- { sourceNode ?. name } → { targetNode ?. name }
415- </ strong >
416- </ div >
417- < div > Count: { value } </ div >
418- </ div >
419- ) ;
420- }
421- return null ;
422- } }
423- />
424- </ Sankey >
425- </ ResponsiveContainer >
426- < div className = "absolute bottom-2 left-2 text-xs text-gray-500 opacity-70" >
427- { ( ( ) => {
428- // Calculate date range from the data
429- if ( data && data . length > 0 ) {
430- const dates = data
431- . map ( ( item ) => ( item . received_at ? new Date ( item . received_at ) : null ) )
432- . filter ( ( date ) => date !== null ) ;
433-
434- if ( dates . length > 0 ) {
435- const minDate = new Date ( Math . min ( ...dates . map ( ( d ) => d . getTime ( ) ) ) ) ;
436- const maxDate = new Date ( Math . max ( ...dates . map ( ( d ) => d . getTime ( ) ) ) ) ;
437-
438- const formatDate = ( date : Date ) => {
439- const month = date . toLocaleDateString ( "en-US" , { month : "long" } ) ;
440- const year = date . getFullYear ( ) ;
441- return { month, year } ;
442- } ;
443-
444- const minFormatted = formatDate ( minDate ) ;
445- const maxFormatted = formatDate ( maxDate ) ;
446-
447- let dateRange ;
448- if ( minFormatted . year === maxFormatted . year ) {
449- dateRange = `${ minFormatted . month } - ${ maxFormatted . month } ${ maxFormatted . year } ` ;
450- } else {
451- dateRange = `${ minFormatted . month } ${ minFormatted . year } - ${ maxFormatted . month } ${ maxFormatted . year } ` ;
452- }
453-
454- return `JustAJobApp.com • ${ dateRange } ` ;
455- }
456- }
457- return "JustAJobApp.com" ;
458- } ) ( ) }
459- </ div >
460- </ div >
461- </ div >
462- ) : null ;
463-
464234 return (
465235 < JobApplicationsDashboard
466236 companyFilter = { companyFilter }
@@ -471,14 +241,11 @@ export default function Dashboard() {
471241 hideRejections = { hideRejections }
472242 loading = { loading }
473243 normalizedJobTitleFilter = { normalizedJobTitleFilter }
474- responseRate = { responseRateContent }
475- sankeyChart = { sankeyChartContent }
476244 searchTerm = { searchTerm }
477245 statusFilter = { statusFilter }
478246 totalPages = { totalPages }
479247 onCompanyFilterChange = { setCompanyFilter }
480248 onDownloadCsv = { downloadCsv }
481- onDownloadSankey = { downloadSankey }
482249 onHideApplicationConfirmationsChange = { setHideApplicationConfirmations }
483250 onHideRejectionsChange = { setHideRejections }
484251 onNextPage = { nextPage }
0 commit comments