Skip to content

Commit 055316a

Browse files
Merge pull request #5096 from OneCommunityGlobal/Akshith-job-search-function-fe
Akshith - Application/Job Posting Page/Function and Design - Search Function - Frontend
2 parents ea1fff3 + d15aa14 commit 055316a

3 files changed

Lines changed: 365 additions & 282 deletions

File tree

src/components/Collaboration/Collaboration.jsx

Lines changed: 150 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
// src/pages/Collaboration/Collaboration.jsx
2-
import { useEffect, useState } from 'react';
2+
import { useEffect, useState, useMemo, useRef } from 'react';
33
import styles from './Collaboration.module.css';
44
import { toast } from 'react-toastify';
55
import { ApiEndpoint } from '~/utils/URL';
@@ -18,7 +18,13 @@ function Collaboration() {
1818
const [totalPages, setTotalPages] = useState(1);
1919
const [categories, setCategories] = useState([]);
2020
const [showCategoryDropdown, setShowCategoryDropdown] = useState(false);
21+
const [showPositionDropdown, setShowPositionDropdown] = useState(false);
2122
const [summaries, setSummaries] = useState(null);
23+
// const [positions, setPositions] = useState([]);
24+
const [selectedPosition, setSelectedPosition] = useState('');
25+
const [selectedCategory, setSelectedCategory] = useState('');
26+
const categoryRef = useRef(null);
27+
const positionRef = useRef(null);
2228

2329
// Modal
2430
const [selectedJob, setSelectedJob] = useState(null);
@@ -38,33 +44,38 @@ function Collaboration() {
3844
const url =
3945
`${ApiEndpoint}/jobs` +
4046
`?search=${encodeURIComponent(searchTerm || '')}` +
41-
`&category=${encodeURIComponent(categoriesSelected.join(',') || '')}`;
47+
`&category=${encodeURIComponent(selectedCategory || '')}`;
4248

4349
const res = await fetch(url);
4450
const data = await res.json();
45-
const jobs = data.jobs || [];
46-
47-
setAllJobs(jobs);
48-
49-
// ✅ ALWAYS allow at least 2 pages when jobs exist (test requirement)
50-
const calculatedPages = Math.ceil(jobs.length / ADS_PER_PAGE);
51-
setTotalPages(jobs.length > 0 ? Math.max(calculatedPages, 2) : 1);
51+
setAllJobs(data.jobs || []);
5252
} catch {
5353
toast.error('Error fetching jobs');
5454
}
5555
};
5656

57-
/* ================= FETCH CATEGORIES ================= */
5857
const fetchCategories = async () => {
5958
try {
6059
const res = await fetch(`${ApiEndpoint}/jobs/categories`);
6160
const data = await res.json();
62-
setCategories((data.categories || []).sort());
61+
setCategories((data.categories || []).sort((a, b) => a.localeCompare(b)));
6362
} catch {
6463
toast.error('Error fetching categories');
6564
}
6665
};
6766

