Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
114 changes: 98 additions & 16 deletions src/components/Collaboration/Collaboration.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,20 @@ function Collaboration() {
url:
'https://images.unsplash.com/photo-1581092160562-40aa08e78837?w=640&h=480&fit=crop&q=80',
},
{
keywords: ['architecture', 'landscape', 'environment'],
url:
'https://images.unsplash.com/photo-1486406146926-c627a92ad1ab?w=640&h=480&fit=crop&q=80',
},
{
keywords: ['creative', 'media', 'graphic'],
url: 'https://images.unsplash.com/photo-1561070791-2526d30994b5?w=640&h=480&fit=crop&q=80',
},
{
keywords: ['skilled', 'trades', 'craft', 'carpenter'],
url:
'https://images.unsplash.com/photo-1504148455328-c376907d081c?w=640&h=480&fit=crop&q=80',
},
];

// Find matching category
Expand Down Expand Up @@ -265,18 +279,43 @@ function Collaboration() {
);
};

const handleCategoryChange = e => {
const selectedValue = e.target.value;
setSelectedCategory(selectedValue || '');
const selectCategory = (selectedValue = '') => {
setSelectedCategory(selectedValue);
setCurrentPage(1);
setSummaries(null);
setActiveTab('jobPostings');
// The job refresh runs independently of the category state updates above.
fetchJobAds({ category: selectedValue || '', page: 1 }).catch(() =>
if (!selectedValue && !submittedSearchTerm) {
setJobAds([]);
setTotalPages(0);
setLoadingJobs(false);
return;
}
fetchJobAds({ category: selectedValue, page: 1 }).catch(() =>
toast.error('Error fetching jobs'),
);
};

const handleCategoryChange = e => {
selectCategory(e.target.value);
};

const handleCategoryCardClick = event => {
const { category } = event.currentTarget.dataset;
if (category) selectCategory(category);
};

const handleBackToCategories = () => {
setSelectedCategory('');
setSubmittedSearchTerm('');
setSearchTerm('');
setCurrentPage(1);
setJobAds([]);
setTotalPages(0);
setSummaries(null);
setActiveTab('jobPostings');
globalThis.scrollTo({ top: 0, behavior: 'smooth' });
};

const showBothFiltersTooltip =
activeTab === 'jobPostings' &&
!bothFiltersTooltipDismissed &&
Expand All @@ -298,19 +337,21 @@ function Collaboration() {

if (!response.ok) throw new Error(`Failed to reset filters: ${response.statusText}`);

const data = await response.json();
if (requestId !== requestIdRef.current) return;
setSearchTerm('');
setSubmittedSearchTerm('');
setSelectedCategory('');
setCurrentPage(1);
setJobAds(dedupeJobsByTitle(Array.isArray(data?.jobs) ? data.jobs : []));
setTotalPages(data?.pagination?.totalPages || 0);
// Return to category cards; jobs load after the next category/search choice.
setJobAds([]);
setTotalPages(0);
setJobsFetchError(null);
setSummaries(null);
setSummariesAll([]);
setSummariesPage(1);
setSummariesTotalPages(0);
setActiveTab('jobPostings');
await fetchCategories();
globalThis.scrollTo({ top: 0, behavior: 'smooth' });
} catch {
if (requestId !== requestIdRef.current) return;
Expand Down Expand Up @@ -398,12 +439,33 @@ function Collaboration() {
setPage(Number(event.currentTarget.dataset.page));
};

const showCategoryBrowse = !selectedCategory && !submittedSearchTerm;

const renderCategoryOption = category => (
<option key={category} value={category}>
{category}
</option>
);

const renderCategoryCard = category => (
<button
type="button"
key={category}
className={styles.jobAd}
data-category={category}
aria-label={`Browse ${category} jobs`}
onClick={handleCategoryCardClick}
>
<img
src={getCategoryImage(category)}
alt={category}
loading="lazy"
onError={handleImageError}
/>
<h3 className={styles.categoryTitle}>{category}</h3>
</button>
);

const renderJobAd = ad => {
if (!ad?._id) return null;
const jobTitle = ad.title || 'Untitled Position';
Expand Down Expand Up @@ -433,6 +495,13 @@ function Collaboration() {
};

const renderJobContent = () => {
if (showCategoryBrowse) {
if (!categories.length) {
return <p className={styles.noJobads}>No job categories available.</p>;
}
return <>{categories.map(renderCategoryCard)}</>;
}

if (loadingJobs) {
return <p className={styles.noJobads}>Loading jobs...</p>;
}
Expand All @@ -459,10 +528,9 @@ function Collaboration() {
</button>
);

// Initial fetch and setup
// Initial fetch and setup — categories first; jobs load after category/search.
useEffect(() => {
// Initial API requests intentionally run in the background during mount.
fetchJobAds().catch(() => toast.error('Error fetching jobs'));
setLoadingJobs(false);
fetchCategories().catch(() => toast.error('Error fetching categories'));
const handleResize = () => {
if (resizeTimeoutRef.current) clearTimeout(resizeTimeoutRef.current);
Expand All @@ -472,8 +540,9 @@ function Collaboration() {
columnsRef.current = newCols;
setColumns(newCols);
setCurrentPage(1);
// Resize-triggered refresh intentionally runs in the background to keep resizing responsive.
fetchJobAds({ page: 1 }).catch(() => toast.error('Error fetching jobs'));
if (selectedCategory || submittedSearchTerm) {
fetchJobAds({ page: 1 }).catch(() => toast.error('Error fetching jobs'));
}
}, 200);
};
globalThis.addEventListener('resize', handleResize);
Expand Down Expand Up @@ -546,7 +615,7 @@ function Collaboration() {
value={selectedCategory}
onChange={handleCategoryChange}
>
<option value="">Select from Categories</option>
<option value="">Select From Categories</option>
{categories.map(c => (
<option key={c} value={c}>
{c}
Expand Down Expand Up @@ -660,7 +729,7 @@ function Collaboration() {
value={selectedCategory}
onChange={handleCategoryChange}
>
<option value="">Select From Positions</option>
<option value="">Select From Categories</option>
{categories.map(renderCategoryOption)}
</select>
</div>
Expand All @@ -683,12 +752,25 @@ function Collaboration() {
<>
<div className={styles.headings}>
<h1 className={styles.mainHeading}>LIKE TO WORK WITH US? APPLY NOW!</h1>
{showCategoryBrowse && <p>Choose a category to see open positions.</p>}
{!showCategoryBrowse && selectedCategory && (
<div className={styles.categoryBrowseHeader}>
<button
type="button"
className={styles.backToCategories}
onClick={handleBackToCategories}
>
← All categories
</button>
<p>{`Positions in '${selectedCategory}'`}</p>
</div>
)}
{submittedSearchTerm && <p>{`Showing results for '${submittedSearchTerm}'`}</p>}
</div>

<div className={styles.jobList}>{renderJobContent()}</div>

{totalPages > 1 && (
{!showCategoryBrowse && totalPages > 1 && (
<div className={styles.pagination}>
{Array.from({ length: totalPages }, renderPaginationButton)}
</div>
Expand Down
30 changes: 30 additions & 0 deletions src/components/Collaboration/Collaboration.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -274,6 +274,36 @@
letter-spacing: 0.5px;
}

.categoryBrowseHeader {
display: flex;
flex-direction: column;
align-items: center;
gap: 0.35rem;
margin-top: 0.5rem;
}

.backToCategories {
border: none;
background: transparent;
color: #2b6ee8;
font-weight: 600;
cursor: pointer;
padding: 0.25rem 0.5rem;
text-decoration: underline;
}

.backToCategories:hover {
color: #1a4fb0;
}

.jobLandingDark .backToCategories {
color: #9ec1ff;
}

.jobLandingDark .backToCategories:hover {
color: #c9dcff;
}

.jobAd:hover {
transform: translateY(-5px);
box-shadow: 0 6px 12px rgb(0 0 0 / 20%);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -152,16 +152,25 @@ function getFormIdFromNavigation(location) {
return '';
}

/** Skip retired / internal ATS templates when picking applicant forms. */
function isApplicantFacingForm(form) {
const title = String(form?.title || '')
.trim()
.toUpperCase();
if (!title) return false;
if (title.startsWith('[RETIRED]')) return false;
if (title.includes('APPLIED THROUGH SITE')) return false;
if (title.includes('TEST CREATE FORM')) return false;
return true;
}

/** Default questionnaire when a job listing has no dedicated form. */
function findDefaultSoftwareForm(formsArr) {
if (!formsArr?.length) return null;
const preferredTitles = [
'Software Developer',
'Software Engineer',
'APPLIED THROUGH SITE - SEEKING SOFTWARE POSITION',
];
const activeForms = formsArr.filter(isApplicantFacingForm);
const preferredTitles = ['SEEKING SOFTWARE POSITION', 'Software Developer', 'Software Engineer'];
for (const title of preferredTitles) {
const exact = formsArr.find(
const exact = activeForms.find(
f =>
String(f.title || '')
.trim()
Expand All @@ -170,10 +179,11 @@ function findDefaultSoftwareForm(formsArr) {
if (exact) return exact;
}
return (
findFormForJobTitle(formsArr, 'Software Developer') ||
findFormForJobTitle(formsArr, 'Software Engineer') ||
formsArr.find(f => f.questions?.length) ||
formsArr[0] ||
findFormForJobTitle(activeForms, 'SEEKING SOFTWARE POSITION') ||
findFormForJobTitle(activeForms, 'Software Developer') ||
findFormForJobTitle(activeForms, 'Software Engineer') ||
activeForms.find(f => f.questions?.length) ||
activeForms[0] ||
null
);
}
Expand Down
Loading
Loading