1
0
Fork 0

wip: Saving works now

This commit is contained in:
joostdecock 2025-02-01 17:09:06 +01:00
parent ce7f7c65e0
commit 77ee973355
19 changed files with 363 additions and 278 deletions

View file

@ -83,6 +83,7 @@ packageJson:
"./components/Design": "./components/Design/index.mjs" "./components/Design": "./components/Design/index.mjs"
"./components/Docusaurus": "./components/Docusaurus/index.mjs" "./components/Docusaurus": "./components/Docusaurus/index.mjs"
"./components/Editor": "./components/Editor/index.mjs" "./components/Editor": "./components/Editor/index.mjs"
"./components/Heading": "./components/Heading/index.mjs"
"./components/Highlight": "./components/Highlight/index.mjs" "./components/Highlight": "./components/Highlight/index.mjs"
"./components/Icon": "./components/Icon/index.mjs" "./components/Icon": "./components/Icon/index.mjs"
"./components/Input": "./components/Input/index.mjs" "./components/Input": "./components/Input/index.mjs"

View file

@ -2,6 +2,7 @@
import { missingMeasurements, flattenFlags } from '../lib/index.mjs' import { missingMeasurements, flattenFlags } from '../lib/index.mjs'
// Hooks // Hooks
import React, { useState } from 'react' import React, { useState } from 'react'
import { useBackend } from '@freesewing/react/hooks/useBackend'
import { useDesignTranslation } from '@freesewing/react/hooks/useDesignTranslation' import { useDesignTranslation } from '@freesewing/react/hooks/useDesignTranslation'
// Components // Components
import { Null } from './Null.mjs' import { Null } from './Null.mjs'
@ -207,11 +208,11 @@ export const HeaderMenuDraftViewFlags = (props) => {
} }
export const HeaderMenuDraftViewIcons = (props) => { export const HeaderMenuDraftViewIcons = (props) => {
const { update } = props const { update, state } = props
const Button = HeaderMenuButton const Button = HeaderMenuButton
const size = 'tw-w-5 tw-h-5' const size = 'tw-w-5 tw-h-5'
const muted = 'tw-text-current tw-opacity-50' const muted = 'tw-text-current tw-opacity-50'
const ux = props.state.ui.ux const ux = state.ui.ux
const levels = { const levels = {
...props.config.uxLevels.core, ...props.config.uxLevels.core,
...props.config.uxLevels.ui, ...props.config.uxLevels.ui,
@ -225,25 +226,20 @@ export const HeaderMenuDraftViewIcons = (props) => {
updateHandler={update.toggleSa} updateHandler={update.toggleSa}
tooltip="Turns Seam Allowance on or off (see Core Settings)" tooltip="Turns Seam Allowance on or off (see Core Settings)"
> >
<SaIcon <SaIcon className={`${size} ${state.settings.sabool ? 'tw-text-secondary' : muted}`} />
className={`${size} ${props.state.settings.sabool ? 'tw-text-secondary' : muted}`}
/>
</Button> </Button>
) : null} ) : null}
{ux >= levels.units ? ( {ux >= levels.units ? (
<Button <Button
lgOnly lgOnly
updateHandler={() => updateHandler={() =>
update.settings( update.settings('units', state.settings.units === 'imperial' ? 'metric' : 'imperial')
'units',
props.state.settings.units === 'imperial' ? 'metric' : 'imperial'
)
} }
tooltip="Switches Units between metric and imperial (see Core Settings)" tooltip="Switches Units between metric and imperial (see Core Settings)"
> >
<UnitsIcon <UnitsIcon
className={`${size} ${ className={`${size} ${
props.state.settings.units === 'imperial' ? 'tw-text-secondary' : muted state.settings.units === 'imperial' ? 'tw-text-secondary' : muted
}`} }`}
/> />
</Button> </Button>
@ -251,33 +247,33 @@ export const HeaderMenuDraftViewIcons = (props) => {
{ux >= levels.paperless ? ( {ux >= levels.paperless ? (
<Button <Button
lgOnly lgOnly
updateHandler={() => update.settings('paperless', props.state.settings.paperless ? 0 : 1)} updateHandler={() => update.settings('paperless', state.settings.paperless ? 0 : 1)}
tooltip="Turns Paperless on or off (see Core Settings)" tooltip="Turns Paperless on or off (see Core Settings)"
> >
<PaperlessIcon <PaperlessIcon
className={`${size} ${props.state.settings.paperless ? 'tw-text-secondary' : muted}`} className={`${size} ${state.settings.paperless ? 'tw-text-secondary' : muted}`}
/> />
</Button> </Button>
) : null} ) : null}
{ux >= levels.complete ? ( {ux >= levels.complete ? (
<Button <Button
lgOnly lgOnly
updateHandler={() => update.settings('complete', props.state.settings.complete ? 0 : 1)} updateHandler={() => update.settings('complete', state.settings.complete ? 0 : 1)}
tooltip="Turns Details on or off (see Core Settings)" tooltip="Turns Details on or off (see Core Settings)"
> >
<DetailIcon <DetailIcon
className={`${size} ${!props.state.settings.complete ? 'tw-text-secondary' : muted}`} className={`${size} ${!state.settings.complete ? 'tw-text-secondary' : muted}`}
/> />
</Button> </Button>
) : null} ) : null}
{ux >= levels.expand ? ( {ux >= levels.expand ? (
<Button <Button
lgOnly lgOnly
updateHandler={() => update.settings('expand', props.state.settings.expand ? 0 : 1)} updateHandler={() => update.settings('expand', state.settings.expand ? 0 : 1)}
tooltip="Turns Expand on or off (see Core Settings)" tooltip="Turns Expand on or off (see Core Settings)"
> >
<ExpandIcon <ExpandIcon
className={`${size} ${props.state.settings.expand ? 'tw-text-secondary' : muted}`} className={`${size} ${state.settings.expand ? 'tw-text-secondary' : muted}`}
/> />
</Button> </Button>
) : null} ) : null}
@ -285,24 +281,22 @@ export const HeaderMenuDraftViewIcons = (props) => {
{ux >= levels.rotate ? ( {ux >= levels.rotate ? (
<Button <Button
lgOnly lgOnly
updateHandler={() => update.ui('rotate', props.state.ui.rotate ? 0 : 1)} updateHandler={() => update.ui('rotate', state.ui.rotate ? 0 : 1)}
tooltip="Turns Rotate Pattern on or off (see UI Preferences)" tooltip="Turns Rotate Pattern on or off (see UI Preferences)"
> >
<RotateIcon <RotateIcon className={`${size} ${state.ui.rotate ? 'tw-text-secondary' : muted}`} />
className={`${size} ${props.state.ui.rotate ? 'tw-text-secondary' : muted}`}
/>
</Button> </Button>
) : null} ) : null}
{ux >= levels.renderer ? ( {ux >= levels.renderer ? (
<Button <Button
lgOnly lgOnly
updateHandler={() => updateHandler={() =>
update.ui('renderer', props.state.ui.renderer === 'react' ? 'svg' : 'react') update.ui('renderer', state.ui.renderer === 'react' ? 'svg' : 'react')
} }
tooltip="Switches the Render Engine between React and SVG (see UI Preferences)" tooltip="Switches the Render Engine between React and SVG (see UI Preferences)"
> >
<RocketIcon <RocketIcon
className={`${size} ${props.state.ui.renderer === 'svg' ? 'tw-text-secondary' : muted}`} className={`${size} ${state.ui.renderer === 'svg' ? 'tw-text-secondary' : muted}`}
/> />
</Button> </Button>
) : null} ) : null}
@ -314,7 +308,7 @@ export const HeaderMenuUndoIcons = (props) => {
const { update, state, Design } = props const { update, state, Design } = props
const Button = HeaderMenuButton const Button = HeaderMenuButton
const size = 'tw-w-5 tw-h-5' const size = 'tw-w-5 tw-h-5'
const undos = props.state._?.undos && props.state._.undos.length > 0 ? props.state._.undos : false const undos = state._?.undos && state._.undos.length > 0 ? state._.undos : false
return ( return (
<div className="tw-flex tw-flex-row tw-flex-wrap tw-items-center tw-justify-center tw-px-0.5 lg:tw-px-1"> <div className="tw-flex tw-flex-row tw-flex-wrap tw-items-center tw-justify-center tw-px-0.5 lg:tw-px-1">
@ -386,18 +380,35 @@ export const HeaderMenuUndoIcons = (props) => {
} }
export const HeaderMenuSaveIcons = (props) => { export const HeaderMenuSaveIcons = (props) => {
const { update } = props const { update, state } = props
const backend = useBackend()
const Button = HeaderMenuButton const Button = HeaderMenuButton
const size = 'tw-w-5 tw-h-5' const size = 'tw-w-5 tw-h-5'
const saveable = props.state._?.undos && props.state._.undos.length > 0 const saveable = state._?.undos && state._.undos.length > 0
/*
* Save or Save As, depending on state.pattern
*/
const savePattern = async () => {
const pid = state.pid || false
if (pid) {
const loadingId = 'savePattern'
update.startLoading(loadingId)
const patternData = {
settings: state.settings,
}
const result = await backend.updatePattern(pid, patternData)
if (result[0] === 200) {
update.stopLoading(loadingId)
update.view('draft')
update.notifySuccess('Pattern saved')
} else update.notifyFailure('oops', loadingId)
} else update.view('save')
}
return ( return (
<div className="tw-flex tw-flex-row tw-flex-wrap tw-items-center tw-justify-center tw-px-2"> <div className="tw-flex tw-flex-row tw-flex-wrap tw-items-center tw-justify-center tw-px-2">
<Button <Button updateHandler={savePattern} tooltip="Save pattern" disabled={saveable ? false : true}>
updateHandler={update.clearPattern}
tooltip="Save pattern"
disabled={saveable ? false : true}
>
<SaveIcon className={`${size} ${saveable ? 'tw-text-success' : ''}`} /> <SaveIcon className={`${size} ${saveable ? 'tw-text-success' : ''}`} />
</Button> </Button>
<Button updateHandler={() => update.view('save')} tooltip="Save pattern as..."> <Button updateHandler={() => update.view('save')} tooltip="Save pattern as...">

View file

@ -1,6 +1,6 @@
import React, { useEffect } from 'react' import React, { useEffect } from 'react'
import { Spinner } from '@freesewing/react/components/Spinner' import { Spinner } from '@freesewing/react/components/Spinner'
import { TipIcon } from '@freesewing/react/components/Icon' import { TipIcon, OkIcon } from '@freesewing/react/components/Icon'
import { Null } from './Null.mjs' import { Null } from './Null.mjs'
const config = { const config = {
@ -14,6 +14,7 @@ const config = {
const icons = { const icons = {
tip: TipIcon, tip: TipIcon,
spinner: Spinner, spinner: Spinner,
success: () => <OkIcon stroke={4} />,
} }
export const LoadingStatus = ({ state, update }) => { export const LoadingStatus = ({ state, update }) => {
@ -34,6 +35,11 @@ export const LoadingStatus = ({ state, update }) => {
if (!state._.loading || Object.keys(state._.loading).length < 1) return null if (!state._.loading || Object.keys(state._.loading).length < 1) return null
const colorClasses = {
info: 'tw-bg-info tw-text-info-content',
primary: 'tw-bg-primary tw-text-primary-content',
}
return ( return (
<div className="tw-fixed tw-bottom-4 md:tw-buttom-28 tw-left-0 tw-w-full tw-z-30 md:tw-px-4 md:tw-mx-auto mb-4"> <div className="tw-fixed tw-bottom-4 md:tw-buttom-28 tw-left-0 tw-w-full tw-z-30 md:tw-px-4 md:tw-mx-auto mb-4">
<div className="tw-flex tw-flex-col tw-gap-2"> <div className="tw-flex tw-flex-col tw-gap-2">
@ -48,16 +54,18 @@ export const LoadingStatus = ({ state, update }) => {
key={id} key={id}
className={`tw-w-full md:tw-max-w-2xl tw-m-auto tw-bg-${ className={`tw-w-full md:tw-max-w-2xl tw-m-auto tw-bg-${
conf.color conf.color
} tw-flex tw-flex-row tw-items-center tw-gap-4 tw-p-4 tw-px-4 ${ } tw-text-${conf.color}-content tw-flex tw-flex-row tw-items-center tw-gap-4 tw-p-4 tw-px-4 ${
conf.fading ? 'tw-opacity-0' : 'tw-opacity-100' conf.fading ? 'tw-opacity-0' : 'tw-opacity-100'
} }
tw-transition-opacity tw-delay-[${config.timeout * 1000 - 400}ms] tw-duration-300 tw-transition-opacity tw-delay-[${config.timeout * 1000 - 400}ms] tw-duration-300
md:tw-rounded-lg tw-shadow tw-text-secondary-content tw-text-lg lg:tw-text-xl tw-font-medium md:tw-bg-opacity-90`} md:tw-rounded-lg tw-shadow tw-text-secondary-content tw-text-lg lg:tw-text-xl tw-font-medium md:tw-bg-opacity-90
${conf.color === 'info' ? 'tw-text-neutral' : ''}
`}
> >
<span className="tw-shrink-0"> <span className={`tw-shrink-0 tw-text-${conf.color}-content`}>
<Icon /> <Icon />
</span> </span>
{conf.msg} <div className={conf.color === 'info' ? 'tw-text-neutral' : ''}>{conf.msg}</div>
</div> </div>
) )
})} })}

View file

@ -1,6 +1,6 @@
// Dependencies // Dependencies
import { t, designMeasurements } from '../../lib/index.mjs' import { t, designMeasurements } from '../../lib/index.mjs'
import { capitalize } from '@freesewing/utils' import { capitalize, horFlexClasses as horFlexClasses } from '@freesewing/utils'
import { measurements as measurementsTranslations } from '@freesewing/i18n' import { measurements as measurementsTranslations } from '@freesewing/i18n'
// Hooks // Hooks
import React, { Fragment, useEffect } from 'react' import React, { Fragment, useEffect } from 'react'
@ -21,7 +21,6 @@ const iconClasses = {
className: 'tw-w-8 tw-h-8 md:tw-w-10 md:tw-h-10 lg:tw-w-12 lg:tw-h-12 tw-shrink-0', className: 'tw-w-8 tw-h-8 md:tw-w-10 md:tw-h-10 lg:tw-w-12 lg:tw-h-12 tw-shrink-0',
stroke: 1.5, stroke: 1.5,
} }
const horFlexClasses = 'tw-flex tw-flex-row tw-items-center tw-justify-between tw-gap-4 tw-w-full'
/** /**
* The measurements view is loaded to update/set measurements * The measurements view is loaded to update/set measurements
@ -72,7 +71,7 @@ export const MeasurementsView = ({ config, Design, missingMeasurements, state, u
items.push( items.push(
[ [
<Fragment key={1}> <Fragment key={1}>
<div className={horFlexClasses}> <div className={`${horFlexClasses} tw-w-full`}>
<h4 id="ownsets">Choose one of your own measurements sets</h4> <h4 id="ownsets">Choose one of your own measurements sets</h4>
<MeasurementsSetIcon {...iconClasses} /> <MeasurementsSetIcon {...iconClasses} />
</div> </div>
@ -92,7 +91,7 @@ export const MeasurementsView = ({ config, Design, missingMeasurements, state, u
], ],
[ [
<Fragment key={1}> <Fragment key={1}>
<div className={horFlexClasses}> <div className={`${horFlexClasses} tw-w-full`}>
<h4 id="bookmarkedsets">Choose one of the measurements sets you have bookmarked</h4> <h4 id="bookmarkedsets">Choose one of the measurements sets you have bookmarked</h4>
<BookmarkIcon {...iconClasses} /> <BookmarkIcon {...iconClasses} />
</div> </div>
@ -111,7 +110,7 @@ export const MeasurementsView = ({ config, Design, missingMeasurements, state, u
], ],
[ [
<Fragment key={1}> <Fragment key={1}>
<div className={horFlexClasses}> <div className={`${horFlexClasses} tw-w-full`}>
<h4 id="curatedsets">Choose one of FreeSewing&apos;s curated measurements sets</h4> <h4 id="curatedsets">Choose one of FreeSewing&apos;s curated measurements sets</h4>
<CuratedMeasurementsSetIcon {...iconClasses} /> <CuratedMeasurementsSetIcon {...iconClasses} />
</div> </div>
@ -127,7 +126,7 @@ export const MeasurementsView = ({ config, Design, missingMeasurements, state, u
// Manual editing is always an option // Manual editing is always an option
items.push([ items.push([
<Fragment key={1}> <Fragment key={1}>
<div className={horFlexClasses}> <div className={`${horFlexClasses} tw-w-full`}>
<h4 id="editmeasurements">Edit Measurements</h4> <h4 id="editmeasurements">Edit Measurements</h4>
<EditIcon {...iconClasses} /> <EditIcon {...iconClasses} />
</div> </div>

View file

@ -0,0 +1,181 @@
// Dependencies
import yaml from 'js-yaml'
import { capitalize, shortDate, notEmpty } from '@freesewing/utils'
import { linkClasses, classesHorFlexNoSm } from '@freesewing/utils'
// Hooks
import React, { useState } from 'react'
import { useBackend } from '@freesewing/react/hooks/useBackend'
// Components
import { RoleBlock } from '@freesewing/react/components/Role'
import { Popout } from '@freesewing/react/components/Popout'
import { StringInput } from '@freesewing/react/components/Input'
import { SaveAsIcon } from '@freesewing/react/components/Icon'
import { H1 } from '@freesewing/react/components/Heading'
import { Link, SuccessLink } from '@freesewing/react/components/Link'
import { HeaderMenu } from '../HeaderMenu.mjs'
/**
* This is the save view, it allows you to save your pattern to a FreeSewing account
*
* @param {Object} props - All the props
* @param {Function} props.config - The editor configuration
* @param {Object} props.state - The editor state object
* @param {Object} props.update - Helper object for updating the editor state
*/
export const SaveView = ({ config, state, update }) => {
// Hooks
const backend = useBackend()
// State
const [name, setName] = useState(`${capitalize(state.design)} / ${shortDate()}`)
const [withNotes, setWithNotes] = useState(false)
const [notes, setNotes] = useState('')
const [savedId, setSavedId] = useState()
const [saveAs] = useState(false) // FIXME
const addSettingsToNotes = () => {
setNotes(notes + '\n\n#### Settings\n\n```yaml\n' + yaml.dump(state.settings) + '\n````')
}
const saveAsNewPattern = async () => {
const loadingId = 'savePatternAs'
update.startLoading(loadingId)
const patternData = {
design: state.design,
name,
public: false,
settings: state.settings,
data: {},
}
if (withNotes) patternData.notes = notes
const result = await backend.createPattern(patternData)
if (result[0] === 201 && result[1].pattern.id) {
const id = result[1].pattern.id
update.stopLoading(loadingId)
update.view('draft')
update.pid(id)
update.notifySuccess(
<span>
{' '}
Pattern saved as:{' '}
<SuccessLink href={`/account/data/patterns/pattern?id=${id}`}> #{id} </SuccessLink>
</span>,
id
)
} else update.notifyFailure('oops', loadingId)
}
const savePattern = async () => {
setLoadingStatus([true, 'Saving pattern...'])
const patternData = {
design: state.design,
settings: state.settings,
name,
public: false,
data: {},
}
const result = await backend.updatePattern(saveAs.pattern, patternData)
if (result.success) {
//setLoadingStatus([
// true,
// <>
// {t('status:patternSaved')} <small>[#{saveAs.pattern}]</small>
// </>,
// true,
// true,
//])
setSavedId(saveAs.pattern)
update.notify({ color: 'success', msg: 'boom' }, saveAs.pattern)
} //else setLoadingStatus([true, 'backendError', true, false])
}
return (
<RoleBlock user>
<HeaderMenu state={state} {...{ config, update }} />
<div className="tw-m-auto tw-mt-8 tw-max-w-2xl tw-px-4">
{saveAs && saveAs.pattern ? (
<>
<h2>Save Pattern</h2>
{savedId && (
<Popout link>
<h5>Pattern Saved</h5>
See: <Link href={`/account/patterns/${savedId}`}>/account/patterns/{savedId}</Link>
</Popout>
)}
<button
className={`${classeshorFlexNoSm} tw-btn tw-btn-primary tw-btn-lg tw-w-full tw-mt-2 tw-my-8`}
onClick={savePattern}
>
<SaveIcon className="tw-h-8 tw-w-8" />
Save Patter #{saveAs.pattern}
</button>
</>
) : null}
<H1>Save As New Pattern</H1>
<div className="tw-mb-4">
<StringInput
label="Pattern title"
current={name}
update={setName}
valid={notEmpty}
labelBR={
<>
{withNotes ? (
<div className="tw-flex tw-flex-row tw-items-center tw-gap-4">
<button
className={`tw-font-bold ${linkClasses}`}
onClick={() => setWithNotes(false)}
>
Hide notes
</button>
<button className={`tw-font-bold ${linkClasses}`} onClick={addSettingsToNotes}>
Add settings to notes
</button>
</div>
) : (
<button
className={`tw-font-bold ${linkClasses}`}
onClick={() => setWithNotes(true)}
>
Add notes
</button>
)}
</>
}
/>
{withNotes ? (
<Swizzled.components.MarkdownInput
label="Pattern notes"
current={notes}
update={setNotes}
/>
) : null}
<div className="tw-flex tw-flex-row tw-gap-2 tw-mt-8">
<button
className={`tw-daisy-btn tw-daisy-btn-primary lg:tw-daisy-btn-lg tw-daisy-btn-outline`}
onClick={update.viewBack}
title="Cancel"
>
<span>Cancel</span>
</button>
<button
className={`tw-flex tw-flex-row tw-items-center tw-justify-between tw-daisy-btn tw-daisy-btn-primary lg:tw-daisy-btn-lg tw-grow`}
onClick={saveAsNewPattern}
title="Save as new pattern"
>
<SaveAsIcon className="tw-w-8 tw-h-8" />
<span>Save as new pattern</span>
</button>
</div>
<p className="tw-text-sm tw-text-right">
To access your saved patterns, go to:
<b>
{' '}
<Link href="/account/patterns">/account/patterns</Link>
</b>
</p>
</div>
</div>
</RoleBlock>
)
}

View file

@ -3,6 +3,7 @@ import { ViewPicker } from './ViewPicker.mjs'
import { DesignsView } from './DesignsView.mjs' import { DesignsView } from './DesignsView.mjs'
import { MeasurementsView } from './MeasurementsView.mjs' import { MeasurementsView } from './MeasurementsView.mjs'
import { DraftView } from './DraftView.mjs' import { DraftView } from './DraftView.mjs'
import { SaveView } from './SaveView.mjs'
import { ErrorIcon } from '@freesewing/react/components/Icon' import { ErrorIcon } from '@freesewing/react/components/Icon'
import { import {
OptionsIcon, OptionsIcon,
@ -51,6 +52,7 @@ export const View = (props) => {
if (view === 'designs') return <DesignsView {...props} /> if (view === 'designs') return <DesignsView {...props} />
if (view === 'measurements') return <MeasurementsView {...props} /> if (view === 'measurements') return <MeasurementsView {...props} />
if (view === 'draft') return <DraftView {...props} /> if (view === 'draft') return <DraftView {...props} />
if (view === 'save') return <SaveView {...props} />
/* /*
viewComponents: { viewComponents: {
draft: 'DraftView', draft: 'DraftView',

View file

@ -122,11 +122,6 @@ export const defaultConfig = {
ux: 4, ux: 4,
}, },
}, },
classes: {
horFlex: 'flex flex-row items-center justify-between gap-4 w-full',
horFlexNoSm: 'md:flex md:flex-row md:items-center md:justify-between md:gap-4 md-w-full',
link: 'underline decoration-2 hover:decoration-4 text-secondary hover:text-secondary-focus',
},
roles: { roles: {
levels: { levels: {
readNone: 0, readNone: 0,

View file

@ -1,7 +1,7 @@
// Dependencies // Dependencies
import React from 'react' import React from 'react'
import { defaultConfig } from '../config/index.mjs' import { defaultConfig } from '../config/index.mjs'
import { round, formatMm } from '@freesewing/utils' import { round, formatMm, randomLoadingMessage, horFlexClasses } from '@freesewing/utils'
import { formatDesignOptionValue, menuCoreSettingsStructure } from './index.mjs' import { formatDesignOptionValue, menuCoreSettingsStructure } from './index.mjs'
import { menuUiPreferencesStructure } from './ui-preferences.mjs' import { menuUiPreferencesStructure } from './ui-preferences.mjs'
// Components // Components
@ -13,6 +13,7 @@ import {
UiIcon, UiIcon,
} from '@freesewing/react/components/Icon' } from '@freesewing/react/components/Icon'
import { HtmlSpan } from '../components/HtmlSpan.mjs' import { HtmlSpan } from '../components/HtmlSpan.mjs'
import { Spinner } from '@freesewing/react/components/Spinner'
/* /*
* This method drafts the pattern * This method drafts the pattern
@ -531,6 +532,8 @@ export function stateUpdateFactory(setState, setEphemeralState, config) {
return eph return eph
}) })
}, },
// Pattern ID (pid)
pid: (val) => setState((cur) => ({ ...cur, pid: val })),
ux: (val) => setState((cur) => objUpdate({ ...cur }, 'ux', val)), ux: (val) => setState((cur) => objUpdate({ ...cur }, 'ux', val)),
clearPattern: () => clearPattern: () =>
setState((cur) => { setState((cur) => {
@ -555,7 +558,8 @@ export function stateUpdateFactory(setState, setEphemeralState, config) {
if (typeof newState.loading !== 'object') newState.loading = {} if (typeof newState.loading !== 'object') newState.loading = {}
if (typeof conf.color === 'undefined') conf.color = 'info' if (typeof conf.color === 'undefined') conf.color = 'info'
newState.loading[id] = { newState.loading[id] = {
msg: t('pe:genericLoadingMsg'), msg: randomLoadingMessage(),
icon: 'spinner',
...conf, ...conf,
} }
return newState return newState

View file

@ -1,214 +0,0 @@
import { useState } from 'react'
import yaml from 'js-yaml'
export const SaveView = ({ Swizzled, state, update }) => {
// Hooks
const backend = Swizzled.hooks.useBackend()
const { t } = Swizzled.methods
// State
const [name, setName] = useState(
`${Swizzled.methods.capitalize(state.design)} / ${Swizzled.methods.shortDate(status.locale)}`
)
const [withNotes, setWithNotes] = useState(false)
const [notes, setNotes] = useState('')
const [savedId, setSavedId] = useState()
const [bookmarkedId] = useState() // FIXME
const [saveAs] = useState(false) // FIXME
const addSettingsToNotes = () => {
setNotes(
notes +
'\n\n#### ' +
Swizzled.methods.t('pe:settings') +
'\n\n' +
'```yaml\n' +
yaml.dump(state.settings) +
'````'
)
}
const saveAsNewPattern = async () => {
const loadingId = 'savePatternAs'
update.startLoading(loadingId)
const patternData = {
design: state.design,
name,
public: false,
settings: state.settings,
data: {},
}
if (withNotes) patternData.notes = notes
const result = await backend.createPattern(patternData)
if (result.success) {
const id = result.data.pattern.id
update.stopLoading(loadingId)
update.view('draft')
update.notifySuccess(
<span>
{t('pe:patternSavedAs')}:{' '}
<Swizzled.components.Link
href={`/account/pattern?id=${id}`}
className={`${Swizzled.config.classes.link} text-secondary-content`}
>
/account/pattern?id={id}
</Swizzled.components.Link>
</span>,
id
)
} else update.notifyFailure('oops', loadingId)
}
const savePattern = async () => {
//setLoadingStatus([true, 'savingPattern'])
const patternData = {
design: state.design,
settings: state.settings,
name,
public: false,
data: {},
}
const result = await backend.updatePattern(saveAs.pattern, patternData)
if (result.success) {
//setLoadingStatus([
// true,
// <>
// {t('status:patternSaved')} <small>[#{saveAs.pattern}]</small>
// </>,
// true,
// true,
//])
setSavedId(saveAs.pattern)
update.notify({ color: 'success', msg: 'boom' }, saveAs.pattern)
} //else setLoadingStatus([true, 'backendError', true, false])
}
//const bookmarkPattern = async () => {
// setLoadingStatus([true, 'creatingBookmark'])
// const result = await backend.createBookmark({
// type: 'pattern',
// title: name,
// url: window.location.pathname + window.location.search + window.location.hash,
// })
// if (result.success) {
// const id = result.data.bookmark.id
// setLoadingStatus([
// true,
// <>
// {t('status:bookmarkCreated')} <small>[#{id}]</small>
// </>,
// true,
// true,
// ])
// setBookmarkedId(id)
// } else setLoadingStatus([true, 'backendError', true, false])
//}
return (
<Swizzled.components.AuthWrapper>
<Swizzled.components.HeaderMenu {...{ state, update }} />
<div className="m-auto mt-8 max-w-2xl px-4">
{saveAs && saveAs.pattern ? (
<>
<h2>{t('pe:savePattern')}</h2>
{savedId && (
<Swizzled.components.Popout link>
<h5>{t('workbend:patternSaved')}</h5>
{t('pe:see')}:{' '}
<Swizzled.components.PageLink
href={`/account/patterns/${savedId}`}
txt={`/account/patterns/${savedId}`}
/>
</Swizzled.components.Popout>
)}
<button
className={`${Swizzled.config.classeshorFlexNoSm} btn btn-primary btn-lg w-full mt-2 my-8`}
onClick={savePattern}
>
<Swizzled.components.SaveIcon className="h-8 w-8" />
{t('pe:savePattern')} #{saveAs.pattern}
</button>
</>
) : null}
<h2>{t('pe:saveAsNewPattern')}</h2>
{bookmarkedId && (
<Swizzled.components.Popout link>
<h5>{t('pe:patternBookmarkCreated')}</h5>
{t('pe:see')}:{' '}
<Swizzled.components.PageLink
href={`/account/bookmarks/${bookmarkedId}`}
txt={`/account/bookmarks/${bookmarkedId}`}
/>
</Swizzled.components.Popout>
)}
<div className="mb-4">
<Swizzled.components.StringInput
label={t('pe:patternTitle')}
current={name}
update={setName}
valid={Swizzled.methods.notEmpty}
labelBR={
<>
{withNotes ? (
<div className="flex flex-row items-center gap-4">
<button
className={`font-bold ${Swizzled.config.classes.link}`}
onClick={() => setWithNotes(false)}
>
{t('pe:hideNotes')}
</button>
<button
className={`font-bold ${Swizzled.config.classes.link}`}
onClick={addSettingsToNotes}
>
{t('pe:addSettingsToNotes')}
</button>
</div>
) : (
<button
className={`font-bold ${Swizzled.config.classes.link}`}
onClick={() => setWithNotes(true)}
>
{t('pe:addNotes')}
</button>
)}
</>
}
/>
{withNotes ? (
<Swizzled.components.MarkdownInput
label={t('pe:patternNotes')}
current={notes}
update={setNotes}
/>
) : null}
<div className="flex flex-row gap-2 mt-8">
<button
className={`btn btn-primary btn-lg btn-outline`}
onClick={update.viewBack}
title={t('pe:cancel')}
>
<span>{t('pe:cancel')}</span>
</button>
<button
className={`flex flex-row items-center justify-between btn btn-primary btn-lg grow`}
onClick={saveAsNewPattern}
title={t('pe:saveAsNewPattern')}
>
<Swizzled.components.SaveAsIcon className="w-8 h-8" />
<span>{t('pe:saveAsNewPattern')}</span>
</button>
</div>
<p className="text-sm text-right">
To access your saved patterns, go to:{' '}
<b>
<Swizzled.components.PageLink href="//account/patterns">
/account/patterns
</Swizzled.components.PageLink>
</b>
</p>
</div>
</div>
</Swizzled.components.AuthWrapper>
)
}

View file

@ -1,4 +0,0 @@
/*
* To spread icon + text horizontal
*/
export const horFlexClasses = 'flex flex-row items-center justify-between gap-4 w-full'

View file

@ -0,0 +1,37 @@
import React from 'react'
export const H1 = ({ children }) => (
<h1 className="tw-text-5xl tw-pt-5 tw-pb-4 tw-font-thin tw-tracking-tighter lg:tw-text-6xl">
{children}
</h1>
)
export const H2 = ({ children }) => (
<h2 className="tw-text-3xl tw-pt-4 tw-pb-3 tw-font-black tw-tracking-tighter tw-m-0 lg:tw-text-4xl">
{children}
</h2>
)
export const H3 = ({ children }) => (
<h3 className="tw-text-2xl tw-pt-3 tw-pb-2 tw-font-extrabold tw-m-0 tw-tracking-tighter lg:tw-text-3xl">
{children}
</h3>
)
export const H4 = ({ children }) => (
<h4 className="tw-text-xl tw-pt-2 tw-pb-1 tw-font-bold tw-m-0 tw-tracking-tighter lg:tw-text-2xl">
{children}
</h4>
)
export const H5 = ({ children }) => (
<h5 className="tw-text-lg tw-py-1 tw-font-semibold tw-m-0 tw-tracking-tight lg:tw-text-xl">
{children}
</h5>
)
export const H6 = ({ children }) => (
<h6 className="tw-text-base tw-py-1 tw-font-medium tw-italic tw-m-0 tw-tracking-tight lg:tw-text-lg">
{children}
</h6>
)

View file

@ -138,7 +138,11 @@ export const NumberInput = ({
value={current} value={current}
onChange={(evt) => update(evt.target.value)} onChange={(evt) => update(evt.target.value)}
className={`tw-daisy-input tw-w-full tw-daisy-input-bordered ${ className={`tw-daisy-input tw-w-full tw-daisy-input-bordered ${
current === original ? 'input-secondary' : valid(current) ? 'input-success' : 'input-error' current === original
? 'tw-daisy-input-secondary'
: valid(current)
? 'tw-daisy-input-success'
: 'tw-daisy-input-error'
}`} }`}
{...{ max, min, step }} {...{ max, min, step }}
/> />
@ -166,12 +170,12 @@ export const StringInput = ({
placeholder={placeholder} placeholder={placeholder}
value={current} value={current}
onChange={(evt) => update(evt.target.value)} onChange={(evt) => update(evt.target.value)}
className={`tw-daisy-input tw-w-full tw-daisy-input-bordered ${ className={`tw-daisy-input tw-w-full tw-daisy-input-bordered tw-text-current ${
current === original current === original
? 'daisy-input-secondary' ? 'tw-daisy-input-secondary'
: valid(current) : valid(current)
? 'daisy-input-success' ? 'tw-daisy-input-success'
: 'daisy-input-error' : 'tw-daisy-input-error'
}`} }`}
/> />
</FormControl> </FormControl>

View file

@ -33,6 +33,28 @@ export const Link = ({ href, title = false, children, className = linkClasses, s
const BaseLink = Link const BaseLink = Link
/**
* A regular link, but on a success background
*
* @param {object} props - All React props
* @param {array} props.children - The content to go in the layout
* @param {array} props.href - The target to link to
* @param {array} props.title - An optional link title
* @param {string} props.className - Any non-default CSS classes to apply
* @param {string} props.style - Any non-default styles to apply
*/
export const SuccessLink = ({
href,
title = false,
children,
className = `${linkClasses} tw-text-success-content hover:tw-text-success-content`,
style = {},
}) => (
<a href={href} className={className} title={title ? title : ''} style={style}>
{children}
</a>
)
export const CardLink = ({ href, title, Icon, children, Link }) => { export const CardLink = ({ href, title, Icon, children, Link }) => {
if (!Link) Link = BaseLink if (!Link) Link = BaseLink

View file

@ -36,6 +36,7 @@
"./components/Design": "./components/Design/index.mjs", "./components/Design": "./components/Design/index.mjs",
"./components/Docusaurus": "./components/Docusaurus/index.mjs", "./components/Docusaurus": "./components/Docusaurus/index.mjs",
"./components/Editor": "./components/Editor/index.mjs", "./components/Editor": "./components/Editor/index.mjs",
"./components/Heading": "./components/Heading/index.mjs",
"./components/Highlight": "./components/Highlight/index.mjs", "./components/Highlight": "./components/Highlight/index.mjs",
"./components/Icon": "./components/Icon/index.mjs", "./components/Icon": "./components/Icon/index.mjs",
"./components/Input": "./components/Input/index.mjs", "./components/Input": "./components/Input/index.mjs",

View file

@ -3,6 +3,7 @@ import { cloudflare as cloudflareConfig } from '@freesewing/config'
import _set from 'lodash/set.js' import _set from 'lodash/set.js'
import _unset from 'lodash/unset.js' import _unset from 'lodash/unset.js'
import _orderBy from 'lodash/orderBy.js' import _orderBy from 'lodash/orderBy.js'
import { loadingMessages } from './loading-messages.mjs'
/* /*
* Re-export lodash utils * Re-export lodash utils
@ -18,13 +19,14 @@ export const orderBy = _orderBy
/* /*
* CSS classes to spread icon + text horizontally on a button * CSS classes to spread icon + text horizontally on a button
*/ */
export const horFlexClasses = 'flex flex-row items-center justify-between gap-4' export const horFlexClasses =
'tw-flex tw-flex-row tw-items-center tw-justify-between tw-gap-4 tw-w-full'
/* /*
* CSS classes to spread icon + text horizontally on a button, only from md upwards * CSS classes to spread icon + text horizontally on a button, only from md upwards
*/ */
export const horFlexClassesNoSm = export const horFlexClassesNoSm =
'md:flex md:flex-row md:items-center md:justify-between md:gap-4 md-w-full' 'md:tw-flex md:tw-flex-row md:tw-items-center md:tw-justify-between md:tw-gap-4 tw-w-full'
/* /*
* These classes are what makes a link a link * These classes are what makes a link a link
@ -415,6 +417,15 @@ export const optionType = (option) => {
return 'constant' return 'constant'
} }
/*
* Returns a random loading message
*
* @return {string} msg - A random loading message
*/
export function randomLoadingMessage() {
return loadingMessages[Math.floor(Math.random() * loadingMessages.length)]
}
/* /*
* Generic rounding method * Generic rounding method
* *

View file

@ -0,0 +1,27 @@
export const loadingMessages = [
'Unfolding ideas...',
'Teaching hamsters to run faster...',
'Convincing pixels to get in line...',
'Warming up the flux capacitor...',
'Untangling digital spaghetti...',
'Counting backwards from infinity...',
'Generating witty loading messages...',
'Brewing digital coffee...',
'Herding cats into quantum boxes...',
'Downloading more RAM...',
'Dividing by zero...',
'Spinning up the hamster wheel...',
'Converting caffeine to code...',
'Bending the space-time continuum...',
'Charging laser batteries...',
'Summoning the data spirits...',
'Searching for the lost semicolon...',
'Consulting the digital rolodex...',
'Calibrating the flux capacitor...',
'Collecting magic internet points...',
'Solving P vs NP...',
'Compressing time and space...',
'Entering the matrix...',
'Upgrading to Web 4.0...',
'Debugging the debugger...',
]

View file

@ -15,8 +15,8 @@ export default function SignInPage() {
title="Sign In" title="Sign In"
description="Sign In to your FreeSewing account to unlock all features" description="Sign In to your FreeSewing account to unlock all features"
> >
<div className="flex flex-col items-center h-screen justify-center text-base-content px-4"> <div className="tw-flex tw-flex-col tw-items-center tw-text-base-content tw-px-4">
<div className="max-w-lg w-full"> <div className="tw-max-w-lg tw-w-full">
<SignIn /> <SignIn />
</div> </div>
</div> </div>

View file

@ -24,7 +24,7 @@ export const theme = {
'neutral-focus': colors.neutral['700'], 'neutral-focus': colors.neutral['700'],
'neutral-content': colors.neutral['50'], 'neutral-content': colors.neutral['50'],
info: colors.indigo['400'], info: colors.yellow['400'],
success: colors.green['400'], success: colors.green['400'],
warning: colors.orange['400'], warning: colors.orange['400'],
error: colors.red['400'], error: colors.red['400'],

View file

@ -79,9 +79,9 @@ export const theme = {
// info: Used rarely, can be another color best somewhat neutral looking // info: Used rarely, can be another color best somewhat neutral looking
// and should work with the default text color // and should work with the default text color
info: colors.indigo['600'], info: colors.yellow['300'],
// Text color on info // Text color on info
'info-content': colors.neutral[50], 'info-content': colors.neutral[900],
// success: Used rarely, but if it is it's in notifications indicating success // success: Used rarely, but if it is it's in notifications indicating success
// Typically some shade of green // Typically some shade of green