Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
sagemathinc
GitHub Repository: sagemathinc/cocalc
Path: blob/master/src/packages/next/components/path/path.tsx
1448 views
1
/*
2
* This file is part of CoCalc: Copyright © 2020 Sagemath, Inc.
3
* License: MS-RSL – see LICENSE.md for details
4
*/
5
6
import {
7
Alert,
8
Avatar as AntdAvatar,
9
Button,
10
Divider,
11
Flex,
12
Space,
13
Tooltip,
14
QRCode,
15
} from "antd";
16
import Link from "next/link";
17
import { useRouter } from "next/router";
18
import { join } from "path";
19
import { useEffect, useState } from "react";
20
import basePath from "lib/base-path";
21
import { Icon } from "@cocalc/frontend/components/icon";
22
import {
23
SHARE_AUTHENTICATED_EXPLANATION,
24
SHARE_AUTHENTICATED_ICON,
25
} from "@cocalc/util/consts/ui";
26
import InPlaceSignInOrUp from "components/auth/in-place-sign-in-or-up";
27
import A from "components/misc/A";
28
import Badge from "components/misc/badge";
29
import SanitizedMarkdown from "components/misc/sanitized-markdown";
30
import { Layout } from "components/share/layout";
31
import License from "components/share/license";
32
import LinkedPath from "components/share/linked-path";
33
import Loading from "components/share/loading";
34
import PathActions from "components/share/path-actions";
35
import PathContents from "components/share/path-contents";
36
import ProjectLink from "components/share/project-link";
37
import Avatar from "components/share/proxy/avatar";
38
import apiPost from "lib/api/post";
39
import type { CustomizeType } from "lib/customize";
40
import useCounter from "lib/share/counter";
41
import { Customize } from "lib/share/customize";
42
import type { PathContents as PathContentsType } from "lib/share/get-contents";
43
import { getTitle } from "lib/share/util";
44
45
import { SocialMediaShareLinks } from "components/landing/social-media-share-links";
46
47
export interface PublicPathProps {
48
id: string;
49
path: string;
50
url: string;
51
project_id: string;
52
projectTitle?: string;
53
relativePath?: string;
54
description?: string;
55
counter?: number;
56
compute_image?: string;
57
license?: string;
58
contents?: PathContentsType;
59
error?: string;
60
customize: CustomizeType;
61
disabled?: boolean;
62
has_site_license?: boolean;
63
unlisted?: boolean;
64
authenticated?: boolean;
65
stars?: number;
66
isStarred?: boolean;
67
githubOrg?: string; // if given, this is being mirrored from this github org
68
githubRepo?: string; // if given, mirrored from this github repo.
69
projectAvatarImage?: string; // optional 320x320 image representing the project from which this was shared
70
// Do a redirect to here; this is due to names versus id and is needed when
71
// visiting this by following a link from within the share server that
72
// doesn't use the names. See https://github.com/sagemathinc/cocalc/issues/6115
73
redirect?: string;
74
jupyter_api: boolean;
75
created: string | null; // ISO 8601 string
76
last_edited: string | null; // ISO 8601 string
77
ogUrl?: string; // Open Graph URL for social media sharing
78
ogImage?: string; // Open Graph image for social media sharing
79
}
80
81
export default function PublicPath({
82
id,
83
path,
84
url,
85
project_id,
86
projectTitle,
87
relativePath = "",
88
description,
89
counter,
90
compute_image,
91
license,
92
contents,
93
error,
94
customize,
95
disabled,
96
has_site_license,
97
unlisted,
98
authenticated,
99
stars = 0,
100
isStarred: isStarred0,
101
githubOrg,
102
githubRepo,
103
projectAvatarImage,
104
redirect,
105
jupyter_api,
106
ogUrl,
107
}: PublicPathProps) {
108
useCounter(id);
109
const [numStars, setNumStars] = useState<number>(stars);
110
111
const [isStarred, setIsStarred] = useState<boolean | null | undefined>(
112
isStarred0 ?? null,
113
);
114
useEffect(() => {
115
setIsStarred(isStarred0);
116
}, [isStarred0]);
117
118
const [qrcode, setQrcode] = useState<string>("");
119
useEffect(() => {
120
setQrcode(location.href);
121
}, []);
122
123
const [signingUp, setSigningUp] = useState<boolean>(false);
124
const router = useRouter();
125
const [invalidRedirect, setInvalidRedirect] = useState<boolean>(false);
126
127
useEffect(() => {
128
if (redirect) {
129
// User can in theory pass in an arbitrary redirect, which could probably be dangerous (e.g., to an external
130
// spam/hack site!?). So we only automatically redirect to the SAME site we're on right now.
131
if (redirect) {
132
const site = siteName(redirect);
133
if (!site) {
134
// no site specified -- path relative to our own site
135
router.replace(redirect);
136
} else if (site == siteName(location.href)) {
137
// site specified and it is our own site.
138
router.replace(redirect);
139
} else {
140
// user can manually inspect url and click
141
setInvalidRedirect(true);
142
}
143
}
144
}
145
}, [redirect]);
146
147
if (id == null || (redirect && !invalidRedirect)) {
148
return (
149
<div style={{ margin: "30px", textAlign: "center" }}>
150
<Loading style={{ fontSize: "30px" }} />
151
</div>
152
);
153
}
154
155
function visibility_explanation() {
156
if (disabled) {
157
return (
158
<>
159
<Icon name="lock" /> private
160
</>
161
);
162
}
163
if (unlisted) {
164
return (
165
<>
166
<Icon name="eye-slash" /> unlisted
167
</>
168
);
169
}
170
if (authenticated) {
171
return (
172
<>
173
<Icon name={SHARE_AUTHENTICATED_ICON} /> authenticated (
174
{SHARE_AUTHENTICATED_EXPLANATION})
175
</>
176
);
177
}
178
}
179
180
function visibility() {
181
if (unlisted || disabled || authenticated) {
182
return <div>{visibility_explanation()}</div>;
183
}
184
}
185
186
async function star() {
187
setIsStarred(true);
188
setNumStars(numStars + 1);
189
// Actually do the api call after changing state, so it is
190
// maximally snappy. Also, being absolutely certain that star/unstar
191
// actually worked is not important.
192
await apiPost("/public-paths/star", { id });
193
}
194
195
async function unstar() {
196
setIsStarred(false);
197
setNumStars(numStars - 1);
198
await apiPost("/public-paths/unstar", { id });
199
}
200
201
function renderStar() {
202
const badge = (
203
<Badge
204
count={numStars}
205
style={{
206
marginLeft: "10px",
207
marginTop: "-2.5px",
208
}}
209
/>
210
);
211
if (isStarred == null) {
212
// not signed in ==> isStarred is null or undefined.
213
return (
214
<Button
215
onClick={() => {
216
setSigningUp(!signingUp);
217
}}
218
title={"Sign in to star"}
219
>
220
<Icon name="star" /> Star {badge}
221
</Button>
222
);
223
}
224
// Signed in so isStarred is true or false.
225
let btn;
226
if (isStarred == true) {
227
btn = (
228
<Button onClick={unstar}>
229
<Icon name="star-filled" style={{ color: "#eac54f" }} /> Starred{" "}
230
{badge}
231
</Button>
232
);
233
} else {
234
btn = (
235
<Button onClick={star}>
236
<Icon name="star" /> Star {badge}
237
</Button>
238
);
239
}
240
return (
241
<div>
242
<Space.Compact>
243
{btn}
244
<Button href={join(basePath, "stars")}>...</Button>
245
</Space.Compact>
246
</div>
247
);
248
}
249
250
function renderProjectLink() {
251
if (githubOrg && githubRepo) {
252
return (
253
<Tooltip
254
title="Go to the top level of the repository."
255
placement="right"
256
>
257
<b>
258
<Icon name="home" /> GitHub Repository:{" "}
259
</b>
260
<A href={`/github/${githubOrg}/${githubRepo}`}>
261
{githubOrg}/{githubRepo}
262
</A>
263
<br />
264
</Tooltip>
265
);
266
}
267
if (url) {
268
let name, target;
269
const i = url.indexOf("/");
270
if (url.startsWith("gist")) {
271
target = `https://gist.github.com/${url.slice(i + 1)}`;
272
name = "GitHub Gist";
273
} else {
274
target = "https://" + url.slice(i + 1);
275
name = "URL";
276
}
277
// NOTE: it could conceivable only be http:// display will work, but this
278
// link will be wrong. I'm not going to worry about that.
279
return (
280
<Tooltip
281
placement="right"
282
title={`This file is hosted at ${target}. Click to open in a new tab.`}
283
>
284
<b>
285
<Icon name="external-link" /> {name}:{" "}
286
</b>
287
<A href={target}>{target}</A>
288
<br />
289
</Tooltip>
290
);
291
}
292
return (
293
<div>
294
<ProjectLink project_id={project_id} title={projectTitle} />
295
<br />
296
</div>
297
);
298
}
299
300
function renderPathLink() {
301
if (githubRepo) {
302
const segments = url.split("/");
303
return (
304
<Tooltip
305
placement="right"
306
title="This is hosted on GitHub. Click to open GitHub in a new tab."
307
>
308
<b>
309
<Icon name="external-link" /> Path:{" "}
310
</b>
311
<A href={`https://github.com/${join(...segments.slice(1))}`}>
312
{segments.length > 3
313
? join(...segments.slice(3))
314
: join(...segments.slice(1))}
315
</A>
316
<br />
317
</Tooltip>
318
);
319
}
320
321
if (url) return;
322
323
return (
324
<div>
325
<LinkedPath
326
path={path}
327
relativePath={relativePath}
328
id={id}
329
isDir={contents?.isdir}
330
/>
331
<br />
332
</div>
333
);
334
}
335
336
return (
337
<Customize value={customize}>
338
<Layout
339
title={getTitle({ path, relativePath })}
340
top={
341
projectAvatarImage ? (
342
<AntdAvatar
343
shape="square"
344
size={160}
345
icon={
346
<img
347
src={projectAvatarImage}
348
alt={`Avatar for ${projectTitle}.`}
349
/>
350
}
351
style={{ float: "left", margin: "20px" }}
352
/>
353
) : undefined
354
}
355
>
356
{githubOrg && (
357
<Avatar
358
size={96}
359
name={githubOrg}
360
style={{ float: "right", marginLeft: "15px" }}
361
/>
362
)}
363
<div>
364
{invalidRedirect && (
365
<Alert
366
type="warning"
367
message={
368
<>
369
<Icon name="external-link" /> External Redirect
370
</>
371
}
372
description={
373
<div>
374
The author has configured a redirect to:{" "}
375
<div style={{ fontSize: "13pt", textAlign: "center" }}>
376
<A href={redirect}>{redirect}</A>
377
</div>
378
</div>
379
}
380
style={{ margin: "15px 0" }}
381
/>
382
)}
383
<PathActions
384
id={id}
385
path={path}
386
url={url}
387
relativePath={relativePath}
388
isDir={contents?.isdir}
389
exclude={new Set(["hosted"])}
390
project_id={project_id}
391
image={compute_image}
392
description={description}
393
has_site_license={has_site_license}
394
/>
395
<Space
396
style={{
397
marginTop: "-30px",
398
float: "right",
399
justifyContent: "flex-end",
400
}}
401
direction="vertical"
402
>
403
<Flex>
404
<div style={{ flex: 1 }} />
405
{qrcode && <QRCode value={qrcode} size={110} color="#5a687d" />}
406
</Flex>
407
<div style={{ float: "right" }}>{renderStar()}</div>
408
</Space>
409
{signingUp && (
410
<Alert
411
closable
412
onClick={() => setSigningUp(false)}
413
style={{ margin: "0 auto", maxWidth: "400px" }}
414
type="warning"
415
message={
416
<InPlaceSignInOrUp
417
title="Star Shared Files"
418
why="to star this"
419
onSuccess={() => {
420
star();
421
setSigningUp(false);
422
router.reload();
423
}}
424
/>
425
}
426
/>
427
)}
428
{description?.trim() && (
429
<SanitizedMarkdown
430
style={
431
{ marginBottom: "-1em" } /* -1em to undo it being a paragraph */
432
}
433
value={description}
434
/>
435
)}
436
437
{renderProjectLink()}
438
{renderPathLink()}
439
{counter && (
440
<>
441
<Badge count={counter} /> views
442
<br />
443
</>
444
)}
445
{license && (
446
<>
447
<b>License:</b> <License license={license} />
448
<br />
449
</>
450
)}
451
{visibility()}
452
{compute_image && (
453
<>
454
{compute_image}
455
<br />
456
</>
457
)}
458
</div>
459
{ogUrl && (
460
<SocialMediaShareLinks
461
title={getTitle({ path, relativePath })}
462
url={ogUrl}
463
showText
464
/>
465
)}
466
<Divider />
467
{error != null && (
468
<Alert
469
showIcon
470
type="error"
471
style={{ maxWidth: "700px", margin: "30px auto" }}
472
message="Error loading file"
473
description={
474
<div>
475
There was a problem loading{" "}
476
{relativePath ? relativePath : "this file"} in{" "}
477
<Link href={`/share/public_paths/${id}`}>{path}.</Link>
478
<br />
479
<br />
480
{error}
481
</div>
482
}
483
/>
484
)}
485
{contents != null && (
486
<PathContents
487
id={id}
488
relativePath={relativePath}
489
path={path}
490
jupyter_api={jupyter_api}
491
{...contents}
492
/>
493
)}
494
</Layout>
495
</Customize>
496
);
497
}
498
499
function siteName(url) {
500
const i = url.indexOf("://");
501
if (i == -1) {
502
return "";
503
}
504
const j = url.indexOf("/", i + 3);
505
if (j == -1) {
506
return url;
507
}
508
return url.slice(0, j);
509
}
510
511