1
0
Fork 0

feat(org); Saving of patterns and patterns views

This commit is contained in:
joostdecock 2023-09-26 19:41:20 +02:00
parent b0367ab3f8
commit cbe56c3647
7 changed files with 249 additions and 49 deletions

View 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',
}
}

View file

@ -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>
)
}

View file

@ -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

View file

@ -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>
)

View file

@ -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}`}>

View file

@ -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"
/>
)
}

View file

@ -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 + '"')}`