feat(org); Saving of patterns and patterns views
This commit is contained in:
parent
b0367ab3f8
commit
cbe56c3647
7 changed files with 249 additions and 49 deletions
51
sites/org/pages/edit/[design]/[id].mjs
Normal file
51
sites/org/pages/edit/[design]/[id].mjs
Normal file
|
@ -0,0 +1,51 @@
|
|||
// 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 EditDesignPage = ({ page, design, id }) => {
|
||||
const Design = useDesign(design)
|
||||
|
||||
return (
|
||||
<PageWrapper {...page} title={design} layout={WorkbenchLayout} header={null}>
|
||||
<Workbench {...{ design, Design, DynamicDocs }} saveAs={{ pattern: id }} />
|
||||
</PageWrapper>
|
||||
)
|
||||
}
|
||||
|
||||
export default EditDesignPage
|
||||
|
||||
export async function getStaticProps({ locale, params }) {
|
||||
return {
|
||||
props: {
|
||||
...(await serverSideTranslations(locale, [params.design, ...namespaces])),
|
||||
design: params.design,
|
||||
id: params.id,
|
||||
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: [],
|
||||
fallback: 'blocking',
|
||||
}
|
||||
}
|
|
@ -17,7 +17,7 @@ const ns = nsMerge(patternsNs, authNs, pageNs, 'status')
|
|||
* So for these, we run a dynamic import and disable SSR rendering
|
||||
*/
|
||||
const DynamicPattern = dynamic(
|
||||
() => import('shared/components/account/patterns.mjs').then((mod) => mod.Pattern),
|
||||
() => import('shared/components/account/patterns.mjs').then((mod) => mod.ShowPattern),
|
||||
{ ssr: false }
|
||||
)
|
||||
|
||||
|
@ -32,7 +32,7 @@ const PatternPage = ({ page, id }) => {
|
|||
|
||||
return (
|
||||
<PageWrapper {...page} title={`${t('patterns')}: #${id}`}>
|
||||
<DynamicPattern id={id} publicOnly />
|
||||
<DynamicPattern id={id} />
|
||||
</PageWrapper>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -307,3 +307,7 @@ examples: Examples
|
|||
noExamples: We currently do not have any examples for this design
|
||||
noExamplesMsg: We rely on the FreeSewing community to submit examples in our showcase posts.
|
||||
|
||||
ownPublicPattern: This is the public view on one of your own patterns. For more options, access the private view.
|
||||
ownPrivatePattern: This is the private view on your pattern. The public view will work for you even when the pattern is private. It will only work for others when the pattern is public.
|
||||
privateView: Private view
|
||||
publicView: Public view
|
||||
|
|
|
@ -1,7 +1,13 @@
|
|||
// Dependencies
|
||||
import { useState, useEffect, useContext } from 'react'
|
||||
import { useTranslation } from 'next-i18next'
|
||||
import { capitalize, shortDate, cloudflareImageUrl, horFlexClasses } from 'shared/utils.mjs'
|
||||
import {
|
||||
capitalize,
|
||||
shortDate,
|
||||
cloudflareImageUrl,
|
||||
horFlexClasses,
|
||||
patternUrl,
|
||||
} from 'shared/utils.mjs'
|
||||
import { freeSewingConfig as conf, controlLevels } from 'shared/config/freesewing.config.mjs'
|
||||
// Context
|
||||
import { LoadingStatusContext } from 'shared/context/loading-status-context.mjs'
|
||||
|
@ -14,6 +20,7 @@ import { ModalContext } from 'shared/context/modal-context.mjs'
|
|||
// Components
|
||||
import { PageLink, Link, AnchorLink } from 'shared/components/link.mjs'
|
||||
import { BackToAccountButton } from './shared.mjs'
|
||||
import { Popout } from 'shared/components/popout/index.mjs'
|
||||
import {
|
||||
StringInput,
|
||||
MarkdownInput,
|
||||
|
@ -33,6 +40,8 @@ import {
|
|||
CloneIcon,
|
||||
BoolYesIcon,
|
||||
BoolNoIcon,
|
||||
LockIcon,
|
||||
PatternIcon,
|
||||
} from 'shared/components/icons.mjs'
|
||||
import { DisplayRow } from './shared.mjs'
|
||||
import { ModalWrapper } from 'shared/components/wrappers/modal.mjs'
|
||||
|
@ -40,10 +49,112 @@ import Markdown from 'react-markdown'
|
|||
import Timeago from 'react-timeago'
|
||||
import { TableWrapper } from 'shared/components/wrappers/table.mjs'
|
||||
import { DynamicOrgDocs } from 'site/components/dynamic-org-docs.mjs'
|
||||
import { PatternReactPreview } from 'shared/components/pattern/preview.mjs'
|
||||
import { Lightbox } from 'shared/components/lightbox.mjs'
|
||||
|
||||
export const ns = ['account', 'patterns', 'status']
|
||||
|
||||
export const Pattern = ({ id, publicOnly = false }) => {
|
||||
export const ShowPattern = ({ id }) => {
|
||||
// Hooks
|
||||
const { account, control } = useAccount()
|
||||
const { setLoadingStatus } = useContext(LoadingStatusContext)
|
||||
const backend = useBackend()
|
||||
const { t, i18n } = useTranslation(ns)
|
||||
|
||||
// Context
|
||||
const { setModal } = useContext(ModalContext)
|
||||
|
||||
// State
|
||||
const [pattern, setPattern] = useState()
|
||||
const [isOwn, setIsOwn] = useState(false)
|
||||
|
||||
// Effect
|
||||
useEffect(() => {
|
||||
const getPattern = async () => {
|
||||
setLoadingStatus([true, t('backendLoadingStarted')])
|
||||
let result = await backend.getPattern(id)
|
||||
if (result.success) {
|
||||
setPattern(result.data.pattern)
|
||||
setIsOwn(true)
|
||||
setLoadingStatus([true, 'backendLoadingCompleted', true, true])
|
||||
} else {
|
||||
result = await backend.getPublicPattern(id)
|
||||
if (result.success) setPattern({ ...result.data, public: true })
|
||||
else setLoadingStatus([true, 'backendError', true, false])
|
||||
}
|
||||
}
|
||||
if (id) getPattern()
|
||||
}, [id])
|
||||
|
||||
if (!pattern) return <p>loading</p>
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flex flex-row flex-wrap gap-4 max-w-7xl w-full">
|
||||
<div className="max-w-lg grow w-full">
|
||||
<Lightbox buttonClasses="w-full" boxClasses="" modalProps={{ fullWidth: 1 }}>
|
||||
<PatternReactPreview {...pattern} />
|
||||
</Lightbox>
|
||||
</div>
|
||||
<div className="w-full md:max-w-lg">
|
||||
<DisplayRow title={t('name')}>{pattern.name}</DisplayRow>
|
||||
<DisplayRow title="#">{pattern.id}</DisplayRow>
|
||||
<DisplayRow title={t('account:publicView')}>
|
||||
<PageLink href={`/patterns/${pattern.id}`} txt={`/patterns/${pattern.id}`} />
|
||||
</DisplayRow>
|
||||
<DisplayRow title={t('account:privateView')}>
|
||||
<PageLink
|
||||
href={`/account/patterns/${pattern.id}`}
|
||||
txt={`/account/patterns/${pattern.id}`}
|
||||
/>
|
||||
</DisplayRow>
|
||||
<DisplayRow title={t('created')}>
|
||||
<Timeago date={pattern.createdAt} />
|
||||
<span className="px-2 opacity-50">|</span>
|
||||
{shortDate(i18n.language, pattern.createdAt, false)}
|
||||
</DisplayRow>
|
||||
<DisplayRow title={t('updated')}>
|
||||
<Timeago date={pattern.createdAt} />
|
||||
<span className="px-2 opacity-50">|</span>
|
||||
{shortDate(i18n.language, pattern.updatedAt, false)}
|
||||
</DisplayRow>
|
||||
<DisplayRow title={t('public')}>
|
||||
{pattern.public ? <BoolYesIcon /> : <BoolNoIcon />}
|
||||
</DisplayRow>
|
||||
<DisplayRow title={t('img')}>
|
||||
<Lightbox buttonClasses="mask mask-squircle w-36 h-35">
|
||||
<img src={cloudflareImageUrl({ id: pattern.img, variant: 'sq500' })} />
|
||||
</Lightbox>
|
||||
</DisplayRow>
|
||||
<Link
|
||||
href={patternUrl({ design: pattern.design, settings: pattern.settings })}
|
||||
className={`btn btn-primary ${horFlexClasses}`}
|
||||
>
|
||||
<CloneIcon /> {t('clonePattern')}
|
||||
</Link>
|
||||
{isOwn ? (
|
||||
<>
|
||||
<Popout tip noP>
|
||||
<p>{t('account:ownPublicPattern')}</p>
|
||||
<Link
|
||||
href={`/account/patterns/${pattern.id}/`}
|
||||
className={`btn btn-secondary ${horFlexClasses} mt-2`}
|
||||
>
|
||||
<LockIcon /> {t('account:privateView')}
|
||||
</Link>
|
||||
</Popout>
|
||||
</>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
<h2>{t('account:notes')}</h2>
|
||||
{isOwn ? 'is own' : 'is not own'}
|
||||
<Markdown>{pattern.notes}</Markdown>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export const Pattern = ({ id }) => {
|
||||
// Hooks
|
||||
const { account, control } = useAccount()
|
||||
const { setLoadingStatus } = useContext(LoadingStatusContext)
|
||||
|
@ -80,27 +191,8 @@ export const Pattern = ({ id, publicOnly = false }) => {
|
|||
setLoadingStatus([true, 'backendLoadingCompleted', true, true])
|
||||
} else setLoadingStatus([true, 'backendError', true, false])
|
||||
}
|
||||
const getPublicPattern = async () => {
|
||||
setLoadingStatus([true, t('backendLoadingStarted')])
|
||||
const result = await backend.getPublicPattern(id)
|
||||
if (result.success) {
|
||||
setPattern({
|
||||
...result.data,
|
||||
public: true,
|
||||
})
|
||||
setName(result.data.name)
|
||||
setImage(result.data.image)
|
||||
setIsPublic(true)
|
||||
setNotes(result.data.notes)
|
||||
setLoadingStatus([true, 'backendLoadingCompleted', true, true])
|
||||
} else setLoadingStatus([true, 'backendError', true, false])
|
||||
}
|
||||
if (id) {
|
||||
if (publicOnly) getPublicPattern()
|
||||
else getPattern()
|
||||
}
|
||||
console.log(' in useeffect')
|
||||
}, [id, publicOnly])
|
||||
if (id) getPattern()
|
||||
}, [id])
|
||||
|
||||
const save = async () => {
|
||||
setLoadingStatus([true, 'gatheringInfo'])
|
||||
|
@ -108,8 +200,8 @@ export const Pattern = ({ id, publicOnly = false }) => {
|
|||
const data = {}
|
||||
if (name || name !== pattern.name) data.name = name
|
||||
if (image || image !== pattern.image) data.img = image
|
||||
if ([true, false].includes(isPublic) && isPublic !== pattern.public) data.public = isPublic
|
||||
if (notes || notes !== pattern.notes) data.notes = notes
|
||||
if ([true, false].includes(isPublic) && isPublic !== pattern.public) data.public = isPublic
|
||||
setLoadingStatus([true, 'savingPattern'])
|
||||
const result = await backend.updatePattern(pattern.id, data)
|
||||
if (result.success) {
|
||||
|
@ -159,23 +251,7 @@ export const Pattern = ({ id, publicOnly = false }) => {
|
|||
<CameraIcon />
|
||||
{t('showImage')}
|
||||
</button>
|
||||
{pattern.userId === account.id && publicOnly && (
|
||||
<Link
|
||||
href={`/patterns/${pattern.id}/edit`}
|
||||
className={`btn btn-primary btn-outline ${horFlexClasses}`}
|
||||
>
|
||||
<FreeSewingIcon /> {t('updatePattern')}
|
||||
</Link>
|
||||
)}
|
||||
{account.username && (
|
||||
<Link
|
||||
href={`/patterns/${pattern.id}/clone`}
|
||||
className={`btn btn-primary btn-outline ${horFlexClasses}`}
|
||||
>
|
||||
<CloneIcon /> {t('clonePattern')}
|
||||
</Link>
|
||||
)}
|
||||
{!publicOnly && (
|
||||
{pattern.userId === account.id && (
|
||||
<>
|
||||
{edit ? (
|
||||
<>
|
||||
|
@ -194,11 +270,22 @@ export const Pattern = ({ id, publicOnly = false }) => {
|
|||
) : (
|
||||
<>
|
||||
<Link
|
||||
href={`/patterns/${pattern.id}/edit`}
|
||||
href={patternUrl({
|
||||
design: pattern.design,
|
||||
settings: pattern.settings,
|
||||
type: 'edit',
|
||||
id: pattern.id,
|
||||
})}
|
||||
className={`btn btn-primary btn-outline ${horFlexClasses}`}
|
||||
>
|
||||
<FreeSewingIcon /> {t('updatePattern')}
|
||||
</Link>
|
||||
<Link
|
||||
href={patternUrl({ design: pattern.design, settings: pattern.settings })}
|
||||
className={`btn btn-primary btn-outline ${horFlexClasses}`}
|
||||
>
|
||||
<CloneIcon /> {t('clonePattern')}
|
||||
</Link>
|
||||
<button
|
||||
onClick={() => setEdit(true)}
|
||||
className={`btn btn-primary ${horFlexClasses}`}
|
||||
|
@ -254,6 +341,13 @@ export const Pattern = ({ id, publicOnly = false }) => {
|
|||
{control >= controlLevels.patterns.id && (
|
||||
<DisplayRow title={t('id')}>{pattern.id}</DisplayRow>
|
||||
)}
|
||||
<Popout tip noP>
|
||||
<p>{t('account:ownPrivatePattern')}</p>
|
||||
<Link className={`btn btn-secondary ${horFlexClasses}`} href={`/patterns/${pattern.id}`}>
|
||||
<PatternIcon />
|
||||
{t('account:publicView')}
|
||||
</Link>
|
||||
</Popout>
|
||||
</div>
|
||||
)
|
||||
|
||||
|
|
|
@ -1,6 +1,9 @@
|
|||
import { useState } from 'react'
|
||||
import { useContext, useState } from 'react'
|
||||
import { ModalContext } from 'shared/context/modal-context.mjs'
|
||||
import { ModalWrapper } from 'shared/components/wrappers/modal.mjs'
|
||||
|
||||
export const Lightbox = ({ children }) => {
|
||||
export const Lightbox = ({ children, buttonClasses = '', boxClasses = false, modalProps = {} }) => {
|
||||
const { setModal } = useContext(ModalContext)
|
||||
const [box, setBox] = useState(false)
|
||||
|
||||
if (box)
|
||||
|
@ -14,7 +17,7 @@ export const Lightbox = ({ children }) => {
|
|||
onClick={() => setBox(false)}
|
||||
>
|
||||
<div
|
||||
className="m-auto text-neutral-content lightbox"
|
||||
className={`m-auto text-neutral-content lightbox ${className}`}
|
||||
style={{
|
||||
maxHeight: 'calc(100vh - 6rem)',
|
||||
maxWidth: 'calc(100vw - 6rem)',
|
||||
|
@ -26,8 +29,24 @@ export const Lightbox = ({ children }) => {
|
|||
)
|
||||
|
||||
return (
|
||||
<button onClick={() => setBox(!box)} className="hover:cursor-zoom-in">
|
||||
<button
|
||||
onClick={() =>
|
||||
setModal(
|
||||
<ModalWrapper
|
||||
flex="col"
|
||||
justify="top lg:justify-center"
|
||||
slideFrom="right"
|
||||
{...modalProps}
|
||||
>
|
||||
{boxClasses ? <div className={boxClasses}>{children}</div> : children}
|
||||
</ModalWrapper>
|
||||
)
|
||||
}
|
||||
className={buttonClasses}
|
||||
>
|
||||
{children}
|
||||
</button>
|
||||
)
|
||||
}
|
||||
|
||||
//<button onClick={() => setBox(!box)} className={`hover:cursor-zoom-in ${className}`}>
|
||||
|
|
|
@ -1,8 +1,13 @@
|
|||
import { useDesign } from 'shared/hooks/use-design.mjs'
|
||||
import { Popout } from 'shared/components/popout/index.mjs'
|
||||
import { themePlugin } from '@freesewing/plugin-theme'
|
||||
import { PanZoomPattern as ShowPattern } from 'shared/components/workbench/pan-zoom-pattern.mjs'
|
||||
import { Pattern as ReactPattern } from 'pkgs/react-components/src/index.mjs'
|
||||
import { useTranslation } from 'next-i18next'
|
||||
|
||||
export const PatternPreview = ({ design, settings }) => {
|
||||
export const ns = ['account', 'patterns', 'status']
|
||||
|
||||
export const PatternSvgPreview = ({ design, settings }) => {
|
||||
const Pattern = useDesign(design)
|
||||
if (!Pattern) return <Popout warning>not a valid pattern constructor for {design}</Popout>
|
||||
|
||||
|
@ -17,3 +22,25 @@ export const PatternPreview = ({ design, settings }) => {
|
|||
|
||||
return <figure dangerouslySetInnerHTML={{ __html: svg }} />
|
||||
}
|
||||
|
||||
export const PatternReactPreview = ({ design, settings }) => {
|
||||
const { t } = useTranslation([design])
|
||||
const Pattern = useDesign(design)
|
||||
if (!Pattern) return <Popout warning>not a valid pattern constructor for {design}</Popout>
|
||||
|
||||
let draft, renderProps
|
||||
try {
|
||||
draft = new Pattern({ ...settings, embed: true }).draft()
|
||||
renderProps = draft.getRenderProps()
|
||||
} catch (err) {
|
||||
console.log(err)
|
||||
}
|
||||
|
||||
return (
|
||||
<ReactPattern
|
||||
t={t}
|
||||
renderProps={renderProps}
|
||||
className="freesewing pattern p-4 shadow border border-solid text-base-content bg-base-100 w-full"
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -477,3 +477,8 @@ export const patternNsFromPatternConfig = (config) => {
|
|||
|
||||
return [...ns]
|
||||
}
|
||||
|
||||
export const patternUrl = ({ design, settings = {}, view = 'draft', type = 'new', id }) =>
|
||||
`/${type}/${design}/${type === 'edit' ? id + '/' : ''}#settings=${encodeURIComponent(
|
||||
JSON.stringify(settings)
|
||||
)}&view=${encodeURIComponent('"' + view + '"')}`
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue