Merge branch 'develop' into i18n3
This commit is contained in:
commit
fe61e85fa9
101 changed files with 3291 additions and 2445 deletions
|
@ -27,6 +27,7 @@ module.exports = {
|
|||
extends: 'eslint:recommended',
|
||||
env: {
|
||||
es2021: true,
|
||||
node: true,
|
||||
},
|
||||
// Required when using experimental EcmaScript features
|
||||
parser: '@babel/eslint-parser',
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -5,6 +5,7 @@ import { dimensions } from './shared.mjs'
|
|||
export const front = {
|
||||
from: base,
|
||||
name: 'aaron.front',
|
||||
measurements: ['hips'],
|
||||
options: {
|
||||
brianFitCollar: false,
|
||||
brianFitSleeve: false,
|
||||
|
|
|
@ -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' },
|
||||
|
|
|
@ -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' },
|
||||
|
|
|
@ -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,
|
||||
|
|
38
markdown/dev/reference/api/snippet/rotate/en.md
Normal file
38
markdown/dev/reference/api/snippet/rotate/en.md
Normal 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>
|
||||
|
38
markdown/dev/reference/api/snippet/scale/en.md
Normal file
38
markdown/dev/reference/api/snippet/scale/en.md
Normal 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>
|
||||
|
|
@ -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.
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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)
|
||||
},
|
||||
|
|
|
@ -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
|
||||
*
|
||||
|
|
|
@ -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 = {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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 && (
|
||||
|
|
|
@ -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) => (
|
||||
|
|
|
@ -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>
|
||||
)
|
||||
}
|
|
@ -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",
|
||||
|
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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} />
|
||||
|
|
|
@ -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={`
|
||||
|
|
|
@ -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>
|
||||
)
|
||||
|
|
|
@ -1 +1,2 @@
|
|||
export const ns = []
|
||||
export const Search = () => null
|
||||
|
|
|
@ -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>
|
||||
)
|
||||
}
|
|
@ -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",
|
||||
|
|
|
@ -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
|
||||
|
|
95
sites/lab/pages/new/pattern/[design]/[type]/[id].mjs
Normal file
95
sites/lab/pages/new/pattern/[design]/[type]/[id].mjs
Normal 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,
|
||||
}
|
||||
}
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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 && (
|
||||
|
|
|
@ -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>
|
||||
)
|
||||
|
|
43
sites/org/components/sanity/author.mjs
Normal file
43
sites/org/components/sanity/author.mjs
Normal 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>
|
||||
)
|
||||
}
|
30
sites/org/components/sanity/mdx-wrapper.mjs
Normal file
30
sites/org/components/sanity/mdx-wrapper.mjs
Normal 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
|
80
sites/org/components/sanity/page-wrapper.mjs
Normal file
80
sites/org/components/sanity/page-wrapper.mjs
Normal 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>
|
||||
)
|
||||
}
|
44
sites/org/components/sanity/utils.mjs
Normal file
44
sites/org/components/sanity/utils.mjs
Normal 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
|
|
@ -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')
|
||||
|
|
|
@ -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",
|
||||
|
|
67
sites/org/pages/blog/[slug].mjs
Normal file
67
sites/org/pages/blog/[slug].mjs
Normal 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
|
|
@ -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'],
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
131
sites/org/pages/blog/page/[page].mjs
Normal file
131
sites/org/pages/blog/page/[page].mjs
Normal 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,
|
||||
}
|
||||
}
|
50
sites/org/pages/new/[design].mjs
Normal file
50
sites/org/pages/new/[design].mjs
Normal 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',
|
||||
}
|
||||
}
|
|
@ -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'],
|
||||
},
|
||||
},
|
||||
}
|
71
sites/org/pages/showcase/[slug].mjs
Normal file
71
sites/org/pages/showcase/[slug].mjs
Normal 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
|
122
sites/org/pages/showcase/page/[page].mjs
Normal file
122
sites/org/pages/showcase/page/[page].mjs
Normal 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,
|
||||
}
|
||||
}
|
|
@ -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',
|
||||
}
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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>
|
||||
)
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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">
|
||||
|
|
79
sites/shared/components/navigation/pagination.mjs
Normal file
79
sites/shared/components/navigation/pagination.mjs
Normal 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>
|
||||
)
|
||||
}
|
|
@ -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} />
|
||||
}
|
212
sites/shared/components/sets/measie-input.mjs
Normal file
212
sites/shared/components/sets/measie-input.mjs
Normal 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>
|
||||
)
|
||||
}
|
|
@ -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} />
|
||||
}
|
||||
|
|
|
@ -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} />
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
})
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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],
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
}
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -64,6 +64,7 @@ export const CutSettings = ({ update, settings, account, materialSettings }) =>
|
|||
ns,
|
||||
passProps,
|
||||
updateFunc,
|
||||
isFirst: true,
|
||||
}}
|
||||
/>
|
||||
)
|
||||
|
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
23
sites/shared/components/workbench/views/measies/editor.mjs
Normal file
23
sites/shared/components/workbench/views/measies/editor.mjs
Normal 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>
|
||||
)
|
||||
}
|
|
@ -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>
|
||||
)
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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.
|
||||
|
|
@ -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,
|
||||
|
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -41,6 +41,7 @@ export const PrintSettings = ({ update, settings, ui, account }) => {
|
|||
ns,
|
||||
passProps,
|
||||
updateFunc,
|
||||
isFirst: true,
|
||||
}}
|
||||
/>
|
||||
)
|
||||
|
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
|
|
147
sites/shared/components/workbench/views/view-header.mjs
Normal file
147
sites/shared/components/workbench/views/view-header.mjs
Normal 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>
|
||||
)
|
||||
}
|
|
@ -30,4 +30,4 @@ patternLogs: Pattern logs
|
|||
patternInspector: Pattern Inspector
|
||||
docs: Documentation
|
||||
configurePattern: Configure pattern
|
||||
measies: Measurements
|
||||
measies: Pattern Measurements
|
||||
|
|
26
sites/shared/components/wrappers/header.mjs
Normal file
26
sites/shared/components/wrappers/header.mjs
Normal 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>
|
||||
)
|
||||
}
|
|
@ -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
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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) {
|
||||
|
|
21
sites/shared/hooks/use-evaled-mdx.mjs
Normal file
21
sites/shared/hooks/use-evaled-mdx.mjs
Normal 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
|
||||
}
|
6
sites/shared/hooks/use-pattern-settings.mjs
Normal file
6
sites/shared/hooks/use-pattern-settings.mjs
Normal 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)
|
|
@ -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"
|
||||
|
|
|
@ -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]
|
||||
}
|
||||
|
|
|
@ -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
Loading…
Add table
Add a link
Reference in a new issue