CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutSign UpSign In
sagemathinc

Real-time collaboration for Jupyter Notebooks, Linux Terminals, LaTeX, VS Code, R IDE, and more,
all in one place. Commercial Alternative to JupyterHub.

GitHub Repository: sagemathinc/cocalc
Path: blob/master/src/packages/next/components/landing/info.tsx
Views: 923
1
/*
2
* This file is part of CoCalc: Copyright © 2021 Sagemath, Inc.
3
* License: MS-RSL – see LICENSE.md for details
4
*/
5
6
import { Col, Row, Space } from "antd";
7
import { CSSProperties, ReactNode } from "react";
8
9
import { Icon, IconName } from "@cocalc/frontend/components/icon";
10
import { capitalize } from "@cocalc/util/misc";
11
import { COLORS } from "@cocalc/util/theme";
12
import { TitleProps } from "antd/es/typography/Title";
13
import { CSS, Paragraph, Title } from "components/misc";
14
import A from "components/misc/A";
15
import { MAX_WIDTH_LANDING } from "lib/config";
16
import Image, { StaticImageData } from "./image";
17
import { MediaURL, SHADOW } from "./util";
18
19
interface Props {
20
alt?: string;
21
anchor: string;
22
below?: ReactNode;
23
belowWide?: boolean;
24
caption?: ReactNode;
25
children: ReactNode;
26
icon?: IconName;
27
image?: string | StaticImageData;
28
imageComponent?: ReactNode; // if set, this replaces the image!
29
level?: TitleProps["level"];
30
narrow?: boolean; // emphasis on the text part, not the image.
31
style?: CSSProperties;
32
swapCols?: boolean; // if true, then put text on left and image on right.
33
textStyle?: CSSProperties;
34
textStyleExtra?: CSSProperties;
35
title: ReactNode;
36
video?: string | string[];
37
wide?: boolean; // if given image is wide and could use more space or its very hard to see.
38
innerRef?;
39
icons?: { icon: IconName | JSX.Element; title?: string; link?: string }[];
40
}
41
42
export default function Info({
43
alt,
44
anchor,
45
below,
46
belowWide = false,
47
caption,
48
children,
49
icon,
50
image,
51
imageComponent,
52
level = 1,
53
narrow = false,
54
style,
55
swapCols,
56
textStyle,
57
textStyleExtra,
58
title,
59
video,
60
wide,
61
innerRef,
62
icons,
63
}: Props) {
64
function renderIcons() {
65
if (icons == null || icons.length == 0) {
66
return null;
67
}
68
return (
69
<div style={{ margin: "auto" }}>
70
<Space size={"large"}>
71
{icons.map(({ icon, title, link }, idx) => {
72
const elt = (
73
<div style={{ textAlign: "center", color: "#333" }}>
74
{typeof icon === "string" ? (
75
<Icon name={icon} style={{ fontSize: "28pt" }} key={icon} />
76
) : (
77
icon
78
)}
79
<br />
80
{title ?? capitalize(typeof icon === "string" ? icon : "")}
81
</div>
82
);
83
if (link) {
84
return (
85
<A key={idx} href={link}>
86
{elt}
87
</A>
88
);
89
}
90
return elt;
91
})}
92
</Space>
93
</div>
94
);
95
}
96
function renderBelow() {
97
if (!below) return;
98
99
if (belowWide) {
100
return (
101
<Col
102
lg={{ span: 20, offset: 2 }}
103
md={{ span: 22, offset: 1 }}
104
style={{ paddingTop: "30px" }}
105
>
106
{below}
107
</Col>
108
);
109
} else {
110
return (
111
<Col
112
lg={{ span: 16, offset: 4 }}
113
md={{ span: 18, offset: 3 }}
114
style={{ paddingTop: "30px" }}
115
>
116
{below}
117
</Col>
118
);
119
}
120
}
121
122
const head = (
123
<Title
124
level={level}
125
id={anchor}
126
style={{
127
textAlign: "center",
128
marginBottom: "30px",
129
color: COLORS.GRAY_D,
130
...textStyle,
131
}}
132
>
133
{icon && (
134
<span style={{ fontSize: "24pt", marginRight: "5px" }}>
135
<Icon name={icon} />{" "}
136
</span>
137
)}
138
{title}
139
</Title>
140
);
141
142
let graphic: ReactNode = null;
143
144
// common for "text" and "text + image" div wrappers
145
const padding: CSS = {
146
paddingTop: "45px",
147
paddingBottom: "45px",
148
paddingLeft: "15px",
149
paddingRight: "15px",
150
};
151
152
if (image != null) {
153
graphic = <Image shadow src={image} alt={alt ?? ""} />;
154
} else if (video != null) {
155
const videoSrcs = typeof video == "string" ? [video] : video;
156
verifyHasMp4(videoSrcs);
157
graphic = (
158
<div style={{ position: "relative", width: "100%" }}>
159
<video style={{ width: "100%", ...SHADOW }} loop controls>
160
{sources(videoSrcs)}
161
</video>
162
</div>
163
);
164
} else if (imageComponent != null) {
165
graphic = imageComponent;
166
}
167
168
if (graphic != null && caption != null) {
169
graphic = (
170
<div>
171
{graphic}
172
<br />
173
<br />
174
<Paragraph
175
style={{
176
textAlign: "center",
177
color: COLORS.GRAY_D,
178
}}
179
>
180
{caption}
181
</Paragraph>
182
</div>
183
);
184
}
185
186
if (!graphic) {
187
const noGraphicTextStyle: CSSProperties = {
188
...style,
189
};
190
191
if (textStyleExtra != null) {
192
// if textColStyleExtra is given, then merge it into noGraphicTextStyle.
193
Object.assign(noGraphicTextStyle, textStyleExtra);
194
}
195
196
const icons = renderIcons();
197
198
return (
199
<div
200
style={{
201
width: "100%",
202
...padding,
203
...style,
204
}}
205
>
206
<div style={{ maxWidth: MAX_WIDTH_LANDING, margin: "0 auto" }}>
207
<div style={noGraphicTextStyle}>
208
<div style={{ textAlign: "center" }}>{head}</div>
209
<div
210
style={{ margin: "auto", maxWidth: wide ? "600px" : undefined }}
211
>
212
{children}
213
</div>
214
{icons && (
215
<div style={{ marginTop: "20px", textAlign: "center" }}>
216
{icons}
217
</div>
218
)}
219
{below && <div style={{ marginTop: "20px" }}>{below}</div>}
220
</div>
221
</div>
222
</div>
223
);
224
}
225
226
const textColStyle: CSSProperties = {
227
padding: "0 20px 0 20px",
228
marginBottom: "15px",
229
display: "flex",
230
justifyContent: "start",
231
alignContent: "start",
232
flexDirection: "column",
233
};
234
235
if (textStyleExtra != null) {
236
// if textColStyleExtra is given, then merge it into textColStyle.
237
Object.assign(textColStyle, textStyleExtra);
238
}
239
240
const widths = wide ? [7, 17] : narrow ? [12, 12] : [9, 15];
241
242
const textCol = (
243
<Col key="text" lg={widths[0]} style={textColStyle}>
244
{children}
245
</Col>
246
);
247
const graphicCol = (
248
<Col
249
key="graphics"
250
lg={widths[1]}
251
style={{ padding: "0 30px 15px 30px", width: "100%" }}
252
>
253
{graphic}
254
</Col>
255
);
256
257
const cols = swapCols ? [textCol, graphicCol] : [graphicCol, textCol];
258
259
return (
260
<div
261
ref={innerRef}
262
style={{
263
...padding,
264
background: "white",
265
fontSize: "11pt",
266
...style,
267
}}
268
>
269
<>
270
{head}
271
<Row
272
style={{
273
maxWidth: MAX_WIDTH_LANDING,
274
marginLeft: "auto",
275
marginRight: "auto",
276
}}
277
>
278
{cols}
279
{renderBelow()}
280
{renderIcons()}
281
</Row>
282
</>
283
</div>
284
);
285
}
286
287
function sources(video: string[]) {
288
const v: JSX.Element[] = [];
289
for (const x of video) {
290
v.push(<source key={x} src={MediaURL(x)} />);
291
}
292
return v;
293
}
294
295
function verifyHasMp4(video: string[]) {
296
for (const x of video) {
297
if (x.endsWith(".mp4")) {
298
return;
299
}
300
}
301
console.warn(
302
"include mp4 format for the video, so that it is viewable on iOS!!",
303
video,
304
);
305
}
306
307
interface HeadingProps {
308
children: ReactNode;
309
description?: ReactNode;
310
style?: CSSProperties;
311
textStyle?: CSSProperties;
312
level?: TitleProps["level"];
313
}
314
315
Info.Heading = (props: HeadingProps) => {
316
const { level = 1, children, description, style, textStyle } = props;
317
return (
318
<div
319
style={{
320
...{
321
textAlign: "center",
322
margin: "0",
323
padding: "20px",
324
},
325
...style,
326
}}
327
>
328
<Title
329
level={level}
330
style={{
331
color: COLORS.GRAY_D,
332
maxWidth: MAX_WIDTH_LANDING,
333
margin: "0 auto",
334
...textStyle,
335
}}
336
>
337
{children}
338
</Title>
339
{description && (
340
<Paragraph
341
style={{
342
fontSize: "13pt",
343
color: COLORS.GRAY_D,
344
...textStyle,
345
}}
346
>
347
{description}
348
</Paragraph>
349
)}
350
</div>
351
);
352
};
353
354