Real-time collaboration for Jupyter Notebooks, Linux Terminals, LaTeX, VS Code, R IDE, and more,
all in one place.
Real-time collaboration for Jupyter Notebooks, Linux Terminals, LaTeX, VS Code, R IDE, and more,
all in one place.
Path: blob/master/src/packages/next/components/landing/sub-nav.tsx
Views: 687
/*1* This file is part of CoCalc: Copyright © 2021 Sagemath, Inc.2* License: MS-RSL – see LICENSE.md for details3*/45import { Divider } from "antd";6import { isEmpty } from "lodash";7import { useEffect, useRef, useState } from "react";89import { Icon } from "@cocalc/frontend/components/icon";10import { r_join } from "@cocalc/frontend/components/r_join";11import {12SOFTWARE_ENV_DEFAULT,13SOFTWARE_ENV_NAMES,14SoftwareEnvNames,15} from "@cocalc/util/consts/software-envs";16import { COLORS } from "@cocalc/util/theme";17import Logo from "components/logo";18import { CSS } from "components/misc";19import A from "components/misc/A";20import { MAX_WIDTH_LANDING } from "lib/config";21import { CustomizeType, useCustomize } from "lib/customize";2223const BASE_STYLE: CSS = {24backgroundColor: "white",25textAlign: "center",26paddingLeft: "45px",27paddingRight: "45px",28paddingTop: "10px",29paddingBottom: "10px",30width: "100%",31zIndex: 1,32lineHeight: "2rem", // important to increase line height for narrow screens, otherwise text+underline is rendered on top of each other33maxHeight: "5rem",34overflow: "hidden",35};3637const FLOAT_STYLE: CSS = {38...BASE_STYLE,39position: "fixed",40paddingBottom: "5px",41paddingRight: 0,42paddingLeft: 0,43top: "0",44boxShadow: "0 4px 6px 0 rgba(0.1,0.1,0.1,0.20)",45} as const;4647const INNER_STYLE: CSS = {48maxWidth: MAX_WIDTH_LANDING,49margin: "0 auto",50overflow: "auto",51whiteSpace: "nowrap",52};5354const about = {55index: {},56events: { label: "Events" },57team: { label: "Team" },58} as const;5960const software = {61index: {},62executables: { label: "Executables" },63python: { label: "Python" },64r: { label: "R Stats" },65julia: { label: "Julia" },66octave: { label: "Octave" },67sagemath: { label: "SageMath" },68} as const;6970const features = {71index: {},72"jupyter-notebook": { label: "Jupyter" },73julia: { label: "Julia" },74"latex-editor": { label: "LaTeX" },75linux: { label: "Linux" },76octave: { label: "Octave" },77python: { label: "Python" },78"r-statistical-software": { label: "R Stats" },79sage: { label: "SageMath" },80slides: { label: "Slides" },81teaching: { label: "Teaching" },82terminal: { label: "Terminal" },83whiteboard: { label: "Whiteboard" },84x11: { label: "X11" },85div1: { type: "divider" },86"compute-server": { label: "Compute" },87ai: { label: "AI Assistant" },88compare: { label: "Compare" },89api: { label: "API" },90} as const;9192const pricing = {93index: {},94products: { label: "Products" },95subscriptions: { label: "Subscriptions" },96courses: { label: "Courses" },97institutions: { label: "Institutions" },98onprem: { label: "OnPrem" },99dedicated: { label: "Dedicated" },100} as const;101102export const POLICIES = {103index: {},104terms: { label: "Terms of Service", hide: (c) => !c.onCoCalcCom },105copyright: { label: "Copyright", hide: (c) => !c.onCoCalcCom },106privacy: { label: "Privacy", hide: (c) => !c.onCoCalcCom },107trust: { label: "Trust", hide: (c) => !c.onCoCalcCom },108thirdparties: { label: "Third Parties", hide: (c) => !c.onCoCalcCom },109ferpa: { label: "FERPA", hide: (c) => !c.onCoCalcCom },110accessibility: { label: "Accessibility", hide: (c) => !c.onCoCalcCom },111imprint: { label: "Imprint", hide: (c) => !c.imprint },112policies: { label: "Policies", hide: (c) => !c.policies },113} as const;114115const info = {116index: {},117doc: { label: "Documentation" },118status: { label: "Status" },119run: { label: "Run CoCalc" },120} as const;121122const support = {123index: {},124community: { label: "Community" },125new: { label: "New Ticket", hide: (customize) => !customize.zendesk },126tickets: { label: "Tickets", hide: (customize) => !customize.zendesk },127chatgpt: {128label: "AI",129hide: (customize) => !customize.openaiEnabled || !customize.onCoCalcCom,130},131} as const;132133type PageKey =134| "about"135| "features"136| "software"137| "pricing"138| "policies"139| "share"140| "info"141| "sign-up"142| "sign-in"143| "try"144| "support"145| "news"146| "store";147148const PAGES: {149[top in PageKey]:150| {151[page: string]: { label: string; hide?: (c: CustomizeType) => boolean };152}153| { index: {} };154} = {155about,156features,157software,158pricing,159policies: POLICIES,160share: {},161info,162"sign-up": {},163"sign-in": {},164try: {},165support,166news: {},167store: {},168} as const;169170export type Page = PageKey | "account";171export type SubPage =172| keyof typeof software173| keyof typeof features174| keyof typeof pricing175| keyof typeof POLICIES176| keyof typeof info177| keyof typeof support178| keyof typeof about;179180interface Props {181page?: Page;182subPage?: SubPage;183softwareEnv?: SoftwareEnvNames;184}185186const SEP = <div style={{ width: "16px", display: "inline-block" }} />;187188export default function SubNav(props: Props) {189const { page, subPage, softwareEnv } = props;190const customize = useCustomize();191192const [floating, setFloating] = useState(false);193const subnavRef = useRef<HTMLDivElement>(null);194195// a hook tracking the vertical scroll position.196useEffect(() => {197const subnav = subnavRef.current;198if (subnav == null) return;199const onScroll = () => {200const offset = subnav.getBoundingClientRect().top;201setFloating(offset < 0);202};203window.addEventListener("scroll", onScroll);204return () => window.removeEventListener("scroll", onScroll);205}, [subnavRef]);206207if (page == null) return null;208209// if we define a custom support page, render it instead – and hide the sub menu210if (customize.support && !customize.onCoCalcCom) return null;211212const tabs: JSX.Element[] = [];213const p = PAGES[page];214if (p == null || isEmpty(p)) return null;215216function renderSoftwareEnvs() {217if (page != "software") return;218219const links = SOFTWARE_ENV_NAMES.map((name) => {220const selected = name === softwareEnv;221const style =222SOFTWARE_ENV_DEFAULT === name ? { fontWeight: "bold" } : undefined;223// clicking on the software env link should not switch between subpages224const sub =225subPage != null && software[subPage] != null ? subPage : "executables";226return (227<A228key={name}229style={{ ...tabStyle(selected), ...style }}230href={`/software/${sub}/${name}`}231>232{name}233</A>234);235});236return (237<>238{SEP}239<Divider type="vertical" style={{ borderColor: COLORS.GRAY_D }} />240{SEP}241<span style={{ marginRight: "15px" }}>Ubuntu</span>242{r_join(links, SEP)}243</>244);245}246247for (const name in p) {248if (p[name]?.disabled) continue;249if (p[name]?.hide?.(customize)) continue;250251if (p[name].type === "divider") {252tabs.push(253<Divider254key={name}255type="vertical"256style={{257borderColor: COLORS.GRAY_D,258}}259/>,260);261continue; // this is a divider, not a tab to click on262}263264let { label, href, icon } = p[name];265if (name == "index") {266if (!href) href = `/${page}`;267if (!icon) icon = "home";268}269const selected = name == "index" ? !subPage : subPage == name;270tabs.push(271<SubPageTab272key={`${name}${subPage ?? ""}${softwareEnv ?? ""}`}273page={page}274selected={selected}275name={name}276softwareEnv={softwareEnv}277style={{ marginRight: "5px", marginLeft: "5px" }}278label={279<>280{icon && (281<>282<Icon name={icon} />283{label ? " " : ""}284</>285)}286{label}287</>288}289href={href}290/>,291);292}293294const links = (295<>296{tabs}297{renderSoftwareEnvs()}298</>299);300301function renderFloating() {302return (303<div304style={{305...FLOAT_STYLE,306...{ paddingLeft: "0px" },307...{ display: floating ? "block" : "none" }, // we always render it, to make sure the logo has been loaded (no flickering)308}}309>310<div style={INNER_STYLE}>311<A312href={"/"}313style={{314display: "inline-block",315float: "left",316position: "relative",317marginLeft: "10px",318marginRight: "5px",319}}320>321<Logo322type="icon"323style={{324height: "30px",325width: "30px",326}}327/>328</A>329<A330onClick={() => window.scrollTo(0, 0)}331style={{332display: "inline-block",333float: "right",334position: "relative",335marginLeft: "5px",336marginRight: "10px",337}}338>339<Icon340name="arrow-circle-up"341style={{342color: COLORS.GRAY_D,343fontSize: "30px",344}}345/>346</A>347{links}348</div>349</div>350);351}352353return (354<>355<div ref={subnavRef} style={BASE_STYLE}>356<div style={INNER_STYLE}>{links}</div>357</div>358{renderFloating()}359</>360);361}362363interface SubPageTabProps {364href?: string;365label: JSX.Element;366name: string;367page: string;368selected: boolean;369softwareEnv?: SoftwareEnvNames;370style?: CSS;371}372373function SubPageTab(props: SubPageTabProps) {374const { page, name, selected, label, href, softwareEnv, style } = props;375376// those software subpages also need the image name as the subpage377const suffix =378page === "software" ? `/${softwareEnv ?? SOFTWARE_ENV_DEFAULT}` : "";379380const url = href ?? `/${page}/${name}${suffix}`;381382return (383<A href={url} style={{ ...tabStyle(selected), ...style }}>384{label}385</A>386);387}388389function tabStyle(selected: boolean): React.CSSProperties {390return selected391? {392fontWeight: "bold",393color: "blue",394paddingBottom: "3px",395borderBottom: "3px solid blue",396}397: { color: COLORS.GRAY_D };398}399400401