1
0
Fork 0

Merge pull request #4524 from freesewing/joost

feat(sites): Refactor prebuild step - Remove useNavigation hook
This commit is contained in:
Joost De Cock 2023-07-21 10:33:41 +02:00 committed by GitHub
commit 15078438a6
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
62 changed files with 1574 additions and 1110 deletions

View file

@ -5,6 +5,8 @@
*/
export const authors = {
joostdecock: { id: 0, name: 'Joost De Cock' },
'Prof. dr. Sorcha Ní Dhubhghaill': { id: 0, name: 'Prof. dr. Sorcha Ní Dhubhghaill' },
mocked: { id: 0, name: 'Unknown (mocked in dev)' },
benjamesben: { id: 0, name: 'Benjamin' },
nikhil: { id: 0, name: 'nikhil' },
jackseye: { id: 0, name: 'jackseye' },
@ -30,8 +32,7 @@ export const authors = {
Natalia: { id: 0, name: 'Natalia Sayang' },
chri5b: { id: 0, name: 'chri5b' },
tangerineshark: { id: 0, name: 'tangerineshark' },
'MA-TATAS': { id: 0, name: 'MA-TATAS' },
'Ivo Bek': { id: 0, name: 'Ivo Bek' },
'bekivo@gmail.com': { id: 0, name: 'Ivo Bek' },
}
/*
@ -47,4 +48,5 @@ export const gitToAuthor = {
'bobgeorgethe3rd@googlemail.com': 'bobgeorgethe3rd',
'70777269+tangerineshark@users.noreply.github.com': 'tangerineshark',
'thijs.assies@gmail.com': 'MA-TATAS',
'Natalia Sayang': 'Natalia',
}

View file

@ -104,7 +104,7 @@ new-design:
'chalk': '5.0.1'
'execa': '7.1.1'
'mustache': '4.2.0'
'ora': '6.1.0'
'ora': '6.3.1'
'prompts': '2.4.2'
'recursive-readdir': '2.2.3'
noble:

View file

@ -24,8 +24,6 @@ core:
prettier: "npx prettier --write 'src/*.mjs' 'tests/*.mjs'"
lint: "npx eslint 'src/*.mjs' 'tests/*.mjs'"
jsdoc: 'jsdoc -c jsdoc.json -r src'
i18n:
prebuild: 'node scripts/prebuilder.mjs'
models:
test: 'npx mocha tests/*.test.mjs'
new-design:
@ -67,13 +65,14 @@ dev:
build: &nextBuild 'next build'
cibuild: 'yarn build && node scripts/algolia.mjs'
clean: &nextClean 'rimraf prebuild/* && rimraf public/locales/*/* && rimraf public/feeds/* && rimraf ../shared/prebuild/data/*'
predev: 'FAST=1 SITE=dev node --experimental-json-modules ../shared/prebuild/index.mjs'
dev: &nextDev 'next dev -p 8000'
develop: *nextDev
i18n: "SITE=dev node ../shared/prebuild/i18n-only.mjs"
lint: &nextLint 'next lint'
buildsitedeps: &buildsitedeps 'cd ../../ && yarn buildall && cd -'
prebuild: 'yarn buildsitedeps && SITE=dev node --experimental-json-modules ../shared/prebuild/index.mjs'
prebuild: 'yarn buildsitedeps && node --experimental-json-modules ./prebuild.mjs'
prebuildonly: 'node --experimental-json-modules ./prebuild.mjs'
predev: 'node --experimental-json-modules ./prebuild.mjs'
serve: "pm2 start npm --name 'dev' -- run start"
start: &nextStart 'yarn prebuild && yarn dev'
@ -91,20 +90,22 @@ lab:
e2e: &e2e 'yarn playwright test'
lint: *nextLint
buildsitedeps: *buildsitedeps
prebuild: 'yarn buildsitedeps && SITE=lab node --experimental-json-modules ../shared/prebuild/index.mjs'
prebuild: 'yarn buildsitedeps && node --experimental-json-modules ./prebuild.mjs'
prebuildonly: 'node --experimental-json-modules ./prebuild.mjs'
start: *nextStart
org:
build: *nextBuild
cibuild: 'yarn build'
clean: *nextClean
predev: 'FAST=1 SITE=org node --experimental-json-modules ../shared/prebuild/index.mjs'
dev: *nextDev
develop: *nextDev
i18n: 'SITE=org node ../shared/prebuild/i18n-only.mjs'
lint: *nextLint
buildsitedeps: *buildsitedeps
prebuild: 'yarn buildsitedeps && SITE=org node --experimental-json-modules ../shared/prebuild/index.mjs'
prebuild: 'yarn buildsitedeps && node --experimental-json-modules ./prebuild.mjs'
prebuildonly: 'node --experimental-json-modules ./prebuild.mjs'
predev: 'node --experimental-json-modules ./prebuild.mjs'
start: *nextStart
sanity:

View file

@ -108,7 +108,8 @@ After the lab, dev, or org website starts:
custom URL.
- You can also access the custom URL via the "Ports" panel.
<Tip compact>
<Tip>
An example of a custom URL: `https://username-ominous-space-waffle-rwpgzw5q15vqc52q9-8000.preview.app.github.dev/`
</Tip>

View file

@ -39,7 +39,7 @@
"chalk": "5.0.1",
"execa": "7.1.1",
"mustache": "4.2.0",
"ora": "6.1.0",
"ora": "6.3.1",
"prompts": "2.4.2",
"recursive-readdir": "2.2.3"
},

View file

@ -1,5 +1,7 @@
// Dependencies
import { NavigationContext } from 'shared/context/navigation-context.mjs'
// Hooks
import { useNavigation } from 'site/hooks/use-navigation.mjs'
import { useContext } from 'react'
// Components
import { SectionsMenu, ns as sectionsNs } from 'site/components/navigation/sections-menu.mjs'
import { ModalWrapper } from 'shared/components/wrappers/modal.mjs'
@ -11,7 +13,8 @@ import { NavLinks, Breadcrumbs } from 'shared/components/navigation/sitenav.mjs'
export const ns = nsMerge(sectionsNs)
export const ModalMenu = ({ slug }) => {
const { siteNav } = useNavigation()
// Grab siteNav from the navigation context
const { siteNav } = useContext(NavigationContext)
return (
<ModalWrapper flex="col" justify="top" slideFrom="left">

View file

@ -1,9 +1,10 @@
import { useContext } from 'react'
import { NavigationContext } from 'shared/context/navigation-context.mjs'
import Link from 'next/link'
import { icons, ns as sectionsNs } from 'shared/components/navigation/primary.mjs'
import { useTranslation } from 'next-i18next'
import orderBy from 'lodash.orderby'
import { colors } from 'shared/components/header.mjs'
import { useNavigation } from 'site/hooks/use-navigation.mjs'
export const ns = sectionsNs
@ -11,7 +12,7 @@ const onlySections = (tree) => orderBy(tree, ['t'], ['asc']).filter((entry) => e
export const SectionsMenu = ({ bOnly = false }) => {
const { t } = useTranslation(ns)
const { siteNav } = useNavigation()
const { siteNav } = useContext(NavigationContext)
const output = []
let i = 1

View file

@ -1,108 +0,0 @@
import { prebuildNavigation as pbn } from 'site/prebuild/navigation.mjs'
import { orderedSlugLut } from 'shared/hooks/use-navigation-helpers.mjs'
/*
* prebuildNavvigation[locale] holds the navigation structure based on MDX content.
* The entire website only has a few pages that are now MDX-based:
* - 404 => no navigation shown
* - home page => no navvigation shown
* - /contact => Added below
*
* Remember Mc_Shifton:
* Note: Set 'm' to truthy to show this as a main section in the side-navigation (optional)
* Note: Set 'c' to set the control level to hide things from users (optional)
* Note: Set 's' to the slug (optional insofar as it's not a real page (a spacer for the header))
* Note: Set '_' to never show the page in the site navigation (like the tags pages)
* Note: Set 'h' to indicate this is a top-level page that should be hidden from the side-nav (like search)
* Note: Set 'i' when something should be included as top-level in the collapse side-navigation (optional)
* Note: Set 'f' to add the page to the footer
* Note: Set 't' to the title
* Note: Set 'o' to set the order (optional)
* Note: Set 'n' to mark this as a noisy entry that should always be closed unless active (like blog)
*/
export const ns = ['account', 'sections', 'design', 'tags']
const sitePages = () => {
const pages = {
// Top-level pages that are the sections menu
api: {
m: 1,
s: 'api',
t: 'API Documentation',
o: 10,
},
design: {
m: 1,
s: 'design',
t: 'Design Sewing Patterns',
o: 10,
},
contribute: {
m: 1,
s: 'contribute',
t: 'Contribute to FreeSewing',
o: 20,
},
i18n: {
m: 1,
s: 'i18n',
t: 'Help Translate FreeSewing',
o: 40,
},
infra: {
m: 1,
s: 'infra',
t: 'FreeSewing Infrastructure',
o: 50,
},
about: {
m: 1,
s: 'about',
f: 1,
t: 'About FreeSewing',
o: 60,
},
support: {
m: 1,
s: 'support',
f: 1,
t: 'Support FreeSewing',
o: 70,
},
search: {
s: 'search',
h: 1,
f: 1,
t: 'Search',
o: 270,
},
sitemap: {
s: 'sitemap',
h: 1,
f: 1,
t: 'Sitemap',
o: 270,
},
}
return pages
}
export const useNavigation = () => {
// Dev is EN only
const siteNav = { ...pbn.en, ...sitePages() }
// Make top-level documentation entries appear in i-list
for (const page of ['tutorials', 'guides', 'howtos', 'reference', 'training']) {
siteNav[page].o = 1000
siteNav[page].i = 1
}
// Hide contact from the sitenav
siteNav.contact.h = 1
return {
siteNav, // Site navigation
slugLut: orderedSlugLut(siteNav), // Slug lookup table
}
}

View file

@ -17,13 +17,14 @@
"build": "next build",
"cibuild": "yarn build && node scripts/algolia.mjs",
"clean": "rimraf prebuild/* && rimraf public/locales/*/* && rimraf public/feeds/* && rimraf ../shared/prebuild/data/*",
"predev": "FAST=1 SITE=dev node --experimental-json-modules ../shared/prebuild/index.mjs",
"dev": "next dev -p 8000",
"develop": "next dev -p 8000",
"i18n": "SITE=dev node ../shared/prebuild/i18n-only.mjs",
"lint": "next lint",
"buildsitedeps": "cd ../../ && yarn buildall && cd -",
"prebuild": "yarn buildsitedeps && SITE=dev node --experimental-json-modules ../shared/prebuild/index.mjs",
"prebuild": "yarn buildsitedeps && node --experimental-json-modules ./prebuild.mjs",
"prebuildonly": "node --experimental-json-modules ./prebuild.mjs",
"predev": "node --experimental-json-modules ./prebuild.mjs",
"serve": "pm2 start npm --name 'dev' -- run start",
"start": "yarn prebuild && yarn dev"
},

View file

@ -1,64 +1,71 @@
// Hooks
import { useNavigation } from 'site/hooks/use-navigation.mjs'
// Dependencies
import { serverSideTranslations } from 'next-i18next/serverSideTranslations'
// Components
import Head from 'next/head'
import { PageWrapper } from 'shared/components/wrappers/page.mjs'
import { PageWrapper, ns as pageNs } from 'shared/components/wrappers/page.mjs'
import { Robot } from 'shared/components/robot/index.mjs'
import { Popout } from 'shared/components/popout.mjs'
import { PageLink } from 'shared/components/page-link.mjs'
import { BaseLayout, BaseLayoutLeft, BaseLayoutWide } from 'shared/components/base-layout.mjs'
import { NavLinks, MainSections } from 'shared/components/navigation/sitenav.mjs'
const Page404 = () => {
const title = '404: Page not found'
const { siteNav } = useNavigation({ ignoreControl: true })
const slug = '404'
const namespaces = [...pageNs]
return (
<PageWrapper title={title}>
<Head>
<meta property="og:type" content="article" key="type" />
<meta
property="og:description"
content="There's nothing here. If you followed a link to get here, that link is broken"
key="description"
/>
<meta property="og:article:author" content="Joost De Cock" key="author" />
<meta property="og:image" content={`https://freesewing.dev/og/404/og.png`} key="image" />
<meta property="og:image:type" content="image/png" />
<meta property="og:image:width" content="1200" />
<meta property="og:image:height" content="630" />
<meta property="og:url" content={`https://freesewing.dev/`} key="url" />
<meta property="og:locale" content="en_US" key="locale" />
<meta property="og:site_name" content="freesewing.dev" key="site" />
</Head>
<BaseLayout>
<BaseLayoutLeft>
<MainSections {...{ siteNav, slug }} />
<NavLinks {...{ siteNav, slug }} />
</BaseLayoutLeft>
<BaseLayoutWide>
<div className="max-w-2xl">
<h1>404: Page not found</h1>
<div className="max-w-sm m-auto px-12 mb-4">
<Robot embed pose="fail" />
</div>
<h3>We could not find what you are looking for</h3>
<div className="text-left">
<Popout comment by="joost">
<h5>Did you arrive here from a link?</h5>
<p>In that case, that link is broken.</p>
<p>
If it was one of our links, please <PageLink href="/contact" txt="let us know" />{' '}
so we can fix it.
</p>
</Popout>
</div>
const Page404 = () => (
<PageWrapper title="404: Page not found">
<Head>
<meta property="og:type" content="article" key="type" />
<meta
property="og:description"
content="There's nothing here. If you followed a link to get here, that link is broken"
key="description"
/>
<meta property="og:article:author" content="Joost De Cock" key="author" />
<meta property="og:image" content={`https://freesewing.dev/og/404/og.png`} key="image" />
<meta property="og:image:type" content="image/png" />
<meta property="og:image:width" content="1200" />
<meta property="og:image:height" content="630" />
<meta property="og:url" content={`https://freesewing.dev/`} key="url" />
<meta property="og:locale" content="en_US" key="locale" />
<meta property="og:site_name" content="freesewing.dev" key="site" />
</Head>
<BaseLayout>
<BaseLayoutLeft>
<MainSections />
<NavLinks />
</BaseLayoutLeft>
<BaseLayoutWide>
<div className="max-w-2xl">
<h1>404: Page not found</h1>
<div className="max-w-sm m-auto px-12 mb-4">
<Robot embed pose="fail" />
</div>
</BaseLayoutWide>
</BaseLayout>
</PageWrapper>
)
}
<h3>We could not find what you are looking for</h3>
<div className="text-left">
<Popout comment by="joost">
<h5>Did you arrive here from a link?</h5>
<p>In that case, that link is broken.</p>
<p>
If it was one of our links, please <PageLink href="/contact" txt="let us know" /> so
we can fix it.
</p>
</Popout>
</div>
</div>
</BaseLayoutWide>
</BaseLayout>
</PageWrapper>
)
export default Page404
export async function getStaticProps() {
return {
props: {
...(await serverSideTranslations('en', namespaces)),
page: {
path: ['404'],
},
},
}
}

