careers.tsx 5.8 KB

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