diff --git a/config/dependencies.yaml b/config/dependencies.yaml
index ca46b770689..04ee761ae09 100644
--- a/config/dependencies.yaml
+++ b/config/dependencies.yaml
@@ -129,6 +129,7 @@ react:
axios: *axios
highlight.js: "^11.11.0"
html-react-parser: "^5.0.7"
+ luxon: "^3.5.0"
nuqs: "^1.17.6"
react-markdown: "^9.0.1"
use-local-storage-state: "19.1.0"
diff --git a/config/exceptions.yaml b/config/exceptions.yaml
index ac3e782f0be..05e96332f4a 100644
--- a/config/exceptions.yaml
+++ b/config/exceptions.yaml
@@ -84,6 +84,7 @@ packageJson:
"./components/Icon": "./components/Icon/index.mjs"
"./components/Input": "./components/Input/index.mjs"
"./components/Json": "./components/Json/index.mjs"
+ "./components/KeyVal": "./components/KeyVal/index.mjs"
"./components/Layout": "./components/Layout/index.mjs"
"./components/LineDrawing": "./components/LineDrawing/index.mjs"
"./components/Link": "./components/Link/index.mjs"
@@ -96,6 +97,8 @@ packageJson:
"./components/SignIn": "./components/SignIn/index.mjs"
"./components/Spinner": "./components/Spinner/index.mjs"
"./components/Tab": "./components/Tab/index.mjs"
+ "./components/Table": "./components/Table/index.mjs"
+ "./components/Time": "./components/Time/index.mjs"
"./components/Yaml": "./components/Yaml/index.mjs"
"./components/Xray": "./components/Xray/index.mjs"
# Context
@@ -104,6 +107,7 @@ packageJson:
# Hooks
"./hooks/useAccount": "./hooks/useAccount/index.mjs"
"./hooks/useBackend": "./hooks/useBackend/index.mjs"
+ "./hooks/useSelection": "./hooks/useSelection/index.mjs"
# Lib
"./lib/RestClient": "./lib/RestClient/index.mjs"
"./lib/logoPath": "./components/Logo/path.mjs"
diff --git a/package-lock.json b/package-lock.json
index df15e01c596..e5b5ba477b1 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -28341,6 +28341,10 @@
"resolved": "designs/hugo",
"link": true
},
+ "node_modules/@freesewing/i18n": {
+ "resolved": "packages/i18n",
+ "link": true
+ },
"node_modules/@freesewing/jaeger": {
"resolved": "designs/jaeger",
"link": true
@@ -47212,6 +47216,14 @@
"node": "14 || >=16.14"
}
},
+ "node_modules/luxon": {
+ "version": "3.5.0",
+ "resolved": "https://registry.npmjs.org/luxon/-/luxon-3.5.0.tgz",
+ "integrity": "sha512-rh+Zjr6DNfUYR3bPwJEnuwDdqMbxZW7LOQfUN4B54+Cl+0o5zaU9RJ6bcidfDtC1cWCZXQ+nvX8bf6bAji37QQ==",
+ "engines": {
+ "node": ">=12"
+ }
+ },
"node_modules/magic-string": {
"version": "0.25.9",
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.25.9.tgz",
@@ -61552,6 +61564,18 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "packages/i18n": {
+ "version": "3.3.0-rc.1",
+ "license": "MIT",
+ "devDependencies": {},
+ "engines": {
+ "node": ">= 20"
+ },
+ "funding": {
+ "type": "individual",
+ "url": "https://freesewing.org/patrons/join"
+ }
+ },
"packages/models": {
"name": "@freesewing/models",
"version": "3.3.0-rc.1",
@@ -61925,6 +61949,7 @@
"axios": "1.7.2",
"highlight.js": "^11.11.0",
"html-react-parser": "^5.0.7",
+ "luxon": "^3.5.0",
"nuqs": "^1.17.6",
"react-markdown": "^9.0.1",
"use-local-storage-state": "19.1.0",
diff --git a/packages/react/components/Account/Pattern.mjs b/packages/react/components/Account/Pattern.mjs
new file mode 100644
index 00000000000..f84c1d9ce04
--- /dev/null
+++ b/packages/react/components/Account/Pattern.mjs
@@ -0,0 +1,401 @@
+// Dependencies
+import orderBy from 'lodash/orderBy.js'
+import {
+ cloudflareImageUrl,
+ capitalize,
+ shortDate,
+ horFlexClasses,
+ newPatternUrl,
+} from '@freesewing/utils'
+import { urls, control as controlConfig } from '@freesewing/config'
+
+// Context
+import { LoadingStatusContext } from '@freesewing/react/context/LoadingStatus'
+import { ModalContext } from '@freesewing/react/context/Modal'
+
+// Hooks
+import React, { useState, useEffect, useContext } from 'react'
+import { useAccount } from '@freesewing/react/hooks/useAccount'
+import { useBackend } from '@freesewing/react/hooks/useBackend'
+import { useSelection } from '@freesewing/react/hooks/useSelection'
+
+// Components
+import Markdown from 'react-markdown'
+import {
+ StringInput,
+ MarkdownInput,
+ PassiveImageInput,
+ ListInput,
+} from '@freesewing/react/components/Input'
+import { Link as WebLink, AnchorLink } from '@freesewing/react/components/Link'
+import {
+ BoolNoIcon,
+ BoolYesIcon,
+ CloneIcon,
+ EditIcon,
+ FreeSewingIcon,
+ OkIcon,
+ NoIcon,
+ PatternIcon,
+ ShowcaseIcon,
+ ResetIcon,
+ UploadIcon,
+} from '@freesewing/react/components/Icon'
+import { DisplayRow } from './shared.mjs'
+import { TimeAgo } from '@freesewing/react/components/Time'
+import { Popout } from '@freesewing/react/components/Popout'
+import { ModalWrapper } from '@freesewing/react/components/Modal'
+import { KeyVal } from '@freesewing/react/components/KeyVal'
+
+export const Pattern = ({ id, Link }) => {
+ if (!Link) Link = WebLink
+ // Hooks
+ const { account, control } = useAccount()
+ const { setLoadingStatus } = useContext(LoadingStatusContext)
+ const backend = useBackend()
+
+ // Context
+ const { setModal } = useContext(ModalContext)
+
+ const [edit, setEdit] = useState(false)
+ const [pattern, setPattern] = useState()
+ // Set fields for editing
+ const [name, setName] = useState(pattern?.name)
+ const [image, setImage] = useState(pattern?.image)
+ const [isPublic, setIsPublic] = useState(pattern?.public ? true : false)
+ const [notes, setNotes] = useState(pattern?.notes || '')
+
+ // Effect
+ useEffect(() => {
+ const getPattern = async () => {
+ setLoadingStatus([true, 'Loading pattern from backend'])
+ const [status, body] = await backend.getPattern(id)
+ if (status === 200) {
+ setPattern(body.pattern)
+ setName(body.pattern.name)
+ setImage(body.pattern.image)
+ setIsPublic(body.pattern.public ? true : false)
+ setNotes(body.pattern.notes)
+ setLoadingStatus([true, 'Loaded pattern', true, true])
+ } else setLoadingStatus([true, 'An error occured. Please report this', true, false])
+ }
+ if (id) getPattern()
+ }, [id])
+
+ const save = async () => {
+ setLoadingStatus([true, 'Gathering info'])
+ // Compile data
+ const data = {}
+ if (name || name !== pattern.name) data.name = name
+ if (image || image !== pattern.image) data.img = image
+ if (notes || notes !== pattern.notes) data.notes = notes
+ if ([true, false].includes(isPublic) && isPublic !== pattern.public) data.public = isPublic
+ setLoadingStatus([true, 'Saving pattern'])
+ const [status, body] = await backend.updatePattern(pattern.id, data)
+ if (status === 200 && body.result === 'success') {
+ setPattern(body.pattern)
+ setEdit(false)
+ setLoadingStatus([true, 'Nailed it', true, true])
+ } else setLoadingStatus([true, 'An error occured. Please report this.', true, false])
+ }
+
+ const clone = async () => {
+ setLoadingStatus([true, 'Cloning pattern'])
+ // Compile data
+ const data = { ...pattern }
+ delete data.id
+ delete data.createdAt
+ delete data.data
+ delete data.userId
+ delete data.img
+ data.settings = JSON.parse(data.settings)
+ const [status, body] = await backend.createPattern(data)
+ if (status === 201 && body.result === 'created') {
+ setLoadingStatus([true, 'Loading newly created pattern', true, true])
+ window.location = `/account/pattern/?id=${body.pattern.id}`
+ } else setLoadingStatus([true, 'We failed to create this pattern', true, false])
+ }
+
+ const togglePublic = async () => {
+ setLoadingStatus([true, 'Updating pattern'])
+ // Compile data
+ const data = { public: !pattern.public }
+ const [status, body] = await backend.updatePattern(pattern.id, data)
+ if (status === 200 && body.result === 'success') {
+ setPattern(body.pattern)
+ setLoadingStatus([true, 'Nailed it', true, true])
+ } else setLoadingStatus([true, 'An error occured. Please report this.', true, false])
+ }
+ if (!pattern) return null
+
+ const header = (
+
+ )
+
+ if (!edit)
+ return (
+
+ {pattern.public ? (
+
+ This is the private view of your pattern
+
+ Everyone can access the public view since this is a public pattern.
+
+ But only you can access this private view.
+
+
+
+
+ Public View
+
+
+
+ ) : null}
+ {header}
+ {control >= controlConfig.account.patterns.notes && (
+ <>
+
Notes
+
{pattern.notes}
+ >
+ )}
+
+ )
+
+ return (
+
+
Edit pattern {pattern.name}
+
+ {/* Name is always shown */}
+
+
val && val.length > 0}
+ />
+
+ {/* img: Control level determines whether or not to show this */}
+
+ {account.control >= controlConfig.account.sets.img ? (
+ val.length > 0}
+ />
+ ) : null}
+
+ {/* public: Control level determines whether or not to show this */}
+
+ {account.control >= controlConfig.account.patterns.public ? (
+
+ Public Pattern
+
+
+ ),
+ desc: 'Public patterns can be shared with other FreeSewing users',
+ },
+ {
+ val: false,
+ label: (
+
+ Private Pattern
+
+
+ ),
+ desc: 'Private patterns are yours and yours alone',
+ },
+ ]}
+ current={isPublic}
+ />
+ ) : null}
+
+ {/* notes: Control level determines whether or not to show this */}
+
+ {account.control >= controlConfig.account.patterns.notes ? (
+
+ ) : null}
+
+ setEdit(false)}
+ className={`daisy-btn daisy-btn-primary daisy-btn-outline`}
+ >
+
+ Cancel
+
+
+
+ Save Pattern
+
+
+
+ )
+}
+
+export const PatternCard = ({
+ pattern,
+ href = false,
+ onClick = false,
+ useA = false,
+ size = 'md',
+ Link = false,
+}) => {
+ if (!Link) Link = WebLink
+ const sizes = {
+ lg: 96,
+ md: 52,
+ sm: 36,
+ xs: 20,
+ }
+ const s = sizes[size]
+
+ const wrapperProps = {
+ className: `bg-base-300 w-full mb-2 mx-auto flex flex-col items-start text-center justify-center rounded shadow py-4 w-${s} aspect-square`,
+ style: {
+ backgroundImage: `url(${cloudflareImageUrl({ type: 'w1000', id: pattern.img })})`,
+ backgroundSize: 'cover',
+ backgroundRepeat: 'no-repeat',
+ backgroundPosition: '50%',
+ },
+ }
+ if (pattern.img === 'default-avatar') wrapperProps.style.backgroundPosition = 'bottom right'
+
+ const inner = null
+
+ // Is it a button with an onClick handler?
+ if (onClick)
+ return (
+
+ {inner}
+
+ )
+
+ // Returns a link to an internal page
+ if (href && !useA)
+ return (
+
+ {inner}
+
+ )
+
+ // Returns a link to an external page
+ if (href && useA)
+ return (
+
+ {inner}
+
+ )
+
+ // Returns a div
+ return {inner}
+}
+
+const BadgeLink = ({ label, href }) => (
+
+ {label}
+
+)
+
+/**
+ * Helper component to show the pattern title, image, and various buttons
+ */
+const PatternHeader = ({
+ pattern,
+ Link,
+ account,
+ setModal,
+ setEdit,
+ togglePublic,
+ save,
+ clone,
+}) => (
+ <>
+ {pattern.name}
+
+
+ } color="secondary" />
+ } color="secondary" />
+
+
+
+
+
+ {account.control > 3 && (pattern?.public || pattern.userId === account.id) ? (
+
+
+
+
+ ) : (
+
+ )}
+
+ setModal(
+
+
+
+ )
+ }
+ className={`daisy-btn daisy-btn-secondary daisy-btn-outline ${horFlexClasses}`}
+ >
+
+ Show Image
+
+ {account.control > 3 ? (
+
togglePublic()}
+ className={`daisy-btn daisy-btn-${pattern.public ? 'error' : 'success'} daisy-btn-outline ${horFlexClasses} hover:text-${pattern.public ? 'error' : 'success'}-content`}
+ >
+ {pattern.public ? : }
+ Make pattern {pattern.public ? 'private' : 'public'}
+
+ ) : null}
+ {pattern.userId === account.id && (
+ <>
+
+
Update Pattern
+
+
+ Clone Pattern
+
+
setEdit(true)}
+ className={`daisy-btn daisy-btn-primary ${horFlexClasses}`}
+ >
+ Edit Pattern Metadata
+
+ >
+ )}
+
+
+ >
+)
diff --git a/packages/react/components/Account/Patterns.mjs b/packages/react/components/Account/Patterns.mjs
new file mode 100644
index 00000000000..16dc2a463c8
--- /dev/null
+++ b/packages/react/components/Account/Patterns.mjs
@@ -0,0 +1,236 @@
+// Dependencies
+import orderBy from 'lodash/orderBy.js'
+import { capitalize, shortDate } from '@freesewing/utils'
+
+// Context
+import { LoadingStatusContext } from '@freesewing/react/context/LoadingStatus'
+//import { ModalContext } from '@freesewing/react/context/Modal'
+
+// Hooks
+import React, { useState, useEffect, useContext } from 'react'
+import { useAccount } from '@freesewing/react/hooks/useAccount'
+import { useBackend } from '@freesewing/react/hooks/useBackend'
+import { useSelection } from '@freesewing/react/hooks/useSelection'
+
+//import {
+// measurements,
+// isDegreeMeasurement,
+// control as controlConfig,
+// urls,
+//} from '@freesewing/config'
+//import { measurements as measurementTranslations } from '@freesewing/i18n'
+//import { measurements as designMeasurements } from '@freesewing/collection'
+//import {
+// cloudflareImageUrl,
+// capitalize,
+// formatMm,
+// horFlexClasses,
+// linkClasses,
+// notEmpty,
+// roundDistance,
+// shortDate,
+// timeAgo,
+//} from '@freesewing/utils'
+
+//// Hooks
+//import React, { useState, useEffect, Fragment, useContext } from 'react'
+//import { useAccount } from '@freesewing/react/hooks/useAccount'
+//import { useBackend } from '@freesewing/react/hooks/useBackend'
+// Components
+import { TableWrapper } from '@freesewing/react/components/Table'
+import { PatternCard } from '@freesewing/react/components/Account'
+import { Link as WebLink } from '@freesewing/react/components/Link'
+import {
+ BoolNoIcon,
+ BoolYesIcon,
+ // CloneIcon,
+ // CuratedMeasurementsSetIcon,
+ // EditIcon,
+ // ShowcaseIcon,
+ // NewMeasurementsSetIcon,
+ // NoIcon,
+ // OkIcon,
+ PlusIcon,
+ RightIcon,
+ // ResetIcon,
+ TrashIcon,
+ // UploadIcon,
+ // // WarningIcon,
+ // // BoolYesIcon,
+ // // BoolNoIcon,
+} from '@freesewing/react/components/Icon'
+//import { BookmarkButton, MsetCard } from '@freesewing/react/components/Account'
+//import {
+// DesignInput,
+// MarkdownInput,
+// ListInput,
+// MeasieInput,
+// PassiveImageInput,
+// StringInput,
+// ToggleInput,
+//} from '@freesewing/react/components/Input'
+//import { DisplayRow } from './shared.mjs'
+//import Markdown from 'react-markdown'
+//import { ModalWrapper } from '@freesewing/react/components/Modal'
+//import { Json } from '@freesewing/react/components/Json'
+//import { Yaml } from '@freesewing/react/components/Yaml'
+//import { Popout } from '@freesewing/react/components/Popout'
+
+const t = (input) => {
+ console.log('t called', input)
+ return input
+}
+
+/*
+ * Component for the account/patterns page
+ *
+ * @params {object} props - All React props
+ * @params {function} Link - A framework specific Link component for client-side routing
+ */
+export const Patterns = ({ Link = false }) => {
+ if (!Link) Link = WebLink
+
+ // State
+ const [patterns, setPatterns] = useState([])
+ const [refresh, setRefresh] = useState(0)
+ const [order, setOrder] = useState('id')
+ const [desc, setDesc] = useState(false)
+
+ // Hooks
+ const backend = useBackend()
+ const { setLoadingStatus, LoadingProgress } = useContext(LoadingStatusContext)
+ const { count, selection, setSelection, toggle, toggleAll } = useSelection(patterns)
+
+ // Effects
+ useEffect(() => {
+ const getPatterns = async () => {
+ setLoadingStatus([true, 'Loading patterns from backend'])
+ const [status, body] = await backend.getPatterns()
+ console.log({ status, body })
+ if (status === 200) {
+ setPatterns(body.patterns)
+ setLoadingStatus([true, 'Patterns loaded', true, true])
+ } else setLoadingStatus([false, 'Failed to load patterns from backend', true, true])
+ }
+ getPatterns()
+ }, [refresh])
+
+ // Helper to delete one or more patterns
+ const removeSelectedPatterns = async () => {
+ let i = 0
+ for (const pattern in selection) {
+ i++
+ await backend.removePattern(pattern)
+ setLoadingStatus([
+ true,
+ ,
+ ])
+ }
+ setSelection({})
+ setRefresh(refresh + 1)
+ setLoadingStatus([true, 'Nailed it', true, true])
+ }
+
+ const fields = {
+ id: '#',
+ img: 'Image',
+ name: 'Name',
+ design: 'Design',
+ createdAt: 'Date',
+ public: 'Public',
+ }
+
+ return (
+ <>
+
+
+ {count} {t('patterns')}
+
+
+
+ Create a new pattern
+
+
+
+
+
+ >
+ )
+}
diff --git a/packages/react/components/Account/Sets.mjs b/packages/react/components/Account/Sets.mjs
index 37450c53148..667d0a72c70 100644
--- a/packages/react/components/Account/Sets.mjs
+++ b/packages/react/components/Account/Sets.mjs
@@ -3,71 +3,13 @@ import { measurements } from '@freesewing/config'
import { cloudflareImageUrl, capitalize } from '@freesewing/utils'
// Context
import { LoadingStatusContext } from '@freesewing/react/context/LoadingStatus'
-//import { ModalContext } from '@freesewing/react/context/Modal'
// Hooks
import React, { useState, useEffect, Fragment, useContext } from 'react'
import { useAccount } from '@freesewing/react/hooks/useAccount'
import { useBackend } from '@freesewing/react/hooks/useBackend'
// Components
import { Link as WebLink } from '@freesewing/react/components/Link'
-import {
- NoIcon,
- OkIcon,
- PlusIcon,
- TrashIcon,
- UploadIcon,
- // EditIcon,
- // ResetIcon,
- // WarningIcon,
- // CameraIcon,
- // CsetIcon,
- // BoolYesIcon,
- // BoolNoIcon,
- // CloneIcon,
-} from '@freesewing/react/components/Icon'
-
-//import { measurements as designMeasurements } from 'shared/prebuild/data/design-measurements.mjs'
-//import { freeSewingConfig as conf, controlLevels } from 'shared/config/freesewing.config.mjs'
-//import { isDegreeMeasurement } from 'config/measurements.mjs'
-//import {
-// shortDate,
-// cloudflareImageUrl,
-// formatMm,
-// hasRequiredMeasurements,
-// capitalize,
-// horFlexClasses,
-//} from 'shared/utils.mjs'
-//// Hooks
-//import { useState, useEffect, useContext } from 'react'
-//import { useTranslation } from 'next-i18next'
-//import { useAccount } from 'shared/hooks/use-account.mjs'
-//import { useBackend } from 'shared/hooks/use-backend.mjs'
-//import { useRouter } from 'next/router'
-//// Context
-//import { LoadingStatusContext } from 'shared/context/loading-status-context.mjs'
-//import { ModalContext } from 'shared/context/modal-context.mjs'
-//// Components
-//import { Popout } from 'shared/components/popout/index.mjs'
-//import { BackToAccountButton } from './shared.mjs'
-//import { AnchorLink, PageLink, Link } from 'shared/components/link.mjs'
-//import { Json } from 'shared/components/json.mjs'
-//import { Yaml } from 'shared/components/yaml.mjs'
-//import { ModalWrapper } from 'shared/components/wrappers/modal.mjs'
-//import { Mdx } from 'shared/components/mdx/dynamic.mjs'
-//import Timeago from 'react-timeago'
-//import { DisplayRow } from './shared.mjs'
-//import {
-// StringInput,
-// ToggleInput,
-// PassiveImageInput,
-// ListInput,
-// MarkdownInput,
-// MeasieInput,
-// DesignDropdown,
-// ns as inputNs,
-//} from 'shared/components/inputs.mjs'
-//import { BookmarkButton } from 'shared/components/bookmarks.mjs'
-//import { DynamicMdx } from 'shared/components/mdx/dynamic.mjs'
+import { NoIcon, OkIcon, PlusIcon, TrashIcon, UploadIcon } from '@freesewing/react/components/Icon'
/*
* The component for the an account/sets page
@@ -323,963 +265,3 @@ export const MsetCard = ({
// Returns a div
return {inner}
}
-
-/*
-export const NewSet = () => {
- // Hooks
- const { setLoadingStatus } = useContext(LoadingStatusContext)
- const backend = useBackend()
- const { t } = useTranslation(ns)
- const router = useRouter()
- const { account } = useAccount()
-
- // State
- const [name, setName] = useState('')
-
- // Use account setting for imperial
- const imperial = account.imperial
-
- // Helper method to create a new set
- const createSet = async () => {
- setLoadingStatus([true, 'processingUpdate'])
- const result = await backend.createSet({ name, imperial })
- if (result.success) {
- setLoadingStatus([true, t('nailedIt'), true, true])
- router.push(`/account/set?id=${result.data.set.id}`)
- } else setLoadingStatus([true, 'backendError', true, false])
- }
-
- return (
-
-
{t('name')}
-
{t('setNameDesc')}
-
setName(evt.target.value)}
- className="input w-full input-bordered flex flex-row"
- type="text"
- placeholder={'Georg Cantor'}
- />
-
-
- {t('newSet')}
-
-
-
- )
-}
-
-export const MeasieVal = ({ val, m, imperial }) =>
- isDegreeMeasurement(m) ? (
- {val}°
- ) : (
-
- )
-
-export const Mset = ({ id, publicOnly = false }) => {
- // Hooks
- const { account, control } = useAccount()
- const { setLoadingStatus } = useContext(LoadingStatusContext)
- const backend = useBackend()
- const { t, i18n } = useTranslation(ns)
-
- // Context
- const { setModal } = useContext(ModalContext)
-
- const [filter, setFilter] = useState(false)
- const [edit, setEdit] = useState(false)
- const [suggest, setSuggest] = useState(false)
- const [mset, setMset] = useState()
- // Set fields for editing
- const [name, setName] = useState(mset?.name)
- const [image, setImage] = useState(mset?.image)
- const [isPublic, setIsPublic] = useState(mset?.public ? true : false)
- const [imperial, setImperial] = useState(mset?.imperial ? true : false)
- const [notes, setNotes] = useState(mset?.notes || '')
- const [measies, setMeasies] = useState({})
- const [displayAsMetric, setDisplayAsMetric] = useState(mset?.imperial ? false : true)
-
- // Effect
- useEffect(() => {
- const getSet = async () => {
- setLoadingStatus([true, t('backendLoadingStarted')])
- const result = await backend.getSet(id)
- if (result.success) {
- setMset(result.data.set)
- setName(result.data.set.name)
- setImage(result.data.set.image)
- setIsPublic(result.data.set.public ? true : false)
- setImperial(result.data.set.imperial ? true : false)
- setNotes(result.data.set.notes)
- setMeasies(result.data.set.measies)
- setLoadingStatus([true, 'backendLoadingCompleted', true, true])
- } else setLoadingStatus([true, 'backendError', true, false])
- }
- const getPublicSet = async () => {
- setLoadingStatus([true, t('backendLoadingStarted')])
- const result = await backend.getPublicSet(id)
- if (result.success) {
- setMset({
- ...result.data,
- public: true,
- measies: result.data.measurements,
- })
- setName(result.data.name)
- setImage(result.data.image)
- setIsPublic(result.data.public ? true : false)
- setImperial(result.data.imperial ? true : false)
- setNotes(result.data.notes)
- setMeasies(result.data.measurements)
- setLoadingStatus([true, 'backendLoadingCompleted', true, true])
- } else setLoadingStatus([true, 'backendError', true, false])
- }
- if (id) {
- if (publicOnly) getPublicSet()
- else getSet()
- }
- }, [id, publicOnly])
-
- const filterMeasurements = () => {
- if (!filter) return measurements.map((m) => t(`measurements:${m}`) + `|${m}`).sort()
- else return designMeasurements[filter].map((m) => t(`measurements:${m}`) + `|${m}`).sort()
- }
-
- if (!id || !mset) return null
-
- const updateMeasies = (m, val) => {
- const newMeasies = { ...measies }
- newMeasies[m] = val
- setMeasies(newMeasies)
- }
-
- const save = async () => {
- setLoadingStatus([true, 'gatheringInfo'])
- // Compile data
- const data = { measies: {} }
- if (name || name !== mset.name) data.name = name
- if (image || image !== mset.image) data.img = image
- if ([true, false].includes(isPublic) && isPublic !== mset.public) data.public = isPublic
- if ([true, false].includes(imperial) && imperial !== mset.imperial) data.imperial = imperial
- if (notes || notes !== mset.notes) data.notes = notes
- // Add measurements
- for (const m of measurements) {
- if (measies[m] || measies[m] !== mset.measies[m]) data.measies[m] = measies[m]
- }
- setLoadingStatus([true, 'savingSet'])
- const result = await backend.updateSet(mset.id, data)
- if (result.success) {
- setMset(result.data.set)
- setEdit(false)
- setLoadingStatus([true, 'nailedIt', true, true])
- } else setLoadingStatus([true, 'backendError', true, false])
- }
-
- const togglePublic = async () => {
- setLoadingStatus([true, 'gatheringInfo'])
- const result = await backend.updateSet(mset.id, { public: !mset.public })
- if (result.success) {
- setMset(result.data.set)
- setLoadingStatus([true, 'nailedIt', true, true])
- } else setLoadingStatus([true, 'backendError', true, false])
- }
-
- const importSet = async () => {
- setLoadingStatus([true, t('account.importing')])
- // Compile data
- const data = {
- ...mset,
- userId: account.id,
- measies: { ...mset.measies },
- }
- delete data.img
- const result = await backend.createSet(data)
- if (result.success) {
- setMset(result.data.set)
- setEdit(false)
- setLoadingStatus([true, 'nailedIt', true, true])
- } else setLoadingStatus([true, 'backendError', true, false])
- }
-
- const docs = {}
- for (const option of ['name', 'units', 'public', 'notes', 'image']) {
- docs[option] =
- }
-
- const heading = (
- <>
-
-
-
-
-
- {account.control > 2 && mset.public && mset.userId !== account.id ? (
-
- ) : (
-
- )}
- {account.control > 3 && mset.userId === account.id ? (
-
-
- setModal(
-
-
-
- )
- }
- >
- JSON
-
-
- setModal(
-
-
-
- )
- }
- >
- YAML
-
-
- ) : (
-
- )}
- {account.id && account.control > 2 && mset.public && mset.userId !== account.id ? (
-
-
-
- {t('account:importSet')}
-
-
- ) : null}
- {account.control > 2 ? (
-
- ) : null}
-
- setModal(
-
-
-
- )
- }
- className={`btn btn-secondary btn-outline ${horFlexClasses}`}
- >
-
- {t('showImage')}
-
- {!publicOnly && (
- <>
- {account.control > 2 ? (
-
{
- setSuggest(!suggest)
- setEdit(false)
- }}
- className={`btn ${
- suggest ? 'btn-neutral' : 'btn-primary'
- } btn-outline ${horFlexClasses}`}
- >
- {suggest ? : }
- {t(suggest ? 'account:cancel' : 'account:suggestForCuration')}
-
- ) : null}
- {edit ? (
- <>
-
{
- setEdit(false)
- setSuggest(false)
- }}
- className={`btn btn-neutral btn-outline ${horFlexClasses}`}
- >
-
- {t('cancel')}
-
-
-
- {t('saveThing', { thing: t('account:set') })}
-
- >
- ) : (
-
{
- setEdit(true)
- setSuggest(false)
- }}
- className={`btn btn-primary ${horFlexClasses}`}
- >
- {t('editThing', { thing: t('account:set') })}
-
- )}
- >
- )}
- {account.control > 2 && mset.userId === account.id ? (
-
-
-
- {t('account:cloneSet')}
-
-
- ) : null}
-
-
-
- >
- )
-
- if (suggest)
- return (
-
- {heading}
-
-
- )
-
- if (!edit)
- return (
-
- {heading}
-
-
{t('data')}
-
{mset.name}
-
- {mset.imperial ? t('imperialUnits') : t('metricUnits')}
-
- {control >= controlLevels.sets.notes && (
-
-
-
- )}
- {control >= controlLevels.sets.public && (
- <>
- {mset.userId === account.id && (
-
-
- {mset.public ? (
-
- ) : (
-
- )}
-
- {t(`account:make${mset.public ? 'Private' : 'Public'}`)}
-
-
-
- )}
- {mset.public && (
-
-
-
- )}
- >
- )}
- {control >= controlLevels.sets.createdAt && (
-
-
- |
- {shortDate(i18n.language, mset.createdAt, false)}
-
- )}
- {control >= controlLevels.sets.updatedAt && (
-
-
- |
- {shortDate(i18n.language, mset.updatedAt, false)}
-
- )}
- {control >= controlLevels.sets.id &&
{mset.id} }
-
- {Object.keys(mset.measies).length > 0 && (
- <>
-
{t('measies')}
-
setDisplayAsMetric(!displayAsMetric)}
- current={displayAsMetric}
- />
- {Object.entries(mset.measies).map(([m, val]) =>
- val > 0 ? (
- }
- key={m}
- >
- {t(m)}
-
- ) : null
- )}
- >
- )}
-
- )
-
- return (
-
- {heading}
-
- {['measies', 'data'].map((s) => (
-
-
-
- ))}
-
-
-
-
- {account.control >= conf.account.sets.img ? (
-
-
-
- ) : null}
- {['public', 'units', 'notes'].map((id) =>
- account.control >= conf.account.sets[id] ? (
-
-
-
- ) : null
- )}
-
-
-
-
{t('measies')}
-
- {t('noFilter')}}
- />
-
- {filterMeasurements().map((mplus) => {
- const [translated, m] = mplus.split('|')
-
- return (
-
- }
- />
- )
- })}
-
-
{t('data')}
-
- {// Name is always shown //}
-
-
val && val.length > 0}
- docs={docs.name}
- />
-
- {// img: Control level determines whether or not to show this //}
-
- {account.control >= conf.account.sets.img ? (
- val.length > 0}
- docs={docs.image}
- />
- ) : null}
-
- {// public: Control level determines whether or not to show this //}
-
- {account.control >= conf.account.sets.public ? (
-
- {t('publicSet')}
-
-
- ),
- desc: t('publicSetDesc'),
- },
- {
- val: false,
- label: (
-
- {t('privateSet')}
-
-
- ),
- desc: t('privateSetDesc'),
- },
- ]}
- current={isPublic}
- docs={docs.public}
- />
- ) : null}
-
- {// units: Control level determines whether or not to show this //}
-
- {account.control >= conf.account.sets.units ? (
- <>
-
- {t('metricUnits')}
- cm
-
- ),
- desc: t('metricUnitsd'),
- },
- {
- val: true,
- label: (
-
- {t('imperialUnits')}
- ″
-
- ),
- desc: t('imperialUnitsd'),
- },
- ]}
- current={imperial}
- docs={docs.units}
- />
- {t('unitsMustSave')}
- >
- ) : null}
-
- {// notes: Control level determines whether or not to show this //}
-
- {account.control >= conf.account.sets.notes ? (
-
- ) : null}
-
-
- {t('saveThing', { thing: t('account:set') })}
-
-
- )
-}
-
-export const SetCard = ({
- set,
- requiredMeasies = [],
- href = false,
- onClick = false,
- useA = false,
-}) => {
- // Hooks
- const { t } = useTranslation(['sets'])
-
- const [hasMeasies] = hasRequiredMeasurements(requiredMeasies, set.measies, true)
-
- const wrapperProps = {
- className:
- 'bg-base-300 w-full mb-2 mx-auto flex flex-col items-start text-center justify-center rounded shadow py-4',
- style: {
- backgroundImage: `url(${cloudflareImageUrl({ type: 'w1000', id: set.img })})`,
- backgroundSize: 'cover',
- backgroundRepeat: 'no-repeat',
- backgroundPosition: '50%',
- },
- }
- if (set.img === 'default-avatar') wrapperProps.style.backgroundPosition = 'bottom right'
-
- const inner = hasMeasies ? null : (
-
-
- {t('setLacksMeasiesForDesign')}
-
- )
-
- // Is it a button with an onClick handler?
- if (onClick)
- return (
-
- {inner}
-
- )
-
- // Returns a link to an internal page
- if (href && !useA)
- return (
-
- {inner}
-
- )
-
- // Returns a link to an external page
- if (href && useA)
- return (
-
- {inner}
-
- )
-
- // Returns a div
- return {inner}
-}
-
-export const MsetButton = (props) =>
-export const MsetLink = (props) =>
-export const MsetA = (props) =>
-
-export const UserSetPicker = ({
- design,
- t,
- href,
- clickHandler,
- missingClickHandler,
- size = 'lg',
-}) => {
- // Hooks
- const backend = useBackend()
- const { control } = useAccount()
-
- // State
- const [sets, setSets] = useState({})
-
- // Effects
- useEffect(() => {
- const getSets = async () => {
- const result = await backend.getSets()
- if (result.success) {
- const all = {}
- for (const set of result.data.sets) all[set.id] = set
- setSets(all)
- }
- }
- getSets()
- }, [backend])
-
- let hasSets = false
- const okSets = []
- const lackingSets = []
- if (Object.keys(sets).length > 0) {
- hasSets = true
- for (const setId in sets) {
- const [hasMeasies] = hasRequiredMeasurements(
- designMeasurements[design],
- sets[setId].measies,
- true
- )
- if (hasMeasies) okSets.push(sets[setId])
- else lackingSets.push(sets[setId])
- }
- }
-
- if (!hasSets)
- return (
-
-
- {t('account:noOwnSets')}
- {t('account:pleaseMtm')}
- {t('account:noOwnSetsMsg')}
-
-
-
- {t('account:newSet')}
-
-
-
-
- )
-
- return (
- <>
- {okSets.length > 0 && (
-
- {okSets.map((set) => (
-
- ))}
-
- )}
- {lackingSets.length > 0 && (
-
-
- {t('account:someSetsLacking')}
-
-
- {lackingSets.map((set) => (
-
- ))}
-
-
- )}
- >
- )
-}
-
-export const BookmarkedSetPicker = ({ design, clickHandler, t, size, href }) => {
- // Hooks
- const { control } = useAccount()
- const backend = useBackend()
-
- // State
- const [sets, setSets] = useState({})
-
- // Effects
- useEffect(() => {
- const getBookmarks = async () => {
- const result = await backend.getBookmarks()
- const loadedSets = {}
- if (result.success) {
- for (const bookmark of result.data.bookmarks.filter(
- (bookmark) => bookmark.type === 'set'
- )) {
- let set
- try {
- set = await backend.getSet(bookmark.url.slice(6))
- if (set.success) {
- const [hasMeasies] = hasRequiredMeasurements(
- designMeasurements[design],
- set.data.set.measies,
- true
- )
- loadedSets[set.data.set.id] = { ...set.data.set, hasMeasies }
- }
- } catch (err) {
- console.log(err)
- }
- }
- }
- setSets(loadedSets)
- }
- getBookmarks()
- }, [])
-
- const okSets = Object.values(sets).filter((set) => set.hasMeasies)
- const lackingSets = Object.values(sets).filter((set) => !set.hasMeasies)
-
- return (
- <>
- {okSets.length > 0 && (
-
- {okSets.map((set) => (
-
- ))}
-
- )}
- {lackingSets.length > 0 && (
-
-
- {t('account:someSetsLacking')}
-
-
- {lackingSets.map((set) => (
-
- ))}
-
-
- )}
- >
- )
-}
-
-const SuggestCset = ({ mset, backend, setLoadingStatus, t }) => {
- // State
- const [height, setHeight] = useState('')
- const [img, setImg] = useState('')
- const [name, setName] = useState('')
- const [notes, setNotes] = useState('')
- const [submission, setSubmission] = useState(false)
-
- // Method to submit the form
- const suggestSet = async () => {
- setLoadingStatus([true, 'status:contactingBackend'])
- const result = await backend.suggestCset({ set: mset.id, height, img, name, notes })
- if (result.success && result.data.submission) {
- setSubmission(result.data.submission)
- setLoadingStatus([true, 'status:nailedIt', true, true])
- } else setLoadingStatus([true, 'backendError', true, false])
- }
-
- const missing = []
- for (const m of measurements) {
- if (typeof mset.measies[m] === 'undefined') missing.push(m)
- }
-
- if (submission) {
- const url = `/curate/sets/suggested/${submission.id}`
-
- return (
- <>
- {t('account:thankYouVeryMuch')}
- {t('account:csetSuggestedMsg')}
-
- {t('account:itIsAvailableAt')}:
-
- >
- )
- }
-
- return (
- <>
- {t('account:suggestCset')}
-
- {missing.length > 0 ? : }
- {t('account:measurements')}
-
- {missing.length > 0 ? (
- <>
- {t('account:csetAllMeasies')}
- {t('account:csetMissing')}:
-
- {missing.map((m) => (
- {t(`measurements:${m}`)}
- ))}
-
- >
- ) : (
- {t('account:allMeasiesAvailable')}
- )}
-
- {name.length > 1 ? : }
- {t('account:name')}
-
- {t('account:csetNameMsg')}
- val.length > 1}
- />
-
- {height.length > 1 ? : }
- {t('measurements:height')}
-
- {t('account:csetHeightMsg1')}
- val.length > 1}
- />
-
- {img.length > 0 ? : }
- {t('account:img')}
-
-
- {t('account:csetImgMsg')}:{' '}
- {t('account:docs')}
-
- val.length > 1}
- />
-
-
- {t('account:notes')}
-
- {t('account:csetNotesMsg')}
-
- {t('account:mdSupport')}
-
- true}
- />
- 1 && img.length > 0)}
- onClick={suggestSet}
- >
- {t('account:suggestForCuration')}
-
- >
- )
-}
-
-*/
diff --git a/packages/react/components/Account/index.mjs b/packages/react/components/Account/index.mjs
index 83df04c04ff..b9c44aafa38 100644
--- a/packages/react/components/Account/index.mjs
+++ b/packages/react/components/Account/index.mjs
@@ -4,5 +4,18 @@ import { Bookmarks, BookmarkButton } from './Bookmarks.mjs'
import { Links } from './Links.mjs'
import { Set, NewSet } from './Set.mjs'
import { Sets, MsetCard } from './Sets.mjs'
+import { Patterns } from './Patterns.mjs'
+import { Pattern, PatternCard } from './Pattern.mjs'
-export { Bookmarks, BookmarkButton, Links, Set, NewSet, Sets, MsetCard }
+export {
+ Bookmarks,
+ BookmarkButton,
+ Links,
+ Set,
+ NewSet,
+ Sets,
+ MsetCard,
+ Patterns,
+ Pattern,
+ PatternCard,
+}
diff --git a/packages/react/components/KeyVal/index.mjs b/packages/react/components/KeyVal/index.mjs
new file mode 100644
index 00000000000..b0bbe602da2
--- /dev/null
+++ b/packages/react/components/KeyVal/index.mjs
@@ -0,0 +1,38 @@
+import React, { useState, useContext } from 'react'
+import { CopyToClipboard as Copy } from 'react-copy-to-clipboard'
+import { LoadingStatusContext } from '@freesewing/react/context/LoadingStatus'
+
+export const KeyVal = ({ k, val, color = 'primary', small = false }) => {
+ const [copied, setCopied] = useState(false)
+ const { setLoadingStatus } = useContext(LoadingStatusContext)
+
+ return (
+ handleCopied(setCopied, setLoadingStatus, k)}>
+
+
+ {k}
+
+
+ {val}
+
+
+
+ )
+}
+
+const sharedClasses = `px-1 text-sm font-medium whitespace-nowrap border-2`
+
+const handleCopied = (setCopied, setLoadingStatus, label) => {
+ setCopied(true)
+ setLoadingStatus([
+ true,
+ label ? `${label} copied to clipboard` : 'Copied to clipboard',
+ true,
+ true,
+ ])
+ setTimeout(() => setCopied(false), 1000)
+}
diff --git a/packages/react/components/Pattern/index.mjs b/packages/react/components/Pattern/index.mjs
index e445cf58e62..caec4f1eed9 100644
--- a/packages/react/components/Pattern/index.mjs
+++ b/packages/react/components/Pattern/index.mjs
@@ -1,3 +1,6 @@
+// Dependencies
+import { cloudflareImageUrl } from '@freesewing/utils'
+// Components
import React, { forwardRef } from 'react'
import { Svg as DefaultSvg } from './svg.mjs'
import { Defs as DefaultDefs } from './defs.mjs'
@@ -11,6 +14,7 @@ import { Grid as DefaultGrid } from './grid.mjs'
import { Text as DefaultText, TextOnPath as DefaultTextOnPath } from './text.mjs'
import { Circle as DefaultCircle } from './circle.mjs'
import { getId, getProps, withinPartBounds, translateStrings } from './utils.mjs'
+import { Link as WebLink } from '@freesewing/react/components/Link'
/*
* Allow people to override these components
diff --git a/packages/react/components/Table/index.mjs b/packages/react/components/Table/index.mjs
new file mode 100644
index 00000000000..71ad2e81371
--- /dev/null
+++ b/packages/react/components/Table/index.mjs
@@ -0,0 +1,9 @@
+import React from 'react'
+
+/*
+ * Tables on mobile will almost always break the layout
+ * unless we set the overflow behaviour explicitly
+ */
+export const TableWrapper = ({ children }) => (
+ {children}
+)
diff --git a/packages/react/components/Time/index.mjs b/packages/react/components/Time/index.mjs
new file mode 100644
index 00000000000..51d23f8c5b9
--- /dev/null
+++ b/packages/react/components/Time/index.mjs
@@ -0,0 +1,58 @@
+import React from 'react'
+import { DateTime, Interval } from 'luxon'
+
+const day = 86400000
+const hour = 3600000
+const minute = 60000
+const second = 1000
+
+export const DateAndTime = ({ iso }) => {
+ const dt = DateTime.fromISO(iso)
+ return dt.toLocaleString(DateTime.DATETIME_MED)
+}
+
+export const TimeForHumans = ({ iso, future = false }) => {
+ const suffix = future ? 'from now' : 'ago'
+ const dates = [DateTime.fromISO(iso), DateTime.now()]
+ if (future) dates.reverse()
+ const i = Interval.fromDateTimes(...dates)
+ .toDuration(['minutes', 'hours', 'days', 'months', 'years'])
+ .toObject()
+ const years = Math.floor(i.years)
+ const months = Math.floor(i.months)
+ const days = Math.floor(i.days)
+ const hours = Math.floor(i.hours)
+ const minutes = Math.floor(i.minutes)
+ if (years < 1 && months < 1 && days < 1 && hours < 1 && minutes < 1) return `seconds ${suffix}`
+ else if (years < 1 && months < 1 && days < 1 && hours < 1)
+ return minutes < 2 ? `one minute ${suffix}` : `${minutes} minutes ${suffix}`
+ else if (i.years < 1 && i.months < 1 && i.days < 1)
+ return hours < 2 ? `${hours * 60 + minutes} minutes ${suffix}` : `${hours} hours ${suffix}`
+ else if (years < 1 && months < 1)
+ return days < 2 ? `${days * 24 + hours} hours ${suffix}` : `${days} days ${suffix}`
+ else if (years < 1)
+ return months < 4 ? `${months} months and ${days} days ${suffix}` : `${months} months ${suffix}`
+ if (years < 3) return `${years * 12 + i.months} months ${suffix}`
+ return `${years} years ${suffix}`
+}
+
+export const TimeAgo = (props) =>
+export const TimeToGo = (props) =>
+
+export const TimeAgoBrief = ({ time }) => {
+ const d = Math.floor(Date.now() - time)
+ if (d > day) return `${Math.floor(d / day)}d ago`
+ if (d > hour) return `${Math.floor(d / hour)}h ago`
+ if (d > minute * 2) return `${Math.floor(d / minute)}m ago`
+ if (d > second) return `${Math.floor(d / second)}s ago`
+ return `${d}ms ago`
+}
+
+export const TimeToGoBrief = ({ time }) => {
+ const d = Math.floor(time * 1000 - Date.now())
+ if (d > day) return `${Math.floor(d / day)}d`
+ if (d > hour) return `${Math.floor(d / hour)}h`
+ if (d > minute * 2) return `${Math.floor(d / minute)}m`
+ if (d > second) return `${Math.floor(d / second)}s`
+ return `${d}s`
+}
diff --git a/packages/react/hooks/useSelection/index.mjs b/packages/react/hooks/useSelection/index.mjs
new file mode 100644
index 00000000000..ac3825ef3c0
--- /dev/null
+++ b/packages/react/hooks/useSelection/index.mjs
@@ -0,0 +1,40 @@
+import React, { useState } from 'react'
+
+export const useSelection = (items) => {
+ const [selection, setSelection] = useState({})
+
+ /*
+ * This variable keeps track of how many are selected
+ */
+ const count = Object.keys(selection).length
+
+ /*
+ * This method toggles a single item in the selection
+ */
+ const toggle = (id) => {
+ const newSelection = { ...selection }
+ if (newSelection[id]) delete newSelection[id]
+ else newSelection[id] = 1
+ setSelection(newSelection)
+ }
+
+ /*
+ * This method toggles all on or off
+ */
+ const toggleAll = () => {
+ if (count === items.length) setSelection({})
+ else {
+ const newSelection = {}
+ for (const item of items) newSelection[item.id] = 1
+ setSelection(newSelection)
+ }
+ }
+
+ return {
+ count,
+ selection,
+ setSelection,
+ toggle,
+ toggleAll,
+ }
+}
diff --git a/packages/react/package.json b/packages/react/package.json
index 8681a788e92..f0d10e03798 100644
--- a/packages/react/package.json
+++ b/packages/react/package.json
@@ -37,6 +37,7 @@
"./components/Icon": "./components/Icon/index.mjs",
"./components/Input": "./components/Input/index.mjs",
"./components/Json": "./components/Json/index.mjs",
+ "./components/KeyVal": "./components/KeyVal/index.mjs",
"./components/Layout": "./components/Layout/index.mjs",
"./components/LineDrawing": "./components/LineDrawing/index.mjs",
"./components/Link": "./components/Link/index.mjs",
@@ -49,12 +50,15 @@
"./components/SignIn": "./components/SignIn/index.mjs",
"./components/Spinner": "./components/Spinner/index.mjs",
"./components/Tab": "./components/Tab/index.mjs",
+ "./components/Table": "./components/Table/index.mjs",
+ "./components/Time": "./components/Time/index.mjs",
"./components/Yaml": "./components/Yaml/index.mjs",
"./components/Xray": "./components/Xray/index.mjs",
"./context/LoadingStatus": "./context/LoadingStatus/index.mjs",
"./context/Modal": "./context/Modal/index.mjs",
"./hooks/useAccount": "./hooks/useAccount/index.mjs",
"./hooks/useBackend": "./hooks/useBackend/index.mjs",
+ "./hooks/useSelection": "./hooks/useSelection/index.mjs",
"./lib/RestClient": "./lib/RestClient/index.mjs",
"./lib/logoPath": "./components/Logo/path.mjs"
},
@@ -69,6 +73,7 @@
"axios": "1.7.2",
"highlight.js": "^11.11.0",
"html-react-parser": "^5.0.7",
+ "luxon": "^3.5.0",
"nuqs": "^1.17.6",
"react-markdown": "^9.0.1",
"use-local-storage-state": "19.1.0",
diff --git a/packages/utils/src/index.mjs b/packages/utils/src/index.mjs
index 98d7dd80b05..bff94a1676b 100644
--- a/packages/utils/src/index.mjs
+++ b/packages/utils/src/index.mjs
@@ -244,6 +244,12 @@ export function measurementAsUnits(mmValue, units = 'metric') {
return round(mmValue / (units === 'imperial' ? 25.4 : 10), 3)
}
+/** Generate a URL to create a new pattern with a given design, settings, and view */
+export const newPatternUrl = ({ design, settings = {}, view = 'draft' }) =>
+ `/-/#settings=${encodeURIComponent(
+ JSON.stringify(settings)
+ )}&view=${encodeURIComponent('"' + view + '"')}`
+
/*
* A method to ensure input is not empty
*
diff --git a/sites/org/docs/account/pattern/index.mdx b/sites/org/docs/account/pattern/index.mdx
new file mode 100644
index 00000000000..a697004b15a
--- /dev/null
+++ b/sites/org/docs/account/pattern/index.mdx
@@ -0,0 +1,17 @@
+---
+title: Pattern
+sidebar_label: ' '
+sidebar_position: 99
+---
+
+import { getSearchParam } from '@freesewing/utils'
+import { DocusaurusDoc } from '@freesewing/react/components/Docusaurus'
+import { RoleBlock } from '@freesewing/react/components/Role'
+import { Pattern } from '@freesewing/react/components/Account'
+import Link from '@docusaurus/Link'
+
+
+
+
+
+
diff --git a/sites/org/docs/account/patterns/index.mdx b/sites/org/docs/account/patterns/index.mdx
new file mode 100644
index 00000000000..00d5a51ea7b
--- /dev/null
+++ b/sites/org/docs/account/patterns/index.mdx
@@ -0,0 +1,15 @@
+---
+title: Your Patterns
+sidebar_position: 3
+---
+
+import { DocusaurusDoc } from '@freesewing/react/components/Docusaurus'
+import { RoleBlock } from '@freesewing/react/components/Role'
+import { Patterns as AccountPatterns } from '@freesewing/react/components/Account'
+import Link from '@docusaurus/Link'
+
+
+
+
+
+