mirror of
https://github.com/wavetermdev/docusaurus-og.git
synced 2026-08-05 13:46:35 -07:00
Merge pull request #125 from acid-info/create-job-lists
Implement JobsPerDepartment component
This commit is contained in:
@@ -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 />
|
||||
|
||||
+52
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
+79
@@ -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>
|
||||
)
|
||||
}
|
||||
+50
@@ -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'
|
||||
+555
File diff suppressed because it is too large
Load Diff
+161
@@ -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'
|
||||
|
||||
Reference in New Issue
Block a user