2025-04-01 16:15:20 +02:00
|
|
|
// Dependencies
|
|
|
|
import { designs as collectionDesigns } from '@freesewing/collection'
|
|
|
|
import { capitalize, hasRequiredMeasurements } from '@freesewing/utils'
|
|
|
|
import { initialEditorState } from './lib/index.mjs'
|
|
|
|
import { mergeConfig } from './config/index.mjs'
|
|
|
|
// Hooks
|
|
|
|
import React, { useState, useEffect } from 'react'
|
|
|
|
import { useEditorState } from './hooks/useEditorState.mjs'
|
|
|
|
// Components
|
|
|
|
import { View, viewLabels } from './components/views/index.mjs'
|
|
|
|
import { Spinner } from '@freesewing/react/components/Spinner'
|
|
|
|
import { AsideViewMenu } from './components/AsideViewMenu.mjs'
|
|
|
|
import { LoadingStatus } from './components/LoadingStatus.mjs'
|
|
|
|
import { ModalContextProvider } from '@freesewing/react/context/Modal'
|
|
|
|
import { LoadingStatusContextProvider } from '@freesewing/react/context/LoadingStatus'
|
2025-05-01 15:34:32 +02:00
|
|
|
import { useAccount } from '../../hooks/useAccount/index.mjs'
|
2025-04-01 16:15:20 +02:00
|
|
|
|
|
|
|
/**
|
|
|
|
* FreeSewing's pattern editor
|
|
|
|
*
|
|
|
|
* Editor is the high-level FreeSewing component
|
2025-05-10 15:47:00 +02:00
|
|
|
* that provides the entire pattern editing environment.
|
2025-04-01 16:15:20 +02:00
|
|
|
* This is a high-level wrapper that figures out what view to load initially,
|
2025-05-10 15:47:00 +02:00
|
|
|
* and handles state for the pattern, including the view.
|
2025-04-01 16:15:20 +02:00
|
|
|
*
|
2025-05-10 15:47:00 +02:00
|
|
|
* @component
|
2025-04-01 16:15:20 +02:00
|
|
|
* @param {object} props - All React props
|
2025-05-10 15:47:00 +02:00
|
|
|
* @param {object} [props.config = {}] - A configuration object for the editor
|
|
|
|
* @param {object} [props.design = false] - A design name to preset the editor to use this design
|
|
|
|
* @param {object} [props.preload = {}] - Any state to preload
|
|
|
|
* @param {function} [props.setTitle = false] - A way to set the page title
|
|
|
|
* @param {object} [props.localDesigns = {}] - A way to add local designs to the editor
|
|
|
|
* @param {function} [props.measurementsHelpProvider = false] - A function that should return to a URL for measurements help
|
2025-04-01 16:15:20 +02:00
|
|
|
*/
|
|
|
|
export const Editor = ({
|
|
|
|
config = {},
|
|
|
|
design = false,
|
|
|
|
preload = {},
|
|
|
|
setTitle = false,
|
|
|
|
localDesigns = {},
|
2025-04-13 08:57:54 +00:00
|
|
|
measurementHelpProvider = false,
|
2025-04-01 16:15:20 +02:00
|
|
|
}) => {
|
|
|
|
/*
|
|
|
|
* Bundle all designs
|
|
|
|
*/
|
|
|
|
const designs = { ...collectionDesigns, ...localDesigns }
|
|
|
|
|
|
|
|
/*
|
|
|
|
* Ephemeral state will not be stored in the state backend
|
|
|
|
* It is used for things like loading state and so on
|
|
|
|
*/
|
|
|
|
const [ephemeralState, setEphemeralState] = useState({})
|
|
|
|
|
|
|
|
/*
|
|
|
|
* Merge custom and default configuration
|
|
|
|
*/
|
|
|
|
const editorConfig = mergeConfig(config)
|
|
|
|
|
|
|
|
/*
|
|
|
|
* The Editor state is kept in a state backend (URL)
|
|
|
|
*/
|
|
|
|
const allState = useEditorState(
|
|
|
|
initialEditorState(preload, config),
|
|
|
|
setEphemeralState,
|
|
|
|
editorConfig
|
|
|
|
)
|
|
|
|
|
|
|
|
const state = allState[0]
|
|
|
|
const update = allState[2]
|
|
|
|
|
|
|
|
/*
|
|
|
|
* If state is not loaded, we return early
|
|
|
|
* However, we cannot return before the useEffect call and we need
|
|
|
|
* the view in the useEffect call which depends on state.
|
|
|
|
* So, if state is not ready, we make sure view is set to false and
|
|
|
|
* then return right after the useEffect code
|
|
|
|
*/
|
|
|
|
const [view, extraProps] = state
|
|
|
|
? viewfinder({ design, designs, preload, state, config: editorConfig })
|
|
|
|
: [false, {}]
|
|
|
|
|
|
|
|
/*
|
|
|
|
* Title is typically kept in state by the parent component
|
|
|
|
* so we should not call it inside the regular render but
|
|
|
|
* in the useEffect hook instead
|
|
|
|
*/
|
|
|
|
useEffect(() => {
|
|
|
|
if (typeof setTitle === 'function' && state.design) {
|
|
|
|
setTitle(`${capitalize(state.design)}${viewLabels[view] ? ' | ' + viewLabels[view].t : ''}`)
|
|
|
|
}
|
|
|
|
}, [setTitle, state.design, view])
|
|
|
|
|
|
|
|
/*
|
|
|
|
* Don't bother before state is initialized
|
|
|
|
*/
|
|
|
|
if (!state) return <Spinner />
|
|
|
|
|
|
|
|
/*
|
|
|
|
* Pass this down to allow disabling features that require measurements
|
|
|
|
*/
|
|
|
|
const { missingMeasurements = [] } = extraProps
|
|
|
|
|
|
|
|
/*
|
|
|
|
* Almost all editor state has a default settings, and when that is selected
|
|
|
|
* we just unset that value in the state. This way, state holds only what is
|
|
|
|
* customized, and it makes it a lot easier to see how a pattern was edited.
|
|
|
|
* The big exception is the 'ui.ux' setting. If it is unset, a bunch of
|
|
|
|
* components will not function properly. We could guard against this by passing
|
|
|
|
* the default to all of these components, but instead, we just check that state
|
|
|
|
* is undefined, and if so pass down the default ux value here.
|
|
|
|
* This way, should more of these exceptions get added over time, we can use
|
|
|
|
* the same centralized solution.
|
|
|
|
* We also ensure that settings is always set to an empty object in case there
|
|
|
|
* are no settings yet, as this avoids having to null-check them everywhere.
|
|
|
|
*/
|
2025-04-12 08:01:16 +00:00
|
|
|
const passDownState = {
|
|
|
|
...state,
|
|
|
|
_: { ...ephemeralState, missingMeasurements },
|
|
|
|
}
|
|
|
|
|
2025-05-01 15:34:32 +02:00
|
|
|
const { account } = useAccount()
|
|
|
|
|
2025-05-01 15:51:23 +02:00
|
|
|
// this should be just account.control, but there was a bug where a non-logged-in user had an
|
|
|
|
// object stored in account.control in localStorage instead of an integer
|
|
|
|
const ux = Number.isInteger(account.control) ? account.control : 3
|
|
|
|
|
2025-05-01 17:36:40 +02:00
|
|
|
/*
|
|
|
|
* Ensure we respect the units in the user's account
|
|
|
|
* But only if not units are set
|
|
|
|
*/
|
|
|
|
if (!state.settings?.units && account.imperial) {
|
|
|
|
if (passDownState.settings) passDownState.settings.units = 'imperial'
|
|
|
|
else passDownState.settings = { units: 'imperial' }
|
|
|
|
}
|
|
|
|
|
2025-04-12 08:01:16 +00:00
|
|
|
if (state.ui?.ux === undefined) {
|
2025-05-01 15:51:23 +02:00
|
|
|
passDownState.ui = { ...(state.ui || {}), ux: ux }
|
2025-04-12 08:01:16 +00:00
|
|
|
}
|
2025-04-01 16:15:20 +02:00
|
|
|
|
|
|
|
return (
|
|
|
|
<div className="flex flex-row items-top">
|
|
|
|
{editorConfig.withAside ? <AsideViewMenu update={update} state={passDownState} /> : null}
|
|
|
|
<div
|
|
|
|
className={
|
|
|
|
state.ui?.kiosk
|
|
|
|
? 'md:z-30 md:w-screen md:h-screen md:fixed md:top-0 md:left-0 md:bg-base-100'
|
|
|
|
: 'grow w-full'
|
|
|
|
}
|
|
|
|
>
|
|
|
|
<LoadingStatus state={passDownState} update={update} />
|
|
|
|
<ModalContextProvider>
|
|
|
|
<LoadingStatusContextProvider>
|
|
|
|
<View
|
|
|
|
{...extraProps}
|
|
|
|
{...{ view, update, designs, config: editorConfig }}
|
|
|
|
state={passDownState}
|
2025-04-13 08:57:54 +00:00
|
|
|
measurementHelpProvider={measurementHelpProvider}
|
2025-04-01 16:15:20 +02:00
|
|
|
/>
|
|
|
|
</LoadingStatusContextProvider>
|
|
|
|
</ModalContextProvider>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
2025-05-10 15:47:00 +02:00
|
|
|
/*
|
2025-04-01 16:15:20 +02:00
|
|
|
* Helper method to figure out what view to load
|
|
|
|
* based on the props passed in, and destructure
|
|
|
|
* the props we need for it.
|
|
|
|
*
|
|
|
|
* @param (object) props - All the props
|
|
|
|
* @param {object} props.design - The (name of the) current design
|
|
|
|
* @param {object} props.designs - An object holding all designs
|
|
|
|
* @param (object) props.state - React state passed down from the wrapper view
|
|
|
|
* @param (object) props.config - The editor config
|
|
|
|
*/
|
|
|
|
const viewfinder = ({ design, designs, state, config }) => {
|
2025-04-28 07:06:35 +02:00
|
|
|
const { settings = {} } = state // Guard against undefined settings
|
2025-04-01 16:15:20 +02:00
|
|
|
/*
|
|
|
|
* Grab Design from props or state and make them extra props
|
|
|
|
*/
|
|
|
|
if (!design && state?.design) design = state.design
|
|
|
|
const Design = designs[design] || false
|
|
|
|
const extraProps = { design, Design }
|
|
|
|
|
|
|
|
/*
|
|
|
|
* If no design is set, return the designs view
|
|
|
|
*/
|
|
|
|
if (!designs[design]) return ['designs', extraProps]
|
|
|
|
|
|
|
|
/*
|
|
|
|
* If we have a design, do we have the measurements?
|
|
|
|
*/
|
|
|
|
const [measurementsOk, missingMeasurements] = hasRequiredMeasurements(
|
|
|
|
designs[design],
|
2025-04-28 07:06:35 +02:00
|
|
|
settings.measurements
|
2025-04-01 16:15:20 +02:00
|
|
|
)
|
|
|
|
if (missingMeasurements) extraProps.missingMeasurements = missingMeasurements
|
|
|
|
|
|
|
|
/*
|
|
|
|
* Allow all views that do not require measurements before
|
|
|
|
* we force the user to the measurements view
|
|
|
|
*/
|
|
|
|
if (state.view && config.measurementsFreeViews.includes(state.view))
|
|
|
|
return [state.view, extraProps]
|
|
|
|
|
|
|
|
/*
|
|
|
|
* Force the measurements view if measurements are missing
|
|
|
|
*/
|
|
|
|
if (!measurementsOk) return ['measurements', extraProps]
|
|
|
|
|
|
|
|
/*
|
|
|
|
* If a view is set, return that
|
|
|
|
*/
|
|
|
|
if (state.view) return [state.view, extraProps]
|
|
|
|
|
|
|
|
/*
|
|
|
|
* If no obvious view was found, return the view picker
|
|
|
|
*/
|
|
|
|
return ['picker', extraProps]
|
|
|
|
}
|