View file

@ -1,10 +1,9 @@
// Used in static paths
import { mdxPaths } from 'site/prebuild/mdx-paths.en.mjs'
import { pages } from 'site/prebuild/docs.en.mjs'
// Dependencies
import { serverSideTranslations } from 'next-i18next/serverSideTranslations'
// Hooks
import { useState, useEffect } from 'react'
import { useNavigation } from 'site/hooks/use-navigation.mjs'
// Components
import Head from 'next/head'
import { PageWrapper, ns } from 'shared/components/wrappers/page.mjs'
@ -31,11 +30,6 @@ import {
const DocsPage = ({ page, slug }) => {
const [frontmatter, setFrontmatter] = useState({ title: 'FreeSewing.dev' })
const [MDX, setMDX] = useState(<Spinner />)
/*
* Get the siteNav object from the useNavigation hook
* FIXME: ignorecontrol is not yet implmented here
*/
const { siteNav } = useNavigation({ ignoreControl: true })
/* Load MDX dynamically */
useEffect(() => {
@ -71,12 +65,12 @@ const DocsPage = ({ page, slug }) => {
</Head>
<BaseLayout>
<BaseLayoutLeft>
<MainSections {...{ siteNav, slug }} />
<NavLinks {...{ siteNav, slug }} />
<MainSections />
<NavLinks />
</BaseLayoutLeft>
<BaseLayoutProse>
<div className="w-full">
<Breadcrumbs {...{ siteNav, slug }} />
<Breadcrumbs />
<h1 className="break-words searchme">{frontmatter.title}</h1>
<div className="block xl:hidden">
<Toc toc={frontmatter.toc} wrap />
@ -126,7 +120,7 @@ export async function getStaticProps({ params }) {
*/
export async function getStaticPaths() {
return {
paths: mdxPaths.map((slug) => '/' + slug),
paths: Object.keys(pages).map((slug) => '/' + slug),
fallback: false,
}
}

View file

@ -1,8 +1,6 @@
// Dependencies
import { serverSideTranslations } from 'next-i18next/serverSideTranslations'
import { freeSewingConfig } from 'shared/config/freesewing.config.mjs'
// Hooks
import { useNavigation } from 'site/hooks/use-navigation.mjs'
// Components
import Head from 'next/head'
import { PageWrapper } from 'shared/components/wrappers/page.mjs'
@ -17,13 +15,8 @@ import {
BaseLayoutRight,
} from 'shared/components/base-layout.mjs'
const ContactPage = ({ page, slug }) => {
/*
* Get the siteNav object from the useNavigation hook
* FIXME: ignorecontrol is not yet implmented here
*/
const { siteNav } = useNavigation({ ignoreControl: true })
const title = siteNav.about.t
const ContactPage = ({ page }) => {
const title = 'About FreeSewing'
return (
<PageWrapper {...page}>
@ -45,12 +38,12 @@ const ContactPage = ({ page, slug }) => {
</Head>
<BaseLayout>
<BaseLayoutLeft>
<MainSections {...{ siteNav, slug }} />
<NavLinks {...{ siteNav, slug }} />
<MainSections />
<NavLinks />
</BaseLayoutLeft>
<BaseLayoutProse>
<div className="w-full">
<Breadcrumbs {...{ siteNav, slug }} />
<Breadcrumbs />
<h1 className="break-words searchme">{title}</h1>
</div>
<div className="mdx max-w-prose">
@ -187,7 +180,6 @@ export async function getStaticProps() {
return {
props: {
...(await serverSideTranslations('en')),
slug: 'about',
page: {
path: ['about'],
},

View file

@ -1,9 +1,7 @@
// Dependencies
import { serverSideTranslations } from 'next-i18next/serverSideTranslations'
// Hooks
import { useNavigation } from 'site/hooks/use-navigation.mjs'
// Components
import { PageWrapper } from 'shared/components/wrappers/page.mjs'
import { PageWrapper, ns as pageNs } from 'shared/components/wrappers/page.mjs'
import { Search } from 'site/components/search.mjs'
import { Popout } from 'shared/components/popout.mjs'
import { PageLink } from 'shared/components/page-link.mjs'
@ -15,13 +13,10 @@ import {
BaseLayoutRight,
} from 'shared/components/base-layout.mjs'
const SearchPage = ({ page, slug }) => {
/*
* Get the siteNav object from the useNavigation hook
* FIXME: ignorecontrol is not yet implmented here
*/
const { siteNav } = useNavigation({ ignoreControl: true })
const title = siteNav.about.t
const namespaces = [...pageNs]
const SearchPage = ({ page }) => {
const title = 'Search'
const tip = (
<Popout tip compact>
@ -33,12 +28,12 @@ const SearchPage = ({ page, slug }) => {
<PageWrapper {...page}>
<BaseLayout>
<BaseLayoutLeft>
<MainSections {...{ siteNav, slug }} />
<NavLinks {...{ siteNav, slug }} />
<MainSections />
<NavLinks />
</BaseLayoutLeft>
<BaseLayoutProse>
<div className="w-full">
<Breadcrumbs {...{ siteNav, slug }} />
<Breadcrumbs />
<h1 className="break-words searchme">{title}</h1>
<div className="block xl:hidden">{tip}</div>
</div>
@ -55,8 +50,7 @@ export default SearchPage
export async function getStaticProps() {
return {
props: {
...(await serverSideTranslations('en')),
slug: 'search',
...(await serverSideTranslations('en', namespaces)),
page: {
path: ['search'],
},

93
sites/dev/prebuild.mjs Normal file
View file

@ -0,0 +1,93 @@
import { prebuildRunner } from '../shared/prebuild/runner.mjs'
/*
* This handles the prebuild step for FreeSewing.dev
* It runs via an NPM run script, so in a pure NodeJS context
*/
prebuildRunner({
/*
* Pass the site to the runner
*/
site: 'dev',
/*
* This prebuild config determines which prebuild step to run
* For each step, the options are:
*
* - true: Always run
* - false: Never run
* - 'productionOnly': Run in production, mock or skip in develop to save time
*
*/
prebuild: {
// ALWAYS PREBUILD ////////////////////////////////////////////////////////
/*
* Always prebuild the designs
*/
designs: true,
/*
* Always prebuild the MDX documentation
*/
docs: true,
/*
* Always prebuild the translation files
* Even if we only support English on FreeSewing.dev,
* we still rely on the (English) translation of strings
*/
i18n: true,
/*
* Always prebuild the navigation object (sitenav) and slug lookup tables (sluglut)
*/
navigation: true,
// PREBUILD IN PRUDUCTION - MOCK/SKIP IN DEV ///////////////////////////////
/*
* Only prebuild the contributor info in production
* Will be mocked in development mode to save time
*/
contributors: 'productionOnly',
/*
* Only prebuild the crowdin info (translation statistics) in production
* Will be mocked in development mode to save time
*/
crowdin: 'productionOnly',
/*
* Only prebuild the favicon files in production
* Will be mocked in development mode to save time
*/
favicon: 'productionOnly',
/*
* Only prebuild the git author info in production
* Will be mocked in development mode to save time
*/
git: 'productionOnly',
/*
* Only prebuild the Open Graph (og) images in production
* Will be skipped in development mode to save time
*/
ogImages: 'productionOnly',
/*
* Only prebuild the patron info in production
* Will be mocked in development mode to save time
*/
patrons: 'productionOnly',
// NEVER PREBUILD //////////////////////////////////////////////////////////
/*
* Never prebuild the MDX posts because there are no such posts on FreeSewing.dev
* We could have leave this step out, but it's included here for documenation purposes
*/
posts: false,
},
})

View file

@ -23,7 +23,8 @@
"e2e": "yarn playwright test",
"lint": "next lint",
"buildsitedeps": "cd ../../ && yarn buildall && cd -",
"prebuild": "yarn buildsitedeps && SITE=lab node --experimental-json-modules ../shared/prebuild/index.mjs",
"prebuild": "yarn buildsitedeps && node --experimental-json-modules ./prebuild.mjs",
"prebuildonly": "node --experimental-json-modules ./prebuild.mjs",
"start": "yarn prebuild && yarn dev"
},
"peerDependencies": {},

View file

@ -1,36 +1,24 @@
// Hooks
import { useNavigation } from 'site/hooks/use-navigation.mjs'
// Components
import { BaseLayout, BaseLayoutLeft, BaseLayoutWide } from 'shared/components/base-layout.mjs'
import { NavLinks, Breadcrumbs, MainSections } from 'shared/components/navigation/sitenav.mjs'
export const ns = [] //navNs
export const ns = []
export const DefaultLayout = ({ children = [], slug, pageTitle = false }) => {
const { siteNav } = useNavigation({ ignoreControl: true })
export const DefaultLayout = ({ children = [], pageTitle = false }) => (
<BaseLayout>
<BaseLayoutLeft>
<MainSections />
<NavLinks />
</BaseLayoutLeft>
return (
<BaseLayout>
<BaseLayoutLeft>
{slug ? (
<>
<MainSections {...{ siteNav, slug }} />
<NavLinks {...{ siteNav, slug }} />
</>
) : (
<p>Slug not passed to layout</p>
)}
</BaseLayoutLeft>
<BaseLayoutWide>
{pageTitle && (
<div className="xl:pl-4">
{slug && <Breadcrumbs {...{ siteNav, slug }} />}
<h1 className="break-words">{pageTitle}</h1>
</div>
)}
<div className="xl:pl-4">{children}</div>
</BaseLayoutWide>
</BaseLayout>
)
}
<BaseLayoutWide>
{pageTitle && (
<div className="xl:pl-4">
<Breadcrumbs />
<h1 className="break-words">{pageTitle}</h1>
</div>
)}
<div className="xl:pl-4">{children}</div>
</BaseLayoutWide>
</BaseLayout>
)

View file

@ -1,5 +1,6 @@
import { NavigationContext } from 'shared/context/navigation-context.mjs'
// Hooks
import { useNavigation } from 'site/hooks/use-navigation.mjs'
import { useContext } from 'react'
// Components
import Head from 'next/head'
import {
@ -45,21 +46,21 @@ export const FrontmatterHead = ({ frontmatter, slug, locale }) => (
</Head>
)
export const DocsLayout = ({ children = [], slug, frontmatter, locale }) => {
const { siteNav } = useNavigation({ ignoreControl: true })
export const DocsLayout = ({ children = [], frontmatter }) => {
const { slug, locale } = useContext(NavigationContext)
return (
<>
<FrontmatterHead {...{ frontmatter, slug, locale }} />
<BaseLayout>
<BaseLayoutLeft>
<MainSections {...{ siteNav, slug }} />
<NavLinks {...{ siteNav, slug }} />
<MainSections />
<NavLinks />
</BaseLayoutLeft>
<BaseLayoutProse>
<div className="w-full">
<Breadcrumbs {...{ siteNav, slug }} />
<Breadcrumbs />
<h1 className="break-words searchme">{frontmatter.title}</h1>
<div className="block xl:hidden">
<Toc toc={frontmatter.toc} wrap />

View file

@ -1,5 +1,3 @@
// Hooks
import { useNavigation } from 'site/hooks/use-navigation.mjs'
// Components
import { FrontmatterHead } from './docs.mjs'
import {
@ -17,40 +15,36 @@ import {
import { Toc } from 'shared/components/mdx/toc.mjs'
import { PrevNext } from 'shared/components/prev-next.mjs'
export const ns = [navNs, 'docs'] //navNs
export const ns = [navNs, 'docs']
const isEndSlug = (slug) => slug.split('/').length === 1
export const PostLayout = ({ children = [], slug, frontmatter, locale }) => {
const { siteNav } = useNavigation({ ignoreControl: true })
export const PostLayout = ({ children = [], slug, frontmatter, locale }) => (
<>
<FrontmatterHead {...{ frontmatter, slug, locale }} />
<BaseLayout>
<BaseLayoutLeft>
<MainSections />
<NavLinks />
</BaseLayoutLeft>
return (
<>
<FrontmatterHead {...{ frontmatter, slug, locale }} />
<BaseLayout>
<BaseLayoutLeft>
<MainSections {...{ siteNav, slug }} />
<NavLinks {...{ siteNav, slug }} />
</BaseLayoutLeft>
<BaseLayoutProse>
<div className="w-full">
<Breadcrumbs {...{ siteNav, slug }} />
<h1 className="break-words searchme">{frontmatter.title}</h1>
<div className="block xl:hidden">
<Toc toc={frontmatter.toc} wrap />
</div>
</div>
{children}
<PrevNext slug={slug} noPrev={isEndSlug} noNext={isEndSlug} />
</BaseLayoutProse>
<BaseLayoutRight>
<div className="hidden xl:block">
<BaseLayoutProse>
<div className="w-full">
<Breadcrumbs />
<h1 className="break-words searchme">{frontmatter.title}</h1>
<div className="block xl:hidden">
<Toc toc={frontmatter.toc} wrap />
</div>
</BaseLayoutRight>
</BaseLayout>
</>
)
}
</div>
{children}
<PrevNext noPrev={isEndSlug} noNext={isEndSlug} />
</BaseLayoutProse>
<BaseLayoutRight>
<div className="hidden xl:block">
<Toc toc={frontmatter.toc} wrap />
</div>
</BaseLayoutRight>
</BaseLayout>
</>
)

View file

@ -1,22 +1,23 @@
import { localePath } from 'shared/utils.mjs'
const preGenerate = 6
export const numPerPage = 12
import { siteConfig as config } from 'site/site.config.mjs'
export const getPostSlugPaths = (order) => {
export const getPostSlugPaths = (posts) => {
const paths = []
for (const lang in order) {
for (let i = 0; i < preGenerate; i++) {
paths.push(localePath(lang, `${order[lang][i]}`))
}
for (const lang in posts) {
paths.push(
...Object.keys(posts[lang])
.slice(0, config.posts.preGenerate)
.map((slug) => localePath(lang, slug))
)
}
return paths
}
export const getPostIndexPaths = (order, type) => {
export const getPostIndexPaths = (posts, type) => {
const paths = []
for (const language in order) {
for (const language in posts) {
paths.push(localePath(language, `${type}/page/1`))
paths.push(localePath(language, `${type}/page/2`))
}
@ -24,13 +25,18 @@ export const getPostIndexPaths = (order, type) => {
return paths
}
export const getPostIndexProps = (locale, params, order, postInfo) => {
const pageNum = parseInt(params.page)
const numLocPages = Math.ceil(order[locale].length / numPerPage)
export const getPostIndexProps = (pagenr, posts, meta) => {
const pageNum = parseInt(pagenr)
const numLocPages = Math.ceil(Object.keys(posts).length / config.posts.perPage)
if (pageNum > numLocPages) return false
const postSlugs = order[locale].slice(numPerPage * (pageNum - 1), numPerPage * pageNum)
const posts = postSlugs.map((s) => ({ ...postInfo[locale][s], s }))
const pagePosts = Object.entries(posts)
.slice(config.posts.perPage * (pageNum - 1), config.posts.perPage * pageNum)
.map(([slug, post]) => ({
s: slug,
...post,
...meta[slug],
}))
return { posts, current: pageNum, total: numLocPages }
return { posts: pagePosts, current: pageNum, total: numLocPages }
}

View file

@ -1,219 +0,0 @@
import { prebuildNavigation as pbn } from 'site/prebuild/navigation.mjs'
import { useTranslation } from 'next-i18next'
import { freeSewingConfig as conf } from 'shared/config/freesewing.config.mjs'
import { useAccount } from 'shared/hooks/use-account.mjs'
import { designs, tags } from 'shared/config/designs.mjs'
import { objUpdate } from 'shared/utils.mjs'
import { orderedSlugLut } from 'shared/hooks/use-navigation-helpers.mjs'
import { useRouter } from 'next/router'
import { useMemo } from 'react'
/*
* prebuildNavvigation[locale] holds the navigation structure based on MDX content.
* The entire website only has a few pages that are now MDX-based:
* - 404 => no navigation shown
* - home page => no navvigation shown
* - /contact => Added below
*
* Remember Mc_Shifton:
* Note: Set 'm' to truthy to show this as a main section in the side-navigation (optional)
* Note: Set 'c' to set the control level to hide things from users (optional)
* Note: Set 's' to the slug (optional insofar as it's not a real page (a spacer for the header))
* Note: Set '_' to never show the page in the site navigation (like the tags pages)
* Note: Set 'h' to indicate this is a top-level page that should be hidden from the side-nav (like search)
* Note: Set 'i' when something should be included as top-level in the collapse side-navigation (optional)
* Note: Set 'f' to add the page to the footer
* Note: Set 't' to the title
* Note: Set 'o' to set the order (optional)
* Note: Set 'n' to mark this as a noisy entry that should always be closed unless active (like blog)
*/
export const ns = ['account', 'sections', 'design', 'tags', 'designs']
const sitePages = (t = false, control = 99) => {
// Handle t not being present
if (!t) t = (string) => string
const pages = {
// Top-level pages that are the sections menu
designs: {
m: 1,
s: 'designs',
t: t('sections:designs'),
n: 1,
tags: {
_: 1,
s: 'designs/tags',
t: t('design:tags'),
o: 'aaa',
},
},
patterns: {
m: 1,
s: 'patterns',
t: t('sections:patterns'),
},
sets: {
m: 1,
s: 'sets',
t: t('sections:sets'),
},
community: {
m: 1,
s: 'community',
t: t('sections:community'),
},
account: {
m: 1,
s: 'account',
t: t('sections:account'),
n: 1,
},
// Top-level pages that are not in the sections menu
apikeys: {
_: 1,
s: 'apikeys',
h: 1,
t: t('apikeys'),
},
curate: {
s: 'curate',
h: 1,
t: t('curate'),
sets: {
t: t('curateSets'),
s: 'curate/sets',
},
},
new: {
m: 1,
s: 'new',
h: 1,
t: t('sections:new'),
pattern: {
t: t('patternNew'),
s: 'new/pattern',
o: 10,
},
set: {
t: t('newSet'),
s: 'new/set',
0: 20,
},
},
profile: {
s: 'profile',
h: 1,
t: t('yourProfile'),
},
translation: {
s: 'translation',
h: 1,
t: t('translation'),
join: {
t: t('translation:joinATranslationTeam'),
s: 'translation',
},
'suggest-language': {
t: t('translation:suggestLanguage'),
s: 'translation',
},
},
sitemap: {
s: 'sitemap',
h: 1,
t: t('sitemap'),
},
// Not translated, this is a developer page
typography: {
s: 'typography',
h: 1,
t: 'Typography',
},
}
for (const section in conf.account.fields) {
for (const [field, controlScore] of Object.entries(conf.account.fields[section])) {
if (Number(control) >= controlScore)
pages.account[field] = {
s: `account/${field}`,
t: t(`account:${field}`),
}
}
}
if (Number(control) >= conf.account.fields.developer.apikeys)
pages.new.apikey = {
s: 'new/apikey',
t: t('newApikey'),
o: 30,
}
pages.account.reload = {
s: `account/reload`,
t: t(`account:reload`),
}
for (const design in designs) {
// pages.designs[design] = {
// t: t(`designs:${design}.t`),
// s: `designs/${design}`,
// }
pages.new.pattern[design] = {
s: `new/${design}`,
t: t(`account:generateANewThing`, { thing: t(`designs:${design}.t`) }),
}
}
for (const tag of tags) {
pages.designs.tags[tag] = {
s: `designs/tags/${tag}`,
t: t(`tags:${tag}`),
}
}
return pages
}
export const useNavigation = (param = {}, extra = []) => {
const { ignoreControl = false } = param
// Passing in the locale is not very DRY so let's just grab it from the router
const { locale } = useRouter()
// We need translation
const { t } = useTranslation(ns)
// We need the account if we want to take control into account
const { account } = useAccount()
const control = ignoreControl ? undefined : account.control
const value = useMemo(() => {
const siteNav = {
...pbn[locale],
...sitePages(t, control),
}
for (const [_path, _data] of extra) {
objUpdate(siteNav, _path, _data)
}
// Apply some tweaks
siteNav.blog.m = 1
siteNav.blog.n = 1
siteNav.showcase.m = 1
siteNav.showcase.n = 1
siteNav.docs.m = 1
siteNav.newsletter._ = true
// Set order on main sections
siteNav.designs.o = 10
siteNav.docs.o = 20
siteNav.blog.o = 30
siteNav.showcase.o = 40
siteNav.community.o = 50
siteNav.patterns.o = 60
siteNav.sets.o = 70
siteNav.account.o = 80
siteNav.new.o = 90
return {
siteNav, // Site navigation
slugLut: orderedSlugLut(siteNav), // Slug lookup table
}
}, [locale, extra, control])
return value
}

View file

@ -17,13 +17,14 @@
"build": "next build",
"cibuild": "yarn build",
"clean": "rimraf prebuild/* && rimraf public/locales/*/* && rimraf public/feeds/* && rimraf ../shared/prebuild/data/*",
"predev": "FAST=1 SITE=org node --experimental-json-modules ../shared/prebuild/index.mjs",
"dev": "next dev -p 8000",
"develop": "next dev -p 8000",
"i18n": "SITE=org node ../shared/prebuild/i18n-only.mjs",
"lint": "next lint",
"buildsitedeps": "cd ../../ && yarn buildall && cd -",
"prebuild": "yarn buildsitedeps && SITE=org node --experimental-json-modules ../shared/prebuild/index.mjs",
"prebuild": "yarn buildsitedeps && node --experimental-json-modules ./prebuild.mjs",
"prebuildonly": "node --experimental-json-modules ./prebuild.mjs",
"predev": "node --experimental-json-modules ./prebuild.mjs",
"start": "yarn prebuild && yarn dev"
},
"peerDependencies": {},

View file

@ -1,4 +1,4 @@
import { order } from 'site/prebuild/blog-paths.mjs'
import { pages as posts } from 'site/prebuild/blog.mjs'
import { getPostSlugPaths } from 'site/components/mdx/posts/utils.mjs'
import { serverSideTranslations } from 'next-i18next/serverSideTranslations'
import { useDynamicMdx } from 'shared/hooks/use-dynamic-mdx.mjs'
@ -68,7 +68,7 @@ export async function getStaticProps({ params, locale }) {
export const getStaticPaths = async () => {
return {
paths: getPostSlugPaths(order),
paths: getPostSlugPaths(posts),
fallback: 'blocking',
}
}

View file

@ -1,6 +1,7 @@
// Dependencies
import { serverSideTranslations } from 'next-i18next/serverSideTranslations'
import { postInfo, order } from 'site/prebuild/blog-paths.mjs'
import { pages as posts } from 'site/prebuild/blog.mjs'
import { meta } from 'site/prebuild/blog-meta.mjs'
import { getPostIndexPaths, getPostIndexProps } from 'site/components/mdx/posts/utils.mjs'
// Hooks
import { useTranslation } from 'next-i18next'
@ -91,7 +92,7 @@ const BlogIndexPage = ({ posts, page, current, total }) => {
export default BlogIndexPage
export async function getStaticProps({ locale, params }) {
const props = getPostIndexProps(locale, params, order, postInfo)
const props = getPostIndexProps(params.page, posts[locale], meta)
if (props === false) return { notFound: true }
@ -110,7 +111,7 @@ export async function getStaticProps({ locale, params }) {
export const getStaticPaths = async () => {
return {
paths: getPostIndexPaths(order, 'blog'),
paths: getPostIndexPaths(posts, 'blog'),
fallback: 'blocking',
}
}

View file

@ -1,5 +1,5 @@
// Used in static paths
import { mdxPaths } from 'site/prebuild/mdx-paths.en.mjs'
import { pages } from 'site/prebuild/docs.en.mjs'
// Dependencies
import { serverSideTranslations } from 'next-i18next/serverSideTranslations'
// Hooks
@ -85,7 +85,7 @@ export async function getStaticProps({ locale, params }) {
* To learn more, see: https://nextjs.org/docs/basic-features/data-fetching
*/
export async function getStaticPaths() {
const somePaths = mdxPaths
const somePaths = Object.keys(pages)
.filter((path) => path.split('/').length < 5)
.filter((path) => path !== 'docs')
@ -96,6 +96,7 @@ export async function getStaticPaths() {
...somePaths.map((key) => `/de/${key}`),
...somePaths.map((key) => `/fr/${key}`),
...somePaths.map((key) => `/nl/${key}`),
...somePaths.map((key) => `/uk/${key}`),
],
fallback: 'blocking',
}

View file

@ -5,9 +5,8 @@ import { useTranslation } from 'next-i18next'
// Components
import { PageWrapper, ns as pageNs } from 'shared/components/wrappers/page.mjs'
import { ReadMore } from 'shared/components/mdx/read-more.mjs'
import { ns as navNs } from 'site/hooks/use-navigation.mjs'
const ns = [...pageNs, navNs, 'common']
const ns = [...pageNs, 'common']
const SitemapPage = ({ page }) => {
const { t } = useTranslation(ns)

90
sites/org/prebuild.mjs Normal file
View file

@ -0,0 +1,90 @@
import { prebuildRunner } from '../shared/prebuild/runner.mjs'
/*
* This handles the prebuild step for FreeSewing.org
* It runs via an NPM run script, so in a pure NodeJS context
*/
prebuildRunner({
/*
* Pass the site to the runner
*/
site: 'org',
/*
* This prebuild config determines which prebuild step to run
* For each step, the options are:
*
* - true: Always run
* - false: Never run
* - 'productionOnly': Run in production, mock or skip in develop to save time
*
*/
prebuild: {
// ALWAYS PREBUILD ////////////////////////////////////////////////////////
/*
* Always prebuild the designs
*/
designs: true,
/*
* Always prebuild the MDX documentation
*/
docs: true,
/*
* Always prebuild the MDX posts
*/
posts: true,
/*
* Always prebuild the translation files
* Even if we only support English on FreeSewing.dev,
* we still rely on the (English) translation of strings
*/
i18n: true,
/*
* Always prebuild the navigation object (sitenav) and slug lookup tables (sluglut)
*/
navigation: true,
// PREBUILD IN PRUDUCTION - MOCK/SKIP IN DEV ///////////////////////////////
/*
* Only prebuild the contributor info in production
* Will be mocked in development mode to save time
*/
contributors: 'productionOnly',
/*
* Only prebuild the crowdin info (translation statistics) in production
* Will be mocked in development mode to save time
*/
crowdin: 'productionOnly',
/*
* Only prebuild the favicon files in production
* Will be mocked in development mode to save time
*/
favicon: 'productionOnly',
/*
* Only prebuild the git author info in production
* Will be mocked in development mode to save time
*/
git: 'productionOnly',
/*
* Only prebuild the Open Graph (og) images in production
* Will be skipped in development mode to save time
*/
ogImages: 'productionOnly',
/*
* Only prebuild the patron info in production
* Will be mocked in development mode to save time
*/
patrons: 'productionOnly',
},
})

View file

@ -20,4 +20,8 @@ export const siteConfig = {
languagesWip: [],
site: 'FreeSewing.org',
tld: 'org',
posts: {
preGenerate: 6,
perPage: 12,
},
}

View file

@ -2,7 +2,7 @@
* The default full-page FreeSewing layout
*/
export const BaseLayout = ({ children = [] }) => (
<div className="flex flex-row items-start mt-8 w-full justify-between 2xl:px-36 xl:px-12 px-4">
<div className="flex flex-row items-start mt-8 w-full justify-between 2xl:px-36 xl:px-12 px-4 gap-0 lg:gap-4 xl:gap-8 3xl: gap-12">
{children}
</div>
)

View file

@ -4,7 +4,7 @@ import { Difficulty } from 'shared/components/designs/difficulty.mjs'
import { designs } from 'shared/config/designs.mjs'
import { DesignTag } from 'shared/components/designs/tag.mjs'
export const ns = ['design', 'designs', 'tags']
export const ns = ['designs', 'tags']
const defaultLink = (design) => `/new/${design}`
@ -27,7 +27,7 @@ export const Design = ({ name, hrefBuilder = false }) => {
<h5 className="flex flex-row items-center justify-between w-full">
<span>{t(`designs:${name}.t`)}</span>
<span className="flex flex-col items-end">
<span className="text-xs font-medium opacity-70">{t('design:difficulty')}</span>
<span className="text-xs font-medium opacity-70">{t('tags:difficulty')}</span>
<Difficulty score={designs[name].difficulty} />
</span>
</h5>

View file

@ -1,7 +1,8 @@
// Dependencies
import orderBy from 'lodash.orderby'
import { NavigationContext } from 'shared/context/navigation-context.mjs'
// Hooks
import { useNavigation } from 'site/hooks/use-navigation.mjs'
import { useContext } from 'react'
// Components
import Link from 'next/link'
import { Ribbon } from 'shared/components/ribbon.mjs'
@ -14,7 +15,8 @@ export const ns = ['footer', ...sponsorsNs]
const onlyFooterLinks = (tree) => orderBy(tree, ['t'], ['asc']).filter((entry) => entry.f)
export const Footer = () => {
const { siteNav } = useNavigation({ ignoreControl: true })
// Grab siteNav from the navigation context
const { siteNav } = useContext(NavigationContext)
return (
<footer className="bg-neutral">

View file

@ -1,5 +1,6 @@
import { useContext } from 'react'
import { NavigationContext } from 'shared/context/navigation-context.mjs'
import get from 'lodash.get'
import { useNavigation } from 'site/hooks/use-navigation.mjs'
import Link from 'next/link'
const getPage = {
@ -13,7 +14,7 @@ const getPage = {
}
export const DocsTitle = ({ slug, className = '', site = 'org' }) => {
const { siteNav } = useNavigation()
const { siteNav } = useContext(NavigationContext)
const page = getPage[site](slug, siteNav)
return page ? <span className={className}>{page.t}</span> : null

View file

@ -93,7 +93,6 @@ export const MdxMetaData = ({ frontmatter, locale, slug }) => {
className="btn btn-success flex flex-row justify-between items-center w-full px-4 bg-gradient-to-r from-primary to-accent mb-4 hover:from-accent hover:to-accent"
>
<EditIcon />
<span>Found a mistake?</span>
<span>{t('editThisPage')}</span>
</a>
<div

View file

@ -2,7 +2,6 @@ import get from 'lodash.get'
import Link from 'next/link'
import { useContext } from 'react'
import { NavigationContext } from 'shared/context/navigation-context.mjs'
import { useNavigation } from 'site/hooks/use-navigation.mjs'
import { BulletIcon, RightIcon } from 'shared/components/icons.mjs'
import { pageHasChildren } from 'shared/utils.mjs'
import orderBy from 'lodash.orderby'
@ -70,15 +69,8 @@ const RenderTree = ({ tree, recurse, depth = 1, level = 0 }) => {
)
}
export const ReadMore = ({
recurse = 0,
root = false,
site = 'org',
depth = 99,
ignoreControl,
}) => {
const { slug } = useContext(NavigationContext)
const { siteNav } = useNavigation({ ignoreControl })
export const ReadMore = ({ recurse = 0, root = false, site = 'org', depth = 99 }) => {
const { siteNav, slug } = useContext(NavigationContext)
// Deal with recurse not being a number
if (recurse && recurse !== true) {

View file

@ -1,3 +1,5 @@
import { useContext } from 'react'
import { NavigationContext } from 'shared/context/navigation-context.mjs'
import Link from 'next/link'
import { pageHasChildren, isSlugPart } from 'shared/utils.mjs'
import get from 'lodash.get'
@ -130,10 +132,12 @@ const MainLink = ({
/*
* A React component to render breadcrumbs to the current page
*
* @param slug {string} - The slug of the current page
* @param siteNav {object} - The site navigation object as returned by the useNavigation hook
* @param lead {string} - A lead to display before the cumbs (eg: You are here)
*/
export const Breadcrumbs = ({ slug = false, lead = false, siteNav }) => {
export const Breadcrumbs = ({ lead = false }) => {
// Grab siteNav and slug from the navigation context
const { siteNav, slug } = useContext(NavigationContext)
const { t } = useTranslation(['common'])
if (slug === false) {
@ -193,33 +197,34 @@ export const Breadcrumbs = ({ slug = false, lead = false, siteNav }) => {
/*
* A React component to render sidebar navigation based on the siteNav object and current slug
*
* The main sections are determined in the use-navigation hook.
* The main sections are determined in the navigation prebuild code.
* We always display the navigation as:
* - Always show all top-level entries
* - Always show all direct children of all top-level entries (this allows for better discoverability)
* - If we're deeper down, only expand the active page
*
* @param slug {string} - The slug of the current page
* @param siteNav {object} - The siteNav object from the useNavigation hook
*/
export const NavLinks = ({ slug, siteNav }) => (
<ul className="w-full list mb-8 mt-3">
{onlyValidChildren(siteNav).map((page, i) => (
<li key={i} className="w-full">
<MainLink s={page.s} t={page.t} slug={slug} />
{pageHasChildren(page) && !page.n && <Section {...{ tree: page, slug }} />}
</li>
))}
</ul>
)
export const NavLinks = () => {
// Grab siteNav and slug from the navigation context
const { siteNav, slug } = useContext(NavigationContext)
return (
<ul className="w-full list mb-8 mt-3">
{onlyValidChildren(siteNav).map((page, i) => (
<li key={i} className="w-full">
<MainLink s={page.s} t={page.t} slug={slug} />
{pageHasChildren(page) && !page.n && <Section {...{ tree: page, slug }} />}
</li>
))}
</ul>
)
}
/*
* A React component to render sidebar navigation for the main sections
*
* @param siteNav {object} - The siteNav object from the useNavigation hook
* @param slug {string} - The slug of the current page
*/
export const MainSections = ({ siteNav, slug }) => {
export const MainSections = () => {
// Grab siteNav and slug from the navigation context
const { siteNav, slug } = useContext(NavigationContext)
const output = []
for (const page of onlyMainSections(siteNav)) {
const act = isSlugPart(page.s, slug)

View file

@ -1,7 +1,8 @@
// Dependencies
import get from 'lodash.get'
import { NavigationContext } from 'shared/context/navigation-context.mjs'
// Hooks
import { useNavigation } from 'site/hooks/use-navigation.mjs'
import { useContext } from 'react'
// Components
import Link from 'next/link'
import { LeftIcon, RightIcon } from 'shared/components/icons.mjs'
@ -39,9 +40,9 @@ const getItemWithCaveat = (index, slugLut, siteNav, shouldHide) => {
return get(siteNav, slugLut[index].split('/'))
}
export const PrevNext = ({ slug, noPrev = false, noNext = false }) => {
// Grab site navigation and slug lookup table from the useNavigatin hook
const { siteNav, slugLut } = useNavigation()
export const PrevNext = ({ noPrev = false, noNext = false }) => {
// Grab siteNav and slugLut from the navigation context
const { siteNav, slugLut, slug } = useContext(NavigationContext)
// Lookup the current slug in the LUT
const index = slugLut.indexOf(slug)

View file

@ -4,9 +4,9 @@
//import { designs } from 'shared/config/designs.mjs'
//import { DesignTag } from 'shared/components/designs/tag.mjs'
export const ns = ['design', 'designs', 'tags']
export const ns = ['designs', 'tags']
export const Set = ({ name }) => {
export const Set = () => {
//const { t } = useTranslation(ns)
return <p>fixme</p>
@ -21,7 +21,7 @@ export const Set = ({ name }) => {
<h5 className="flex flex-row items-center justify-between w-full">
<span>{t(`designs:${name}.t`)}</span>
<div className="flex flex-col items-end">
<span className="text-xs font-medium opacity-70">{t('design:difficulty')}</span>
<span className="text-xs font-medium opacity-70">{t('tags:difficulty')}</span>
<Difficulty score={designs[name].difficulty} />
</div>
</h5>

View file

@ -1,6 +1,5 @@
// Dependencies
import React, { useState, useEffect, useContext } from 'react'
//import { useHotkeys } from 'react-hotkeys-hook'
// Hooks
import { useTheme } from 'shared/hooks/use-theme.mjs'
// Components
@ -10,7 +9,6 @@ import { LayoutWrapper, ns as layoutNs } from 'shared/components/wrappers/layout
import { DefaultLayout, ns as defaultLayoutNs } from 'site/components/layouts/default.mjs'
import { Feeds } from 'site/components/feeds.mjs'
import { ModalContext } from 'shared/context/modal-context.mjs'
import { NavigationContext } from 'shared/context/navigation-context.mjs'
export const ns = [...new Set([...layoutNs, ...defaultLayoutNs])]
@ -19,14 +17,7 @@ export const PageWrapper = (props) => {
/*
* Deconstruct props
*/
const {
layout = DefaultLayout,
footer = true,
header = false,
children = [],
path = [],
locale = 'en',
} = props
const { layout = DefaultLayout, footer = true, header = false, children = [], path = [] } = props
// Title is typically set in props.t but check props.title too
const pageTitle = props.t ? props.t : props.title ? props.title : null
@ -40,7 +31,6 @@ export const PageWrapper = (props) => {
* Contexts
*/
const { modalContent } = useContext(ModalContext)
const { setNavigation } = useContext(NavigationContext)
/*
* This forces a re-render upon initial bootstrap of the app
@ -50,29 +40,6 @@ export const PageWrapper = (props) => {
const [currentTheme, setCurrentTheme] = useState()
useEffect(() => setCurrentTheme(theme), [currentTheme, theme])
/*
* Update navigation context with title and path
*/
useEffect(() => {
setNavigation({
title: pageTitle,
locale,
path,
})
}, [path, pageTitle, locale, setNavigation])
/*
* Hotkeys (keyboard actions)
*/
// Trigger search with /
//useHotkeys('/', (evt) => {
// evt.preventDefault()
// setSearch(true)
//})
// Search state
//const [search, setSearch] = useState(false)
// Helper object to pass props down (keeps things DRY)
const childProps = { footer, header, pageTitle, slug }

View file

@ -1,4 +1,8 @@
import allDesigns from 'config/software/designs.json'
/*
* Do not use the 'config' webpack alias here because
* this is used in the prebuild step which is pure NodeJS
*/
import allDesigns from '../../../config/software/designs.json' assert { type: 'json' }
/*
* Filter out utility patterns by checking whether they have any tags

View file

@ -1,4 +1,8 @@
import { social } from 'config/social.mjs'
/*
* Do not use the 'config' webpack alias here because
* this is used in the prebuild step which is pure NodeJS
*/
import { social } from '../../../config//social.mjs'
export const freeSewingConfig = {
monorepo: 'https://github.com/freesewing/freesewing',

View file

@ -1,6 +1,6 @@
import React, { useState } from 'react'
import { createContext, useState } from 'react'
export const ModalContext = React.createContext(null)
export const ModalContext = createContext(null)
export const ModalContextProvider = ({ children }) => {
function clearModal() {

View file

@ -1,80 +1,95 @@
import orderBy from 'lodash.orderby'
import get from 'lodash.get'
import React, { useState } from 'react'
import { useNavigation } from 'site/hooks/use-navigation.mjs'
import { createContext, useState } from 'react'
import { objUpdate } from 'shared/utils.mjs'
import { siteNav as defaultSiteNav } from 'site/prebuild/sitenav.mjs'
import { slugLut as defaultSlugLut } from 'site/prebuild/slugLut.mjs'
import { useRouter } from 'next/router'
const defaultNavigationContext = {
path: [],
title: 'FIXME: No title (default)',
locale: 'en',
crumbs: [],
}
export const NavigationContext = React.createContext(defaultNavigationContext)
const createCrumbs = (path, nav) =>
path.map((crumb, i) => {
const slice = path.slice(0, i + 1)
const entry = get(nav, slice, { t: 'no-actual-title', s: slice.join('/') })
const val = { t: entry.t, s: entry.s }
if (entry.o) val.o = entry.o
return val
})
const createSections = (nav) => {
const sections = {}
for (const slug of Object.keys(nav)) {
const entry = nav[slug]
const val = { t: entry.t, s: entry.s }
if (entry.o) val.o = entry.o
if (!entry.h) sections[slug] = val
}
return orderBy(sections, ['o', 't'])
}
const buildNavState = (value, siteNav, extra = []) => {
for (const [path, data] of extra) {
siteNav = objUpdate(siteNav, path, data)
}
const obj = {
siteNav,
crumbs: createCrumbs(value.path, siteNav),
sections: createSections(siteNav),
slug: value.path.join('/'),
}
obj.title = obj.crumbs.length > 0 ? obj.crumbs.slice(-1)[0].t : ''
return obj
}
/*
* The context, which uses the default pre-build data
*/
export const NavigationContext = createContext({
siteNav: defaultSiteNav,
slugLut: defaultSlugLut,
})
/*
* The context provider which will pass the value down
*/
export const NavigationContextProvider = ({ children }) => {
function setNavigation(newValues) {
setValue({
...value,
...newValues,
setNavigation,
addPages,
})
/*
* Get the locale and slug from the Next's router object
*/
const { locale, asPath } = useRouter()
const slug = asPath.slice(1) // Strip the leading slash
/*
* Helper method to hot-update the siteNav object
* This object is created in the prebuild step and holds all site pages.
* However, sometimes we want to update it with user-generated pages such
* as individual patterns or measurments sets
*
* This uses objUpdate() from utils, which is just a wrapper around lodash.set
* that has additional functionality to unset/delete values.
*
* @param path {string} - Path to the value to update. See lodash.set for details.
* @param value {value} to set
*/
function updateSiteNav(path, value) {
setSiteNav(objUpdate(siteNav[locale], path, value))
}
const [value, setValue] = useState({
...defaultNavigationContext,
setNavigation,
})
const [extraPages, setExtraPages] = useState([])
const { siteNav } = useNavigation({ path: value.path, locale: value.locale }, extraPages)
const navState = buildNavState(value, siteNav)
const addPages = (extra) => {
setExtraPages([...extraPages, ...extra])
/*
* Helper method to update the slugLut
* This is a list of slugs of all the pages in the order one would expect them.
* This makes it easy to put a 'next page' or 'previous page' link as we just grab
* the next/previous entry on the list and lookup its title in the siteNav objectt.
*
* Currently not implemented: How do we update this for user-generated content?
* Perhaps something like: updateSlugLut('after', slug)
* We'll do this later. Not even certain it's needed as we may just not place
* previous/next links on user-generated content.
*/
function updateSlugLut() {
// FIXME: Is this even needed?
console.log('updateSlugLut is not implemented (yet)')
}
/*
* Helper method for when we want to update both the siteNav and slugLut object in one call
*/
function update(obj) {
if (obj.siteNav) updateSiteNav(...obj.siteNav)
if (obj.slugLut) updateSlugLut(...obj.slugLut)
}
/*
* Local state
* Remember that only a state of prop change will trigger a re-render.
* So if we want changes to the context to propogate throughout the components
* using the context, we need to make the context value itself a state value
* so that updating it will trigger a re-render and the propagated value will update.
*/
const [siteNav, setSiteNav] = useState(defaultSiteNav)
const [slugLut] = useState(defaultSlugLut)
/*
* Pass everything down as the value object, including the methods to update
* the state (which will in turn update the context value)
*
* Note that we're only passing down the siteNav object for the current locale
*/
return (
<NavigationContext.Provider value={{ ...value, ...navState }}>
<NavigationContext.Provider
value={{
siteNav: siteNav[locale],
slugLut,
slug,
locale,
updateSiteNav,
updateSlugLut,
update,
}}
>
{children}
</NavigationContext.Provider>
)

View file

@ -1,4 +1,4 @@
import { pageHasChildren } from 'shared/utils.mjs'
import { pageHasChildren } from '../utils.mjs'
import orderBy from 'lodash.orderby'
/*

View file

@ -1,3 +1,5 @@
difficulty: Difficulty
tags: Design tags
accessories: Accessories
bags: Bags
blocks: Blocks

View file

@ -1,25 +1,45 @@
import path from 'path'
import fs from 'fs'
import { readFile, writeFile } from 'node:fs/promises'
/*
* Main method that does what needs doing
* Prebuilds the list of all contributors
*/
export const prebuildContributors = async(site) => {
export const prebuildContributors = async (store, mock = false) => {
if (mock) return (store.contributors = mockedData)
// Say hi
console.log()
console.log(`Prebuilding contributor list for freesewing.${site}`)
/*
* Read from all-contributors configuration file
*/
const contributorsFile = await readFile(path.resolve('..', '..', '.all-contributorsrc'), 'utf-8')
// Read from rc file
const contributors = JSON.parse(fs.readFileSync(
path.resolve('..', '..', '.all-contributorsrc'),
'utf-8'
))
/*
* Parse as JSON and get contributors list
*/
const { contributors } = JSON.parse(contributorsFile)
// Write to json
fs.writeFileSync(
path.resolve('..', site, 'prebuild', `allcontributors.js`),
`export default ${JSON.stringify(contributors.contributors, null ,2)}`
/*
* Update the store
*/
store.contributors = contributors
/*
* Write out prebuild results
*/
return await writeFile(
path.resolve('..', store.site, 'prebuild', `allcontributors.js`),
`export default ${JSON.stringify(contributors, null, 2)}`
)
}
/*
* In development, we return this mocked data to speed things up
*/
const mockedData = [
{
login: 'joostdecock',
name: 'Joost De Cock',
avatar_url: 'https://avatars.githubusercontent.com/u/1708494?v=4',
profile: 'https://joost.at/',
contributions: ['maintenance'],
},
]

View file

@ -43,18 +43,17 @@ const sendApiRequest = async (url = '', body = false, download = false) => {
return false
}
//const loadProjectMembers = async () => await sendApiRequest('members?limit=100')
const loadTopMembers = async (languageId) =>
await sendApiRequest('reports?limit=500', { ...report, schema: { ...report.schema, languageId } })
const checkReportStatus = async (id) => await sendApiRequest(`reports/${id}`)
const getReportUrl = async (id) => await sendApiRequest(`reports/${id}/download`)
const downloadReport = async (url) => await sendApiRequest('', false, url)
export const prebuildCrowdin = async () => {
export const prebuildCrowdin = async (store, mock = false) => {
if (mock) return (store.crowdin = mockedData)
const contributions = {}
for (let language of languages) {
console.log(`Loading translator contributions for ${language}`)
contributions[language] = {}
const report = await loadTopMembers(language)
const id = report.identifier
@ -85,6 +84,19 @@ export const prebuildCrowdin = async () => {
path.resolve('..', 'org', 'prebuild', 'translators.json'),
JSON.stringify(contributions)
)
store.crowdin = contributions
return
}
//prebuildCrowdin()
/*
* In development, we return this mocked data to speed things up
*/
const mockedData = {
nl: { 'Joost De Cock (joostdecock)': { translated: 16427 } },
fr: { bret76: { translated: 36800 } },
de: { starf: { translated: 22370 } },
uk: { 'Morgan Frost (KaerMorhan)': { translated: 10505 } },
es: { 'Sara Latorre (Tyrannogina)': { translated: 6713 } },
}

View file

@ -17,7 +17,7 @@ async function loadDesign(design) {
return result
}
export const prebuildDesigns = async () => {
export const prebuildDesigns = async (store) => {
const promises = []
const designs = []
@ -36,6 +36,13 @@ export const prebuildDesigns = async () => {
options[design] = config.options
}
// Update the store
store.designs = {
designs,
options,
measurements,
}
// Write out prebuild files
const header =
'// This file is auto-generated by the prebuild script | Any changes will be overwritten\n'
@ -57,5 +64,5 @@ export const prebuildDesigns = async () => {
)
)
await Promise.all(promises)
return await Promise.all(promises)
}

View file

@ -1,177 +0,0 @@
import fs from 'fs'
import path from 'path'
import rdir from 'recursive-readdir'
import { unified } from 'unified'
import remarkParser from 'remark-parse'
import remarkCompiler from 'remark-stringify'
import remarkFrontmatter from 'remark-frontmatter'
import remarkFrontmatterExtractor from 'remark-extract-frontmatter'
import { readSync } from 'to-vfile'
import yaml from 'js-yaml'
import { mdIntro } from './md-intro.mjs'
// Some arbitrary future time
const future = new Date('10-12-2026').getTime()
export const header = `/*
*
* This page was auto-generated by the prebuild script
* Any changes you make to it will be lost on the next (pre)build.
*/
`
/*
* There's an issue in crowdin where it changes the frontmatter marker:
* ---
* into this:
* - - -
* which breaks stuff. So this method takes the input and replaces all
* - - - with ---
*/
export const fixCrowdinBugs = (md) => {
md.value = md.value.split('- - -\n').join('---\n')
return md
}
/*
* Helper method to get the title and meta data from an MDX file
*
* Parameters:
*
* - file: the full path to the file
*/
export const mdxMetaInfo = async (file) => {
let result
try {
result = await unified()
.use(remarkParser)
.use(remarkCompiler)
.use(remarkFrontmatter)
.use(remarkFrontmatterExtractor, { yaml: yaml.load })
.process(fixCrowdinBugs(readSync(file, { encoding: 'utf-8' })))
} catch (err) {
console.log(err)
}
return result
}
/*
* Helper method to get a list of MDX files in a folder.
* Will traverse recursively to get all files from a given root folder.
*
* Parameters:
*
* - folder: the root folder to look in
* - lang: the language files to looks for
*
* Exported because it's also used by the Algolia index script
*/
export const getMdxFileList = async (folder, lang) => {
let allFiles
try {
allFiles = await rdir(folder)
} catch (err) {
console.log(err)
return false
}
// Filter out all that's not a language-specific markdown file
// and avoid including the 'ui' files
const files = []
for (const file of allFiles) {
if (file.slice(-5) === `${lang}.md`) files.push(file)
}
return files.sort()
}
/*
* Helper method to get the website slug (path) from the file path
*/
export const fileToSlug = (file, site, lang) =>
file.slice(-6) === `/${lang}.md` ? file.split(`/markdown/${site}/`).pop().slice(0, -6) : false
export const loadMdxForPrebuild = async (site, folder, locales) => {
const pages = {}
// Loop over languages
for (const lang of locales) {
pages[lang] = {}
// Get list of filenames
const list = await getMdxFileList(folder, lang)
// Loop over files
for (const file of list) {
const slug = fileToSlug(file, site, lang)
const meta = await mdxMetaInfo(file)
// minify the metadat
if (meta.data?.title) {
const minMeta = { t: meta.data.title }
if (meta.data.order) minMeta.o = `${meta.data.order}${meta.data.title}`
else if (meta.data.date) {
minMeta.d = meta.data.date
minMeta.o = (future - new Date(meta.data.date).getTime()) / 100000
}
if (meta.data.image) minMeta.i = meta.data.image
if (meta.data.author) minMeta.a = meta.data.author
if (meta.data.maker) minMeta.a = meta.data.maker
pages[lang][slug] = minMeta
} else {
if (pages.en[slug]) {
console.log(`l Falling back to EN metadata for ${lang} ${slug}`)
pages[lang][slug] = pages.en[slug]
} else {
console.log(`❌ [${lang}] Failed to extract meta info from: ${slug}`)
if (meta.messages.length > 0) console.log(meta.messages)
}
}
const intros = {}
intros[lang] = await mdIntro(lang, site, slug)
//if (process.env.GENERATE_OG_IMAGES) {
// // Create og image
// await generateOgImage({ lang, site, slug, title: meta.data.title, intro: intros[lang] })
//}
}
}
return pages
}
/*
* Main method that does what needs doing
*/
export const prebuildDocs = async (site) => {
// Say hi
console.log()
console.log(`Compiling list of docs pages for freesewing.${site}`)
// Languages
const locales = site === 'dev' ? ['en'] : ['en', 'fr', 'es', 'nl', 'de', 'uk']
// Setup MDX root path
const root = ['..', '..', 'markdown', site]
if (site === 'org') root.push('docs')
const mdxRoot = path.resolve(...root)
const pages = await loadMdxForPrebuild(site, mdxRoot, locales)
// Write files with MDX paths
let allPaths = ``
for (const lang of locales) {
fs.writeFileSync(
path.resolve('..', site, 'prebuild', `mdx-paths.${lang}.mjs`),
`${header}export const mdxPaths = ${JSON.stringify(Object.keys(pages[lang]))}`
)
allPaths += `import { mdxPaths as ${lang} } from './mdx-paths.${lang}.mjs'` + '\n'
}
// Write umbrella file
fs.writeFileSync(
path.resolve('..', site, 'prebuild', `mdx-paths.mjs`),
`${allPaths}${header}
export const mdxPaths = { ${locales.join(',')} }`
)
return pages
}

View file

@ -1,4 +1,4 @@
import { copyFile } from 'node:fs/promises';
import { copyFile } from 'node:fs/promises'
import path from 'path'
const files = [
@ -14,22 +14,16 @@ const files = [
'site.webmanifest',
]
/*
* Main method that does what needs doing
*/
export const prebuildFavicon = async (site) => {
// Say hi
console.log()
console.log(`Copying favicon data for FreeSewing.${site}`)
export const prebuildFavicon = async (store) => {
// Setup from/to folders
const from = ['..', 'shared', 'favicon']
const to = ['..', site, 'public']
const to = ['..', store.site, 'public']
const promises = []
for (const file of files) promises.push(
copyFile(path.resolve(...from, file), path.resolve(...to, file))
)
for (const file of files)
promises.push(copyFile(path.resolve(...from, file), path.resolve(...to, file)))
store.favicon = files
return Promise.all(promises)
}

View file

@ -1,19 +1,27 @@
import execa from 'execa'
import { exec } from 'node:child_process'
import { gitToAuthor, authors as authorInfo } from '../../../config/authors.mjs'
import path from 'path'
import fs from 'fs'
import { getMdxFileList, fileToSlug } from './docs.mjs'
import { yyyymmdd } from '../utils.mjs'
const divider = '____'
const parseLog = (line) => line.split(divider).map((item) => item.trim())
/*
* Helper method to get the website slug (path) from the file path
*/
const fileToSlug = (file, site, lang) =>
file.slice(-6) === `/${lang}.md` ? file.split(`/markdown/${site}/`).pop().slice(0, -6) : false
/*
* Extracts git authors and last modification date from git log.
* Strictly speaking, it's the last commit date, but you get the idea.
*/
export const getGitMetadata = async (file, site) => {
const slug = fileToSlug(file, site, 'en')
if (!slug) console.log({ file, slug })
const log = await execa.command(
`git log --pretty="format:%cs${divider}%aN${divider}%aE" ${file}`,
{ shell: true }
@ -26,12 +34,10 @@ export const getGitMetadata = async (file, site) => {
if (!lastUpdated) lastUpdated = date.split('-').join('')
let key = false
if (typeof authorInfo[author] !== 'undefined') key = author
else if (typeof authorInfo[email] !== 'undefined') key = author
else {
if (typeof gitToAuthor[author] !== 'undefined') {
key = gitToAuthor[author]
} else if (typeof gitToAuthor[email] !== 'undefined') {
key = gitToAuthor[email]
}
if (typeof gitToAuthor[author] !== 'undefined') key = gitToAuthor[author]
else if (typeof gitToAuthor[email] !== 'undefined') key = gitToAuthor[email]
}
if (!key) {
if (typeof email === 'undefined') {
@ -39,8 +45,9 @@ export const getGitMetadata = async (file, site) => {
authors.add('unknown')
} else {
// There is a git history, but the author is not known
console.log({ email, author, slug })
throw `Git author email ${email} is unknown in the git-to-author table`
console.log('Missing git author info for:', { email, author, slug })
// Don't throw, it's annotying
//throw `Git author email ${email} is unknown in the git-to-author table`
}
} else authors.add(key)
}
@ -53,49 +60,101 @@ export const getGitMetadata = async (file, site) => {
}
/*
* Main method that does what needs doing
* Writes data to the prebuild files
*/
export const prebuildGitData = async (site) => {
// Say hi
console.log()
console.log(`Prebuilding git author data for freesewing.${site}`)
// Setup MDX root path
const root = ['..', '..', 'markdown', site]
if (site === 'org') root.push('docs')
const mdxRoot = path.resolve(...root)
const pages = {}
// Get list of filenames
const list = await getMdxFileList(mdxRoot, 'en')
// Loop over files
for (const file of list) {
const { lastUpdated, authors, slug } = await getGitMetadata(file, site)
pages[slug] = { u: lastUpdated, a: [...authors] }
}
const writeData = async (store) => {
// Write page to disk
const dir = path.resolve('..', site, 'prebuild')
const dir = path.resolve('..', store.site, 'prebuild')
fs.mkdirSync(dir, { recursive: true })
fs.writeFileSync(
path.resolve(dir, `doc-updates.mjs`),
`export const docUpdates = ${JSON.stringify(pages)}`
`export const docUpdates = ${JSON.stringify(store.git.pages)}`
)
// How about some stats
const stats = {}
for (const slug in pages) {
for (const author of pages[slug].a) {
if (typeof stats[author] === 'undefined') stats[author] = 0
stats[author]++
}
}
fs.writeFileSync(
path.resolve(dir, `doc-stats.mjs`),
`export const docStats = ${JSON.stringify(stats, null, 2)}`
`export const docStats = ${JSON.stringify(store.git.stats)}`
)
return pages
}
/*
* Helper method to load all MDX files from a folder
*/
const getMdxFileList = async (cwd) => {
const cmd = `find ${cwd} -type f -name "en.md"`
const find = exec(cmd, { cwd }, (error, stdout, stderr) => {
if (error) {
console.error(`exec error: ${error} - ${stderr}`)
return
}
return stdout
})
/*
* Stdout is buffered, so we need to gather all of it
*/
let stdout = ''
for await (const data of find.stdout) stdout += data
/*
* Rerturn all matches as a sorted array
*/
return stdout.split('\n').sort()
}
/*
* Main method that does what needs doing
*/
export const prebuildGitData = async (store, mock) => {
if (mock) {
store.git = mockedData(store)
return writeData(store)
}
// Setup MDX root path
const root = ['..', '..', 'markdown', store.site]
if (store.site === 'org') root.push('docs')
const mdxRoot = path.resolve(...root)
store.git = {
pages: {},
stats: {},
}
// Get list of filenames
const list = await getMdxFileList(mdxRoot)
// Loop over files
for (const file of list) {
// This list will include '' which we don't want to get the git log for as that
// will return the entire history
if (file) {
const { lastUpdated, authors, slug } = await getGitMetadata(file, store.site)
store.git.pages[slug] = { u: lastUpdated, a: [...authors] }
}
}
// How about some stats
for (const slug in store.git.pages) {
for (const author of store.git.pages[slug].a) {
if (typeof store.git.stats[author] === 'undefined') store.git.stats[author] = 0
store.git.stats[author]++
}
}
return writeData(store)
}
/*
* In development, we return this mocked data to speed things up
*/
const mockedData = (store) => {
const pages = {}
const u = yyyymmdd()
for (const slug of store.navigation.sluglut) pages[slug] = { u, a: ['mocked'] }
return {
pages,
stats: { mocked: store.navigation.sluglut.length },
}
}

View file

@ -168,24 +168,25 @@ const patternTranslationAsNamespace = (i18n, language) => {
/*
* The method that does the actual work
*/
export const prebuildI18n = async (site) => {
export const prebuildI18n = async (store) => {
/*
* FreeSewing.dev is only available in English
*/
const languages = site === 'dev' ? ['en'] : allLanguages
const languages = store.site === 'dev' ? ['en'] : allLanguages
/*
* Handle code-adjacent translations (for React components and so on)
*/
const files = await getI18nFileList(site, languages)
const files = await getI18nFileList(store.site, languages)
const data = filesAsNamespaces(files)
const namespaces = fixData(data, languages)
// Write out code-adjacent source files
for (const language of languages) {
// Fan out into namespaces
for (const namespace in namespaces)
writeJson(site, language, namespace, namespaces[namespace][language])
writeJson(store.site, language, namespace, namespaces[namespace][language])
}
/*
* Handle design translations
*/
@ -197,8 +198,13 @@ export const prebuildI18n = async (site) => {
const content = patternTranslationAsNamespace(designs[design], language)
designNs[language][`${design}.t`] = content.t
designNs[language][`${design}.d`] = content.d
writeJson(site, language, design, content)
writeJson(store.site, language, design, content)
}
}
for (const language of languages) writeJson(site, language, 'designs', designNs[language])
for (const language of languages) writeJson(store.site, language, 'designs', designNs[language])
/*
* Update the store
*/
store.i18n = { namespaces, designNs }
}

View file

@ -1,60 +0,0 @@
import { prebuildDocs } from './docs.mjs'
import { prebuildNavigation } from './navigation.mjs'
import { prebuildGitData } from './git.mjs'
import { prebuildContributors } from './contributors.mjs'
import { prebuildPatrons } from './patrons.mjs'
import { prebuildI18n } from './i18n.mjs'
import { prebuildLab } from './lab.mjs'
import { prebuildDesigns } from './designs.mjs'
import { prebuildFavicon } from './favicon.mjs'
import { generateOgImage } from './og/index.mjs'
import { prebuildPosts } from './posts.mjs'
import { prebuildCrowdin } from './crowdin.mjs'
const run = async () => {
const now = Date.now()
if (process.env.LINTER) return true
const FAST = process.env.FAST ? true : false
const SITE = process.env.SITE || 'lab'
await prebuildDesigns()
if (['org', 'dev'].includes(SITE)) {
if (!FAST) {
await prebuildGitData(SITE)
await prebuildCrowdin()
}
const docPages = await prebuildDocs(SITE)
const postPages = await prebuildPosts(SITE)
prebuildNavigation(docPages, postPages, SITE)
if (!FAST && process.env.GENERATE_OG_IMAGES) {
// Create og image for the home page
await generateOgImage({
lang: 'en',
site: SITE,
slug: '',
title: 'FreeSewing.dev',
})
// Create og image for the 404 page
await generateOgImage({
lang: 'en',
site: SITE,
slug: '/404',
intro: "There's nothing here. Only this message to say there's nothing here.",
title: 'Page not found',
lead: '404',
})
}
} else {
await prebuildLab()
}
await prebuildI18n(SITE)
if (!FAST) {
await prebuildContributors(SITE)
await prebuildPatrons(SITE)
await prebuildFavicon(SITE)
}
console.log('completed prebuild in ', Date.now() - now, 'ms')
console.log()
}
run()

View file

@ -0,0 +1,284 @@
import fs from 'node:fs'
import path from 'node:path'
import { exec } from 'node:child_process'
import orderBy from 'lodash.orderby'
/*
* Shared header to include in written .mjs files
*/
export const header = `/*
* This file was auto-generated by the prebuild script
* Any changes you make to it will be lost on the next (pre)build.
*/
`
/*
* Strips quptes from the start/end of a string
*/
const stripQuotes = (str) => {
str = str.trim()
if (str.slice(0, 1) === '"') str = str.slice(1)
if (str.slice(-1) === '"') str = str.slice(0, -1)
return str.trim()
}
/*
* This is the fast and low-tech way to some frontmatter from all files in a folder
*/
const loadFolderFrontmatter = async (key, site, folder, transform = false, lang = false) => {
const prefix = site === 'org' ? `${folder}/` : ''
/*
* Figure out what directory to spawn the child process in
*/
const cwd = await path.resolve(process.cwd(), '..', '..', 'markdown', site, folder)
/*
* When going through a small number of files in a flat directory (eg. blog posts) a
* recursive grep through all files is faster.
* But the biggest task is combing through all the org documentation and for this
* it's much faster to first run find to limit the number of files to open
*/
const cmd = `find ${cwd} -type f -name "${
lang ? lang : '*'
}.md" -exec grep "^${key}:" -ism 1 {} +`
const grep = exec(cmd, { cwd }, (error, stdout, stderr) => {
if (error) {
console.error(`exec error: ${error} - ${stderr}`)
return
}
return stdout
})
/*
* Stdout is buffered, so we need to gather all of it
*/
let stdout = ''
for await (const data of grep.stdout) stdout += data
/*
* Turn all matches into an array
*/
const matches = stdout.split('\n')
/*
* Turns matches into structured data
*/
const pages = {}
for (let match of matches) {
/*
* Trim some of the irrelevant path info prior to splitting on '.md:{key}:'
*/
const chunks = match
.split(`markdown/${site}/${site === 'dev' ? '' : folder + '/'}`)
.pop()
.split(`.md:${key}:`)
if (chunks.length === 2 && chunks[0].length > 1) {
/*
* Figure out the language and make sure we have an key for that language
*/
const lang = chunks[0].slice(-2)
if (!pages[lang]) pages[lang] = {}
/*
* Add page to our object with slug as key and title as value
*/
let slug = prefix + chunks[0].slice(0, -3)
if (slug === prefix) slug = slug.slice(0, -1)
pages[lang][slug] = transform
? transform(stripQuotes(chunks[1]), slug, lang)
: stripQuotes(chunks[1])
}
}
return pages
}
/*
* Merges in order key on those slugs that have it set
*/
const mergeOrder = (titles, order, withSlug = false) => {
const pages = {}
for (const lang in titles) {
pages[lang] = {}
for (const [slug, t] of Object.entries(titles[lang])) {
pages[lang][slug] = { t }
if (order.en[slug]) pages[lang][slug].o = order.en[slug]
if (withSlug) pages[lang][slug].s = slug
}
}
return pages
}
/*
* Fixes the date format to be yyyymmdd
*/
const formatDate = (date, slug, lang) => {
date = date.split('-')
if (date.length === 1) date = date[0].split('.')
if (date.length === 1) console.log(`Could not format date ${date} from ${slug} (${lang})`)
else {
if (date[0].length === 4) return date.join('')
else return date.reverse().join('')
}
}
/*
* Loads all docs files, titles and order
*/
const loadDocs = async (site) => {
const folder = site === 'org' ? 'docs' : '.'
const titles = await loadFolderFrontmatter('title', site, folder)
// Order is the same for all languages, so only grab EN files
const order = await loadFolderFrontmatter('order', site, folder, false, 'en')
return mergeOrder(titles, order)
}
/*
* Loads all blog posts, titles and order
*/
const loadBlog = async () => {
const titles = await loadFolderFrontmatter('title', 'org', 'blog')
// Order is the same for all languages, so only grab EN files
const order = await loadFolderFrontmatter('date', 'org', 'blog', formatDate, 'en')
// Author is the same for all languages, so only grab EN files
const authors = await loadFolderFrontmatter('author', 'org', 'blog', false, 'en')
// Image is the same for all languages, so only grab EN files
const images = await loadFolderFrontmatter('image', 'org', 'blog', false, 'en')
// Merge titles and order for EN
const merged = {}
for (const slug in titles.en)
merged[slug] = {
t: titles.en[slug],
o: order.en[slug],
s: slug,
a: authors.en[slug],
i: images.en[slug],
}
// Order based on post data (descending)
const ordered = orderBy(merged, 'o', 'desc')
// Apply same order to all languages
const posts = {}
const meta = {}
for (const lang of Object.keys(titles)) {
posts[lang] = {}
for (const post of ordered) {
posts[lang][post.s] = { t: post.t }
if (lang === 'en') meta[post.s] = { a: post.a, d: post.o, i: post.i }
}
}
return { posts, meta }
}
/*
* Loads all showcase posts, titles and order
*/
const loadShowcase = async () => {
const titles = await loadFolderFrontmatter('title', 'org', 'showcase')
// Order is the same for all languages, so only grab EN files
const order = await loadFolderFrontmatter('date', 'org', 'showcase', formatDate, 'en')
// Author is the same for all languages, so only grab EN files
const makers = await loadFolderFrontmatter('maker', 'org', 'showcase', false, 'en')
// Image is the same for all languages, so only grab EN files
const images = await loadFolderFrontmatter('image', 'org', 'showcase', false, 'en')
// Merge titles and order for EN
const merged = {}
for (const slug in titles.en)
merged[slug] = {
t: titles.en[slug],
o: order.en[slug],
s: slug,
m: makers.en[slug],
i: images.en[slug],
}
// Order based on post data (descending)
const ordered = orderBy(merged, 'o', 'desc')
// Apply same order to all languages
const posts = {}
const meta = {}
for (const lang of Object.keys(titles)) {
posts[lang] = {}
for (const post of ordered) {
posts[lang][post.s] = { t: post.t }
if (lang === 'en') meta[post.s] = { m: post.m, d: post.o, i: post.i }
}
}
return { posts, meta }
}
/*
* Loads all newsletter posts, titles and order
*/
const loadNewsletter = async () => {
const titles = await loadFolderFrontmatter('title', 'org', 'newsletter')
// Order is the same for all languages, so only grab EN files
const order = await loadFolderFrontmatter('edition', 'org', 'newsletter', false, 'en')
return mergeOrder(titles, order)
}
/*
* Write out prebuild files
*/
const writeFiles = async (type, site, pages) => {
let allPaths = ``
for (const lang in pages) {
fs.writeFileSync(
path.resolve('..', site, 'prebuild', `${type}.${lang}.mjs`),
`${header}export const pages = ${JSON.stringify(pages[lang])}`
)
allPaths += `import { pages as ${lang} } from './${type}.${lang}.mjs'` + '\n'
}
// Write umbrella file
fs.writeFileSync(
path.resolve('..', site, 'prebuild', `${type}.mjs`),
`${allPaths}${header}
export const pages = { ${Object.keys(pages).join(',')} }`
)
}
/*
* Write out a single prebuild file
*/
const writeFile = async (filename, exportname, site, content) => {
fs.writeFileSync(
path.resolve('..', site, 'prebuild', `${filename}.mjs`),
`${header}export const ${exportname} = ${JSON.stringify(content)}`
)
}
/*
* Main method that does what needs doing for the docs
*/
export const prebuildDocs = async (store) => {
store.docs = await loadDocs(store.site)
await writeFiles('docs', store.site, store.docs)
}
/*
* Main method that does what needs doing for the blog/showcase/newsletter posts
*/
export const prebuildPosts = async (store) => {
store.posts = {
blog: await loadBlog(),
showcase: await loadShowcase(),
newsletter: { posts: await loadNewsletter() },
}
await writeFiles('blog', 'org', store.posts.blog.posts)
await writeFiles('showcase', 'org', store.posts.showcase.posts)
await writeFiles('newsletter', 'org', store.posts.newsletter)
await writeFile('blog-meta', 'meta', 'org', store.posts.blog.meta)
await writeFile('showcase-meta', 'meta', 'org', store.posts.showcase.meta)
}

View file

@ -1,73 +1,125 @@
import path from 'path'
import fs from 'fs'
import set from 'lodash.set'
import { loadYaml, folders } from './i18n.mjs'
import orderBy from 'lodash.orderby'
import { extendSiteNav as dev } from './sitenav-dev.mjs'
import { extendSiteNav as org } from './sitenav-org.mjs'
import { pageHasChildren } from '../utils.mjs'
import { header } from './shared.mjs'
// We need to load the translation for blog + showcase
const loadTranslation = (locale) => {
let data
try {
data = loadYaml(`${folders.shared[0]}/navigation/sections.${locale}.yaml`, false)
} catch (err) {
data = {}
const extendNav = { dev, org }
/*
* A method to recursively add the ordered slugs to the LUT
*/
const flattenOrderedChildPages = (nav) => {
const slugs = []
for (const page of orderBy(nav, ['o', 't'], ['asc', 'asc'])) {
if (page.s) {
slugs.push(page.s)
if (pageHasChildren(page)) slugs.push(...flattenOrderedChildPages(page))
}
}
if (!data) data = {}
return data
return slugs
}
/*
* This builds the slugLut (slug look up table) which makes it trivial to
* build the PrevNext component as it builds a flat list of all pages in
* the order they are naturally presented to the reader. So if you have
* a page's slug, you merely need to look it up in the list and return the
* next entry (or previous)
*/
export const orderedSlugLut = (nav) => {
const slugs = []
for (const page of orderBy(nav, ['o', 't'], ['asc', 'asc'])) {
if (page.s) {
slugs.push(page.s)
if (pageHasChildren(page)) slugs.push(...flattenOrderedChildPages(page))
}
}
return slugs
}
/*
* Main method that does what needs doing
*/
export const prebuildNavigation = (docPages, postPages, site) => {
export const prebuildNavigation = async (store) => {
const { docs, site, posts = false } = store
/*
* 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 docPages) {
const translations = loadTranslation(lang)
nav[lang] = {}
const sitenav = {}
const all = {
sitenav: '',
}
const locales = []
for (const lang in docs) {
locales.push(lang)
sitenav[lang] = {}
// Handle MDX content
for (const slug of Object.keys(docPages[lang]).sort()) {
const page = docPages[lang][slug]
const chunks = slug.split('/')
// Handle docs
for (const slug of Object.keys(docs[lang]).sort()) {
const page = docs[lang][slug]
const val = {
t: page.t,
s: slug,
}
if (page.o) val.o = page.o
set(nav, [lang, ...chunks], val)
set(sitenav, [lang, ...slug.split('/')], val)
}
// Handle strapi content
for (const type in postPages) {
set(nav, [lang, type], {
t: translations[type] || type,
s: type,
})
for (const page in postPages[type][lang]) {
const pageData = postPages[type][lang][page]
const chunks = page.split('/')
set(nav, [lang, ...chunks], {
t: pageData.t,
s: page,
o: pageData.o,
})
// Handle posts
if (posts) {
for (const type in posts) {
for (const [slug, post] of Object.entries(posts[type].posts[lang])) {
set(sitenav, [lang, ...slug.split('/')], { t: post.t, o: post.o, s: slug })
}
}
}
// Add imports for umbrella file
all.sitenav += `import { siteNav as ${lang} } from './sitenav.${lang}.mjs'` + '\n'
// Extend navigation if there's a method for that
if (extendNav[site]) sitenav[lang] = extendNav[site](sitenav[lang], lang)
// Write out navigation object
fs.writeFileSync(
path.resolve('..', site, 'prebuild', `sitenav.${lang}.mjs`),
`${header}export const siteNav = ${JSON.stringify(sitenav[lang])}`
)
/*
* Since slugs are language-agnostic, we only need to create a slug lookup tables
* once, for which we'll use the EN locale as that one is always present
*/
if (lang === 'en') {
const sluglut = orderedSlugLut(sitenav[lang])
// Write out slug lookup table (sluglut)
fs.writeFileSync(
path.resolve('..', site, 'prebuild', `sluglut.mjs`),
`${header}export const slugLut = ${JSON.stringify(sluglut)}`
)
store.navigation = { sluglut }
}
}
// Write umbrella siteNav file
fs.writeFileSync(
path.resolve('..', site, 'prebuild', `navigation.mjs`),
`export const prebuildNavigation = ${JSON.stringify(nav, null, 2)}`
path.resolve('..', site, 'prebuild', `sitenav.mjs`),
`${header}${all.sitenav}export const siteNav = { ${locales.join(',')} }`
)
return true
// Update the store
store.navigation.sitenav = sitenav
return
}

View file

@ -144,7 +144,7 @@ const writeAsPng = async (svg, site, slug) => {
* }
*/
export const generateOgImage = async (data) => {
export const prebuildOgImages = async (data) => {
// Inject into SVG
const meta = await getMetaData(data)
const svg = decorateSvg(meta)

View file

@ -2,34 +2,53 @@ import path from 'path'
import fs from 'fs'
import axios from 'axios'
/*
* Main method that does what needs doing
*/
export const prebuildPatrons = async(site) => {
// Say hi
console.log()
console.log(`Prebuilding patron list for freesewing.${site}`)
export const prebuildPatrons = async (store, mock = false) => {
if (mock) return (store.patrons = mockedData)
let patrons
try {
// FIXME: Adapt this when the v3 backend is production-ready
patrons = await axios.get('https://backend.freesewing.org/patrons')
}
catch (err) {
} catch (err) {
console.log(`⚠️ Failed to load patron list`)
}
const list = patrons?.data
? [
...patrons.data['2'].map(p => ({hande: p.handle, username: p.username, img: p.pictureUris.s })),
...patrons.data['4'].map(p => ({hande: p.handle, username: p.username, img: p.pictureUris.s })),
...patrons.data['8'].map(p => ({hande: p.handle, username: p.username, img: p.pictureUris.s })),
] : []
...patrons.data['2'].map((p) => ({
hande: p.handle,
username: p.username,
img: p.pictureUris.s,
})),
...patrons.data['4'].map((p) => ({
hande: p.handle,
username: p.username,
img: p.pictureUris.s,
})),
...patrons.data['8'].map((p) => ({
hande: p.handle,
username: p.username,
img: p.pictureUris.s,
})),
]
: []
// Write to json
fs.writeFileSync(
path.resolve('..', site, 'prebuild', `patrons.js`),
`export default ${JSON.stringify(list, null ,2)}`
path.resolve('..', store.site, 'prebuild', `patrons.js`),
`export default ${JSON.stringify(list, null, 2)}`
)
store.patrons = list
}
/*
* In development, we return this mocked data to speed things up
*/
const mockedData = [
{
hande: 'xdpug',
username: 'wouter.vdub',
img: 'https://static.freesewing.org/users/x/xdpug/s-xdpug.jpg',
},
]

View file

@ -1,48 +0,0 @@
import { loadMdxForPrebuild, header } from './docs.mjs'
import fs from 'fs/promises'
import path from 'path'
const types = ['blog', 'showcase', 'newsletter']
export const prebuildPosts = async (site) => {
if (site !== 'org') return {}
// Languages
const locales = ['en', 'fr', 'es', 'nl', 'de', 'uk']
const results = await Promise.all(
types.map((t) =>
loadMdxForPrebuild(site, path.resolve('..', '..', 'markdown', site, t), locales)
)
)
const writeOps = []
const pages = {}
for (var i = 0; i < types.length; i++) {
const sorted = {}
const resultPages = results[i]
pages[types[i]] = resultPages
for (const lang in resultPages) {
sorted[lang] = Object.keys(resultPages[lang]).sort(
(a, b) => resultPages[lang][a].o - resultPages[lang][b].o
)
// get rid of the index page
sorted[lang].shift()
}
writeOps.push(
fs.writeFile(
path.resolve('..', site, 'prebuild', `${types[i]}-paths.mjs`),
`${header}export const order = ${JSON.stringify(
sorted,
null,
2
)}\nexport const postInfo = ${JSON.stringify(resultPages, null, 2)}`
)
)
}
await Promise.all(writeOps)
return pages
}

View file

@ -0,0 +1,91 @@
// Dependencies
import { oraPromise } from 'ora'
import { capitalize } from '../utils.mjs'
// Handlers
import { prebuildDocs as docs, prebuildPosts as posts } from './markdown.mjs'
import { prebuildNavigation as navigation } from './navigation.mjs'
import { prebuildGitData as git } from './git.mjs'
import { prebuildContributors as contributors } from './contributors.mjs'
import { prebuildPatrons as patrons } from './patrons.mjs'
import { prebuildI18n as i18n } from './i18n.mjs'
import { prebuildDesigns as designs } from './designs.mjs'
import { prebuildFavicon as favicon } from './favicon.mjs'
import { prebuildCrowdin as crowdin } from './crowdin.mjs'
//import { prebuildLab as lab} from './lab.mjs'
//import { prebuildOgImages as ogImages } from './og/index.mjs'
/*
* Are we running in production?
*/
const PRODUCTION = process.env.NODE_ENV === 'production'
/*
* Structure handlers in a single object
*/
const handlers = {
designs,
contributors,
crowdin,
i18n,
favicon,
patrons,
docs,
posts,
navigation,
git,
// FIXME: This needs work, but perhaps after v3
//ogImages,
}
export const prebuildRunner = async ({
site, // The site we are running the prebuild for
prebuild, // The prebuild configuration object. See sites/[site]/prebuild.mjs
}) => {
/*
* Setup a place where we can keep data
*/
const store = { site }
/*
* Let the user know what's going to happen
*/
logSummary(site, prebuild)
/*
* To avoid order issues, we use the order as configured
* above, not the order as passed by the prebuild script
*/
for (const step in handlers) {
if (prebuild[step] === true)
await oraPromise(handlers[step](store), { text: `Prebuild ${capitalize(step)}` })
else if (prebuild[step] === 'productionOnly')
await oraPromise(handlers[step](store, !PRODUCTION), {
text: `Prebuild ${capitalize(step)}${PRODUCTION ? '' : ' (mocked)'}`,
})
else await oraPromise(() => true, { text: `Prebuild ${capitalize(step)} (skipped)` })
}
console.log()
return
}
const logSummary = (site, prebuild) => {
console.log()
console.log()
console.log(`👷 Preparing prebuild step for FreeSewing's ${site} site`)
console.log(
`${PRODUCTION ? '🚀' : '🚧'} This ${PRODUCTION ? 'is' : 'is not'} a production build`
)
console.log(`🏁 We will run the following prebuild steps:`)
console.log()
for (const step in prebuild) {
if (prebuild[step] === 'productionOnly') {
if (PRODUCTION) console.log(`🟢 Prebuild ${capitalize(step)}`)
else console.log(`🟡 Mock ${capitalize(step)}`)
} else if (prebuild[step]) console.log(`🟢 Prebuild ${capitalize(step)}`)
else console.log(`🔴 Skip ${capitalize(step)}`)
}
console.log()
console.log(`👷 Let's get to work...`)
console.log()
}

View file

@ -0,0 +1,8 @@
/*
* A header to include in auto-generated files
*/
export const header = `/*
* This file was auto-generated by the prebuild script
* Any changes you make to it will be lost on the next (pre)build.
*/
`

View file

@ -0,0 +1,54 @@
/* Remember Mc_Shifton:
* Note: Set 'm' to truthy to show this as a main section in the side-navigation (optional)
* Note: Set 'c' to set the control level to hide things from users (optional)
* Note: Set 's' to the slug (optional insofar as it's not a real page (a spacer for the header))
* Note: Set '_' to never show the page in the site navigation (like the tags pages)
* Note: Set 'h' to indicate this is a top-level page that should be hidden from the side-nav (like search)
* Note: Set 'i' when something should be included as top-level in the collapse side-navigation (optional)
* Note: Set 'f' to add the page to the footer
* Note: Set 't' to the title
* Note: Set 'o' to set the order (optional)
* Note: Set 'n' to mark this as a noisy entry that should always be closed unless active (like blog)
*/
export const extendSiteNav = (pages) => {
pages.about = {
s: 'about',
t: 'About FreeSewing',
}
let order = 10
for (const slug of ['api', 'design', 'contribute', 'i18n', 'infra', 'about', 'support']) {
pages[slug].m = 1
pages[slug].o = order
order += 10
}
pages.search = {
s: 'search',
h: 1,
f: 1,
t: 'Search',
o: 270,
}
pages.sitemap = {
s: 'sitemap',
h: 1,
f: 1,
t: 'Sitemap',
o: 270,
}
// Make top-level documentation entries appear in i-list
order = 10
for (const slug of ['tutorials', 'guides', 'howtos', 'reference', 'training']) {
pages[slug].o = order
pages[slug].i = 1
order += 10
}
// Hide contact from the sitenav
pages.contact.h = 1
return pages
}

View file

@ -0,0 +1,270 @@
import { freeSewingConfig as conf } from '../config/freesewing.config.mjs'
import { designs, tags } from '../config/designs.mjs'
// Translation via i18next directly
import i18next from 'i18next'
// Actual translations for various languages
// EN
import accountEn from '../../org/public/locales/en/sections.json' assert { type: 'json' }
import designsEn from '../../org/public/locales/en/sections.json' assert { type: 'json' }
import sectionsEn from '../../org/public/locales/en/sections.json' assert { type: 'json' }
import tagsEn from '../../org/public/locales/en/sections.json' assert { type: 'json' }
// DE
import accountDe from '../../org/public/locales/de/sections.json' assert { type: 'json' }
import designsDe from '../../org/public/locales/de/sections.json' assert { type: 'json' }
import sectionsDe from '../../org/public/locales/de/sections.json' assert { type: 'json' }
import tagsDe from '../../org/public/locales/de/sections.json' assert { type: 'json' }
// ES
import accountEs from '../../org/public/locales/es/sections.json' assert { type: 'json' }
import designsEs from '../../org/public/locales/es/sections.json' assert { type: 'json' }
import sectionsEs from '../../org/public/locales/es/sections.json' assert { type: 'json' }
import tagsEs from '../../org/public/locales/es/sections.json' assert { type: 'json' }
// FR
import accountFr from '../../org/public/locales/fr/sections.json' assert { type: 'json' }
import designsFr from '../../org/public/locales/fr/sections.json' assert { type: 'json' }
import sectionsFr from '../../org/public/locales/fr/sections.json' assert { type: 'json' }
import tagsFr from '../../org/public/locales/fr/sections.json' assert { type: 'json' }
// NL
import accountNl from '../../org/public/locales/nl/sections.json' assert { type: 'json' }
import designsNl from '../../org/public/locales/nl/sections.json' assert { type: 'json' }
import sectionsNl from '../../org/public/locales/nl/sections.json' assert { type: 'json' }
import tagsNl from '../../org/public/locales/nl/sections.json' assert { type: 'json' }
// UK
import accountUk from '../../org/public/locales/uk/sections.json' assert { type: 'json' }
import designsUk from '../../org/public/locales/uk/sections.json' assert { type: 'json' }
import sectionsUk from '../../org/public/locales/uk/sections.json' assert { type: 'json' }
import tagsUk from '../../org/public/locales/uk/sections.json' assert { type: 'json' }
/*
* Construct an object we can load the translations from
*/
const translations = {
en: {
account: accountEn,
design: designsEn,
sections: sectionsEn,
tags: tagsEn,
},
de: {
account: accountDe,
design: designsDe,
sections: sectionsDe,
tags: tagsDe,
},
es: {
account: accountEs,
design: designsEs,
sections: sectionsEs,
tags: tagsEs,
},
fr: {
account: accountFr,
design: designsFr,
sections: sectionsFr,
tags: tagsFr,
},
nl: {
account: accountNl,
design: designsNl,
sections: sectionsNl,
tags: tagsNl,
},
uk: {
account: accountUk,
design: designsUk,
sections: sectionsUk,
tags: tagsUk,
},
}
/* Remember Mc_Shifton:
* Note: Set 'm' to truthy to show this as a main section in the side-navigation (optional)
* Note: Set 'c' to set the control level to hide things from users (optional)
* Note: Set 's' to the slug (optional insofar as it's not a real page (a spacer for the header))
* Note: Set '_' to never show the page in the site navigation (like the tags pages)
* Note: Set 'h' to indicate this is a top-level page that should be hidden from the side-nav (like search)
* Note: Set 'i' when something should be included as top-level in the collapse side-navigation (optional)
* Note: Set 'f' to add the page to the footer
* Note: Set 't' to the title
* Note: Set 'o' to set the order (optional)
* Note: Set 'n' to mark this as a noisy entry that should always be closed unless active (like blog)
*/
export const extendSiteNav = (pages, lang) => {
const resources = {}
resources[lang] = translations[lang]
i18next.init({
lng: lang,
resources,
})
const { t } = i18next
const addThese = {
blog: {
m: 1,
s: 'blog',
t: t('sections:blog'),
n: 1,
},
showcase: {
m: 1,
s: 'showcase',
t: t('sections:showcase'),
n: 1,
},
docs: {
m: 1,
s: 'docs',
t: t('sections:docs'),
},
newsletter: {
s: 'newsletter',
t: t('sections:newsletter'),
_: 1,
},
designs: {
m: 1,
s: 'designs',
t: t('sections:designs'),
n: 1,
tags: {
_: 1,
s: 'designs/tags',
t: t('design:tags'),
o: 'aaa',
},
},
patterns: {
m: 1,
s: 'patterns',
t: t('sections:patterns'),
},
sets: {
m: 1,
s: 'sets',
t: t('sections:sets'),
},
community: {
m: 1,
s: 'community',
t: t('sections:community'),
},
account: {
m: 1,
s: 'account',
t: t('sections:account'),
n: 1,
reload: {
s: `account/reload`,
t: t(`account:reload`),
},
},
// Top-level pages that are not in the sections menu
apikeys: {
_: 1,
s: 'apikeys',
h: 1,
t: t('apikeys'),
},
curate: {
s: 'curate',
h: 1,
t: t('curate'),
sets: {
t: t('curateSets'),
s: 'curate/sets',
},
},
new: {
m: 1,
s: 'new',
h: 1,
t: t('sections:new'),
apikey: {
c: conf.account.fields.developer.apikeys,
s: 'new/apikey',
t: t('newApikey'),
o: 30,
},
pattern: {
t: t('patternNew'),
s: 'new/pattern',
o: 10,
},
set: {
t: t('newSet'),
s: 'new/set',
0: 20,
},
},
profile: {
s: 'profile',
h: 1,
t: t('yourProfile'),
},
translation: {
s: 'translation',
h: 1,
t: t('translation'),
join: {
t: t('translation:joinATranslationTeam'),
s: 'translation',
},
'suggest-language': {
t: t('translation:suggestLanguage'),
s: 'translation',
},
},
sitemap: {
s: 'sitemap',
h: 1,
t: t('sitemap'),
},
// Not translated, this is a developer page
typography: {
s: 'typography',
h: 1,
t: 'Typography',
},
}
for (const section in conf.account.fields) {
for (const [field, controlScore] of Object.entries(conf.account.fields[section])) {
addThese.account[field] = {
s: `account/${field}`,
t: t(`account:${field}`),
c: controlScore,
}
}
}
for (const design in designs) {
// addThese.designs[design] = {
// t: t(`designs:${design}.t`),
// s: `designs/${design}`,
// }
addThese.new.pattern[design] = {
s: `new/${design}`,
t: t(`account:generateANewThing`, { thing: t(`designs:${design}.t`) }),
}
}
for (const tag of tags) {
addThese.designs.tags[tag] = {
s: `designs/tags/${tag}`,
t: t(`tags:${tag}`),
}
}
// Set order on main sections
addThese.designs.o = 10
addThese.docs.o = 20
addThese.blog.o = 30
addThese.showcase.o = 40
addThese.community.o = 50
addThese.patterns.o = 60
addThese.sets.o = 70
addThese.account.o = 80
addThese.new.o = 90
return { ...pages, ...addThese }
}

View file

@ -279,6 +279,17 @@ export const shortDate = (locale = 'en', timestamp = false) => {
return ts.toLocaleDateString(locale, options)
}
export const yyyymmdd = (timestamp = false) => {
const ts = timestamp ? new Date(timestamp) : new Date()
let m = String(ts.getMonth() + 1)
if (m.length === 1) m = '0' + m
let d = '' + ts.getDate()
if (d.length === 1) d = '0' + d
return `${ts.getFullYear()}${m}${d}`
}
export const scrollTo = (id) => {
// eslint-disable-next-line no-undef
const el = document ? document.getElementById(id) : null

View file

@ -14672,6 +14672,21 @@ ora@^5.1.0, ora@^5.4.1:
strip-ansi "^6.0.0"
wcwidth "^1.0.1"
ora@^6.3.1:
version "6.3.1"
resolved "https://registry.yarnpkg.com/ora/-/ora-6.3.1.tgz#a4e9e5c2cf5ee73c259e8b410273e706a2ad3ed6"
integrity sha512-ERAyNnZOfqM+Ao3RAvIXkYh5joP220yf59gVe2X/cI6SiCxIdi4c9HZKZD8R6q/RDXEje1THBju6iExiSsgJaQ==
dependencies:
chalk "^5.0.0"
cli-cursor "^4.0.0"
cli-spinners "^2.6.1"
is-interactive "^2.0.0"
is-unicode-supported "^1.1.0"
log-symbols "^5.1.0"
stdin-discarder "^0.1.0"
strip-ansi "^7.0.1"
wcwidth "^1.0.1"
org-regex@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/org-regex/-/org-regex-1.0.0.tgz#67ebb9ab3cb124fea5841289d60b59434f041a59"
@ -18341,6 +18356,13 @@ statuses@~1.4.0:
resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.4.0.tgz#bb73d446da2796106efcc1b601a253d6c46bd087"
integrity sha512-zhSCtt8v2NDrRlPQpCNtw/heZLtfUDqxBM1udqikb/Hbk52LK4nQSwr10u77iopCW5LsyHpuXS0GnEc48mLeew==
stdin-discarder@^0.1.0:
version "0.1.0"
resolved "https://registry.yarnpkg.com/stdin-discarder/-/stdin-discarder-0.1.0.tgz#22b3e400393a8e28ebf53f9958f3880622efde21"
integrity sha512-xhV7w8S+bUwlPTb4bAOUQhv8/cSS5offJuX8GQGq32ONF0ZtDWKfkdomM3HMRA+LhX6um/FZ0COqlwsjD53LeQ==
dependencies:
bl "^5.0.0"
stop-iteration-iterator@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/stop-iteration-iterator/-/stop-iteration-iterator-1.0.0.tgz#6a60be0b4ee757d1ed5254858ec66b10c49285e4"