index.tsx 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557
  1. import { useEffect, useState } from "react"
  2. import { FormattedMessage, useIntl } from "react-intl"
  3. import Image from "next/legacy/image"
  4. import Head from "next/head"
  5. import classnames from "classnames"
  6. import { useKeenSlider } from "keen-slider/react"
  7. import "keen-slider/keen-slider.min.css"
  8. import resolveConfig from "tailwindcss/resolveConfig"
  9. import twConfig from "../tailwind.config"
  10. import { withDefaultStaticProps } from "../utils/defaultStaticProps"
  11. import LinkButton from "../components/LinkButton"
  12. import TestimonialCard from "../components/TestimonialCard"
  13. import SponsorLogoGroup from "../components/SponsorLogoGroup"
  14. import { IconCard } from "../components/IconCard"
  15. import testimonials from "../data/testimonials"
  16. import { platinum, additionalFunding } from "../data/sponsors"
  17. import illoTimeline from "../public/illustrations/features_timeline.png"
  18. import illoAudience from "../public/illustrations/features_audience.png"
  19. import illoModeration from "../public/illustrations/features_moderation.png"
  20. import illoCustomization from "../public/illustrations/features_customization.png"
  21. import illoWorld from "../public/illustrations/home_sponsors_world.png"
  22. import homeHeroMobile from "../public/illustrations/home_hero_mobile.webp"
  23. import homeHeroDesktop from "../public/illustrations/home_hero_desktop.png"
  24. import Hero from "../components/Hero"
  25. import { getDirForLocale } from "../utils/locales"
  26. import { useRouter } from "next/router"
  27. import Layout from "../components/Layout"
  28. function Home() {
  29. const intl = useIntl()
  30. return (
  31. <Layout>
  32. <Hero
  33. mobileImage={homeHeroMobile}
  34. desktopImage={homeHeroDesktop}
  35. homepage
  36. >
  37. <h1 className="h1 mb-4 max-w-[17ch] md:mb-7">
  38. <FormattedMessage
  39. id="home.hero.headline"
  40. defaultMessage="Social networking that's not for sale."
  41. />
  42. </h1>
  43. <p className="sh1 mb-11 max-w-[50ch]">
  44. <FormattedMessage
  45. id="home.hero.body"
  46. defaultMessage="Your home feed should be filled with what matters to you most, not what a corporation thinks you should see. Radically different social media, back in the hands of the people."
  47. />
  48. </p>
  49. <div className="flex flex-wrap justify-center gap-4 md:gap-12">
  50. <LinkButton size="large" href="https://mastodon.social/auth/sign_up">
  51. <FormattedMessage
  52. id="home.join_now"
  53. defaultMessage="Join {domain}"
  54. values={{ domain: "mastodon.social" }}
  55. />
  56. </LinkButton>
  57. <LinkButton size="large" href="/servers" light borderless>
  58. <FormattedMessage
  59. id="home.pick_another_server"
  60. defaultMessage="Pick another server"
  61. />
  62. </LinkButton>
  63. </div>
  64. </Hero>
  65. <Features />
  66. <WhyMastodon />
  67. <Testimonials testimonials={testimonials} />
  68. <Sponsors sponsors={{ platinum, additionalFunding }} />
  69. <Head>
  70. <title>
  71. {`Mastodon - ${intl.formatMessage({
  72. id: "home.page_title",
  73. defaultMessage: "Decentralized social media",
  74. })}`}
  75. </title>
  76. <meta
  77. property="og:title"
  78. content={`Mastodon - ${intl.formatMessage({
  79. id: "home.page_title",
  80. defaultMessage: "Decentralized social media",
  81. })}`}
  82. />
  83. <meta
  84. property="og:description"
  85. content={intl.formatMessage({
  86. id: "home.page_description",
  87. defaultMessage:
  88. "Learn more about Mastodon, the radically different, free and open-source decentralized social media platform.",
  89. })}
  90. />
  91. <meta
  92. property="description"
  93. content={intl.formatMessage({
  94. id: "home.page_description",
  95. defaultMessage:
  96. "Learn more about Mastodon, the radically different, free and open-source decentralized social media platform.",
  97. })}
  98. />
  99. <link rel="me" href="https://mastodon.social/@MastodonEngineering" />
  100. <link rel="me" href="https://mastodon.social/@ServerHighlights" />
  101. </Head>
  102. </Layout>
  103. )
  104. }
  105. export default Home
  106. const Features = () => {
  107. return (
  108. <section>
  109. {[
  110. {
  111. title: (
  112. <FormattedMessage
  113. id="home.features.timeline.title"
  114. defaultMessage="Stay in control of your own timeline"
  115. />
  116. ),
  117. body: (
  118. <FormattedMessage
  119. id="home.features.timeline.body"
  120. defaultMessage="You know best what you want to see on your home feed. No algorithms or ads to waste your time. Follow anyone across any Mastodon server from a single account and receive their posts in chronological order, and make your corner of the internet a little more like you."
  121. />
  122. ),
  123. button: (
  124. <LinkButton
  125. size="large"
  126. href="https://docs.joinmastodon.org/user/moderating/"
  127. >
  128. <FormattedMessage
  129. id="home.features.button.learn_more"
  130. defaultMessage="Learn more"
  131. />
  132. </LinkButton>
  133. ),
  134. image: illoTimeline,
  135. },
  136. {
  137. title: (
  138. <FormattedMessage
  139. id="home.features.audience.title"
  140. defaultMessage="Build your audience in confidence"
  141. />
  142. ),
  143. body: (
  144. <FormattedMessage
  145. id="home.features.audience.body"
  146. defaultMessage="Mastodon provides you with a unique possibility of managing your audience without middlemen. Mastodon deployed on your own infrastructure allows you to follow and be followed from any other Mastodon server online and is under no one's control but yours."
  147. />
  148. ),
  149. button: (
  150. <LinkButton
  151. size="large"
  152. href="https://docs.joinmastodon.org/user/run-your-own/"
  153. >
  154. <FormattedMessage
  155. id="home.features.button.learn_more"
  156. defaultMessage="Learn more"
  157. />
  158. </LinkButton>
  159. ),
  160. image: illoAudience,
  161. },
  162. {
  163. title: (
  164. <FormattedMessage
  165. id="home.features.moderation.title"
  166. defaultMessage="Moderating the way it should be"
  167. />
  168. ),
  169. body: (
  170. <FormattedMessage
  171. id="home.features.moderation.body"
  172. defaultMessage="Mastodon puts decision making back in your hands. Each server creates their own rules and regulations, which are enforced locally and not top-down like corporate social media, making it the most flexible in responding to the needs of different groups of people. Join a server with the rules you agree with, or host your own."
  173. />
  174. ),
  175. button: (
  176. <LinkButton size="large" href="/servers">
  177. <FormattedMessage
  178. id="home.features.button.find_a_server"
  179. defaultMessage="Find a server"
  180. />
  181. </LinkButton>
  182. ),
  183. image: illoModeration,
  184. },
  185. {
  186. title: (
  187. <FormattedMessage
  188. id="home.features.self_expression.title"
  189. defaultMessage="Unparalleled creativity"
  190. />
  191. ),
  192. body: (
  193. <FormattedMessage
  194. id="home.features.self_expression.body"
  195. defaultMessage="Mastodon supports audio, video and picture posts, accessibility descriptions, polls, content warnings, animated avatars, custom emojis, thumbnail crop control, and more, to help you express yourself online. Whether you're publishing your art, your music, or your podcast, Mastodon is there for you."
  196. />
  197. ),
  198. button: (
  199. <LinkButton
  200. size="large"
  201. href="https://docs.joinmastodon.org/user/posting/"
  202. >
  203. <FormattedMessage
  204. id="home.features.button.learn_more"
  205. defaultMessage="Learn more"
  206. />
  207. </LinkButton>
  208. ),
  209. image: illoCustomization,
  210. },
  211. ].map((block, i) => {
  212. const isOdd = i % 2 != 0
  213. return (
  214. <div
  215. className={classnames("full-width-bg", isOdd && "bg-gray-5")}
  216. key={i}
  217. >
  218. <div className="full-width-bg__inner pt-14 pb-[4.5rem] md:grid md:grid-cols-2 md:items-center md:gap-gutter xl:grid-cols-12">
  219. <div
  220. className={classnames(
  221. "row-span-full xl:col-span-5",
  222. isOdd ? "xl:col-start-2" : "order-2 xl:col-start-8"
  223. )}
  224. >
  225. <Image src={block.image} alt="" />
  226. </div>
  227. <div
  228. className={classnames(
  229. "row-span-full xl:col-span-5",
  230. isOdd ? "xl:col-start-8" : "xl:col-start-2"
  231. )}
  232. >
  233. <h2 className="h4 md:h2 mb-2 md:mb-5">{block.title}</h2>
  234. <p className="sh1 mb-8 text-gray-1">{block.body}</p>
  235. {block.button}
  236. </div>
  237. </div>
  238. </div>
  239. )
  240. })}
  241. </section>
  242. )
  243. }
  244. const WhyMastodon = () => {
  245. const [currentSlide, setCurrentSlide] = useState(0)
  246. const [loaded, setLoaded] = useState(false)
  247. const fullConfig = resolveConfig(twConfig)
  248. const options = {
  249. slideChanged(slider) {
  250. setCurrentSlide(slider.track.details.rel)
  251. },
  252. created() {
  253. setLoaded(true)
  254. },
  255. slides: {
  256. perView: 1,
  257. spacing: 16,
  258. },
  259. breakpoints: {
  260. [`(min-width: ${fullConfig.theme.screens["md"]})`]: {
  261. disabled: true,
  262. },
  263. },
  264. }
  265. const [sliderRef, instanceRef] = useKeenSlider(options)
  266. const intl = useIntl()
  267. return (
  268. <section className="py-20">
  269. <h3 className="h3 pb-16 text-center">
  270. <FormattedMessage id="home.why.title" defaultMessage="Why Mastodon?" />
  271. </h3>
  272. <div dir="ltr">
  273. <div
  274. ref={sliderRef}
  275. className="keen-slider mb-8 md:mb-0 md:grid md:grid-cols-2 md:gap-gutter lg:grid-cols-4"
  276. >
  277. <IconCard
  278. className="keen-slider__slide shadow-none md:border md:border-gray-3"
  279. title={
  280. <FormattedMessage
  281. id="home.why.decentralized.title"
  282. defaultMessage="Decentralized"
  283. />
  284. }
  285. icon="decentralized"
  286. copy={
  287. <FormattedMessage
  288. id="home.why.decentralized.copy"
  289. defaultMessage="Instant global communication is too important to belong to one company. Each Mastodon server is a completely independent entity, able to interoperate with others to form one global social network."
  290. />
  291. }
  292. />
  293. <IconCard
  294. className="keen-slider__slide shadow-none md:border md:border-gray-3"
  295. title={
  296. <FormattedMessage
  297. id="home.why.opensource.title"
  298. defaultMessage="Open Source"
  299. />
  300. }
  301. icon="open-source"
  302. copy={
  303. <FormattedMessage
  304. id="home.why.opensource.copy"
  305. defaultMessage="Mastodon is free and open-source software. We believe in your right to use, copy, study and change Mastodon as you see fit, and we benefit from contributions from the community."
  306. />
  307. }
  308. />
  309. <IconCard
  310. className="keen-slider__slide shadow-none md:border md:border-gray-3"
  311. title={
  312. <FormattedMessage
  313. id="home.why.not_for_sale.title"
  314. defaultMessage="Not for Sale"
  315. />
  316. }
  317. icon="price-tag"
  318. copy={
  319. <FormattedMessage
  320. id="home.why.not_for_sale.copy"
  321. defaultMessage="We respect your agency. Your feed is curated and created by you. We will never serve ads or push profiles for you to see. That means your data and your time are yours and yours alone."
  322. />
  323. }
  324. />
  325. <IconCard
  326. className="keen-slider__slide shadow-none md:border md:border-gray-3"
  327. title={
  328. <FormattedMessage
  329. id="home.why.interoperability.title"
  330. defaultMessage="Interoperable"
  331. />
  332. }
  333. icon="move"
  334. copy={
  335. <FormattedMessage
  336. id="home.why.interoperability.copy"
  337. defaultMessage="Built on open web protocols, Mastodon can speak with any other platform that implements ActivityPub. With one account you get access to a whole universe of social apps—the fediverse."
  338. />
  339. }
  340. />
  341. </div>
  342. {loaded && instanceRef.current && (
  343. <div className="flex items-center justify-center gap-2 md:hidden">
  344. {instanceRef.current.slides.map((_, idx) => {
  345. return (
  346. <button
  347. key={idx}
  348. onClick={() => {
  349. instanceRef.current?.moveToIdx(idx)
  350. }}
  351. className={
  352. "rounded-[50%] p-1.5 " +
  353. (currentSlide === idx ? "bg-blurple-500" : "bg-gray-3")
  354. }
  355. aria-label={intl.formatMessage({
  356. id: "home.slider.go_to_slide",
  357. defaultMessage: "Go to slide",
  358. })}
  359. ></button>
  360. )
  361. })}
  362. </div>
  363. )}
  364. </div>
  365. </section>
  366. )
  367. }
  368. const Testimonials = ({ testimonials }) => {
  369. const [currentSlide, setCurrentSlide] = useState(0)
  370. const [loaded, setLoaded] = useState(false)
  371. const fullConfig = resolveConfig(twConfig)
  372. const options = {
  373. loop: true,
  374. slideChanged(slider) {
  375. setCurrentSlide(slider.track.details.rel)
  376. },
  377. created() {
  378. setLoaded(true)
  379. },
  380. slides: {
  381. perView: 1,
  382. spacing: 16,
  383. },
  384. breakpoints: {
  385. [`(min-width: ${fullConfig.theme.screens["md"]})`]: {
  386. slides: { perView: 2, spacing: 16 },
  387. },
  388. [`(min-width: ${fullConfig.theme.screens["lg"]})`]: {
  389. slides: { perView: 3, spacing: 16 },
  390. },
  391. },
  392. }
  393. const [sliderRef, instanceRef] = useKeenSlider(options)
  394. const intl = useIntl()
  395. return (
  396. <section className="full-width-bg bg-gray-5 pt-20 pb-28">
  397. <div className="full-width-bg__inner">
  398. <h2 className="h3 pb-16 text-center">
  399. <FormattedMessage
  400. id="home.testimonials.title"
  401. defaultMessage="What our users are saying"
  402. />
  403. </h2>
  404. <div dir="ltr">
  405. <div ref={sliderRef} className="keen-slider mb-8">
  406. {testimonials.map((testimonial) => {
  407. return (
  408. <TestimonialCard
  409. key={testimonial.name}
  410. testimonial={testimonial}
  411. />
  412. )
  413. })}
  414. </div>
  415. {loaded && instanceRef.current && (
  416. <div className="flex items-center justify-center gap-2">
  417. {testimonials.map((_, idx) => {
  418. return (
  419. <button
  420. key={idx}
  421. onClick={() => {
  422. instanceRef.current?.moveToIdx(idx)
  423. }}
  424. className={
  425. "rounded-[50%] p-1.5 " +
  426. (currentSlide === idx ? "bg-blurple-500" : "bg-gray-3")
  427. }
  428. aria-label={intl.formatMessage({
  429. id: "home.slider.go_to_slide",
  430. defaultMessage: "Go to slide",
  431. })}
  432. ></button>
  433. )
  434. })}
  435. </div>
  436. )}
  437. </div>
  438. </div>
  439. </section>
  440. )
  441. }
  442. const Sponsors = ({ sponsors }) => {
  443. return (
  444. <section className="grid gap-x-gutter text-center lg:grid-cols-12">
  445. <div className="py-20 lg:col-span-12 lg:grid lg:grid-cols-12 lg:gap-x-gutter lg:py-28">
  446. <div className="mx-auto mb-12 max-w-lg lg:col-span-4 lg:col-start-5 lg:mb-10 lg:w-full">
  447. <Image
  448. src={illoWorld}
  449. alt="Illustration of elephant characters on a globe."
  450. />
  451. </div>
  452. <div className=" lg:col-span-8 lg:col-start-3">
  453. <h2 className="h2 mb-6">
  454. <FormattedMessage
  455. id="home.sponsors.title"
  456. defaultMessage="Independent always"
  457. />
  458. </h2>
  459. <p className="b1 lg:sh1 mb-12 lg:mb-10">
  460. <FormattedMessage
  461. id="home.sponsors.body"
  462. defaultMessage="Mastodon is free and open-source software developed by a non-profit organization. Public support directly sustains development and evolution."
  463. />
  464. </p>
  465. <div className="flex flex-col items-center justify-center gap-6 sm:flex-row">
  466. <LinkButton href="https://patreon.com/mastodon" size="large">
  467. <FormattedMessage
  468. id="sponsors.donate_on_patreon"
  469. defaultMessage="Donate on Patreon"
  470. />
  471. </LinkButton>
  472. <LinkButton
  473. href="https://donate.stripe.com/00g5l42h8ezY3YcaEE"
  474. size="large"
  475. >
  476. <FormattedMessage
  477. id="sponsors.donate_directly"
  478. defaultMessage="Donate directly"
  479. />
  480. </LinkButton>
  481. <LinkButton href="/sponsors" light size="large">
  482. <FormattedMessage
  483. id="sponsors.learn_more"
  484. defaultMessage="Learn more"
  485. />
  486. </LinkButton>
  487. </div>
  488. </div>
  489. </div>
  490. <h3 className="h5 mb-8 text-center lg:col-span-12">
  491. <FormattedMessage
  492. id="sponsors.supported_by"
  493. defaultMessage="Supported by"
  494. />
  495. </h3>
  496. <div className="lg:col-start-2 lg:col-end-12">
  497. <SponsorLogoGroup sponsors={sponsors.platinum} />
  498. </div>
  499. {sponsors.additionalFunding.length > 0 && (
  500. <>
  501. <h4 className="h5 mb-8 pt-20 text-center lg:col-span-12">
  502. <FormattedMessage
  503. id="home.additional_support_from"
  504. defaultMessage="Additional support from"
  505. />
  506. </h4>
  507. <div className="lg:col-start-4 lg:col-end-10 lg:mb-16">
  508. <SponsorLogoGroup sponsors={sponsors.additionalFunding} />
  509. </div>
  510. </>
  511. )}
  512. <p className="mt-8 text-gray-2 lg:col-span-12 lg:mb-16">
  513. <FormattedMessage
  514. id="sponsors.sponsorship.statement"
  515. defaultMessage="Sponsorship does not equal influence. Mastodon is fully independent."
  516. />
  517. </p>
  518. </section>
  519. )
  520. }
  521. export const getStaticProps = withDefaultStaticProps()