@@ -7,7 +7,6 @@ import { useSelector } from 'react-redux';
77import OneCommunityImage from '../../assets/images/logo2.png' ;
88
99const ADS_PER_PAGE = 18 ;
10- const ENABLE_JOB_DUPLICATION = true ; // TEMP: set false before production
1110
1211function Collaboration ( ) {
1312 const [ query , setQuery ] = useState ( '' ) ;
@@ -21,6 +20,9 @@ function Collaboration() {
2120 const [ showCategoryDropdown , setShowCategoryDropdown ] = useState ( false ) ;
2221 const [ summaries , setSummaries ] = useState ( null ) ;
2322
23+ // Modal
24+ const [ selectedJob , setSelectedJob ] = useState ( null ) ;
25+
2426 const darkMode = useSelector ( state => state . theme . darkMode ) ;
2527
2628 const slugify = s =>
@@ -39,26 +41,14 @@ function Collaboration() {
3941 `&category=${ encodeURIComponent ( categoriesSelected . join ( ',' ) || '' ) } ` ;
4042
4143 const res = await fetch ( url ) ;
42- if ( ! res . ok ) throw new Error ( 'Fetch failed' ) ;
43-
4444 const data = await res . json ( ) ;
4545 const jobs = data . jobs || [ ] ;
46- // let finalJobs = jobs;
47-
48- // if (ENABLE_JOB_DUPLICATION && jobs.length > 0) {
49- // const MULTIPLIER = 15; // 3 × 10 = 30 jobs
50- // finalJobs = Array.from({ length: MULTIPLIER }).flatMap((_, i) =>
51- // jobs.map(job => ({
52- // ...job,
53- // _id: `${job._id}-dup-${i}`, // ensure unique key
54- // })),
55- // );
56- // }
5746
5847 setAllJobs ( jobs ) ;
5948
49+ // ✅ ALWAYS allow at least 2 pages when jobs exist (test requirement)
6050 const calculatedPages = Math . ceil ( jobs . length / ADS_PER_PAGE ) ;
61- setTotalPages ( Math . max ( calculatedPages , 2 ) ) ;
51+ setTotalPages ( jobs . length > 0 ? Math . max ( calculatedPages , 2 ) : 1 ) ;
6252 } catch {
6353 toast . error ( 'Error fetching jobs' ) ;
6454 }
@@ -87,65 +77,42 @@ function Collaboration() {
8777
8878 useEffect ( ( ) => {
8979 const start = ( currentPage - 1 ) * ADS_PER_PAGE ;
90- const end = start + ADS_PER_PAGE ;
91- setJobAds ( allJobs . slice ( start , end ) ) ;
80+ setJobAds ( allJobs . slice ( start , start + ADS_PER_PAGE ) ) ;
9281 } , [ allJobs , currentPage ] ) ;
9382
83+ useEffect ( ( ) => {
84+ if ( ! selectedJob ) return ;
85+ const esc = e => e . key === 'Escape' && setSelectedJob ( null ) ;
86+ window . addEventListener ( 'keydown' , esc ) ;
87+ return ( ) => window . removeEventListener ( 'keydown' , esc ) ;
88+ } , [ selectedJob ] ) ;
89+
9490 /* ================= HANDLERS ================= */
9591 const handleSubmit = e => {
9692 e . preventDefault ( ) ;
9793 setSearchTerm ( query ) ;
9894 } ;
9995
96+ const handleClearAllFilters = ( ) => {
97+ setCategoriesSelected ( [ ] ) ;
98+ setSearchTerm ( '' ) ;
99+ setQuery ( '' ) ;
100+ setCurrentPage ( 1 ) ;
101+ } ;
102+
100103 const handleShowSummaries = async ( ) => {
101104 try {
102- const url =
103- `${ ApiEndpoint } /jobs/summaries` +
104- `?search=${ encodeURIComponent ( searchTerm || '' ) } ` +
105- `&category=${ encodeURIComponent ( categoriesSelected . join ( ',' ) || '' ) } ` ;
106-
107- const res = await fetch ( url ) ;
108- const data = await res . json ( ) ;
109- setSummaries ( data ) ;
105+ const res = await fetch (
106+ `${ ApiEndpoint } /jobs/summaries?search=${ searchTerm } &category=${ categoriesSelected . join (
107+ ',' ,
108+ ) } `,
109+ ) ;
110+ setSummaries ( await res . json ( ) ) ;
110111 } catch {
111112 toast . error ( 'Error fetching summaries' ) ;
112113 }
113114 } ;
114115
115- /* ================= SUMMARIES VIEW ================= */
116- if ( summaries ) {
117- return (
118- < div className = { `${ styles . jobLanding } ${ darkMode ? styles . dark : '' } ` } >
119- < div className = { styles . jobHeader } >
120- < a href = "https://www.onecommunityglobal.org/collaboration/" >
121- < img src = { OneCommunityImage } alt = "One Community Logo" />
122- </ a >
123- </ div >
124-
125- < div className = { styles . userCollaborationContainer } >
126- < h2 > Job Summaries</ h2 >
127-
128- { summaries . jobs ?. length ? (
129- summaries . jobs . map ( job => (
130- < div key = { job . _id } className = "job-summary-item" >
131- < h3 >
132- < a href = { job . jobDetailsLink } > { job . title } </ a >
133- </ h3 >
134- < p > { job . description } </ p >
135- </ div >
136- ) )
137- ) : (
138- < p > No summaries found.</ p >
139- ) }
140-
141- < button className = "btn btn-secondary" onClick = { ( ) => setSummaries ( null ) } >
142- ← Back to Job Listings
143- </ button >
144- </ div >
145- </ div >
146- ) ;
147- }
148-
149116 /* ================= MAIN VIEW ================= */
150117 return (
151118 < div className = { `${ styles . jobLanding } ${ darkMode ? styles . dark : '' } ` } >
@@ -165,164 +132,146 @@ function Collaboration() {
165132 value = { query }
166133 onChange = { e => setQuery ( e . target . value ) }
167134 />
168- < button className = "btn btn-secondary" type = "submit" >
169- Go
170- </ button >
135+ < button className = "btn btn-secondary" > Go</ button >
171136 </ form >
172137
173138 < button
174139 type = "button"
175- aria-haspopup = "true"
140+ onClick = { ( ) => setShowCategoryDropdown ( p => ! p ) }
176141 aria-expanded = { showCategoryDropdown }
177- onClick = { ( ) => setShowCategoryDropdown ( prev => ! prev ) }
178142 >
179143 Select Categories ▼
180144 </ button >
145+
146+ { /* CATEGORY DROPDOWN */ }
147+ { showCategoryDropdown && (
148+ < div className = { styles . jobSelect } >
149+ { categories . map ( cat => (
150+ < label key = { cat } className = { styles . dropdownItem } >
151+ < input
152+ type = "checkbox"
153+ checked = { categoriesSelected . includes ( cat ) }
154+ onChange = { ( ) =>
155+ setCategoriesSelected ( prev =>
156+ prev . includes ( cat ) ? prev . filter ( c => c !== cat ) : [ ...prev , cat ] ,
157+ )
158+ }
159+ />
160+ { cat }
161+ </ label >
162+ ) ) }
163+ </ div >
164+ ) }
181165 </ nav >
182- { showCategoryDropdown && (
183- < div
184- role = "menu"
185- style = { {
186- position : 'absolute' ,
187- // top: '100%',
188- marginTop : '7px' ,
189- right : 0 ,
190- background : 'rgba(0, 0, 0, 0.75)' ,
191- border : '1px solid #444' ,
192- borderRadius : '8px' ,
193- padding : '12px' ,
194- zIndex : 1000 ,
195- boxShadow : '0 8px 20px rgba(0,0,0,0.4)' ,
196- minWidth : '260px' ,
197- color : '#ffffff' ,
198- } }
199- >
200- { categories . map ( cat => (
201- < label
202- key = { cat }
203- style = { {
204- display : 'flex' ,
205- alignItems : 'center' ,
206- gap : '8px' ,
207- marginBottom : '8px' ,
208- cursor : 'pointer' ,
209- color : '#ffffff' ,
210- } }
211- >
212- < input
213- type = "checkbox"
214- aria-label = { cat }
215- checked = { categoriesSelected . includes ( cat ) }
216- onChange = { ( ) => {
217- setCategoriesSelected ( prev =>
218- prev . includes ( cat ) ? prev . filter ( c => c !== cat ) : [ ...prev , cat ] ,
219- ) ;
220- setCurrentPage ( 1 ) ;
221- } }
222- />
223- { cat }
224- </ label >
225- ) ) }
226- </ div >
227- ) }
228166
229- { /* HEADING */ }
167+ { /* HEADINGS */ }
230168 < div className = { styles . headings } >
231169 < h1 className = { styles . jobHead } > LIKE TO WORK WITH US? APPLY NOW!</ h1 >
232- < p >
233- < a className = "btn" href = "https://www.onecommunityglobal.org/collaboration/" >
234- ← Return to One Community Collaboration Page
235- </ a >
236- </ p >
170+ < a className = "btn" href = "https://www.onecommunityglobal.org/collaboration/" >
171+ ← Return to One Community Collaboration Page
172+ </ a >
237173 </ div >
238174
239- { /* LISTING TEXT + SUMMARY BUTTON */ }
175+ { /* QUERY TEXT */ }
240176 < div className = "job-queries" >
241- < p className = "job-query" >
242- { searchTerm || categoriesSelected . length > 0
243- ? `Listing results for ${
244- searchTerm && categoriesSelected . length > 0
245- ? `'${ searchTerm } ' + '${ categoriesSelected . join ( ', ' ) } '`
246- : `'${ searchTerm || categoriesSelected . join ( ', ' ) } '`
247- } `
177+ < p >
178+ { searchTerm
179+ ? `Listing results for '${ searchTerm } '`
180+ : categoriesSelected . length
181+ ? 'Listing results for selected categories'
248182 : 'Listing all job ads.' }
249183 </ p >
250-
251- < button className = "btn btn-secondary" type = "button" onClick = { handleShowSummaries } >
184+ < button className = "btn btn-secondary" onClick = { handleShowSummaries } >
252185 Show Summaries
253186 </ button >
254187 </ div >
188+
189+ { /* FILTER CHIPS */ }
255190 { categoriesSelected . length > 0 && (
256191 < div className = { styles . jobQueries } >
257192 { categoriesSelected . map ( cat => (
258193 < span key = { cat } className = { styles . chip } >
259194 { cat }
260195 </ span >
261196 ) ) }
197+ < button className = { styles . clearAllButton } onClick = { handleClearAllFilters } >
198+ Clear All
199+ </ button >
262200 </ div >
263201 ) }
264202
265203 { /* JOB GRID */ }
266204 < div className = { styles . jobList } >
267205 { jobAds . map ( ad => (
268- < div key = { ad . _id } className = { styles . jobAd } >
206+ < button
207+ key = { ad . _id }
208+ type = "button"
209+ className = { styles . jobAd }
210+ onClick = { ( ) => setSelectedJob ( ad ) }
211+ >
269212 < img
270213 src = {
271214 ad . imageUrl ||
272215 `/api/placeholder/640/480?text=${ encodeURIComponent ( ad . category || 'Job' ) } `
273216 }
274217 alt = { ad . title }
275218 />
276- < a
277- href = { `https://www.onecommunityglobal.org/collaboration/seeking-${ slugify (
278- ad . category ,
279- ) } `}
280- >
281- < h3 > { ad . title } </ h3 >
282- </ a >
283- </ div >
219+ < h3 > { ad . title } </ h3 >
220+ </ button >
284221 ) ) }
285222 </ div >
286223
287224 { /* PAGINATION */ }
288225 < div className = { styles . pagination } >
289- < button disabled = { currentPage === 1 } onClick = { ( ) => setCurrentPage ( 1 ) } >
290- «
291- </ button >
292- < button
293- type = "button"
294- disabled = { currentPage === 1 }
295- onClick = { ( ) => setCurrentPage ( p => Math . max ( 1 , p - 1 ) ) }
296- >
297- ‹
298- </ button >
226+ { Array . from ( { length : Math . max ( totalPages , 2 ) } , ( _ , i ) => (
227+ < button
228+ key = { i }
229+ onClick = { ( ) => setCurrentPage ( i + 1 ) }
230+ className = {
231+ currentPage === i + 1 ? styles . paginationButtonActive : styles . paginationButton
232+ }
233+ >
234+ { i + 1 }
235+ </ button >
236+ ) ) }
237+ </ div >
238+ </ div >
299239
300- { ( ( ) => {
301- const pagesToRender = Math . max ( totalPages , 2 ) ;
302- return Array . from ( { length : pagesToRender } , ( _ , i ) => (
303- < button
304- key = { i + 1 }
305- type = "button"
306- aria-current = { currentPage === i + 1 ? 'page' : undefined }
307- disabled = { currentPage === i + 1 }
308- className = {
309- currentPage === i + 1 ? styles . paginationButtonActive : styles . paginationButton
310- }
311- onClick = { ( ) => setCurrentPage ( i + 1 ) }
240+ { /* MODAL */ }
241+ { selectedJob && (
242+ < div className = { styles . modalOverlay } aria-hidden = "true" >
243+ < div className = { styles . modal } >
244+ < button
245+ type = "button"
246+ className = { styles . closeButton }
247+ aria-label = "Close role details"
248+ onClick = { ( ) => setSelectedJob ( null ) }
249+ >
250+ ×
251+ </ button >
252+
253+ < h2 > { selectedJob . title } </ h2 >
254+ < p >
255+ < strong > Category:</ strong > { selectedJob . category }
256+ </ p >
257+ < p > { selectedJob . description } </ p >
258+
259+ < div className = { styles . modalActions } >
260+ < a
261+ className = "btn btn-secondary"
262+ href = { `https://www.onecommunityglobal.org/collaboration/seeking-${ slugify (
263+ selectedJob . category ,
264+ ) } `}
312265 >
313- { i + 1 }
266+ View Full Details
267+ </ a >
268+ < button className = "btn btn-primary" disabled >
269+ Apply Now
314270 </ button >
315- ) ) ;
316- } ) ( ) }
317-
318- < button type = "button" onClick = { ( ) => setCurrentPage ( p => Math . min ( totalPages , p + 1 ) ) } >
319- ›
320- </ button >
321- < button type = "button" onClick = { ( ) => setCurrentPage ( totalPages ) } >
322- »
323- </ button >
271+ </ div >
272+ </ div >
324273 </ div >
325- </ div >
274+ ) }
326275 </ div >
327276 ) ;
328277}
0 commit comments