1
0
Fork 0

wip(dev): Refactoring navigation

This commit is contained in:
Joost De Cock 2023-03-26 06:50:59 +02:00
parent 264e7a0b9d
commit 244f4524c4
20 changed files with 362 additions and 265 deletions

View file

@ -0,0 +1,6 @@
/*
* Placeholder feeds component that does nothing
* but allows us to re-use code that expects this
* to be here
*/
export const Feeds = () => null

View file

@ -1,14 +1,11 @@
import { useRouter } from 'next/router' import { useRouter } from 'next/router'
import { AsideNavigation } from 'shared/components/navigation/aside.mjs' import { AsideNavigation } from 'shared/components/navigation/aside.mjs'
export const BareLayout = ({ app, children = [] }) => { export const ns = []
const router = useRouter()
const slug = router.asPath.slice(1)
return ( export const BareLayout = ({ app, children = [] }) => (
<> <>
<AsideNavigation app={app} slug={slug} mobileOnly /> <AsideNavigation app={app} mobileOnly />
{children} {children}
</> </>
) )
}

View file

@ -5,44 +5,22 @@ import Link from 'next/link'
import { AsideNavigation } from 'site/components/navigation/aside.mjs' import { AsideNavigation } from 'site/components/navigation/aside.mjs'
import { ThemePicker } from 'shared/components/theme-picker/index.mjs' import { ThemePicker } from 'shared/components/theme-picker/index.mjs'
import { Breadcrumbs } from 'shared/components/breadcrumbs.mjs' import { Breadcrumbs } from 'shared/components/breadcrumbs.mjs'
import { getCrumbs } from 'shared/utils.mjs'
import { HomeIcon } from 'shared/components/icons.mjs' import { HomeIcon } from 'shared/components/icons.mjs'
import { useState, useEffect } from 'react' import { useState, useEffect } from 'react'
export const DocsLayout = ({ app, title = false, crumbs = false, children = [] }) => { export const ns = []
const router = useRouter()
const [slug, setSlug] = useState('')
const [breadcrumbs, setBreadcrumbs] = useState(crumbs)
useEffect(() => { export const DocsLayout = ({ app, children = [], title }) => (
const newSlug = router.asPath.slice(1)
setSlug(newSlug)
if (!breadcrumbs) setBreadcrumbs(getCrumbs(app, newSlug, title))
}, [router.asPath, breadcrumbs, app, title])
return (
<div className="grid grid-cols-4 m-auto justify-center place-items-stretch"> <div className="grid grid-cols-4 m-auto justify-center place-items-stretch">
<AsideNavigation <AsideNavigation app={app} />
app={app}
slug={slug}
before={[
<div className="flex flex-row items-center justify-between border-b mb-4" key="home-key">
<Link href="/">
<HomeIcon />
</Link>
<ThemePicker app={app} />
</div>,
]}
/>
<section className="col-span-4 lg:col-span-3 py-24 px-4 lg:pl-8 bg-base-50"> <section className="col-span-4 lg:col-span-3 py-24 px-4 lg:pl-8 bg-base-50">
{title && ( {title && (
<div className="xl:pl-4"> <div className="xl:pl-4">
<Breadcrumbs title={title} crumbs={breadcrumbs} /> <Breadcrumbs crumbs={app.state.crumbs} title={title} />
<h1 className="break-words">{title}</h1> <h1 className="break-words">{title}</h1>
</div> </div>
)} )}
{children} {children}
</section> </section>
</div> </div>
) )
}

View file

@ -1,26 +1,26 @@
import { MainSections, ActiveSection } from './primary.mjs' import { MainSections, ActiveSection } from './primary.mjs'
import Link from 'next/link'
export const AsideNavigation = ({ app, slug, mobileOnly = false, before = [], after = [] }) => ( export const AsideNavigation = ({ app, mobileOnly = false, before = [], after = [] }) => (
<aside <aside
className={` className={`
fixed top-0 right-0 h-screen fixed top-0 right-0 h-screen
overflow-y-auto z-20 overflow-y-auto z-20
bg-base-100 text-base-content bg-base-100 text-base-content
${app.primaryMenu ? '' : 'translate-x-[-120%]'} transition-transform ${app.state.primaryMenu ? '' : 'translate-x-[-120%]'} transition-transform
px-6 pb-20 pt-8 shrink-0 px-0 pb-20 pt-8 shrink-0
lg:w-auto lg:w-auto
lg:sticky lg:relative lg:transform-none lg:sticky lg:relative lg:transform-none
lg:justify-center lg:justify-center
lg:border-r-2 lg:border-base-200 lg:bg-base-200 lg:bg-opacity-50 lg:border-r-2 lg:border-base-200 lg:bg-base-200 lg:bg-opacity-50
lg:mt-16
${mobileOnly ? 'block lg:hidden w-full ' : ''} ${mobileOnly ? 'block lg:hidden w-full ' : ''}
`} `}
> >
<div>
{before} {before}
<MainSections app={app} active={slug} /> <MainSections app={app} />
<ActiveSection app={app} active={slug} /> <ActiveSection app={app} />
{after} {after}
</div>
</aside> </aside>
) )

