Merge pull request #125 from acid-info/create-job-lists

Implement JobsPerDepartment component
This commit is contained in:
amir houieh
2023-10-27 12:31:20 +02:00
committed by GitHub
8 changed files with 901 additions and 0 deletions
@@ -18,6 +18,7 @@ import {
SectionHeader,
TimelineItem,
ShowcaseCard,
JobsPerDepartment,
} from '../components/mdx'
<Hero size="large">
@@ -352,3 +353,4 @@ import {
Build on Waku
</CallToActionButton>
</Box>
<JobsPerDepartment jobBoard="codex" useDummyData />
@@ -0,0 +1,52 @@
@use '../../../css/utils';
@use '../../../css/lsd';
.mdx-jpd__header {
margin-bottom: 40px;
margin-top: 16px;
}
.mdx-jpd__single-job-department-container {
border-top: 1px solid rgb(var(--lsd-border-primary));
padding-top: 8px;
}
.mdx-jpd__department-title {
font-size: 12px !important;
line-height: 16px !important;
}
.mdx-jpd__job-title-container {
display: flex;
align-items: center;
padding-bottom: 8px;
}
.mdx-jpd__external-link-icon {
margin-left: 8px;
}
.mdx-jpd__job-list {
list-style-type: none;
padding: 8px 0 24px 0;
margin: 0;
}
.mdx-jpd__job-list-item {
padding: 14px 0;
}
.mdx-jpd__job-link {
display: block;
width: fit-content;
text-decoration: none;
&:hover {
text-decoration: none;
.mdx-jpd__job-title {
text-decoration: underline;
}
}
}
@@ -0,0 +1,79 @@
import { Typography } from '@acid-info/lsd-react'
import { SingleDepartmentJobs } from './SingleDepartmentJobs'
import useFetchJobs, { JobDepartmentArray } from './useFetchJobs'
import React from 'react'
import { jobsPerDepartmentDummyData } from './jobsPerDepartmentDummyData'
type JobsPerDepartmentHeaderProps = {
message?: string
}
const JobsPerDepartmentHeader: React.FC<JobsPerDepartmentHeaderProps> = ({
message,
}) => {
return (
<>
<Typography variant="h1" className="mdx-jpd__header">
Current job openings
</Typography>
{!!message && <Typography variant="body1">{message}</Typography>}
</>
)
}
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,
)
if (isLoading) {
// Skipping loading state for now, as per the designer's request.
return <JobsPerDepartmentHeader />
}
if (error) {
return <JobsPerDepartmentHeader message="Error fetching data" />
}
if (!data || !hasJobs(data)) {
return <JobsPerDepartmentHeader message="No job openings to show" />
}
return (
<div {...props}>
<JobsPerDepartmentHeader />
{data.map((department) => {
return (
<SingleDepartmentJobs key={department.name} department={department} />
)
})}
</div>
)
}
@@ -0,0 +1,50 @@
import { Typography } from '@acid-info/lsd-react'
import { JobDepartmentData } from './useFetchJobs'
import React from 'react'
import { IconExternalLink } from '@logos-theme/components/Icon'
import './JobsPerDepartment.scss'
import Link from '@docusaurus/Link'
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">
<Link
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>
)}
</Link>
</li>
))}
</ul>
</div>
)
}
@@ -0,0 +1 @@
export * from './JobsPerDepartment'
@@ -0,0 +1,161 @@
import { useEffect, useRef, useState } from 'react'
/*
// Here's an array with some 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) {
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,3 +24,4 @@ export * from './Showcase'
export * from './ShowcaseCard'
export * from './SocialCard'
export * from './TimelineItem'
export * from './JobsPerDepartment'