Files
xrpl-dev-portal/@theme/components/Navbar/Navbar.tsx
2024-03-12 08:10:43 -07:00

545 lines
15 KiB
TypeScript

import * as React from 'react';
import styled from 'styled-components';
import { useThemeConfig } from '@theme/hooks/useThemeConfig';
import { LanguagePicker } from '@theme/i18n/LanguagePicker';
import { useI18n, useTranslate } from '@portal/hooks';
import { slugify } from '../../helpers';
import { Link } from '@portal/Link';
import { ColorModeSwitcher } from '@theme/components/ColorModeSwitcher/ColorModeSwitcher';
import { Search } from '@theme/components/Search/Search';
import { useLocation } from 'react-router-dom';
// @ts-ignore
// import navbar from '../../../top-nav.yaml';
// const alertBanner = {
// show: true,
// message: 'This is the draft Redocly version of the site!',
// button: 'Cool',
// link: 'https://github.com/ripple/xrpl-org-dev-portal',
// };
export function Navbar(props) {
// const [isOpen, setIsOpen] = useMobileMenu(false);
const themeConfig = useThemeConfig();
const { changeLanguage } = useI18n();
const menu = themeConfig.navbar?.items;
const logo = themeConfig.logo;
const { href, altText, items } = props;
const pathPrefix = '';
const navItems = menu.map((item, index) => {
if (item.type === 'group') {
return <NavDropdown key={index} label={item.label} labelTranslationKey={item.labelTranslationKey} items={item.items} pathPrefix={pathPrefix} />;
} else {
return (
<NavItem key={index}>
<Link to={item.link} className="nav-link">
{item.label}
</Link>
</NavItem>
);
}
});
const { pathname } = useLocation();
const blogNavs = getBlogNavigationConfig();
const blogNavItems = [];
for (const blogNav of blogNavs) {
if (blogNav.type === "group") {
blogNavItems.push(
<NavDropdown
key={blogNav.index}
label={blogNav.label}
items={blogNav.items}
pathPrefix={pathPrefix}
/>
);
} else {
blogNavItems.push(
<NavItem key={blogNav.index}>
<Link to={blogNav.link} className="nav-link">
{blogNav.label}
</Link>
</NavItem>
);
}
}
React.useEffect(() => {
// Turns out jQuery is necessary for firing events on Bootstrap v4
// dropdowns. These events set classes so that the search bar and other
// submenus collapse on mobile when you expand one submenu.
const dds = $('#topnav-pages .dropdown');
const top_main_nav = document.querySelector('#top-main-nav');
dds.on('show.bs.dropdown', evt => {
top_main_nav.classList.add('submenu-expanded');
});
dds.on('hidden.bs.dropdown', evt => {
top_main_nav.classList.remove('submenu-expanded');
});
// Close navbar on .dropdown-item click
const toggleNavbar = () => {
const navbarToggler = document.querySelector('.navbar-toggler');
const isNavbarCollapsed = navbarToggler.getAttribute('aria-expanded') === 'true';
if (isNavbarCollapsed) {
navbarToggler.click(); // Simulate click to toggle navbar
}
};
const dropdownItems = document.querySelectorAll('.dropdown-item');
dropdownItems.forEach(item => {
item.addEventListener('click', toggleNavbar);
});
// Cleanup function to remove event listeners
return () => {
dropdownItems.forEach(item => {
item.removeEventListener('click', toggleNavbar);
});
};
},[]);
// Render a different top nav for the Blog site.
if (pathname.includes("blog")) {
return (
<>
{/* <AlertBanner
show={alertBanner.show}
message={alertBanner.message}
button={alertBanner.button}
link={alertBanner.link}
/> */}
<NavWrapper>
<LogoBlock to={href} img={logo} alt={altText} />
<NavControls>
<MobileMenuIcon />
</NavControls>
<TopNavCollapsible>
<NavItems>
{blogNavItems}
<div id="topnav-search" className="nav-item search">
<Search className="topnav-search" />
</div>
<div id="topnav-button" className="nav-item">
<GetStartedButton />
</div>
<div id="topnav-language" className="nav-item">
<LanguagePicker onChangeLanguage={changeLanguage} onlyIcon />
</div>
<div id="topnav-theme" className="nav-item">
<StyledColorModeSwitcher />
</div>
</NavItems>
</TopNavCollapsible>
</NavWrapper>
</>
);
} else {
return (
<>
{/* <AlertBanner
show={alertBanner.show}
message={alertBanner.message}
button={alertBanner.button}
link={alertBanner.link}
/> */}
<NavWrapper>
<LogoBlock to={href} img={logo} alt={altText} />
<NavControls>
<MobileMenuIcon />
</NavControls>
<TopNavCollapsible>
<NavItems>
{navItems}
<div id="topnav-search" className="nav-item search">
<Search className="topnav-search" />
</div>
<div id="topnav-language" className="nav-item">
<LanguagePicker onChangeLanguage={changeLanguage} onlyIcon />
</div>
<div id="topnav-theme" className="nav-item">
<StyledColorModeSwitcher />
</div>
</NavItems>
</TopNavCollapsible>
</NavWrapper>
</>
);
}
}
const StyledColorModeSwitcher = styled(ColorModeSwitcher)`
padding: 10px;
`;
export function AlertBanner(props) {
const { show, message, button, link } = props;
return (
<div className="top-banner fixed-top">
<div className="d-flex justify-content-center">
<span>
<p className="mb-0">{message}</p>
</span>
<span>
<a href={link} target="_blank" className="btn btn-outline-secondary">
{button}
</a>
</span>
</div>
</div>
);
}
export function TopNavCollapsible(props) {
return (
<div className="collapse navbar-collapse justify-content-between" id="top-main-nav">
{props.children}
</div>
);
}
export function NavDropdown(props) {
const { label, items, pathPrefix, labelTranslationKey } = props;
const { translate } = useTranslate();
const dropdownGroups = items.map((item, index) => {
if (item.items) {
const groupLinks = item.items.map((item2, index2) => {
const cls2 = item2.external ? 'dropdown-item external-link' : 'dropdown-item';
let item2_href = item2.link;
if (item2_href && !item2_href.match(/^https?:/)) {
item2_href = pathPrefix + item2_href;
}
return (
<a key={index2} className={cls2} href={item2_href}>
{item2.label}
</a>
);
});
const clnm = 'navcol col-for-' + slugify(item.label);
return (
<div key={index} className={clnm}>
<h5 className="dropdown-item">{item.label}</h5>
{groupLinks}
</div>
);
} else if (item.icon) {
const hero_id = 'dropdown-hero-for-' + slugify(label);
const img_alt = item.label + ' icon';
let hero_href = item.link;
if (hero_href && !hero_href.match(/^https?:/)) {
hero_href = pathPrefix + hero_href;
}
const splitlabel = item.label.split(" || ")
const newlabel = splitlabel[0]
const description = splitlabel[1] // might be undefined, that's ok
return (
<a key={index} className="dropdown-item dropdown-hero" id={hero_id} href={hero_href}>
<img id={item.hero} alt={img_alt} src={item.icon} />
<div className="dropdown-hero-text">
<h4>{newlabel}</h4>
<p>{description}</p>
</div>
</a>
);
} else {
const cls = item.external ? 'dropdown-item ungrouped external-link' : 'dropdown-item ungrouped';
let item_href = item.link;
if (item_href && !item_href.match(/^https?:/)) {
item_href = pathPrefix + item_href;
}
return (
<a key={index} className={cls} href={item_href}>
{item.label}
</a>
);
}
});
const toggler_id = 'topnav_' + slugify(label);
const dd_id = 'topnav_dd_' + slugify(label);
return (
<li className="nav-item dropdown">
<a
className="nav-link dropdown-toggle"
href="#"
id={toggler_id}
role="button"
data-toggle="dropdown"
aria-haspopup="true"
aria-expanded="false"
>
<span>{translate(labelTranslationKey, label)}</span>
</a>
<div className="dropdown-menu" aria-labelledby={toggler_id} id={dd_id}>
{dropdownGroups}
</div>
</li>
);
}
export function NavWrapper(props) {
return (
<nav
className="top-nav navbar navbar-expand-lg navbar-dark fixed-top"
style={props.belowAlertBanner ? { marginTop: '46px' } : {}}
>
{props.children}
</nav>
);
}
export function NavControls(props) {
return (
<button
className="navbar-toggler collapsed"
type="button"
data-toggle="collapse"
data-target="#top-main-nav"
aria-controls="navbarHolder"
aria-expanded="false"
aria-label="Toggle navigation"
>
{props.children}
</button>
);
}
export function MobileMenuIcon() {
return (
<span className="navbar-toggler-icon">
<div></div>
</span>
);
}
export function GetStartedButton() {
const { translate } = useTranslate();
return (
<a className="btn btn-primary" href={"/docs/tutorials"} style={{ height: "38px", paddingTop: "11px"}}>
{translate("Get Started")}
</a>
);
}
export function NavItems(props) {
return (
<ul className="nav navbar-nav" id="topnav-pages">
{props.children}
</ul>
);
}
export function NavItem(props) {
return <li className="nav-item">{props.children}</li>;
}
export function LogoBlock(props) {
const { to, img, altText } = props;
return (
<a className="navbar-brand" href="/">
<img className="logo" alt={"XRP LEDGER"} height="40" src="data:," />
</a>
);
}
export class ThemeToggle extends React.Component {
auto_update_theme() {
const upc = window.localStorage.getItem('user-prefers-color');
let theme = 'dark'; // Default to dark theme
if (!upc) {
// User hasn't saved a preference specifically for this site; check
// the browser-level preferences.
if (window.matchMedia && window.matchMedia('(prefers-color-scheme: light)').matches) {
theme = 'light';
}
} else {
// Follow user's saved setting.
theme = upc == 'light' ? 'light' : 'dark';
}
const disable_theme = theme == 'dark' ? 'light' : 'dark';
document.documentElement.classList.add(theme);
document.documentElement.classList.remove(disable_theme);
}
user_choose_theme() {
const new_theme = document.documentElement.classList.contains('dark') ? 'light' : 'dark';
window.localStorage.setItem('user-prefers-color', new_theme);
document.body.style.transition = 'background-color .2s ease';
const disable_theme = new_theme == 'dark' ? 'light' : 'dark';
document.documentElement.classList.add(new_theme);
document.documentElement.classList.remove(disable_theme);
}
render() {
return (
<div className="nav-item" id="topnav-theme">
<form className="form-inline">
<div
className="custom-control custom-theme-toggle form-inline-item"
title=""
data-toggle="tooltip"
data-placement="left"
data-original-title="Toggle Dark Mode"
>
<input
type="checkbox"
className="custom-control-input"
id="css-toggle-btn"
onClick={this.user_choose_theme}
/>
<label className="custom-control-label" htmlFor="css-toggle-btn">
<span className="d-lg-none">Light/Dark Theme</span>
</label>
</div>
</form>
</div>
);
}
componentDidMount() {
this.auto_update_theme();
}
}
function getBlogNavigationConfig() {
const { translate } = useTranslate();
return [
{
index: "0",
label: translate("Learn"),
type: "group",
items: [
{
type: "group",
label: translate("XRP Ledger"),
items: [
{
type: "link",
fsPath: "about/index.page.tsx",
label: translate("Overview"),
link: "/about/",
},
{
type: "link",
fsPath: "about/uses.page.tsx",
label: translate("Uses"),
link: "/about/uses",
},
{
type: "link",
fsPath: "about/history.page.tsx",
label: translate("History"),
link: "/about/history",
},
{
type: "link",
fsPath: "about/impact.page.tsx",
label: translate("Impact"),
link: "/about/impact",
},
{
type: "link",
fsPath: "about/impact.page.tsx",
label: translate("Carbon Calculator"),
link: "/about/impact",
},
],
},
],
pathPrefix: "",
},
{
index: "1",
label: translate("Explore"),
type: "group",
items: [
{
type: "group",
label: translate("Explore the XRP Ledger"),
items: [
{
type: "link",
fsPath: "/docs/introduction/crypto-wallets.md",
label: translate("Wallets"),
link: "/docs/introduction/crypto-wallets",
},
{
type: "link",
fsPath: "about/xrp.page.tsx",
label: translate("Exchanges"),
link: "/about/xrp",
},
{
type: "link",
fsPath: "about/uses.page.tsx",
label: translate("Businesses"),
link: "/about/uses",
},
{
type: "link",
fsPath: "",
label: translate("Ledger Explorer"),
link: "https://livenet.xrpl.org/",
},
],
},
],
pathPrefix: "",
},
{
index: "2",
label: translate("Build"),
type: "group",
items: [
{
type: "group",
label: translate("Build with XRPL"),
items: [
{
type: "link",
fsPath: "/docs/index.page.tsx",
label: translate("Docs"),
link: "/docs/",
},
{
type: "link",
fsPath: "/resources/dev-tools/index.page.tsx",
label: translate("Dev Tools"),
link: "/resources/dev-tools/",
},
{
type: "link",
fsPath: "/blog/index.page.tsx",
label: translate("Dev Blog"),
link: "/blog/",
},
],
},
],
pathPrefix: "",
},
{
index: "3",
type: "link",
fsPath: "community/index.page.tsx",
label: translate("Contribute"),
link: "/community",
},
];
}