Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
mastodon
GitHub Repository: mastodon/joinmastodon
Path: blob/main/pages/hosting.tsx
1006 views
1
import {
2
defineMessages,
3
FormattedMessage,
4
MessageDescriptor,
5
useIntl,
6
} from "react-intl"
7
import Hero from "../components/Hero"
8
import Layout from "../components/Layout"
9
import { withDefaultStaticProps } from "../utils/defaultStaticProps"
10
import { PropsWithChildren } from "react"
11
import classNames from "classnames"
12
import Head from "next/head"
13
import {
14
partnerCards,
15
benefitsCards,
16
CardItem,
17
ImageOrSvg,
18
stepsCards,
19
SalesScheduleLink,
20
hostingLogos,
21
} from "../data/hosting"
22
import Image from "next/image"
23
24
const messages = defineMessages({
25
title: {
26
id: "hosting.pageTitle",
27
defaultMessage: "Managed Mastodon",
28
},
29
managedTitle: {
30
id: "hosting.services.managed.title",
31
defaultMessage: "All-in-One Managed Service",
32
},
33
managedSubtitle: {
34
id: "hosting.services.managed.subtitle",
35
defaultMessage: "Tailored for large organisations",
36
},
37
singleTitle: {
38
id: "hosting.services.single.title",
39
defaultMessage: "Single Service",
40
},
41
singleSubtitle: {
42
id: "hosting.services.single.subtitle",
43
defaultMessage: "Designed for organisations that want to stay in control",
44
},
45
partnersTitle: {
46
id: "hosting.partners.title",
47
defaultMessage:
48
"Leading organisations and institutions trust Mastodon to manage their services",
49
},
50
benefitsTitle: {
51
id: "hosting.benefits.title",
52
defaultMessage:
53
"Your domain. Your people. Your voices. For a resilient social network.",
54
},
55
})
56
57
const HostingPage = () => {
58
const intl = useIntl()
59
return (
60
<Layout>
61
<Hero>
62
<div className="flex gap-4 items-center text-b3 mb-4">
63
<div className="flex -space-x-3">
64
{hostingLogos.map(({ title, image }, index) => (
65
<span
66
key={index}
67
title={title}
68
className="relative flex size-8 shrink-0 overflow-hidden rounded-full bg-white border border-gray-0"
69
>
70
<ImageOrSVG data={image} className="w-full h-full" />
71
</span>
72
))}
73
</div>
74
<FormattedMessage
75
id="hosting.label"
76
defaultMessage="{num}+ organisations use Mastodon for public communication"
77
values={{ num: 100 }}
78
/>
79
</div>
80
<h1 className="h2 mb-4">
81
<FormattedMessage
82
id="hosting.title"
83
defaultMessage="Your fully-managed Mastodon service"
84
/>
85
</h1>
86
<p className="my-4">
87
<FormattedMessage
88
id="hosting.subtitle"
89
defaultMessage="Your own Mastodon server hosted by the team who built it"
90
/>
91
</p>
92
<SalesButton />
93
</Hero>
94
95
<div className="md:grid md:grid-cols-12 md:gap-gutter">
96
<div className="md:col-span-12 xl:col-span-10 xl:col-start-2">
97
<div className="flex flex-col md:grid md:grid-rows-[repeat(5,min-content)] gap-8 mb-16">
98
<ServicesCard
99
title={messages.managedTitle}
100
subtitle={messages.managedSubtitle}
101
>
102
<FormattedMessage
103
id="hosting.services.managed.description"
104
defaultMessage="We handle everything - from infrastructure, security, backups, updates, moderation, legal takedowns."
105
tagName="p"
106
/>
107
<FormattedMessage
108
id="hosting.services.managed.who"
109
defaultMessage="Perfect for organisations that want fast access to their own Mastodon server with guaranteed compliance and low resource investment."
110
tagName="p"
111
/>
112
<FormattedMessage
113
id="hosting.services.managed.details"
114
defaultMessage="Including Hosting, Moderation and Support"
115
tagName="p"
116
/>
117
</ServicesCard>
118
<ServicesCard
119
title={messages.singleTitle}
120
subtitle={messages.singleSubtitle}
121
>
122
<FormattedMessage
123
id="hosting.services.single.description"
124
defaultMessage="You stay in control. You can decide which services you want to run yourself, and where to get support from Mastodon."
125
tagName="p"
126
/>
127
<FormattedMessage
128
id="hosting.services.single.who"
129
defaultMessage="Ideal for organisations that already have internal resources or want to build internal capabilities."
130
tagName="p"
131
/>
132
<FormattedMessage
133
id="hosting.services.single.details"
134
defaultMessage="Choose one or multiple services from Hosting, Moderation or Support"
135
tagName="p"
136
/>
137
</ServicesCard>
138
</div>
139
<CardList title={messages.partnersTitle} items={partnerCards} />
140
141
<CardList title={messages.benefitsTitle} items={benefitsCards} />
142
143
<section className="my-8">
144
<h2 className="text-h5 font-bold mb-4">
145
<FormattedMessage
146
id="hosting.guide.title"
147
defaultMessage="How It Works"
148
/>
149
</h2>
150
<div className="flex flex-col gap-8">
151
{stepsCards.map(({ title, body }, index) => (
152
<div
153
className="p-8 rounded-xl relative bg-gray-5 shadow-lg grid grid-cols-[min-content_1fr] gap-4"
154
key={index}
155
>
156
<span className="flex items-center justify-center text-b1 text-white bg-blurple-900 rounded-full font-bold row-span-2 size-10">
157
{index + 1}
158
</span>
159
<h3 className="font-bold col-start-2 h6">
160
{typeof title === "string"
161
? title
162
: intl.formatMessage(title)}
163
</h3>
164
<p className="col-start-2">{intl.formatMessage(body)}</p>
165
</div>
166
))}
167
</div>
168
</section>
169
</div>
170
</div>
171
172
<Head>
173
<title>{intl.formatMessage(messages.title)}</title>
174
</Head>
175
</Layout>
176
)
177
}
178
179
export const getStaticProps = withDefaultStaticProps()
180
export default HostingPage
181
182
const SalesButton = ({ className }: { className?: string }) => (
183
<a
184
href={SalesScheduleLink}
185
className={classNames(
186
className,
187
"px-4 rounded-md text-white b3 text-center bg-blurple-500 transition-colors hocus:bg-blurple-600 h-12 inline-flex items-center"
188
)}
189
>
190
<FormattedMessage id="hosting.sales" defaultMessage="Talk to sales" />
191
</a>
192
)
193
194
const ServicesCard = ({
195
children,
196
title,
197
subtitle,
198
featured,
199
}: PropsWithChildren<{
200
title: MessageDescriptor
201
subtitle: MessageDescriptor
202
featured?: MessageDescriptor
203
}>) => {
204
const intl = useIntl()
205
return (
206
<section className="grid grid-rows-subgrid gap-8 row-span-full relative p-4 bg-nightshade-600 text-white rounded-lg">
207
<div className="flex flex-col justify-between">
208
<h2 className="h5 mb-4">{intl.formatMessage(title)}</h2>
209
<h3 className="italic text-b3">{intl.formatMessage(subtitle)}</h3>
210
</div>
211
{children}
212
<SalesButton className="justify-self-center" />
213
{featured && (
214
<span className="absolute -top-6 left-2 bg-nightshade-100 border-2 border-nightshade-300 p-2 rounded-xl italic text-b4 text-nightshade-600 font-semibold">
215
{intl.formatMessage(featured)}
216
</span>
217
)}
218
</section>
219
)
220
}
221
222
type CardListProps = {
223
title: MessageDescriptor
224
items: CardItem[]
225
}
226
const CardList = ({ title, items }: CardListProps) => {
227
const intl = useIntl()
228
return (
229
<section className="my-16 flex flex-col gap-2">
230
<h2 className="text-h5 font-bold mb-4">{intl.formatMessage(title)}</h2>
231
<div className="flex flex-col md:grid md:grid-cols-3 gap-8">
232
{items.map(({ title, body, image, icon: Icon }, index) => (
233
<div className="bg-gray-5 shadow-lg p-8 rounded-xl" key={index}>
234
{image && (
235
<figure className="aspect-square flex flex-col justify-center items-center p-4 lg:p-8 mb-4">
236
<ImageOrSVG data={image} className="block" />
237
</figure>
238
)}
239
<h4 className="h6 mb-2 flex gap-4 items-center">
240
{Icon && (
241
<Icon className="block w-8 flex-shrink-0 text-blurple-900 fill-current" />
242
)}
243
{typeof title === "string" ? title : intl.formatMessage(title)}
244
</h4>
245
<p>{intl.formatMessage(body)}</p>
246
</div>
247
))}
248
</div>
249
</section>
250
)
251
}
252
253
const ImageOrSVG = ({
254
className,
255
data,
256
}: {
257
data: ImageOrSvg
258
className?: string
259
}) => {
260
if (typeof data === "function") {
261
const SVGComponent = data
262
return <SVGComponent className={className} />
263
}
264
return (
265
<Image
266
src={data.src}
267
alt=""
268
className={className}
269
role="presentation"
270
width={data.width}
271
height={data.height}
272
/>
273
)
274
}
275
276