1
0
Fork 0

Merge branch 'develop' into i18n3

This commit is contained in:
joostdecock 2023-06-26 18:50:52 +02:00
commit fe61e85fa9
101 changed files with 3291 additions and 2445 deletions

View file

@ -27,6 +27,7 @@ module.exports = {
extends: 'eslint:recommended',
env: {
es2021: true,
node: true,
},
// Required when using experimental EcmaScript features
parser: '@babel/eslint-parser',

View file

@ -62,7 +62,7 @@ core:
'lodash.unset': &_unset '4.5.2'
'lodash.clonedeep': '^4.5.0'
dev:
'eslint': &eslint '8.42.0'
'eslint': &eslint '8.43.0'
'nyc': '15.1.0'
'mocha': *mocha
'chai': *chai
@ -192,12 +192,12 @@ yuri:
backend:
_:
'@aws-sdk/client-sesv2': '3.352.0'
'@prisma/client': &prisma '4.15.0'
'@aws-sdk/client-sesv2': '3.354.0'
'@prisma/client': &prisma '4.16.1'
'bcryptjs': '2.4.3'
'cors': '2.8.5'
'crypto': '1.0.1'
'dotenv': '16.1.4'
'dotenv': '16.3.1'
'express': '4.18.2'
'js-yaml': *jsyaml
'lodash.get': *_get
@ -208,12 +208,12 @@ backend:
'passport-jwt': '4.0.1'
'pino': '8.14.1'
'qrcode': '1.5.3'
'swagger-ui-dist': '4.19.0'
'swagger-ui-dist': '5.1.0'
'swagger-ui-express': '4.6.3'
dev:
'chai': *chai
'chai-http': '4.4.0'
'esbuild': '0.18.2'
'esbuild': '0.18.8'
'mocha': *mocha
'mocha-steps': '1.3.0'
'nodemon': '2.0.22'
@ -225,10 +225,10 @@ dev:
'@mdx-js/mdx': *mdx
'@mdx-js/react': *mdx
'@mdx-js/runtime': &mdxRuntime '2.0.0-next.9'
'@next/bundle-analyzer': &next '13.4.6'
'@next/bundle-analyzer': &next '13.4.7'
'@tailwindcss/typography': &tailwindTypography '0.5.9'
'algoliasearch': '4.17.2'
'daisyui': &daisyui '3.1.0'
'algoliasearch': '4.18.0'
'daisyui': &daisyui '3.1.1'
'lodash.get': *_get
'lodash.orderby': &_orderby '4.6.0'
'lodash.set': *_set
@ -238,7 +238,7 @@ dev:
'react-dom': *react
'react-hotkeys-hook': &reactHotkeysHook '4.4.0'
'react-instantsearch-dom': &reactInstantsearchDom '6.40.0'
'react-instantsearch-hooks-web': '6.44.2'
'react-instantsearch-hooks-web': '6.45.0'
'react-markdown': &reactMarkdown '8.0.7'
'react-swipeable': &reactSwipeable '7.0.1'
'react-timeago': &reactTimeago '7.1.0'
@ -276,7 +276,7 @@ lab:
'@mdx-js/react': *mdx
'@mdx-js/runtime': *mdxRuntime
'@tailwindcss/typography': *tailwindTypography
'algoliasearch': &algoliasearch '4.17.2'
'algoliasearch': &algoliasearch '4.18.0'
'd3-dispatch': '3.0.1'
'd3-drag': '3.0.0'
'd3-selection': '3.0.0'
@ -286,7 +286,7 @@ lab:
'lodash.orderby': *_orderby
'lodash.set': *_set
'next': *next
'next-i18next': &nextI18next '13.3.0'
'next-i18next': &nextI18next '14.0.0'
'react': *react
'react-copy-to-clipboard': *reactCopyToClipboard
'react-hotkeys-hook': *reactHotkeysHook
@ -314,15 +314,20 @@ org:
'@mdx-js/mdx': *mdx
'@mdx-js/react': *mdx
'@mdx-js/runtime': *mdxRuntime
'@portabletext/react': '^1.0.6'
'@sanity/client': '^6.1.2'
'@tailwindcss/typography': *tailwindTypography
'algoliasearch': *algoliasearch
'react-copy-to-clipboard': 5.1.0
'daisyui': *daisyui
'jotai': &jotai '2.1.1'
'jotai-location': &jotai-location '0.5.1'
'lodash.get': *_get
'lodash.orderby': *_orderby
'lodash.set': *_set
'luxon': '3.3.0'
'next': *next
'next-sanity': '^4.3.3'
'react-dropzone': '14.2.3'
'react-hotkeys-hook': *reactHotkeysHook
'react-instantsearch-dom': *reactInstantsearchDom
@ -344,8 +349,8 @@ org:
sanity:
_:
'@sanity/vision': &sanity '3.12.0'
'easymde': '2.16.0'
'@sanity/vision': &sanity '3.12.2'
'easymde': '2.18.0'
'react': *react
'react-dom': *react
'react-is': *react
@ -357,12 +362,12 @@ sanity:
'eslint': *eslint
'prettier': '2.8.8'
'typescript': '5.1.3'
'@sanity/cli': '3.12.1'
'@sanity/cli': '3.12.2'
shared:
_:
'@headlessui/react': *headlessUiReact
'@next/mdx': '13.4.6'
'@next/mdx': '13.4.7'
'@resvg/resvg-js': '2.4.1'
'@tailwindcss/typography': *tailwindTypography
'Buffer': '0.0.0'
@ -375,8 +380,8 @@ shared:
'front-matter': '4.0.2'
'highlight.js': '11.8.0'
'github-slugger': '2.0.0'
'jotai': '2.1.1'
'jotai-location': '0.5.1'
'jotai': *jotai
'jotai-location': *jotai-location
'lodash.clonedeep': '4.5.0'
'lodash.orderby': *_orderby
'lodash.unset': *_unset
@ -398,8 +403,8 @@ shared:
'remark-smartypants': '2.0.0'
'sharp': '0.32.1'
'svg-to-pdfkit': 'https://github.com/eriese/SVG-to-PDFKit'
'tlds': '1.239.0'
'to-vfile': '7.2.4'
'tlds': '1.240.0'
'to-vfile': '8.0.0'
'unist-util-visit': *unist-util-visit
'use-persisted-state': *use-persisted-state
'web-worker': '1.2.0'

View file

@ -5,6 +5,7 @@ import { dimensions } from './shared.mjs'
export const front = {
from: base,
name: 'aaron.front',
measurements: ['hips'],
options: {
brianFitCollar: false,
brianFitSleeve: false,

View file

@ -8,6 +8,7 @@ export const cup = {
inherited: true,
},
after: neckTie,
measurements: ['bustPointToUnderbust'],
options: {
topDepth: { pct: 54, min: 50, max: 80, menu: 'fit' },
bottomCupDepth: { pct: 8, min: 0, max: 20, menu: 'fit' },

View file

@ -483,7 +483,7 @@ export const front = {
name: 'carlton.front',
from: bentFront,
hide: hidePresets.HIDE_TREE,
measurements: ['waist', 'waistToFloor', 'waistToSeat'],
measurements: ['waist', 'waistToFloor', 'waistToSeat', 'seat'],
options: {
chestEase: { pct: 10, min: 5, max: 20, menu: 'fit' },
buttonSpacingHorizontal: { pct: 43.5, min: 15, max: 60, menu: 'style' },

View file

@ -415,7 +415,7 @@ function simoneFbaFront({
export const fbaFront = {
name: 'simone.fbaFront',
from: front,
measurements: ['highBust'],
measurements: ['highBust', 'bustSpan', 'hpsToBust'],
hide: {
self: true,
from: true,

View file

@ -0,0 +1,38 @@
---
title: Snippet.rotate()
---
The `Snippet.rotate()` method allows you to scale a snippet. Under the hood, it
sets the `data-rotate` property.
## Signature
```js
Snippet snippet.rotate(rotation, overwrite=true)
```
<Tip compact>This method is chainable as it returns the `Snippet` object</Tip>
## Example
<Example caption="An example of the Snippet.rotate() method">
```js
({ Point, Path, paths, Snippet, snippets, part }) => {
for (const i of [0,1,2,3,4,5,6]) {
snippets[`demo${i}`] = new Snippet(
"logo",
new Point(60*i, 0)
).rotate(60 * i)
}
// Prevent clipping
paths.diag = new Path()
.move(new Point(-30,-50))
.move(new Point(400,50))
return part
}
```
</Example>

View file

@ -0,0 +1,38 @@
---
title: Snippet.scale()
---
The `Snippet.scale()` method allows you to scale a snippet. Under the hood, it
sets the `data-scale` property.
## Signature
```js
Snippet snippet.scale(scale, overwrite=true)
```
<Tip compact>This method is chainable as it returns the `Snippet` object</Tip>
## Example
<Example caption="An example of the Snippet.clone() method">
```js
({ Point, Path, paths, Snippet, snippets, part }) => {
for (const i of [1,2,3,4,5,6]) {
snippets[`demo${i}`] = new Snippet(
"logo",
new Point(30*i, 0)
).scale(i/10)
}
// Prevent clipping
paths.diag = new Path()
.move(new Point(0,-30))
.move(new Point(200,20))
return part
}
```
</Example>

View file

@ -2,4 +2,4 @@
title: Waist to hips
---
The **waist to hips** measurement is measured from your waist down to the top of your hip bone (where your trousers sit). Measure it at the side of your body.
The **waist to hips** measurement is measured from your waist down to the top of your hip bone. Measure it at the side of your body.

View file

@ -88,7 +88,7 @@
"handlebars": "^4.7.7",
"husky": "^8.0.1",
"js-yaml": "^4.0.0",
"lerna": "^6.0.0",
"lerna": "^7.0.2",
"lint-staged": "^13.0.3",
"mocha": "^10.0.0",
"mustache": "^4.0.1",
@ -116,7 +116,7 @@
"version": "0.0.0",
"dependencies": {
"autoprefixer": "^10.4.0",
"c8": "^7.12.0",
"c8": "^8.0.0",
"handlebars": "^4.7.7",
"jsonfile": "^6.1.0",
"postcss": "^8.4.5",

View file

@ -59,7 +59,7 @@
"lodash.clonedeep": "^4.5.0"
},
"devDependencies": {
"eslint": "8.42.0",
"eslint": "8.43.0",
"nyc": "15.1.0",
"mocha": "10.2.0",
"chai": "4.3.7",

View file

@ -176,7 +176,7 @@ Part.prototype.shorthand = function () {
get: function (measurements, name) {
if (typeof measurements[name] === 'undefined')
self.context.store.log.warning(
`Tried to access \`measurements.${name}\` but it is \`undefined\``
`${self.name} tried to access \`measurements.${name}\` but it is \`undefined\``
)
return Reflect.get(...arguments)
},

View file

@ -52,6 +52,30 @@ Snippet.prototype.clone = function () {
return clone
}
/**
* Helper method to scale a snippet
*
* @param {number} scale - The scale to set
* @param {bool} overwrite - Whether to overwrite the existing scale or not (default is true)
*
* @return {Snippet} this - The snippet instance
*/
Snippet.prototype.scale = function (scale, overwrite = true) {
return this.attr('data-scale', scale, overwrite)
}
/**
* Helper method to rotate a snippet
*
* @param {number} rotation - The rotation to set
* @param {bool} overwrite - Whether to overwrite the existing rotation or not (default is true)
*
* @return {Snippet} this - The snippet instance
*/
Snippet.prototype.rotate = function (rotation, overwrite = true) {
return this.attr('data-rotate', rotation, overwrite)
}
/**
* Returns a snippet as an object suitable for inclusion in renderprops
*

View file

@ -27,6 +27,16 @@ describe('Snippet', () => {
expect(s.attributes.get('class')).to.equal('less')
})
it('Should scale a snippet', () => {
let s = new Snippet('test', new Point(12, -34)).scale(0.1234)
expect(s.attributes.get('data-scale')).to.equal('0.1234')
})
it('Should rotate a snippet', () => {
let s = new Snippet('test', new Point(12, -34)).rotate(123)
expect(s.attributes.get('data-rotate')).to.equal('123')
})
it('Should get a snippet via the snippets proxy', () => {
let result
const part = {

View file

@ -1,5 +1,5 @@
import chai from 'chai'
import * as all from './dist/index.mjs'
import * as all from '../src/index.mjs'
const expect = chai.expect
const { measurements, sizes } = all

View file

@ -86,12 +86,14 @@ const PaperlessDefs = ({ units = 'metric', stacks }) =>
<MetricPaperlessDefs stacks={stacks} />
)
export const Defs = (props) =>
props.svg ? (
export const Defs = (props) => {
console.log(props.svg.defs)
return props.svg ? (
<defs>
{props.svg.defs.forSvg ? sanitize(props.svg.defs.forSvg) : null}
{props.svg.defs.list ? sanitize(Object.values(props.svg.defs.list).join('')) : null}
{props.settings[0].paperless ? (
<PaperlessDefs units={props.settings[0].units} stacks={props.stacks} />
) : null}
</defs>
) : null
}

View file

@ -28,12 +28,12 @@
},
"peerDependencies": {},
"dependencies": {
"@aws-sdk/client-sesv2": "3.352.0",
"@prisma/client": "4.15.0",
"@aws-sdk/client-sesv2": "3.354.0",
"@prisma/client": "4.16.1",
"bcryptjs": "2.4.3",
"cors": "2.8.5",
"crypto": "1.0.1",
"dotenv": "16.1.4",
"dotenv": "16.3.1",
"express": "4.18.2",
"js-yaml": "4.1.0",
"lodash.get": "4.4.2",
@ -44,17 +44,17 @@
"passport-jwt": "4.0.1",
"pino": "8.14.1",
"qrcode": "1.5.3",
"swagger-ui-dist": "4.19.0",
"swagger-ui-dist": "5.1.0",
"swagger-ui-express": "4.6.3"
},
"devDependencies": {
"chai": "4.3.7",
"chai-http": "4.4.0",
"esbuild": "0.18.2",
"esbuild": "0.18.8",
"mocha": "10.2.0",
"mocha-steps": "1.3.0",
"nodemon": "2.0.22",
"prisma": "4.15.0"
"prisma": "4.16.1"
},
"engines": {
"node": ">=16.0.0",

View file

@ -1,9 +1,8 @@
// Hooks
import { useState, useEffect, useContext } from 'react'
import { useContext } from 'react'
import { useTranslation } from 'next-i18next'
// Context
import { ModalContext } from 'shared/context/modal-context.mjs'
import { LoadingContext } from 'shared/context/loading-context.mjs'
// Components
import {
I18nIcon,
@ -17,7 +16,7 @@ import {
FreeSewingIcon,
HeartIcon,
} from 'shared/components/icons.mjs'
import { Ribbon } from 'shared/components/ribbon.mjs'
import { HeaderWrapper } from 'shared/components/wrappers/header.mjs'
import { ModalThemePicker, ns as themeNs } from 'shared/components/modal/theme-picker.mjs'
import { ModalMenu } from 'site/components/navigation/modal-menu.mjs'
@ -100,38 +99,11 @@ const NavIcons = ({ setModal }) => {
)
}
export const Header = () => {
export const Header = (props) => {
const { setModal } = useContext(ModalContext) || {}
const { loading } = useContext(LoadingContext)
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 bottom-0 lg:bottom-auto lg:top-0 left-0
bg-neutral
w-full
z-30
transition-transform
${show || loading ? '' : 'fixed bottom-0 lg:top-0 left-0 translate-y-36 lg:-translate-y-36'}
drop-shadow-xl
`}
>
<HeaderWrapper {...props}>
<div className="m-auto md:px-8">
<div className="p-0 flex flex-row gap-2 justify-between text-neutral-content items-center">
{/* Non-mobile content */}
@ -145,7 +117,6 @@ export const Header = () => {
</div>
</div>
</div>
<Ribbon />
</header>
</HeaderWrapper>
)
}

View file

@ -10,7 +10,7 @@ export const DocsLayout = ({ children = [], pageTitle = false }) => {
const { title, crumbs } = useContext(NavigationContext)
return (
<div className="grid grid-cols-4 m-auto justify-center place-items-stretch lg:mt-16">
<div className="grid grid-cols-4 m-auto justify-center place-items-stretch">
<AsideNavigation />
<section className="col-span-4 lg:col-span-3 py-8 lg:py-16 px-4 lg:pl-8 bg-base-50">
{title && (

View file

@ -4,6 +4,8 @@ import { siteConfig } from 'site/site.config.mjs'
import Link from 'next/link'
import { ClearIcon } from 'shared/components/icons.mjs'
export const ns = ['search']
const searchClient = algoliasearch(siteConfig.algolia.app, siteConfig.algolia.key)
const Hit = (props) => (

View file

@ -1,26 +0,0 @@
import Head from 'next/head'
import { Header, ns as headerNs } from 'site/components/header/index.mjs'
import { Footer, ns as footerNs } from 'shared/components/footer/index.mjs'
export const ns = [...new Set([...headerNs, ...footerNs])]
export const LayoutWrapper = ({ children = [], header = false }) => {
const ChosenHeader = header ? header : Header
return (
<div
className={`
flex flex-col justify-between
min-h-screen
bg-base-100
`}
>
<Head>
<meta name="viewport" content="width=device-width, initial-scale=1" />
</Head>
<ChosenHeader />
<main className="grow">{children}</main>
<Footer />
</div>
)
}

View file

@ -33,20 +33,20 @@
"@mdx-js/mdx": "2.3.0",
"@mdx-js/react": "2.3.0",
"@mdx-js/runtime": "2.0.0-next.9",
"@next/bundle-analyzer": "13.4.6",
"@next/bundle-analyzer": "13.4.7",
"@tailwindcss/typography": "0.5.9",
"algoliasearch": "4.17.2",
"daisyui": "3.1.0",
"algoliasearch": "4.18.0",
"daisyui": "3.1.1",
"lodash.get": "4.4.2",
"lodash.orderby": "4.6.0",
"lodash.set": "4.3.2",
"next": "13.4.6",
"next": "13.4.7",
"react": "18.2.0",
"react-copy-to-clipboard": "5.1.0",
"react-dom": "18.2.0",
"react-hotkeys-hook": "4.4.0",
"react-instantsearch-dom": "6.40.0",
"react-instantsearch-hooks-web": "6.44.2",
"react-instantsearch-hooks-web": "6.45.0",
"react-markdown": "8.0.7",
"react-swipeable": "7.0.1",
"react-timeago": "7.1.0",
@ -61,7 +61,7 @@
"devDependencies": {
"@playwright/test": "^1.32.3",
"autoprefixer": "10.4.14",
"eslint-config-next": "13.4.6",
"eslint-config-next": "13.4.7",
"js-yaml": "4.1.0",
"postcss": "8.4.24",
"playwright": "^1.32.3",

View file

@ -1,9 +1,8 @@
// Hooks
import { useState, useEffect, useContext } from 'react'
import { useContext } from 'react'
import { useTranslation } from 'next-i18next'
// Context
import { ModalContext } from 'shared/context/modal-context.mjs'
import { LoadingContext } from 'shared/context/loading-context.mjs'
// Components
import {
DesignIcon,
@ -12,12 +11,12 @@ import {
UserIcon,
ThemeIcon,
I18nIcon,
MeasureIcon,
MeasieIcon,
PageIcon,
GitHubIcon,
PlusIcon,
} from 'shared/components/icons.mjs'
import { Ribbon } from 'shared/components/ribbon.mjs'
import { HeaderWrapper } from 'shared/components/wrappers/header.mjs'
import { ModalThemePicker, ns as themeNs } from 'shared/components/modal/theme-picker.mjs'
import { ModalLocalePicker, ns as localeNs } from 'shared/components/modal/locale-picker.mjs'
import { ModalMenu } from 'site/components/navigation/modal-menu.mjs'
@ -53,7 +52,7 @@ const NavIcons = ({ setModal }) => {
color={colors[3]}
extraClasses="hidden lg:flex"
>
<MeasureIcon className={iconSize} />
<MeasieIcon className={iconSize} />
</NavButton>
<NavSpacer />
<NavButton
@ -93,42 +92,15 @@ const NavIcons = ({ setModal }) => {
)
}
export const Header = ({ setSearch }) => {
export const Header = ({ setSearch, show }) => {
const { setModal } = useContext(ModalContext)
const { loading } = useContext(LoadingContext)
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 bottom-0 lg:bottom-auto lg:top-0 left-0
bg-neutral
w-full
z-30
transition-transform
${show || loading ? '' : 'fixed bottom-0 lg:top-0 left-0 translate-y-36 lg:-translate-y-36'}
drop-shadow-xl
`}
>
<HeaderWrapper {...{ setSearch, show }}>
<div className="m-auto md:px-8">
<div className="p-0 flex flex-row gap-2 justify-between text-neutral-content items-center">
{/* Non-mobile content */}
<div className="hidden lg:flex lg:px-2 flex-row items-center justify-center w-full">
<div className="hidden lg:flex lg:flex-row lg:justify-between items-center xl:justify-center w-full">
<NavIcons setModal={setModal} setSearch={setSearch} />
</div>
@ -138,7 +110,6 @@ export const Header = ({ setSearch }) => {
</div>
</div>
</div>
<Ribbon />
</header>
</HeaderWrapper>
)
}

View file

@ -10,9 +10,9 @@ export const DocsLayout = ({ children = [], pageTitle = false }) => {
const { title, crumbs } = useContext(NavigationContext)
return (
<div className="grid grid-cols-4 m-auto justify-center place-items-stretch lg:mt-16">
<div className="grid grid-cols-4 m-auto justify-center place-items-stretch">
<AsideNavigation />
<section className="col-span-4 lg:col-span-3 py-24 px-4 lg:pl-8 bg-base-50">
<section className="col-span-4 lg:col-span-3 py-8 lg:py-24 px-4 lg:pl-8 bg-base-50">
{title && (
<div className="xl:pl-4">
<Breadcrumbs crumbs={crumbs} title={pageTitle ? pageTitle : title} />

View file

@ -14,7 +14,7 @@ export const BeforeNav = ({ app }) => (
)
export const LabLayout = ({ app, AltMenu, children = [] }) => (
<div className="py-24 lg:py-36 flex flex-row">
<div className="pb-24 flex flex-row">
<div className="w-full xl:w-3/4 px-8">{children}</div>
<aside
className={`

View file

@ -1,7 +1,7 @@
export const ns = []
export const WorkbenchLayout = (props) => (
<section id="fs-workbench" className="m-0 lg:mt-24 p-0">
<section id="fs-workbench" className="m-0 p-0">
{props.children}
</section>
)

View file

@ -1 +1,2 @@
export const ns = []
export const Search = () => null

View file

@ -1,26 +0,0 @@
import Head from 'next/head'
import { Header, ns as headerNs } from 'site/components/header/index.mjs'
import { Footer, ns as footerNs } from 'shared/components/footer/index.mjs'
export const ns = [...new Set([...headerNs, ...footerNs])]
export const LayoutWrapper = ({ children = [], header = false }) => {
const ChosenHeader = header ? header : Header
return (
<div
className={`
flex flex-col justify-between
min-h-screen
bg-base-100
`}
>
<Head>
<meta name="viewport" content="width=device-width, initial-scale=1" />
</Head>
<ChosenHeader />
<main className="grow">{children}</main>
<Footer />
</div>
)
}

View file

@ -34,17 +34,17 @@
"@mdx-js/react": "2.3.0",
"@mdx-js/runtime": "2.0.0-next.9",
"@tailwindcss/typography": "0.5.9",
"algoliasearch": "4.17.2",
"algoliasearch": "4.18.0",
"d3-dispatch": "3.0.1",
"d3-drag": "3.0.0",
"d3-selection": "3.0.0",
"daisyui": "3.1.0",
"daisyui": "3.1.1",
"i18next": "22.5.1",
"lodash.get": "4.4.2",
"lodash.orderby": "4.6.0",
"lodash.set": "4.3.2",
"next": "13.4.6",
"next-i18next": "13.3.0",
"next": "13.4.7",
"next-i18next": "14.0.0",
"react": "18.2.0",
"react-copy-to-clipboard": "5.1.0",
"react-hotkeys-hook": "4.4.0",
@ -65,7 +65,7 @@
"devDependencies": {
"@playwright/test": "^1.32.3",
"autoprefixer": "10.4.14",
"eslint-config-next": "13.4.6",
"eslint-config-next": "13.4.7",
"js-yaml": "4.1.0",
"postcss": "8.4.24",
"playwright": "^1.32.3",

View file

@ -5,10 +5,10 @@ import { useTranslation } from 'next-i18next'
// Components
import Head from 'next/head'
import { PageWrapper, ns as pageNs } from 'shared/components/wrappers/page.mjs'
import { Popout } from 'shared/components/popout.mjs'
import { Popout, ns as popoutNs } from 'shared/components/popout.mjs'
import { WebLink } from 'shared/components/web-link.mjs'
const ns = ['lab', ...pageNs]
const ns = ['lab', ...pageNs, ...popoutNs]
/*
* Each page MUST be wrapped in the PageWrapper component.
* You also MUST spread props.page into this wrapper component

View file

@ -0,0 +1,95 @@
// Hooks
import { useEffect, useState } from 'react'
import { useAccount } from 'shared/hooks/use-account.mjs'
import { useBackend } from 'shared/hooks/use-backend.mjs'
import { useDesign } from 'shared/hooks/use-design.mjs'
// Dependencies
import { serverSideTranslations } from 'next-i18next/serverSideTranslations'
import { nsMerge } from 'shared/utils.mjs'
// Components
import { PageWrapper, ns as pageNs } from 'shared/components/wrappers/page.mjs'
import { Workbench, ns as wbNs } from 'shared/components/workbench/index.mjs'
import { WorkbenchLayout } from 'site/components/layouts/workbench.mjs'
import { DynamicOrgDocs as DynamicDocs } from 'site/components/dynamic-org-docs.mjs'
import { VagueError, ns as errorNs } from 'shared/components/errors/vague.mjs'
// Translation namespaces used on this page
const namespaces = nsMerge(errorNs, wbNs, pageNs)
const loadMeasurements = async ({ type, id, backend }) => {
const method = {
set: backend.getSet,
cset: backend.getCuratedSet,
}
const key = {
set: 'set',
cset: 'curatedSet',
}
if (!type || !method[type]) return false
const result = await method[type](id)
if (result.success) return result.data[key[type]]
else return false
}
const NewDesignFromSetPage = ({ page, id, design, type }) => {
const { token } = useAccount()
const backend = useBackend(token)
const [set, setSet] = useState(false)
const [error, setError] = useState(false)
const Design = useDesign(design)
useEffect(() => {
// Guard against loops as the backend object is recreated on each render
const getSet = async () => {
const data = await loadMeasurements({ type, id, backend })
if (data) setSet(data)
else setError(true)
}
if (set === false) getSet()
else if (set?.id && set.id !== id) getSet()
}, [id, type, backend, set])
// Short-circuit errors
if (error)
return (
<PageWrapper {...page} title={false}>
<div className="max-w-lg flex flex-col items-center m-auto justify-center text-center">
<VagueError />
</div>
</PageWrapper>
)
const baseSettings = set?.measies ? { measurements: set.measies } : null
return (
<PageWrapper {...page} title={design} layout={WorkbenchLayout}>
<Workbench {...{ design, Design, set, DynamicDocs, baseSettings }} />
</PageWrapper>
)
}
export default NewDesignFromSetPage
export async function getStaticProps({ locale, params }) {
return {
props: {
...(await serverSideTranslations(locale, [`o_${params.design}`, ...namespaces])),
id: Number(params.id),
design: params.design,
type: params.type,
page: {
locale,
path: ['new', 'pattern', params.design, 'set', params.id],
title: '',
},
},
}
}
export async function getStaticPaths() {
return {
paths: [],
fallback: true,
}
}

View file

@ -1,9 +1,8 @@
// Hooks
import { useState, useEffect, useContext } from 'react'
import { useContext } from 'react'
import { useTranslation } from 'next-i18next'
// Context
import { ModalContext } from 'shared/context/modal-context.mjs'
import { LoadingContext } from 'shared/context/loading-context.mjs'
// Components
import {
DesignIcon,
@ -18,7 +17,7 @@ import {
PageIcon,
PlusIcon,
} from 'shared/components/icons.mjs'
import { Ribbon } from 'shared/components/ribbon.mjs'
import { HeaderWrapper } from 'shared/components/wrappers/header.mjs'
import { ModalThemePicker, ns as themeNs } from 'shared/components/modal/theme-picker.mjs'
import { ModalLocalePicker, ns as localeNs } from 'shared/components/modal/locale-picker.mjs'
import { ModalMenu } from 'site/components/navigation/modal-menu.mjs'
@ -101,38 +100,10 @@ const NavIcons = ({ setModal, setSearch }) => {
)
}
export const Header = ({ setSearch }) => {
export const Header = ({ setSearch, show }) => {
const { setModal } = useContext(ModalContext)
const { loading } = useContext(LoadingContext)
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 bottom-0 lg:bottom-auto lg:top-0 left-0
bg-neutral
w-full
z-30
transition-transform
${show || loading ? '' : 'fixed bottom-0 lg:top-0 left-0 translate-y-36 lg:-translate-y-36'}
drop-shadow-xl
`}
>
<HeaderWrapper setSearch={setSearch} show={show}>
<div className="m-auto md:px-8">
<div className="p-0 flex flex-row gap-2 justify-between text-neutral-content items-center">
{/* Non-mobile content */}
@ -146,7 +117,6 @@ export const Header = ({ setSearch }) => {
</div>
</div>
</div>
<Ribbon />
</header>
</HeaderWrapper>
)
}

View file

@ -10,7 +10,7 @@ export const DocsLayout = ({ children = [], pageTitle = false }) => {
const { crumbs } = useContext(NavigationContext)
return (
<div className="grid grid-cols-4 mx-auto justify-center place-items-stretch lg:mt-16">
<div className="grid grid-cols-4 mx-auto justify-center place-items-stretch">
<AsideNavigation />
<section className="col-span-4 lg:col-span-3 py-8 lg:py-24 px-4 lg:pl-8 bg-base-50">
{pageTitle && (

View file

@ -1,7 +1,7 @@
export const ns = []
export const WorkbenchLayout = (props) => (
<section id="fs-workbench" className="m-0 lg:mt-24 p-0">
<section id="fs-workbench" className="m-0 p-0">
{props.children}
</section>
)

View file

@ -0,0 +1,43 @@
import { SanityMdxWrapper } from './mdx-wrapper.mjs'
import { useTranslation } from 'next-i18next'
export const Author = ({ author = {} }) => {
const { t } = useTranslation(['posts'])
return (
<div id="author" className="flex flex-col lg:flex-row m-auto p-2 items-center">
<div className="theme-gradient w-40 h-40 p-2 rounded-full aspect-square hidden lg:block">
<div
className={`
w-lg bg-cover bg-center rounded-full aspect-square
hidden lg:block
`}
style={{ backgroundImage: `url(${author.image})` }}
></div>
</div>
<div className="theme-gradient p-2 rounded-full aspect-square w-40 h-40 lg:hidden m-auto">
<img
className={`block w-full h-full mx-auto rounded-full`}
src={author.image}
alt={author.displayname}
/>
</div>
<div
className={`
text-center p-2 px-4 rounded-r-lg bg-opacity-50
lg:text-left
`}
>
<p
className="text-xl"
dangerouslySetInnerHTML={{
__html: t('xMadeThis', { x: author.displayname }),
}}
/>
<div className="prose mdx">
<SanityMdxWrapper MDX={author.about} />
</div>
</div>
</div>
)
}

View file

@ -0,0 +1,30 @@
import { compile, run } from '@mdx-js/mdx'
import * as runtime from 'react/jsx-runtime' // Production.
import { useState, useEffect } from 'react'
import { PlainMdxWrapper } from 'shared/components/wrappers/mdx.mjs'
export const useEvaledMdx = (mdxStr = '') => {
const [mdxModule, setMdxModule] = useState(false)
useEffect(() => {
const runEffect = async () => {
const code = await compile(mdxStr, {
outputFormat: 'function-body',
development: false,
})
const evaled = await run(code, runtime)
setMdxModule(() => evaled.default)
}
runEffect()
}, [mdxStr])
return mdxModule
}
export const MdxEvalWrapper = ({ MDX = false, components = {}, site = 'org' }) => {
const evaled = useEvaledMdx(MDX)
return <PlainMdxWrapper {...{ MDX: evaled, components, site }} />
}
export const SanityMdxWrapper = MdxEvalWrapper

View file

@ -0,0 +1,80 @@
import Head from 'next/head'
import { PageLink } from 'shared/components/page-link.mjs'
import { Lightbox } from 'shared/components/lightbox.mjs'
import { ImageWrapper } from 'shared/components/wrappers/img.mjs'
import { PageWrapper, ns as pageNs } from 'shared/components/wrappers/page.mjs'
import { Author } from './author.mjs'
import { TimeAgo } from 'shared/components/wrappers/mdx.mjs'
import { SanityMdxWrapper } from './mdx-wrapper.mjs'
import { useTranslation } from 'next-i18next'
export const ns = ['common', 'posts', ...pageNs]
export const SanityPageWrapper = ({
post = {},
author = {},
page = {},
namespaces = ['common'],
}) => {
const { t } = useTranslation(namespaces)
return (
<PageWrapper title={post.title} {...page}>
<Head>
<meta property="og:type" content="article" key="type" />
<meta property="og:description" content={post.intro || post.title} key="description" />
<meta property="og:article:author" content={author.displayname} key="author" />
<meta property="og:url" content={`https://freesewing.org/blog/${post.slug}`} key="url" />
<meta
property="og:image"
content={`https://canary.backend.freesewing.org/og-img/en/dev/blog/${post.slug}`}
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:locale" content="en_US" key="locale" />
<meta property="og:site_name" content="freesewing.org" key="site" />
</Head>
<article className="mb-12 px-8 max-w-7xl">
<div className="flex flex-row justify-between text-sm mb-1 mt-2">
<div>
<TimeAgo date={post.date} t={t} /> [{post.date}]
</div>
<div>
{post.designs?.map((design) => (
<PageLink
href={`/showcase/designs/${design}`}
txt={design}
key={design}
className="px-2 capitalize"
/>
))}
</div>
<div>
By{' '}
<a href="#maker" className="text-secondary hover:text-secondary-focus">
{author.displayname || 'FIXME: No displayname'}
</a>
</div>
</div>
<figure>
<Lightbox>
<ImageWrapper>
<img src={post.image} alt={post.caption} className="shadow m-auto" />
</ImageWrapper>
<figcaption
className="text-center mb-8 prose m-auto"
dangerouslySetInnerHTML={{ __html: post.caption }}
/>
</Lightbox>
</figure>
<div className="strapi prose lg:prose-lg mb-12 m-auto">
<SanityMdxWrapper MDX={post.body} />
</div>
<div className="max-w-prose text-lg lg:text-xl">
<Author author={author} />
</div>
</article>
</PageWrapper>
)
}

View file

@ -0,0 +1,44 @@
import { createClient } from 'next-sanity'
import { siteConfig } from 'site/site.config.mjs'
let sanityClient
const cache = {}
export const sanityLoader = async ({ query, language, type, slug, order, filters = '' }) => {
sanityClient =
sanityClient ||
createClient({
projectId: 'hl5bw8cj',
dataset: 'site-content',
apiVersion: '2023-06-17',
// token: process.env.SANITY_TOKEN,
useCdn: false,
})
if (!query) {
query = `*[_type == "${type}${language}"`
if (slug) query += ` && slug.current == "${slug}"`
query += ']'
}
if (order) {
query += ` | order(${order})`
}
query += filters
if (cache[query]) return cache[query]
const result = await sanityClient.fetch(query)
cache[query] = result
return result
}
export const sanityImage = (image, dataset = 'site-content') => {
const [, assetName, origSize, format] = image.asset._ref.split('-')
return `https://cdn.sanity.io/images/${siteConfig.sanity.project}/${dataset}/${assetName}-${origSize}.${format}`
}
export const sanitySiteImage = (image) => sanityImage(image, 'site-content')
export const sanityUserImage = (image) => sanityImage(image, 'user-content')
export const numPerPage = 12

View file

@ -6,6 +6,18 @@ import { jargon } from './jargon.mjs'
let config = configBuilder({ site: 'org', jargon })
config.i18n = i18nConfig.i18n
config.rewrites = async () => {
return [
{
source: '/blog',
destination: '/blog/page/1',
},
{
source: '/showcase',
destination: '/showcase/page/1',
},
]
}
// Say hi
console.log(banner + '\n')

View file

@ -34,15 +34,20 @@
"@mdx-js/mdx": "2.3.0",
"@mdx-js/react": "2.3.0",
"@mdx-js/runtime": "2.0.0-next.9",
"@portabletext/react": "^1.0.6",
"@sanity/client": "^6.1.2",
"@tailwindcss/typography": "0.5.9",
"algoliasearch": "4.17.2",
"algoliasearch": "4.18.0",
"react-copy-to-clipboard": "5.1.0",
"daisyui": "3.1.0",
"daisyui": "3.1.1",
"jotai": "2.1.1",
"jotai-location": "0.5.1",
"lodash.get": "4.4.2",
"lodash.orderby": "4.6.0",
"lodash.set": "4.3.2",
"luxon": "3.3.0",
"next": "13.4.6",
"next": "13.4.7",
"next-sanity": "^4.3.3",
"react-dropzone": "14.2.3",
"react-hotkeys-hook": "4.4.0",
"react-instantsearch-dom": "6.40.0",
@ -64,7 +69,7 @@
"devDependencies": {
"@playwright/test": "^1.32.3",
"autoprefixer": "10.4.14",
"eslint-config-next": "13.4.6",
"eslint-config-next": "13.4.7",
"js-yaml": "4.1.0",
"postcss": "8.4.24",
"playwright": "^1.32.3",

View file

@ -0,0 +1,67 @@
import { SanityPageWrapper, ns as sanityNs } from 'site/components/sanity/page-wrapper.mjs'
import { serverSideTranslations } from 'next-i18next/serverSideTranslations'
import { sanityLoader, sanityImage } from 'site/components/sanity/utils.mjs'
const namespaces = [...sanityNs]
const BlogPostPage = (props) => {
return <SanityPageWrapper {...props} namespaces={namespaces} />
}
/*
* getStaticProps() is used to fetch data at build-time.
*
* On this page, it is loading the blog content from strapi.
*
* This, in combination with getStaticPaths() below means this
* page will be used to render/generate all blog content.
*
* To learn more, see: https://nextjs.org/docs/basic-features/data-fetching
*/
export async function getStaticProps({ params, locale }) {
const { slug } = params
const post = await sanityLoader({ type: 'blog', language: locale, slug })
.then((data) => data[0])
.catch((err) => console.log(err))
return {
props: {
post: {
slug,
body: post.body,
title: post.title,
date: post.date,
caption: post.caption,
image: sanityImage(post.image),
},
// FIXME load the author separately
author: {
displayname: post.author,
// slug: post.author.slug,
// about: post.author.about,
// image: strapiImage(post.author.picture, ['small']),
// about: post.author.about,
},
...(await serverSideTranslations(locale, namespaces)),
},
}
}
export const getStaticPaths = async () => {
const paths = await sanityLoader({ language: 'en', type: 'blog' })
.then((data) => data.map((post) => `/blog/${post.slug.current}`))
.catch((err) => console.log(err))
return {
paths: [
...paths,
...paths.map((p) => `/de${p}`),
...paths.map((p) => `/es${p}`),
...paths.map((p) => `/fr${p}`),
...paths.map((p) => `/nl${p}`),
],
fallback: false,
}
}
export default BlogPostPage

View file

@ -1,36 +0,0 @@
// Dependencies
import { serverSideTranslations } from 'next-i18next/serverSideTranslations'
// Components
import { PageWrapper, ns as pageNs } from 'shared/components/wrappers/page.mjs'
import { V3Wip } from 'shared/components/v3-wip.mjs'
// Translation namespaces used on this page
const namespaces = [...new Set(['designs', ...pageNs])]
/*
* Each page MUST be wrapped in the PageWrapper component.
* You also MUST spread props.page into this wrapper component
* when path and locale come from static props (as here)
* or set them manually.
*/
const BlogIndexPage = ({ page }) => (
<PageWrapper {...page}>
<div className="max-w-2xl">
<V3Wip />
</div>
</PageWrapper>
)
export default BlogIndexPage
export async function getStaticProps({ locale }) {
return {
props: {
...(await serverSideTranslations(locale, namespaces)),
page: {
locale,
path: ['blog'],
},
},
}
}

View file

@ -0,0 +1,131 @@
// Dependencies
import { serverSideTranslations } from 'next-i18next/serverSideTranslations'
import { sanitySiteImage, numPerPage, sanityLoader } from 'site/components/sanity/utils.mjs'
// Hooks
import { useTranslation } from 'next-i18next'
// Components
import Link from 'next/link'
import { TimeAgo } from 'shared/components/wrappers/mdx.mjs'
import { PageWrapper, ns as pageNs } from 'shared/components/wrappers/page.mjs'
import { Pagination } from 'shared/components/navigation/pagination.mjs'
import { siteConfig } from 'site/site.config.mjs'
// Translation namespaces used on this page
const namespaces = [...new Set(['designs', ...pageNs])]
const textShadow = {
style: {
textShadow:
'1px 1px 1px #000000, -1px -1px 1px #000000, 1px -1px 1px #000000, -1px 1px 1px #000000, 2px 2px 1px #000000',
},
}
const Preview = ({ post, t }) => (
<div className="shadow rounded-lg">
<Link href={`/blog/${post.slug.current}`} className="hover:underline">
<div
className="bg-base-100 w-full h-full overflow-hidden shadow flex flex-column items-center rounded-lg"
style={{
backgroundImage: `url(${sanitySiteImage(post.image) + '?fit=clip&w=400'})`,
backgroundSize: 'cover',
}}
>
<div className="text-right my-2 w-full">
<div
className={`
bg-neutral text-neutral-content bg-opacity-40 text-right
px-4 py-1
lg:px-8 lg:py-4
`}
>
<h5
className={`
text-neutral-content
text-xl font-bold
md:text-2xl md:font-normal
xl:text-3xl
`}
{...textShadow}
>
{post.title}
</h5>
<p
className={`
hidden md:block
m-0 p-1 -mt-2
text-neutral-content
leading-normal text-sm font-normal
opacity-70
`}
{...textShadow}
>
<TimeAgo date={post.date} t={t} /> by <strong>{post.author}</strong>
</p>
</div>
</div>
</div>
</Link>
</div>
)
/*
* Each page MUST be wrapped in the PageWrapper component.
* You also MUST spread props.page into this wrapper component
* when path and locale come from static props (as here)
* or set them manually.
*/
const BlogIndexPage = ({ posts, page, current, total }) => {
const { t } = useTranslation()
return (
<PageWrapper {...page}>
<div className="grid grid-cols-1 gap-4 lg:grid-cols-2 xl:grid-cols-3 max-w-7xl lg:pr-4 xl:pr-6">
{posts.map((post) => (
<Preview post={post} t={t} key={post.slug.current} />
))}
</div>
<Pagination {...{ current, total }} />
</PageWrapper>
)
}
export default BlogIndexPage
export async function getStaticProps({ locale, params }) {
const allPosts = await sanityLoader({
language: locale,
type: 'blog',
order: 'date desc',
filters: '{_id, date, slug, title, author, image}',
})
const pageNum = parseInt(params.page)
return {
props: {
posts: allPosts.slice(numPerPage * (pageNum - 1), numPerPage * pageNum),
current: pageNum,
total: allPosts.length,
...(await serverSideTranslations(locale, namespaces)),
page: {
locale,
// title: 'Freesewing Blog',
path: ['blog', 'page', params.page],
},
},
}
}
export const getStaticPaths = async () => {
const numPosts = await sanityLoader({ query: `count(*[_type == "blogen"])` })
const numPages = Math.ceil(numPosts / numPerPage)
const paths = []
for (let i = 0; i < numPages; i++) {
const pathName = `/blog/page/${i + 1}`
siteConfig.languages.forEach((l) => paths.push(`${l.length ? '/' : ''}${l}${pathName}`))
}
return {
paths,
fallback: false,
}
}

View file

@ -0,0 +1,50 @@
// Hooks
import { useDesign, collection } from 'shared/hooks/use-design.mjs'
// Dependencies
import { serverSideTranslations } from 'next-i18next/serverSideTranslations'
import { nsMerge } from 'shared/utils.mjs'
// Components
import { PageWrapper, ns as pageNs } from 'shared/components/wrappers/page.mjs'
import { Workbench, ns as wbNs } from 'shared/components/workbench/new.mjs'
import { WorkbenchLayout } from 'site/components/layouts/workbench.mjs'
import { DynamicOrgDocs as DynamicDocs } from 'site/components/dynamic-org-docs.mjs'
// Translation namespaces used on this page
const namespaces = nsMerge(wbNs, pageNs)
const NewDesignPage = ({ page, design }) => {
const Design = useDesign(design)
return (
<PageWrapper {...page} title={design} layout={WorkbenchLayout}>
<Workbench {...{ design, Design, DynamicDocs }} />
</PageWrapper>
)
}
export default NewDesignPage
export async function getStaticProps({ locale, params }) {
return {
props: {
...(await serverSideTranslations(locale, [`o_${params.design}`, ...namespaces])),
design: params.design,
page: {
locale,
path: ['new', params.design],
title: '',
},
},
}
}
/*
* getStaticPaths() is used to specify for which routes (think URLs)
* this page should be used to generate the result.
*/
export async function getStaticPaths() {
return {
paths: [...collection.map((design) => `/new/${design}`)],
fallback: 'blocking',
}
}

View file

@ -2,10 +2,12 @@
import { serverSideTranslations } from 'next-i18next/serverSideTranslations'
// Components
import { PageWrapper, ns as pageNs } from 'shared/components/wrappers/page.mjs'
import { V3Wip } from 'shared/components/v3-wip.mjs'
import { ns as authNs } from 'shared/components/wrappers/auth/index.mjs'
import { ns as setsNs } from 'shared/components/account/sets.mjs'
import { DesignPicker, ns as designNs } from 'shared/components/designs/design-picker.mjs'
// Translation namespaces used on this page
const namespaces = [...new Set(['showcase', ...pageNs])]
const namespaces = [...new Set([...designNs, ...setsNs, ...authNs, ...pageNs])]
/*
* Each page MUST be wrapped in the PageWrapper component.
@ -13,15 +15,13 @@ const namespaces = [...new Set(['showcase', ...pageNs])]
* when path and locale come from static props (as here)
* or set them manually.
*/
const DesignsPage = ({ page }) => (
const NewSetPage = ({ page }) => (
<PageWrapper {...page}>
<div className="max-w-2xl">
<V3Wip />
</div>
<DesignPicker />
</PageWrapper>
)
export default DesignsPage
export default NewSetPage
export async function getStaticProps({ locale }) {
return {
@ -29,7 +29,7 @@ export async function getStaticProps({ locale }) {
...(await serverSideTranslations(locale, namespaces)),
page: {
locale,
path: ['showcase'],
path: ['new', 'pattern'],
},
},
}

View file

@ -0,0 +1,71 @@
import { SanityPageWrapper, ns as sanityNs } from 'site/components/sanity/page-wrapper.mjs'
import { serverSideTranslations } from 'next-i18next/serverSideTranslations'
import { sanityLoader, sanityImage } from 'site/components/sanity/utils.mjs'
const namespaces = [...sanityNs]
const ShowcasePage = (props) => {
return <SanityPageWrapper {...props} namespaces={namespaces} />
}
/*
* getStaticProps() is used to fetch data at build-time.
*
* On this page, it is loading the showcase content from strapi.
*
* This, in combination with getStaticPaths() below means this
* page will be used to render/generate all showcase content.
*
* To learn more, see: https://nextjs.org/docs/basic-features/data-fetching
*/
export async function getStaticProps({ params, locale }) {
const { slug } = params
const post = await sanityLoader({ type: 'showcase', language: locale, slug })
.then((data) => data[0])
.catch((err) => console.log(err))
const designs = [post.design1 || null]
if (post.design2 && post.design2.length > 2) designs.push(post.design2)
if (post.design3 && post.design3.length > 2) designs.push(post.design3)
return {
props: {
post: {
slug,
body: post.body,
title: post.title,
date: post.date,
caption: post.caption,
image: sanityImage(post.image[0]),
designs,
},
// FIXME load the author separately
author: {
displayname: post.maker,
// slug: post.maker.slug,
// image: strapiImage(post.maker.picture, ['small']),
// ...(await mdxCompiler(post.maker.about)),
},
...(await serverSideTranslations(locale, namespaces)),
},
}
}
export const getStaticPaths = async () => {
const paths = await sanityLoader({ language: 'en', type: 'showcase' })
.then((data) => data.map((post) => `/showcase/${post.slug.current}`))
.catch((err) => console.log(err))
return {
paths: [
...paths,
...paths.map((p) => `/de${p}`),
...paths.map((p) => `/es${p}`),
...paths.map((p) => `/fr${p}`),
...paths.map((p) => `/nl${p}`),
],
fallback: false,
}
}
export default ShowcasePage

View file

@ -0,0 +1,122 @@
// Dependencies
import { serverSideTranslations } from 'next-i18next/serverSideTranslations'
import { sanitySiteImage, numPerPage, sanityLoader } from 'site/components/sanity/utils.mjs'
// Hooks
import { useTranslation } from 'next-i18next'
// Components
import { PageWrapper, ns as pageNs } from 'shared/components/wrappers/page.mjs'
import Link from 'next/link'
import { Pagination } from 'shared/components/navigation/pagination.mjs'
import { siteConfig } from 'site/site.config.mjs'
// Translation namespaces used on this page
const namespaces = [...new Set(['common', 'designs', ...pageNs])]
export const PreviewTile = ({ img, slug, title }) => (
<Link href={`/showcase/${slug}`} className="text-center">
<span
style={{ backgroundImage: `url(${img})`, backgroundSize: 'cover' }}
className={`
rounded-full inline-block border-base-100
w-40 h-40
md:w-56 md:h-56
`}
></span>
<p>{title}</p>
</Link>
)
// const DesignPosts = ({ design, posts }) => {
// const { t } = useTranslation(['patterns'])
// return (
// <div className='py-2'>
// <h2>
// <Link href={`/showcase/designs/${design}`}>
// <a className="hover:text-secondary-focus hover:underline">{t(`${design}.t`)}</a>
// </Link>
// </h2>
// </div>
// )
// }
const Posts = ({ posts }) => {
const previews = []
posts.forEach((post) => {
// for (const design of post.designs) {
// if (typeof designs[design] === 'undefined') designs[design] = []
// designs[design].push(post)
// }
previews.push(
<PreviewTile
img={sanitySiteImage(post.image[0]) + '?fit=clip&w=400'}
slug={post.slug.current}
title={post.title}
key={post.slug.current}
/>
)
})
return (
<div className="grid grid-cols-1 gap-4 xl:gap-8 lg:grid-cols-2 xl:grid-cols-3 lg:pr-4 xl:pr-8">
{previews}
</div>
)
}
const ShowcaseIndexPage = ({ posts, page, current, total }) => {
const { t } = useTranslation()
// const designKeys = useMemo(() => Object.keys(designs).sort(), [designs])
return (
<PageWrapper title={t('showcase')} {...page}>
<div className="text-center">
<Posts locale={page.locale} posts={posts} />
<Pagination {...{ current, total }} />
</div>
</PageWrapper>
)
}
export default ShowcaseIndexPage
export async function getStaticProps({ locale, params }) {
const allPosts = await sanityLoader({
language: locale,
type: 'showcase',
order: 'date desc',
filters: '{_id, date, slug, title, maker, image}',
})
const pageNum = parseInt(params.page)
return {
props: {
// designs,
posts: allPosts.slice(numPerPage * (pageNum - 1), numPerPage * pageNum),
current: pageNum,
total: allPosts.length,
...(await serverSideTranslations(locale, namespaces)),
page: {
locale,
// title: 'Freesewing Blog',
path: ['showcase', 'page', params.page],
},
},
}
}
export const getStaticPaths = async () => {
const numPosts = await sanityLoader({ query: `count(*[_type == "showcaseen"])` })
const numPages = Math.ceil(numPosts / numPerPage)
const paths = []
for (let i = 0; i < numPages; i++) {
const pathName = `/showcase/page/${i + 1}`
siteConfig.languages.forEach((l) => paths.push(`${l.length ? '/' : ''}${l}${pathName}`))
}
return {
paths,
fallback: false,
}
}

View file

@ -7,6 +7,9 @@ export const siteConfig = {
bugsnag: {
key: '1b3a900d6ebbfd071975e39b534e1ff5',
},
sanity: {
project: process.env.SANITY_PROJECT || 'hl5bw8cj',
},
languages: ['en', 'es', 'de', 'fr', 'nl'],
site: 'FreeSewing.org',
}

View file

@ -20,21 +20,21 @@
},
"peerDependencies": {},
"dependencies": {
"@sanity/vision": "3.12.0",
"easymde": "2.16.0",
"@sanity/vision": "3.12.2",
"easymde": "2.18.0",
"react": "18.2.0",
"react-dom": "18.2.0",
"react-is": "18.2.0",
"sanity": "3.12.0",
"sanity": "3.12.2",
"styled-components": "5.3.11",
"sanity-plugin-markdown": "4.1.0"
},
"devDependencies": {
"@sanity/eslint-config-studio": "2.0.1",
"eslint": "8.42.0",
"eslint": "8.43.0",
"prettier": "2.8.8",
"typescript": "5.1.3",
"@sanity/cli": "3.12.1"
"@sanity/cli": "3.12.2"
},
"engines": {
"node": ">=16.0.0",

View file

@ -65,7 +65,7 @@ const transformBlogPost = async (p, lang) => {
const asIs = ['title', 'linktitle', 'caption', 'body']
const post = {
_id: `${lang}.blog.${p.slug}`,
_id: `${lang}--blog--${p.slug}`,
_type: `blog${lang}`,
}
for (const field of asIs) post[field] = p[field]
@ -106,7 +106,7 @@ const transformShowcasePost = async (p, lang) => {
const asIs = ['title', 'caption', 'body']
const post = {
_id: `${lang}.showcase.${p.slug}`,
_id: `${lang}--showcase--${p.slug}`,
_type: `showcase${lang}`,
}
for (const field of asIs) post[field] = p[field]
@ -149,7 +149,7 @@ const transformNewsletterPost = async (p) => {
const asIs = ['title', 'body']
const post = {
_id: `newsletter.${p.slug}`,
_id: `newsletter--${p.slug}`,
_type: 'newsletter',
}
for (const field of asIs) post[field] = p[field]

View file

@ -2,8 +2,7 @@
import { useState, useEffect, useContext, useCallback } from 'react'
import { useTranslation } from 'next-i18next'
import orderBy from 'lodash.orderby'
import { measurements, isDegreeMeasurement } from 'config/measurements.mjs'
import { measurementAsMm, formatMm } from 'shared/utils.mjs'
import { measurements } from 'config/measurements.mjs'
import { measurements as designMeasurements } from 'shared/prebuild/data/design-measurements.mjs'
import { freeSewingConfig as conf } from 'shared/config/freesewing.config.mjs'
// Hooks
@ -23,7 +22,6 @@ import { ModalDesignPicker } from 'shared/components/modal/design-picker.mjs'
import {
FilterIcon,
ClearIcon,
PlusIcon,
OkIcon,
NoIcon,
TrashIcon,
@ -34,6 +32,7 @@ import Markdown from 'react-markdown'
import { Tab } from './bio.mjs'
import Timeago from 'react-timeago'
import { Spinner } from 'shared/components/spinner.mjs'
import { MeasieRow } from 'shared/components/sets/measie-input.mjs'
export const ns = ['account', 'patterns', 'toast']
@ -140,214 +139,6 @@ export const EditRow = (props) => (
</Collapse>
)
const Mval = ({ m, val = false, imperial = false, className = '' }) =>
val ? (
isDegreeMeasurement(m) ? (
<span>{val}°</span>
) : (
<span
dangerouslySetInnerHTML={{ __html: formatMm(val, imperial ? 'imperial' : 'metric') }}
className={className}
/>
)
) : null
export const MeasieRow = (props) => {
const { t, m, mset } = props
const isSet = typeof mset.measies?.[m] === 'undefined' ? false : true
return (
<Collapse
color="secondary"
openTitle={t(m)}
title={
<>
<div className="grow text-left md:text-right block md:inline font-bold pr-4">{t(m)}</div>
{isSet ? (
<Mval m={m} val={mset.measies[m]} imperial={mset.imperial} className="w-1/3" />
) : (
<div className="w-1/3" />
)}
</>
}
toggle={isSet ? <EditIcon /> : <PlusIcon />}
toggleClasses={`btn ${isSet ? 'btn-secondary' : 'btn-neutral bg-opacity-50'}`}
>
<MeasieInput {...props} />
</Collapse>
)
}
const MeasieInput = ({ t, m, mset, startLoading, stopLoading, backend, refresh, toast }) => {
const isDegree = isDegreeMeasurement(m)
const factor = isDegree ? 1 : mset.imperial ? 25.4 : 10
const isValValid = (val) =>
typeof val === 'undefined' || val === '' ? null : val != false && !isNaN(val)
const isValid = (newVal) => (typeof newVal === 'undefined' ? isValValid(val) : isValValid(newVal))
const [val, setVal] = useState(mset.measies?.[m] / factor || '')
const [valid, setValid] = useState(isValid(mset.measies?.[m] / factor || ''))
// Update onChange
const update = (evt) => {
setVal(evt.target.value)
let useVal = isDegree
? evt.target.value
: measurementAsMm(evt.target.value, mset.imperial ? 'imperial' : 'metric')
setValid(isValid(useVal))
}
const save = async () => {
// FIXME
startLoading()
const measies = {}
measies[m] = val * factor
const result = await backend.updateSet(mset.id, { measies })
if (result.success) {
refresh()
toast.for.settingsSaved()
} else toast.for.backendError()
stopLoading()
}
const fraction = (i, base) => update({ target: { value: Math.floor(val) + i / base } })
if (!m) return null
const fractionClasses =
'h-3 border-2 border-solid border-base-100 hover:border-secondary bg-secondary rounded bg-opacity-50 hover:bg-opacity-100'
return (
<div className="form-control mb-2 flex flex-row flexwrap gap-2">
<div className="flex flex-col items-center">
<label className="input-group w-full">
<input
type="text"
className={`
input input-bordered text-base-content border-r-0 w-full
${valid === false && 'input-error'}
${valid === true && 'input-success'}
`}
value={val}
onChange={update}
/>
{mset.imperial ? (
<span
className={`bg-transparent border-y
${valid === false && 'border-error text-neutral-content'}
${valid === true && 'border-success text-neutral-content'}
${valid === null && 'border-base-200 text-base-content'}
`}
>
<Mval
imperial={true}
val={val * 25.4}
m={m}
className="text-base-content bg-transparent text-success text-xs font-bold p-0"
/>
</span>
) : null}
<span
role="img"
className={`bg-transparent border-y
${valid === false && 'border-error text-neutral-content'}
${valid === true && 'border-success text-neutral-content'}
${valid === null && 'border-base-200 text-base-content'}
`}
>
{valid === true && '👍'}
{valid === false && '🤔'}
</span>
<span
className={`
${valid === false && 'bg-error text-neutral-content'}
${valid === true && 'bg-success text-neutral-content'}
${valid === null && 'bg-base-200 text-base-content'}
`}
>
{isDegree ? '° ' : mset.imperial ? 'in' : 'cm'}
</span>
</label>
{mset.imperial ? (
<div className="w-full mt-2">
<div className="flex flex-row items-center">
<span className="text-xs inline-block w-8 text-right pr-2">
<sup>1</sup>/<sub>2</sub>
</span>
<button
className={`w-[50%] ${fractionClasses}`}
title={`1/2"`}
onClick={() => fraction(1, 2)}
/>
</div>
<div className="flex flex-row">
<span className="text-xs inline-block w-8 text-right pr-2">
<sup>1</sup>/<sub>4</sub>
</span>
{[1, 2, 3].map((i) => (
<button
key={i}
className={`w-[25%] ${fractionClasses}`}
title={`${i}1/4"`}
onClick={() => fraction(i, 4)}
/>
))}
</div>
<div className="flex flex-row">
<span className="text-xs inline-block w-8 text-right pr-2">
<sup>1</sup>/<sub>8</sub>
</span>
{[1, 2, 3, 4, 5, 6, 7].map((i) => (
<button
key={i}
className={`w-[12.5%] ${fractionClasses}`}
title={`${i}1/8"`}
onClick={() => fraction(i, 8)}
/>
))}
</div>
<div className="flex flex-row mt-1">
<span className="text-xs inline-block w-8 text-right pr-2">
<sup>1</sup>/<sub>16</sub>
</span>
{[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15].map((i) => (
<button
key={i}
className={`w-[6.25%] ${fractionClasses}`}
title={`${i}1/16"`}
onClick={() => fraction(i, 16)}
/>
))}
</div>
<div className="flex flex-row mt-1">
<span className="text-xs inline-block w-8 text-right pr-2">
<sup>1</sup>/<sub>32</sub>
</span>
{[
1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23,
24, 25, 26, 27, 28, 29, 30, 31,
].map((i) => (
<button
key={i}
className={`w-[3.125%] ${fractionClasses}`}
title={`${i}/32"`}
onClick={() => fraction(i, 32)}
/>
))}
</div>
</div>
) : null}
</div>
<button className="btn btn-secondary w-24" onClick={save} disabled={!valid}>
{t('save')}
</button>
</div>
)
}
const EditImg = ({ t, mset, account, backend, startLoading, stopLoading, toast, refresh }) => {
const [img, setImg] = useState(mset.img)

View file

@ -1,5 +1,5 @@
import { useState } from 'react'
import { CloseIcon } from 'shared/components/icons.mjs'
import { DownIcon } from 'shared/components/icons.mjs'
import Link from 'next/link'
const OpenTitleButton = ({
@ -17,12 +17,11 @@ const OpenTitleButton = ({
bg-${color} text-${color}-content px-4 py-1 text-lg font-medium`}
onClick={toggle}
>
{title}
{<DownIcon className="rotate-180 w-6 h-6 mr-4" />}
{!bottom && title}
<div className="flex flex-row items-center gap-2 z-5">
{openButtons}
<button className="btn btn-ghost btn-xs px-0" onClick={toggle}>
<CloseIcon stroke={3} />
</button>
<button className="btn btn-ghost btn-xs px-0" onClick={toggle}></button>
</div>
</div>
)
@ -68,8 +67,7 @@ export const Collapse = ({
grow flex flex-row gap-4 py-1 px-4 items-center justify-start hover:cursor-pointer hover:bg-${color} hover:bg-opacity-20`}
onClick={onClick ? onClick : () => setOpen(true)}
>
{title}
</div>
<DownIcon /> {title}
{toggle ? (
<button onClick={() => setOpen(true)} className={toggleClasses}>
{toggle}
@ -78,6 +76,7 @@ export const Collapse = ({
buttons
)}
</div>
</div>
)
}

View file

@ -6,7 +6,7 @@ import { DesignTag } from 'shared/components/designs/tag.mjs'
export const ns = ['design', 'designs', 'tags']
const defaultLink = (design) => `/new/pattern/${design}`
const defaultLink = (design) => `/new/${design}`
export const Design = ({ name, hrefBuilder = false }) => {
const { t } = useTranslation(ns)

View file

@ -184,7 +184,16 @@ export const DocsIcon = (props) => (
<path d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
</IconWrapper>
)
export const DoubleLeftIcon = (props) => (
<IconWrapper {...props}>
<path d="M11 19l-7-7 7-7 M19 19l-7-7 7-7" />
</IconWrapper>
)
export const DoubleRightIcon = (props) => (
<IconWrapper {...props}>
<path d="M5 5l7 7-7 7 M13 5l7 7-7 7" />
</IconWrapper>
)
export const DownIcon = (props) => (
<IconWrapper {...props}>
<path
@ -264,11 +273,7 @@ export const HeartIcon = (props) => (
export const HelpIcon = (props) => (
<IconWrapper {...props}>
<path
d="m 20.608816,6.2251433 q 0,1.5957853 -0.473995,2.8281739 -0.458196,1.2165888 -1.311388,2.1171808 -0.853192,0.900591 -2.053981,1.611585 -1.184989,0.710993 -2.685975,1.295588 v 3.602366 H 8.521928 v -5.32455 q 1.1217896,-0.300198 2.022381,-0.616195 0.916392,-0.315997 1.911783,-1.02699 0.932191,-0.631994 1.453586,-1.4693866 0.537195,-0.8373922 0.537195,-1.8959824 0,-1.5799854 -1.02699,-2.2435793 Q 12.408692,4.42396 10.560109,4.42396 9.4225197,4.42396 7.9847329,4.9137554 6.5627461,5.4035509 5.377757,6.1777438 H 4.7457629 V 1.3587883 Q 5.7569535,0.93219225 7.8583341,0.47399649 9.9597147,8.6799264e-7 12.124295,8.6799264e-7 q 3.902564,0 6.193542,1.72218403200736 2.290979,1.7221841 2.290979,4.5029584 z M 14.525872,23.999979 H 8.1427315 v -4.171161 h 6.3831405 z"
stroke="none"
fill="currentColor"
/>
<path d="M9.879 7.519c1.171-1.025 3.071-1.025 4.242 0 1.172 1.025 1.172 2.687 0 3.712-.203.179-.43.326-.67.442-.745.361-1.45.999-1.45 1.827v.75M21 12a9 9 0 11-18 0 9 9 0 0118 0zm-9 5.25h.008v.008H12v-.008z" />
</IconWrapper>
)

View file

@ -67,15 +67,7 @@ const ShowPattern = ({ renderProps, logs, mode = 'normal' }) => {
</div>
)
if (mode === 'xray')
return (
<>
<p>xray</p>
<PatternXray {...{ renderProps }} />
</>
)
return <Pattern {...{ renderProps }} />
return mode === 'xray' ? <PatternXray {...{ renderProps }} /> : <Pattern {...{ renderProps }} />
}
// Wrapper component dealing with the tabs and code view

View file

@ -6,20 +6,18 @@ export const AsideNavigation = ({ mobileOnly = false, before = [], after = [] })
<aside
className={`
hidden lg:block
fixed top-0 right-0 h-screen
overflow-y-auto z-20
min-h-screen
z-20
bg-base-100 text-base-content
px-0 pb-20 pt-8 shrink-0
px-0 pb-20 shrink-0 pt-8
lg:w-auto
lg:sticky lg:relative lg:transform-none
lg:justify-center
lg:bg-base-300 lg:bg-opacity-10
lg:pt-16
${mobileOnly ? 'block lg:hidden w-full ' : ''}
`}
>
<div className="w-screen lg:w-auto">
<div className="w-screen lg:w-auto lg:sticky lg:top-28 max-h-screen overflow-y-auto">
{before}
<MainSections />
<div className="mt-4 pt-4">

View file

@ -0,0 +1,79 @@
import Link from 'next/link'
import { useTranslation } from 'next-i18next'
import { LeftIcon, DoubleLeftIcon, RightIcon, DoubleRightIcon } from 'shared/components/icons.mjs'
const pageButtonClasses = 'btn btn-primary btn-ghost text-base leading-none'
const PageButton = ({ pageNum, label, title, hrefBuilder, visible = true }) => (
<Link
className={`${pageButtonClasses} ${visible ? 'join-item' : 'invisible'}`}
href={hrefBuilder(pageNum)}
title={title || label}
>
{label}
</Link>
)
const defaultHrefBuilder = (pageNum) => `${pageNum}`
export const Pagination = ({ current, total, hrefBuilder = defaultHrefBuilder }) => {
const { t } = useTranslation('common')
const prevButtons = []
const nextButtons = []
const buttonProps = { hrefBuilder }
for (let i = 1; i < 4; i++) {
const isEnd = i === 3
prevButtons.unshift(
<PageButton
key={`prev-${i}`}
{...{
pageNum: isEnd ? 1 : current - i,
label: isEnd ? <DoubleLeftIcon /> : current - i,
visible: current > i,
...buttonProps,
}}
/>
)
nextButtons.push(
<PageButton
key={`next-${i}`}
{...{
pageNum: isEnd ? total : current + i,
label: isEnd ? <DoubleRightIcon /> : current + i,
visible: current < total + 1 - i,
...buttonProps,
}}
/>
)
}
return (
<div className="flex justify-evenly items-center mt-8">
<PageButton
{...{
pageNum: current - 1,
label: <LeftIcon />,
title: t('previous'),
...buttonProps,
}}
/>
<div className="flex items-center">
{prevButtons}
<span className={`text-primary text-xl mx-4`} disabled>
{current}
</span>
{nextButtons}
</div>
<PageButton
{...{
pageNum: current + 1,
label: <RightIcon />,
title: t('next'),
...buttonProps,
}}
/>
</div>
)
}

View file

@ -1,56 +0,0 @@
import { ChoiceLink } from 'shared/components/choice-link.mjs'
import { OkIcon, NoIcon, WarningIcon } from 'shared/components/icons.mjs'
import { useTranslation } from 'next-i18next'
import { capitalize } from 'shared/utils.mjs'
export const ns = ['sets']
const Title = ({ set, language }) => (
<div className="flex flex-row items-center gap-2">
<img
alt="img"
src={set.img}
className="shadow mask mask-squircle bg-neutral aspect-square w-12 h-12"
/>
<span>{set[`name${capitalize(language)}`]}</span>
</div>
)
export const CuratedSetLacksMeasies = ({ set, design, t, language }) => (
<ChoiceLink
icon={<NoIcon className="w-10 h-10 text-error" />}
title={<Title set={set} language={language} />}
href={`/new/pattern/${design}/sets/${set.id}`}
>
<div className="flex flex-row gap-2 items-center">
<WarningIcon className="w-6 h-6 shrink-0 text-error" />
<span>{t('setLacksMeasiesForDesign', { design: t(`designs:${design}.t`) })}</span>
</div>
</ChoiceLink>
)
export const CuratedSetSummary = ({ set, language, href }) => (
<ChoiceLink
title={<Title set={set} language={language} />}
icon={<OkIcon className="w-10 h-10 text-success" stroke={3} />}
href={href}
/>
)
export const CuratedSetCandidate = ({ set, design, requiredMeasies = [], href }) => {
const { t, i18n } = useTranslation(['sets'])
const { language } = i18n
const setProps = { set, design, t, language, href }
// Quick check for required measurements
if (!set.measies || Object.keys(set.measies).length < requiredMeasies.length)
return <CuratedSetLacksMeasies {...setProps} />
// Proper check for required measurements
for (const m of requiredMeasies) {
if (!Object.keys(set.measies).includes(m)) return <CuratedSetLacksMeasies {...setProps} />
}
return <CuratedSetSummary {...setProps} />
}

View file

@ -0,0 +1,212 @@
import { isDegreeMeasurement } from 'config/measurements.mjs'
import { measurementAsMm, formatMm } from 'shared/utils.mjs'
import { Collapse } from 'shared/components/collapse.mjs'
import { PlusIcon, EditIcon } from 'shared/components/icons.mjs'
import { useState } from 'react'
export const ns = ['account']
const Mval = ({ m, val = false, imperial = false, className = '' }) =>
val ? (
isDegreeMeasurement(m) ? (
<span className={className}>{val}°</span>
) : (
<span
dangerouslySetInnerHTML={{ __html: formatMm(val, imperial ? 'imperial' : 'metric') }}
className={className}
/>
)
) : null
const heightClasses = {
2: 'h-12',
4: 'h-10',
8: 'h-8',
16: 'h-6',
32: 'h-4',
}
const fractionClasses =
'w-full border-2 border-solid border-base-100 hover:border-secondary bg-secondary rounded bg-opacity-50 hover:bg-opacity-100'
const FractionButtons = ({ t, fraction }) => (
<div className="flex flex-row mt-1 content-center items-center justify-around">
<span className="text-xs inline-block pr-2">{t('fractions')}</span>
<div className="grow max-w-2xl flex items-baseline">
{Array.from({ length: 31 }, (_null, i) => {
let denom = 32
let num = i + 1
for (let n = 4; n > 0; n--) {
const fac = Math.pow(2, n)
if (num % fac === 0) {
denom = 32 / fac
num = num / fac
break
}
}
return (
<span className="group w-[3.125%] relative" key={i}>
<button
className={`${heightClasses[denom]} ${fractionClasses}`}
title={`${num}/${denom}`}
onClick={() => fraction(num, denom)}
/>
<span className="group-hover:visible invisible text-xs text-center absolute left-0 -bottom-6">{`${num}/${denom}"`}</span>
</span>
)
})}
</div>
</div>
)
export const MeasieRow = (props) => {
const { t, m, mset } = props
const isSet = typeof mset.measies?.[m] !== 'undefined'
return (
<Collapse
color="secondary"
openTitle={t(m)}
title={
<>
<div className="grow text-left md:text-right block md:inline font-bold pr-4">{t(m)}</div>
{isSet ? (
<Mval m={m} val={mset.measies[m]} imperial={mset.imperial} className="w-1/3" />
) : (
<div className="w-1/3" />
)}
</>
}
toggle={isSet ? <EditIcon /> : <PlusIcon />}
toggleClasses={`btn ${isSet ? 'btn-secondary' : 'btn-neutral bg-opacity-50'}`}
>
<MeasieInput {...props} />
</Collapse>
)
}
export const MeasieInput = ({
t,
m,
mset,
backend,
refresh,
toast,
children,
onUpdate,
startLoading = () => null,
stopLoading = () => null,
}) => {
const isDegree = isDegreeMeasurement(m)
const factor = isDegree ? 1 : mset.imperial ? 25.4 : 10
const isValValid = (val) =>
typeof val === 'undefined' || val === '' ? null : val != false && !isNaN(val)
const isValid = (newVal) => (typeof newVal === 'undefined' ? isValValid(val) : isValValid(newVal))
const [val, setVal] = useState(mset.measies?.[m] / factor || '')
const [valid, setValid] = useState(isValid(mset.measies?.[m] / factor || ''))
// Update onChange
const update = (evt) => {
setVal(evt.target.value)
const useVal = isDegree
? evt.target.value
: measurementAsMm(evt.target.value, mset.imperial ? 'imperial' : 'metric')
const validUpdate = isValid(useVal)
setValid(validUpdate)
if (validUpdate && typeof onUpdate === 'function') {
onUpdate(m, useVal)
}
}
const save = async () => {
// FIXME
startLoading()
const measies = {}
measies[m] = val * factor
const result = await backend.updateSet(mset.id, { measies })
if (result.success) {
refresh()
toast.for.settingsSaved()
} else toast.for.backendError()
stopLoading()
}
const fraction = (i, base) => update({ target: { value: Math.floor(val) + i / base } })
if (!m) return null
return (
<div className="form-control mb-2 ">
<div className="flex items-center gap-4 flex-wrap mx-auto">
<label className="shrink-0 grow max-w-full">
{children}
<span className="input-group">
<input
type="number"
step={mset.imperial && !isDegree ? 0.03125 : 0.1}
className={`
input input-bordered text-base-content border-r-0 w-full
${valid === false && 'input-error'}
${valid === true && 'input-success'}
`}
value={val}
onChange={update}
/>
{mset.imperial ? (
<span
className={`bg-transparent border-y w-20
${valid === false && 'border-error text-neutral-content'}
${valid === true && 'border-success text-neutral-content'}
${valid === null && 'border-base-200 text-base-content'}
`}
>
<Mval
imperial={true}
val={val * 25.4}
m={m}
className="text-base-content bg-transparent text-success text-xs font-bold p-0"
/>
</span>
) : null}
<span
role="img"
className={`bg-transparent border-y
${valid === false && 'border-error text-neutral-content'}
${valid === true && 'border-success text-neutral-content'}
${valid === null && 'border-base-200 text-base-content'}
`}
>
{valid === true && '👍'}
{valid === false && '🤔'}
</span>
<span
className={`w-14 text-center
${valid === false && 'bg-error text-neutral-content'}
${valid === true && 'bg-success text-neutral-content'}
${valid === null && 'bg-base-200 text-base-content'}
`}
>
{isDegree ? '°' : mset.imperial ? 'in' : 'cm'}
</span>
</span>
</label>
{mset.imperial && (
<div className="grow my-2 sm:min-w-[22rem]">
{!isDegree && <FractionButtons {...{ t, fraction }} />}
</div>
)}
</div>
{backend && (
<button className="btn btn-secondary w-24" onClick={save} disabled={!valid}>
{t('save')}
</button>
)}
</div>
)
}

View file

@ -1,54 +1,59 @@
import { ChoiceButton } from 'shared/components/choice-button.mjs'
import { ChoiceLink } from 'shared/components/choice-link.mjs'
import { OkIcon, NoIcon, WarningIcon } from 'shared/components/icons.mjs'
import { useTranslation } from 'next-i18next'
import { capitalize, hasRequiredMeasurements } from 'shared/utils.mjs'
import Image from 'next/image'
export const ns = ['sets']
const Title = ({ set }) => (
const Title = ({ set, language }) => (
<div className="flex flex-row items-center gap-2">
<img
<Image
alt="img"
src={set.img}
src={set.img || ''}
width={100}
height={100}
className="shadow mask mask-squircle bg-neutral aspect-square w-12 h-12"
/>
<span>{set.name}</span>
<span>{set[`name${language ? capitalize(language) : ''}`]}</span>
</div>
)
export const SetLacksMeasies = ({ set, design, t }) => (
<ChoiceLink
icon={<NoIcon className="w-10 h-10 text-error" />}
title={<Title set={set} />}
href={`/sets/${set.id}`}
>
export const SetSummary = ({ set, href, clickHandler, language, hasMeasies, t, design }) => {
const inner = hasMeasies ? null : (
<div className="flex flex-row gap-2 items-center">
<WarningIcon className="w-6 h-6 shrink-0 text-error" />
<span>{t('setLacksMeasiesForDesign', { design: t(`designs:${design}.t`) })}</span>
</div>
</ChoiceLink>
)
export const SetSummary = ({ set, design, t }) => (
<ChoiceLink
title={<Title set={set} />}
icon={<OkIcon className="w-10 h-10 text-success" stroke={3} />}
href={`/new/pattern/${design}/set/${set.id}`}
></ChoiceLink>
)
export const SetCandidate = ({ set, design, requiredMeasies = [] }) => {
const { t } = useTranslation(['sets'])
const setProps = { set, design, t }
// Quick check for required measurements
if (!set.measies || Object.keys(set.measies).length < requiredMeasies.length)
return <SetLacksMeasies {...setProps} />
// Proper check for required measurements
for (const m of requiredMeasies) {
if (!Object.keys(set.measies).includes(m)) return <SetLacksMeasies {...setProps} />
)
const wrapProps = {
icon: hasMeasies ? (
<OkIcon className="w-10 h-10 text-success" />
) : (
<NoIcon className="w-10 h-10 text-error" />
),
title: <Title set={set} language={language} />,
}
if (clickHandler) wrapProps.onClick = () => clickHandler(set)
else if (href) wrapProps.href = href
const Component = clickHandler ? ChoiceButton : ChoiceLink
return <Component {...wrapProps}>{inner}</Component>
}
export const SetCandidate = ({
set,
design,
requiredMeasies = [],
href,
clickHandler,
language,
}) => {
const { t } = useTranslation(['sets', design])
const [hasMeasies, missingMeasies] = hasRequiredMeasurements(requiredMeasies, set.measies, true)
const setProps = { set, design, t, href, clickHandler, hasMeasies, language }
return <SetSummary {...setProps} />
}

View file

@ -10,24 +10,22 @@ import { useAccount } from 'shared/hooks/use-account.mjs'
import { useBackend } from 'shared/hooks/use-backend.mjs'
// Components
import { SetCandidate, ns as setNs } from 'shared/components/sets/set-candidate.mjs'
import { CuratedSetCandidate } from 'shared/components/sets/curated-set-candidate.mjs'
import { PopoutWrapper } from 'shared/components/wrappers/popout.mjs'
import { Tag } from 'shared/components/tag.mjs'
import { FilterIcon } from 'shared/components/icons.mjs'
export const ns = setNs
export const CuratedSetPicker = ({ design, language }) => {
export const CuratedSetPicker = ({ design, language, href, clickHandler }) => {
// Hooks
const { token } = useAccount()
const backend = useBackend(token)
const { t } = useTranslation('sets')
const { t, i18n } = useTranslation('sets')
// State
const [curatedSets, setCuratedSets] = useState([])
const [filter, setFilter] = useState([])
const [tags, setTags] = useState([])
const [reload, setReload] = useState(0)
// Effects
useEffect(() => {
@ -45,7 +43,7 @@ export const CuratedSetPicker = ({ design, language }) => {
}
}
getCuratedSets()
}, [reload])
}, [backend, language])
const addFilter = (tag) => {
const newFilter = [...filter, tag]
@ -108,11 +106,9 @@ export const CuratedSetPicker = ({ design, language }) => {
<div className="flex flex-row flex-wrap gap-2">
{orderBy(list, ['name'], ['asc']).map((set) => (
<div className="w-full lg:w-96" key={set.id}>
<CuratedSetCandidate
href={`/new/pattern/${design}/cset/${set.id}`}
set={set}
<SetCandidate
requiredMeasies={measurements[design]}
design={design}
{...{ set, design, href, clickHandler, language: i18n.language }}
/>
</div>
))}
@ -121,14 +117,13 @@ export const CuratedSetPicker = ({ design, language }) => {
)
}
export const UserSetPicker = ({ design, t, language }) => {
export const UserSetPicker = ({ design, t, href, clickHandler }) => {
// Hooks
const { token } = useAccount()
const backend = useBackend(token)
// State
const [sets, setSets] = useState({})
const [list, setList] = useState([])
// Effects
useEffect(() => {
@ -141,14 +136,10 @@ export const UserSetPicker = ({ design, t, language }) => {
}
}
getSets()
}, [])
// Need to sort designs by their translated title
const translated = {}
for (const d of list) translated[t(`${d}.t`)] = d
})
return Object.keys(sets).length < 1 ? (
<PopoutWrapper tip>
<PopoutWrapper tip noP>
<h5>{t('patternForWhichSet')}</h5>
<p>{t('fsmtm')}</p>
</PopoutWrapper>
@ -160,7 +151,10 @@ export const UserSetPicker = ({ design, t, language }) => {
<div className="flex flex-row flex-wrap gap-2">
{orderBy(sets, ['name'], ['asc']).map((set) => (
<div className="w-full lg:w-96" key={set.id}>
<SetCandidate set={set} requiredMeasies={measurements[design]} design={design} />
<SetCandidate
requiredMeasies={measurements[design]}
{...{ set, design, href, clickHandler }}
/>
</div>
))}
</div>
@ -174,25 +168,27 @@ export const UserSetPicker = ({ design, t, language }) => {
)
}
export const BookmarkedSetPicker = ({ design, t }) => (
export const BookmarkedSetPicker = ({ t }) => (
<>
<h3>{t('bookmarkedSets')}</h3>
<PopoutWrapper fixme>Implement bookmarked set picker (also implement bookmarks)</PopoutWrapper>
</>
)
export const SetPicker = ({ design }) => {
export const SetPicker = ({ design, href = false, clickHandler = false }) => {
const { t, i18n } = useTranslation('sets')
const { language } = i18n
const pickerProps = { design, t, language }
const pickerProps = { design, t, language, href, clickHandler }
return (
<>
<h2>{t('chooseSet')}</h2>
<UserSetPicker {...pickerProps} />
<BookmarkedSetPicker {...pickerProps} />
<CuratedSetPicker {...pickerProps} />
</>
)
}
//<BookmarkedSetPicker {...pickerProps} />
//<CuratedSetPicker {...pickerProps} />

View file

@ -59,7 +59,10 @@ const NavIcons = ({ setView, setDense, dense, view }) => {
extraClasses="text-success bg-neutral hover:bg-success hover:text-neutral"
>
{dense ? (
<RightIcon className={`${iconSize} animate-bounce-right`} stroke={4} />
<RightIcon
className={`${iconSize} group-hover:animate-[bounceright_1s_infinite] animate-[bounceright_1s_5]`}
stroke={4}
/>
) : (
<LeftIcon className={`${iconSize} animate-bounce-right`} stroke={4} />
)}
@ -143,19 +146,23 @@ const NavIcons = ({ setView, setDense, dense, view }) => {
export const WorkbenchHeader = ({ view, setView }) => {
const [dense, setDense] = useState(true)
return (
<header
className={`
hidden lg:block
h-full w-64 min-h-screen pt-4
bg-neutral
w-64 min-h-screen pt-4
transition-all
drop-shadow-xl
${dense ? '-ml-52' : 'ml-0'}
group
`}
>
<div className="hidden lg:flex lg:flex-col lg:justify-between items-center w-full">
<div
className={`
flex flex-col
items-center w-full sticky top-4 lg:top-28`}
>
<NavIcons {...{ setView, setDense, dense, view }} />
</div>
</header>

View file

@ -11,7 +11,7 @@ import {
} from 'shared/components/icons.mjs'
export const defaultSamm = (units, inMm = true) => {
const dflt = units === 'metric' ? 1 : 0.5
const dflt = units === 'imperial' ? 0.5 : 1
return inMm ? measurementAsMm(dflt, units) : dflt
}
@ -39,9 +39,8 @@ export const loadSettingsConfig = ({
? {
control: 2, // Show when control > 1
min: 0,
max: units === 'metric' ? 2.5 : 2,
dflt: defaultSamm(units, false),
step: units === 'metric' ? 0.1 : 0.125,
max: units === 'imperial' ? 2 : 2.5,
dflt: defaultSamm(units),
icon: SaIcon,
}
: false,
@ -126,8 +125,7 @@ export const loadSettingsConfig = ({
control: 4, // Show when control > 3
min: 0,
max: 2.5,
dflt: units === 'metric' ? 0.2 : 0.125,
step: units === 'metric' ? 0.1 : 0.125,
dflt: measurementAsMm(units === 'imperial' ? 0.125 : 0.2, units),
icon: MarginIcon,
},
})

View file

@ -1,7 +1,7 @@
//Dependencies
import { loadSettingsConfig, defaultSamm } from './config.mjs'
// Components
import { SettingsIcon } from 'shared/components/icons.mjs'
import { SettingsIcon, TrashIcon } from 'shared/components/icons.mjs'
import { WorkbenchMenu } from '../shared/index.mjs'
import { MenuItem } from '../shared/menu-item.mjs'
// input components and event handlers
@ -9,6 +9,8 @@ import { inputs, handlers } from './inputs.mjs'
// values
import { values } from './values.mjs'
import { useTranslation } from 'next-i18next'
export const ns = ['core-settings', 'modal']
/** A wrapper for {@see MenuItem} to handle core settings-specific business */
@ -41,6 +43,21 @@ const CoreSetting = ({ name, config, control, updateFunc, current, passProps, ..
)
}
export const ClearAllButton = ({ setSettings, compact = false }) => {
const { t } = useTranslation('core-settings')
return (
<div className={`${compact ? '' : 'text-center mt-8'}`}>
<button
className={`justify-self-center btn btn-error btn-outline ${compact ? 'btn-sm' : ''}`}
onClick={() => setSettings({})}
>
<TrashIcon />
{t('clearSettings')}
</button>
</div>
)
}
/**
* The core settings menu
* @param {Object} options.update settings and ui update functions

View file

@ -1,4 +1,3 @@
import { measurementAsMm } from 'shared/utils.mjs'
import { ListInput, SliderInput, BoolInput, MmInput } from '../shared/inputs.mjs'
/** an input for the 'only' setting. toggles individual parts*/
@ -45,10 +44,10 @@ export const handlers = {
updateFunc(path, newParts)
},
samm:
({ updateFunc, config, units }) =>
({ updateFunc, config }) =>
(_path, newCurrent) => {
// convert to millimeters if there's a value
newCurrent = newCurrent === undefined ? measurementAsMm(config.dflt, units) : newCurrent
newCurrent = newCurrent === undefined ? config.dflt : newCurrent
// update both values to match
updateFunc([
[['samm'], newCurrent],

View file

@ -85,9 +85,10 @@ export const ListToggle = ({ config, changed, updateFunc, name }) => {
return (
<input
type="checkbox"
className={`toggle toggle-sm ${changed ? 'toggle-accent' : 'toggle-secondary'}`}
className={`toggle ${changed ? 'toggle-accent' : 'toggle-secondary'}`}
checked={checked}
onChange={doToggle}
onClick={(evt) => evt.stopPropagation()}
/>
)
}
@ -220,7 +221,7 @@ export const SliderInput = ({
}
/** A {@see SliderInput} to handle percentage values */
export const PctInput = ({ current, changed, updateFunc, ...rest }) => {
export const PctInput = ({ current, changed, updateFunc, config, ...rest }) => {
const factor = 100
let pctCurrent = changed ? current * factor : current
const pctUpdateFunc = useCallback(
@ -232,6 +233,7 @@ export const PctInput = ({ current, changed, updateFunc, ...rest }) => {
<SliderInput
{...{
...rest,
config: { ...config, dflt: config.dflt * factor },
current: pctCurrent,
updateFunc: pctUpdateFunc,
suffix: '%',
@ -257,13 +259,17 @@ export const MmInput = (props) => {
)
// add a default step that's appropriate to the unit. can be overwritten by config
const defaultStep = units === 'metric' ? 0.1 : 0.125
const defaultStep = units === 'imperial' ? 0.125 : 0.1
return (
<SliderInput
{...{
...props,
config: { step: defaultStep, ...config },
config: {
step: defaultStep,
...config,
dflt: measurementAsUnits(config.dflt, units),
},
current: current === undefined ? undefined : measurementAsUnits(current, units),
updateFunc: mmUpdateFunc,
valFormatter: (val) => (units === 'imperial' ? formatFraction128(val, null) : val),

View file

@ -95,7 +95,7 @@ export const MenuItem = ({
if (loadDocs)
openButtons.push(
<button className={openButtonClass} key="help" onClick={(evt) => loadDocs(evt, name)}>
<HelpIcon className="w-4 h-4" />
<HelpIcon className="w-6 h-6" />
</button>
)
if (allowOverride)
@ -111,10 +111,10 @@ export const MenuItem = ({
<EditIcon className={`w-6 h-6 ${override ? 'bg-base-100 text-accent rounded' : ''}`} />
</button>
)
if (changed && !allowToggle) {
const ResetButton = ({ open }) => (
const ResetButton = ({ open, disabled = false }) => (
<button
className={open ? openButtonClass : 'btn btn-accent'}
className={`${open ? openButtonClass : 'btn btn-accent'} disabled:bg-opacity-0`}
disabled={disabled}
onClick={(evt) => {
evt.stopPropagation()
updateFunc([name])
@ -123,12 +123,15 @@ export const MenuItem = ({
<ClearIcon />
</button>
)
if (changed && !allowToggle) {
buttons.push(<ResetButton key="clear" />)
openButtons.push(<ResetButton open key="clear" />)
}
if (allowToggle) {
buttons.push(<ListToggle key="toggle" {...{ config, changed, updateFunc, name }} />)
} else {
openButtons.push(<ResetButton open disabled={!changed} key="clear" />)
}
// props to pass to the ItemTitle

View file

@ -1,4 +1,4 @@
import { formatMm, formatFraction128 } from 'shared/utils.mjs'
import { formatMm } from 'shared/utils.mjs'
/*********************************************************************************************************
* This file contains the base components to be used for displaying values in menu titles in the workbench
@ -54,11 +54,7 @@ export const MmValue = ({ current, config, units, changed }) => (
<HighlightedValue changed={changed}>
<span
dangerouslySetInnerHTML={{
__html: changed
? formatMm(current, units)
: units === 'imperial'
? formatFraction128(config.dflt)
: `${config.dflt}cm`,
__html: formatMm(changed ? current : config.dflt, units),
}}
/>
</HighlightedValue>

View file

@ -2,12 +2,13 @@
import { useEffect, useState } from 'react'
import { useTranslation } from 'next-i18next'
import { useView } from 'shared/hooks/use-view.mjs'
import { usePatternSettings } from 'shared/hooks/use-pattern-settings.mjs'
import { useAccount } from 'shared/hooks/use-account.mjs'
import { useControlState } from 'shared/components/account/control.mjs'
// Dependencies
import { pluginTheme } from '@freesewing/plugin-theme'
import { pluginI18n } from '@freesewing/plugin-i18n'
import { objUpdate } from 'shared/utils.mjs'
import { objUpdate, hasRequiredMeasurements } from 'shared/utils.mjs'
// Components
import { WorkbenchHeader } from './header.mjs'
import { ErrorView } from 'shared/components/error/view.mjs'
@ -23,6 +24,7 @@ import { ExportView, ns as exportNs } from 'shared/components/workbench/views/ex
import { LogView, ns as logNs } from 'shared/components/workbench/views/logs/index.mjs'
import { InspectView, ns as inspectNs } from 'shared/components/workbench/views/inspect/index.mjs'
import { MeasiesView, ns as measiesNs } from 'shared/components/workbench/views/measies/index.mjs'
export const ns = [
'account',
'workbench',
@ -56,7 +58,7 @@ const views = {
const draftViews = ['draft', 'inspect']
export const Workbench = ({ design, Design, baseSettings, DynamicDocs, from }) => {
export const Workbench = ({ design, Design, DynamicDocs }) => {
// Hooks
const { t, i18n } = useTranslation(ns)
const { language } = i18n
@ -65,15 +67,27 @@ export const Workbench = ({ design, Design, baseSettings, DynamicDocs, from }) =
// State
const [view, setView] = useView()
const [settings, setSettings] = useState({ ...baseSettings, embed: true })
const [settings, setSettings] = usePatternSettings()
const [ui, setUi] = useState(defaultUi)
const [error, setError] = useState(false)
const [mounted, setMounted] = useState(false)
const [missingMeasurements, setMissingMeasurements] = useState(false)
// set mounted on mount
useEffect(() => setMounted(true), [setMounted])
// Effect
useEffect(() => {
// Force re-render when baseSettings changes. Required when they are loaded async.
setSettings({ ...baseSettings, embed: true })
}, [baseSettings])
// protect against loops
if (!mounted) return
const [ok, missing] = hasRequiredMeasurements(Design, settings.measurements)
if (ok) setMissingMeasurements(false)
// Force the measurements view if we have missing measurements
else {
setMissingMeasurements(missing)
if (view !== 'measies') setView('measies')
}
}, [Design, settings.measurements, mounted, view, setView])
// Helper methods for settings/ui updates
const update = {
@ -104,7 +118,7 @@ export const Workbench = ({ design, Design, baseSettings, DynamicDocs, from }) =
}
// Don't bother without a Design
if (!Design || !baseSettings) return <ModalSpinner />
if (!Design) return <ModalSpinner />
// Short-circuit errors early
if (error)
@ -123,6 +137,7 @@ export const Workbench = ({ design, Design, baseSettings, DynamicDocs, from }) =
setView,
update,
settings,
setSettings,
ui,
language,
DynamicDocs,
@ -133,7 +148,7 @@ export const Workbench = ({ design, Design, baseSettings, DynamicDocs, from }) =
switch (view) {
// Save view
case 'save':
viewContent = <SaveView {...viewProps} from={from} />
viewContent = <SaveView {...viewProps} />
break
case 'export':
viewContent = <ExportView {...viewProps} />
@ -141,10 +156,14 @@ export const Workbench = ({ design, Design, baseSettings, DynamicDocs, from }) =
case 'edit':
viewContent = <EditView {...viewProps} setSettings={setSettings} />
break
case 'measies':
viewContent = <MeasiesView {...viewProps} {...{ missingMeasurements }} />
break
default: {
const layout = ui.layouts?.[view] || settings.layout || true
// Generate the pattern here so we can pass it down to both the view and the options menu
const pattern = settings.measurements !== undefined && new Design({ layout, ...settings })
const pattern =
settings.measurements !== undefined && new Design({ layout, embed: true, ...settings })
// Return early if the pattern is not initialized yet
if (typeof pattern.getConfig !== 'function') return null

View file

@ -1,8 +1,9 @@
// Dependencies
import { forwardRef } from 'react'
import { forwardRef, useContext } from 'react'
// Hooks
import { useTranslation } from 'next-i18next'
// Context
import { PanZoomContext } from 'shared/components/workbench/pattern/pan-zoom-context.mjs'
// Components
import { SizeMe } from 'react-sizeme'
import { TransformWrapper, TransformComponent } from 'react-zoom-pan-pinch'
@ -34,8 +35,7 @@ export const PanZoomPattern = forwardRef((props, ref) => {
const { t } = useTranslation(ns)
const { renderProps = false, components = {} } = props
if (!renderProps) return null
const { onTransformed, setZoomFunctions } = useContext(PanZoomContext)
return (
<SizeMe refreshRate={64}>
@ -44,10 +44,13 @@ export const PanZoomPattern = forwardRef((props, ref) => {
minScale={0.1}
centerZoomedOut={true}
wheel={{ activationKeys: ['Control'] }}
doubleClick={{ mode: 'reset' }}
onTransformed={onTransformed}
onInit={setZoomFunctions}
>
<TransformComponent>
<div style={{ width: size.width + 'px' }} className="max-h-screen">
<Pattern {...{ t, components, renderProps }} ref={ref} />
{props.children || <Pattern {...{ t, components, renderProps }} ref={ref} />}
</div>
</TransformComponent>
</TransformWrapper>

View file

@ -0,0 +1,46 @@
import React, { useState, useMemo, useCallback } from 'react'
/**
* A context for managing zoom state of a {@see PanZoomPattern}
* Allows transform handlers to be available in components outside of the TransformWrapper without a bunch of prop drilling
* */
export const PanZoomContext = React.createContext({})
/** Provider for the {@see PanZoomContext} */
export const PanZoomContextProvider = ({ children }) => {
const [zoomed, setZoomed] = useState(false)
const [zoomFunctions, _setZoomFunctions] = useState(false)
const setZoomFunctions = useCallback(
(zoomInstance) => {
const reset = () => {
setZoomed(false)
zoomInstance.resetTransform()
}
if (zoomInstance) {
const { zoomIn, zoomOut, resetTransform } = zoomInstance
_setZoomFunctions({ zoomIn, zoomOut, resetTransform, reset })
}
},
[_setZoomFunctions, setZoomed]
)
const onTransformed = useCallback(
(_ref, state) => {
setZoomed(state.scale !== 1)
},
[setZoomed]
)
const value = useMemo(() => {
return {
zoomed,
zoomFunctions,
setZoomFunctions,
onTransformed,
}
}, [zoomed, zoomFunctions, setZoomFunctions, onTransformed])
return <PanZoomContext.Provider value={value}>{children}</PanZoomContext.Provider>
}

View file

@ -38,6 +38,7 @@ export const CutView = ({
design,
patternConfig,
settings,
setSettings,
ui,
update,
language,
@ -59,7 +60,8 @@ export const CutView = ({
)
useEffect(() => {
if (!materialList.includes(materialSettings.activeMaterial)) setActiveMaterial(materialList[0])
if (materialList.length && !materialList.includes(materialSettings.activeMaterial))
setActiveMaterial(materialList[0])
}, [materialSettings, materialList, setActiveMaterial])
return (
@ -112,6 +114,7 @@ export const CutView = ({
account,
DynamicDocs,
materialSettings,
setSettings,
}}
/>
</div>

View file

@ -5,6 +5,7 @@ import {
} from 'shared/components/workbench/menus/design-options/index.mjs'
import {
CoreSettings,
ClearAllButton,
ns as coreMenuNs,
} from 'shared/components/workbench/menus/core-settings/index.mjs'
import { CutSettings, ns as cutNs } from './settings.mjs'
@ -41,6 +42,7 @@ export const CutMenu = ({
account,
DynamicDocs,
materialSettings,
setSettings,
}) => {
const control = account.control
const menuProps = {
@ -57,8 +59,9 @@ export const CutMenu = ({
<nav className="grow mb-12">
<CutActions update={update} ui={ui} materialSettings={materialSettings} />
<CutSettings {...menuProps} ui={ui} materialSettings={materialSettings} />
<DesignOptions {...menuProps} />
<DesignOptions {...menuProps} isFirst={false} />
<CoreSettings {...menuProps} />
<ClearAllButton setSettings={setSettings} />
</nav>
)
}

View file

@ -64,6 +64,7 @@ export const CutSettings = ({ update, settings, account, materialSettings }) =>
ns,
passProps,
updateFunc,
isFirst: true,
}}
/>
)

View file

@ -1,88 +1,16 @@
import { PanZoomPattern as ShowPattern } from 'shared/components/workbench/pan-zoom-pattern.mjs'
import { DraftMenu, ns as menuNs } from './menu.mjs'
import {
PaperlessIcon,
SaIcon,
RocketIcon,
BulletIcon,
UnitsIcon,
DetailIcon,
} from 'shared/components/icons.mjs'
import { ViewHeader, ns as headerNs } from 'shared/components/workbench/views/view-header.mjs'
import { PanZoomContextProvider } from 'shared/components/workbench/pattern/pan-zoom-context.mjs'
export const ns = menuNs
const IconButton = ({ Icon, onClick, dflt = true }) => (
<button
onClick={onClick}
className={`text-${dflt ? 'neutral-content' : 'accent'} hover:text-secondary-focus`}
>
<Icon />
</button>
)
const Spacer = () => <span className="opacity-50">|</span>
export const DraftViewHeader = ({ update, settings, ui, control }) => {
return (
<div className="flex flex-row gap-4 py-4 mt-2 pt-4 w-full bg-neutral text-neutral-content items-center justify-center">
<div className="flex flex-row items-center gap-4">
<IconButton
Icon={SaIcon}
dflt={settings.sabool ? false : true}
onClick={() => update.toggleSa()}
/>
<IconButton
Icon={PaperlessIcon}
dflt={settings.paperless ? false : true}
onClick={() => update.settings(['paperless'], !settings.paperless)}
/>
<IconButton
Icon={DetailIcon}
dflt={settings.complete}
onClick={() =>
update.settings(
['complete'],
typeof settings.complete === 'undefined' ? 0 : settings.complete ? 0 : 1
)
}
/>
<IconButton
Icon={
settings.units !== 'imperial'
? UnitsIcon
: ({ className }) => <UnitsIcon className={`${className} rotate-180 w-6 h-6`} />
}
dflt={settings.units !== 'imperial'}
onClick={() =>
update.settings(['units'], settings.units === 'imperial' ? 'metric' : 'imperial')
}
/>
</div>
<Spacer />
<div className="flex flex-row items-center">
{[1, 2, 3, 4, 5].map((score) => (
<button onClick={() => update.setControl(score)} className="text-primary" key={score}>
<BulletIcon fill={control >= score ? true : false} />
</button>
))}
</div>
<Spacer />
<div className="flex flex-row items-center gap-4">
<IconButton
Icon={RocketIcon}
dflt={ui.renderer !== 'svg'}
onClick={() => update.ui(['renderer'], ui.renderer === 'react' ? 'svg' : 'react')}
/>
</div>
</div>
)
}
export const ns = [menuNs, ...headerNs]
export const DraftView = ({
design,
pattern,
patternConfig,
settings,
setSettings,
ui,
update,
language,
@ -96,7 +24,11 @@ export const DraftView = ({
if (ui.renderer === 'svg') {
try {
const __html = pattern.render()
output = <div dangerouslySetInnerHTML={{ __html }} />
output = (
<ShowPattern>
<div className="w-full h-full" dangerouslySetInnerHTML={{ __html }} />
</ShowPattern>
)
} catch (err) {
console.log(err)
}
@ -106,13 +38,16 @@ export const DraftView = ({
}
return (
<PanZoomContextProvider>
<div className="flex flex-col">
<DraftViewHeader
<ViewHeader
{...{
settings,
setSettings,
ui,
update,
control: account.control,
setSettings,
}}
/>
<div className="flex flex-row">
@ -123,6 +58,7 @@ export const DraftView = ({
design,
pattern,
patternConfig,
setSettings,
settings,
ui,
update,
@ -137,5 +73,6 @@ export const DraftView = ({
</div>
</div>
</div>
</PanZoomContextProvider>
)
}

View file

@ -4,6 +4,7 @@ import {
} from 'shared/components/workbench/menus/design-options/index.mjs'
import {
CoreSettings,
ClearAllButton,
ns as coreMenuNs,
} from 'shared/components/workbench/menus/core-settings/index.mjs'
import { UiSettings, ns as uiNs } from 'shared/components/workbench/menus/ui-settings/index.mjs'
@ -13,6 +14,7 @@ export const ns = [...coreMenuNs, ...designMenuNs, ...uiNs]
export const DraftMenu = ({
design,
patternConfig,
setSettings,
settings,
ui,
update,
@ -39,6 +41,7 @@ export const DraftMenu = ({
<DesignOptions {...menuProps} />
<CoreSettings {...menuProps} />
<UiSettings {...menuProps} {...{ ui, view, setView }} />
<ClearAllButton setSettings={setSettings} />
</nav>
)
}

View file

@ -1,16 +1,18 @@
import { useState } from 'react'
import { PanZoomPattern as ShowPattern } from 'shared/components/workbench/pan-zoom-pattern.mjs'
import { InspectorPattern } from './inspector/pattern.mjs'
import { DraftMenu, ns as menuNs } from './menu.mjs'
import { objUpdate } from 'shared/utils.mjs'
import { ViewHeader } from '../view-header.mjs'
import { PanZoomContextProvider } from 'shared/components/workbench/pattern/pan-zoom-context.mjs'
export const ns = menuNs
export const DraftView = ({
export const InspectView = ({
design,
pattern,
patternConfig,
settings,
setSettings,
ui,
update,
language,
@ -62,14 +64,21 @@ export const DraftView = ({
}
} else {
renderProps = pattern.getRenderProps()
output = ui.inspect ? (
<InspectorPattern {...{ renderProps, inspector }} />
) : (
<ShowPattern {...{ renderProps, inspector }} />
)
output = <InspectorPattern {...{ renderProps, inspector }} />
}
return (
<PanZoomContextProvider>
<div className="flex flex-col">
<ViewHeader
{...{
settings,
setSettings,
ui,
update,
control: account.control,
}}
/>
<div className="flex flex-row">
<div className="w-2/3 shrink-0 grow lg:p-4 sticky top-0">{output}</div>
<div className="w-1/3 shrink grow-0 lg:p-4 max-w-2xl h-screen overflow-scroll">
@ -79,6 +88,7 @@ export const DraftView = ({
pattern,
patternConfig,
settings,
setSettings,
ui,
update,
language,
@ -92,5 +102,7 @@ export const DraftView = ({
/>
</div>
</div>
</div>
</PanZoomContextProvider>
)
}

View file

@ -4,6 +4,7 @@ import {
} from 'shared/components/workbench/menus/design-options/index.mjs'
import {
CoreSettings,
ClearAllButton,
ns as coreMenuNs,
} from 'shared/components/workbench/menus/core-settings/index.mjs'
import { UiSettings, ns as uiNs } from 'shared/components/workbench/menus/ui-settings/index.mjs'
@ -14,6 +15,7 @@ export const ns = [...coreMenuNs, ...designMenuNs, ...uiNs, inspectorNs]
export const DraftMenu = ({
design,
patternConfig,
setSettings,
settings,
ui,
update,
@ -39,10 +41,11 @@ export const DraftMenu = ({
return (
<nav className="grow mb-12">
{ui.inspect ? <Inspector {...menuProps} {...{ ui, inspector, renderProps }} /> : null}
<Inspector {...menuProps} {...{ ui, inspector, renderProps }} />
<DesignOptions {...menuProps} />
<CoreSettings {...menuProps} />
<UiSettings {...menuProps} {...{ ui, view, setView }} />
<ClearAllButton setSettings={setSettings} />
</nav>
)
}

View file

@ -0,0 +1,23 @@
import { MeasieInput, ns as inputNs } from 'shared/components/sets/measie-input.mjs'
import { useTranslation } from 'next-i18next'
export const ns = ['wbmeasies', ...inputNs]
export const MeasiesEditor = ({ Design, settings, update }) => {
const { t } = useTranslation(ns)
const mset = { measies: settings.measurements, imperial: settings.units === 'imperial' }
const onUpdate = (m, newVal) => {
update.settings(['measurements', m], newVal)
}
return (
<div>
{Design.patternConfig.measurements.map((m) => (
<MeasieInput {...{ t, m, mset, onUpdate }} key={m}>
<span className="label">{t(m)}</span>
</MeasieInput>
))}
</div>
)
}

View file

@ -1,7 +1,61 @@
export const ns = ['wbmeasies']
import { ns as authNs } from 'shared/components/wrappers/auth/index.mjs'
import { SetPicker, ns as setsNs } from 'shared/components/sets/set-picker.mjs'
import { Tabs, Tab } from 'shared/components/mdx/tabs.mjs'
import { MeasiesEditor } from './editor.mjs'
import { Popout } from 'shared/components/popout.mjs'
import { Collapse } from 'shared/components/collapse.mjs'
import { designMeasurements } from 'shared/utils.mjs'
import { useTranslation } from 'next-i18next'
import { useToast } from 'shared/hooks/use-toast.mjs'
export const MeasiesView = () => (
<div className="m-auto mt-24">
<h1 className="max-w-6xl m-auto text-center">fixme: Implement measies view</h1>
export const ns = ['wbmeasies', ...authNs, setsNs]
const tabNames = ['chooseNew', 'editCurrent']
export const MeasiesView = ({ design, Design, settings, update, missingMeasurements, setView }) => {
const { t } = useTranslation(['wbmeasies'])
const toast = useToast()
const tabs = tabNames.map((n) => t(n)).join(',')
const loadMeasurements = (set) => {
update.settings([
[['measurements'], designMeasurements(Design, set.measies)],
[['units'], set.imperial ? 'imperial' : 'metric'],
])
setView('draft')
toast.success(t('updatedMeasurements'))
}
return (
<div className="m-6">
<h1 className="max-w-6xl m-auto text-center">{t('measurements')}</h1>
{missingMeasurements ? (
<Popout note compact dense noP>
<h5>{t('weLackSomeMeasies')}:</h5>
<p>
<b>{t('youCanPickOrEnter')}</b>
</p>
<Collapse title={t('seeMissingMeasies')}>
<ul className="list list-inside list-disc ml-4">
{missingMeasurements.map((m) => (
<li key={m}>{m}</li>
))}
</ul>
</Collapse>
</Popout>
) : (
<Popout tip compact dense noP>
<h5>{t('measiesOk')}</h5>
</Popout>
)}
<Tabs tabs={tabs}>
<Tab key="choose">
<SetPicker design={design} clickHandler={loadMeasurements} />
</Tab>
<Tab key="edit">
<MeasiesEditor {...{ Design, settings, update }} />
</Tab>
</Tabs>
</div>
)
)
}

View file

@ -0,0 +1,7 @@
changeMeasies: Change Pattern Measurements
editCurrent: Edit Current Measurements
chooseNew: Choose a New Measurements Set
weLackSomeMeasies: We lack { nr } measurements to create this pattern
youCanPickOrEnter: You can either pick a measurements set, or enter them by hand, but we cannot proceed without these measurements.
measiesOk: We have all required measurements to create this pattern.

View file

@ -40,6 +40,7 @@ export const PrintView = ({
pattern,
patternConfig,
settings,
setSettings,
ui,
update,
language,
@ -110,6 +111,7 @@ export const PrintView = ({
design,
pattern,
patternConfig,
setSettings,
settings,
ui,
update,

View file

@ -4,6 +4,7 @@ import {
} from 'shared/components/workbench/menus/design-options/index.mjs'
import {
CoreSettings,
ClearAllButton,
ns as coreMenuNs,
} from 'shared/components/workbench/menus/core-settings/index.mjs'
import { PrintSettings, ns as printMenuNs } from './settings.mjs'
@ -14,6 +15,7 @@ export const ns = [...coreMenuNs, ...designMenuNs, ...printMenuNs]
export const PrintMenu = ({
design,
patternConfig,
setSettings,
settings,
ui,
update,
@ -37,8 +39,9 @@ export const PrintMenu = ({
<nav className="grow mb-12">
<PrintActions {...menuProps} ui={ui} exportIt={exportIt} />
<PrintSettings {...menuProps} ui={ui} />
<DesignOptions {...menuProps} />
<DesignOptions {...menuProps} isFirst={false} />
<CoreSettings {...menuProps} />
<ClearAllButton setSettings={setSettings} />
</nav>
)
}

View file

@ -41,6 +41,7 @@ export const PrintSettings = ({ update, settings, ui, account }) => {
ns,
passProps,
updateFunc,
isFirst: true,
}}
/>
)

View file

@ -1,7 +1,8 @@
import { useTranslation } from 'next-i18next'
import { PanZoomPattern } from 'shared/components/workbench/pan-zoom-pattern.mjs'
import { TestMenu, ns as menuNs } from './menu.mjs'
import { DraftViewHeader } from '../draft/index.mjs'
import { ViewHeader } from '../view-header.mjs'
import { PanZoomContextProvider } from 'shared/components/workbench/pattern/pan-zoom-context.mjs'
export const ns = menuNs
@ -9,6 +10,7 @@ export const TestView = ({
design,
pattern,
settings,
setSettings,
ui,
update,
language,
@ -25,10 +27,12 @@ export const TestView = ({
const title = t('testThing', { design, thing: t(settings.sample?.[settings.sample.type]) })
return (
<PanZoomContextProvider>
<div className="flex flex-col">
<DraftViewHeader
<ViewHeader
{...{
settings,
setSettings,
ui,
update,
control: account.control,
@ -46,6 +50,7 @@ export const TestView = ({
pattern,
patternConfig,
settings,
setSettings,
ui,
update,
language,
@ -57,5 +62,6 @@ export const TestView = ({
</div>
</div>
</div>
</PanZoomContextProvider>
)
}

View file

@ -1,4 +1,4 @@
import { MenuItem } from 'shared/components/workbench/menus/shared/menu-item.mjs'
import { useRef } from 'react'
import { WorkbenchMenu } from 'shared/components/workbench/menus/shared/index.mjs'
import {
emojis,
@ -9,34 +9,48 @@ import { optionsMenuStructure } from 'shared/utils.mjs'
export const ns = ['test-view', ...designMenuNs]
const SampleInput = ({ changed, name, t, updateFunc, type }) => {
return (
<>
<p>{t([`${name}.d`, ''])}</p>
<div className="text-center">
<button
className={`btn btn-primary`}
disabled={changed}
onClick={() => updateFunc([name], true)}
>
{t(`testThis.${type}`)}
</button>
</div>
</>
)
}
const closedClasses = `border-r-0 border-t-0 border-b-0 hover:cursor-pointer hover:bg-secondary border-secondary hover:bg-opacity-20`
const openClasses = `border-l-0 border-r-0 border-b-2 lg:border lg:rounded-lg border-primary`
export const SampleItem = ({ name, passProps, t, updateFunc }) => {
const input = useRef(null)
const checked = passProps.settings.sample?.[passProps.type] === name
const onChange = (evt) => {
if (evt.target.checked) updateFunc([name], true)
}
export const SampleItem = ({ name, passProps, ...rest }) => {
return (
<MenuItem
{...{
...rest,
name,
passProps,
changed: passProps.settings.sample?.[passProps.type] === name,
Input: SampleInput,
}}
<div
className={`collapse my-2 shadow border-solid border-l-[6px] min-h-10 rounded-none
${checked ? openClasses : closedClasses}`}
>
<input
ref={input}
type="radio"
name="test-item"
onChange={onChange}
checked={checked}
className="min-h-0"
/>
<div
className={`collapse-title flex items-center p-2 h-auto min-h-0 ${
checked ? 'bg-primary text-primary-content' : ''
}`}
>
<input
ref={input}
type="radio"
checked={checked}
className="radio radio-primary mr-2 radio-sm"
/>
<span className="ml-2">{t([name + '.t', name])}</span>
</div>
{t(name + '.d', '') && (
<div className="collapse-content h-auto pb-0">
<p>{t(name + '.d', '')}</p>
</div>
)}
</div>
)
}

View file

@ -0,0 +1,147 @@
import { useContext } from 'react'
import { PanZoomContext } from 'shared/components/workbench/pattern/pan-zoom-context.mjs'
import { useTranslation } from 'next-i18next'
import {
PaperlessIcon,
SaIcon,
RocketIcon,
BulletIcon,
UnitsIcon,
DetailIcon,
IconWrapper,
ClearIcon,
} from 'shared/components/icons.mjs'
import { ClearAllButton } from 'shared/components/workbench/menus/core-settings/index.mjs'
export const ns = ['common', 'core-settings', 'ui-settings']
const ZoomInIcon = (props) => (
<IconWrapper {...props}>
<path d="M21 21l-5.197-5.197m0 0A7.5 7.5 0 105.196 5.196a7.5 7.5 0 0010.607 10.607zM10.5 7.5v6m3-3h-6" />
</IconWrapper>
)
const ZoomOutIcon = (props) => (
<IconWrapper {...props}>
<path d="M21 21l-5.197-5.197m0 0A7.5 7.5 0 105.196 5.196a7.5 7.5 0 0010.607 10.607zM13.5 10.5h-6" />
</IconWrapper>
)
const IconButton = ({ Icon, onClick, dflt = true, title, hide = false }) => (
<div className="tooltip tooltip-bottom tooltip-primary flex items-center" data-tip={title}>
<button
onClick={onClick}
className={`text-${dflt ? 'neutral-content' : 'accent'} hover:text-secondary-focus ${
hide ? 'invisible' : ''
}`}
title={title}
>
<Icon />
</button>
</div>
)
const ZoomButtons = ({ t }) => {
const { zoomFunctions, zoomed } = useContext(PanZoomContext)
if (!zoomFunctions) return null
return (
<div className="flex flex-row content-center gap-4">
<IconButton
Icon={ClearIcon}
onClick={zoomFunctions.reset}
title={t('resetZoom')}
hide={!zoomed}
/>
<IconButton
Icon={ZoomOutIcon}
onClick={() => zoomFunctions.zoomOut()}
title={t('zoomOut')}
dflt
/>
<IconButton
Icon={ZoomInIcon}
onClick={() => zoomFunctions.zoomIn()}
title={t('zoomIn')}
dflt
/>
</div>
)
}
const Spacer = () => <span className="opacity-50">|</span>
export const ViewHeader = ({ update, settings, ui, control, setSettings }) => {
const { t } = useTranslation(ns)
return (
<div
className={`flex flex-row flex-wrap gap-4 py-4 pt-4 w-full bg-neutral text-neutral-content items-center justify-center lg:sticky top-0 z-20 lg:group-[.header-shown]/layout:top-24 transition-[top] duration-300 ease-in-out`}
>
<div className="hidden lg:flex flex-row flex-wrap gap-4 py-4 pt-4 w-full bg-neutral text-neutral-content items-center justify-center">
<ZoomButtons t={t} />
<Spacer />
<div className="flex flex-row items-center gap-4">
<IconButton
Icon={SaIcon}
dflt={settings.sabool ? false : true}
onClick={() => update.toggleSa()}
title={t('core-settings:sabool.t')}
/>
<IconButton
Icon={PaperlessIcon}
dflt={settings.paperless ? false : true}
onClick={() => update.settings(['paperless'], !settings.paperless)}
title={t('core-settings:paperless.t')}
/>
<IconButton
Icon={DetailIcon}
dflt={settings.complete}
onClick={() =>
update.settings(
['complete'],
typeof settings.complete === 'undefined' ? 0 : settings.complete ? 0 : 1
)
}
title={t('core-settings:complete.t')}
/>
<IconButton
Icon={
settings.units !== 'imperial'
? UnitsIcon
: ({ className }) => <UnitsIcon className={`${className} rotate-180 w-6 h-6`} />
}
dflt={settings.units !== 'imperial'}
onClick={() =>
update.settings(['units'], settings.units === 'imperial' ? 'metric' : 'imperial')
}
title={t('core-settings:units.t')}
/>
</div>
<Spacer />
<div
className="tooltip tooltip-primary tooltip-bottom flex flex-row items-center"
data-tip={t('ui-settings:control.t')}
>
{[1, 2, 3, 4, 5].map((score) => (
<button onClick={() => update.setControl(score)} className="text-primary" key={score}>
<BulletIcon fill={control >= score ? true : false} />
</button>
))}
</div>
<Spacer />
<div className="flex flex-row items-center gap-4">
<IconButton
Icon={RocketIcon}
dflt={ui.renderer !== 'svg'}
onClick={() => update.ui(['renderer'], ui.renderer === 'react' ? 'svg' : 'react')}
title={t('ui-settings:renderer.t')}
/>
</div>
<Spacer />
<div className="flex flex-row items-center gap-4">
<ClearAllButton setSettings={setSettings} compact />
</div>
</div>
</div>
)
}

View file

@ -30,4 +30,4 @@ patternLogs: Pattern logs
patternInspector: Pattern Inspector
docs: Documentation
configurePattern: Configure pattern
measies: Measurements
measies: Pattern Measurements

View file

@ -0,0 +1,26 @@
// Hooks
import { useContext } from 'react'
import { LoadingContext } from 'shared/context/loading-context.mjs'
import { Ribbon } from 'shared/components/ribbon.mjs'
export const HeaderWrapper = ({ show, children }) => {
const { loading } = useContext(LoadingContext)
return (
<header
className={`
fixed bottom-0 lg:bottom-auto lg:top-0 left-0
bg-neutral
w-full
z-30
transition-transform
duration-300 ease-in-out
${show || loading ? '' : 'bottom-0 lg:top-0 left-0 translate-y-36 lg:-translate-y-36'}
drop-shadow-xl
`}
>
{' '}
{children}
<Ribbon />
</header>
)
}

View file

@ -1,3 +1,4 @@
import { useState, useEffect } from 'react'
import Head from 'next/head'
import { Header, ns as headerNs } from 'site/components/header/index.mjs'
import { Footer, ns as footerNs } from 'shared/components/footer/index.mjs'
@ -14,25 +15,51 @@ export const LayoutWrapper = ({
}) => {
const ChosenHeader = header ? header : Header
const [prevScrollPos, setPrevScrollPos] = useState(0)
const [showHeader, setShowHeader] = useState(true)
useEffect(() => {
if (typeof window !== 'undefined') {
const handleScroll = () => {
const curScrollPos = typeof window !== 'undefined' ? window.pageYOffset : 0
if (curScrollPos >= prevScrollPos) {
if (showHeader && curScrollPos > 20) setShowHeader(false)
} else setShowHeader(true)
setPrevScrollPos(curScrollPos)
}
window.addEventListener('scroll', handleScroll)
return () => window.removeEventListener('scroll', handleScroll)
}
}, [prevScrollPos, showHeader])
return (
<div
className={`
flex flex-col justify-between
min-h-screen
bg-base-100
group/layout
header-${showHeader ? 'shown' : 'hidden'}
`}
>
<Head>
<meta name="viewport" content="width=device-width, initial-scale=1" />
</Head>
<ChosenHeader setSearch={setSearch} />
<main className="grow">{children}</main>
<ChosenHeader show={showHeader} />
<main
className={`grow transition-margin duration-300 ease-in-out lg:group-[.header-shown]/layout:mt-24 lg:mt-4
}`}
>
{children}
</main>
{!noSearch && search && (
<>
<div
className={`
fixed w-full max-h-screen bg-base-100 top-0 z-30 pt-0 pb-16 px-8
md:rounded-lg md:top-24
w-full max-h-screen bg-base-100 top-0 z-30 pt-0 pb-16 px-8
md:rounded-lg
md:max-w-xl md:m-auto md:inset-x-12
md:max-w-2xl
lg:max-w-4xl

View file

@ -16,7 +16,7 @@ import { useTranslation } from 'next-i18next'
//import { PrevNext } from '../mdx/prev-next.mjs'
//
//
const TimeAgo = ({ date, t }) => {
export const TimeAgo = ({ date, t }) => {
const i = Interval.fromDateTimes(DateTime.fromISO(date), DateTime.now())
.toDuration(['hours', 'days', 'months', 'years'])
.toObject()
@ -101,9 +101,21 @@ const MetaData = ({ authors = [], maintainers = [], updated = '20220825', locale
</div>
)
export const MdxWrapper = ({ MDX = false, frontmatter = {}, components = {}, children = [] }) => {
export const PlainMdxWrapper = ({ MDX = false, components = {}, children, site = 'org' }) => {
const allComponents = { ...baseComponents(site), ...components }
return <div className="searchme">{MDX ? <MDX components={allComponents} /> : children}</div>
}
export const MdxWrapper = ({
MDX = false,
frontmatter = {},
components = {},
children = [],
site = 'org',
}) => {
const { t } = useTranslation('docs')
const allComponents = { ...baseComponents, ...components }
const { locale, slug } = useContext(NavigationContext)
const updates = docUpdates[slug] || {}
@ -116,7 +128,7 @@ export const MdxWrapper = ({ MDX = false, frontmatter = {}, components = {}, chi
updated={updates.u}
{...{ locale, slug, t }}
/>
<div className="searchme">{MDX ? <MDX components={allComponents} /> : children}</div>
<PlainMdxWrapper {...{ MDX, components, children }} />
</div>
)
}

View file

@ -4,8 +4,9 @@ import React, { useState, useEffect, useContext } from 'react'
// Hooks
import { useTheme } from 'shared/hooks/use-theme.mjs'
// Components
import Head from 'next/head'
import { SwipeWrapper } from 'shared/components/wrappers/swipes.mjs'
import { LayoutWrapper, ns as layoutNs } from 'site/components/wrappers/layout.mjs'
import { LayoutWrapper, ns as layoutNs } from 'shared/components/wrappers/layout.mjs'
import { DocsLayout, ns as docsNs } from 'site/components/layouts/docs.mjs'
import { Feeds } from 'site/components/feeds.mjs'
import { ModalContext } from 'shared/context/modal-context.mjs'
@ -57,7 +58,7 @@ export const PageWrapper = (props) => {
})
setNavupdates(navupdates + 1)
}
}, [path, pageTitle, slug])
}, [path, pageTitle, slug, locale, navupdates, setNavigation])
/*
* Hotkeys (keyboard actions)
@ -80,6 +81,11 @@ export const PageWrapper = (props) => {
// Return wrapper
return (
<SwipeWrapper>
{pageTitle && (
<Head>
<meta property="og:title" content={pageTitle} key="title" />
</Head>
)}
<div
data-theme={currentTheme} // This facilitates CSS selectors
key={currentTheme} // This forces the data-theme update

View file

@ -120,6 +120,16 @@ const config = ({ site, jargon = {} }) => {
externalDir: true,
},
pageExtensions: ['mjs'],
images: {
remotePatterns: [
{
protocol: 'https',
hostname: 'cdn.sanity.io',
pathname: '/images/**',
port: '',
},
],
},
webpack: (config, options) => {
// Fixes npm packages that depend on node modules
if (!options.isServer) {

View file

@ -0,0 +1,21 @@
import { compile, run } from '@mdx-js/mdx'
import * as runtime from 'react/jsx-runtime' // Production.
import { useState, useEffect } from 'react'
export const useEvaledMdx = (mdxStr = '') => {
const [mdxModule, setMdxModule] = useState(false)
useEffect(() => {
;(async () => {
const code = await compile(mdxStr, {
outputFormat: 'function-body',
development: false,
})
const evaled = await run(code, runtime)
setMdxModule(() => evaled.default)
})()
}, [mdxStr])
return mdxModule
}

View file

@ -0,0 +1,6 @@
import { useAtom } from 'jotai'
import { atomWithHash } from 'jotai-location'
const baseSettings = atomWithHash('settings', false, { delayInit: true })
export const usePatternSettings = () => useAtom(baseSettings)

View file

@ -17,14 +17,14 @@
"peerDependencies": {},
"dependencies": {
"@headlessui/react": "1.7.15",
"@next/mdx": "13.4.6",
"@next/mdx": "13.4.7",
"@resvg/resvg-js": "2.4.1",
"@tailwindcss/typography": "0.5.9",
"Buffer": "0.0.0",
"d3-dispatch": "3.0.1",
"d3-drag": "3.0.0",
"d3-selection": "3.0.0",
"daisyui": "3.1.0",
"daisyui": "3.1.1",
"feed": "4.2.2",
"file-saver": "2.0.5",
"front-matter": "4.0.2",
@ -53,8 +53,8 @@
"remark-smartypants": "2.0.0",
"sharp": "0.32.1",
"svg-to-pdfkit": "https://github.com/eriese/SVG-to-PDFKit",
"tlds": "1.239.0",
"to-vfile": "7.2.4",
"tlds": "1.240.0",
"to-vfile": "8.0.0",
"unist-util-visit": "4.1.2",
"use-persisted-state": "0.3.3",
"web-worker": "1.2.0"

View file

@ -54,7 +54,7 @@ export const formatFraction128 = (fraction, format = 'html') => {
rest = fraction - inches
}
let fraction128 = Math.round(rest * 128)
if (fraction128 == 0) return formatImperial(negative, inches, false, false, format)
if (fraction128 == 0) return formatImperial(negative, inches || fraction128, false, false, format)
for (let i = 1; i < 7; i++) {
const numoFactor = Math.pow(2, 7 - i)
@ -198,7 +198,9 @@ export const optionsMenuStructure = (options) => {
// Fixme: One day we should sort this based on the translation
for (const option of orderBy(sorted, ['menu', 'name'], ['asc'])) {
if (typeof option === 'object') {
option.dflt = option.dflt || option[optionType(option)]
const oType = optionType(option)
option.dflt = option.dflt || option[oType]
if (oType === 'pct') option.dflt /= 100
if (option.menu) {
set(menu, `${option.menu}.isGroup`, true)
set(menu, `${option.menu}.${option.name}`, option)
@ -281,3 +283,24 @@ export const scrollTo = (id) => {
// eslint-disable-next-line no-undef
if (document) document.getElementById(id).scrollIntoView()
}
const structureMeasurementsAsDesign = (measurements) => ({ patternConfig: { measurements } })
export const designMeasurements = (Design, measies = {}, DesignIsMeasurementsPojo = false) => {
if (DesignIsMeasurementsPojo) Design = structureMeasurementsAsDesign(Design)
const measurements = {}
for (const m of Design.patternConfig?.measurements || []) measurements[m] = measies[m]
for (const m of Design.patternConfig?.optionalMeasurements || []) measurements[m] = measies[m]
return measurements
}
export const hasRequiredMeasurements = (Design, measies = {}, DesignIsMeasurementsPojo = false) => {
if (DesignIsMeasurementsPojo) Design = structureMeasurementsAsDesign(Design)
const missing = []
for (const m of Design.patternConfig?.measurements || []) {
if (typeof measies[m] === 'undefined') missing.push(m)
}
return [missing.length === 0, missing]
}

View file

@ -1,4 +1,4 @@
import { measurements } from '@freesewing/models'
import { measurements, cisFemaleAdult28 } from '@freesewing/models'
import designs from '../../config/software/designs.json' assert { type: 'json' }
import chai from 'chai'
@ -89,6 +89,28 @@ export const testPatternConfig = (Pattern) => {
expect(measurements.indexOf(measurement)).to.not.equal(-1)
})
}
it('Requests all measurements it uses', () => {
const requested = {}
const patternMeasies = patternConfig.measurements.concat(patternConfig.optionalMeasurements)
for (let measurement of patternMeasies) {
requested[measurement] = cisFemaleAdult28[measurement]
}
const draft = new Pattern({
measurements: requested,
}).draft()
const missWarnings = draft.setStores[0].logs.warning.filter((w, i, a) => {
return w.match(/tried to access \`measurements/) && a.indexOf(w) === i
})
chai.assert(
missWarnings.length === 0,
`expected part to request all used measurements. \nThe following measurements were requested in the config: ${patternMeasies.join(
', '
)} \nbut got the following warnings: \n${missWarnings.join('\n')}
`
)
})
}
// Test validity of the pattern's options

Some files were not shown because too many files have changed in this diff Show more