wip(dev): Refactoring navigation
This commit is contained in:
parent
264e7a0b9d
commit
244f4524c4
20 changed files with 362 additions and 265 deletions
6
sites/dev/components/feeds.mjs
Normal file
6
sites/dev/components/feeds.mjs
Normal 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
|
|
@ -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}
|
||||
</>
|
||||
)
|
||||
|
|
|
@ -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>
|
||||
)
|
||||
|
|
|
@ -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>
|
||||
)
|
||||
|
|
|
@ -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 ? <>•</> : <>°</>}
|
||||
</span>
|
||||
<span className={child.__slug === active ? 'font-bold' : ''}>
|
||||
{child.__linktitle || child.__title}
|
||||
{child.s === active ? <>•</> : <>°</>}
|
||||
</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 ? <>•</> : <>°</>}
|
||||
</span>
|
||||
<span className={child.__slug === active ? 'font-bold' : ''}>
|
||||
{child.__linktitle || child.__title}
|
||||
{child.s === active ? <>•</> : <>°</>}
|
||||
</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>
|
||||
)
|
||||
|
|
|
@ -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={`
|
||||
|
|
|
@ -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>
|
||||
)
|
||||
}
|
33
sites/dev/hooks/use-bugsnag.mjs
Normal file
33
sites/dev/hooks/use-bugsnag.mjs
Normal 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
|
||||
}
|
38
sites/dev/lib/load-navigation.mjs
Normal file
38
sites/dev/lib/load-navigation.mjs
Normal 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,
|
||||
}
|
||||
}
|
|
@ -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'
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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'
|
||||
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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">»</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
|
||||
|
|
|
@ -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))}
|
||||
|
|
97
sites/shared/components/wrappers/page.mjs
Normal file
97
sites/shared/components/wrappers/page.mjs
Normal 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>
|
||||
)
|
||||
}
|
|
@ -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,
|
||||
|
||||
|
|
36
sites/shared/hooks/use-bugsnag.mjs
Normal file
36
sites/shared/hooks/use-bugsnag.mjs
Normal 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
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue