Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
mastodon
GitHub Repository: mastodon/joinmastodon
Path: blob/main/pages/careers.tsx
1006 views
1
import { useQuery } from "@tanstack/react-query"
2
import Head from "next/head"
3
import Hero from "../components/Hero"
4
import { withDefaultStaticProps } from "../utils/defaultStaticProps"
5
import Layout from "../components/Layout"
6
import LinkButton from "../components/LinkButton"
7
import Link from "next/link"
8
import { groupBy as _groupBy } from "lodash"
9
import Arrow from "../public/ui/arrow-right.svg?inline"
10
import type { JobsResponse, Job } from "../types/api"
11
import { fetchEndpoint } from "../utils/api"
12
import SkeletonText from "../components/SkeletonText"
13
import LinkWithArrow from "../components/LinkWithArrow"
14
import PressArticle from "../components/PressArticle"
15
import press from "../data/press"
16
17
/** This page does not require translations */
18
const Careers = () => {
19
const jobsResponse = useQuery({
20
queryKey: ["jobs"],
21
queryFn: () => fetchEndpoint<JobsResponse>("jobs"),
22
23
// 10 minutes
24
gcTime: 10 * 60 * 1000,
25
26
select: (data) => _groupBy(data.results, "departmentName"),
27
})
28
29
return (
30
<Layout>
31
<div dir="ltr" className="[unicode-bidi:plaintext]">
32
<Hero homepage>
33
<div className="b2 pt-6 !font-bold uppercase text-nightshade-100">
34
Careers
35
</div>
36
37
<h1 className="h1 mb-4">Join our team</h1>
38
39
<p className="sh1 mb-11 max-w-[50ch]">
40
We&apos;re building open source, decentralized social media that
41
gives people back control over their data and their reach.
42
</p>
43
44
<div className="flex justify-center">
45
<LinkButton size="large" href="#open-positions">
46
See open positions
47
</LinkButton>
48
</div>
49
</Hero>
50
51
<div className="full-width-bg">
52
<div className="full-width-bg__inner">
53
<div className="grid grid-cols-12 gap-y-24 py-20 md:gap-x-12">
54
<div className="col-span-12 md:col-span-4">
55
<h2 className="h3 mb-4">Work with us</h2>
56
<p className="b1 mb-4">
57
Mastodon is a non-profit with a remote-only, primarily
58
English-speaking team distributed across the world.
59
</p>
60
<p className="b1 mb-6">
61
<LinkWithArrow href="/about#team">
62
Meet the team
63
</LinkWithArrow>
64
</p>
65
66
<ul className="b1 list-disc space-y-2">
67
<li>
68
Be part of a small team working on the generational
69
opportunity of the future of social media.
70
</li>
71
<li>
72
We can offer work contracts through a payroll provider such
73
as Remote.com or directly if you&apos;re based in Germany.
74
</li>
75
</ul>
76
</div>
77
78
<div className="col-span-12 md:col-span-8">
79
<h2 id="open-positions" className="h3 mb-6">
80
Open positions
81
</h2>
82
83
<JobBoard jobs={jobsResponse} />
84
</div>
85
86
<div className="col-span-12">
87
<h2 className="h3 mb-4">In the news</h2>
88
<p className="b1 mb-8">
89
<LinkWithArrow href="/about#press">Read more</LinkWithArrow>
90
</p>
91
92
<div className="grid grid-cols-12 gap-gutter">
93
{press
94
.sort((a, b) => a.date.localeCompare(b.date) * -1)
95
.slice(0, 4)
96
.map((story) => (
97
<PressArticle key={story.url} story={story} />
98
))}
99
</div>
100
</div>
101
</div>
102
</div>
103
</div>
104
105
<Head>
106
<title>Careers - Mastodon</title>
107
<meta property="og:title" content="Careers at Mastodon" />
108
<meta property="og:description" content="Join our team." />
109
<meta property="description" content="Join our team." />
110
</Head>
111
</div>
112
</Layout>
113
)
114
}
115
116
const Job = ({ job }: { job?: Job }) => (
117
<li className="flex border-b border-gray-4 py-4 last:border-0">
118
<div className="b1 flex-1 !font-bold">
119
{job ? job.title : <SkeletonText className="w-[14ch]" />}
120
</div>
121
122
<div className="b2 flex-shrink-0">
123
{job ? (
124
<Link
125
href={job.externalLink}
126
className="flex items-center gap-2 font-semibold text-blurple-600 hocus:underline"
127
>
128
{job.locationName}
129
<Arrow className="h-[1em]" />
130
</Link>
131
) : (
132
<SkeletonText className="w-[7ch]" />
133
)}
134
</div>
135
</li>
136
)
137
138
const JobBoard = ({ jobs }) => {
139
if (jobs.error) {
140
return (
141
<div className="b2 flex justify-center rounded bg-gray-5 p-4 text-gray-1 md:p-8 md:py-20">
142
<p className="max-w-[48ch] text-center">
143
Failed to retrieve positions, please try again later.
144
</p>
145
</div>
146
)
147
} else if (!jobs.data || Object.keys(jobs.data).length === 0) {
148
return (
149
<div className="b2 flex justify-center rounded bg-gray-5 p-4 text-gray-1 md:p-8 md:py-20">
150
<p className="max-w-[48ch] text-center">
151
No positions available right now.
152
</p>
153
</div>
154
)
155
}
156
157
return (
158
<div>
159
{jobs.isLoading
160
? Array(4)
161
.fill(null)
162
.map((_, i) => <Job key={i} />)
163
: Object.keys(jobs.data).map((departmentName) => (
164
<div
165
key={departmentName}
166
className="mb-6 border-b border-gray-4 pb-6 last:border-0"
167
>
168
<h3 className="b2 text-nightshade-300">{departmentName}</h3>
169
170
{jobs.data[departmentName].map((job) => (
171
<Job key={job.id} job={job} />
172
))}
173
</div>
174
))}
175
</div>
176
)
177
}
178
179
export const getStaticProps = withDefaultStaticProps()
180
181
export default Careers
182
183