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'
|
||||
florence: 'A FreeSewing pattern for a face mask'
|
||||
freesewing.dev: 'FreeSewing website with documentation for contributors & developers'
|
||||
freesewing.lab: 'FreeSewing website to test various patterns'
|
||||
freesewing.org: 'FreeSewing website'
|
||||
freesewing.shared: 'Shared code and React components for different websites'
|
||||
gatsby-remark-jargon: 'A gatsby-transformer-remark sub-plugin for jargon terms'
|
||||
|
|
|
@ -60,8 +60,23 @@ const Header = ({ app, setSearch }) => {
|
|||
`}
|
||||
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></>
|
||||
? (
|
||||
<>
|
||||
<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="flex flex-row gap-2 sm:hidden">
|
||||
|
@ -100,7 +115,10 @@ const Header = ({ app, setSearch }) => {
|
|||
</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>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -6,6 +6,8 @@ import useLocalStorage from 'shared/hooks/useLocalStorage.js'
|
|||
import prebuildNavigation from 'site/prebuild/navigation.js'
|
||||
|
||||
function useApp(full = true) {
|
||||
// No translation for freesewing.dev
|
||||
const language = 'en'
|
||||
|
||||
// User color scheme preference
|
||||
const prefersDarkMode = (typeof window !== 'undefined' && typeof window.matchMedia === 'function')
|
||||
|
@ -15,7 +17,6 @@ function useApp(full = true) {
|
|||
// 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)
|
||||
|
@ -41,9 +42,9 @@ function useApp(full = true) {
|
|||
return {
|
||||
// Static vars
|
||||
site: 'dev',
|
||||
language,
|
||||
|
||||
// State
|
||||
language,
|
||||
loading,
|
||||
navigation,
|
||||
primaryMenu,
|
||||
|
@ -51,7 +52,6 @@ function useApp(full = true) {
|
|||
theme,
|
||||
|
||||
// State setters
|
||||
setLanguage,
|
||||
setLoading,
|
||||
setNavigation,
|
||||
setPrimaryMenu,
|
||||
|
@ -63,6 +63,10 @@ function useApp(full = true) {
|
|||
|
||||
// State handlers
|
||||
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 PrimaryNavigation from 'shared/components/navigation/primary'
|
||||
import get from 'lodash.get'
|
||||
import Right from 'shared/components/icons/right.js'
|
||||
import Left from 'shared/components/icons/left.js'
|
||||
// Site components
|
||||
import Header from 'site/components/header'
|
||||
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 = () => {
|
||||
app.startLoading()
|
||||
// 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('routeChangeComplete', () => app.stopLoading())
|
||||
const slug = router.asPath.slice(1)
|
||||
const [leftNav, setLeftNav] = useState(false)
|
||||
const [collapsePrimaryNav, setCollapsePrimaryNav] = useState(workbench || false)
|
||||
const [collapseAltMenu, setCollapseAltMenu] = useState(false)
|
||||
|
||||
return (
|
||||
<div className={`
|
||||
|
@ -85,35 +113,41 @@ const DefaultLayout = ({ app, title=false, children=[], search, setSearch}) => {
|
|||
<main className={`
|
||||
grow flex flex-row
|
||||
gap-2
|
||||
lg:gap-8
|
||||
xl:gap-16
|
||||
${!workbench ? 'lg:gap-8 xl:gap-16' : ''}
|
||||
`}>
|
||||
<aside className={`
|
||||
fixed top-0 right-0
|
||||
${asideClasses}
|
||||
${app.primaryMenu ? '' : 'translate-x-[-100%]'} transition-transform
|
||||
pt-28
|
||||
sm:pt-8 sm:mt-16
|
||||
pb-4 px-2
|
||||
sm:px-1 md:px-4 lg:px-8 xl:px-16 2xl:px-32
|
||||
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
|
||||
sm:flex-row-reverse
|
||||
${workbench && collapsePrimaryNav
|
||||
? '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%]'
|
||||
}
|
||||
`}>
|
||||
{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}/>
|
||||
</aside>
|
||||
<section className={`
|
||||
max-w-61.8% p-4 pt-24 sm:pt-28 w-full
|
||||
sm:px-1 md:px-4 lg:px-8 xl:px-16 2xl:px-32
|
||||
p-4 pt-24 sm:pt-28 w-full
|
||||
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 && (
|
||||
<>
|
||||
<Breadcrumbs app={app} slug={slug} title={title} />
|
||||
|
@ -123,8 +157,32 @@ const DefaultLayout = ({ app, title=false, children=[], search, setSearch}) => {
|
|||
{children}
|
||||
</div>
|
||||
</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>
|
||||
{search && (
|
||||
{!noSearch && search && (
|
||||
<>
|
||||
<div className={`
|
||||
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'])
|
||||
|
||||
// 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}
|
||||
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'))
|
||||
|
||||
// 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
|
||||
text-base-content sm:text-neutral-content
|
||||
hover:text-secondary
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import themes from 'shared/themes/index.js'
|
||||
import ThemeIcon from 'shared/components/icons/theme.js'
|
||||
|
||||
const ThemePicker = ({ app, className='' }) => {
|
||||
const ThemePicker = ({ app, className }) => {
|
||||
return (
|
||||
<div className={`dropdown ${className}`}>
|
||||
<div tabIndex="0" className={`
|
||||
|
@ -10,15 +10,25 @@ const ThemePicker = ({ app, className='' }) => {
|
|||
hover:bg-neutral hover:border-neutral-content
|
||||
`}>
|
||||
<ThemeIcon />
|
||||
<span>{app.theme}</span>
|
||||
<span>Theme</span>
|
||||
<span>{app.i18n
|
||||
? app.t(`${app.theme}Theme`)
|
||||
: `${app.theme} Theme`
|
||||
}</span>
|
||||
</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 => (
|
||||
<li key={theme}>
|
||||
<button onClick={() => app.setTheme(theme)} className="btn btn-ghost text-base-content hover:bg-base-200">
|
||||
{theme}
|
||||
<span> Theme</span>
|
||||
<button
|
||||
onClick={() => app.setTheme(theme)}
|
||||
className="btn btn-ghost text-base-content hover:bg-base-200"
|
||||
>
|
||||
{app.i18n
|
||||
? app.t(`${app.theme}Theme`)
|
||||
: `${app.theme} Theme`
|
||||
}
|
||||
</button>
|
||||
</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 { useRouter } from 'next/router'
|
||||
import { useHotkeys } from 'react-hotkeys-hook'
|
||||
|
@ -6,7 +6,7 @@ import { useHotkeys } from 'react-hotkeys-hook'
|
|||
import Layout from 'shared/components/layouts/default'
|
||||
|
||||
/* This component should wrap all page content */
|
||||
const AppWrapper= props => {
|
||||
const PageWrapper= props => {
|
||||
|
||||
const swipeHandlers = useSwipeable({
|
||||
onSwipedLeft: evt => (props.app.primaryMenu) ? props.app.setPrimaryMenu(false) : null,
|
||||
|
@ -15,7 +15,9 @@ const AppWrapper= props => {
|
|||
})
|
||||
|
||||
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
|
||||
useHotkeys('ctrl+k', (evt) => {
|
||||
|
@ -29,6 +31,9 @@ const AppWrapper= props => {
|
|||
app: props.app,
|
||||
title: props.title,
|
||||
search, setSearch, toggleSearch: () => setSearch(!search),
|
||||
noSearch: props.noSearch,
|
||||
workbench: props.workbench,
|
||||
AltMenu: props.AltMenu || null
|
||||
}
|
||||
|
||||
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