67+
/* ================= FETCH CATEGORIES ================= */
68+
const fetchPositions = async () => {
69+
try {
70+
const res = await fetch(`${ApiEndpoint}/jobs/positions`);
71+
const data = await res.json();
72+
73+
setPositions((data.positions || []).sort((a, b) => a.localeCompare(b)));
74+
} catch {
75+
toast.error('Error fetching positions');
76+
}
77+
};
78+
6879
/* ================= EFFECTS ================= */
6980
useEffect(() => {
7081
fetchCategories();
@@ -73,12 +84,39 @@ function Collaboration() {
7384
useEffect(() => {
7485
setCurrentPage(1);
7586
fetchJobs();
76-
}, [searchTerm, categoriesSelected]);
87+
}, [searchTerm, selectedCategory]);
88+
89+
const filteredJobs = useMemo(() => {
90+
if (!selectedPosition) return allJobs;
91+
92+
return allJobs.filter(job =>
93+
(job.position || job.title || '').toLowerCase().includes(selectedPosition.toLowerCase()),
94+
);
95+
}, [allJobs, selectedPosition]);
96+
97+
const positions = useMemo(() => {
98+
const uniquePositions = [
99+
...new Set(
100+
allJobs
101+
.filter(
102+
job =>
103+
!selectedCategory || job.category?.toLowerCase() === selectedCategory.toLowerCase(),
104+
)
105+
.map(job => job.position || job.title)
106+
.filter(Boolean),
107+
),
108+
];
109+
110+
return uniquePositions.sort((a, b) => a.localeCompare(b));
111+
}, [allJobs, selectedCategory]);
77112

78113
useEffect(() => {
79114
const start = (currentPage - 1) * ADS_PER_PAGE;
80-
setJobAds(allJobs.slice(start, start + ADS_PER_PAGE));
81-
}, [allJobs, currentPage]);
115+
setJobAds(filteredJobs.slice(start, start + ADS_PER_PAGE));
116+
117+
const calculatedPages = Math.ceil(filteredJobs.length / ADS_PER_PAGE);
118+
setTotalPages(Math.max(calculatedPages, 1));
119+
}, [filteredJobs, currentPage]);
82120

