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