View file

@ -1,6 +1,7 @@
import Link from 'next/link' import Link from 'next/link'
import orderBy from 'lodash.orderby' import orderBy from 'lodash.orderby'
import { TutorialIcon, GuideIcon, HelpIcon, DocsIcon } from 'shared/components/icons.mjs' import { TutorialIcon, GuideIcon, HelpIcon, DocsIcon } from 'shared/components/icons.mjs'
import { Breadcrumbs } from 'shared/components/breadcrumbs.mjs'
// List of icons matched to top-level slug // List of icons matched to top-level slug
const icons = { const icons = {
@ -11,7 +12,7 @@ const icons = {
} }
/* helper method to order nav entries */ /* helper method to order nav entries */
const order = (obj) => orderBy(obj, ['__order', '__title'], ['asc', 'asc']) const order = (obj) => orderBy(obj, ['o', 't'], ['asc', 'asc'])
// Component for the collapse toggle // Component for the collapse toggle
// Exported for re-use // Exported for re-use
@ -55,12 +56,12 @@ const isActive = (slug, active) => {
} }
// Component that renders a sublevel of navigation // Component that renders a sublevel of navigation
const SubLevel = ({ nodes = {}, active }) => ( const SubLevel = ({ nodes = {}, active = '' }) => (
<ul className="pl-5 list-inside"> <ul className="pl-5 list-inside">
{currentChildren(nodes).map((child) => {currentChildren(nodes).map((child) =>
Object.keys(child).length > 4 ? ( Object.keys(child).length > 4 ? (
<li key={child.__slug} className="flex flex-row"> <li key={child.s} className="flex flex-row">
<details className="grow" open={isActive(child.__slug, active)}> <details className="grow" open={isActive(child.s, active)}>
<summary <summary
className={` className={`
flex flex-row flex flex-row
@ -72,8 +73,8 @@ const SubLevel = ({ nodes = {}, active }) => (
`} `}
> >
<Link <Link
href={`${child.__slug}`} href={`${child.s}`}
title={child.__title} title={child.t}
className={` className={`
grow pl-2 border-l-2 grow pl-2 border-l-2
${linkClasses} ${linkClasses}
@ -81,7 +82,7 @@ const SubLevel = ({ nodes = {}, active }) => (
hover:border-secondary hover:border-secondary
sm:hover:border-secondary sm:hover:border-secondary
${ ${
child.__slug === active child.s === active
? 'text-secondary border-secondary sm:text-secondary sm:border-secondary' ? 'text-secondary border-secondary sm:text-secondary sm:border-secondary'
: 'text-base-content sm:text-base-content' : 'text-base-content sm:text-base-content'
} }
@ -92,17 +93,15 @@ const SubLevel = ({ nodes = {}, active }) => (
className={` className={`
text-3xl mr-2 inline-block p-0 leading-3 text-3xl mr-2 inline-block p-0 leading-3
${ ${
child.__slug === active child.s === active
? 'text-secondary sm:text-secondary translate-y-1' ? 'text-secondary sm:text-secondary translate-y-1'
: 'translate-y-3' : 'translate-y-3'
} }
`} `}
> >
{child.__slug === active ? <>&bull;</> : <>&deg;</>} {child.s === active ? <>&bull;</> : <>&deg;</>}
</span>
<span className={child.__slug === active ? 'font-bold' : ''}>
{child.__linktitle || child.__title}
</span> </span>
<span className={child.s === active ? 'font-bold' : ''}>{child.t}</span>
</span> </span>
</Link> </Link>
<Chevron w={6} m={3} /> <Chevron w={6} m={3} />
@ -111,10 +110,10 @@ const SubLevel = ({ nodes = {}, active }) => (
</details> </details>
</li> </li>
) : ( ) : (
<li className="pl-2 flex flex-row items-center" key={child.__slug}> <li className="pl-2 flex flex-row items-center" key={child.s}>
<Link <Link
href={`${child.__slug}`} href={`${child.s}`}
title={child.__title} title={child.t}
className={` className={`
pl-2 border-l-2 pl-2 border-l-2
grow grow
@ -123,7 +122,7 @@ const SubLevel = ({ nodes = {}, active }) => (
hover:border-secondary hover:border-secondary
sm:hover:border-secondary sm:hover:border-secondary
${ ${
child.__slug === active child.s === active
? 'text-secondary border-secondary sm:text-secondary sm:border-secondary' ? 'text-secondary border-secondary sm:text-secondary sm:border-secondary'
: 'text-base-content sm:text-base-content' : 'text-base-content sm:text-base-content'
}`} }`}
@ -133,17 +132,15 @@ const SubLevel = ({ nodes = {}, active }) => (
className={` className={`
text-3xl mr-2 inline-block p-0 leading-3 text-3xl mr-2 inline-block p-0 leading-3
${ ${
child.__slug === active child.s === active
? 'text-secondary sm:text-secondary translate-y-1' ? 'text-secondary sm:text-secondary translate-y-1'
: 'translate-y-3' : 'translate-y-3'
} }
`} `}
> >
{child.__slug === active ? <>&bull;</> : <>&deg;</>} {child.s === active ? <>&bull;</> : <>&deg;</>}
</span>
<span className={child.__slug === active ? 'font-bold' : ''}>
{child.__linktitle || child.__title}
</span> </span>
<span className={child.s === active ? 'font-bold' : ''}>{child.t}</span>
</span> </span>
</Link> </Link>
</li> </li>
@ -164,15 +161,10 @@ export const Icons = ({
const output = [] const output = []
for (const page of order(app.navigation)) { for (const page of order(app.navigation)) {
output.push( output.push(
<li key={page.__slug}> <li key={page.s}>
<Link <Link href={`${page.s}`} className={linkClasses} title={page.t} style={linkStyle}>
href={`${page.__slug}`} {icons[page.s] ? icons[page.s]('w-14 h-14') : <HelpIcon />}
className={linkClasses} <span className="font-bold">{page.t}</span>
title={page.__title}
style={linkStyle}
>
{icons[page.__slug] ? icons[page.__slug]('w-14 h-14') : <HelpIcon />}
<span className="font-bold">{page.__title}</span>
</Link> </Link>
</li> </li>
) )
@ -181,24 +173,20 @@ export const Icons = ({
return <ul className={ulClasses}>{output}</ul> return <ul className={ulClasses}>{output}</ul>
} }
export const MainSections = ({ app, active }) => { export const MainSections = ({ app }) => {
if (!app.navigation) return null if (!app.state.sections) return null
const output = [] const output = []
for (const page of order(app.navigation)) { for (const page of app.state.sections) {
const act = isActive(page.__slug, active) const act = isActive(page.s, app.state.slug)
const txt = ( const txt = (
<> <>
{icons[page.__slug] ? ( {icons[page.s] ? icons[page.s](`w-8 h-8 ${act ? 'text-accent' : ''}`) : <HelpIcon />}
icons[page.__slug](`w-8 h-8 ${act ? 'text-accent' : ''}`) <span className={`font-bold ${act ? 'text-accent' : ''}`}>{page.t}</span>
) : (
<HelpIcon />
)}
<span className={`font-bold ${act ? 'text-accent' : ''}`}>{page.__title}</span>
</> </>
) )
const item = ( const item = (
<li key={page.__slug}> <li key={page.s}>
{act ? ( {act ? (
<span <span
className={` className={`
@ -208,19 +196,19 @@ export const MainSections = ({ app, active }) => {
p-2 rounded p-2 rounded
bg-base-200 bg-base-200
`} `}
title={page.__title} title={page.t}
> >
{txt} {txt}
</span> </span>
) : ( ) : (
<Link <Link
href={`/${page.__slug}`} href={`/${page.s}`}
className={` className={`
flex flex-row gap-4 items-center flex flex-row gap-4 items-center
hover:bg-secondary hover:bg-opacity-10 hover:cursor-pointer hover:bg-secondary hover:bg-opacity-10 hover:cursor-pointer
p-2 rounded p-2 rounded
`} `}
title={page.__title} title={page.t}
> >
{txt} {txt}
</Link> </Link>
@ -233,13 +221,15 @@ export const MainSections = ({ app, active }) => {
return <ul>{output}</ul> return <ul>{output}</ul>
} }
export const ActiveSection = ({ app, active }) => ( export const ActiveSection = ({ app }) => (
<div className="-ml-5 mt-4 pt-4 border-t-2"> <div className="mt-4 pt-4 border-t-2">
<SubLevel {app.state.crumbs ? (
hasChildren={1} <div className="pl-4">
nav={app.navigation} <Breadcrumbs crumbs={app.state.crumbs.slice(0, 2)} />
active={active} </div>
nodes={order(app.navigation[active.split('/')[0]])} ) : null}
/> <div className="pr-2">
<SubLevel hasChildren={1} nodes={app.state.nav} active={app.state.slug} />
</div>
</div> </div>
) )

View file

@ -1,22 +1,9 @@
import { useRouter } from 'next/router'
import Head from 'next/head' import Head from 'next/head'
import { Header } from 'site/components/header.mjs' import { Header } from 'site/components/header.mjs'
import { Footer } from 'site/components/footer.mjs' import { Footer } from 'site/components/footer.mjs'
import { Search } from 'site/components/search.mjs' import { Search } from 'site/components/search.mjs'
export const LayoutWrapper = ({ app, children = [], search, setSearch, noSearch = false }) => { export const LayoutWrapper = ({ app, children = [], search, setSearch, noSearch = false }) => {
const startNavigation = () => {
app.startLoading()
// Force close of menu on mobile if it is open
if (app.primaryNavigation) app.setPrimaryNavigation(false)
// Force close of search modal if it is open
if (search) setSearch(false)
}
const router = useRouter()
router.events?.on('routeChangeStart', startNavigation)
router.events?.on('routeChangeComplete', () => app.stopLoading())
return ( return (
<div <div
className={` className={`

View file

@ -1,71 +0,0 @@
// Hooks
import React, { useState, useEffect } from 'react'
import { useSwipeable } from 'react-swipeable'
import { useRouter } from 'next/router'
import { useHotkeys } from 'react-hotkeys-hook'
// Components
import Head from 'next/head'
import { LayoutWrapper } from 'site/components/wrappers/layout.mjs'
import { DocsLayout } from 'site/components/layouts/docs.mjs'
import { Modal } from 'shared/components/modal.mjs'
import { Loader } from 'shared/components/loader.mjs'
/* This component should wrap all page content */
export const PageWrapper = ({
title = 'FIXME: No title set',
noSearch = false,
app = false,
layout = DocsLayout,
crumbs = false,
children = [],
}) => {
const swipeHandlers = useSwipeable({
onSwipedLeft: () => (app.primaryMenu ? app.setPrimaryMenu(false) : null),
onSwipedRight: () => (app.primaryMenu ? null : app.setPrimaryMenu(true)),
trackMouse: true,
})
const router = useRouter()
const slug = router.asPath.slice(1)
useEffect(() => app.setSlug(slug), [slug, app])
// Trigger search with Ctrl+k
useHotkeys('ctrl+k', (evt) => {
evt.preventDefault()
setSearch(true)
})
const [search, setSearch] = useState(false)
const childProps = {
app: app,
title: title,
crumbs: crumbs,
search,
setSearch,
toggleSearch: () => setSearch(!search),
noSearch: noSearch,
}
const Layout = layout
return (
<div
ref={swipeHandlers.ref}
onMouseDown={swipeHandlers.onMouseDown}
data-theme={app.theme}
key={app.theme} // This forces the data-theme update
>
<Head>
<meta property="og:title" content={`${title} - FreeSewing.dev`} key="title" />
<title>{title} - FreeSewing.dev</title>
</Head>
<LayoutWrapper {...childProps}>
{Layout ? <Layout {...childProps}>{children}</Layout> : children}
</LayoutWrapper>
{app.popup && <Modal cancel={() => app.setPopup(false)}>{app.popup}</Modal>}
{app.loading && <Loader />}
</div>
)
}

View file

@ -0,0 +1,33 @@
/*
* Dumb method to generate a unique (enough) ID for submissions to bugsnag
*/
function createErrorId() {
let result = ''
const characters = 'abcdefghijklmnopqrstuvwxyz0123456789'
const charactersLength = characters.length
for (let s = 0; s < 3; s++) {
for (let i = 0; i < 4; i++) {
result += characters.charAt(Math.floor(Math.random() * charactersLength))
}
if (s < 2) result += '-'
}
return result
}
/*
* The hook
*/
export function useBugsnag(bugsnag) {
const reportError = (err) => {
const id = createErrorId()
bugsnag.notify(err, (evt) => {
evt.setUser(account.username ? account.username : '__visitor')
evt.context = id
})
return id
}
return reportError
}

View file

@ -0,0 +1,38 @@
import get from 'lodash.get'
import { prebuildNavigation as pbn } from 'site/prebuild/navigation.mjs'
import orderBy from 'lodash.orderby'
const createCrumbs = (saa) =>
saa.map((crumb, i) => {
const entry = get(pbn.en, saa.slice(0, i + 1))
const val = { t: entry.t, s: entry.s }
if (entry.o) val.o = entry.o
return val
})
const createSections = () => {
const sections = {}
for (const slug of Object.keys(pbn.en)) {
const entry = pbn.en[slug]
const val = { t: entry.t, s: entry.s }
if (entry.o) val.o = entry.o
sections[slug] = val
}
return orderBy(sections, 'o')
}
export const loadNavigation = (saa = []) => {
// Creat crumbs array
const crumbs = createCrumbs(saa)
return {
saa,
slug: saa.join('/'),
crumbs,
sections: createSections(),
nav: saa.length > 1 ? get(pbn.en, saa[0]) : pbn.en[saa[0]],
title: crumbs.slice(-1)[0].t,
}
}

View file

@ -1,8 +1,8 @@
// Hooks // Hooks
import { useApp } from 'site/hooks/useApp.mjs' import { useApp } from 'shared/hooks/use-app.mjs'
// Components // Components
import Head from 'next/head' import Head from 'next/head'
import { PageWrapper } from 'site/components/wrappers/page.mjs' import { PageWrapper } from 'shared/components/wrappers/page.mjs'
import { BareLayout } from 'site/components/layouts/bare.mjs' import { BareLayout } from 'site/components/layouts/bare.mjs'
import { Robot } from 'shared/components/robot/index.mjs' import { Robot } from 'shared/components/robot/index.mjs'
import { Popout } from 'shared/components/popout.mjs' import { Popout } from 'shared/components/popout.mjs'

View file

@ -1,12 +1,12 @@
// Hooks // Hooks
import { useApp } from 'site/hooks/useApp.mjs' import { useApp } from 'shared/hooks/use-app.mjs'
// Dependencies // Dependencies
import mdxMeta from 'site/prebuild/mdx.en.js' import mdxMeta from 'site/prebuild/mdx.en.js'
import { mdxLoader } from 'shared/mdx/loader.mjs' import { mdxLoader } from 'shared/mdx/loader.mjs'
import { serverSideTranslations } from 'next-i18next/serverSideTranslations' import { serverSideTranslations } from 'next-i18next/serverSideTranslations'
// Components // Components
import Head from 'next/head' import Head from 'next/head'
import { PageWrapper } from 'site/components/wrappers/page.mjs' import { PageWrapper, ns as pageNs } from 'shared/components/wrappers/page.mjs'
import { MdxWrapper } from 'shared/components/wrappers/mdx.mjs' import { MdxWrapper } from 'shared/components/wrappers/mdx.mjs'
import { TocWrapper } from 'shared/components/wrappers/toc.mjs' import { TocWrapper } from 'shared/components/wrappers/toc.mjs'
import { HelpUs } from 'site/components/help-us.mjs' import { HelpUs } from 'site/components/help-us.mjs'
@ -14,7 +14,7 @@ import { jargon } from 'site/jargon.mjs'
const MdxPage = (props) => { const MdxPage = (props) => {
// This hook is used for shared code and global state // This hook is used for shared code and global state
const app = useApp() const app = useApp(props)
/* /*
* Each page should be wrapped in the Page wrapper component * Each page should be wrapped in the Page wrapper component
@ -26,7 +26,7 @@ const MdxPage = (props) => {
* active state * active state
*/ */
return ( return (
<PageWrapper app={app} {...props.page}> <PageWrapper app={app} title={app.state.title}>
<Head> <Head>
<meta property="og:type" content="article" key="type" /> <meta property="og:type" content="article" key="type" />
<meta property="og:description" content={props.intro} key="type" /> <meta property="og:description" content={props.intro} key="type" />
@ -83,9 +83,7 @@ export async function getStaticProps({ params }) {
toc, toc,
intro: intro.join(' '), intro: intro.join(' '),
page: { page: {
slug: params.mdxslug.join('/'), saa: params.mdxslug, // slug as array (saa)
path: '/' + params.mdxslug.join('/'),
slugArray: params.mdxslug,
...mdxMeta[params.mdxslug.join('/')], ...mdxMeta[params.mdxslug.join('/')],
}, },
params, params,

View file

@ -1,10 +1,10 @@
// Hooks // Hooks
import { useApp } from 'site/hooks/useApp.mjs' import { useApp } from 'shared/hooks/use-app.mjs'
// Dependencies // Dependencies
import { serverSideTranslations } from 'next-i18next/serverSideTranslations' import { serverSideTranslations } from 'next-i18next/serverSideTranslations'
// Components // Components
import Head from 'next/head' import Head from 'next/head'
import { PageWrapper } from 'site/components/wrappers/page.mjs' import { PageWrapper } from 'shared/components/wrappers/page.mjs'
import { Popout } from 'shared/components/popout.mjs' import { Popout } from 'shared/components/popout.mjs'
import { WebLink } from 'shared/components/web-link.mjs' import { WebLink } from 'shared/components/web-link.mjs'

View file

@ -1,10 +1,10 @@
// Hooks // Hooks
import { useApp } from 'site/hooks/useApp.mjs' import { useApp } from 'shared/hooks/use-app.mjs'
// Dependencies // Dependencies
import { serverSideTranslations } from 'next-i18next/serverSideTranslations' import { serverSideTranslations } from 'next-i18next/serverSideTranslations'
// Components // Components
import Head from 'next/head' import Head from 'next/head'
import { PageWrapper } from 'site/components/wrappers/page.mjs' import { PageWrapper } from 'shared/components/wrappers/page.mjs'
import { BareLayout } from 'site/components/layouts/bare.mjs' import { BareLayout } from 'site/components/layouts/bare.mjs'
import { Icons } from 'shared/components/navigation/primary.mjs' import { Icons } from 'shared/components/navigation/primary.mjs'
import { Highlight } from 'shared/components/mdx/highlight.mjs' import { Highlight } from 'shared/components/mdx/highlight.mjs'

View file

@ -1,32 +1,22 @@
import { Fragment } from 'react' import { HomeIcon } from 'shared/components/icons.mjs'
import Link from 'next/link' import Link from 'next/link'
import { FreeSewingIcon } from 'shared/components/icons.mjs'
export const Breadcrumbs = ({ crumbs = [] }) => export const Breadcrumbs = ({ crumbs, title }) =>
crumbs ? ( crumbs ? (
<ul className="flex flex-row flex-wrap gap-2 font-bold"> <div className="text-sm breadcrumbs flex-wrap">
<ul>
<li> <li>
<Link href="/" title="FreeSewing" className="text-base-content"> <Link href="/" title="FreeSewing">
<FreeSewingIcon /> <HomeIcon />
</Link> </Link>
</li> </li>
{crumbs.map((crumb) => ( {crumbs.map((crumb) => (
<Fragment key={crumb[1] + crumb[0]}> <li key={crumb.s}>
<li className="text-base-content px-2">&raquo;</li> <Link href={crumb.s} title={crumb.t} className="text-secondary-focus font-bold">
<li> {crumb.t}
{crumb[1] ? (
<Link
href={crumb[1]}
title={crumb[0]}
className="text-secondary hover:text-secondary-focus"
>
{crumb[0]}
</Link> </Link>
) : (
<span className="text-base-content">{crumb[0]}</span>
)}
</li> </li>
</Fragment>
))} ))}
</ul> </ul>
</div>
) : null ) : null

View file

@ -96,6 +96,7 @@ const renderNext = (node) =>
) )
export const PrevNext = ({ app }) => { export const PrevNext = ({ app }) => {
return <p>fixme: prevnext</p>
return ( return (
<div className="grid grid-cols-2 gap-4 border-t mt-12 py-2"> <div className="grid grid-cols-2 gap-4 border-t mt-12 py-2">
{renderPrevious(previous(app))} {renderPrevious(previous(app))}

View file

@ -0,0 +1,97 @@
// Dependencies
import React, { useState, useEffect } from 'react'
import { useSwipeable } from 'react-swipeable'
import { useHotkeys } from 'react-hotkeys-hook'
// Hooks
import { useTheme } from 'shared/hooks/use-theme.mjs'
// Components
import { LayoutWrapper, ns as layoutNs } from 'site/components/wrappers/layout.mjs'
import { DocsLayout } from 'site/components/layouts/docs.mjs'
import { Feeds } from 'site/components/feeds.mjs'
//export const ns = [...layoutNs]
/* This component should wrap all page content */
export const PageWrapper = ({
noSearch = false,
app = false,
layout = DocsLayout,
footer = true,
children = [],
title = 'FIXME: No title set',
}) => {
/*
* This forces a re-render upon initial bootstrap of the app
* This is needed to avoid hydration errors because theme can't be set reliably in SSR
*/
const [theme, setTheme] = useTheme()
const [currentTheme, setCurrentTheme] = useState()
useEffect(() => setCurrentTheme(theme), [currentTheme, theme])
/*
* Swipe handling for the entire site
*/
const swipeHandlers = useSwipeable({
onSwipedLeft: () => (app.primaryMenu ? app.setPrimaryMenu(false) : null),
onSwipedRight: () => (app.primaryMenu ? null : app.setPrimaryMenu(true)),
trackMouse: true,
})
/*
* Hotkeys (keyboard actions)
*/
// Trigger search with /
useHotkeys('/', (evt) => {
evt.preventDefault()
setSearch(true)
})
// Always close modal when Escape key is hit
useHotkeys('esc', (evt) => {
evt.preventDefault()
app.setModal(false)
})
// Search state
const [search, setSearch] = useState(false)
// Helper object to pass props down (keeps things DRY)
const childProps = {
app: app,
footer,
search,
setSearch,
toggleSearch: () => setSearch(!search),
noSearch: noSearch,
title,
}
// Make layout prop into a (uppercase) component
const Layout = layout
// Return wrapper
return (
<div
ref={swipeHandlers.ref}
onMouseDown={swipeHandlers.onMouseDown}
data-theme={currentTheme} // This facilitates CSS selectors
key={currentTheme} // This forces the data-theme update
>
<Feeds />
<LayoutWrapper {...childProps}>
{Layout ? <Layout {...childProps}>{children}</Layout> : children}
</LayoutWrapper>
{app.modal ? (
<div
className={`fixed top-0 left-0 m-0 p-0 shadow drop-shadow-lg w-full h-screen
bg-base-100 bg-opacity-90 z-50 hover:cursor-pointer
flex flex-row items-center justify-center
`}
onClick={() => app.setModal(false)}
>
{app.modal}
</div>
) : null}
</div>
)
}

View file

@ -1,28 +1,36 @@
import { loadNavigation } from 'site/lib/load-navigation.mjs'
// Hooks // Hooks
import { useState } from 'react' import { useState, useEffect } from 'react'
import { useBugsnag } from 'site/hooks/use-bugsnag.mjs' import { useBugsnag } from 'shared/hooks/use-bugsnag.mjs'
import { useRouter } from 'next/router'
// Dependencies // Dependencies
import get from 'lodash.get'
import set from 'lodash.set' import set from 'lodash.set'
import unset from 'lodash.unset' import unset from 'lodash.unset'
const defaultState = { const defaultState = {
loading: false, loading: false,
modal: null, modal: null,
menu: {
main: null,
},
} }
/* /*
* The useApp hook * The useApp hook
*/ */
export function useApp({ bugsnag }) { export function useApp(props = {}) {
const router = useRouter() const { bugsnag = false, page = {}, loadState = {} } = props
const reportError = useBugsnag ? useBugsnag(bugsnag) : () => false const { saa = [] } = page
const reportError = useBugsnag(props?.bugsnag)
// React state // React state
const [state, setState] = useState({ const [state, setState] = useState(() => ({ ...defaultState, ...loadState }))
...defaultState,
slug: useRouter().asPath.slice(1), useEffect(() => {
}) // Force update of navigation info (nav, title, crumbs) on each page change
if (saa.length > 0) setState({ ...state, ...loadNavigation(saa) })
}, [saa, state.slug, state.title])
/* /*
* Helper methods for partial state updates * Helper methods for partial state updates
@ -33,7 +41,9 @@ export function useApp({ bugsnag }) {
/* /*
* Helper methods for specific state updates * Helper methods for specific state updates
*/ */
const clearModal = () => updateState('modal', null) const closeModal = () => updateState('modal', null)
const closeMenu = (name) =>
get(state, `menu.${name}`, false) ? updateState(`menu.${name}`, false) : null
const startLoading = () => updateState('loading', true) const startLoading = () => updateState('loading', true)
const stopLoading = () => updateState('loading', false) const stopLoading = () => updateState('loading', false)
@ -45,7 +55,8 @@ export function useApp({ bugsnag }) {
unsetState, unsetState,
// Helper methods // Helper methods
clearModal, closeModal,
closeMenu,
startLoading, startLoading,
stopLoading, stopLoading,

View file

@ -0,0 +1,36 @@
/*
* Dumb method to generate a unique (enough) ID for submissions to bugsnag
*/
function createErrorId() {
let result = ''
const characters = 'abcdefghijklmnopqrstuvwxyz0123456789'
const charactersLength = characters.length
for (let s = 0; s < 3; s++) {
for (let i = 0; i < 4; i++) {
result += characters.charAt(Math.floor(Math.random() * charactersLength))
}
if (s < 2) result += '-'
}
return result
}
/*
* The hook
*/
export function useBugsnag(bugsnag) {
// If we don't use bugsnag, return a placeholder method
if (!bugsnag) return () => false
const reportError = (err) => {
const id = createErrorId()
bugsnag.notify(err, (evt) => {
evt.setUser(account.username ? account.username : '__visitor')
evt.context = id
})
return id
}
return reportError
}

View file

@ -122,11 +122,8 @@ export const prebuildMdx = async (site) => {
if (slug) { if (slug) {
const meta = await mdxMetaInfo(file) const meta = await mdxMetaInfo(file)
if (meta.data?.title) { if (meta.data?.title) {
pages[lang][slug] = { pages[lang][slug] = { t: meta.data.title }
title: meta.data.title, if (meta.data.order) pages[lang][slug].o = `${meta.data.order}${meta.data.title}`
slug,
order: meta.data.order ? `${meta.data.order}${meta.data.title}` : meta.data.title,
}
} else { } else {
if (pages.en[slug]) { if (pages.en[slug]) {
console.log(`l Falling back to EN metadata for ${slug}`) console.log(`l Falling back to EN metadata for ${slug}`)
@ -146,14 +143,14 @@ export const prebuildMdx = async (site) => {
fs.writeFileSync( fs.writeFileSync(
path.resolve('..', site, 'prebuild', `mdx.${lang}.js`), path.resolve('..', site, 'prebuild', `mdx.${lang}.js`),
`export default ${JSON.stringify(pages[lang], null, 2)}` `export default ${JSON.stringify(pages[lang])}`
) )
} }
// Write list of all MDX paths (in one language) // Write list of all MDX paths (in one language)
fs.writeFileSync( fs.writeFileSync(
path.resolve('..', site, 'prebuild', `mdx.paths.mjs`), path.resolve('..', site, 'prebuild', `mdx.paths.mjs`),
`export const mdxPaths = ${JSON.stringify(Object.keys(pages.en), null, 2)}` `export const mdxPaths = ${JSON.stringify(Object.keys(pages.en))}`
) )
return pages return pages

View file

@ -9,6 +9,15 @@ const future = new Date('10-12-2026').getTime()
* Main method that does what needs doing * Main method that does what needs doing
*/ */
export const prebuildNavigation = (mdxPages, strapiPosts, site) => { export const prebuildNavigation = (mdxPages, strapiPosts, site) => {
/*
* Since this is written to disk and loaded as JSON, we minimize
* the data to load by using the following 1-character keys:
*
* t: title
* l: link title (shorter version of the title, optional
* o: order, optional
* s: slug without leading or trailing slash (/)
*/
const nav = {} const nav = {}
for (const lang in mdxPages) { for (const lang in mdxPages) {
nav[lang] = {} nav[lang] = {}
@ -17,29 +26,29 @@ export const prebuildNavigation = (mdxPages, strapiPosts, site) => {
for (const slug of Object.keys(mdxPages[lang]).sort()) { for (const slug of Object.keys(mdxPages[lang]).sort()) {
const page = mdxPages[lang][slug] const page = mdxPages[lang][slug]
const chunks = slug.split('/') const chunks = slug.split('/')
set(nav, [lang, ...chunks], { const val = {
__title: page.title, t: page.t,
__linktitle: page.linktitle || page.title, s: slug,
__slug: slug, }
__order: page.order, if (page.o) val._o = page.o
}) set(nav, [lang, ...chunks], val)
} }
// Handle strapi content // Handle strapi content
for (const type in strapiPosts) { for (const type in strapiPosts) {
set(nav, [lang, type], { set(nav, [lang, type], {
__title: type, t: type,
__linktitle: type, l: type,
__slug: type, s: type,
__order: type, o: type,
}) })
for (const [slug, page] of Object.entries(strapiPosts[type][lang])) { for (const [slug, page] of Object.entries(strapiPosts[type][lang])) {
const chunks = slug.split('/') const chunks = slug.split('/')
set(nav, [lang, type, ...chunks], { set(nav, [lang, type, ...chunks], {
__title: page.title, t: page.title,
__linktitle: page.linktitle, l: page.linktitle,
__slug: type + '/' + slug, s: type + '/' + slug,
__order: (future - new Date(page.date).getTime()) / 100000, o: (future - new Date(page.date).getTime()) / 100000,
}) })
} }
} }