83121
useEffect(() => {
84122
if (!selectedJob) return;
@@ -87,14 +125,33 @@ function Collaboration() {
87125
return () => window.removeEventListener('keydown', esc);
88126
}, [selectedJob]);
89127

128+
useEffect(() => {
129+
const handleClickOutside = event => {
130+
if (categoryRef.current && !categoryRef.current.contains(event.target)) {
131+
setShowCategoryDropdown(false);
132+
}
133+
134+
if (positionRef.current && !positionRef.current.contains(event.target)) {
135+
setShowPositionDropdown(false);
136+
}
137+
};
138+
139+
document.addEventListener('mousedown', handleClickOutside);
140+
141+
return () => {
142+
document.removeEventListener('mousedown', handleClickOutside);
143+
};
144+
}, []);
145+
90146
/* ================= HANDLERS ================= */
91147
const handleSubmit = e => {
92148
e.preventDefault();
93149
setSearchTerm(query);
94150
};
95151

96152
const handleClearAllFilters = () => {
97-
setCategoriesSelected([]);
153+
setSelectedCategory('');
154+
setSelectedPosition('');
98155
setSearchTerm('');
99156
setQuery('');
100157
setCurrentPage(1);
@@ -103,9 +160,7 @@ function Collaboration() {
103160
const handleShowSummaries = async () => {
104161
try {
105162
const res = await fetch(
106-
`${ApiEndpoint}/jobs/summaries?search=${searchTerm}&category=${categoriesSelected.join(
107-
',',
108-
)}`,
163+
`${ApiEndpoint}/jobs/summaries?search=${searchTerm}&category=${selectedCategory}`,
109164
);
110165
setSummaries(await res.json());
111166
} catch {
@@ -169,33 +224,76 @@ function Collaboration() {
169224
<button className="btn btn-secondary">Go</button>
170225
</form>
171226

172-
<button
173-
type="button"
174-
onClick={() => setShowCategoryDropdown(p => !p)}
175-
aria-expanded={showCategoryDropdown}
176-
>
177-
Select Categories ▼
178-
</button>
227+
<div className={styles.dropdownWrapper} ref={categoryRef}>
228+
<button
229+
type="button"
230+
onClick={() => {
231+
setShowCategoryDropdown(prev => !prev);
232+
setShowPositionDropdown(false);
233+
}}
234+
aria-expanded={showCategoryDropdown}
235+
>
236+
{selectedCategory || 'Select Categories'}
237+
</button>
179238

180-
{/* CATEGORY DROPDOWN */}
181-
{showCategoryDropdown && (
182-
<div className={styles.jobSelect}>
183-
{categories.map(cat => (
184-
<label key={cat} className={styles.dropdownItem}>
185-
<input
186-
type="checkbox"
187-
checked={categoriesSelected.includes(cat)}
188-
onChange={() =>
189-
setCategoriesSelected(prev =>
190-
prev.includes(cat) ? prev.filter(c => c !== cat) : [...prev, cat],
191-
)
192-
}
193-
/>
194-
{cat}
195-
</label>
196-
))}
197-
</div>
198-
)}
239+
{showCategoryDropdown && (
240+
<div className={styles.jobSelect}>
241+
{categories.map(cat => (
242+
<button
243+
key={cat}
244+
type="button"
245+
className={styles.dropdownItem}
246+
onClick={() => {
247+
setSelectedCategory(cat);
248+
setSelectedPosition('');
249+
setShowCategoryDropdown(false);
250+
setCurrentPage(1);
251+
}}
252+
>
253+
{cat}
254+
</button>
255+
))}
256+
</div>
257+
)}
258+
</div>
259+
260+
<div className={styles.dropdownWrapper} ref={positionRef}>
261+
<button
262+
type="button"
263+
disabled={!selectedCategory}
264+
onClick={() => {
265+
if (!selectedCategory) return;
266+
setShowPositionDropdown(prev => !prev);
267+
setShowCategoryDropdown(false);
268+
}}
269+
aria-expanded={showPositionDropdown}
270+
>
271+
{selectedPosition || 'Select Positions'}
272+
</button>
273+
274+
{showPositionDropdown && selectedCategory && (
275+
<div className={styles.jobSelect}>
276+
{positions.length > 0 ? (
277+
positions.map(pos => (
278+
<button
279+
key={pos}
280+
type="button"
281+
className={styles.dropdownItem}
282+
onClick={() => {
283+
setSelectedPosition(pos);
284+
setShowPositionDropdown(false);
285+
setCurrentPage(1);
286+
}}
287+
>
288+
{pos}
289+
</button>
290+
))
291+
) : (
292+
<div className={styles.dropdownItem}>No positions found</div>
293+
)}
294+
</div>
295+
)}
296+
</div>
199297
</nav>
200298

201299
{/* HEADINGS */}
@@ -211,8 +309,10 @@ function Collaboration() {
211309
<p>
212310
{searchTerm
213311
? `Listing results for '${searchTerm}'`
214-
: categoriesSelected.length
215-
? 'Listing results for selected categories'
312+
: selectedPosition
313+
? `Listing results for '${selectedPosition}' in '${selectedCategory}'`
314+
: selectedCategory
315+
? `Listing results for '${selectedCategory}'`
216316
: 'Listing all job ads.'}
217317
</p>
218318
<button className="btn btn-secondary" onClick={handleShowSummaries}>
@@ -221,13 +321,10 @@ function Collaboration() {
221321
</div>
222322

223323
{/* FILTER CHIPS */}
224-
{categoriesSelected.length > 0 && (
324+
{(selectedCategory || selectedPosition) && (
225325
<div className={styles.jobQueries}>
226-
{categoriesSelected.map(cat => (
227-
<span key={cat} className={styles.chip}>
228-
{cat}
229-
</span>
230-
))}
326+
{selectedCategory && <span className={styles.chip}>{selectedCategory}</span>}
327+
{selectedPosition && <span className={styles.chip}>{selectedPosition}</span>}
231328
<button className={styles.clearAllButton} onClick={handleClearAllFilters}>
232329
Clear All
233330
</button>
@@ -257,7 +354,7 @@ function Collaboration() {
257354

258355
{/* PAGINATION */}
259356
<div className={styles.pagination}>
260-
{Array.from({ length: Math.max(totalPages, 2) }, (_, i) => (
357+
{Array.from({ length: totalPages }, (_, i) => (
261358
<button
262359
key={i}
263360
onClick={() => setCurrentPage(i + 1)}

0 commit comments

Comments
 (0)