11"use client" ;
22
3- import { useState , useEffect } from "react" ;
3+ import { useState , useEffect , useRef } from "react" ;
44import { GlowCard } from "./GlowCard" ;
55import { Card , CardContent , CardHeader , CardTitle } from "@/components/ui/card" ;
66import type { Job } from "@/types/models" ;
@@ -11,11 +11,54 @@ interface JobListProps {
1111 isSearching ?: boolean ;
1212}
1313
14+ interface JobWithPosition extends Job {
15+ previousPosition ?: number ;
16+ currentPosition : number ;
17+ isNew ?: boolean ;
18+ scoreChange ?: number ;
19+ }
20+
1421export default function JobList ( { jobs, searchComponent, isSearching = false } : JobListProps ) {
1522 const [ selectedJob , setSelectedJob ] = useState < Job | null > ( null ) ;
23+ const [ jobsWithPosition , setJobsWithPosition ] = useState < JobWithPosition [ ] > ( [ ] ) ;
24+ const previousJobsRef = useRef < Map < string , { score : number ; position : number } > > ( new Map ( ) ) ;
25+
26+ // Process jobs to track position changes
27+ useEffect ( ( ) => {
28+ if ( jobs . length === 0 ) {
29+ setJobsWithPosition ( [ ] ) ;
30+ return ;
31+ }
32+
33+ // Sort jobs by Job Fit Score in descending order (highest first)
34+ const sortedJobs = [ ...jobs ] . sort ( ( a , b ) => b . jobFitScore - a . jobFitScore ) ;
35+
36+ const newJobsWithPosition : JobWithPosition [ ] = sortedJobs . map ( ( job , index ) => {
37+ const previousData = previousJobsRef . current . get ( job . id ) ;
38+ const isNew = ! previousData ;
39+ const scoreChange = previousData ? job . jobFitScore - previousData . score : 0 ;
40+
41+ return {
42+ ...job ,
43+ currentPosition : index ,
44+ previousPosition : previousData ?. position ,
45+ isNew,
46+ scoreChange
47+ } ;
48+ } ) ;
49+
50+ setJobsWithPosition ( newJobsWithPosition ) ;
51+
52+ // Update the reference for next comparison
53+ const newMap = new Map < string , { score : number ; position : number } > ( ) ;
54+ sortedJobs . forEach ( ( job , index ) => {
55+ newMap . set ( job . id , { score : job . jobFitScore , position : index } ) ;
56+ } ) ;
57+ previousJobsRef . current = newMap ;
58+ } , [ jobs ] ) ;
1659
1760 // Sort jobs by Job Fit Score in descending order (highest first)
18- const sortedJobs = [ ... jobs ] . sort ( ( a , b ) => b . jobFitScore - a . jobFitScore ) ;
61+ const sortedJobs = jobsWithPosition ;
1962
2063 // Auto-select first job when jobs change
2164 useEffect ( ( ) => {
@@ -43,20 +86,55 @@ export default function JobList({ jobs, searchComponent, isSearching = false }:
4386 return colors [ index % colors . length ] ;
4487 } ;
4588
46- const renderJobListItem = ( job : Job ) => {
89+ const renderJobListItem = ( job : JobWithPosition ) => {
4790 const scoreColors = getScoreColor ( job . jobFitScore ) ;
4891 const isSelected = selectedJob ?. id === job . id ;
4992
93+ // Calculate position change
94+ const positionChange = job . previousPosition !== undefined
95+ ? job . previousPosition - job . currentPosition
96+ : 0 ;
97+
98+ // Determine animation state
99+ let animationClass = '' ;
100+ let positionIndicator = null ;
101+
102+ if ( job . isNew ) {
103+ animationClass = 'animate-slide-in-left' ;
104+ } else if ( positionChange > 0 ) {
105+ // Moved up
106+ animationClass = 'animate-bounce-subtle' ;
107+ positionIndicator = (
108+ < div className = "absolute -top-1 -right-1 flex items-center justify-center w-5 h-5 bg-green-500 text-white text-xs font-bold rounded-full animate-pulse" >
109+ ↑
110+ </ div >
111+ ) ;
112+ } else if ( positionChange < 0 ) {
113+ // Moved down
114+ animationClass = 'animate-shake' ;
115+ positionIndicator = (
116+ < div className = "absolute -top-1 -right-1 flex items-center justify-center w-5 h-5 bg-orange-500 text-white text-xs font-bold rounded-full" >
117+ ↓
118+ </ div >
119+ ) ;
120+ }
121+
50122 return (
51123 < div
52124 key = { job . id }
53125 onClick = { ( ) => setSelectedJob ( job ) }
54- className = { `p-3 border rounded-lg cursor-pointer transition-all ${
126+ className = { `relative p-3 border rounded-lg cursor-pointer transition-all duration-300 ${ animationClass } ${
55127 isSelected
56128 ? 'border-primary-500 bg-primary-50'
57129 : 'border-gray-200 hover:border-gray-300 hover:bg-gray-50'
58130 } `}
131+ style = { {
132+ transitionProperty : 'all' ,
133+ transitionDuration : '500ms' ,
134+ transitionTimingFunction : 'cubic-bezier(0.4, 0, 0.2, 1)' ,
135+ } }
59136 >
137+ { positionIndicator }
60138 < div className = "flex items-center justify-between" >
61139 < div className = "flex-1 min-w-0" >
62140 < h3 className = { `font-medium text-sm truncate ${
@@ -71,10 +149,15 @@ export default function JobList({ jobs, searchComponent, isSearching = false }:
71149 </ p >
72150 </ div >
73151 < div className = "flex items-center gap-2 flex-shrink-0" >
74- < div className = { `text-xs font-bold ${ scoreColors . text } ` } >
152+ < div className = { `text-xs font-bold ${ scoreColors . text } transition-all duration-300 ` } >
75153 { job . jobFitScore }
154+ { job . scoreChange !== undefined && job . scoreChange !== 0 && (
155+ < span className = { `ml-1 text-[10px] ${ job . scoreChange > 0 ? 'text-green-600' : 'text-red-600' } ` } >
156+ { job . scoreChange > 0 ? '+' : '' } { job . scoreChange . toFixed ( 1 ) }
157+ </ span >
158+ ) }
76159 </ div >
77- < div className = { `w-2 h-6 ${ scoreColors . bg } rounded-full` } > </ div >
160+ < div className = { `w-2 h-6 ${ scoreColors . bg } rounded-full transition-all duration-300 ` } > </ div >
78161 </ div >
79162 </ div >
80163 </ div >
0 commit comments