1
0
Fork 0

Merge branch 'joost' into eriese-ui-tweaks

This commit is contained in:
Enoch Riese 2023-06-20 13:56:38 -05:00
commit 1ca98d8fe2
33 changed files with 1467 additions and 523 deletions

View file

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

View file

@ -192,12 +192,12 @@ yuri:
backend:
_:
'@aws-sdk/client-sesv2': '3.352.0'
'@aws-sdk/client-sesv2': '3.354.0'
'@prisma/client': &prisma '4.15.0'
'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
@ -213,7 +213,7 @@ backend:
dev:
'chai': *chai
'chai-http': '4.4.0'
'esbuild': '0.18.2'
'esbuild': '0.18.5'
'mocha': *mocha
'mocha-steps': '1.3.0'
'nodemon': '2.0.22'
@ -228,7 +228,7 @@ dev:
'@next/bundle-analyzer': &next '13.4.6'
'@tailwindcss/typography': &tailwindTypography '0.5.9'
'algoliasearch': '4.17.2'
'daisyui': &daisyui '3.1.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.44.3'
'react-markdown': &reactMarkdown '8.0.7'
'react-swipeable': &reactSwipeable '7.0.1'
'react-timeago': &reactTimeago '7.1.0'
@ -314,6 +314,8 @@ 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
@ -323,6 +325,7 @@ org:
'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 +347,8 @@ org:
sanity:
_:
'@sanity/vision': &sanity '3.12.0'
'easymde': '2.16.0'
'@sanity/vision': &sanity '3.12.1'
'easymde': '2.18.0'
'react': *react
'react-dom': *react
'react-is': *react

View file

@ -28,12 +28,12 @@
},
"peerDependencies": {},
"dependencies": {
"@aws-sdk/client-sesv2": "3.352.0",
"@aws-sdk/client-sesv2": "3.354.0",
"@prisma/client": "4.15.0",
"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",
@ -50,7 +50,7 @@
"devDependencies": {
"chai": "4.3.7",
"chai-http": "4.4.0",
"esbuild": "0.18.2",
"esbuild": "0.18.5",
"mocha": "10.2.0",
"mocha-steps": "1.3.0",
"nodemon": "2.0.22",

View file

@ -36,7 +36,7 @@
"@next/bundle-analyzer": "13.4.6",
"@tailwindcss/typography": "0.5.9",
"algoliasearch": "4.17.2",
"daisyui": "3.1.0",
"daisyui": "3.1.1",
"lodash.get": "4.4.2",
"lodash.orderby": "4.6.0",
"lodash.set": "4.3.2",
@ -46,7 +46,7 @@
"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.44.3",
"react-markdown": "8.0.7",
"react-swipeable": "7.0.1",
"react-timeago": "7.1.0",

View file

@ -38,7 +38,7 @@
"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",

View file

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

View file

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

View file

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

View file

@ -0,0 +1,32 @@
import { createClient } from 'next-sanity'
import { siteConfig } from 'site/site.config.mjs'
let sanityClient
export const sanityLoader = ({ query, language, type, slug, order }) => {
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})`
}
return sanityClient.fetch(query)
}
export const sanityImage = (image, dataset = 'site-content') => {
const [, assetName, origSize, format] = image.asset._ref.split('-')
return `https://cdn.sanity.io/images/${siteConfig.sanity.projectt}/${dataset}/${assetName}-${origSize}.${format}`
}

View file

@ -34,15 +34,18 @@
"@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",
"react-copy-to-clipboard": "5.1.0",
"daisyui": "3.1.0",
"daisyui": "3.1.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-sanity": "^4.3.3",
"react-dropzone": "14.2.3",
"react-hotkeys-hook": "4.4.0",
"react-instantsearch-dom": "6.40.0",

View file

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

View file

