1
0
Fork 0

wip(org): Started working on v3 workbench

This commit is contained in:
joostdecock 2023-05-08 19:28:03 +02:00
parent bd3ce90e1a
commit 0dece4d70e
27 changed files with 1332 additions and 636 deletions

File diff suppressed because it is too large Load diff

View file

@ -378,6 +378,8 @@ shared:
'file-saver': '2.0.5'
'front-matter': '4.0.2'
'highlight.js': '11.7.0'
'jotai': '2.1.0'
'jotai-location': '0.5.1'
'lodash.clonedeep': '4.5.0'
'lodash.orderby': *_orderby
'lodash.unset': *_unset

View file

@ -1,4 +1,4 @@
import designsByType from './designs.json' assert { type: 'json' }
import designs from './designs.json' assert { type: 'json' }
import packages from './packages.json' assert { type: 'json' }
import plugins from './plugins.json' assert { type: 'json' }
import sites from './sites.json' assert { type: 'json' }
@ -21,15 +21,8 @@ const unpackDesigns = (obj, folder) =>
])
)
const designs = {
...designsByType.accessories,
...designsByType.blocks,
...designsByType.garments,
...designsByType.utilities,
}
// Re-Export imported JSON
export { designs, designsByType, packages, plugins, sites }
export { designs, packages, plugins, sites }
// All software
export const software = {

View file

@ -6,12 +6,7 @@ import chalk from 'chalk'
import mustache from 'mustache'
import conf from '../lerna.json' assert { type: 'json' }
const { version } = conf
import {
software as software,
publishedTypes as types,
designs,
plugins,
} from '../config/software/index.mjs'
import { software, publishedTypes as types, designs, plugins } from '../config/software/index.mjs'
import { buildOrder } from '../config/build-order.mjs'
import rootPackageJson from '../package.json' assert { type: 'json' }
import { capitalize } from '../packages/core/src/index.mjs'
@ -466,7 +461,7 @@ function formatDate(date) {
function validate() {
for (const type in repo.dirs) {
for (const dir of repo.dirs[type]) {
if (typeof software[dir] === 'undefined' || typeof software[dir].description !== 'string') {
if (typeof software?.[dir]?.description !== 'string') {
log.write(chalk.redBright(` No description for package ${type}/${dir}` + '\n'))
return false
}

View file

@ -142,11 +142,11 @@ sets.push(
nameEs: `Cis-Hombre Gigante - ${size}%`,
nameFr: `Cis-Homme Géant - ${size}%`,
nameNl: `Cis-Heer Reus - ${size}%`,
tagsEn: ['cis-female', 'giants'],
tagsDe: ['cis-weiblich', 'riesen'],
tagsEs: ['cis-mujer', 'gigantes'],
tagsFr: ['cis-femme', 'géants'],
tagsNl: ['cis-dame', 'reuzen'],
tagsEn: ['cis-male', 'giants'],
tagsDe: ['cis-männlich', 'riesen'],
tagsEs: ['cis-hombre', 'gigantes'],
tagsFr: ['cis-homme', 'géants'],
tagsNl: ['cis-heer', 'reuzen'],
measies: cisMaleGiant[size],
})
)

View file

@ -25,65 +25,27 @@ import { ModalThemePicker, ns as themeNs } from 'shared/components/modal/theme-p
import { ModalLocalePicker, ns as localeNs } from 'shared/components/modal/locale-picker.mjs'
import { ModalMenu } from 'site/components/navigation/modal-menu.mjs'
import { NavButton, NavSpacer, colors } from 'shared/components/workbench/header.mjs'
export const ns = ['header', 'sections', ...themeNs, ...localeNs]
const NavButton = ({ href, label, color, children, onClick = false, extraClasses = '' }) => {
const className =
'border-0 px-1 lg:px-4 text-base py-3 lg:py-4 text-center flex flex-col items-center 2xl:w-36 ' +
`hover:bg-${color}-400 text-${color}-400 hover:text-neutral grow lg:grow-0 ${extraClasses}`
const span = <span className="block font-bold hidden 2xl:block">{label}</span>
return onClick ? (
<button {...{ onClick, className }} title={label}>
{children}
{span}
</button>
) : (
<Link {...{ href, className }} title={label}>
{children}
{span}
</Link>
)
}
const NavSpacer = () => (
<div className="hidden lg:block text-base lg:text-4xl font-thin opacity-30 px-0.5 lg:px-2">|</div>
)
export const colors = {
menu: 'red',
designs: 'orange',
patterns: 'yellow',
sets: 'lime',
showcase: 'green',
docs: 'cyan',
theme: 'blue',
language: 'indigo',
search: 'violet',
account: 'purple',
}
const NavIcons = ({ setModal, setSearch }) => {
const { t } = useTranslation(['header'])
const iconSize = 'h-6 w-6 lg:h-12 lg:w-12'
return (
<>
<NavButton
onClick={() => setModal(<ModalMenu />)}
label={t('header:menu')}
color={colors.menu}
>
<NavButton onClick={() => setModal(<ModalMenu />)} label={t('header:menu')} color={colors[0]}>
<MenuIcon className={iconSize} />
</NavButton>
<NavSpacer />
<NavButton href="/designs" label={t('header:designs')} color={colors.designs}>
<NavButton href="/designs" label={t('header:designs')} color={colors[1]}>
<DesignIcon className={iconSize} />
</NavButton>
<NavButton
href="/patterns"
label={t('header:patterns')}
color={colors.patterns}
color={colors[2]}
extraClasses="hidden lg:flex"
>
<PageIcon className={iconSize} />
@ -91,7 +53,7 @@ const NavIcons = ({ setModal, setSearch }) => {
<NavButton
href="/sets"
label={t('header:sets')}
color={colors.sets}
color={colors[3]}
extraClasses="hidden lg:flex"
>
<MeasureIcon className={iconSize} />
@ -99,7 +61,7 @@ const NavIcons = ({ setModal, setSearch }) => {
<NavButton
href="/showcase"
label={t('header:showcase')}
color={colors.showcase}
color={colors[4]}
extraClasses="hidden lg:flex"
>
<ShowcaseIcon className={iconSize} />
@ -107,7 +69,7 @@ const NavIcons = ({ setModal, setSearch }) => {
<NavButton
href="/docs"
label={t('header:docs')}
color={colors.docs}
color={colors[5]}
extraClasses="hidden lg:flex"
>
<DocsIcon className={iconSize} />
@ -116,22 +78,22 @@ const NavIcons = ({ setModal, setSearch }) => {
<NavButton
onClick={() => setModal(<ModalThemePicker />)}
label={t('header:theme')}
color={colors.theme}
color={colors[6]}
>
<ThemeIcon className={iconSize} />
</NavButton>
<NavButton
onClick={() => setModal(<ModalLocalePicker />)}
label={t('header:language')}
color={colors.language}
color={colors[7]}
>
<I18nIcon className={iconSize} />
</NavButton>
<NavButton onClick={() => setSearch(true)} label={t('header:search')} color={colors.search}>
<NavButton onClick={() => setSearch(true)} label={t('header:search')} color={colors[8]}>
<SearchIcon className={iconSize} />
</NavButton>
<NavSpacer />
<NavButton href="/account" label={t('header:account')} color={colors.account}>
<NavButton href="/account" label={t('header:account')} color={colors[9]}>
<UserIcon className={iconSize} />
</NavButton>
</>

View file

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

View file

@ -5,7 +5,7 @@ import { ModalWrapper } from 'shared/components/wrappers/modal.mjs'
export const ns = primaryNs
export const ModalMenu = ({ app }) => {
export const ModalMenu = () => {
const { t } = useTranslation(ns)
return (
@ -20,11 +20,11 @@ export const ModalMenu = ({ app }) => {
>
<div className="w-full lg:w-1/2">
<h3>{t('mainSections')}</h3>
<SectionsMenu app={app} />
<SectionsMenu />
</div>
<div className="w-full lg:w-1/2">
<h3>{t('currentSection')}</h3>
<ActiveSection app={app} bare />
<ActiveSection bare />
</div>
</div>
</div>

View file

@ -1,17 +1,20 @@
import { useContext } from 'react'
import Link from 'next/link'
import { icons, ns as sectionsNs } from 'shared/components/navigation/primary.mjs'
import { useTranslation } from 'next-i18next'
import orderBy from 'lodash.orderby'
import { colors } from 'site/components/header/index.mjs'
import { NavigationContext } from 'shared/context/navigation-context.mjs'
export const ns = sectionsNs
export const SectionsMenu = ({ app }) => {
export const SectionsMenu = () => {
const { t } = useTranslation(ns)
if (!app.state.sections) return null
const { sections = false, slug } = useContext(NavigationContext)
if (!sections) return null
// Ensure each page as an `o` key so we can put them in order
const sortableSections = app.state.sections.map((s) => ({ ...s, o: s.o ? s.o : s.t }))
const sortableSections = sections.map((s) => ({ ...s, o: s.o ? s.o : s.t }))
const output = []
for (const page of orderBy(sortableSections, ['o', 't'])) {
const item = (

View file

@ -5,7 +5,15 @@ import { Search, ns as searchNs } from 'site/components/search.mjs'
export const ns = [...new Set([...headerNs, ...footerNs, ...searchNs])]
export const LayoutWrapper = ({ app, children = [], search, setSearch, noSearch = false }) => {
export const LayoutWrapper = ({
children = [],
search,
setSearch,
noSearch = false,
header = false,
}) => {
const ChosenHeader = header ? header : Header
return (
<div
className={`
@ -17,7 +25,7 @@ export const LayoutWrapper = ({ app, children = [], search, setSearch, noSearch
<Head>
<meta name="viewport" content="width=device-width, initial-scale=1" />
</Head>
<Header app={app} setSearch={setSearch} />
<ChosenHeader setSearch={setSearch} />
<main className="grow">{children}</main>
{!noSearch && search && (
<>
@ -30,12 +38,12 @@ export const LayoutWrapper = ({ app, children = [], search, setSearch, noSearch
lg:max-w-4xl
`}
>
<Search app={app} search={search} setSearch={setSearch} />
<Search search={search} setSearch={setSearch} />
</div>
<div className="fixed top-0 left-0 w-full min-h-screen bg-neutral z-20 bg-opacity-70"></div>
</>
)}
<Footer app={app} />
<Footer />
</div>
)
}

View file

@ -0,0 +1,57 @@
// Hooks
import { useEffect, useState } from 'react'
import { useBackend } from 'shared/hooks/use-backend.mjs'
import { useTranslation } from 'next-i18next'
// Dependencies
import { serverSideTranslations } from 'next-i18next/serverSideTranslations'
import { Aaron } from '@freesewing/aaron'
// 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 { Null } from 'shared/components/null.mjs'
// Translation namespaces used on this page
const namespaces = [...new Set(['aaron', ...wbNs, ...pageNs])]
const NewAaronPage = ({ page, id }) => {
const backend = useBackend()
const [set, setSet] = useState(false)
useEffect(() => {
const getCuratedSet = async () => {
const result = await backend.getCuratedSet(id)
if (result.success) setSet(result.data.curatedSet)
}
getCuratedSet()
}, [id])
return (
<PageWrapper {...page} title="Aaron" layout={WorkbenchLayout} header={Null}>
<Workbench design="aaron" Design={Aaron} set={set} />
</PageWrapper>
)
}
export default NewAaronPage
export async function getStaticProps({ locale, params }) {
return {
props: {
...(await serverSideTranslations(locale, namespaces)),
id: params.id,
page: {
locale,
path: ['new', 'pattern', 'aaron', params.id],
title: '',
},
},
}
}
export async function getStaticPaths() {
return {
paths: [],
fallback: true,
}
}

View file

@ -0,0 +1,37 @@
// Dependencies
import { useState } from 'react'
import { serverSideTranslations } from 'next-i18next/serverSideTranslations'
import { useTranslation } from 'next-i18next'
// Components
import { PageWrapper, ns as pageNs } from 'shared/components/wrappers/page.mjs'
import { ns as authNs } from 'shared/components/wrappers/auth/index.mjs'
import { SetPicker, ns as setsNs } from 'shared/components/sets/set-picker.mjs'
// Translation namespaces used on this page
const namespaces = [...new Set(['account', ...setsNs, ...authNs, ...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 NewPatternPickSetPage = ({ page }) => (
<PageWrapper {...page}>
<SetPicker design="aaron" />
</PageWrapper>
)
export default NewPatternPickSetPage
export async function getStaticProps({ locale }) {
return {
props: {
...(await serverSideTranslations(locale, namespaces)),
page: {
locale,
path: ['new', 'pattern', 'aaron'],
},
},
}
}

View file

@ -24,6 +24,12 @@ export const IconWrapper = ({
<> {children} </>
)
export const BeakerIcon = (props) => (
<IconWrapper {...props}>
<path d="M9.75 3.104v5.714a2.25 2.25 0 01-.659 1.591L5 14.5M9.75 3.104c-.251.023-.501.05-.75.082m.75-.082a24.301 24.301 0 014.5 0m0 0v5.714c0 .597.237 1.17.659 1.591L19.8 15.3M14.25 3.104c.251.023.501.05.75.082M19.8 15.3l-1.57.393A9.065 9.065 0 0112 15a9.065 9.065 0 00-6.23-.693L5 14.5m14.8.8l1.402 1.402c1.232 1.232.65 3.318-1.067 3.611A48.309 48.309 0 0112 21c-2.773 0-5.491-.235-8.135-.687-1.718-.293-2.3-2.379-1.067-3.61L5 14.5" />
</IconWrapper>
)
export const BioIcon = (props) => (
<IconWrapper {...props}>
<path d="M7.5 8.25h9m-9 3H12m-9.75 1.51c0 1.6 1.123 2.994 2.707 3.227 1.129.166 2.27.293 3.423.379.35.026.67.21.865.501L12 21l2.755-4.133a1.14 1.14 0 01.865-.501 48.172 48.172 0 003.423-.379c1.584-.233 2.707-1.626 2.707-3.228V6.741c0-1.602-1.123-2.995-2.707-3.228A48.394 48.394 0 0012 3c-2.392 0-4.744.175-7.043.513C3.373 3.746 2.25 5.14 2.25 6.741v6.018z" />
@ -36,6 +42,12 @@ export const BoxIcon = (props) => (
</IconWrapper>
)
export const BriefcaseIcon = (props) => (
<IconWrapper {...props}>
<path d="M20.25 14.15v4.25c0 1.094-.787 2.036-1.872 2.18-2.087.277-4.216.42-6.378.42s-4.291-.143-6.378-.42c-1.085-.144-1.872-1.086-1.872-2.18v-4.25m16.5 0a2.18 2.18 0 00.75-1.661V8.706c0-1.081-.768-2.015-1.837-2.175a48.114 48.114 0 00-3.413-.387m4.5 8.006c-.194.165-.42.295-.673.38A23.978 23.978 0 0112 15.75c-2.648 0-5.195-.429-7.577-1.22a2.016 2.016 0 01-.673-.38m0 0A2.18 2.18 0 013 12.489V8.706c0-1.081.768-2.015 1.837-2.175a48.111 48.111 0 013.413-.387m7.5 0V5.25A2.25 2.25 0 0013.5 3h-3a2.25 2.25 0 00-2.25 2.25v.894m7.5 0a48.667 48.667 0 00-7.5 0M12 12.75h.008v.008H12v-.008z" />
</IconWrapper>
)
export const Camera = (props) => (
<IconWrapper {...props}>
<>
@ -91,6 +103,12 @@ export const CloseIcon = (props) => (
</IconWrapper>
)
export const CodeIcon = (props) => (
<IconWrapper {...props}>
<path d="M17.25 6.75L22.5 12l-5.25 5.25m-10.5 0L1.5 12l5.25-5.25m7.5-3l-4.5 16.5" />
</IconWrapper>
)
export const CogIcon = (props) => (
<IconWrapper {...props}>
<path d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z" />
@ -121,6 +139,12 @@ export const CopyIcon = (props) => (
</IconWrapper>
)
export const CutIcon = (props) => (
<IconWrapper {...props}>
<path d="M7.848 8.25l1.536.887M7.848 8.25a3 3 0 11-5.196-3 3 3 0 015.196 3zm1.536.887a2.165 2.165 0 011.083 1.839c.005.351.054.695.14 1.024M9.384 9.137l2.077 1.199M7.848 15.75l1.536-.887m-1.536.887a3 3 0 11-5.196 3 3 3 0 015.196-3zm1.536-.887a2.165 2.165 0 001.083-1.838c.005-.352.054-.695.14-1.025m-1.223 2.863l2.077-1.199m0-3.328a4.323 4.323 0 012.068-1.379l5.325-1.628a4.5 4.5 0 012.48-.044l.803.215-7.794 4.5m-2.882-1.664A4.331 4.331 0 0010.607 12m3.736 0l7.794 4.5-.802.215a4.5 4.5 0 01-2.48-.043l-5.326-1.629a4.324 4.324 0 01-2.068-1.379M14.343 12l-2.882 1.664" />
</IconWrapper>
)
export const DesignIcon = (props) => (
<IconWrapper {...props} stroke={0} fill>
<path d="m11.975 2.9104c-1.5285 0-2.7845 1.2563-2.7845 2.7848 0 0.7494 0.30048 1.4389 0.78637 1.9394a0.79437 0.79437 0 0 0 0.0084 0.00839c0.38087 0.38087 0.74541 0.62517 0.94538 0.82483 0.19998 0.19966 0.25013 0.2645 0.25013 0.51907v0.65964l-9.1217 5.2665c-0.28478 0.16442-0.83603 0.46612-1.3165 0.9611-0.48047 0.49498-0.92451 1.3399-0.66684 2.2585 0.22026 0.78524 0.7746 1.3486 1.3416 1.5878 0.56697 0.23928 1.0982 0.23415 1.4685 0.23415h18.041c0.37033 0 0.90158 0.0051 1.4686-0.23415 0.56697-0.23928 1.1215-0.80261 1.3418-1.5878 0.25767-0.91859-0.18662-1.7636-0.66709-2.2585-0.48046-0.49498-1.0315-0.79669-1.3162-0.9611l-8.9844-5.1873v-0.73889c0-0.70372-0.35623-1.2837-0.71653-1.6435-0.35778-0.3572-0.70316-0.58503-0.93768-0.81789-0.20864-0.21601-0.33607-0.50298-0.33607-0.83033 0-0.67 0.52595-1.1962 1.1959-1.1962 0.67001 0 1.1962 0.5262 1.1962 1.1962a0.79429 0.79429 0 0 0 0.79434 0.79427 0.79429 0.79429 0 0 0 0.79427-0.79427c0-1.5285-1.2563-2.7848-2.7848-2.7848zm-0.06859 8.2927 8.9919 5.1914c0.28947 0.16712 0.69347 0.41336 0.94393 0.67138 0.25046 0.25803 0.31301 0.3714 0.24754 0.60483-0.10289 0.36677-0.19003 0.40213-0.35969 0.47373-0.16967 0.07161-0.47013 0.09952-0.80336 0.09952h-18.041c-0.33323 0-0.6337-0.02792-0.80336-0.09952-0.16967-0.07161-0.25675-0.10696-0.35963-0.47373-0.06548-0.23342-0.00303-0.3468 0.24748-0.60483 0.25046-0.25803 0.65471-0.50426 0.94418-0.67138z" />
@ -150,6 +174,12 @@ export const DownIcon = (props) => (
</IconWrapper>
)
export const DownloadIcon = (props) => (
<IconWrapper {...props}>
<path d="M12 9.75v6.75m0 0l-3-3m3 3l3-3m-8.25 6a4.5 4.5 0 01-1.41-8.775 5.25 5.25 0 0110.233-2.33 3 3 0 013.758 3.848A3.752 3.752 0 0118 19.5H6.75z" />
</IconWrapper>
)
export const EditIcon = (props) => (
<IconWrapper {...props}>
<path d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z" />
@ -321,7 +351,7 @@ export const OpenSourceIcon = (props) => (
export const OptionsIcon = (props) => (
<IconWrapper {...props}>
<path d="M12 6V4m0 2a2 2 0 100 4m0-4a2 2 0 110 4m-6 8a2 2 0 100-4m0 4a2 2 0 110-4m0 4v2m0-6V4m6 6v10m6-2a2 2 0 100-4m0 4a2 2 0 110-4m0 4v2m0-6V4" />
<path d="M10.5 6h9.75M10.5 6a1.5 1.5 0 11-3 0m3 0a1.5 1.5 0 10-3 0M3.75 6H7.5m3 12h9.75m-9.75 0a1.5 1.5 0 01-3 0m3 0a1.5 1.5 0 00-3 0m-3.75 0H7.5m9-6h3.75m-3.75 0a1.5 1.5 0 01-3 0m3 0a1.5 1.5 0 00-3 0m-9.75 0h9.75" />
</IconWrapper>
)
@ -437,6 +467,12 @@ export const UnitsIcon = (props) => (
</IconWrapper>
)
export const UploadIcon = (props) => (
<IconWrapper {...props}>
<path d="M12 16.5V9.75m0 0l3 3m-3-3l-3 3M6.75 19.5a4.5 4.5 0 01-1.41-8.775 5.25 5.25 0 0110.233-2.33 3 3 0 013.758 3.848A3.752 3.752 0 0118 19.5H6.75z" />
</IconWrapper>
)
export const UserIcon = (props) => (
<IconWrapper {...props}>
<path d="M5.121 17.804A13.937 13.937 0 0112 16c2.5 0 4.847.655 6.879 1.804M15 10a3 3 0 11-6 0 3 3 0 016 0zm6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
@ -455,6 +491,12 @@ export const WarningIcon = (props) => (
</IconWrapper>
)
export const WrenchIcon = (props) => (
<IconWrapper {...props}>
<path d="M11.42 15.17L17.25 21A2.652 2.652 0 0021 17.25l-5.877-5.877M11.42 15.17l2.496-3.03c.317-.384.74-.626 1.208-.766M11.42 15.17l-4.655 5.653a2.548 2.548 0 11-3.586-3.586l6.837-5.63m5.108-.233c.55-.164 1.163-.188 1.743-.14a4.5 4.5 0 004.486-6.336l-3.276 3.277a3.004 3.004 0 01-2.25-2.25l3.276-3.276a4.5 4.5 0 00-6.336 4.486c.091 1.076-.071 2.264-.904 2.95l-.102.085m-1.745 1.437L5.909 7.5H4.5L2.25 3.75l1.5-1.5L7.5 4.5v1.409l4.26 4.26m-1.745 1.437l1.745-1.437m6.615 8.206L15.75 15.75M4.867 19.125h.008v.008h-.008v-.008z" />
</IconWrapper>
)
export const XrayIcon = (props) => (
<IconWrapper {...props}>
<path d="M14 10l-2 1m0 0l-2-1m2 1v2.5M20 7l-2 1m2-1l-2-1m2 1v2.5M14 4l-2-1-2 1M4 7l2-1M4 7l2 1M4 7v2.5M12 21l-2-1m2 1l2-1m-2 1v-2.5M6 18l-2-1v-2.5M18 18l2-1v-2.5" />

View file

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

View file

@ -20,7 +20,7 @@ export const CuratedSetLacksMeasies = ({ set, design, t, language }) => (
<ChoiceLink
icon={<NoIcon className="w-10 h-10 text-error" />}
title={<Title set={set} language={language} />}
href={`/sets/${set.id}`}
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" />
@ -29,21 +29,19 @@ export const CuratedSetLacksMeasies = ({ set, design, t, language }) => (
</ChoiceLink>
)
export const SetSummary = ({ set, design, t }) => (
export const CuratedSetSummary = ({ set, language, href }) => (
<ChoiceLink
title={<Title set={set} />}
title={<Title set={set} language={language} />}
icon={<OkIcon className="w-10 h-10 text-success" stroke={3} />}
href="/new/pattern"
>
<button className="btn btn-secondary w-full">Use it</button>
</ChoiceLink>
href={href}
/>
)
export const CuratedSetCandidate = ({ set, design, requiredMeasies = [] }) => {
export const CuratedSetCandidate = ({ set, design, requiredMeasies = [], href }) => {
const { t, i18n } = useTranslation(['sets'])
const { language } = i18n
const setProps = { set, design, t, language }
const setProps = { set, design, t, language, href }
// Quick check for required measurements
if (!set.measies || Object.keys(set.measies).length < requiredMeasies.length)

View file

@ -1,6 +1,8 @@
// Dependencies
import orderBy from 'lodash.orderby'
import { measurements } from 'site/prebuild/design-measurements.mjs'
import { siteConfig } from 'site/site.config.mjs'
import { capitalize } from 'shared/utils.mjs'
// Hooks
import { useState, useEffect } from 'react'
import { useTranslation } from 'next-i18next'
@ -10,31 +12,73 @@ import { useBackend } from 'shared/hooks/use-backend.mjs'
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 }) => {
export const CuratedSetPicker = ({ design, language }) => {
// Hooks
const { account, token } = useAccount()
const backend = useBackend(token)
const { t } = useTranslation('sets')
// State
const [curatedSets, setCuratedSets] = useState({})
const [list, setList] = useState([])
const [curatedSets, setCuratedSets] = useState([])
const [filter, setFilter] = useState([])
const [tags, setTags] = useState([])
const [reload, setReload] = useState(0)
// Force a refresh
const refresh = () => setReload(reload + 1)
// Effects
useEffect(() => {
const getCuratedSets = async () => {
const result = await backend.getCuratedSets()
if (result.success) {
const all = {}
for (const set of result.data.curatedSets) all[set.id] = set
const all = []
const allTags = new Set()
for (const set of result.data.curatedSets) {
all.push(set)
for (const tag of set[`tags${capitalize(language)}`]) allTags.add(tag)
}
setCuratedSets(all)
setTags([...allTags])
}
}
getCuratedSets()
}, [])
}, [reload])
const addFilter = (tag) => {
const newFilter = [...filter, tag]
setFilter(newFilter)
}
const removeFilter = (tag) => {
const newFilter = filter.filter((t) => t !== tag)
setFilter(newFilter)
}
const applyFilter = () => {
const newList = new Set()
for (const set of curatedSets) {
const setTags = []
for (const lang of siteConfig.languages) {
const key = `tags${capitalize(lang)}`
setTags.push(...set[key])
}
let match = 0
for (const tag of filter) {
if (setTags.includes(tag)) match++
}
if (match === filter.length) newList.add(set)
}
return [...newList]
}
const list = applyFilter()
// Need to sort designs by their translated title
const translated = {}
@ -42,39 +86,46 @@ export const CuratedSetPicker = ({ design }) => {
return (
<>
<h2>{t('chooseSet')}</h2>
<PopoutWrapper tip>
<h5>{t('patternForWhichSet')}</h5>
<p>{t('fsmtm')}</p>
</PopoutWrapper>
{Object.keys(curatedSets).length > 0 ? (
<>
<div className="flex flex-row flex-wrap gap-2">
{orderBy(curatedSets, ['name'], ['asc']).map((set) => (
<div className="w-full lg:w-96">
<CuratedSetCandidate
set={set}
requiredMeasies={measurements[design]}
design={design}
/>
</div>
))}
<h3>{t('curatedSets')}</h3>
{tags.map((tag) => (
<Tag onClick={() => addFilter(tag)}>{tag}</Tag>
))}
<div className="my-2 p-2 px-4 border rounded-lg bg-secondary bg-opacity-10 max-w-xl">
<div className="flex flex-row items-center justify-between gap-2">
<FilterIcon className="w-6 h-6 text-secondary" />
<span>
{list.length} / {curatedSets.length}
</span>
<button onClick={() => setFilter([])} className="btn btn-secondary btn-sm">
clear
</button>
</div>
{filter.map((tag) => (
<Tag onClick={() => removeFilter(tag)} color="success" hoverColor="error">
{tag}
</Tag>
))}
</div>
<div className="flex flex-row flex-wrap gap-2">
{orderBy(list, ['name'], ['asc']).map((set) => (
<div className="w-full lg:w-96">
<CuratedSetCandidate
href={`/new/pattern/${design}/cset/${set.id}`}
set={set}
requiredMeasies={measurements[design]}
design={design}
/>
</div>
</>
) : (
<PopoutWrapper fixme compact>
Implement UI for when there are no sets
</PopoutWrapper>
)}
))}
</div>
</>
)
}
export const UserSetPicker = ({ design }) => {
export const UserSetPicker = ({ design, t, language }) => {
// Hooks
const { account, token } = useAccount()
const backend = useBackend(token)
const { t } = useTranslation('sets')
// State
const [sets, setSets] = useState({})
@ -97,13 +148,14 @@ export const UserSetPicker = ({ design }) => {
const translated = {}
for (const d of list) translated[t(`${d}.t`)] = d
return (
return Object.keys(sets).length < 1 ? (
<PopoutWrapper tip>
<h5>{t('patternForWhichSet')}</h5>
<p>{t('fsmtm')}</p>
</PopoutWrapper>
) : (
<>
<h2>{t('chooseSet')}</h2>
<PopoutWrapper tip>
<h5>{t('patternForWhichSet')}</h5>
<p>{t('fsmtm')}</p>
</PopoutWrapper>
<h3>{t('yourSets')}</h3>
{Object.keys(sets).length > 0 ? (
<>
<div className="flex flex-row flex-wrap gap-2">
@ -123,9 +175,25 @@ export const UserSetPicker = ({ design }) => {
)
}
export const SetPicker = ({ design }) => (
export const BookmarkedSetPicker = ({ design, t }) => (
<>
<UserSetPicker design={design} />
<CuratedSetPicker design={design} />
<h3>{t('bookmarkedSets')}</h3>
<PopoutWrapper fixme>Implement bookmarked set picker (also implement bookmarks)</PopoutWrapper>
</>
)
export const SetPicker = ({ design }) => {
const { t, i18n } = useTranslation('sets')
const { language } = i18n
const pickerProps = { design, t, language }
return (
<>
<h2>{t('chooseSet')}</h2>
<UserSetPicker {...pickerProps} />
<BookmarkedSetPicker {...pickerProps} />
<CuratedSetPicker {...pickerProps} />
</>
)
}

View file

@ -3,8 +3,8 @@ chooseSet: Please choose a set of measurements
fsmtm: FreeSewing generates made-to-measure sewing patterns.
patternForWhichSet: Which set of measurements should we generate a pattern for?
yourSets: Your measurements sets
starredSets: Measurements sets you've starred
ourSets: Some popular public measurements sets
curatedSets: Curated Measurements Sets
bookmarkedSets: Measurements sets you've bookmarked
curatedSets: FreeSewing's Curated Measurements Sets
curatedSetsAbout: Sets of measurements curated by FreeSewing that you can use to test our platform, or your designs.
curateCuratedSets: Curate our selection of Curated Measurements Sets
useThisSet: Use this set of measurements

View file

@ -1,17 +1,16 @@
import { SvgWrapper } from './svg.mjs'
import { DraftError } from './error.mjs'
export const LabDraft = (props) => {
const { app, draft, gist, updateGist, unsetGist, showInfo, feedback, hasRequiredMeasurements } =
props
export const DraftView = ({ pattern, setView, gist, updateGist }) => {
//const { app, draft, gist, updateGist, unsetGist, showInfo, feedback, hasRequiredMeasurements } = props
if (!draft || !hasRequiredMeasurements) return null
if (!pattern) return null
// Render as SVG
if (gist?.renderer === 'svg') {
let svg
try {
svg = draft.render()
svg = pattern.render()
} catch (error) {
console.log('Failed to render design', error)
return <DraftError error={error} {...props} />
@ -22,7 +21,7 @@ export const LabDraft = (props) => {
// Render as React
let patternProps = {}
try {
patternProps = draft.getRenderProps()
patternProps = pattern.getRenderProps()
} catch (error) {
console.log('Failed to get render props for design', error)
return (
@ -41,23 +40,23 @@ export const LabDraft = (props) => {
errors.push(...set.error)
}
console.log(patternProps)
return (
<>
{errors.length > 0 ? (
<DraftError
{...{
draft,
pattern,
patternProps,
updateGist,
patternLogs: draft.store.logs,
setLogs: draft.setStores[0].logs,
patternLogs: pattern.store.logs,
setLogs: pattern.setStores[0].logs,
errors,
}}
/>
) : null}
<SvgWrapper
{...{ draft, patternProps, gist, updateGist, unsetGist, showInfo, app, feedback }}
/>
<SvgWrapper {...{ pattern, patternProps, gist, updateGist }} />
</>
)
}

View file

@ -0,0 +1,195 @@
// Hooks
import { useState, useEffect, 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 {
BeakerIcon,
BriefcaseIcon,
ClearIcon,
CodeIcon,
CutIcon,
HelpIcon,
MenuIcon,
OptionsIcon,
PrintIcon,
SettingsIcon,
UploadIcon,
WrenchIcon,
} from 'shared/components/icons.mjs'
import { Ribbon } from 'shared/components/ribbon.mjs'
import Link from 'next/link'
import { ModalMenu } from 'site/components/navigation/modal-menu.mjs'
export const ns = ['workbench', 'sections']
export const NavButton = ({
href,
label,
color,
children,
onClick = false,
extraClasses = '',
active = false,
}) => {
const className =
'border-0 px-1 lg:px-4 text-base py-3 lg:py-4 text-center flex flex-col items-center 2xl:w-36 ' +
`hover:bg-${color}-400 text-${color}-400 hover:text-neutral grow lg:grow-0 relative ${extraClasses} ${
active ? 'font-heavy' : ''
}`
const span = <span className="block font-bold hidden 2xl:block">{label}</span>
return onClick ? (
<button {...{ onClick, className }} title={label}>
{children}
{span}
</button>
) : (
<Link {...{ href, className }} title={label}>
{children}
{span}
</Link>
)
}
export const NavSpacer = () => (
<div className="hidden lg:block text-base lg:text-4xl font-thin opacity-30 px-0.5 lg:px-2">|</div>
)
export const colors = [
'red',
'orange',
'yellow',
'lime',
'green',
'cyan',
'blue',
'indigo',
'violet',
'purple',
]
const views = ['menu', 'draft', 'test', 'print', 'cut', 'save', 'export', 'edit', 'clear', 'help']
const NavIcons = ({ setModal, setView, view }) => {
const { t } = useTranslation(['header'])
const iconSize = 'h-6 w-6 lg:h-12 lg:w-12'
return (
<>
<NavButton
onClick={() => setModal(<ModalMenu />)}
label={t('workbench:menu')}
color={colors[0]}
>
<MenuIcon className={iconSize} />
</NavButton>
<NavSpacer />
<NavButton
onClick={() => setView('draft')}
label={t('workbench:draft')}
color={colors[1]}
active={view === 'draft'}
>
<OptionsIcon className={iconSize} />
</NavButton>
<NavButton
onClick={() => setView('test')}
label={t('workbench:test')}
color={colors[2]}
extraClasses="hidden lg:flex"
>
<BeakerIcon className={iconSize} />
</NavButton>
<NavButton
onClick={() => setView('print')}
label={t('workbench:printLayout')}
color={colors[3]}
extraClasses="hidden lg:flex"
>
<PrintIcon className={iconSize} />
</NavButton>
<NavButton
onClick={() => setView('cut')}
label={t('workbench:cutLayout')}
color={colors[4]}
extraClasses="hidden lg:flex"
>
<CutIcon className={iconSize} />
</NavButton>
<NavSpacer />
<NavButton
onClick={() => setView('save')}
label={t('workbench:save')}
color={colors[5]}
extraClasses="hidden lg:flex"
>
<UploadIcon className={iconSize} />
</NavButton>
<NavButton
onClick={() => setView('export')}
label={t('workbench:export')}
color={colors[6]}
extraClasses="hidden lg:flex"
>
<BriefcaseIcon className={iconSize} />
</NavButton>
<NavButton
onClick={() => setView('edit')}
label={t('workbench:edit')}
color={colors[7]}
extraClasses="hidden lg:flex"
>
<CodeIcon className={iconSize} />
</NavButton>
<NavButton
onClick={() => setView('clear')}
label={t('workbench:clear')}
color={colors[8]}
extraClasses="hidden lg:flex"
>
<ClearIcon className={iconSize} />
</NavButton>
<NavSpacer />
<NavButton href="/account" label={t('workbench:help')} color={colors[9]}>
<HelpIcon className={iconSize} />
</NavButton>
</>
)
}
export const WorkbenchHeader = ({ view, setView }) => {
const { setModal } = useContext(ModalContext)
const { loading } = useContext(LoadingContext)
const [show, setShow] = useState(true)
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
`}
>
<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">
<NavIcons setModal={setModal} setView={setView} view={view} />
</div>
{/* Mobile content */}
<div className="flex lg:hidden flex-row items-center justify-between w-full">
<NavIcons setModal={setModal} setView={setView} />
</div>
</div>
</div>
<Ribbon />
</header>
)
}

View file

@ -0,0 +1,292 @@
// Hooks
import { useEffect, useState, useMemo } from 'react'
import { useGist } from 'shared/hooks/useGist'
import { useTranslation } from 'next-i18next'
import { useView } from 'shared/hooks/use-view.mjs'
import { useAccount } from 'shared/hooks/use-account.mjs'
import { useBackend } from 'shared/hooks/use-backend.mjs'
// Dependencies
import { pluginTheme } from '@freesewing/plugin-theme'
import { pluginI18n } from '@freesewing/plugin-i18n'
import { preloaders } from 'shared/components/workbench/preloaders.mjs'
import _set from 'lodash.set'
// Components
import { WorkbenchMenu } from 'shared/components/workbench/menu/index.mjs'
import { DraftError } from 'shared/components/workbench/draft/error.mjs'
import { Modal } from 'shared/components/modal/modal.mjs'
import { ErrorBoundary } from 'shared/components/error/error-boundary.mjs'
// Views
import { WorkbenchMeasurements } from 'shared/components/workbench/measurements/index.mjs'
import { LabSample } from 'shared/components/workbench/sample.mjs'
import { ExportDraft } from 'shared/components/workbench/exporting/index.mjs'
import { GistAsJson, GistAsYaml } from 'shared/components/workbench/gist.mjs'
import { DraftLogs } from 'shared/components/workbench/logs.mjs'
import { CutLayout } from 'shared/components/workbench/layout/cut/index.mjs'
import { PrintLayout } from 'shared/components/workbench/layout/print/index.mjs'
import { EditYaml } from 'shared/components/workbench/edit/index.mjs'
// Components
import { WorkbenchHeader } from './header.mjs'
import { ErrorView } from 'shared/components/error/view.mjs'
// Views
import { DraftView } from 'shared/components/workbench/draft/index.mjs'
export const ns = ['workbench']
const loadDefaultSettings = ({ locale = 'en', units = 'metric' }) => ({
settings: {
sa: 0,
scale: 1,
complete: true,
paperless: false,
margin: 2,
units,
locale,
embed: true,
},
renderer: 'react',
//saBool: false,
//saMm: 10,
//debug: true,
})
const draftViews = ['draft', 'test']
export const Workbench = ({ design, Design, set = false }) => {
// Hooks
const { t, i18n } = useTranslation(ns)
const { language } = i18n
const { account, token } = useAccount()
const { backend } = useBackend(token)
const defaults = loadDefaultSettings({
units: account.imperial ? 'imperial' : 'metric',
locale: language,
})
if (set) defaults.settings.measurements = set.measies
// State
const [view, setView] = useView()
const [gist, setGist] = useState({ ...defaults, embed: true, renderer: 'react' })
const [error, setError] = useState(false)
// Effects
useEffect(() => {
if (set.measies) updateGist('settings.measurements', set.measies)
}, [set])
// Don't bother without a set or Design
if (!set || !Design) return null
// Short-circuit errors early
if (error)
return (
<>
<WorkbenchHeader setView={setView} />
{error}
</>
)
// Helper method to update the gist
const updateGist = (path, val) => {
const newGist = { ...gist }
_set(newGist, path, val)
setGist(newGist)
}
// Generate the pattern here so we can pass it down to both the view and the options menu
const pattern = draftViews.includes(view) ? new Design(gist.settings) : false
if (pattern) {
// add theme to svg renderer
if (gist.renderer === 'svg') {
pattern.use(pluginI18n, { t })
pattern.use(pluginTheme, { skipGrid: ['pages'] })
}
// draft it for draft and event views. Other views may add plugins, etc and we don't want to draft twice
try {
pattern.draft()
} catch (error) {
console.log(error)
setError(<ErrorView>{JSON.stringify(error)}</ErrorView>)
}
}
return (
<>
<WorkbenchHeader setView={setView} view={view} />
{view === 'draft' && <DraftView {...{ pattern, setView, gist, updateGist }} />}
<p>view is {view}</p>
<button onClick={() => setView('alt')}>alt</button>
<button onClick={() => setView('draft')}>draft</button>
</>
)
}
const views = {
measurements: WorkbenchMeasurements,
//draft: LabDraft,
test: LabSample,
printingLayout: PrintLayout,
cuttingLayout: CutLayout,
export: ExportDraft,
logs: DraftLogs,
yaml: GistAsYaml,
json: GistAsJson,
edit: EditYaml,
welcome: () => <p>TODO</p>,
}
const hasRequiredMeasurementsMethod = (design, gist) => {
if (design.patternConfig?.measurements?.length > 0 && !gist.measurements) return false
for (const m of design.patternConfig?.measurements || []) {
if (!gist.measurements[m]) return false
}
return true
}
const doPreload = async (preload, from, design, gist, setGist, setPreloaded) => {
const g = await preloaders[from](preload, design)
setPreloaded(preload)
setGist({ ...gist, ...g.settings })
}
/*
* This component wraps the workbench and is in charge of
* keeping the gist state, which will trickle down
* to all workbench subcomponents
*/
export const WorkbenchWrapper = ({
app,
design,
preload = false,
from = false,
layout = false,
}) => {
// State for gist
const { gist, setGist, unsetGist, updateGist, gistReady, undoGist, resetGist } = useGist(
design.designConfig?.data?.name,
app.locale
)
const [messages, setMessages] = useState([])
const [popup, setPopup] = useState(false)
const [preloaded, setPreloaded] = useState(false)
// we'll only use this if the renderer is svg, but we can't call hooks conditionally
const { t } = useTranslation(['plugin'])
// We'll use this in more than one location
const hasRequiredMeasurements = hasRequiredMeasurementsMethod(design, gist)
// If we don't have the required measurements,
// force view to measurements
useEffect(() => {
if (!gistReady) return
if (!['measurements', 'edit'].includes(gist._state?.view) && !hasRequiredMeasurements)
updateGist(['_state', 'view'], 'measurements')
}, [gistReady, gist._state?.view, hasRequiredMeasurements, updateGist])
// If we need to preload the gist, do so
useEffect(() => {
if (preload && preload !== preloaded && from && preloaders[from]) {
doPreload(preload, from, design, gist, setGist, setPreloaded)
}
}, [preload, preloaded, from, design, gist, setGist])
// Helper methods to manage the gist state
const updateWBGist = useMemo(
() =>
(path, value, closeNav = false, addToHistory = true) => {
updateGist(path, value, addToHistory)
// Force close of menu on mobile if it is open
if (closeNav && app.primaryMenu) app.setPrimaryMenu(false)
},
[app, updateGist]
)
// Helper methods to handle messages
const feedback = {
add: (msg) => {
const newMsgs = [...messages]
if (Array.isArray(msg)) newMsgs.push(...msg)
else newMsgs.push(msg)
setMessages(newMsgs)
},
set: setMessages,
clear: () => setMessages([]),
}
// don't do anything until the gist is ready
if (!gistReady) {
return null
}
// Generate the draft here so we can pass it down to both the view and the options menu
let draft = false
if (['draft', 'logs', 'test', 'printingLayout'].indexOf(gist._state?.view) !== -1) {
gist.embed = true
// get the appropriate layout for the view
const layout = gist.layouts?.[gist._state.view] || gist.layout || true
// hand it separately to the design
draft = new design({ ...gist, layout })
//draft.__init()
// add theme to svg renderer
if (gist.renderer === 'svg') {
draft.use(pluginI18n, { t })
draft.use(pluginTheme, { skipGrid: ['pages'] })
}
// draft it for draft and event views. Other views may add plugins, etc and we don't want to draft twice
try {
if (['draft', 'logs'].indexOf(gist._state.view) > -1) draft.draft()
} catch (error) {
return <DraftError error={error} app={app} draft={draft} at={'draft'} />
}
}
// Props to pass down
const componentProps = {
app,
design,
gist,
updateGist: updateWBGist,
unsetGist,
setGist,
feedback,
gistReady,
showInfo: setPopup,
hasRequiredMeasurements,
draft,
}
// Required props for layout
const layoutProps = {
app: app,
noSearch: true,
workbench: true,
AltMenu: <WorkbenchMenu {...componentProps} />,
showInfo: setPopup,
}
const errorProps = {
undoGist,
resetGist,
gist,
}
// Layout to use
const LayoutComponent = layout
const Component = views[gist._state?.view] ? views[gist._state.view] : views.welcome
return (
<LayoutComponent {...layoutProps}>
{messages}
<ErrorBoundary {...errorProps}>
<Component {...componentProps} draft={draft} />
{popup && <Modal cancel={() => setPopup(false)}>{popup}</Modal>}
</ErrorBoundary>
</LayoutComponent>
)
}

View file

@ -0,0 +1,11 @@
menu: Menu
draft: Draft
test: Test
printLayout: Print Layout
cutLayout: Cut Layout
save: Save
export: Export
edit: Edit
clear: Clear
help: Help

View file

@ -19,7 +19,14 @@ export const PageWrapper = (props) => {
/*
* Deconstruct props
*/
const { layout = DocsLayout, footer = true, children = [], path = [], locale = 'en' } = props
const {
layout = DocsLayout,
footer = true,
header = false,
children = [],
path = [],
locale = 'en',
} = props
// Title is typically set in props.t but check props.title too
const pageTitle = props.t ? props.t : props.title ? props.title : null
@ -37,7 +44,6 @@ export const PageWrapper = (props) => {
*/
const { modalContent } = useContext(ModalContext)
const { title, setNavigation } = useContext(NavigationContext)
console.log({ title })
/*
* Update navigation context with title and path
@ -66,7 +72,7 @@ export const PageWrapper = (props) => {
const [search, setSearch] = useState(false)
// Helper object to pass props down (keeps things DRY)
const childProps = { footer, pageTitle }
const childProps = { footer, header, pageTitle }
// Make layout prop into a (uppercase) component
const Layout = layout

View file

@ -12,7 +12,6 @@ export const NavigationContext = React.createContext(defaultNavigationContext)
export const NavigationContextProvider = ({ children }) => {
function setNavigation(newValues) {
console.log('setting title to', newValues.title)
setValue({
...value,
...newValues,

View file

@ -0,0 +1,8 @@
import { useAtom } from 'jotai'
import { atomWithHash } from 'jotai-location'
const viewAtom = atomWithHash('view', 'draft')
export const useView = () => {
return useAtom(viewAtom)
}

View file

@ -28,6 +28,8 @@
"file-saver": "2.0.5",
"front-matter": "4.0.2",
"highlight.js": "11.7.0",
"jotai": "2.1.0",
"jotai-location": "0.5.1",
"lodash.clonedeep": "4.5.0",
"lodash.orderby": "4.6.0",
"lodash.unset": "4.5.2",

View file

@ -92,7 +92,8 @@ export const optionType = (option) => {
return 'constant'
}
export const capitalize = (string) => string.charAt(0).toUpperCase() + string.slice(1)
export const capitalize = (string) =>
typeof string === 'string' ? string.charAt(0).toUpperCase() + string.slice(1) : ''
export const strapiImage = (
img,

View file

@ -11469,6 +11469,16 @@ jiti@^1.17.2:
resolved "https://registry.yarnpkg.com/jiti/-/jiti-1.18.2.tgz#80c3ef3d486ebf2450d9335122b32d121f2a83cd"
integrity sha512-QAdOptna2NYiSSpv0O/BwoHBSmz4YhpzJHyi+fnMRTXFjp7B8i/YG5Z8IfusxB1ufjcD2Sre1F3R+nX3fvy7gg==
jotai-location@0.5.1:
version "0.5.1"
resolved "https://registry.yarnpkg.com/jotai-location/-/jotai-location-0.5.1.tgz#1a08b683cd7823ce57f7fef8b98335f1ce5c7105"
integrity sha512-6b34X6PpUaXmHCcyxdMFUHgRLUEp+SFHq9UxHbg5HxHC1LddVyVZbPJI+P15+SOQJcUTH3KrsIeKmeLko+Vw/A==
jotai@2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/jotai/-/jotai-2.1.0.tgz#b1a9525345518453802e4a64d99e2800598bab76"
integrity sha512-fR82PtHAmEQrc/daMEYGc4EteW96/b6wodtDSCzLvoJA/6y4YG70er4hh2f8CYwYjqwQ0eZUModGfG4DmwkTyQ==
js-base64@^2.1.9:
version "2.6.4"
resolved "https://registry.yarnpkg.com/js-base64/-/js-base64-2.6.4.tgz#f4e686c5de1ea1f867dbcad3d46d969428df98c4"