feat: fetches jobs per board

This commit is contained in:
jongomez
2023-10-25 14:25:02 +01:00
parent 3da1faef69
commit 330abd47ad
12 changed files with 477 additions and 165 deletions
@@ -18,7 +18,7 @@ import {
SectionHeader,
TimelineItem,
ShowcaseCard,
JobList,
JobLists,
} from '../components/mdx'
<Hero size="large">
@@ -353,4 +353,4 @@ import {
Build on Waku
</CallToActionButton>
</Box>
<JobList jobBoard="nomos" />
<JobLists jobBoard="logos" />
@@ -1,53 +0,0 @@
import React, { useState } from 'react'
import useFetchJobs, { JobBoard, Job } from './useFetchJobs'
type Props = {
jobBoard: JobBoard
titleFilter?: string
}
export const JobList: React.FC<Props> = ({ jobBoard, titleFilter = '' }) => {
const { data, error, fetchJobs } = useFetchJobs()
const [isLoading, setIsLoading] = useState(false)
const handleFetchJobs = async () => {
setIsLoading(true)
await fetchJobs([jobBoard], titleFilter)
setIsLoading(false)
}
React.useEffect(() => {
handleFetchJobs()
}, [jobBoard, titleFilter])
if (isLoading) {
return <div>Loading...</div>
}
if (error) {
return <div>Error: {error.message}</div>
}
if (!data) {
return <div>No data</div>
}
return (
<div>
<h2>Job List</h2>
<ul>
{data.jobs.map((job: Job, index: number) => (
<li key={index}>
<a
href={job.absolute_url}
target="_blank"
rel="noopener noreferrer"
>
{job.title}
</a>
</li>
))}
</ul>
</div>
)
}
@@ -1 +0,0 @@
export * from './JobList'
@@ -1,108 +0,0 @@
import { useState } from 'react'
// Inspired by:
// https://github.com/acid-info/rnd-dynamic-list/blob/master/react/useFetchJobs.js
const jobBoards = [
'acidtest',
'logos',
'status',
'nimbus',
'codex',
'nomos',
'statusnetwork',
'ift',
'vac',
'waku',
] as const
export type JobBoard = (typeof jobBoards)[number]
const jobBoardMappings: Record<JobBoard, string> = {
acidtest: 'testacidinfo',
logos: 'logos',
status: 'status72',
nimbus: 'nimbus',
codex: 'codex',
nomos: 'nomos',
statusnetwork: 'thestatusnetwork',
ift: 'instituteoffreetechnologies',
vac: 'vac',
waku: 'waku',
}
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 // Assuming retention_period can be a string or null.
}[]
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
type UseFetchJobs = {
data: JobData
error: any
fetchJobs: (jobBoardsToFetch: JobBoard[], titleFilter: string) => void
}
export const useFetchJobs = (): UseFetchJobs => {
const [data, setData] = useState<JobData>(null)
const [error, setError] = useState<Error | null>(null)
const fetchJobs = async (jobBoardsToFetch: JobBoard[], titleFilter) => {
try {
const resultsPerBoard: Partial<Record<JobBoard, string>> = {}
for (let board of jobBoardsToFetch) {
const response = await fetch(
`https://boards-api.greenhouse.io/v1/boards/${jobBoardMappings[board]}/jobs`,
)
const jobData = await response.json()
resultsPerBoard[board] = jobData.jobs
}
let jobs = Object.values(resultsPerBoard).flat() as unknown as Job[]
if (titleFilter) {
jobs = jobs.filter(
(job) => job.title && job.title.includes(titleFilter),
)
}
setData({ jobs, meta: { total: jobs.length } })
} catch (err) {
if (err instanceof Error) {
setError(err)
} else if (typeof err === 'string') {
setError(new Error(err))
} else {
setError(new Error('Unknown error'))
}
console.error(err)
}
}
return { data, error, fetchJobs }
}
export default useFetchJobs
@@ -0,0 +1,52 @@
@use '../../../css/utils';
@use '../../../css/lsd';
.mdx-jl__header {
margin-bottom: 40px;
margin-top: 16px;
}
.mdx-jl__single-job-board-container {
border-top: 1px solid rgb(var(--lsd-border-primary));
padding-top: 8px;
}
.mdx-jl__board-title {
font-size: 12px;
line-height: 16px;
}
.mdx-jl__job-title-container {
display: flex;
align-items: center;
padding-bottom: 8px;
}
.mdx-jl__external-link-icon {
margin-left: 8px;
}
.mdx-jl__job-list {
list-style-type: none;
padding: 8px 0 24px 0;
margin: 0;
}
.mdx-jl__job-list-item {
padding: 14px 0;
}
.mdx-jl__job-link {
display: block;
width: fit-content;
text-decoration: none;
&:hover {
text-decoration: none;
.mdx-jl__job-title {
text-decoration: underline;
}
}
}
@@ -0,0 +1,77 @@
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>
)
}
@@ -0,0 +1,47 @@
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>
)
}
@@ -0,0 +1 @@
export * from './JobLists'
@@ -0,0 +1,145 @@
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,
},
},
}
@@ -0,0 +1,152 @@
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
@@ -24,4 +24,4 @@ export * from './Showcase'
export * from './ShowcaseCard'
export * from './SocialCard'
export * from './TimelineItem'
export * from './JobList'
export * from './JobLists'