@ -1,34 +1,111 @@
// Dependencies
import { serverSideTranslations } from 'next-i18next/serverSideTranslations'
import { sanityLoader, sanityImage } 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 { V3Wip } from 'shared/components/v3-wip.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}`} 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(${post.image})`,
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 = ({ page }) => (
<PageWrapper {...page}>
<div className="max-w-2xl">
<V3Wip />
</div>
</PageWrapper>
)
const BlogIndexPage = ({ page, posts }) => {
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} />
))}
</div>
</PageWrapper>
)
}
export default BlogIndexPage
export async function getStaticProps({ locale }) {
const posts = await sanityLoader({ language: locale, type: 'blog' }).catch((err) =>
console.log(err)
)
return {
props: {
posts: posts.map((post) => ({
slug: post.slug.current,
title: post.title,
date: post.date,
// FIXME get the authors separately
author: post.author,
image: sanityImage(post.image) + '?fit=clip&w=400',
})),
...(await serverSideTranslations(locale, namespaces)),
page: {
locale,
// title: 'Freesewing Blog',
path: ['blog'],
},
},

View file

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

View file

@ -0,0 +1,36 @@
// Dependencies
import { serverSideTranslations } from 'next-i18next/serverSideTranslations'
// Components
import { PageWrapper, ns as pageNs } from 'shared/components/wrappers/page.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([...designNs, ...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 NewSetPage = ({ page }) => (
<PageWrapper {...page}>
<DesignPicker />
</PageWrapper>
)
export default NewSetPage
export async function getStaticProps({ locale }) {
return {
props: {
...(await serverSideTranslations(locale, namespaces)),
page: {
locale,
path: ['new', 'pattern'],
},
},
}
}

View file

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

View file

@ -1,34 +1,97 @@
// Dependencies
import { serverSideTranslations } from 'next-i18next/serverSideTranslations'
import { useTranslation } from 'next-i18next'
import { sanityLoader, sanityImage } from 'site/components/sanity/utils.mjs'
// Components
import { PageWrapper, ns as pageNs } from 'shared/components/wrappers/page.mjs'
import { V3Wip } from 'shared/components/v3-wip.mjs'
import Link from 'next/link'
// Translation namespaces used on this page
const namespaces = [...new Set(['showcase', ...pageNs])]
const namespaces = [...new Set(['common', '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 DesignsPage = ({ page }) => (
<PageWrapper {...page}>
<div className="max-w-2xl">
<V3Wip />
</div>
</PageWrapper>
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>
)
export default DesignsPage
// 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>
// )
// }
// FIXME paginate
const Posts = ({ posts }) => (
<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">
{posts.map((post) => (
<PreviewTile img={post.image} slug={post.slug} title={post.title} key={post.slug} />
))}
</div>
)
const ShowcaseIndexPage = (props) => {
const { t } = useTranslation()
const { posts } = props
// const designKeys = useMemo(() => Object.keys(designs).sort(), [designs])
return (
<PageWrapper title={t('showcase')} {...props.page}>
<Posts posts={posts} />
</PageWrapper>
)
}
export default ShowcaseIndexPage
export async function getStaticProps({ locale }) {
const posts = await sanityLoader({
language: locale,
type: 'showcase',
order: 'date desc',
}).catch((err) => console.log(err))
const designs = {}
const propPosts = []
posts.forEach((post) => {
// for (const design of post.designs) {
// if (typeof designs[design] === 'undefined') designs[design] = []
// designs[design].push(post)
// }
propPosts.push({
slug: post.slug.current,
title: post.title,
date: post.date,
// FIXME get the authors separately
author: post.maker,
image: sanityImage(post.image[0]) + '?fit=clip&w=400',
})
})
return {
props: {
posts: propPosts,
designs,
...(await serverSideTranslations(locale, namespaces)),
page: {
locale,
// title: 'Freesewing Blog',
path: ['showcase'],
},
},

View file

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

View file

@ -20,12 +20,12 @@
},
"peerDependencies": {},
"dependencies": {
"@sanity/vision": "3.12.0",
"easymde": "2.16.0",
"@sanity/vision": "3.12.1",
"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.1",
"styled-components": "5.3.11",
"sanity-plugin-markdown": "4.1.0"
},

View file

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

View file

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

View file

@ -1,3 +1,4 @@
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'
@ -16,26 +17,37 @@ const Title = ({ set, language }) => (
</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}`}
>
export const CuratedSetLacksMeasies = ({ set, design, t, language, href, clickHandler }) => {
const inner = (
<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>
)
)
const wrapProps = {
icon: <NoIcon className="w-10 h-10 text-error" />,
title: <Title {...{ set, language }} />,
}
if (clickHandler) wrapProps.onClick = clickHandler
else if (href) wrapProps.href = href
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}
/>
)
const Component = clickHandler ? ChoiceButton : ChoiceLink
return <Component {...wrapProps}></Component>
}
export const CuratedSetSummary = ({ set, language, href, clickHandler }) => {
const wrapProps = {
icon: <OkIcon className="w-10 h-10 text-success" />,
title: <Title {...{ set, language }} />,
}
if (clickHandler) wrapProps.onClick = clickHandler
else if (href) wrapProps.href = href
const Component = clickHandler ? ChoiceButton : ChoiceLink
return <Component {...wrapProps}></Component>
}
export const CuratedSetCandidate = ({ set, design, requiredMeasies = [], href }) => {
const { t, i18n } = useTranslation(['sets'])

View file

@ -1,3 +1,4 @@
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'
@ -15,31 +16,42 @@ const Title = ({ set }) => (
</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 SetLacksMeasies = ({ set, design, t, href, clickHandler }) => {
const inner = (
<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>
)
)
const wrapProps = {
icon: <NoIcon className="w-10 h-10 text-error" />,
title: <Title set={set} />,
}
if (clickHandler) wrapProps.onClick = () => clickHandler(set)
else if (href) wrapProps.href = href
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>
)
const Component = clickHandler ? ChoiceButton : ChoiceLink
export const SetCandidate = ({ set, design, requiredMeasies = [] }) => {
return <Component {...wrapProps}>{inner}</Component>
}
export const SetSummary = ({ set, design, t, href, clickHandler }) => {
const wrapProps = {
icon: <OkIcon className="w-10 h-10 text-success" />,
title: <Title set={set} />,
}
if (clickHandler) wrapProps.onClick = () => clickHandler(set)
else if (href) wrapProps.href = href
const Component = clickHandler ? ChoiceButton : ChoiceLink
return <Component {...wrapProps}></Component>
}
export const SetCandidate = ({ set, design, requiredMeasies = [], href, clickHandler }) => {
const { t } = useTranslation(['sets'])
const setProps = { set, design, t }
const setProps = { set, design, t, href, clickHandler }
// Quick check for required measurements
if (!set.measies || Object.keys(set.measies).length < requiredMeasies.length)

View file

@ -17,7 +17,7 @@ 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)
@ -109,10 +109,8 @@ export const CuratedSetPicker = ({ design, language }) => {
{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}
requiredMeasies={measurements[design]}
design={design}
{...{ set, design, href, clickHandler }}
/>
</div>
))}
@ -121,7 +119,7 @@ export const CuratedSetPicker = ({ design, language }) => {
)
}
export const UserSetPicker = ({ design, t, language }) => {
export const UserSetPicker = ({ design, t, language, href, clickHandler }) => {
// Hooks
const { token } = useAccount()
const backend = useBackend(token)
@ -160,7 +158,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 +175,26 @@ export const UserSetPicker = ({ design, t, language }) => {
)
}
export const BookmarkedSetPicker = ({ design, t }) => (
export const BookmarkedSetPicker = ({ design, t, href = false, clickHandler = false }) => (
<>
<h3>{t('bookmarkedSets')}</h3>
<PopoutWrapper fixme>Implement bookmarked set picker (also implement bookmarks)</PopoutWrapper>
</>
)
export const SetPicker = ({ design }) => {
export const SetPicker = ({ design, href = false, clickHandler = false }) => {
const { t, i18n } = useTranslation('sets')
const { language } = i18n
const pickerProps = { design, t, language }
const pickerProps = { design, t, language, href, clickHandler }
return (
<>
<h2>{t('chooseSet')}</h2>
<UserSetPicker {...pickerProps} />
<BookmarkedSetPicker {...pickerProps} />
<CuratedSetPicker {...pickerProps} />
</>
)
}
//<BookmarkedSetPicker {...pickerProps} />
//<CuratedSetPicker {...pickerProps} />

View file

@ -0,0 +1,197 @@
// Hooks
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, hasRequiredMeasurements } from 'shared/utils.mjs'
// Components
import { WorkbenchHeader } from './header.mjs'
import { ErrorView } from 'shared/components/error/view.mjs'
import { ModalSpinner } from 'shared/components/modal/spinner.mjs'
// Views
import { DraftView, ns as draftNs } from 'shared/components/workbench/views/draft/index.mjs'
import { SaveView, ns as saveNs } from 'shared/components/workbench/views/save/index.mjs'
import { PrintView, ns as printNs } from 'shared/components/workbench/views/print/index.mjs'
import { CutView, ns as cutNs } from 'shared/components/workbench/views/cut/index.mjs'
import { EditView, ns as editNs } from './views/edit/index.mjs'
import { TestView, ns as testNs } from 'shared/components/workbench/views/test/index.mjs'
import { ExportView, ns as exportNs } from 'shared/components/workbench/views/exporting/index.mjs'
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',
...draftNs,
...saveNs,
...printNs,
...cutNs,
...editNs,
...testNs,
...exportNs,
...logNs,
...inspectNs,
...measiesNs,
]
const defaultUi = {
renderer: 'react',
}
const views = {
draft: DraftView,
print: PrintView,
cut: CutView,
export: ExportView,
edit: EditView,
test: TestView,
logs: LogView,
inspect: InspectView,
measies: MeasiesView,
}
const draftViews = ['draft', 'inspect']
export const Workbench = ({ design, Design, DynamicDocs }) => {
// Hooks
const { t, i18n } = useTranslation(ns)
const { language } = i18n
const { account } = useAccount()
const controlState = useControlState()
// State
const [view, setView] = useView()
const [settings, setSettings] = usePatternSettings()
const [ui, setUi] = useState(defaultUi)
const [error, setError] = useState(false)
const [mounted, setMounted] = useState(false)
const [missingMeasurements, setMissingMeasurements] = useState(false)
useEffect(() => {
// Force the measurements view if we have missing measurements
const [ok, missing] = hasRequiredMeasurements(Design, settings.measurements)
if (ok) setMissingMeasurements(false)
else {
// Guard against loops
if (JSON.stringify(missing) !== JSON.stringify(missingMeasurements))
setMissingMeasurements(missing)
if (view !== 'measies') setView('measies')
}
}, [Design, settings.measurements, missingMeasurements, view])
// Helper methods for settings/ui updates
const update = {
settings: (path, val) => setSettings(objUpdate({ ...settings }, path, val)),
ui: (path, val) => setUi(objUpdate({ ...ui }, path, val)),
toggleSa: () => {
const sa = settings.samm || (account.imperial ? 15.3125 : 10)
if (settings.sabool)
setSettings(
objUpdate({ ...settings }, [
[['sabool'], 0],
[['sa'], 0],
[['samm'], sa],
])
)
else {
const sa = settings.samm || (account.imperial ? 15.3125 : 10)
setSettings(
objUpdate({ ...settings }, [
[['sabool'], 1],
[['sa'], sa],
[['samm'], sa],
])
)
}
},
setControl: controlState.update,
}
// Don't bother without a Design
if (!Design) return <ModalSpinner />
// Short-circuit errors early
if (error)
return (
<>
<WorkbenchHeader {...{ view, setView, update }} />
{error}
</>
)
// Deal with each view
const viewProps = {
account,
design,
view,
setView,
update,
settings,
ui,
language,
DynamicDocs,
Design,
}
let viewContent = null
switch (view) {
// Save view
case 'save':
viewContent = <SaveView {...viewProps} from={from} />
break
case 'export':
viewContent = <ExportView {...viewProps} />
break
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 })
// Return early if the pattern is not initialized yet
if (typeof pattern.getConfig !== 'function') return null
const patternConfig = pattern.getConfig()
if (ui.renderer === 'svg') {
// Add theme to svg renderer
pattern.use(pluginI18n, { t })
pattern.use(pluginTheme, { skipGrid: ['pages'] })
}
if (draftViews.includes(view)) {
// Draft the pattern or die trying
try {
pattern.draft()
const errors = [...pattern.store.logs.error]
for (const store of pattern.setStores) errors.push(...store.logs.error)
if (errors.length > 0) setView('logs')
} catch (error) {
console.log(error)
setError(<ErrorView>{JSON.stringify(error)}</ErrorView>)
}
}
const View = views[view]
viewContent = <View {...{ ...viewProps, pattern, patternConfig }} />
}
}
return (
<div className="flex flex-row">
<div className="grow-no shrink-no">
<WorkbenchHeader {...{ view, setView, update }} />
</div>
<div className="grow">{viewContent}</div>
</div>
)
}

View file

@ -2,23 +2,51 @@ 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 { designMeasurements } from 'shared/utils.mjs'
import { useTranslation } from 'next-i18next'
export const ns = ['wbmeasies', ...authNs, setsNs]
const tabNames = ['editCurrent', 'chooseNew']
export const MeasiesView = ({ design, Design, settings, update }) => {
const tabNames = ['chooseNew', 'editCurrent']
export const MeasiesView = ({ design, Design, settings, update, missingMeasurements, setView }) => {
const { t } = useTranslation(['wbmeasies'])
const tabs = tabNames.map((n) => t(n)).join(',')
const loadMeasurements = (set) => {
update.settings(['measurements'], designMeasurements(Design, set.measies))
setView('draft')
}
return (
<div className="m-6">
<h1 className="max-w-6xl m-auto text-center"> {t('changeMeasies')}</h1>
<h1 className="max-w-6xl m-auto text-center">{t('measurements')}</h1>
{missingMeasurements ? (
<Popout note>
<h5>We lack {missingMeasurements.length} measurements to create this pattern:</h5>
<ul className="list list-inside list-disc ml-4">
{missingMeasurements.map((m) => (
<li key={m}>{m}</li>
))}
</ul>
<p>
<b>
You can either pick a measurements set, or enter them by hand, but we cannot proceed
without these measurements.
</b>
</p>
</Popout>
) : (
<Popout tip>
<h5>We have all required measurements to create this pattern.</h5>
</Popout>
)}
<Tabs tabs={tabs}>
<Tab key="choose">
<SetPicker design={design} clickHandler={loadMeasurements} />
</Tab>
<Tab key="edit">
<MeasiesEditor {...{ Design, settings, update }} />
</Tab>
<Tab key="choose">
<SetPicker design={design} />
</Tab>
</Tabs>
</div>
)

View file

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

View file

@ -4,6 +4,7 @@ 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 { DocsLayout, ns as docsNs } from 'site/components/layouts/docs.mjs'
@ -80,6 +81,11 @@ export const PageWrapper = (props) => {
// Return wrapper
return (
<SwipeWrapper>
{pageTitle && (
<Head>
<meta property="og:title" content={pageTitle} key="title" />
</Head>
)}
<div
data-theme={currentTheme} // This facilitates CSS selectors
key={currentTheme} // This forces the data-theme update

View file

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

View file

@ -24,7 +24,7 @@
"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",

View file

@ -281,3 +281,22 @@ export const scrollTo = (id) => {
// eslint-disable-next-line no-undef
if (document) document.getElementById(id).scrollIntoView()
}
export const designMeasurements = (Design, measies = {}) => {
const measurements = {}
for (const m of Design.patternConfig?.measurements || []) measurements[m] = measies[m]
for (const m of Design.patternConfig?.optionalMeasurements || []) measurements[m] = measies[m]
console.log({ Design, measurements, measies })
return measurements
}
export const hasRequiredMeasurements = (Design, measies = {}) => {
const missing = []
for (const m of Design.patternConfig?.measurements || []) {
if (typeof measies[m] === 'undefined') missing.push(m)
}
return [missing.length === 0, missing]
}

926
yarn.lock

File diff suppressed because it is too large Load diff