fix: use the department API endpoint instead - working version

This commit is contained in:
jongomez
2023-10-25 15:24:02 +01:00
parent 330abd47ad
commit ebeb007931
13 changed files with 847 additions and 436 deletions
@@ -18,7 +18,7 @@ import {
SectionHeader,
TimelineItem,
ShowcaseCard,
JobLists,
JobsPerDepartment,
} from '../components/mdx'
<Hero size="large">
@@ -353,4 +353,4 @@ import {
Build on Waku
</CallToActionButton>
</Box>
<JobLists jobBoard="logos" />
<JobsPerDepartment jobBoard="status72" useDummyData />
@@ -1,77 +0,0 @@
import { Typography } from '@acid-info/lsd-react'
import { SingleJobBoardList } from './SingleJobBoardList'
import useFetchJobs, { JobBoard, JobsPerBoard } from './useFetchJobs'
import React from 'react'
import { jobListsDummyData } from './jobListsDummyData'
const hasJobs = (jobsPerBoard: JobsPerBoard): boolean => {
if (!jobsPerBoard) return false
let jobFound = false
Object.keys(jobsPerBoard).forEach((board) => {
if (jobsPerBoard[board]!.jobs.length > 0) {
jobFound = true
return
}
})
return jobFound
}
type JobListsProps = React.HTMLAttributes<HTMLDivElement> & {
jobBoard: JobBoard
titleFilter?: string
fetchAll?: boolean
useDummyData?: boolean
}
export const JobLists: React.FC<JobListsProps> = ({
jobBoard,
titleFilter = '',
fetchAll = false,
useDummyData = false,
...props
}) => {
const { data, error, isLoading } = useFetchJobs(
[jobBoard],
fetchAll,
titleFilter,
useDummyData ? jobListsDummyData : null,
)
console.log('data', data)
console.log('error', error)
console.log('isLoading', isLoading)
if (isLoading) {
return <div>Loading...</div>
}
if (error) {
return <div>Error fetching data</div>
}
if (!data || !hasJobs(data)) {
return <div>No job openings to show</div>
}
const returnedJobBoards = Object.keys(data)
return (
<div {...props}>
<Typography variant="h1" className="mdx-jl__header">
Current job openings
</Typography>
{returnedJobBoards.map((board) => {
const jobDataForBoard = data[board]
return (
<SingleJobBoardList
key={board}
jobBoard={board}
jobs={jobDataForBoard ? jobDataForBoard.jobs : []}
/>
)
})}
</div>
)
}
@@ -1,47 +0,0 @@
import { Typography } from '@acid-info/lsd-react'
import { Job, JobBoard } from './useFetchJobs'
import React from 'react'
import { IconExternalLink } from '@logos-theme/components/Icon'
import './JobLists.scss'
type SingleJobBoardListProps = {
jobBoard: JobBoard
jobs: Job[]
}
export const SingleJobBoardList: React.FC<SingleJobBoardListProps> = ({
jobBoard,
jobs,
}) => {
return (
<div className="mdx-jl__single-job-board-container">
<Typography variant="subtitle2" className="mdx-jl__board-title">
{jobBoard}
</Typography>
<ul className="mdx-jl__job-list">
{jobs.map((job, index) => (
<li key={index} className="mdx-jl__job-list-item">
<a
href={job.absolute_url}
target="_blank"
className="mdx-jl__job-link"
>
<div className="mdx-jl__job-title-container">
<Typography variant="h6" className="mdx-jl__job-title">
{job.title}
</Typography>
<IconExternalLink className="mdx-jl__external-link-icon" />
</div>
{!!job.location?.name && (
<Typography variant="subtitle2" component="div">
{job.location.name}
</Typography>
)}
</a>
</li>
))}
</ul>
</div>
)
}
@@ -1 +0,0 @@
export * from './JobLists'
@@ -1,145 +0,0 @@
import { JobsPerBoard } from './useFetchJobs'
// Only for dev purposes - prevents making multiple requests to the API while developing.
export const jobListsDummyData: JobsPerBoard = {
nomos: {
jobs: [
{
absolute_url: 'https://boards.greenhouse.io/nomos/jobs/5453089',
data_compliance: [
{
type: 'gdpr',
requires_consent: false,
requires_processing_consent: false,
requires_retention_consent: false,
retention_period: null,
},
],
internal_job_id: 2315608,
location: {
name: 'Remote (Worldwide)',
},
metadata: null,
id: 5453089,
updated_at: '2023-10-20T07:06:31-04:00',
requisition_id: 'PROV-Nom-5',
title: 'Applied Network Researcher',
},
],
meta: {
total: 1,
},
},
waku: {
jobs: [
{
absolute_url: 'https://boards.greenhouse.io/waku/jobs/5453096',
data_compliance: [
{
type: 'gdpr',
requires_consent: false,
requires_processing_consent: false,
requires_retention_consent: false,
retention_period: null,
},
],
internal_job_id: 2055187,
location: {
name: 'Remote (Worldwide)',
},
metadata: null,
id: 5453096,
updated_at: '2023-10-20T07:00:46-04:00',
requisition_id: 'PROV-Sec-2',
title: 'Protocol Engineer',
},
{
absolute_url: 'https://boards.greenhouse.io/waku/jobs/5453098',
data_compliance: [
{
type: 'gdpr',
requires_consent: false,
requires_processing_consent: false,
requires_retention_consent: false,
retention_period: null,
},
],
internal_job_id: 1830496,
location: {
name: 'Remote, Worldwide',
},
metadata: null,
id: 5453098,
updated_at: '2023-10-20T07:01:13-04:00',
requisition_id: 'PROV-Sec-3',
title: 'Protocol Researcher (Distributed Systems)',
},
{
absolute_url: 'https://boards.greenhouse.io/waku/jobs/5453099',
data_compliance: [
{
type: 'gdpr',
requires_consent: false,
requires_processing_consent: false,
requires_retention_consent: false,
retention_period: null,
},
],
internal_job_id: 2584916,
location: {
name: 'Remote (Worldwide)',
},
metadata: null,
id: 5453099,
updated_at: '2023-10-20T07:01:37-04:00',
requisition_id: 'SDK-2',
title: 'Software Engineer (Chat SDK)',
},
{
absolute_url: 'https://boards.greenhouse.io/waku/jobs/5453102',
data_compliance: [
{
type: 'gdpr',
requires_consent: false,
requires_processing_consent: false,
requires_retention_consent: false,
retention_period: null,
},
],
internal_job_id: 2656108,
location: {
name: 'Remote (Worldwide)',
},
metadata: null,
id: 5453102,
updated_at: '2023-10-20T07:02:20-04:00',
requisition_id: 'PROV-Wak-13',
title: 'Software Engineer Distributed Systems Testing',
},
{
absolute_url: 'https://boards.greenhouse.io/waku/jobs/5453103',
data_compliance: [
{
type: 'gdpr',
requires_consent: false,
requires_processing_consent: false,
requires_retention_consent: false,
retention_period: null,
},
],
internal_job_id: 2694724,
location: {
name: 'Remote (Worldwide)',
},
metadata: null,
id: 5453103,
updated_at: '2023-10-20T13:12:19-04:00',
requisition_id: 'WAK-BD-1',
title: 'Technical Business Development Lead ',
},
],
meta: {
total: 5,
},
},
}
@@ -1,152 +0,0 @@
import { useEffect, useRef, useState } from 'react'
/*
const allJobBoards = [
'testacidinfo', // Acid Test job board
'logos',
'nimbus',
'codex',
'nomos',
'thestatusnetwork',
'instituteoffreetechnologies', // aka IFT
'vac',
'waku',
] as const
*/
const allJobBoards = ['codex', 'nomos', 'vac', 'waku'] as const
export type JobBoard = string
export type Job = {
absolute_url: string
data_compliance?: {
type: string
requires_consent: boolean
requires_processing_consent: boolean
requires_retention_consent: boolean
retention_period: null | string
}[]
internal_job_id: number
location: {
name: string
}
metadata?: any
id: number
updated_at: string
requisition_id: string
title: string
}
type JobData = {
jobs: Job[]
meta: {
total: number
}
} | null
export type JobsPerBoard = Record<JobBoard, JobData> | null
type UseFetchJobsReturn = {
data: JobsPerBoard
error: Error | null
isLoading: boolean
}
const parseJobs = (rawJobs: Job[], titleFilter?: string): JobData => {
const jobs = titleFilter
? rawJobs.filter((job) => job.title?.includes(titleFilter))
: rawJobs
return { jobs, meta: { total: jobs.length } }
}
export const useFetchJobs = (
jobBoardsToFetch: JobBoard[] = [],
fetchAllJobBoards: boolean = false,
titleFilter: string = '',
dummyResponse: JobsPerBoard = null,
): UseFetchJobsReturn => {
const [data, setData] = useState<JobsPerBoard>(null)
const [error, setError] = useState<Error | null>(null)
const [isLoading, setIsLoading] = useState<boolean>(false)
const isMounted = useRef(true)
const fetchJobs = async (): Promise<{
jobsPerBoard: JobsPerBoard
errorData: Error | null
}> => {
try {
if (dummyResponse) {
return { jobsPerBoard: dummyResponse, errorData: null }
}
const jobsPerBoard: JobsPerBoard = {}
if (fetchAllJobBoards) {
const promises = allJobBoards.map((boardName) =>
fetch(
`https://boards-api.greenhouse.io/v1/boards/${boardName}/jobs`,
).then((response) => response.json()),
)
const allResults = await Promise.all(promises)
allResults.forEach((result, index) => {
const currentBoard = allJobBoards[index]
if (!currentBoard) {
throw new Error('No current board found for ' + result)
}
jobsPerBoard[currentBoard] = parseJobs(result.jobs, titleFilter)
})
} else {
for (let board of jobBoardsToFetch) {
const response = await fetch(
`https://boards-api.greenhouse.io/v1/boards/${board}/jobs`,
)
const jobData = await response.json()
jobsPerBoard[board] = parseJobs(jobData.jobs, titleFilter)
}
}
return {
jobsPerBoard,
errorData: null,
}
} catch (err) {
const errorData =
err instanceof Error
? err
: typeof err === 'string'
? new Error(err)
: new Error('Unknown error')
console.error(err)
return { jobsPerBoard: null, errorData }
}
}
useEffect(() => {
if (isLoading) return
setIsLoading(true)
fetchJobs().then(({ jobsPerBoard, errorData }) => {
// We should not update state if the component is unmounted.
if (!isMounted.current) return
if (jobsPerBoard) setData(jobsPerBoard)
if (errorData) setError(errorData)
setIsLoading(false)
})
return () => {
isMounted.current = false
}
}, [])
return { data, error, isLoading }
}
export default useFetchJobs
@@ -1,43 +1,43 @@
@use '../../../css/utils';
@use '../../../css/lsd';
.mdx-jl__header {
.mdx-jpd__header {
margin-bottom: 40px;
margin-top: 16px;
}
.mdx-jl__single-job-board-container {
.mdx-jpd__single-job-department-container {
border-top: 1px solid rgb(var(--lsd-border-primary));
padding-top: 8px;
}
.mdx-jl__board-title {
font-size: 12px;
line-height: 16px;
.mdx-jpd__department-title {
font-size: 12px !important;
line-height: 16px !important;
}
.mdx-jl__job-title-container {
.mdx-jpd__job-title-container {
display: flex;
align-items: center;
padding-bottom: 8px;
}
.mdx-jl__external-link-icon {
.mdx-jpd__external-link-icon {
margin-left: 8px;
}
.mdx-jl__job-list {
.mdx-jpd__job-list {
list-style-type: none;
padding: 8px 0 24px 0;
margin: 0;
}
.mdx-jl__job-list-item {
.mdx-jpd__job-list-item {
padding: 14px 0;
}
.mdx-jl__job-link {
.mdx-jpd__job-link {
display: block;
width: fit-content;
text-decoration: none;
@@ -45,7 +45,7 @@
&:hover {
text-decoration: none;
.mdx-jl__job-title {
.mdx-jpd__job-title {
text-decoration: underline;
}
}
@@ -0,0 +1,66 @@
import { Typography } from '@acid-info/lsd-react'
import { SingleDepartmentJobs } from './SingleDepartmentJobs'
import useFetchJobs, { JobDepartmentArray } from './useFetchJobs'
import React from 'react'
import { jobsPerDepartmentDummyData } from './jobsPerDepartmentDummyData'
const hasJobs = (jobsPerDepartment: JobDepartmentArray): boolean => {
if (!jobsPerDepartment) return false
// Check if there's any department that has at least one job
let jobFound = jobsPerDepartment.some((department) => {
return department.jobs && department.jobs.length > 0
})
return jobFound
}
type JobsPerDepartmentProps = React.HTMLAttributes<HTMLDivElement> & {
jobBoard: string
titleFilter?: string
fetchAll?: boolean
useDummyData?: boolean
}
export const JobsPerDepartment: React.FC<JobsPerDepartmentProps> = ({
jobBoard,
titleFilter = '',
fetchAll = false,
useDummyData = false,
...props
}) => {
const { data, error, isLoading } = useFetchJobs(
jobBoard,
titleFilter,
useDummyData ? jobsPerDepartmentDummyData : null,
)
console.log('data', data)
console.log('error', error)
console.log('isLoading', isLoading)
if (isLoading) {
return <div>Loading...</div>
}
if (error) {
return <div>Error fetching data</div>
}
if (!data || !hasJobs(data)) {
return <div>No job openings to show</div>
}
return (
<div {...props}>
<Typography variant="h1" className="mdx-jpd__header">
Current job openings
</Typography>
{data.map((department) => {
return (
<SingleDepartmentJobs key={department.name} department={department} />
)
})}
</div>
)
}
@@ -0,0 +1,49 @@
import { Typography } from '@acid-info/lsd-react'
import { Job, JobDepartmentData } from './useFetchJobs'
import React from 'react'
import { IconExternalLink } from '@logos-theme/components/Icon'
import './JobsPerDepartment.scss'
type SingleDepartmentJobsProps = {
department: JobDepartmentData
}
export const SingleDepartmentJobs: React.FC<SingleDepartmentJobsProps> = ({
department,
}) => {
if (!department.jobs || department.jobs.length === 0) {
return null
}
return (
<div className="mdx-jpd__single-job-department-container">
<Typography variant="subtitle2" className="mdx-jpd__department-title">
{department.name}
</Typography>
<ul className="mdx-jpd__job-list">
{department.jobs.map((job, index) => (
<li key={index} className="mdx-jpd__job-list-item">
<a
href={job.absolute_url}
target="_blank"
className="mdx-jpd__job-link"
>
<div className="mdx-jpd__job-title-container">
<Typography variant="h6" className="mdx-jpd__job-title">
{job.title}
</Typography>
<IconExternalLink className="mdx-jpd__external-link-icon" />
</div>
{!!job.location?.name && (
<Typography variant="subtitle2" component="div">
{job.location.name}
</Typography>
)}
</a>
</li>
))}
</ul>
</div>
)
}
@@ -0,0 +1 @@
export * from './JobsPerDepartment'
@@ -0,0 +1,162 @@
import { useEffect, useRef, useState } from 'react'
/*
// Array of possible job boards:
const allJobBoards = [
'testacidinfo', // Acid Test job board
'logos',
'nimbus',
'codex',
'status72', // Note: this job board has all the jobs from the other job boards
'nomos',
'thestatusnetwork',
'instituteoffreetechnologies', // aka IFT
'vac',
'waku',
] as const
*/
export type JobDepartmentName = string
export type JobDepartmentData = {
id: number
name: string
parent_id: number | null
child_ids: number[]
jobs: Job[]
}
export type Job = {
absolute_url: string
data_compliance?: {
type: string
requires_consent: boolean
requires_processing_consent: boolean
requires_retention_consent: boolean
retention_period: null | string
}[]
internal_job_id: number
location: {
name: string
}
metadata?: any
id: number
updated_at: string
requisition_id: string
title: string
}
export type JobDepartmentArray = JobDepartmentData[] | null
type UseFetchJobsReturn = {
data: JobDepartmentArray
error: Error | null
isLoading: boolean
}
const parseJobDepartments = (
rawJobDepartmentData: any,
titleFilter?: string,
): JobDepartmentArray => {
const jobDepartmentArray = rawJobDepartmentData?.departments
if (!jobDepartmentArray) {
throw Error('No departments found in rawJobDepartmentData')
}
if (!Array.isArray(jobDepartmentArray)) {
throw Error('The received jobDepartmentArray is not an array')
}
// Map through each department and filter its jobs based on titleFilter
const processedJobDepartmentArray: JobDepartmentData[] =
jobDepartmentArray.map((department: JobDepartmentData) => {
let filteredJobs = department.jobs
if (titleFilter) {
filteredJobs = department.jobs.filter((job) =>
job.title.includes(titleFilter),
)
}
return {
...department,
jobs: filteredJobs,
}
})
return processedJobDepartmentArray
}
// Fetches jobs from greenhouse via their department endpoint:
// https://developers.greenhouse.io/job-board.html#list-departments
export const useFetchJobs = (
jobBoardToFetch: string,
titleFilter: string = '',
dummyResponse: JobDepartmentArray = null,
): UseFetchJobsReturn => {
const [data, setData] = useState<JobDepartmentArray>(null)
const [error, setError] = useState<Error | null>(null)
const [isLoading, setIsLoading] = useState<boolean>(false)
const isMounted = useRef(true)
const fetchJobs = async (): Promise<{
jobsPerDepartment: JobDepartmentArray
errorData: Error | null
}> => {
try {
if (dummyResponse) {
console.log('dummy data bruh:', dummyResponse)
return { jobsPerDepartment: dummyResponse, errorData: null }
}
const response = await fetch(
`https://boards-api.greenhouse.io/v1/boards/${jobBoardToFetch}/departments`,
)
const jobDepartmentData = await response.json()
const jobsPerDepartment: JobDepartmentArray = parseJobDepartments(
jobDepartmentData,
titleFilter,
)
return {
jobsPerDepartment,
errorData: null,
}
} catch (err) {
const errorData =
err instanceof Error
? err
: typeof err === 'string'
? new Error(err)
: new Error('Unknown error')
console.error(err)
return { jobsPerDepartment: null, errorData }
}
}
useEffect(() => {
if (isLoading) return
setIsLoading(true)
fetchJobs().then(({ jobsPerDepartment, errorData }) => {
// We should not update state if the component is unmounted.
if (!isMounted.current) return
if (jobsPerDepartment) setData(jobsPerDepartment)
if (errorData) setError(errorData)
setIsLoading(false)
})
return () => {
isMounted.current = false
}
}, [])
return { data, error, isLoading }
}
export default useFetchJobs
@@ -24,4 +24,4 @@ export * from './Showcase'
export * from './ShowcaseCard'
export * from './SocialCard'
export * from './TimelineItem'
export * from './JobLists'
export * from './JobsPerDepartment'