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/sub-nav.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 { Divider } from "antd";
7
import { isEmpty } from "lodash";
8
import { useEffect, useRef, useState } from "react";
9
10
import { Icon } from "@cocalc/frontend/components/icon";
11
import { r_join } from "@cocalc/frontend/components/r_join";
12
import {
13
SOFTWARE_ENV_DEFAULT,
14
SOFTWARE_ENV_NAMES,
15
SoftwareEnvNames,
16
} from "@cocalc/util/consts/software-envs";
17
import { COLORS } from "@cocalc/util/theme";
18
import Logo from "components/logo";
19
import { CSS } from "components/misc";
20
import A from "components/misc/A";
21
import { MAX_WIDTH_LANDING } from "lib/config";
22
import { CustomizeType, useCustomize } from "lib/customize";
23
24
const BASE_STYLE: CSS = {
25
backgroundColor: "white",
26
textAlign: "center",
27
paddingLeft: "45px",
28
paddingRight: "45px",
29
paddingTop: "10px",
30
paddingBottom: "10px",
31
width: "100%",
32
zIndex: 1,
33
lineHeight: "2rem", // important to increase line height for narrow screens, otherwise text+underline is rendered on top of each other
34
maxHeight: "5rem",
35
overflow: "hidden",
36
};
37
38
const FLOAT_STYLE: CSS = {
39
...BASE_STYLE,
40
position: "fixed",
41
paddingBottom: "5px",
42
paddingRight: 0,
43
paddingLeft: 0,
44
top: "0",
45
boxShadow: "0 4px 6px 0 rgba(0.1,0.1,0.1,0.20)",
46
} as const;
47
48
const INNER_STYLE: CSS = {
49
maxWidth: MAX_WIDTH_LANDING,
50
margin: "0 auto",
51
overflow: "auto",
52
whiteSpace: "nowrap",
53
};
54
55
const about = {
56
index: {},
57
events: { label: "Events" },
58
team: { label: "Team" },
59
} as const;
60
61
const software = {
62
index: {},
63
executables: { label: "Executables" },
64
python: { label: "Python" },
65
r: { label: "R Stats" },
66
julia: { label: "Julia" },
67
octave: { label: "Octave" },
68
sagemath: { label: "SageMath" },
69
} as const;
70
71
const features = {
72
index: {},
73
"jupyter-notebook": { label: "Jupyter" },
74
julia: { label: "Julia" },
75
"latex-editor": { label: "LaTeX" },
76
linux: { label: "Linux" },
77
octave: { label: "Octave" },
78
python: { label: "Python" },
79
"r-statistical-software": { label: "R Stats" },
80
sage: { label: "SageMath" },
81
slides: { label: "Slides" },
82
teaching: { label: "Teaching" },
83
terminal: { label: "Terminal" },
84
whiteboard: { label: "Whiteboard" },
85
x11: { label: "X11" },
86
div1: { type: "divider" },
87
"compute-server": { label: "Compute" },
88
ai: { label: "AI Assistant" },
89
compare: { label: "Compare" },
90
api: { label: "API" },
91
} as const;
92
93
const pricing = {
94
index: {},
95
products: { label: "Products" },
96
subscriptions: { label: "Subscriptions" },
97
courses: { label: "Courses" },
98
institutions: { label: "Institutions" },
99
onprem: { label: "OnPrem" },
100
dedicated: { label: "Dedicated" },
101
} as const;
102
103
export const POLICIES = {
104
index: {},
105
terms: { label: "Terms of Service", hide: (c) => !c.onCoCalcCom },
106
copyright: { label: "Copyright", hide: (c) => !c.onCoCalcCom },
107
privacy: { label: "Privacy", hide: (c) => !c.onCoCalcCom },
108
trust: { label: "Trust", hide: (c) => !c.onCoCalcCom },
109
thirdparties: { label: "Third Parties", hide: (c) => !c.onCoCalcCom },
110
ferpa: { label: "FERPA", hide: (c) => !c.onCoCalcCom },
111
accessibility: { label: "Accessibility", hide: (c) => !c.onCoCalcCom },
112
imprint: { label: "Imprint", hide: (c) => !c.imprint },
113
policies: { label: "Policies", hide: (c) => !c.policies },
114
} as const;
115
116
const info = {
117
index: {},
118
doc: { label: "Documentation" },
119
status: { label: "Status" },
120
run: { label: "Run CoCalc" },
121
} as const;
122
123
const support = {
124
index: {},
125
community: { label: "Community" },
126
new: { label: "New Ticket", hide: (customize) => !customize.zendesk },
127
tickets: { label: "Tickets", hide: (customize) => !customize.zendesk },
128
chatgpt: {
129
label: "AI",
130
hide: (customize) => !customize.openaiEnabled || !customize.onCoCalcCom,
131
},
132
} as const;
133
134
type PageKey =
135
| "about"
136
| "features"
137
| "software"
138
| "pricing"
139
| "policies"
140
| "share"
141
| "info"
142
| "sign-up"
143
| "sign-in"
144
| "try"
145
| "support"
146
| "news"
147
| "store";
148
149
const PAGES: {
150
[top in PageKey]:
151
| {
152
[page: string]: { label: string; hide?: (c: CustomizeType) => boolean };
153
}
154
| { index: {} };
155
} = {
156
about,
157
features,
158
software,
159
pricing,
160
policies: POLICIES,
161
share: {},
162
info,
163
"sign-up": {},
164
"sign-in": {},
165
try: {},
166
support,
167
news: {},
168
store: {},
169
} as const;
170
171
export type Page = PageKey | "account";
172
export type SubPage =
173
| keyof typeof software
174
| keyof typeof features
175
| keyof typeof pricing
176
| keyof typeof POLICIES
177
| keyof typeof info
178
| keyof typeof support
179
| keyof typeof about;
180
181
interface Props {
182
page?: Page;
183
subPage?: SubPage;
184
softwareEnv?: SoftwareEnvNames;
185
}
186
187
const SEP = <div style={{ width: "16px", display: "inline-block" }} />;
188
189
export default function SubNav(props: Props) {
190
const { page, subPage, softwareEnv } = props;
191
const customize = useCustomize();
192
193
const [floating, setFloating] = useState(false);
194
const subnavRef = useRef<HTMLDivElement>(null);
195
196
// a hook tracking the vertical scroll position.
197
useEffect(() => {
198
const subnav = subnavRef.current;
199
if (subnav == null) return;
200
const onScroll = () => {
201
const offset = subnav.getBoundingClientRect().top;
202
setFloating(offset < 0);
203
};
204
window.addEventListener("scroll", onScroll);
205
return () => window.removeEventListener("scroll", onScroll);
206
}, [subnavRef]);
207
208
if (page == null) return null;
209
210
// if we define a custom support page, render it instead – and hide the sub menu
211
if (customize.support && !customize.onCoCalcCom) return null;
212
213
const tabs: JSX.Element[] = [];
214
const p = PAGES[page];
215
if (p == null || isEmpty(p)) return null;
216
217
function renderSoftwareEnvs() {
218
if (page != "software") return;
219
220
const links = SOFTWARE_ENV_NAMES.map((name) => {
221
const selected = name === softwareEnv;
222
const style =
223
SOFTWARE_ENV_DEFAULT === name ? { fontWeight: "bold" } : undefined;
224
// clicking on the software env link should not switch between subpages
225
const sub =
226
subPage != null && software[subPage] != null ? subPage : "executables";
227
return (
228
<A
229
key={name}
230
style={{ ...tabStyle(selected), ...style }}
231
href={`/software/${sub}/${name}`}
232
>
233
{name}
234
</A>
235
);
236
});
237
return (
238
<>
239
{SEP}
240
<Divider type="vertical" style={{ borderColor: COLORS.GRAY_D }} />
241
{SEP}
242
<span style={{ marginRight: "15px" }}>Ubuntu</span>
243
{r_join(links, SEP)}
244
</>
245
);
246
}
247
248
for (const name in p) {
249
if (p[name]?.disabled) continue;
250
if (p[name]?.hide?.(customize)) continue;
251
252
if (p[name].type === "divider") {
253
tabs.push(
254
<Divider
255
key={name}
256
type="vertical"
257
style={{
258
borderColor: COLORS.GRAY_D,
259
}}
260
/>,
261
);
262
continue; // this is a divider, not a tab to click on
263
}
264
265
let { label, href, icon } = p[name];
266
if (name == "index") {
267
if (!href) href = `/${page}`;
268
if (!icon) icon = "home";
269
}
270
const selected = name == "index" ? !subPage : subPage == name;
271
tabs.push(
272
<SubPageTab
273
key={`${name}${subPage ?? ""}${softwareEnv ?? ""}`}
274
page={page}
275
selected={selected}
276
name={name}
277
softwareEnv={softwareEnv}
278
style={{ marginRight: "5px", marginLeft: "5px" }}
279
label={
280
<>
281
{icon && (
282
<>
283
<Icon name={icon} />
284
{label ? " " : ""}
285
</>
286
)}
287
{label}
288
</>
289
}
290
href={href}
291
/>,
292
);
293
}
294
295
const links = (
296
<>
297
{tabs}
298
{renderSoftwareEnvs()}
299
</>
300
);
301
302
function renderFloating() {
303
return (
304
<div
305
style={{
306
...FLOAT_STYLE,
307
...{ paddingLeft: "0px" },
308
...{ display: floating ? "block" : "none" }, // we always render it, to make sure the logo has been loaded (no flickering)
309
}}
310
>
311
<div style={INNER_STYLE}>
312
<A
313
href={"/"}
314
style={{
315
display: "inline-block",
316
float: "left",
317
position: "relative",
318
marginLeft: "10px",
319
marginRight: "5px",
320
}}
321
>
322
<Logo
323
type="icon"
324
style={{
325
height: "30px",
326
width: "30px",
327
}}
328
/>
329
</A>
330
<A
331
onClick={() => window.scrollTo(0, 0)}
332
style={{
333
display: "inline-block",
334
float: "right",
335
position: "relative",
336
marginLeft: "5px",
337
marginRight: "10px",
338
}}
339
>
340
<Icon
341
name="arrow-circle-up"
342
style={{
343
color: COLORS.GRAY_D,
344
fontSize: "30px",
345
}}
346
/>
347
</A>
348
{links}
349
</div>
350
</div>
351
);
352
}
353
354
return (
355
<>
356
<div ref={subnavRef} style={BASE_STYLE}>
357
<div style={INNER_STYLE}>{links}</div>
358
</div>
359
{renderFloating()}
360
</>
361
);
362
}
363
364
interface SubPageTabProps {
365
href?: string;
366
label: JSX.Element;
367
name: string;
368
page: string;
369
selected: boolean;
370
softwareEnv?: SoftwareEnvNames;
371
style?: CSS;
372
}
373
374
function SubPageTab(props: SubPageTabProps) {
375
const { page, name, selected, label, href, softwareEnv, style } = props;
376
377
// those software subpages also need the image name as the subpage
378
const suffix =
379
page === "software" ? `/${softwareEnv ?? SOFTWARE_ENV_DEFAULT}` : "";
380
381
const url = href ?? `/${page}/${name}${suffix}`;
382
383
return (
384
<A href={url} style={{ ...tabStyle(selected), ...style }}>
385
{label}
386
</A>
387
);
388
}
389
390
function tabStyle(selected: boolean): React.CSSProperties {
391
return selected
392
? {
393
fontWeight: "bold",
394
color: "blue",
395
paddingBottom: "3px",
396
borderBottom: "3px solid blue",
397
}
398
: { color: COLORS.GRAY_D };
399
}
400
401