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.

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