wip: Started working on new development environment
This commit is contained in:
parent
ace86eaf85
commit
54aefa8437
45 changed files with 1722 additions and 43 deletions
|
@ -22,6 +22,7 @@ examples: 'A FreeSewing pattern holding examples for our documentation'
|
||||||
florent: 'A FreeSewing pattern for a flat cap'
|
florent: 'A FreeSewing pattern for a flat cap'
|
||||||
florence: 'A FreeSewing pattern for a face mask'
|
florence: 'A FreeSewing pattern for a face mask'
|
||||||
freesewing.dev: 'FreeSewing website with documentation for contributors & developers'
|
freesewing.dev: 'FreeSewing website with documentation for contributors & developers'
|
||||||
|
freesewing.lab: 'FreeSewing website to test various patterns'
|
||||||
freesewing.org: 'FreeSewing website'
|
freesewing.org: 'FreeSewing website'
|
||||||
freesewing.shared: 'Shared code and React components for different websites'
|
freesewing.shared: 'Shared code and React components for different websites'
|
||||||
gatsby-remark-jargon: 'A gatsby-transformer-remark sub-plugin for jargon terms'
|
gatsby-remark-jargon: 'A gatsby-transformer-remark sub-plugin for jargon terms'
|
||||||
|
|
|
@ -60,8 +60,23 @@ const Header = ({ app, setSearch }) => {
|
||||||
`}
|
`}
|
||||||
onClick={app.togglePrimaryMenu}>
|
onClick={app.togglePrimaryMenu}>
|
||||||
{app.primaryMenu
|
{app.primaryMenu
|
||||||
? <><CloseIcon /><span className="opacity-50 pl-2 flex flex-row items-center gap-1"><Left />swipe</span></>
|
? (
|
||||||
: <><MenuIcon /><span className="opacity-50 pl-2 flex flex-row items-center gap-1"><Right />swipe</span></>
|
<>
|
||||||
|
<CloseIcon />
|
||||||
|
<span className="opacity-50 pl-2 flex flex-row items-center gap-1">
|
||||||
|
<Left />
|
||||||
|
swipe
|
||||||
|
</span>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<MenuIcon />
|
||||||
|
<span className="opacity-50 pl-2 flex flex-row items-center gap-1">
|
||||||
|
<Right />
|
||||||
|
swipe
|
||||||
|
</span>
|
||||||
|
</>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
</button>
|
</button>
|
||||||
<div className="flex flex-row gap-2 sm:hidden">
|
<div className="flex flex-row gap-2 sm:hidden">
|
||||||
|
@ -100,7 +115,10 @@ const Header = ({ app, setSearch }) => {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className={`theme-gradient h-1 w-full z-10 relative -mb-1 ${app.loading ? 'loading' : ''}`}></div>
|
<div className={`
|
||||||
|
theme-gradient h-1 w-full z-10 relative -mb-1
|
||||||
|
${app.loading ? 'loading' : ''}
|
||||||
|
`}></div>
|
||||||
</header>
|
</header>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,6 +6,8 @@ import useLocalStorage from 'shared/hooks/useLocalStorage.js'
|
||||||
import prebuildNavigation from 'site/prebuild/navigation.js'
|
import prebuildNavigation from 'site/prebuild/navigation.js'
|
||||||
|
|
||||||
function useApp(full = true) {
|
function useApp(full = true) {
|
||||||
|
// No translation for freesewing.dev
|
||||||
|
const language = 'en'
|
||||||
|
|
||||||
// User color scheme preference
|
// User color scheme preference
|
||||||
const prefersDarkMode = (typeof window !== 'undefined' && typeof window.matchMedia === 'function')
|
const prefersDarkMode = (typeof window !== 'undefined' && typeof window.matchMedia === 'function')
|
||||||
|
@ -15,7 +17,6 @@ function useApp(full = true) {
|
||||||
// Persistent state
|
// Persistent state
|
||||||
const [account, setAccount] = useLocalStorage('account', { username: false })
|
const [account, setAccount] = useLocalStorage('account', { username: false })
|
||||||
const [theme, setTheme] = useLocalStorage('theme', prefersDarkMode ? 'dark' : 'light')
|
const [theme, setTheme] = useLocalStorage('theme', prefersDarkMode ? 'dark' : 'light')
|
||||||
const [language, setLanguage] = useLocalStorage('language', 'en')
|
|
||||||
|
|
||||||
// React State
|
// React State
|
||||||
const [primaryMenu, setPrimaryMenu] = useState(false)
|
const [primaryMenu, setPrimaryMenu] = useState(false)
|
||||||
|
@ -41,9 +42,9 @@ function useApp(full = true) {
|
||||||
return {
|
return {
|
||||||
// Static vars
|
// Static vars
|
||||||
site: 'dev',
|
site: 'dev',
|
||||||
|
language,
|
||||||
|
|
||||||
// State
|
// State
|
||||||
language,
|
|
||||||
loading,
|
loading,
|
||||||
navigation,
|
navigation,
|
||||||
primaryMenu,
|
primaryMenu,
|
||||||
|
@ -51,7 +52,6 @@ function useApp(full = true) {
|
||||||
theme,
|
theme,
|
||||||
|
|
||||||
// State setters
|
// State setters
|
||||||
setLanguage,
|
|
||||||
setLoading,
|
setLoading,
|
||||||
setNavigation,
|
setNavigation,
|
||||||
setPrimaryMenu,
|
setPrimaryMenu,
|
||||||
|
@ -63,6 +63,10 @@ function useApp(full = true) {
|
||||||
|
|
||||||
// State handlers
|
// State handlers
|
||||||
togglePrimaryMenu,
|
togglePrimaryMenu,
|
||||||
|
|
||||||
|
// Dummy translation method
|
||||||
|
t: s => s,
|
||||||
|
i18n: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
3
packages/freesewing.lab/.eslintrc.json
Normal file
3
packages/freesewing.lab/.eslintrc.json
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
{
|
||||||
|
"extends": "next/core-web-vitals"
|
||||||
|
}
|
25
packages/freesewing.lab/components/about.js
Normal file
25
packages/freesewing.lab/components/about.js
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
import Popout from 'shared/components/popout.js'
|
||||||
|
|
||||||
|
const About = () => (
|
||||||
|
<Popout tip>
|
||||||
|
<h2>What to expect at lab.freesewing.org</h2>
|
||||||
|
<p>
|
||||||
|
The FreeSewing lab is an online environment to road-test our various patterns/designs.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
This website runs the bleeding edge of our code base.
|
||||||
|
Some patterns here may be unreleased or at various states of being worked on.
|
||||||
|
As such, this website is intended for FreeSewing contributors or people interested
|
||||||
|
in what happens under the hood.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
If you want sewing patterns to actually start making something,
|
||||||
|
please visit <a
|
||||||
|
href="https://freesewing.org/"
|
||||||
|
className="text-secondary font-bold hover-text-secondary-focus hover:underline"
|
||||||
|
>freesewing.org</a>, our flagship website for makers.
|
||||||
|
</p>
|
||||||
|
</Popout>
|
||||||
|
)
|
||||||
|
|
||||||
|
export default About
|
166
packages/freesewing.lab/components/footer.js
Normal file
166
packages/freesewing.lab/components/footer.js
Normal file
|
@ -0,0 +1,166 @@
|
||||||
|
import NextLink from 'next/link'
|
||||||
|
import Logo from 'shared/components/logos/freesewing.js'
|
||||||
|
import contributors from 'site/prebuild/allcontributors.js'
|
||||||
|
import patrons from 'site/prebuild/patrons.js'
|
||||||
|
import OsiLogo from 'shared/components/logos/osi.js'
|
||||||
|
import CreativeCommonsLogo from 'shared/components/logos/cc.js'
|
||||||
|
import CcByLogo from 'shared/components/logos/cc-by.js'
|
||||||
|
|
||||||
|
const Link = ({ href, txt }) => (
|
||||||
|
<NextLink href={href}>
|
||||||
|
<a title={txt} className="hover:underline text-secondary font-bold hover:pointer">{txt}</a>
|
||||||
|
</NextLink>
|
||||||
|
)
|
||||||
|
const link = "text-secondary font-bold hover:pointer hover:underline px-1"
|
||||||
|
|
||||||
|
const social = {
|
||||||
|
Discord: 'https://discord.freesewing.org/',
|
||||||
|
Instagram: 'https://instagram.com/freesewing_org',
|
||||||
|
Facebook: 'https://www.facebook.com/groups/627769821272714/',
|
||||||
|
Github: 'https://github.com/freesewing',
|
||||||
|
Reddit: 'https://www.reddit.com/r/freesewing/',
|
||||||
|
Twitter: 'https://twitter.com/freesewing_org',
|
||||||
|
}
|
||||||
|
|
||||||
|
const Footer = ({ app }) => (
|
||||||
|
<footer className="bg-neutral">
|
||||||
|
<div className={`theme-gradient h-1 w-full relative ${app.loading ? 'loading' : ''}`}></div>
|
||||||
|
<div className="p-4 py-16 flex flex-row bg-neutral -mt-1 z-0 gap-8 flex-wrap justify-around text-neutral-content">
|
||||||
|
<div className="w-64 mt-2">
|
||||||
|
<div className="px-4 mb-4"><CreativeCommonsLogo /></div>
|
||||||
|
<div className="flex flex-row gap-2 justify-center items-center">
|
||||||
|
<div className="basis-1/4">
|
||||||
|
<CcByLogo />
|
||||||
|
</div>
|
||||||
|
<p className="text-sm text-neutral-content text-right basis-3/4">
|
||||||
|
Content on freesewing.org is licensed under
|
||||||
|
a <a className={link} href="https://creativecommons.org/licenses/by/4.0/">Creative
|
||||||
|
Commons Attribution 4.0 International license</a>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-row gap-2 justify-center items-center">
|
||||||
|
<div className="basis-1/4">
|
||||||
|
<OsiLogo />
|
||||||
|
</div>
|
||||||
|
<p className="text-sm text-neutral-content text-right basis-3/4">
|
||||||
|
Our source code and markdown is <a href="https://github.com/freesewing/freesewing"
|
||||||
|
className={link}>available
|
||||||
|
on GitHub</a> under <a href="https://opensource.org/licenses/MIT"
|
||||||
|
className={link}>the MIT license</a>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="w-full sm:w-auto">
|
||||||
|
<h5 className="text-neutral-content">{app.t('whatIsThis')}</h5>
|
||||||
|
<div className="theme-gradient h-1 mb-4"></div>
|
||||||
|
<ul>
|
||||||
|
<li>
|
||||||
|
<Link href="https://freesewing.org/docs/guide/what" txt={app.t('aboutFreesewing')} />
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<Link href="https://freesewing.org/docs/faq" txt={app.t('faq')} />
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<Link href="https://freesewing.org/patrons/join" txt={app.t('becomeAPatron')} />
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="w-full sm:w-auto sm:max-w-xs">
|
||||||
|
<h5 className="text-neutral-content">Where can I turn for help?</h5>
|
||||||
|
<div className="theme-gradient h-1 mb-2"></div>
|
||||||
|
<p className="text-sm text-neutral-content">
|
||||||
|
<a className={link} href={social.discord}>Our Discord server</a> is
|
||||||
|
the best place to ask questions and get help. It's where our community hangs out
|
||||||
|
so you'll get the fastest response and might even make a few new friends along the way.
|
||||||
|
</p>
|
||||||
|
<p className="text-sm text-neutral-content">
|
||||||
|
You can also <a href={social.twitter} className={link} >reach out on Twitter</a> or <a
|
||||||
|
href="https://github.com/freesewing/freesewing/issues/new/choose"
|
||||||
|
className={link}
|
||||||
|
> create an issue on Github </a> if Discord is not your jam.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="w-full sm:w-auto">
|
||||||
|
<h5 className="text-neutral-content">Social Media</h5>
|
||||||
|
<div className="theme-gradient h-1 mb-4"></div>
|
||||||
|
<ul>
|
||||||
|
{Object.keys(social).map(item => <li key={item}><Link href={social[item]} txt={item}/></li>)}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="text-center">
|
||||||
|
<Logo fill='currentColor' stroke='none' size={164} className="m-auto"/>
|
||||||
|
<h5 className="text-neutral-content">FreeSewing</h5>
|
||||||
|
<p className="bold text-neutral-content text-sm">
|
||||||
|
Come for the sewing patterns
|
||||||
|
<br />
|
||||||
|
Stay for the community
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p className="text-center text-neutral-content text-sm px-2">
|
||||||
|
<span
|
||||||
|
className="px-1 text-lg font-bold block sm:inline">FreeSewing</span> is made by these <span
|
||||||
|
className="text-accent font-bold text-lg px-1 block sm:inline">wonderful contributors</span>
|
||||||
|
</p>
|
||||||
|
<div className="p-4 pb-16 flex flex-row bg-neutral -mt-2 z-0 gap-1 lg:gap-2 flex-wrap justify-around text-neutral-content lg:px-24">
|
||||||
|
{contributors.map(person => (
|
||||||
|
<a title={person.name} href={person.profile} className="m-auto" key={person.profile+person.name}>
|
||||||
|
<img
|
||||||
|
src={person.avatar_url} alt={`Avatar of ${person.name}`}
|
||||||
|
className="w-12 h-12 lg:w-16 lg:h-16 rounded-full border-2 border-secondary hover:border-accent"
|
||||||
|
/>
|
||||||
|
</a>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p className="text-center text-neutral-content text-sm px-2">
|
||||||
|
<span
|
||||||
|
className="px-1 text-lg font-bold block sm:inline">FreeSewing</span> is supported by these <span
|
||||||
|
className="text-accent font-bold text-lg px-1 block sm:inline">generous patrons</span>
|
||||||
|
</p>
|
||||||
|
<div className="p-4 pb-16 flex flex-row bg-neutral -mt-2 z-0 gap-1 lg:gap-2 flex-wrap justify-around text-neutral-content lg:px-24">
|
||||||
|
{patrons.map(person => (
|
||||||
|
<a
|
||||||
|
title={person.name}
|
||||||
|
href={`https://freesewing.org/users/${person.username}`}
|
||||||
|
className="m-auto"
|
||||||
|
key={person.username}
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
src={person.img}
|
||||||
|
alt={`Avatar of ${person.name}`}
|
||||||
|
className="w-12 h-12 lg:w-16 lg:h-16 rounded-full border-2 border-secondary hover:border-accent"
|
||||||
|
/>
|
||||||
|
</a>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p className="text-center text-neutral-content text-sm px-2">
|
||||||
|
<span
|
||||||
|
className="px-1 text-lg font-bold block sm:inline">FreeSewing</span> is hosted by these <span
|
||||||
|
className="text-accent font-bold text-lg px-1 block sm:inline">awesome companies</span>
|
||||||
|
</p>
|
||||||
|
<div className="p-4 py-16 flex flex-row bg-neutral -mt-2 z-0 gap-8 flex-wrap justify-center items-center text-neutral-content">
|
||||||
|
<a title="Search powered by Algolia" href="https://www.algolia.com/">
|
||||||
|
<img src="/brands/algolia.svg" className="w-64 mx-12 sm:mx-4" alt="Search powered by Algolia"/>
|
||||||
|
</a>
|
||||||
|
<a title="Translation powered by Crowdin" href="https://www.crowdin.com/">
|
||||||
|
<img src="/brands/crowdin.svg" className="w-64 mx-12 sm:mx-4" alt="Translation powered by Crowdin" />
|
||||||
|
</a>
|
||||||
|
<a title="Deploys & hosting by Netlify" href="https://www.netlify.com/">
|
||||||
|
<img src="/brands/netlify.svg" className="w-44 mx-12 sm:mx-4" alt="Deploys & hosting by Netlify" />
|
||||||
|
</a>
|
||||||
|
<a title="Error handling by Bugsnag" href="https://www.bugsnag.com/">
|
||||||
|
<img src="/brands/bugsnag.svg" className="h-36 mx-12 sm:mx-4" alt="Error handling by bugsnag" />
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</footer>
|
||||||
|
)
|
||||||
|
|
||||||
|
export default Footer
|
||||||
|
|
94
packages/freesewing.lab/components/header.js
Normal file
94
packages/freesewing.lab/components/header.js
Normal file
|
@ -0,0 +1,94 @@
|
||||||
|
import { useState, useEffect } from 'react'
|
||||||
|
import Logo from 'shared/components/logos/freesewing.js'
|
||||||
|
import Link from 'next/link'
|
||||||
|
import ThemePicker from 'shared/components/theme-picker.js'
|
||||||
|
import LanguagePicker from 'shared/components/language-picker.js'
|
||||||
|
import PatternPicker from 'site/components/pattern-picker.js'
|
||||||
|
import CloseIcon from 'shared/components/icons/close.js'
|
||||||
|
import MenuIcon from 'shared/components/icons/menu.js'
|
||||||
|
|
||||||
|
const Right = props => (
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" className="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M17 8l4 4m0 0l-4 4m4-4H3" />
|
||||||
|
</svg>
|
||||||
|
)
|
||||||
|
const Left = props => (
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" className="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M10 19l-7-7m0 0l7-7m-7 7h18" />
|
||||||
|
</svg>
|
||||||
|
)
|
||||||
|
|
||||||
|
const Header = ({ app }) => {
|
||||||
|
|
||||||
|
const [prevScrollPos, setPrevScrollPos] = useState(0)
|
||||||
|
const [show, setShow] = useState(true)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (typeof window !== 'undefined') {
|
||||||
|
const handleScroll = () => {
|
||||||
|
const curScrollPos = (typeof window !== 'undefined') ? window.pageYOffset : 0
|
||||||
|
if (curScrollPos >= prevScrollPos) {
|
||||||
|
if (show && curScrollPos > 20) setShow(false)
|
||||||
|
}
|
||||||
|
else setShow(true)
|
||||||
|
setPrevScrollPos(curScrollPos)
|
||||||
|
}
|
||||||
|
window.addEventListener('scroll', handleScroll)
|
||||||
|
return () => window.removeEventListener('scroll', handleScroll)
|
||||||
|
}
|
||||||
|
}, [prevScrollPos, show])
|
||||||
|
|
||||||
|
|
||||||
|
return (
|
||||||
|
<header className={`
|
||||||
|
fixed top-0 left-0
|
||||||
|
bg-neutral
|
||||||
|
w-full
|
||||||
|
z-30
|
||||||
|
transition-transform
|
||||||
|
${show ? '': 'fixed top-0 left-0 -translate-y-20'}
|
||||||
|
`}>
|
||||||
|
<div className="max-w-6xl m-auto">
|
||||||
|
<div className="p-2 flex flex-row gap-2 justify-between text-neutral-content">
|
||||||
|
<button
|
||||||
|
className={`
|
||||||
|
btn btn-sm
|
||||||
|
text-neutral-content bg-transparent
|
||||||
|
border border-transparent
|
||||||
|
hover:bg-transparent hover:border-base-100
|
||||||
|
sm:hidden
|
||||||
|
h-12
|
||||||
|
`}
|
||||||
|
onClick={app.togglePrimaryMenu}>
|
||||||
|
{app.primaryMenu
|
||||||
|
? <><CloseIcon /><span className="opacity-50 pl-2 flex flex-row items-center gap-1"><Left />swipe</span></>
|
||||||
|
: <><MenuIcon /><span className="opacity-50 pl-2 flex flex-row items-center gap-1"><Right />swipe</span></>
|
||||||
|
}
|
||||||
|
</button>
|
||||||
|
<div className="hidden sm:flex flex-row items-center">
|
||||||
|
<PatternPicker app={app} />
|
||||||
|
</div>
|
||||||
|
<div className="hidden md:flex md:flex-row gap-2">
|
||||||
|
<Link href="/">
|
||||||
|
<a className="flex flex-column items-center">
|
||||||
|
<Logo size={36} fill="currentColor" stroke={false} />
|
||||||
|
</a>
|
||||||
|
</Link>
|
||||||
|
<Link href="/">
|
||||||
|
<a role="button" className="btn btn-link btn-sm text-neutral-content h-12">
|
||||||
|
lab.freesewing.dev
|
||||||
|
</a>
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
<div className="hidden sm:flex flex-row items-center">
|
||||||
|
<ThemePicker app={app} />
|
||||||
|
<LanguagePicker app={app} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className={`theme-gradient h-1 w-full z-10 relative -mb-1 ${app.loading ? 'loading' : ''}`}></div>
|
||||||
|
</header>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Header
|
49
packages/freesewing.lab/components/help-us.js
Normal file
49
packages/freesewing.lab/components/help-us.js
Normal file
|
@ -0,0 +1,49 @@
|
||||||
|
import Popout from 'shared/components/popout.js'
|
||||||
|
|
||||||
|
const HelpUs = ({ mdx=false, slug='/' }) => (
|
||||||
|
<details className="mt-4">
|
||||||
|
<summary>Click here to learn how you can help us improve this page</summary>
|
||||||
|
{mdx && (
|
||||||
|
<Popout tip className='max-w-prose'>
|
||||||
|
<h6>Found a mistake?</h6>
|
||||||
|
You can <a
|
||||||
|
href={`https://github.com/freesewing/freesewing/edit/develop/markdown/dev/${slug}/en.md`}
|
||||||
|
className="text-secondary font-bold"
|
||||||
|
>edit this page on Github</a> and help us improve our documentation.
|
||||||
|
</Popout>
|
||||||
|
)}
|
||||||
|
<Popout comment by="joost" className='max-w-prose'>
|
||||||
|
<h6>Does this look ok?</h6>
|
||||||
|
<img
|
||||||
|
className="my-4 rounded"
|
||||||
|
src={`https://canary.backend.freesewing.org/og-img/en/dev${slug}`}
|
||||||
|
/>
|
||||||
|
<p>
|
||||||
|
If it looks ok, great! But if not, please let me know about it.
|
||||||
|
Either by <a href="https://discord.freesewing.org/" className="text-secondary">
|
||||||
|
reaching out on Discord
|
||||||
|
</a> or feel free to <a
|
||||||
|
href="https://github.com/freesewing/freesewing/issues/new/choose"
|
||||||
|
className="text-secondary"
|
||||||
|
>create
|
||||||
|
an issue on Github</a>.
|
||||||
|
</p>
|
||||||
|
<h6>Why do you ask?</h6>
|
||||||
|
<p className="text-base">
|
||||||
|
I recently added a backend endpoint to auto-generate pretty (I hope) Open Graph images.
|
||||||
|
They are those little preview images you see when you paste a link in Discord (for example).
|
||||||
|
</p>
|
||||||
|
<p className="text-base">
|
||||||
|
This idea is that it will auto-generate an image, but I am certain there are some edge cases
|
||||||
|
where that will not work.
|
||||||
|
There are hundreds of pages on this website and checking them all one by one is not something
|
||||||
|
I see myself doing. But since you are here on this page, perhaps you could see if the image
|
||||||
|
above looks ok.
|
||||||
|
</p>
|
||||||
|
<p className="text-base">Thank you, I really appreciate your help with this.</p>
|
||||||
|
</Popout>
|
||||||
|
</details>
|
||||||
|
)
|
||||||
|
|
||||||
|
export default HelpUs
|
||||||
|
|
43
packages/freesewing.lab/components/pattern-picker.js
Normal file
43
packages/freesewing.lab/components/pattern-picker.js
Normal file
|
@ -0,0 +1,43 @@
|
||||||
|
import React from 'react'
|
||||||
|
import config from 'site/freesewing.config.js'
|
||||||
|
import DesignIcon from 'shared/components/icons/design.js'
|
||||||
|
import Link from 'next/link'
|
||||||
|
|
||||||
|
const PatternPicker = ({ app }) => {
|
||||||
|
const { t } = app
|
||||||
|
return (
|
||||||
|
<div className="dropdown">
|
||||||
|
<div tabIndex="0" className={`
|
||||||
|
m-0 btn btn-neutral flex flex-row gap-2
|
||||||
|
sm:btn-ghost
|
||||||
|
hover:bg-neutral hover:border-neutral-content
|
||||||
|
`}>
|
||||||
|
<DesignIcon />
|
||||||
|
<span>Patterns</span>
|
||||||
|
</div>
|
||||||
|
<ul tabIndex="0" className="p-2 shadow menu dropdown-content bg-base-100 rounded-box w-52 max-h-96 overflow-y-scroll">
|
||||||
|
{Object.keys(config.patterns).map(section => (
|
||||||
|
<React.Fragment key={section}>
|
||||||
|
<li className={`
|
||||||
|
capitalize font-bold text-base-content text-center
|
||||||
|
opacity-50 border-b2 my-2 border-base-content
|
||||||
|
`}>
|
||||||
|
{t(config.navigation[section].__title)}
|
||||||
|
</li>
|
||||||
|
{config.patterns[section].map(pattern => (
|
||||||
|
<li key={pattern}>
|
||||||
|
<Link href={`/${section}/${pattern}`}>
|
||||||
|
<button className="btn btn-sm btn-ghost text-base-content">
|
||||||
|
{pattern}
|
||||||
|
</button>
|
||||||
|
</Link>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</React.Fragment>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default PatternPicker
|
1
packages/freesewing.lab/components/search.js
Normal file
1
packages/freesewing.lab/components/search.js
Normal file
|
@ -0,0 +1 @@
|
||||||
|
export default () => null
|
103
packages/freesewing.lab/freesewing.config.js
Normal file
103
packages/freesewing.lab/freesewing.config.js
Normal file
|
@ -0,0 +1,103 @@
|
||||||
|
const patterns = {
|
||||||
|
accessories: [
|
||||||
|
'florence',
|
||||||
|
'hortensia',
|
||||||
|
'florent',
|
||||||
|
'holmes',
|
||||||
|
],
|
||||||
|
blocks: [
|
||||||
|
'bella',
|
||||||
|
'bent',
|
||||||
|
'brian',
|
||||||
|
'titan',
|
||||||
|
],
|
||||||
|
garments: [
|
||||||
|
'aaron',
|
||||||
|
'albert',
|
||||||
|
'bee',
|
||||||
|
'benjamin',
|
||||||
|
'breanna',
|
||||||
|
'bruce',
|
||||||
|
'carlita',
|
||||||
|
'carlton',
|
||||||
|
'cathrin',
|
||||||
|
'charlie',
|
||||||
|
'cornelius',
|
||||||
|
'diana',
|
||||||
|
'huey',
|
||||||
|
'hugo',
|
||||||
|
'jaeger',
|
||||||
|
'lunetius',
|
||||||
|
'paco',
|
||||||
|
'penelope',
|
||||||
|
'sandy',
|
||||||
|
'shin',
|
||||||
|
'simon',
|
||||||
|
'simone',
|
||||||
|
'sven',
|
||||||
|
'tamiko',
|
||||||
|
'teagan',
|
||||||
|
'theo',
|
||||||
|
'tiberius',
|
||||||
|
'trayvon',
|
||||||
|
'ursula',
|
||||||
|
'wahid',
|
||||||
|
'walburga',
|
||||||
|
'waralee',
|
||||||
|
'yuri',
|
||||||
|
],
|
||||||
|
utilities: [
|
||||||
|
'examples',
|
||||||
|
'legend',
|
||||||
|
'plugintest',
|
||||||
|
'rendertest',
|
||||||
|
'tutorial',
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
const navigation = {
|
||||||
|
accessories: {
|
||||||
|
__title: 'accessoryPatterns',
|
||||||
|
__order: 'accessoryPatterns',
|
||||||
|
__linktitle: 'accessoryPatterns',
|
||||||
|
__slug: 'accessories',
|
||||||
|
},
|
||||||
|
blocks: {
|
||||||
|
__title: 'blockPatterns',
|
||||||
|
__order: 'blockPatterns',
|
||||||
|
__linktitle: 'blockPatterns',
|
||||||
|
__slug: 'blocks',
|
||||||
|
},
|
||||||
|
garments: {
|
||||||
|
__title: 'garmentPatterns',
|
||||||
|
__order: 'garmentPatterns',
|
||||||
|
__linktitle: 'GarmentPatterns',
|
||||||
|
__slug: 'garments',
|
||||||
|
},
|
||||||
|
utilities: {
|
||||||
|
__title: 'utilityPatterns',
|
||||||
|
__order: 'utilityPatterns',
|
||||||
|
__linktitle: 'utilityPatterns',
|
||||||
|
__slug: 'utilities',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for (const type in patterns) {
|
||||||
|
for (const design of patterns[type]) {
|
||||||
|
navigation[type][design] = {
|
||||||
|
__title: design,
|
||||||
|
__order: design,
|
||||||
|
__linktitle: design,
|
||||||
|
__slug: `${type}/${design}`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const config = {
|
||||||
|
monorepo: 'https://github.com/freesewing/freesewing',
|
||||||
|
navigation,
|
||||||
|
patterns,
|
||||||
|
}
|
||||||
|
|
||||||
|
export default config
|
||||||
|
|
102
packages/freesewing.lab/hooks/useApp.js
Normal file
102
packages/freesewing.lab/hooks/useApp.js
Normal file
|
@ -0,0 +1,102 @@
|
||||||
|
import { useState } from 'react'
|
||||||
|
import set from 'lodash.set'
|
||||||
|
// Stores state in local storage
|
||||||
|
import useLocalStorage from 'shared/hooks/useLocalStorage.js'
|
||||||
|
// config
|
||||||
|
import config from 'site/freesewing.config.js'
|
||||||
|
// Languages
|
||||||
|
import { strings } from 'pkgs/i18n'
|
||||||
|
|
||||||
|
const translateNavigation = (lang, t) => {
|
||||||
|
const newNav = {...config.navigation}
|
||||||
|
for (const key in newNav) {
|
||||||
|
const translated = t(newNav[key].__title, false, lang)
|
||||||
|
newNav[key].__title = translated
|
||||||
|
newNav[key].__linktitle = translated
|
||||||
|
newNav[key].__order = translated
|
||||||
|
}
|
||||||
|
|
||||||
|
return newNav
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function useApp(full = true) {
|
||||||
|
|
||||||
|
// User color scheme preference
|
||||||
|
const prefersDarkMode = (typeof window !== 'undefined' && typeof window.matchMedia === 'function')
|
||||||
|
? window.matchMedia(`(prefers-color-scheme: dark`).matches
|
||||||
|
: null
|
||||||
|
|
||||||
|
// Persistent state
|
||||||
|
const [account, setAccount] = useLocalStorage('account', { username: false })
|
||||||
|
const [theme, setTheme] = useLocalStorage('theme', prefersDarkMode ? 'dark' : 'light')
|
||||||
|
const [language, setLanguage] = useLocalStorage('language', 'en')
|
||||||
|
|
||||||
|
// React State
|
||||||
|
const [primaryMenu, setPrimaryMenu] = useState(false)
|
||||||
|
const [navigation, setNavigation] = useState(config.navigation)
|
||||||
|
const [slug, setSlug] = useState('/')
|
||||||
|
const [pattern, setPattern] = useState(false)
|
||||||
|
const [loading, setLoading] = useState(false)
|
||||||
|
|
||||||
|
// State methods
|
||||||
|
const togglePrimaryMenu = () => setPrimaryMenu(!primaryMenu)
|
||||||
|
const changeLanguage = lang => {
|
||||||
|
setLanguage(lang)
|
||||||
|
setNavigation(translateNavigation(lang, t))
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Translation method
|
||||||
|
*/
|
||||||
|
const t = (key, props=false, toLanguage=false) => {
|
||||||
|
if (!toLanguage) toLanguage = language
|
||||||
|
if (!props) { // easy
|
||||||
|
if (strings[toLanguage][key]) return strings[toLanguage][key]
|
||||||
|
// app is the most common prefix, so we allow to skip it
|
||||||
|
if (strings[toLanguage][`app.${key}`]) return strings[toLanguage][`app.${key}`]
|
||||||
|
// Can we fall back to English?
|
||||||
|
if (toLanguage !== 'en') {
|
||||||
|
if (strings.en[key]) return strings.en[key]
|
||||||
|
if (strings.en[`app.${key}`]) return strings.en[`app.${key}`]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
console.log('Missing translation key:', key)
|
||||||
|
|
||||||
|
return key
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
// Static vars
|
||||||
|
site: 'lab',
|
||||||
|
|
||||||
|
// State
|
||||||
|
language,
|
||||||
|
loading,
|
||||||
|
navigation,
|
||||||
|
pattern,
|
||||||
|
primaryMenu,
|
||||||
|
slug,
|
||||||
|
theme,
|
||||||
|
|
||||||
|
// State setters
|
||||||
|
setLoading,
|
||||||
|
setNavigation,
|
||||||
|
setPattern,
|
||||||
|
setPrimaryMenu,
|
||||||
|
setSlug,
|
||||||
|
setTheme,
|
||||||
|
startLoading: () => { setLoading(true); setPrimaryMenu(false) }, // Always close menu when navigating
|
||||||
|
stopLoading: () => setLoading(false),
|
||||||
|
|
||||||
|
// State handlers
|
||||||
|
togglePrimaryMenu,
|
||||||
|
changeLanguage,
|
||||||
|
|
||||||
|
// Translation
|
||||||
|
t,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default useApp
|
||||||
|
|
3
packages/freesewing.lab/next.config.mjs
Normal file
3
packages/freesewing.lab/next.config.mjs
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
import configBuilder from '../freesewing.shared/config/next.mjs'
|
||||||
|
|
||||||
|
export default configBuilder('lab')
|
54
packages/freesewing.lab/package.json
Normal file
54
packages/freesewing.lab/package.json
Normal file
|
@ -0,0 +1,54 @@
|
||||||
|
{
|
||||||
|
"name": "freesewing.dev",
|
||||||
|
"version": "2.19.9",
|
||||||
|
"private": true,
|
||||||
|
"scripts": {
|
||||||
|
"dev": "next dev -p 3002",
|
||||||
|
"develop": "next dev -p 3002",
|
||||||
|
"prebuild": "SITE=dev node ../freesewing.shared/prebuild/index.mjs",
|
||||||
|
"build": "next build",
|
||||||
|
"export": "yarn prebuild && next build && next export",
|
||||||
|
"start": "next start -p 3002",
|
||||||
|
"lint": "next lint",
|
||||||
|
"testdeploy": "next build && next export && netlify-cli deploy",
|
||||||
|
"deploy": "next build && next export && netlify-cli deploy --prod",
|
||||||
|
"serve": "pm2 start npm --name 'freesewing.dev' -- run start"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@heroicons/react": "^1.0.5",
|
||||||
|
"@mdx-js/loader": "^2.0.0-rc.2",
|
||||||
|
"@mdx-js/mdx": "^2.0.0-rc.2",
|
||||||
|
"@mdx-js/react": "^2.0.0-rc.2",
|
||||||
|
"@mdx-js/runtime": "next",
|
||||||
|
"@tailwindcss/typography": "^0.5.0",
|
||||||
|
"algoliasearch": "^4.11.0",
|
||||||
|
"daisyui": "^1.16.2",
|
||||||
|
"lodash.get": "^4.4.2",
|
||||||
|
"lodash.orderby": "^4.6.0",
|
||||||
|
"lodash.set": "^4.3.2",
|
||||||
|
"netlify-cli": "^8.4.2",
|
||||||
|
"next": "latest",
|
||||||
|
"react-copy-to-clipboard": "^5.0.4",
|
||||||
|
"react-hotkeys-hook": "^3.4.4",
|
||||||
|
"react-instantsearch-dom": "^6.18.0",
|
||||||
|
"react-markdown": "^7.1.1",
|
||||||
|
"react-swipeable": "^6.2.0",
|
||||||
|
"react-timeago": "^6.2.1",
|
||||||
|
"rehype-highlight": "^5.0.1",
|
||||||
|
"rehype-sanitize": "^5.0.1",
|
||||||
|
"rehype-stringify": "^9.0.2",
|
||||||
|
"remark-copy-linked-files": "https://github.com/joostdecock/remark-copy-linked-files",
|
||||||
|
"remark-gfm": "^3.0.1",
|
||||||
|
"remark-jargon": "^2.19.9"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"autoprefixer": "^10.4.0",
|
||||||
|
"eslint-config-next": "12.0.7",
|
||||||
|
"js-yaml": "^4.1.0",
|
||||||
|
"postcss": "^8.4.4",
|
||||||
|
"tailwindcss": "^3.0.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=14.18.1"
|
||||||
|
}
|
||||||
|
}
|
51
packages/freesewing.lab/page-templates/pattern-list.js
Normal file
51
packages/freesewing.lab/page-templates/pattern-list.js
Normal file
|
@ -0,0 +1,51 @@
|
||||||
|
import Page from 'shared/components/wrappers/page.js'
|
||||||
|
import useApp from 'site/hooks/useApp.js'
|
||||||
|
import Head from 'next/head'
|
||||||
|
import Link from 'next/link'
|
||||||
|
import config from 'site/freesewing.config.js'
|
||||||
|
import About from 'site/components/about.js'
|
||||||
|
|
||||||
|
const links = (section, list) => list.map(design => (
|
||||||
|
<li key={design} className="">
|
||||||
|
<Link href={`/${section}/${design}`}>
|
||||||
|
<a className="text-secondary text-xl capitalize">{design}</a>
|
||||||
|
</Link>
|
||||||
|
</li>
|
||||||
|
))
|
||||||
|
|
||||||
|
const ListPage = ({ sections=Object.keys(config.patterns) }) => {
|
||||||
|
const app = useApp()
|
||||||
|
return (
|
||||||
|
<Page app={app} title="FreeSewing Lab" noSearch>
|
||||||
|
<Head>
|
||||||
|
<meta property="og:title" content="lab.FreeSewing.dev" key="title" />
|
||||||
|
<meta property="og:type" content="article" key='type' />
|
||||||
|
<meta property="og:description" content="The FreeSewing lab is an online test environment for all our patterns" key='description' />
|
||||||
|
<meta property="og:article:author" content='Joost De Cock' key='author' />
|
||||||
|
<meta property="og:image" content="https://canary.backend.freesewing.org/og-img/en/lab/" 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://lab.freesewing.dev/" key='url' />
|
||||||
|
<meta property="og:locale" content="en_US" key='locale' />
|
||||||
|
<meta property="og:site_name" content="lab.freesewing.dev" key='site' />
|
||||||
|
</Head>
|
||||||
|
<div className="max-w-screen-md">
|
||||||
|
{Object.keys(config.navigation).map(section => {
|
||||||
|
if (sections.indexOf(section) !== -1) return (
|
||||||
|
<div>
|
||||||
|
<h2>{config.navigation[section].__title}</h2>
|
||||||
|
<ul className="flex flex-row flex-wrap gap-2">
|
||||||
|
{links(section, config.patterns[section])}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
else return null
|
||||||
|
})}
|
||||||
|
<About />
|
||||||
|
</div>
|
||||||
|
</Page>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ListPage
|
16
packages/freesewing.lab/page-templates/workbench.js
Normal file
16
packages/freesewing.lab/page-templates/workbench.js
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
import Page from 'shared/components/wrappers/page.js'
|
||||||
|
import useApp from 'site/hooks/useApp.js'
|
||||||
|
import WorkbenchWrapper from 'shared/components/wrappers/workbench.js'
|
||||||
|
|
||||||
|
const WorkbenchPage = ({ pattern }) => {
|
||||||
|
const app = useApp()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Page app={app} noLayout>
|
||||||
|
<WorkbenchWrapper app={app} pattern={pattern} />
|
||||||
|
</Page>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default WorkbenchPage
|
||||||
|
|
5
packages/freesewing.lab/pages/_app.js
Normal file
5
packages/freesewing.lab/pages/_app.js
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
import 'shared/styles/globals.css'
|
||||||
|
|
||||||
|
const FreeSewingLab = ({ Component, pageProps }) => <Component {...pageProps} />
|
||||||
|
|
||||||
|
export default FreeSewingLab
|
5
packages/freesewing.lab/pages/accessories/index.js
Normal file
5
packages/freesewing.lab/pages/accessories/index.js
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
import Template from 'site/page-templates/pattern-list.js'
|
||||||
|
|
||||||
|
const Page = props => <Template sections={['accessories']} />
|
||||||
|
|
||||||
|
export default Page
|
5
packages/freesewing.lab/pages/blocks/index.js
Normal file
5
packages/freesewing.lab/pages/blocks/index.js
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
import Template from 'site/page-templates/pattern-list.js'
|
||||||
|
|
||||||
|
const Page = props => <Template sections={['blocks']} />
|
||||||
|
|
||||||
|
export default Page
|
4
packages/freesewing.lab/pages/garments/aaron.js
Normal file
4
packages/freesewing.lab/pages/garments/aaron.js
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
import pattern from 'pkgs/aaron/src/index.js'
|
||||||
|
import PageTemplate from 'site/page-templates/workbench.js'
|
||||||
|
|
||||||
|
export default () => <PageTemplate pattern={pattern} />
|
5
packages/freesewing.lab/pages/garments/index.js
Normal file
5
packages/freesewing.lab/pages/garments/index.js
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
import Template from 'site/page-templates/pattern-list.js'
|
||||||
|
|
||||||
|
const Page = props => <Template sections={['garments']} />
|
||||||
|
|
||||||
|
export default Page
|
8
packages/freesewing.lab/pages/garments/teagan.js
Normal file
8
packages/freesewing.lab/pages/garments/teagan.js
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
import pattern from 'pkgs/teagan/src/index.js'
|
||||||
|
import PageTemplate from 'site/page-templates/workbench.js'
|
||||||
|
|
||||||
|
const Teagan = function() {
|
||||||
|
return <PageTemplate pattern={pattern} />
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Teagan
|
3
packages/freesewing.lab/pages/index.js
Normal file
3
packages/freesewing.lab/pages/index.js
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
import Page from 'site/page-templates/pattern-list.js'
|
||||||
|
|
||||||
|
export default Page
|
5
packages/freesewing.lab/pages/utilities/index.js
Normal file
5
packages/freesewing.lab/pages/utilities/index.js
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
import Template from 'site/page-templates/pattern-list.js'
|
||||||
|
|
||||||
|
const Page = props => <Template sections={['utilities']} />
|
||||||
|
|
||||||
|
export default Page
|
5
packages/freesewing.lab/postcss.config.js
Normal file
5
packages/freesewing.lab/postcss.config.js
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
// Can't seem to make this work as ESM
|
||||||
|
const config = require('../freesewing.shared/config/postcss.config.js')
|
||||||
|
|
||||||
|
module.exports = config
|
||||||
|
|
1
packages/freesewing.lab/public/brands/algolia.svg
Normal file
1
packages/freesewing.lab/public/brands/algolia.svg
Normal file
File diff suppressed because one or more lines are too long
After Width: | Height: | Size: 7.7 KiB |
15
packages/freesewing.lab/public/brands/bugsnag.svg
Normal file
15
packages/freesewing.lab/public/brands/bugsnag.svg
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!-- Generator: Adobe Illustrator 22.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||||
|
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||||
|
viewBox="0 0 900 1295.5" style="enable-background:new 0 0 900 1295.5;" xml:space="preserve">
|
||||||
|
<style type="text/css">
|
||||||
|
.st0{fill:#FF5A60;}
|
||||||
|
</style>
|
||||||
|
<circle class="st0" cx="450" cy="845.5" r="53.7"/>
|
||||||
|
<path class="st0" d="M450,1295.5c-248.1,0-450-201.9-450-450V622.3c0-18.5,15-33.5,33.5-33.5h159.6l-0.6-516.6L66.9,149.5v294.2
|
||||||
|
c0,18.5-15,33.5-33.5,33.5S0,462.2,0,443.7v-298c0-20.7,11-40.4,28.6-51.2L167.8,8.9C186.4-2.5,209.7-3,228.7,7.6
|
||||||
|
s30.8,30.7,30.8,52.5l0.6,528.8h190c141.5,0,256.6,115.1,256.6,256.6s-115.1,256.6-256.6,256.6S193.5,987,193.5,845.5l-0.2-189.7
|
||||||
|
H66.9v189.7c0,211.2,171.8,383.1,383.1,383.1s383.1-171.8,383.1-383.1S661.2,462.4,450,462.4h-58.1c-18.5,0-33.5-15-33.5-33.5
|
||||||
|
s15-33.5,33.5-33.5H450c248.1,0,450,201.9,450,450S698.1,1295.5,450,1295.5z M260.1,655.8l0.2,189.7c0,104.6,85.1,189.7,189.7,189.7
|
||||||
|
s189.7-85.1,189.7-189.7S554.6,655.8,450,655.8H260.1z"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 1.1 KiB |
61
packages/freesewing.lab/public/brands/crowdin.svg
Normal file
61
packages/freesewing.lab/public/brands/crowdin.svg
Normal file
File diff suppressed because one or more lines are too long
After Width: | Height: | Size: 9.2 KiB |
85
packages/freesewing.lab/public/brands/netlify.svg
Normal file
85
packages/freesewing.lab/public/brands/netlify.svg
Normal file
File diff suppressed because one or more lines are too long
After Width: | Height: | Size: 22 KiB |
BIN
packages/freesewing.lab/public/favicon.ico
Normal file
BIN
packages/freesewing.lab/public/favicon.ico
Normal file
Binary file not shown.
After Width: | Height: | Size: 25 KiB |
BIN
packages/freesewing.lab/public/support.jpg
Normal file
BIN
packages/freesewing.lab/public/support.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 313 KiB |
150
packages/freesewing.lab/scripts/algolia.mjs
Normal file
150
packages/freesewing.lab/scripts/algolia.mjs
Normal file
|
@ -0,0 +1,150 @@
|
||||||
|
/*
|
||||||
|
* This will update (replace really) the Algolia index with the
|
||||||
|
* current website contents. Or at least the markdown and Strapi
|
||||||
|
* content
|
||||||
|
*
|
||||||
|
* It expects the following environment vars to be set in a
|
||||||
|
* .env file in the 'packages/freesewing.dev' folder:
|
||||||
|
*
|
||||||
|
* ALGOLIA_APP_ID -> probably MA0Y5A2PF0
|
||||||
|
* ALGOLIA_API_KEY -> Needs permission to index/create/delete
|
||||||
|
* ALGOLIA_INDEX -> Name of the index to index to
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
import dotenv from 'dotenv'
|
||||||
|
import fs from 'fs'
|
||||||
|
import path from 'path'
|
||||||
|
import algoliasearch from 'algoliasearch'
|
||||||
|
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 remarkRehype from 'remark-rehype'
|
||||||
|
import rehypeSanitize from 'rehype-sanitize'
|
||||||
|
import rehypeStringify from 'rehype-stringify'
|
||||||
|
import yaml from 'yaml'
|
||||||
|
import { getPosts } from '../../freesewing.shared/prebuild/strapi.mjs'
|
||||||
|
import { getMdxFileList } from '../../freesewing.shared/prebuild/mdx.mjs'
|
||||||
|
dotenv.config()
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Initialize Algolia client
|
||||||
|
*/
|
||||||
|
const client = algoliasearch(process.env.ALGOLIA_APP_ID, process.env.ALGOLIA_API_KEY)
|
||||||
|
const index = client.initIndex(process.env.ALGOLIA_INDEX)
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Turn a Strapi blog post into an object ready for indexing
|
||||||
|
*/
|
||||||
|
const transformBlogpost = post => ({
|
||||||
|
objectID: `/blog/${post.slug}`,
|
||||||
|
page: `/blog/${post.slug}`,
|
||||||
|
title: post.title,
|
||||||
|
date: post.date,
|
||||||
|
slug: post.slug,
|
||||||
|
body: post.body,
|
||||||
|
author: post.author,
|
||||||
|
caption: post.caption,
|
||||||
|
type: 'blog',
|
||||||
|
})
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Turn a Strapi author into an object ready for indexing
|
||||||
|
*/
|
||||||
|
const transformAuthor = author => ({
|
||||||
|
objectID: `/blog/authors/${author.name}`,
|
||||||
|
page: `/blog/authors/${author.name}`,
|
||||||
|
name: author.name,
|
||||||
|
displayname: author.displayname,
|
||||||
|
about: author.about,
|
||||||
|
})
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Get and index blog posts and author info from Strapi
|
||||||
|
*/
|
||||||
|
const indexStrapiContent = async () => {
|
||||||
|
|
||||||
|
// Say hi
|
||||||
|
console.log()
|
||||||
|
console.log(`Indexing Strapi content to Algolia`)
|
||||||
|
|
||||||
|
const authors = {}
|
||||||
|
const rawPosts = await getPosts('blog', 'dev', 'en')
|
||||||
|
// Extract list of authors
|
||||||
|
for (const [slug, post] of Object.entries(rawPosts)) {
|
||||||
|
authors[post.author.slug] = transformAuthor(post.author)
|
||||||
|
rawPosts[slug].author = post.author.slug
|
||||||
|
}
|
||||||
|
// Index posts to Algolia
|
||||||
|
index
|
||||||
|
.saveObjects(Object.values(rawPosts).map(post => transformBlogpost(post)))
|
||||||
|
.then(({ objectIDs }) => console.log(objectIDs))
|
||||||
|
.catch(err => console.log(err))
|
||||||
|
// Index authors to Algolia
|
||||||
|
index
|
||||||
|
.saveObjects(Object.values(authors))
|
||||||
|
.then(({ objectIDs }) => console.log(objectIDs))
|
||||||
|
.catch(err => console.log(err))
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Loads markdown from disk and compiles it into HTML for indexing
|
||||||
|
*/
|
||||||
|
const markdownLoader = async file => {
|
||||||
|
|
||||||
|
const md = await fs.promises.readFile(file, 'utf-8')
|
||||||
|
|
||||||
|
const page = await unified()
|
||||||
|
.use(remarkParser)
|
||||||
|
.use(remarkCompiler)
|
||||||
|
.use(remarkFrontmatter)
|
||||||
|
.use(remarkFrontmatterExtractor, { yaml: yaml.parse })
|
||||||
|
.use(remarkRehype)
|
||||||
|
.use(rehypeSanitize)
|
||||||
|
.use(rehypeStringify)
|
||||||
|
.process(md)
|
||||||
|
const id = file.split('freesewing/markdown/dev').pop().slice(0, -6)
|
||||||
|
|
||||||
|
return {
|
||||||
|
objectID: id,
|
||||||
|
page: id,
|
||||||
|
title: page.data.title,
|
||||||
|
body: page.value,
|
||||||
|
type: 'docs',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Get and index markdown content
|
||||||
|
*/
|
||||||
|
const indexMarkdownContent = async () => {
|
||||||
|
|
||||||
|
// Say hi
|
||||||
|
console.log()
|
||||||
|
console.log(`Indexing Markdown content to Algolia`)
|
||||||
|
|
||||||
|
// Setup MDX root path
|
||||||
|
const mdxRoot = path.resolve('..', '..', 'markdown', 'dev')
|
||||||
|
|
||||||
|
// Get list of filenames
|
||||||
|
const list = await getMdxFileList(mdxRoot, 'en')
|
||||||
|
const pages = []
|
||||||
|
for (const file of list) pages.push(await markdownLoader(file))
|
||||||
|
// Index markdown to Algolia
|
||||||
|
index
|
||||||
|
.saveObjects(pages)
|
||||||
|
.then(({ objectIDs }) => console.log(objectIDs))
|
||||||
|
.catch(err => console.log(err))
|
||||||
|
}
|
||||||
|
|
||||||
|
const run = async () => {
|
||||||
|
await indexMarkdownContent()
|
||||||
|
await indexStrapiContent()
|
||||||
|
console.log()
|
||||||
|
}
|
||||||
|
|
||||||
|
run()
|
||||||
|
|
4
packages/freesewing.lab/tailwind.config.js
Normal file
4
packages/freesewing.lab/tailwind.config.js
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
// Can't seem to make this work as ESM
|
||||||
|
const config = require('../freesewing.shared/config/tailwind.config.js')
|
||||||
|
|
||||||
|
module.exports = config
|
7
packages/freesewing.shared/components/icons/options.js
Normal file
7
packages/freesewing.shared/components/icons/options.js
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
const OptionsIcon = () => (
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" className="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 6V4m0 2a2 2 0 100 4m0-4a2 2 0 110 4m-6 8a2 2 0 100-4m0 4a2 2 0 110-4m0 4v2m0-6V4m6 6v10m6-2a2 2 0 100-4m0 4a2 2 0 110-4m0 4v2m0-6V4" />
|
||||||
|
</svg>
|
||||||
|
)
|
||||||
|
|
||||||
|
export default OptionsIcon
|
8
packages/freesewing.shared/components/icons/settings.js
Normal file
8
packages/freesewing.shared/components/icons/settings.js
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
const SettingsIcon = () => (
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" className="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z" />
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
|
||||||
|
</svg>
|
||||||
|
)
|
||||||
|
|
||||||
|
export default SettingsIcon
|
29
packages/freesewing.shared/components/language-picker.js
Normal file
29
packages/freesewing.shared/components/language-picker.js
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
import themes from 'shared/themes/index.js'
|
||||||
|
import LanguageIcon from 'shared/components/icons/i18n.js'
|
||||||
|
import { languages } from 'pkgs/i18n'
|
||||||
|
|
||||||
|
const LanguagePicker = ({ app }) => {
|
||||||
|
return (
|
||||||
|
<div className="dropdown">
|
||||||
|
<div tabIndex="0" className={`
|
||||||
|
m-0 btn btn-neutral flex flex-row gap-2
|
||||||
|
sm:btn-ghost
|
||||||
|
hover:bg-neutral hover:border-neutral-content
|
||||||
|
`}>
|
||||||
|
<LanguageIcon />
|
||||||
|
<span>{languages[app.language]}</span>
|
||||||
|
</div>
|
||||||
|
<ul tabIndex="0" className="p-2 shadow menu dropdown-content bg-base-100 rounded-box w-52">
|
||||||
|
{Object.keys(languages).map(language => (
|
||||||
|
<li key={language}>
|
||||||
|
<button onClick={() => app.changeLanguage(language)} className="btn btn-ghost text-base-content hover:bg-base-200">
|
||||||
|
{languages[language]}
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default LanguagePicker
|
|
@ -6,6 +6,8 @@ import Link from 'next/link'
|
||||||
import Logo from 'shared/components/logos/freesewing.js'
|
import Logo from 'shared/components/logos/freesewing.js'
|
||||||
import PrimaryNavigation from 'shared/components/navigation/primary'
|
import PrimaryNavigation from 'shared/components/navigation/primary'
|
||||||
import get from 'lodash.get'
|
import get from 'lodash.get'
|
||||||
|
import Right from 'shared/components/icons/right.js'
|
||||||
|
import Left from 'shared/components/icons/left.js'
|
||||||
// Site components
|
// Site components
|
||||||
import Header from 'site/components/header'
|
import Header from 'site/components/header'
|
||||||
import Footer from 'site/components/footer'
|
import Footer from 'site/components/footer'
|
||||||
|
@ -58,9 +60,34 @@ const Breadcrumbs = ({ app, slug=false, title }) => {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const asideClasses = `
|
||||||
|
fixed top-0 right-0
|
||||||
|
pt-28
|
||||||
|
sm:pt-8 sm:mt-16
|
||||||
|
pb-4 px-2
|
||||||
|
sm:relative sm:transform-none
|
||||||
|
h-screen w-screen
|
||||||
|
bg-base-100
|
||||||
|
sm:bg-base-50
|
||||||
|
sm:flex
|
||||||
|
sm:sticky
|
||||||
|
overflow-y-scroll
|
||||||
|
z-20
|
||||||
|
bg-base-100 text-base-content
|
||||||
|
sm:bg-neutral sm:bg-opacity-95 sm:text-neutral-content
|
||||||
|
transition-all `
|
||||||
|
|
||||||
const DefaultLayout = ({ app, title=false, children=[], search, setSearch}) => {
|
|
||||||
|
|
||||||
|
const DefaultLayout = ({
|
||||||
|
app,
|
||||||
|
title=false,
|
||||||
|
children=[],
|
||||||
|
search,
|
||||||
|
setSearch,
|
||||||
|
noSearch=false,
|
||||||
|
workbench=false,
|
||||||
|
AltMenu=null,
|
||||||
|
}) => {
|
||||||
const startNavigation = () => {
|
const startNavigation = () => {
|
||||||
app.startLoading()
|
app.startLoading()
|
||||||
// Force close of menu on mobile if it is open
|
// Force close of menu on mobile if it is open
|
||||||
|
@ -73,7 +100,8 @@ const DefaultLayout = ({ app, title=false, children=[], search, setSearch}) => {
|
||||||
router.events?.on('routeChangeStart', startNavigation)
|
router.events?.on('routeChangeStart', startNavigation)
|
||||||
router.events?.on('routeChangeComplete', () => app.stopLoading())
|
router.events?.on('routeChangeComplete', () => app.stopLoading())
|
||||||
const slug = router.asPath.slice(1)
|
const slug = router.asPath.slice(1)
|
||||||
const [leftNav, setLeftNav] = useState(false)
|
const [collapsePrimaryNav, setCollapsePrimaryNav] = useState(workbench || false)
|
||||||
|
const [collapseAltMenu, setCollapseAltMenu] = useState(false)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={`
|
<div className={`
|
||||||
|
@ -85,35 +113,41 @@ const DefaultLayout = ({ app, title=false, children=[], search, setSearch}) => {
|
||||||
<main className={`
|
<main className={`
|
||||||
grow flex flex-row
|
grow flex flex-row
|
||||||
gap-2
|
gap-2
|
||||||
lg:gap-8
|
${!workbench ? 'lg:gap-8 xl:gap-16' : ''}
|
||||||
xl:gap-16
|
|
||||||
`}>
|
`}>
|
||||||
<aside className={`
|
<aside className={`
|
||||||
fixed top-0 right-0
|
${asideClasses}
|
||||||
${app.primaryMenu ? '' : 'translate-x-[-100%]'} transition-transform
|
${app.primaryMenu ? '' : 'translate-x-[-100%]'} transition-transform
|
||||||
pt-28
|
sm:flex-row-reverse
|
||||||
sm:pt-8 sm:mt-16
|
${workbench && collapsePrimaryNav
|
||||||
pb-4 px-2
|
? 'sm:px-0 sm:w-16'
|
||||||
sm:px-1 md:px-4 lg:px-8 xl:px-16 2xl:px-32
|
: 'sm:px-1 md:px-4 lg:px-8 xl:px-16 2xl:px-32 sm:w-[38.2%]'
|
||||||
sm:relative sm:transform-none
|
}
|
||||||
h-screen w-screen
|
|
||||||
bg-base-100
|
|
||||||
sm:bg-base-50
|
|
||||||
sm:max-w-[38.2%]
|
|
||||||
sm:flex sm:flex-row-reverse
|
|
||||||
sm:sticky
|
|
||||||
overflow-y-scroll
|
|
||||||
z-20
|
|
||||||
bg-base-100 text-base-content
|
|
||||||
sm:bg-neutral sm:bg-opacity-95 sm:text-neutral-content
|
|
||||||
`}>
|
`}>
|
||||||
|
{workbench && (
|
||||||
|
<div className={`hidden sm:flex`}>
|
||||||
|
<button
|
||||||
|
className="text-secondary-focus h-full px-2 pl-4 hover:animate-pulse"
|
||||||
|
onClick={() => setCollapsePrimaryNav(!collapsePrimaryNav)}
|
||||||
|
>
|
||||||
|
{collapsePrimaryNav
|
||||||
|
? <><Right /><Right /><Right /></>
|
||||||
|
: <><Left /><Left /><Left /></>
|
||||||
|
}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
<PrimaryNavigation app={app} active={slug}/>
|
<PrimaryNavigation app={app} active={slug}/>
|
||||||
</aside>
|
</aside>
|
||||||
<section className={`
|
<section className={`
|
||||||
max-w-61.8% p-4 pt-24 sm:pt-28 w-full
|
p-4 pt-24 sm:pt-28 w-full
|
||||||
sm:px-1 md:px-4 lg:px-8 xl:px-16 2xl:px-32
|
sm:px-1 md:px-4 lg:px-8
|
||||||
|
${workbench && collapsePrimaryNav
|
||||||
|
? ''
|
||||||
|
: 'max-w-61.8% xl:px-16 2xl:px-32'
|
||||||
|
}
|
||||||
`}>
|
`}>
|
||||||
<div className="max-w-5xl">
|
<div className={workbench ? '' : "max-w-5xl"}>
|
||||||
{title && (
|
{title && (
|
||||||
<>
|
<>
|
||||||
<Breadcrumbs app={app} slug={slug} title={title} />
|
<Breadcrumbs app={app} slug={slug} title={title} />
|
||||||
|
@ -123,8 +157,32 @@ const DefaultLayout = ({ app, title=false, children=[], search, setSearch}) => {
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
{workbench && AltMenu && (
|
||||||
|
<aside className={`
|
||||||
|
${asideClasses}
|
||||||
|
${app.primaryMenu ? '' : 'translate-x-[-100%]'} transition-transform
|
||||||
|
sm:flex-row
|
||||||
|
${workbench && collapseAltMenu
|
||||||
|
? 'sm:px-0 sm:w-16'
|
||||||
|
: 'sm:px-1 md:px-4 lg:px-8 xl:px-16 2xl:px-32 sm:w-[38.2%]'
|
||||||
|
}
|
||||||
|
`}>
|
||||||
|
<div className={`hidden sm:flex`}>
|
||||||
|
<button
|
||||||
|
className="text-secondary-focus h-full px-2 pr-4 hover:animate-pulse"
|
||||||
|
onClick={() => setCollapseAltMenu(!collapseAltMenu)}
|
||||||
|
>
|
||||||
|
{collapseAltMenu
|
||||||
|
? <><Left /><Left /><Left /></>
|
||||||
|
: <><Right /><Right /><Right /></>
|
||||||
|
}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<AltMenu />
|
||||||
|
</aside>
|
||||||
|
)}
|
||||||
</main>
|
</main>
|
||||||
{search && (
|
{!noSearch && search && (
|
||||||
<>
|
<>
|
||||||
<div className={`
|
<div className={`
|
||||||
fixed w-full max-h-screen bg-base-100 top-0 z-30 pt-0 pb-16 px-8
|
fixed w-full max-h-screen bg-base-100 top-0 z-30 pt-0 pb-16 px-8
|
||||||
|
|
|
@ -30,7 +30,8 @@ const icons = {
|
||||||
const order = obj => orderBy(obj, ['__order', '__title'], ['asc', 'asc'])
|
const order = obj => orderBy(obj, ['__order', '__title'], ['asc', 'asc'])
|
||||||
|
|
||||||
// Component for the collapse toggle
|
// Component for the collapse toggle
|
||||||
const Chevron = ({w=8, m=2}) => <svg className={`
|
// Exported for re-use
|
||||||
|
export const Chevron = ({w=8, m=2}) => <svg className={`
|
||||||
fill-current opacity-75 w-${w} h-${w} mr-${m}
|
fill-current opacity-75 w-${w} h-${w} mr-${m}
|
||||||
details-toggle hover:text-secondary sm:hover:text-secondary-focus
|
details-toggle hover:text-secondary sm:hover:text-secondary-focus
|
||||||
`}
|
`}
|
||||||
|
@ -43,7 +44,8 @@ const currentChildren = current => Object.values(order(current))
|
||||||
.filter(entry => (typeof entry === 'object'))
|
.filter(entry => (typeof entry === 'object'))
|
||||||
|
|
||||||
// Shared classes for links
|
// Shared classes for links
|
||||||
const linkClasses = `text-lg lg:text-xl
|
// Exported for re-use
|
||||||
|
export const linkClasses = `text-lg lg:text-xl
|
||||||
py-1 hover:cursor-pointer
|
py-1 hover:cursor-pointer
|
||||||
text-base-content sm:text-neutral-content
|
text-base-content sm:text-neutral-content
|
||||||
hover:text-secondary
|
hover:text-secondary
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import themes from 'shared/themes/index.js'
|
import themes from 'shared/themes/index.js'
|
||||||
import ThemeIcon from 'shared/components/icons/theme.js'
|
import ThemeIcon from 'shared/components/icons/theme.js'
|
||||||
|
|
||||||
const ThemePicker = ({ app, className='' }) => {
|
const ThemePicker = ({ app, className }) => {
|
||||||
return (
|
return (
|
||||||
<div className={`dropdown ${className}`}>
|
<div className={`dropdown ${className}`}>
|
||||||
<div tabIndex="0" className={`
|
<div tabIndex="0" className={`
|
||||||
|
@ -10,15 +10,25 @@ const ThemePicker = ({ app, className='' }) => {
|
||||||
hover:bg-neutral hover:border-neutral-content
|
hover:bg-neutral hover:border-neutral-content
|
||||||
`}>
|
`}>
|
||||||
<ThemeIcon />
|
<ThemeIcon />
|
||||||
<span>{app.theme}</span>
|
<span>{app.i18n
|
||||||
<span>Theme</span>
|
? app.t(`${app.theme}Theme`)
|
||||||
|
: `${app.theme} Theme`
|
||||||
|
}</span>
|
||||||
</div>
|
</div>
|
||||||
<ul tabIndex="0" className="p-2 shadow menu dropdown-content bg-base-100 rounded-box w-52">
|
<ul
|
||||||
|
tabIndex="0"
|
||||||
|
className="p-2 shadow menu dropdown-content bg-base-100 rounded-box w-52"
|
||||||
|
>
|
||||||
{Object.keys(themes).map(theme => (
|
{Object.keys(themes).map(theme => (
|
||||||
<li key={theme}>
|
<li key={theme}>
|
||||||
<button onClick={() => app.setTheme(theme)} className="btn btn-ghost text-base-content hover:bg-base-200">
|
<button
|
||||||
{theme}
|
onClick={() => app.setTheme(theme)}
|
||||||
<span> Theme</span>
|
className="btn btn-ghost text-base-content hover:bg-base-200"
|
||||||
|
>
|
||||||
|
{app.i18n
|
||||||
|
? app.t(`${app.theme}Theme`)
|
||||||
|
: `${app.theme} Theme`
|
||||||
|
}
|
||||||
</button>
|
</button>
|
||||||
</li>
|
</li>
|
||||||
))}
|
))}
|
||||||
|
|
192
packages/freesewing.shared/components/workbench/index.js
Normal file
192
packages/freesewing.shared/components/workbench/index.js
Normal file
|
@ -0,0 +1,192 @@
|
||||||
|
import Link from 'next/link'
|
||||||
|
import orderBy from 'lodash.orderby'
|
||||||
|
import ThemePicker from 'shared/components/theme-picker.js'
|
||||||
|
import RssIcon from 'shared/components/icons/rss.js'
|
||||||
|
import TutorialIcon from 'shared/components/icons/tutorial.js'
|
||||||
|
import GuideIcon from 'shared/components/icons/guide.js'
|
||||||
|
import HelpIcon from 'shared/components/icons/help.js'
|
||||||
|
import DocsIcon from 'shared/components/icons/docs.js'
|
||||||
|
|
||||||
|
// Don't show children for blog and showcase posts
|
||||||
|
const keepClosed = ['blog', 'showcase', ]
|
||||||
|
|
||||||
|
// TODO: For now we force tailwind to pickup these styles
|
||||||
|
// At some point this should 'just work' though, but let's not worry about it now
|
||||||
|
const force = [
|
||||||
|
<p className="w-6 mr-2"/>,
|
||||||
|
<p className="w-8 mr-3"/>
|
||||||
|
]
|
||||||
|
|
||||||
|
// List of icons matched to top-level slug
|
||||||
|
const icons = {
|
||||||
|
blog: <RssIcon />,
|
||||||
|
tutorials: <TutorialIcon />,
|
||||||
|
guides: <GuideIcon />,
|
||||||
|
howtos: <HelpIcon />,
|
||||||
|
reference: <DocsIcon />
|
||||||
|
}
|
||||||
|
|
||||||
|
/* helper method to order nav entries */
|
||||||
|
const order = obj => orderBy(obj, ['__order', '__title'], ['asc', 'asc'])
|
||||||
|
|
||||||
|
// Component for the collapse toggle
|
||||||
|
const Chevron = ({w=8, m=2}) => <svg className={`
|
||||||
|
fill-current opacity-75 w-${w} h-${w} mr-${m}
|
||||||
|
details-toggle hover:text-secondary sm:hover:text-secondary-focus
|
||||||
|
`}
|
||||||
|
xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20">
|
||||||
|
<path d="M12.95 10.707l.707-.707L8 4.343 6.586 5.757 10.828 10l-4.242 4.243L8 15.657l4.95-4.95z"/>
|
||||||
|
</svg>
|
||||||
|
|
||||||
|
// Helper method to filter out the real children
|
||||||
|
const currentChildren = current => Object.values(order(current))
|
||||||
|
.filter(entry => (typeof entry === 'object'))
|
||||||
|
|
||||||
|
// Shared classes for links
|
||||||
|
const linkClasses = `text-lg lg:text-xl
|
||||||
|
py-1 hover:cursor-pointer
|
||||||
|
text-base-content sm:text-neutral-content
|
||||||
|
hover:text-secondary
|
||||||
|
sm:hover:text-secondary-focus
|
||||||
|
`
|
||||||
|
|
||||||
|
// Figure out whether a page is on the path to the active page
|
||||||
|
const isActive = (slug, active) => {
|
||||||
|
if (slug === active) return true
|
||||||
|
let result = true
|
||||||
|
const slugParts = slug.split('/')
|
||||||
|
const activeParts = active.split('/')
|
||||||
|
for (const i in slugParts) {
|
||||||
|
if (slugParts[i] !== activeParts[i]) result = false
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// Component that renders a sublevel of navigation
|
||||||
|
const SubLevel = ({ nodes={}, active }) => (
|
||||||
|
<ul className="pl-5 list-inside">
|
||||||
|
{currentChildren(nodes).map(child => (Object.keys(child).length > 4)
|
||||||
|
? (
|
||||||
|
<li key={child.__slug} className="flex flex-row">
|
||||||
|
<details className="grow" open={isActive(child.__slug, active)}>
|
||||||
|
<summary className={`
|
||||||
|
flex flex-row
|
||||||
|
px-2
|
||||||
|
text-base-content
|
||||||
|
sm:text-neutral-content
|
||||||
|
hover:cursor-row-resize
|
||||||
|
items-center
|
||||||
|
`}>
|
||||||
|
<Link href={`/${child.__slug}`}>
|
||||||
|
<a title={child.__title} className={`
|
||||||
|
grow pl-2 border-l-2
|
||||||
|
${linkClasses}
|
||||||
|
hover:border-secondary
|
||||||
|
sm:hover:border-secondary-focus
|
||||||
|
${child.__slug === active
|
||||||
|
? 'text-secondary border-secondary sm:text-secondary-focus sm:border-secondary-focus'
|
||||||
|
: 'text-base-content sm:text-neutral-content'
|
||||||
|
}
|
||||||
|
`}>
|
||||||
|
<span className={`
|
||||||
|
text-3xl mr-2 inline-block p-0 leading-3
|
||||||
|
${child.__slug === active
|
||||||
|
? 'text-secondary sm:text-secondary-focus translate-y-1'
|
||||||
|
: 'translate-y-3'
|
||||||
|
}
|
||||||
|
`}>
|
||||||
|
{child.__slug === active ? <>•</> : <>°</>}
|
||||||
|
</span>
|
||||||
|
<span className={child.__slug === active ? 'font-bold' : ''}>
|
||||||
|
{ child.__linktitle || child.__title }
|
||||||
|
</span>
|
||||||
|
</a>
|
||||||
|
</Link>
|
||||||
|
<Chevron w={6} m={3}/>
|
||||||
|
</summary>
|
||||||
|
<SubLevel nodes={child} active={active} />
|
||||||
|
</details>
|
||||||
|
</li>
|
||||||
|
) : (
|
||||||
|
<li className='pl-2 flex flex-row items-center' key={child.__slug}>
|
||||||
|
<Link href={`/${child.__slug}`} title={child.__title}>
|
||||||
|
<a className={`
|
||||||
|
pl-2 border-l-2
|
||||||
|
grow
|
||||||
|
${linkClasses}
|
||||||
|
hover:border-secondary
|
||||||
|
sm:hover:border-secondary-focus
|
||||||
|
${child.__slug === active
|
||||||
|
? 'text-secondary border-secondary sm:text-secondary-focus sm:border-secondary-focus'
|
||||||
|
: 'text-base-content sm:text-neutral-content'
|
||||||
|
}`}>
|
||||||
|
<span className={`
|
||||||
|
text-3xl mr-2 inline-block p-0 leading-3
|
||||||
|
${child.__slug === active
|
||||||
|
? 'text-secondary sm:text-secondary-focus translate-y-1'
|
||||||
|
: 'translate-y-3'
|
||||||
|
}
|
||||||
|
`}>
|
||||||
|
{child.__slug === active ? <>•</> : <>°</>}
|
||||||
|
</span>
|
||||||
|
<span className={child.__slug === active ? 'font-bold' : ''}>
|
||||||
|
{child.__linktitle}
|
||||||
|
</span>
|
||||||
|
</a>
|
||||||
|
</Link>
|
||||||
|
</li>
|
||||||
|
)
|
||||||
|
|
||||||
|
)}
|
||||||
|
</ul>
|
||||||
|
)
|
||||||
|
|
||||||
|
// Component that renders a toplevel of navigation
|
||||||
|
const TopLevel = ({ icon, title, nav, current, slug, hasChildren=false, active }) => (
|
||||||
|
<details className='py-1' open={((keepClosed.indexOf(current.__slug) === -1) ? 1 : 0)}>
|
||||||
|
<summary className={`
|
||||||
|
flex flex-row uppercase gap-4 font-bold text-lg
|
||||||
|
hover:cursor-row-resize
|
||||||
|
p-2
|
||||||
|
text-base-content
|
||||||
|
sm:text-neutral-content
|
||||||
|
items-center
|
||||||
|
`}>
|
||||||
|
<span className="text-secondary-focus">{icon}</span>
|
||||||
|
<Link href={`/${slug}`}>
|
||||||
|
<a className={`grow ${linkClasses} ${slug === active ? 'text-secondary sm:text-secondary-focus' : ''}`}>
|
||||||
|
{title}
|
||||||
|
</a>
|
||||||
|
</Link>
|
||||||
|
{hasChildren && <Chevron />}
|
||||||
|
</summary>
|
||||||
|
{hasChildren && <SubLevel nodes={current} active={active} />}
|
||||||
|
</details>
|
||||||
|
)
|
||||||
|
|
||||||
|
const Navigation = ({ app, active }) => {
|
||||||
|
if (!app.navigation) return null
|
||||||
|
const output = []
|
||||||
|
for (const page of order(app.navigation)) output.push(<TopLevel
|
||||||
|
key={page.__slug}
|
||||||
|
icon={icons[page.__slug] || <span className="text-3xl mr-2 translate-y-3 inline-block p-0 leading-3">°</span>}
|
||||||
|
title={page.__title}
|
||||||
|
slug={page.__slug}
|
||||||
|
hasChildren={keepClosed.indexOf(page.__slug) === -1}
|
||||||
|
nav={app.navigation}
|
||||||
|
current={order(app.navigation[page.__slug])}
|
||||||
|
active={active}
|
||||||
|
/>)
|
||||||
|
|
||||||
|
return <div className='pb-20'>{output}</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
const PrimaryMenu = ({ app, active }) => (
|
||||||
|
<nav className="sm:max-w-lg grow mb-12">
|
||||||
|
<ThemePicker app={app} className="w-full sm:hidden"/>
|
||||||
|
<Navigation app={app} active={active} />
|
||||||
|
</nav>
|
||||||
|
)
|
||||||
|
|
||||||
|
export default PrimaryMenu
|
|
@ -0,0 +1,88 @@
|
||||||
|
import React, { useState } from 'react'
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This is a single input for a measurements
|
||||||
|
* Note that it keeps local state with whatever the user types
|
||||||
|
* but will only trigger a gist update if the input is valid.
|
||||||
|
*
|
||||||
|
* m holds the measurement name. It's just so long to type
|
||||||
|
* measurement and I always have some typo in it because dyslexia.
|
||||||
|
*/
|
||||||
|
const MeasurementInput = ({ m, gist, app, updateMeasurements }) => {
|
||||||
|
const prefix = (app.site === 'org') ? '' : 'https://freesewing.org'
|
||||||
|
const title = app.t(`measurements.${m}`)
|
||||||
|
console.log('render', m)
|
||||||
|
const isValid = input => {
|
||||||
|
if (input === null || input === '') return null
|
||||||
|
return !isNaN(input)
|
||||||
|
}
|
||||||
|
|
||||||
|
const update = evt => {
|
||||||
|
setVal(evt.target.value)
|
||||||
|
const ok = isValid(evt.target.value)
|
||||||
|
console.log({ok})
|
||||||
|
if (ok) {
|
||||||
|
setValid(true)
|
||||||
|
updateMeasurements(evt.target.value, m)
|
||||||
|
} else setValid(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
const [val, setVal] = useState(gist?.measurements?.[m] || null)
|
||||||
|
const [valid, setValid] = useState(typeof gist?.measurements?.[m] === 'undefined'
|
||||||
|
? null :
|
||||||
|
isValid(gist.measurements[m])
|
||||||
|
)
|
||||||
|
|
||||||
|
if (!m) return null
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="form-control mb-2" key={`wrap-${m}`}>
|
||||||
|
<label className="label">
|
||||||
|
<span className="label-text font-bold text-xl">{title}</span>
|
||||||
|
<a
|
||||||
|
href={`${prefix}/docs/measurements/${m.toLowerCase()}`}
|
||||||
|
className="label-text-alt text-secondary hover:text-secondary-focus hover:underline"
|
||||||
|
title={`${app.t('docs')}: ${app.t(`measurements.${m}`)}`}
|
||||||
|
tabIndex="-1"
|
||||||
|
>
|
||||||
|
{app.t('docs')}
|
||||||
|
</a>
|
||||||
|
</label>
|
||||||
|
<label className="input-group input-group-lg">
|
||||||
|
<input
|
||||||
|
key={`input-${m}`}
|
||||||
|
type="text"
|
||||||
|
placeholder={title}
|
||||||
|
className={`
|
||||||
|
input input-lg input-bordered grow text-base-content
|
||||||
|
${valid === false && 'input-error'}
|
||||||
|
${valid === true && 'input-success'}
|
||||||
|
`}
|
||||||
|
value={val}
|
||||||
|
onChange={update}
|
||||||
|
/>
|
||||||
|
<span className={`
|
||||||
|
${valid === false && 'bg-error text-neutral-content'}
|
||||||
|
${valid === true && 'bg-success text-neutral-content'}
|
||||||
|
${valid === null && 'bg-base-200 text-base-content'}
|
||||||
|
`}>
|
||||||
|
cm
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
<label className="label">
|
||||||
|
<span className="label-text-alt">
|
||||||
|
{valid === null
|
||||||
|
? ''
|
||||||
|
: valid
|
||||||
|
? 'Looks good'
|
||||||
|
: 'Invalid'
|
||||||
|
}
|
||||||
|
{val}
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default MeasurementInput
|
||||||
|
|
|
@ -0,0 +1,48 @@
|
||||||
|
import MeasurementInput from './input-measurement.js'
|
||||||
|
|
||||||
|
const WorkbenchMeasurements = ({ app, pattern, gist, updateGist }) => {
|
||||||
|
|
||||||
|
// Method to handle measurement updates
|
||||||
|
const updateMeasurements = (value, m=false) => {
|
||||||
|
if (m === false) {
|
||||||
|
// Set all measurements
|
||||||
|
} else {
|
||||||
|
// Set one measurement
|
||||||
|
const newValues = {...gist.measurements}
|
||||||
|
newValues[m] = value.trim()
|
||||||
|
updateGist('measurements', newValues)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Save us some typing
|
||||||
|
const inputProps = { app, updateMeasurements, gist }
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="m-auto max-w-prose">
|
||||||
|
<h1>
|
||||||
|
<span className='capitalize mr-4 opacity-70'>
|
||||||
|
{pattern.config.name}:
|
||||||
|
</span>
|
||||||
|
{app.t('measurements')}
|
||||||
|
</h1>
|
||||||
|
{pattern.config.measurements && (
|
||||||
|
<>
|
||||||
|
<h2>{app.t('requiredMeasurements')}</h2>
|
||||||
|
{pattern.config.measurements.map(m => (
|
||||||
|
<MeasurementInput key={m} m={m} {...inputProps} />
|
||||||
|
))}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{pattern.config.optionalMeasurements && (
|
||||||
|
<>
|
||||||
|
<h2>{app.t('optionalMeasurements')}</h2>
|
||||||
|
{pattern.config.optionalMeasurements.map(m => (
|
||||||
|
<MeasurementInput key={m} m={m} {...inputProps} />
|
||||||
|
))}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default WorkbenchMeasurements
|
||||||
|
|
53
packages/freesewing.shared/components/workbench/menu.js
Normal file
53
packages/freesewing.shared/components/workbench/menu.js
Normal file
|
@ -0,0 +1,53 @@
|
||||||
|
import { useState } from 'react'
|
||||||
|
import Link from 'next/link'
|
||||||
|
import orderBy from 'lodash.orderby'
|
||||||
|
import OptionsIcon from 'shared/components/icons/options.js'
|
||||||
|
import SettingsIcon from 'shared/components/icons/settings.js'
|
||||||
|
import { linkClasses, Chevron } from 'shared/components/navigation/primary.js'
|
||||||
|
|
||||||
|
const structure = (pattern, app) => ({
|
||||||
|
modes: [
|
||||||
|
{ title: `Draft ${pattern.config.name}`, action: '' },
|
||||||
|
],
|
||||||
|
options: [
|
||||||
|
{ title: `Draft ${pattern.config.name}`, action: '' },
|
||||||
|
],
|
||||||
|
settings: [
|
||||||
|
{ title: `Draft ${pattern.config.name}`, action: '' },
|
||||||
|
],
|
||||||
|
})
|
||||||
|
|
||||||
|
const TopLevel = ({ icon, title }) => (
|
||||||
|
<details className='py-1'>
|
||||||
|
<summary className={`
|
||||||
|
flex flex-row uppercase gap-4 font-bold text-lg
|
||||||
|
hover:cursor-row-resize
|
||||||
|
p-2
|
||||||
|
text-base-content
|
||||||
|
sm:text-neutral-content
|
||||||
|
items-center
|
||||||
|
`}>
|
||||||
|
<span className="text-secondary-focus mr-4">{icon}</span>
|
||||||
|
<span className={`grow ${linkClasses}`}>
|
||||||
|
{title}
|
||||||
|
</span>
|
||||||
|
<Chevron />
|
||||||
|
</summary>
|
||||||
|
fixme
|
||||||
|
</details>
|
||||||
|
)
|
||||||
|
|
||||||
|
const Menu = ({ app, pattern }) => ([
|
||||||
|
<TopLevel key='a' title='Toggles' icon={<OptionsIcon />} pattern={pattern} />,
|
||||||
|
<TopLevel key='b' title='Modes' icon={<OptionsIcon />} pattern={pattern} />,
|
||||||
|
<TopLevel key='c' title='Design Options' icon={<OptionsIcon />} pattern={pattern} />,
|
||||||
|
<TopLevel key='d' title='Pattern Settings' icon={<SettingsIcon />} pattern={pattern} />,
|
||||||
|
])
|
||||||
|
|
||||||
|
const WorkbenchMenu = ({ app, pattern }) => (
|
||||||
|
<nav className="smmax-w-96 grow mb-12">
|
||||||
|
<Menu app={app} pattern={pattern} />
|
||||||
|
</nav>
|
||||||
|
)
|
||||||
|
|
||||||
|
export default WorkbenchMenu
|
|
@ -1,4 +1,4 @@
|
||||||
import React, { useState } from 'react'
|
import React, { useState, useEffect } from 'react'
|
||||||
import { useSwipeable } from 'react-swipeable'
|
import { useSwipeable } from 'react-swipeable'
|
||||||
import { useRouter } from 'next/router'
|
import { useRouter } from 'next/router'
|
||||||
import { useHotkeys } from 'react-hotkeys-hook'
|
import { useHotkeys } from 'react-hotkeys-hook'
|
||||||
|
@ -6,7 +6,7 @@ import { useHotkeys } from 'react-hotkeys-hook'
|
||||||
import Layout from 'shared/components/layouts/default'
|
import Layout from 'shared/components/layouts/default'
|
||||||
|
|
||||||
/* This component should wrap all page content */
|
/* This component should wrap all page content */
|
||||||
const AppWrapper= props => {
|
const PageWrapper= props => {
|
||||||
|
|
||||||
const swipeHandlers = useSwipeable({
|
const swipeHandlers = useSwipeable({
|
||||||
onSwipedLeft: evt => (props.app.primaryMenu) ? props.app.setPrimaryMenu(false) : null,
|
onSwipedLeft: evt => (props.app.primaryMenu) ? props.app.setPrimaryMenu(false) : null,
|
||||||
|
@ -15,7 +15,9 @@ const AppWrapper= props => {
|
||||||
})
|
})
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
props.app.setSlug(router.asPath.slice(1))
|
const slug = router.asPath.slice(1)
|
||||||
|
|
||||||
|
useEffect(() => props.app.setSlug(slug), [slug])
|
||||||
|
|
||||||
// Trigger search with Ctrl+k
|
// Trigger search with Ctrl+k
|
||||||
useHotkeys('ctrl+k', (evt) => {
|
useHotkeys('ctrl+k', (evt) => {
|
||||||
|
@ -29,6 +31,9 @@ const AppWrapper= props => {
|
||||||
app: props.app,
|
app: props.app,
|
||||||
title: props.title,
|
title: props.title,
|
||||||
search, setSearch, toggleSearch: () => setSearch(!search),
|
search, setSearch, toggleSearch: () => setSearch(!search),
|
||||||
|
noSearch: props.noSearch,
|
||||||
|
workbench: props.workbench,
|
||||||
|
AltMenu: props.AltMenu || null
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -45,5 +50,5 @@ const AppWrapper= props => {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default AppWrapper
|
export default PageWrapper
|
||||||
|
|
||||||
|
|
85
packages/freesewing.shared/components/wrappers/workbench.js
Normal file
85
packages/freesewing.shared/components/wrappers/workbench.js
Normal file
|
@ -0,0 +1,85 @@
|
||||||
|
import { useState, useEffect } from 'react'
|
||||||
|
import Layout from 'shared/components/layouts/default'
|
||||||
|
import Menu from 'shared/components/workbench/menu.js'
|
||||||
|
import Measurements, { Input } from 'shared/components/workbench/measurements.js'
|
||||||
|
import set from 'lodash.set'
|
||||||
|
|
||||||
|
// Generates a default pattern gist to start from
|
||||||
|
const defaultGist = (pattern, language='en') => ({
|
||||||
|
design: pattern.config.name,
|
||||||
|
version: pattern.config.version,
|
||||||
|
settings: {
|
||||||
|
sa: 0,
|
||||||
|
complete: true,
|
||||||
|
paperless: false,
|
||||||
|
units: 'metric',
|
||||||
|
locale: language,
|
||||||
|
margin: 2,
|
||||||
|
debug: true,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const hasRequiredMeasurements = (pattern, gist) => {
|
||||||
|
for (const m of pattern.config.measurements) {
|
||||||
|
console.log(m)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This component wraps the workbench and is in charge of
|
||||||
|
* keeping the mode & gist state, which will trickly down
|
||||||
|
* to all workbench subcomponents
|
||||||
|
*
|
||||||
|
* mode: What to display (draft, sample, measurements, ...)
|
||||||
|
* gist: The runtime pattern configuration
|
||||||
|
*/
|
||||||
|
const WorkbenchWrapper = ({ app, pattern }) => {
|
||||||
|
|
||||||
|
// State for display mode and gist
|
||||||
|
const [mode, setMode] = useState(null)
|
||||||
|
const [gist, setGist] = useState(defaultGist(pattern, app.language))
|
||||||
|
const [fuck, setFuck] = useState('')
|
||||||
|
|
||||||
|
// If we don't have the requiremed measurements,
|
||||||
|
// force mode to measurements
|
||||||
|
useEffect(() => {
|
||||||
|
if (
|
||||||
|
mode !== 'measurements'
|
||||||
|
&& !hasRequiredMeasurements(pattern, gist)
|
||||||
|
) setMode('measurements')
|
||||||
|
})
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Update gist method. See lodash.set
|
||||||
|
*/
|
||||||
|
const updateGist = (path, content) => {
|
||||||
|
const newGist = {...gist}
|
||||||
|
set(newGist, path, content)
|
||||||
|
setGist(newGist)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Required props for layout
|
||||||
|
const layoutProps = {
|
||||||
|
app: app,
|
||||||
|
noSearch: true,
|
||||||
|
workbench: true,
|
||||||
|
AltMenu: Menu
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Layout {...layoutProps}>
|
||||||
|
{mode === 'measurements' && (
|
||||||
|
<Measurements
|
||||||
|
app={app}
|
||||||
|
pattern={pattern}
|
||||||
|
gist={gist}
|
||||||
|
updateGist={updateGist}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<pre>{JSON.stringify(gist, null, 2)}</pre>
|
||||||
|
</Layout>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default WorkbenchWrapper
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue