@@ -46,55 +46,62 @@ export function calculateMatches(
4646) : MatchResult [ ] {
4747 // ⚡ Bolt: Pre-calculate the current user's tech stack as a Set to avoid O(N*M) lookups
4848 const currentUserTechsSet = new Set ( currentUser . tech_stack ) ;
49-
50- return candidates
51- . filter ( ( c ) => c . id !== currentUser . id && c . open_to_collaboration )
52- . map ( ( candidate ) => {
53- let score = 0 ;
54- const reasons : string [ ] = [ ] ;
55-
56- const complementary = COMPLEMENTARY_ROLES [ currentUser . role_type ] || [ ] ;
57- if ( complementary . includes ( candidate . role_type ) ) {
58- score += 30 ;
59- reasons . push ( "Competences complementaires" ) ;
60- }
61-
62- // ⚡ Bolt: Fast Set lookup instead of array.includes() for nested tech stack matching
63- const commonTechs = candidate . tech_stack . filter ( ( t ) =>
64- currentUserTechsSet . has ( t )
65- ) ;
66- if ( commonTechs . length > 0 ) {
67- score += Math . min ( commonTechs . length * 10 , 25 ) ;
68- reasons . push ( `${ commonTechs . length } technologie(s) en commun` ) ;
69- }
70-
71- if (
72- currentUser . city &&
73- candidate . city &&
74- currentUser . city . toLowerCase ( ) === candidate . city . toLowerCase ( )
75- ) {
76- score += 20 ;
77- reasons . push ( "Meme ville" ) ;
78- }
79-
80- const levelMap = { junior : 1 , mid : 2 , senior : 3 } ;
81- const diff = Math . abs (
82- levelMap [ currentUser . experience_level ] -
83- levelMap [ candidate . experience_level ]
84- ) ;
85- if ( diff <= 1 ) {
86- score += 15 ;
87- reasons . push ( "Niveau d'experience compatible" ) ;
88- }
89-
90- if ( candidate . open_to_collaboration ) {
91- score += 10 ;
92- }
93-
94- return { profile : candidate , score : Math . min ( score , 100 ) , reasons } ;
95- } )
96- . filter ( ( m ) => m . score > 0 )
97- . sort ( ( a , b ) => b . score - a . score ) ;
49+ const complementary = COMPLEMENTARY_ROLES [ currentUser . role_type ] || [ ] ;
50+ const levelMap = { junior : 1 , mid : 2 , senior : 3 } ;
51+
52+ // ⚡ Bolt: Consolidate map and filter into a single reduce pass
53+ const results = candidates . reduce < MatchResult [ ] > ( ( acc , candidate ) => {
54+ if ( candidate . id === currentUser . id || ! candidate . open_to_collaboration ) {
55+ return acc ;
56+ }
57+
58+ let score = 0 ;
59+ const reasons : string [ ] = [ ] ;
60+
61+ if ( complementary . includes ( candidate . role_type ) ) {
62+ score += 30 ;
63+ reasons . push ( "Competences complementaires" ) ;
64+ }
65+
66+ // ⚡ Bolt: Fast Set lookup instead of array.includes() for nested tech stack matching
67+ const commonTechs = candidate . tech_stack . filter ( ( t ) =>
68+ currentUserTechsSet . has ( t )
69+ ) ;
70+ if ( commonTechs . length > 0 ) {
71+ score += Math . min ( commonTechs . length * 10 , 25 ) ;
72+ reasons . push ( `${ commonTechs . length } technologie(s) en commun` ) ;
73+ }
74+
75+ if (
76+ currentUser . city &&
77+ candidate . city &&
78+ currentUser . city . toLowerCase ( ) === candidate . city . toLowerCase ( )
79+ ) {
80+ score += 20 ;
81+ reasons . push ( "Meme ville" ) ;
82+ }
83+
84+ const diff = Math . abs (
85+ levelMap [ currentUser . experience_level ] -
86+ levelMap [ candidate . experience_level ]
87+ ) ;
88+ if ( diff <= 1 ) {
89+ score += 15 ;
90+ reasons . push ( "Niveau d'experience compatible" ) ;
91+ }
92+
93+ if ( candidate . open_to_collaboration ) {
94+ score += 10 ;
95+ }
96+
97+ if ( score > 0 ) {
98+ acc . push ( { profile : candidate , score : Math . min ( score , 100 ) , reasons } ) ;
99+ }
100+
101+ return acc ;
102+ } , [ ] ) ;
103+
104+ return results . sort ( ( a , b ) => b . score - a . score ) ;
98105}
99106
100107export function calculateProjectMatches (
@@ -105,58 +112,62 @@ export function calculateProjectMatches(
105112 // it for every single tech in every single project.
106113 const profileTechsLower = profile . tech_stack . map ( ( t ) => t . toLowerCase ( ) ) ;
107114
108- return projects
109- . map ( ( project ) => {
110- let score = 0 ;
111- const reasons : string [ ] = [ ] ;
112-
113- // ⚡ Bolt: Instead of iterating and lowercasing the project's entire tech stack
114- // for *every* item in the profile's tech stack (O(M*K)), we pre-calculate a Set
115- // of the lowercased project techs (O(M)) and do O(1) lookups.
116- const projectTechsSet = new Set ( project . tech_stack . map ( ( pt ) => pt . toLowerCase ( ) ) ) ;
117-
118- const commonTechs = profileTechsLower . filter ( ( tLower ) =>
119- projectTechsSet . has ( tLower )
115+ // ⚡ Bolt: Consolidate map and filter into a single reduce pass
116+ const results = projects . reduce < ProjectMatch [ ] > ( ( acc , project ) => {
117+ let score = 0 ;
118+ const reasons : string [ ] = [ ] ;
119+
120+ // ⚡ Bolt: Instead of iterating and lowercasing the project's entire tech stack
121+ // for *every* item in the profile's tech stack (O(M*K)), we pre-calculate a Set
122+ // of the lowercased project techs (O(M)) and do O(1) lookups.
123+ const projectTechsSet = new Set ( project . tech_stack . map ( ( pt ) => pt . toLowerCase ( ) ) ) ;
124+
125+ const commonTechs = profileTechsLower . filter ( ( tLower ) =>
126+ projectTechsSet . has ( tLower )
127+ ) ;
128+
129+ if ( commonTechs . length > 0 ) {
130+ score += Math . min ( commonTechs . length * 15 , 40 ) ;
131+ reasons . push ( `${ commonTechs . length } techno(s) en commun` ) ;
132+ }
133+
134+ const roleInStack = project . tech_stack . some ( ( t ) => {
135+ const tl = t . toLowerCase ( ) ;
136+ const r = profile . role_type ;
137+ return (
138+ ( r === "frontend" && ( tl . includes ( "react" ) || tl . includes ( "vue" ) || tl . includes ( "angular" ) || tl . includes ( "next" ) || tl . includes ( "svelte" ) ) ) ||
139+ ( r === "mobile" && ( tl . includes ( "flutter" ) || tl . includes ( "react native" ) || tl . includes ( "dart" ) || tl . includes ( "mobile" ) ) ) ||
140+ ( r === "backend" && ( tl . includes ( "node" ) || tl . includes ( "python" ) || tl . includes ( "django" ) || tl . includes ( "php" ) || tl . includes ( "java" ) || tl . includes ( "go" ) || tl . includes ( "rust" ) ) ) ||
141+ ( r === "fullstack" && ( tl . includes ( "react" ) || tl . includes ( "node" ) || tl . includes ( "next" ) || tl . includes ( "supabase" ) || tl . includes ( "typescript" ) ) ) ||
142+ ( r === "data" && ( tl . includes ( "python" ) || tl . includes ( "sql" ) || tl . includes ( "spark" ) || tl . includes ( "data" ) ) ) ||
143+ ( r === "devops" && ( tl . includes ( "docker" ) || tl . includes ( "kubernetes" ) || tl . includes ( "aws" ) || tl . includes ( "linux" ) ) ) ||
144+ ( r === "cybersecurite" && ( tl . includes ( "cyber" ) || tl . includes ( "linux" ) || tl . includes ( "securite" ) || tl . includes ( "reseaux" ) ) ) ||
145+ ( r === "design" && ( tl . includes ( "figma" ) || tl . includes ( "design" ) || tl . includes ( "ui" ) ) )
120146 ) ;
121-
122- if ( commonTechs . length > 0 ) {
123- score += Math . min ( commonTechs . length * 15 , 40 ) ;
124- reasons . push ( `${ commonTechs . length } techno(s) en commun` ) ;
125- }
126-
127- const roleInStack = project . tech_stack . some ( ( t ) => {
128- const tl = t . toLowerCase ( ) ;
129- const r = profile . role_type ;
130- return (
131- ( r === "frontend" && ( tl . includes ( "react" ) || tl . includes ( "vue" ) || tl . includes ( "angular" ) || tl . includes ( "next" ) || tl . includes ( "svelte" ) ) ) ||
132- ( r === "mobile" && ( tl . includes ( "flutter" ) || tl . includes ( "react native" ) || tl . includes ( "dart" ) || tl . includes ( "mobile" ) ) ) ||
133- ( r === "backend" && ( tl . includes ( "node" ) || tl . includes ( "python" ) || tl . includes ( "django" ) || tl . includes ( "php" ) || tl . includes ( "java" ) || tl . includes ( "go" ) || tl . includes ( "rust" ) ) ) ||
134- ( r === "fullstack" && ( tl . includes ( "react" ) || tl . includes ( "node" ) || tl . includes ( "next" ) || tl . includes ( "supabase" ) || tl . includes ( "typescript" ) ) ) ||
135- ( r === "data" && ( tl . includes ( "python" ) || tl . includes ( "sql" ) || tl . includes ( "spark" ) || tl . includes ( "data" ) ) ) ||
136- ( r === "devops" && ( tl . includes ( "docker" ) || tl . includes ( "kubernetes" ) || tl . includes ( "aws" ) || tl . includes ( "linux" ) ) ) ||
137- ( r === "cybersecurite" && ( tl . includes ( "cyber" ) || tl . includes ( "linux" ) || tl . includes ( "securite" ) || tl . includes ( "reseaux" ) ) ) ||
138- ( r === "design" && ( tl . includes ( "figma" ) || tl . includes ( "design" ) || tl . includes ( "ui" ) ) )
139- ) ;
140- } ) ;
141- if ( roleInStack ) {
142- score += 25 ;
143- reasons . push ( "Votre rôle correspond au projet" ) ;
144- }
145-
146- if ( project . open_to_collaboration && profile . open_to_collaboration ) {
147- score += 20 ;
148- reasons . push ( "Les deux parties disponibles" ) ;
149- }
150-
151- if ( profile . experience_level === "senior" ) {
152- score += 10 ;
153- reasons . push ( "Profil senior valorisé" ) ;
154- } else if ( profile . experience_level === "mid" ) {
155- score += 5 ;
156- }
157-
158- return { project, score : Math . min ( score , 100 ) , reasons } ;
159- } )
160- . filter ( ( m ) => m . score > 0 )
161- . sort ( ( a , b ) => b . score - a . score ) ;
147+ } ) ;
148+ if ( roleInStack ) {
149+ score += 25 ;
150+ reasons . push ( "Votre rôle correspond au projet" ) ;
151+ }
152+
153+ if ( project . open_to_collaboration && profile . open_to_collaboration ) {
154+ score += 20 ;
155+ reasons . push ( "Les deux parties disponibles" ) ;
156+ }
157+
158+ if ( profile . experience_level === "senior" ) {
159+ score += 10 ;
160+ reasons . push ( "Profil senior valorisé" ) ;
161+ } else if ( profile . experience_level === "mid" ) {
162+ score += 5 ;
163+ }
164+
165+ if ( score > 0 ) {
166+ acc . push ( { project, score : Math . min ( score , 100 ) , reasons } ) ;
167+ }
168+
169+ return acc ;
170+ } , [ ] ) ;
171+
172+ return results . sort ( ( a , b ) => b . score - a . score ) ;
162173}
0 commit comments