1
0
Fork 0

wip: Started working on new development environment

This commit is contained in:
Joost De Cock 2022-01-22 17:55:03 +01:00
parent ace86eaf85
commit 54aefa8437
45 changed files with 1722 additions and 43 deletions

View file

@ -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'

View file

@ -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>
)
}

View file

@ -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,
}
}

View file

@ -0,0 +1,3 @@
{
"extends": "next/core-web-vitals"
}

View 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

View 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&apos;s where our community hangs out
so you&apos;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

View 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

View 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

View 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

View file

@ -0,0 +1 @@
export default () => null

View 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

View 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

View file

@ -0,0 +1,3 @@
import configBuilder from '../freesewing.shared/config/next.mjs'
export default configBuilder('lab')

View 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"
}
}

View 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

View 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

View file

@ -0,0 +1,5 @@
import 'shared/styles/globals.css'
const FreeSewingLab = ({ Component, pageProps }) => <Component {...pageProps} />
export default FreeSewingLab

View file

@ -0,0 +1,5 @@
import Template from 'site/page-templates/pattern-list.js'
const Page = props => <Template sections={['accessories']} />
export default Page

View file

@ -0,0 +1,5 @@
import Template from 'site/page-templates/pattern-list.js'
const Page = props => <Template sections={['blocks']} />
export default Page

View 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} />

View file

@ -0,0 +1,5 @@
import Template from 'site/page-templates/pattern-list.js'
const Page = props => <Template sections={['garments']} />
export default Page

View 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

View file

@ -0,0 +1,3 @@
import Page from 'site/page-templates/pattern-list.js'
export default Page

View file

@ -0,0 +1,5 @@
import Template from 'site/page-templates/pattern-list.js'
const Page = props => <Template sections={['utilities']} />
export default Page

View 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

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 7.7 KiB

View 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

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 9.2 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 313 KiB

View 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()

View 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

View 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

View 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

View 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

View file

@ -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

View file

@ -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

View file

@ -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>&nbsp;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>
))}

View 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 ? <>&bull;</> : <>&deg;</>}
</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 ? <>&bull;</> : <>&deg;</>}
</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">&deg;</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

View file

@ -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

View file

@ -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

View 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

View file

@ -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

View 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