mirror of
https://github.com/jambonz/next-static-site.git
synced 2026-10-31 07:44:30 +00:00
organize src directory, lighthouse fixes
This commit is contained in:
38 files changed
+129
-89
No files matched your search
@@ -0,0 +1,44 @@
|
||||
import { nanoid } from 'nanoid';
|
||||
|
||||
import Link from 'next/link';
|
||||
|
||||
import { Button } from './jambonz-ui';
|
||||
|
||||
function FooterItem({ obj }) {
|
||||
return (
|
||||
<li>
|
||||
<Link href={obj.link}>
|
||||
<a target={obj.open ? '_blank' : null} className="foot__link">
|
||||
{obj.label}
|
||||
</a>
|
||||
</Link>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
|
||||
export default function Footer({ siteData }) {
|
||||
return (
|
||||
<footer className="bg-jambonz foot">
|
||||
<div className="wrap foot__wrap">
|
||||
<div className="foot__navs">
|
||||
<ul className="foot__links">
|
||||
{siteData.footer.links.map((obj) => {
|
||||
return <FooterItem key={nanoid()} obj={obj} />;
|
||||
})}
|
||||
</ul>
|
||||
<ul className="foot__links">
|
||||
<FooterItem key="home" obj={siteData.navi.home} />
|
||||
{siteData.navi.links.map((obj) => {
|
||||
return <FooterItem key={nanoid()} obj={obj} />;
|
||||
})}
|
||||
</ul>
|
||||
</div>
|
||||
<div className="foot__support">
|
||||
<Button href={`mailto:${siteData.footer.email}?subject=Jambonz Support`} target="_blank" subStyle="light">
|
||||
{siteData.footer.email}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,221 @@
|
||||
import classNames from 'classnames';
|
||||
import * as Icons from 'react-feather';
|
||||
import { nanoid } from 'nanoid';
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
|
||||
import Link from 'next/link';
|
||||
|
||||
// Normalize how we work with the subtext as an array[]
|
||||
export function normalizeSubtext(subtext) {
|
||||
if (!Array.isArray(subtext)) {
|
||||
subtext = [subtext];
|
||||
}
|
||||
|
||||
return subtext;
|
||||
}
|
||||
|
||||
// Simple method to normalize string as slug
|
||||
export function normalizeSlug(key) {
|
||||
return String(key.toLowerCase()).split(' ').join('-');
|
||||
}
|
||||
|
||||
// For accessibility issues we can "fix" our limitations with markdown and remark-html
|
||||
export function normalizeTextLayout(html) {
|
||||
return html
|
||||
.replace(/<(h[1-6])>/g, (m, p1) => {
|
||||
return `<div class="${p1}">`;
|
||||
})
|
||||
.replace(/<\/(h[1-6])>/g, (m, p1) => {
|
||||
return '</div>';
|
||||
});
|
||||
}
|
||||
|
||||
// Normalize how we listen for media queries
|
||||
// Intentionally `null` default value -- will throw Error
|
||||
export function useMatchMedia(mediaQuery = null) {
|
||||
if (!mediaQuery) {
|
||||
throw new Error(`Jambonz UI "useMatchMedia" requires valid Media Query: ${mediaQuery} was passed.`);
|
||||
}
|
||||
|
||||
const [mobile, setMobile] = useState(false);
|
||||
|
||||
const handleMedia = useCallback((e) => {
|
||||
setMobile(e.matches);
|
||||
}, [setMobile]);
|
||||
|
||||
useEffect(() => {
|
||||
const mql = window.matchMedia(mediaQuery);
|
||||
|
||||
mql.addEventListener('change', handleMedia);
|
||||
|
||||
setMobile(mql.matches);
|
||||
|
||||
return function cleanup() {
|
||||
mql.removeEventListener('change', handleMedia);
|
||||
};
|
||||
}, [handleMedia, setMobile, mediaQuery]);
|
||||
|
||||
return mobile;
|
||||
}
|
||||
|
||||
// Normalize for our mobile media query
|
||||
export function useMobileMedia() {
|
||||
return useMatchMedia('(max-width: 896px)');
|
||||
}
|
||||
|
||||
export function H1({ children, ...rest }) {
|
||||
return <h1 {...rest}>{children}</h1>;
|
||||
}
|
||||
|
||||
export function H2({ children, ...rest }) {
|
||||
return <h2 {...rest}>{children}</h2>;
|
||||
}
|
||||
|
||||
export function H3({ children, ...rest }) {
|
||||
return <h3 {...rest}>{children}</h3>;
|
||||
}
|
||||
|
||||
export function H4({ children, ...rest }) {
|
||||
return <h4 {...rest}>{children}</h4>;
|
||||
}
|
||||
|
||||
export function H5({ children, ...rest }) {
|
||||
return <h5 {...rest}>{children}</h5>;
|
||||
}
|
||||
|
||||
export function H6({ children, ...rest }) {
|
||||
return <h6 {...rest}>{children}</h6>;
|
||||
}
|
||||
|
||||
export function P({ children, ...rest }) {
|
||||
return <p {...rest}>{children}</p>;
|
||||
}
|
||||
|
||||
export function M({ children }) {
|
||||
return <div className="m">{children}</div>;
|
||||
}
|
||||
|
||||
export function MS({ children }) {
|
||||
return <div className="ms">{children}</div>;
|
||||
}
|
||||
|
||||
export function MXS({ children }) {
|
||||
return <div className="mxs">{children}</div>;
|
||||
}
|
||||
|
||||
export function Latest({ data }) {
|
||||
const classes = {
|
||||
'latest': true,
|
||||
[`latest--${data.label}`]: true,
|
||||
'pad': true,
|
||||
'bg-pink': true,
|
||||
};
|
||||
|
||||
return (
|
||||
<section className={classNames(classes)}>
|
||||
<div className="wrap latest__wrap">
|
||||
<div className="latest__headline">
|
||||
<H2>{data.headline}</H2>
|
||||
</div>
|
||||
<div className="latest__subtext">
|
||||
<H3 className="h5">
|
||||
{/* Use dangerouslySetInnerHTML to render inline links from YAML data */}
|
||||
{normalizeSubtext(data.subtext).map((subtext) => {
|
||||
return <div key={nanoid()} dangerouslySetInnerHTML={{ __html: subtext }} />;
|
||||
})}
|
||||
</H3>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
export function Hero({ data, subStyle }) {
|
||||
const classes = {
|
||||
'hero': true,
|
||||
'pad': true,
|
||||
};
|
||||
|
||||
if (subStyle) {
|
||||
classes[`hero--${subStyle}`] = true;
|
||||
}
|
||||
|
||||
return (
|
||||
<section className={classNames(classes)}>
|
||||
<div className="wrap hero__wrap">
|
||||
<div className="hero__headline">
|
||||
<H1>{data.headline}</H1>
|
||||
</div>
|
||||
<div className="hero__subtext">
|
||||
<H2 className="h5">
|
||||
{normalizeSubtext(data.subtext).map((subtext) => {
|
||||
return <div key={nanoid()}>{subtext}</div>;
|
||||
})}
|
||||
</H2>
|
||||
</div>
|
||||
{data.cta && (
|
||||
<div className="hero__cta">
|
||||
<Button href={data.url} target="_blank">{data.cta}</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
// Extra {props} get passed to the <a> element
|
||||
export function Button({ children, href, mainStyle = 'fill', subStyle = null, ...props }) {
|
||||
const classes = {
|
||||
'btn': true,
|
||||
[`btn--${mainStyle}`]: true,
|
||||
};
|
||||
|
||||
if (subStyle) {
|
||||
classes[`btn--${mainStyle}--${subStyle}`] = true;
|
||||
}
|
||||
|
||||
return (
|
||||
<Link href={href}>
|
||||
<a {...props} className={classNames(classes)}>{children}</a>
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
|
||||
// Extra {props} get passed to the feather Component
|
||||
// See react-feather for all 286 icons available
|
||||
// https://github.com/feathericons/react-feather
|
||||
export function Icon({ name, mainStyle = 'inline', subStyle = null, ...props }) {
|
||||
const Component = Icons[name];
|
||||
const classes = {
|
||||
'icon': true,
|
||||
[`icon--${mainStyle}`]: true,
|
||||
};
|
||||
|
||||
if (subStyle) {
|
||||
classes[`icon--${mainStyle}--${subStyle}`] = true;
|
||||
}
|
||||
|
||||
if (!Component) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Stylized icon
|
||||
if (mainStyle !== 'inline') {
|
||||
return (
|
||||
<div className={classNames(classes)}>
|
||||
<Component {...props} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Inline icon
|
||||
return <Component {...props} />;
|
||||
}
|
||||
|
||||
export function TextLayout({ data, name }) {
|
||||
return (
|
||||
<div className={`text-layout text-layout--${name}`}>
|
||||
<div className="text-layout__wrap" dangerouslySetInnerHTML={{ __html: data.contentHtml }} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
import Head from 'next/head';
|
||||
import Link from 'next/link';
|
||||
|
||||
import Navi from './navi';
|
||||
import Footer from './footer';
|
||||
|
||||
function Banner({ data }) {
|
||||
return (
|
||||
<div className="banner">
|
||||
<Link href={data.link}>
|
||||
<a target="_blank" className="wrap">{data.text}</a>
|
||||
</Link>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function Layout({ children, siteData, title = 'jambonz' }) {
|
||||
return (
|
||||
<>
|
||||
<Head>
|
||||
<title>{title}</title>
|
||||
<link rel="icon" href="/favicon.ico" />
|
||||
<meta charSet="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<meta name="theme-color" content="#000000" />
|
||||
<meta name="image" property="og:image" content="https://www.jambonz.org/jambonz.png" />
|
||||
<meta
|
||||
name="description"
|
||||
content="The “bring your own everything” CPaaS"
|
||||
/>
|
||||
<link rel="apple-touch-icon" href="/logo192.png" />
|
||||
{/*
|
||||
manifest.json provides metadata used when your web app is installed on a
|
||||
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
|
||||
*/}
|
||||
<link rel="manifest" href="/manifest.json" />
|
||||
</Head>
|
||||
{siteData.banner && siteData.banner.active && <Banner data={siteData.banner} />}
|
||||
<Navi siteData={siteData} />
|
||||
<main className="main">
|
||||
{children}
|
||||
</main>
|
||||
<Footer siteData={siteData} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
import { useState } from 'react';
|
||||
|
||||
import { nanoid } from 'nanoid';
|
||||
import classNames from 'classnames';
|
||||
|
||||
import Link from 'next/link';
|
||||
import { useRouter } from 'next/router';
|
||||
|
||||
import { Icon, TextLayout } from './jambonz-ui';
|
||||
|
||||
function MarkdownSidebar({scope, data}) {
|
||||
const router = useRouter();
|
||||
const regex = new RegExp(`^/${scope}/|^/+|/+$`, 'g');
|
||||
const parsedTab = router.asPath.replace(regex, '').split('/').shift();
|
||||
const parsedPath = router.asPath.replace(/^\/+|\/+$/g, '').split('/').pop();
|
||||
const [active, setActive] = useState({
|
||||
[parsedTab]: true,
|
||||
});
|
||||
|
||||
const handleToggle = (slug) => {
|
||||
setActive((oldActive) => {
|
||||
const newActive = {};
|
||||
|
||||
for (let i in oldActive) {
|
||||
newActive[i] = oldActive[i];
|
||||
}
|
||||
|
||||
newActive[slug] = newActive[slug] ? false : true;
|
||||
|
||||
return newActive;
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<nav className="markdown__navi">
|
||||
<div className="markdown__link">
|
||||
<Link href={data.root.link}>
|
||||
<a className="m">
|
||||
<strong>{data.root.label}</strong>
|
||||
</a>
|
||||
</Link>
|
||||
</div>
|
||||
<ul className="markdown__list">
|
||||
{data.navi.map((item) => {
|
||||
const isActiveToggle = (active[item.path] ? true : false);
|
||||
const subClasses = {
|
||||
'markdown__sublist': true,
|
||||
'active': isActiveToggle,
|
||||
};
|
||||
|
||||
return (
|
||||
<li key={nanoid()} className="markdown__item">
|
||||
<div className="m markdown__label" onClick={() => handleToggle(item.path)}>
|
||||
{isActiveToggle ? <Icon name="ChevronDown" /> : <Icon name="ChevronRight" />}
|
||||
<strong>{item.title}</strong>
|
||||
</div>
|
||||
<ul className={classNames(subClasses)}>
|
||||
{item.pages.map((page) => {
|
||||
const isActiveItem = (parsedPath === page.path && parsedTab === item.path) && isActiveToggle;
|
||||
const itemClasses = {
|
||||
'ms': true,
|
||||
'active': isActiveItem,
|
||||
};
|
||||
|
||||
return (
|
||||
<li key={nanoid()} className="markdown__subitem">
|
||||
<Link href={`/${scope}/${item.path}/${page.path}`}>
|
||||
<a className={classNames(itemClasses)}>{page.title}</a>
|
||||
</Link>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
</nav>
|
||||
);
|
||||
}
|
||||
|
||||
export default function Markdown({scope, data, docs}) {
|
||||
return (
|
||||
<div className="markdown">
|
||||
<div className="wrap markdown__wrap">
|
||||
<MarkdownSidebar scope={scope} data={data} />
|
||||
<TextLayout data={docs} name={scope} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
import { useState } from 'react';
|
||||
import { nanoid } from 'nanoid';
|
||||
import classNames from 'classnames';
|
||||
|
||||
import Link from 'next/link';
|
||||
import { useRouter } from 'next/router';
|
||||
|
||||
import { Button, Icon, useMobileMedia } from './jambonz-ui';
|
||||
|
||||
function NaviItem({obj}) {
|
||||
const router = useRouter();
|
||||
const rSlash = /^\/|\/$/g;
|
||||
const cleanLink = obj.link.replace(rSlash, '');
|
||||
const cleanPath = router.asPath.replace(rSlash, '').split('/')[0];
|
||||
const classes = {
|
||||
navi__link: true,
|
||||
active: cleanLink && cleanLink === cleanPath,
|
||||
};
|
||||
|
||||
return (
|
||||
<li>
|
||||
<Link href={obj.link}>
|
||||
<a target={obj.open ? '_blank' : null} className={classNames(classes)}>
|
||||
{obj.label}
|
||||
</a>
|
||||
</Link>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
|
||||
function NaviMobile({ active, handler, siteData }) {
|
||||
const classes = {
|
||||
'bg-jambonz': true,
|
||||
'wrap': true,
|
||||
'navi__mobile': true,
|
||||
'active': active,
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={classNames(classes)}>
|
||||
<div className="navi__mobile__head">
|
||||
<div className="navi__mobile__login">
|
||||
<Button href={siteData.navi.login.link} target="_blank" mainStyle="login" subStyle="white">
|
||||
{siteData.navi.login.label}
|
||||
</Button>
|
||||
</div>
|
||||
<div className="navi__mobile__icon" onClick={handler}>
|
||||
<Icon mainStyle="fill" subStyle="white" name="X" />
|
||||
</div>
|
||||
</div>
|
||||
<ul className="navi__mobile__links">
|
||||
<NaviItem key="home" obj={siteData.navi.home} />
|
||||
{siteData.navi.links.map((obj) => {
|
||||
return <NaviItem key={nanoid()} obj={obj} />;
|
||||
})}
|
||||
</ul>
|
||||
<ul className="navi__mobile__footer">
|
||||
{siteData.footer.links.map((obj) => {
|
||||
return <NaviItem key={nanoid()} obj={obj} />;
|
||||
})}
|
||||
</ul>
|
||||
<div className="navi__mobile__support">
|
||||
<Button href={`mailto:${siteData.footer.email}`} target="_blank" subStyle="light">
|
||||
{siteData.footer.email}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function Navi({ siteData }) {
|
||||
const [active, setActive] = useState(false);
|
||||
const mobile = useMobileMedia();
|
||||
const classes = {
|
||||
navi: true,
|
||||
mobile,
|
||||
active,
|
||||
};
|
||||
|
||||
const handleNavi = () => {
|
||||
setActive(!active);
|
||||
};
|
||||
|
||||
// Make sure mobile navi is closed on resizes...
|
||||
if (!mobile && active) {
|
||||
setActive(false);
|
||||
}
|
||||
|
||||
return (
|
||||
<nav className={classNames(classes)}>
|
||||
<div className="wrap navi__wrap">
|
||||
<Link href="/">
|
||||
<a className="navi__logo" title="jambonz">
|
||||
<img src="/svg/jambonz.svg" width="128" height="42" alt="jambonz" />
|
||||
</a>
|
||||
</Link>
|
||||
<ul className="navi__links">
|
||||
{siteData.navi.links.map((obj) => {
|
||||
return <NaviItem key={nanoid()} obj={obj} />;
|
||||
})}
|
||||
</ul>
|
||||
<div className="navi__icon" onClick={handleNavi}>
|
||||
<Icon mainStyle="fill" name="Menu" />
|
||||
</div>
|
||||
<div className="navi__login">
|
||||
<Button href={siteData.navi.login.link} target="_blank" mainStyle="login">
|
||||
{siteData.navi.login.label}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
{mobile && <NaviMobile active={active} handler={handleNavi} siteData={siteData} />}
|
||||
</nav>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user