mirror of
https://github.com/wavetermdev/docusaurus-og.git
synced 2026-08-05 13:46:35 -07:00
feat: fetches jobs per board
This commit is contained in:
@@ -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>
|
||||
)
|
||||
}
|
||||
+47
@@ -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'
|
||||
+145
@@ -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'
|
||||
|
||||
Reference in New Issue
Block a user