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 { AsideNavigation } from 'shared/components/navigation/aside.mjs'
export const BareLayout = ({ app, children = [] }) => {
const router = useRouter()
const slug = router.asPath.slice(1)
export const ns = []
return (
<>
<AsideNavigation app={app} slug={slug} mobileOnly />
{children}
</>
)
}
export const BareLayout = ({ app, children = [] }) => (
<>
<AsideNavigation app={app} mobileOnly />
{children}
</>
)

View file

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

View file

@ -1,26 +1,26 @@
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
className={`
fixed top-0 right-0 h-screen
overflow-y-auto z-20
bg-base-100 text-base-content
${app.primaryMenu ? '' : 'translate-x-[-120%]'} transition-transform
px-6 pb-20 pt-8 shrink-0
${app.state.primaryMenu ? '' : 'translate-x-[-120%]'} transition-transform
px-0 pb-20 pt-8 shrink-0
lg:w-auto
lg:sticky lg:relative lg:transform-none
lg:justify-center
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 ' : ''}
`}
>
<div>
{before}
<MainSections app={app} active={slug} />
<ActiveSection app={app} active={slug} />
{after}
</div>
{before}
<MainSections app={app} />
<ActiveSection app={app} />
{after}
</aside>
)

View file

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

View file

@ -1,22 +1,9 @@
import { useRouter } from 'next/router'
import Head from 'next/head'
import { Header } from 'site/components/header.mjs'
import { Footer } from 'site/components/footer.mjs'
import { Search } from 'site/components/search.mjs'
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 (
<div
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
import { useApp } from 'site/hooks/useApp.mjs'
import { useApp } from 'shared/hooks/use-app.mjs'
// Components
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 { Robot } from 'shared/components/robot/index.mjs'
import { Popout } from 'shared/components/popout.mjs'

View file

@ -1,12 +1,12 @@
// Hooks
import { useApp } from 'site/hooks/useApp.mjs'
import { useApp } from 'shared/hooks/use-app.mjs'
// Dependencies
import mdxMeta from 'site/prebuild/mdx.en.js'
import { mdxLoader } from 'shared/mdx/loader.mjs'
import { serverSideTranslations } from 'next-i18next/serverSideTranslations'
// Components
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 { TocWrapper } from 'shared/components/wrappers/toc.mjs'
import { HelpUs } from 'site/components/help-us.mjs'
@ -14,7 +14,7 @@ import { jargon } from 'site/jargon.mjs'
const MdxPage = (props) => {
// 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
@ -26,7 +26,7 @@ const MdxPage = (props) => {
* active state
*/
return (
<PageWrapper app={app} {...props.page}>
<PageWrapper app={app} title={app.state.title}>
<Head>
<meta property="og:type" content="article" key="type" />
<meta property="og:description" content={props.intro} key="type" />
@ -83,9 +83,7 @@ export async function getStaticProps({ params }) {
toc,
intro: intro.join(' '),
page: {
slug: params.mdxslug.join('/'),
path: '/' + params.mdxslug.join('/'),
slugArray: params.mdxslug,
saa: params.mdxslug, // slug as array (saa)
...mdxMeta[params.mdxslug.join('/')],
},
params,

View file

@ -1,10 +1,10 @@
// Hooks
import { useApp } from 'site/hooks/useApp.mjs'
import { useApp } from 'shared/hooks/use-app.mjs'
// Dependencies
import { serverSideTranslations } from 'next-i18next/serverSideTranslations'
// Components
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 { WebLink } from 'shared/components/web-link.mjs'

View file

@ -1,10 +1,10 @@
// Hooks
import { useApp } from 'site/hooks/useApp.mjs'
import { useApp } from 'shared/hooks/use-app.mjs'
// Dependencies
import { serverSideTranslations } from 'next-i18next/serverSideTranslations'
// Components
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 { Icons } from 'shared/components/navigation/primary.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 { FreeSewingIcon } from 'shared/components/icons.mjs'
export const Breadcrumbs = ({ crumbs = [] }) =>
export const Breadcrumbs = ({ crumbs, title }) =>
crumbs ? (
<ul className="flex flex-row flex-wrap gap-2 font-bold">
<li>
<Link href="/" title="FreeSewing" className="text-base-content">
<FreeSewingIcon />
</Link>
</li>
{crumbs.map((crumb) => (
<Fragment key={crumb[1] + crumb[0]}>
<li className="text-base-content px-2">&raquo;</li>
<li>
{crumb[1] ? (
<Link
href={crumb[1]}
title={crumb[0]}
className="text-secondary hover:text-secondary-focus"
>
{crumb[0]}
</Link>
) : (
<span className="text-base-content">{crumb[0]}</span>
)}
<div className="text-sm breadcrumbs flex-wrap">
<ul>
<li>
<Link href="/" title="FreeSewing">
<HomeIcon />
</Link>
</li>
{crumbs.map((crumb) => (
<li key={crumb.s}>
<Link href={crumb.s} title={crumb.t} className="text-secondary-focus font-bold">
{crumb.t}
</Link>
</li>
</Fragment>
))}
</ul>
))}
</ul>
</div>
) : null

View file

@ -96,6 +96,7 @@ const renderNext = (node) =>
)
export const PrevNext = ({ app }) => {
return <p>fixme: prevnext</p>
return (
<div className="grid grid-cols-2 gap-4 border-t mt-12 py-2">
{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
import { useState } from 'react'
import { useBugsnag } from 'site/hooks/use-bugsnag.mjs'
import { useRouter } from 'next/router'
import { useState, useEffect } from 'react'
import { useBugsnag } from 'shared/hooks/use-bugsnag.mjs'
// Dependencies
import get from 'lodash.get'
import set from 'lodash.set'
import unset from 'lodash.unset'
const defaultState = {
loading: false,
modal: null,
menu: {
main: null,
},
}
/*
* The useApp hook
*/
export function useApp({ bugsnag }) {
const router = useRouter()
const reportError = useBugsnag ? useBugsnag(bugsnag) : () => false
export function useApp(props = {}) {
const { bugsnag = false, page = {}, loadState = {} } = props
const { saa = [] } = page
const reportError = useBugsnag(props?.bugsnag)
// React state
const [state, setState] = useState({
...defaultState,
slug: useRouter().asPath.slice(1),
})
const [state, setState] = useState(() => ({ ...defaultState, ...loadState }))
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
@ -33,7 +41,9 @@ export function useApp({ bugsnag }) {
/*
* 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 stopLoading = () => updateState('loading', false)
@ -45,7 +55,8 @@ export function useApp({ bugsnag }) {
unsetState,
// Helper methods
clearModal,
closeModal,
closeMenu,
startLoading,
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) {
const meta = await mdxMetaInfo(file)
if (meta.data?.title) {
pages[lang][slug] = {
title: meta.data.title,
slug,
order: meta.data.order ? `${meta.data.order}${meta.data.title}` : meta.data.title,
}
pages[lang][slug] = { t: meta.data.title }
if (meta.data.order) pages[lang][slug].o = `${meta.data.order}${meta.data.title}`
} else {
if (pages.en[slug]) {
console.log(`l Falling back to EN metadata for ${slug}`)
@ -146,14 +143,14 @@ export const prebuildMdx = async (site) => {
fs.writeFileSync(
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)
fs.writeFileSync(
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

View file

@ -9,6 +9,15 @@ const future = new Date('10-12-2026').getTime()
* Main method that does what needs doing
*/
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 = {}
for (const lang in mdxPages) {
nav[lang] = {}
@ -17,29 +26,29 @@ export const prebuildNavigation = (mdxPages, strapiPosts, site) => {
for (const slug of Object.keys(mdxPages[lang]).sort()) {
const page = mdxPages[lang][slug]
const chunks = slug.split('/')
set(nav, [lang, ...chunks], {
__title: page.title,
__linktitle: page.linktitle || page.title,
__slug: slug,
__order: page.order,
})
const val = {
t: page.t,
s: slug,
}
if (page.o) val._o = page.o
set(nav, [lang, ...chunks], val)
}
// Handle strapi content
for (const type in strapiPosts) {
set(nav, [lang, type], {
__title: type,
__linktitle: type,
__slug: type,
__order: type,
t: type,
l: type,
s: type,
o: type,
})
for (const [slug, page] of Object.entries(strapiPosts[type][lang])) {
const chunks = slug.split('/')
set(nav, [lang, type, ...chunks], {
__title: page.title,
__linktitle: page.linktitle,
__slug: type + '/' + slug,
__order: (future - new Date(page.date).getTime()) / 100000,
t: page.title,
l: page.linktitle,
s: type + '/' + slug,
o: (future - new Date(page.date).getTime()) / 100000,
})
}
}