diff --git a/config/exceptions.yaml b/config/exceptions.yaml index 426879d8f2d..c9599efcec0 100644 --- a/config/exceptions.yaml +++ b/config/exceptions.yaml @@ -93,7 +93,7 @@ packageJson: "./components/Account": "./components/Account/index.mjs" "./components/Breadcrumbs": "./components/Breadcrumbs/index.mjs" "./components/Control": "./components/Control/index.mjs" - "./components/DocusaurusPage": "./components/DocusaurusPage/index.mjs" + "./components/Docusaurus": "./components/Docusaurus/index.mjs" "./components/Editor": "./components/Editor/index.mjs" "./components/Icon": "./components/Icon/index.mjs" "./components/Input": "./components/Input/index.mjs" diff --git a/packages/react/components/Account/AccountBookmarks.mjs b/packages/react/components/Account/Bookmarks.mjs similarity index 55% rename from packages/react/components/Account/AccountBookmarks.mjs rename to packages/react/components/Account/Bookmarks.mjs index ea496e48ed0..bb5cc65a750 100644 --- a/packages/react/components/Account/AccountBookmarks.mjs +++ b/packages/react/components/Account/Bookmarks.mjs @@ -1,26 +1,36 @@ +// Dependencies +import { horFlexClasses, notEmpty } from '@freesewing/utils' // Hooks import React, { useState, useEffect, Fragment, useContext } from 'react' import { useBackend } from '@freesewing/react/hooks/useBackend' // Context import { LoadingStatusContext } from '@freesewing/react/context/LoadingStatus' +import { ModalContext } from '@freesewing/react/context/Modal' // Components -import { PlusIcon, TrashIcon, LeftIcon } from '@freesewing/react/components/Icon' +import { BookmarkIcon, LeftIcon, PlusIcon, TrashIcon } from '@freesewing/react/components/Icon' import { Link as WebLink } from '@freesewing/react/components/Link' -//import { DisplayRow } from './shared.mjs' -//import { StringInput } from 'shared/components/inputs.mjs' -//import { DynamicMdx } from 'shared/components/mdx/dynamic.mjs' +import { ModalWrapper } from '@freesewing/react/components/Modal' +import { StringInput } from '@freesewing/react/components/Input' + +/* + * Various bookmark types + */ +const types = { + design: 'Designs', + pattern: 'Patterns', + set: 'Measurements Sets', + cset: 'Curated Measurements Sets', + doc: 'Documentation', + custom: 'Custom Bookmarks', +} /** * Component for the account/bookmarks page - * - * @param {object} props - All React props - * @param {function} Link - An optional custom Link component */ -export const AccountBookmarks = ({ Link = false }) => { - if (!Link) Link = WebLink - - // Hooks +export const Bookmarks = () => { + // Hooks & Context const backend = useBackend() + const { setModal, clearModal } = useContext(ModalContext) const { setLoadingStatus, LoadingProgress } = useContext(LoadingStatusContext) // State @@ -66,12 +76,12 @@ export const AccountBookmarks = ({ Link = false }) => { await backend.removeBookmark(id) setLoadingStatus([ true, - , + , ]) } setSelected({}) setRefresh(refresh + 1) - setLoadingStatus([true, 'nailedIt', true, true]) + setLoadingStatus([true, 'Nailed it', true, true]) } const perType = {} @@ -80,13 +90,27 @@ export const AccountBookmarks = ({ Link = false }) => { return (

- + setModal( + +

+

New Bookmark

+ setRefresh(refresh + 1)} /> +
+ + ) + } > New Bookmark - +

{bookmarks.length > 0 ? (
) } -const t = (input) => input +/* + * A component to add a bookmark from wherever + * + * @params {object} props - All React props + * @params {string} props.href - The bookmark href + * @params {string} props.title - The bookmark title + * @params {string} props.type - The bookmark type + */ +export const BookmarkButton = ({ slug, type, title }) => { + const { setModal } = useContext(ModalContext) + const typeTitles = { docs: 'page' } + + return ( + + ) +} + +/* + * A component to create a bookmark, preloaded with props + * + * @params {object} props - All React props + * @params {string} props.href - The bookmark href + * @params {string} props.title - The bookmark title + * @params {string} props.type - The bookmark type + * + */ +export const CreateBookmark = ({ type, title, slug }) => { + const backend = useBackend() + const [name, setName] = useState(title) + const { setLoadingStatus } = useContext(LoadingStatusContext) + const { setModal } = useContext(ModalContext) + + const url = `/${slug}` + + const bookmark = async (evt) => { + evt.stopPropagation() + setLoadingStatus([true, 'Contacting backend']) + const [status] = await backend.createBookmark({ type, title, url }) + if (status === 201) { + setLoadingStatus([true, 'Bookmark created', true, true]) + setModal(false) + } else + setLoadingStatus([ + true, + 'Something unexpected happened, failed to create a bookmark', + true, + false, + ]) + } + + return ( +
+

New bookmark

+ + +
+ ) +} diff --git a/packages/react/components/Account/AccountLinks.mjs b/packages/react/components/Account/Links.mjs similarity index 99% rename from packages/react/components/Account/AccountLinks.mjs rename to packages/react/components/Account/Links.mjs index 4d5489927aa..26e1ccfb940 100644 --- a/packages/react/components/Account/AccountLinks.mjs +++ b/packages/react/components/Account/Links.mjs @@ -110,7 +110,7 @@ const t = (input) => input * @param {object} props - All the React props * @param {function} Link - A custom Link component, typically the Docusaurus one, but it's optional */ -export const AccountLinks = ({ Link = false }) => { +export const Links = ({ Link = false }) => { // Use custom Link component if available if (!Link) Link = DefaultLink diff --git a/packages/react/components/Account/Set.mjs b/packages/react/components/Account/Set.mjs new file mode 100644 index 00000000000..5e60685ea5e --- /dev/null +++ b/packages/react/components/Account/Set.mjs @@ -0,0 +1,1069 @@ +// Dependencies +import { measurements, isDegreeMeasurement, control as controlConfig } from '@freesewing/config' +import { + cloudflareImageUrl, + capitalize, + formatMm, + horFlexClasses, + linkClasses, + roundDistance, + shortDate, + timeAgo, +} 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 { + CloneIcon, + CuratedMeasurementsSetIcon, + EditIcon, + ShowcaseIcon, + NewMeasurementsSetIcon, + NoIcon, + OkIcon, + PlusIcon, + ResetIcon, + TrashIcon, + UploadIcon, + // WarningIcon, + // BoolYesIcon, + // BoolNoIcon, +} from '@freesewing/react/components/Icon' +import { BookmarkButton, MsetCard } from '@freesewing/react/components/Account' +import { ToggleInput } from '@freesewing/react/components/Input' +import { DisplayRow } from './shared.mjs' +import Markdown from 'react-markdown' +import { ModalWrapper } from '@freesewing/react/components/Modal' + +//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 { +// 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' + +/* + * Component to show an individual measurements set + * + * @param {object} props - All React props + * @param {number} id - The ID of the measurements set to load + * @param {bool} publicOnly - FIXME + * @param {function} Link - An optional framework-specific Link component to use for client-side routing + */ +export const Set = ({ id, publicOnly = false, Link = false }) => { + if (!Link) Link = WebLink + + // Hooks + const { account, control } = useAccount() + const { setLoadingStatus } = useContext(LoadingStatusContext) + const backend = useBackend() + + // 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, 'Contacting the backend']) + const [status, body] = await backend.getSet(id) + if (status === 200 && body.result === 'success') { + setMset(body.set) + setName(body.set.name) + setImage(body.set.image) + setIsPublic(body.set.public ? true : false) + setImperial(body.set.imperial ? true : false) + setNotes(body.set.notes) + setMeasies(body.set.measies) + setLoadingStatus([true, 'Measurements set loaded', true, true]) + } else setLoadingStatus([true, 'An error occured while contacting the backend', true, false]) + } + const getPublicSet = async () => { + setLoadingStatus([true, 'Contacting the backend']) + const [status, body] = await backend.getPublicSet(id) + if (status === 200 && body.result === 'success') { + setMset({ + ...body.data, + public: true, + measies: body.data.measurements, + }) + setName(body.data.name) + setImage(body.data.image) + setIsPublic(body.data.public ? true : false) + setImperial(body.data.imperial ? true : false) + setNotes(body.data.notes) + setMeasies(body.data.measurements) + setLoadingStatus([true, 'Measurements set loaded', true, true]) + } else + setLoadingStatus([ + true, + 'An error occured while loading this measurements set', + true, + false, + ]) + } + if (id) { + if (publicOnly) getPublicSet() + else getSet() + } + }, [id, publicOnly]) + + const filterMeasurements = () => { + if (!filter) return measurements.map((m) => `measurements:${m}` + `|${m}`).sort() + else return designMeasurements[filter].map((m) => `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, 'Gathering info']) + // 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, 'Saving measurements set']) + const [status, body] = await backend.updateSet(mset.id, data) + if (status === 200 && body.result === 'success') { + setMset(body.data.set) + setEdit(false) + setLoadingStatus([true, 'Nailed it', true, true]) + } else + setLoadingStatus([true, 'That did not go as planned. Saving the set failed.', true, false]) + } + + const togglePublic = async () => { + setLoadingStatus([true, 'Getting ready']) + const [status, body] = await backend.updateSet(mset.id, { public: !mset.public }) + if (status === 200 && body.result === 'success') { + setMset(body.set) + setLoadingStatus([true, 'Alright, done!', true, true]) + } else setLoadingStatus([true, 'Backend says no :(', true, false]) + } + + const importSet = async () => { + setLoadingStatus([true, 'Importing data']) + // Compile data + const data = { + ...mset, + userId: account.id, + measies: { ...mset.measies }, + } + delete data.img + const [status, body] = await backend.createSet(data) + if (status === 200 && body.result === 'success') { + setMset(body.data.set) + setEdit(false) + setLoadingStatus([true, 'Nailed it', true, true]) + } else setLoadingStatus([true, 'We failed to create this measurements set', true, false]) + } + + const heading = ( + <> +
+
+ +
+
+ {account.control > 2 && mset.public && mset.userId !== account.id ? ( +
+ + JSON + + + YAML + +
+ ) : ( + + )} + {account.control > 3 && mset.userId === account.id ? ( +
+ + +
+ ) : ( + + )} + {account.id && account.control > 2 && mset.public && mset.userId !== account.id ? ( + + ) : null} + {account.control > 2 ? ( + + ) : null} + + {!publicOnly && ( + <> + {account.control > 2 ? ( + + ) : null} + {edit ? ( + <> + + + + ) : ( + + )} + + )} + {account.control > 2 && mset.userId === account.id ? ( + + ) : null} +
+
+
+ + ) + + if (suggest) + return ( +
+ {heading} + +
+ ) + + if (!edit) + return ( +
+ {heading} + +

Data

+ {mset.name} + {mset.imperial ? 'Imperial' : 'Metric'} + {control >= controlConfig.account.sets.notes && ( + + {mset.notes} + + )} + {control >= controlConfig.account.sets.public && ( + <> + {mset.userId === account.id && ( + +
+ {mset.public ? ( + + ) : ( + + )} + +
+
+ )} + {mset.public && ( + + {`/set?id=${mset.id}`} + + )} + + )} + {control >= controlConfig.account.sets.createdAt && ( + + {timeAgo(mset.createdAt, false)} + ({shortDate(mset.createdAt, false)}) + + )} + {control >= controlConfig.account.sets.updatedAt && ( + + {timeAgo(mset.updatedAt, false)} + ({shortDate(mset.updatedAt, false)}) + + )} + {control >= controlConfig.account.sets.id && {mset.id}} + + {Object.keys(mset.measies).length > 0 && ( + <> +

Measurements

+ setDisplayAsMetric(!displayAsMetric)} + current={displayAsMetric} + /> + {Object.entries(mset.measies).map(([m, val]) => + val > 0 ? ( + } + key={m} + > + {m} + + ) : null + )} + + )} +
+ ) + + return ( +
+ {heading} +
    + {['measies', 'data'].map((s) => ( +
  • + {s} +
  • + ))} +
      +
    • + Name +
    • + {account.control >= conf.account.sets.img ? ( +
    • + Image +
    • + ) : null} + {['public', 'units', 'notes'].map((id) => + account.control >= conf.account.sets[id] ? ( +
    • + {id} +
    • + ) : null + )} +
    +
+ +

{t('measies')}

+
+ Clear filter} + /> +
+ {filterMeasurements().map((mplus) => { + const [translated, m] = mplus.split('|') + + return ( + + ) + })} + +

Data

+ + {/* Name is always shown */} + + val && val.length > 0} + /> + + {/* img: Control level determines whether or not to show this */} + + {account.control >= conf.account.sets.img ? ( + val.length > 0} + /> + ) : null} + + {/* public: Control level determines whether or not to show this */} + + {account.control >= conf.account.sets.public ? ( + + Public measurements set + +
+ ), + desc: 'Others are allowed to use these measurements to generate or test patterns', + }, + { + val: false, + label: ( +
+ Private measurements set + +
+ ), + desc: 'These measurements cannot be used by other users or visitors', + }, + ]} + current={isPublic} + /> + ) : null} + + {/* units: Control level determines whether or not to show this */} + + {account.control >= conf.account.sets.units ? ( + <> + + Metric units (cm) + cm + + ), + desc: 'Pick this if you prefer cm over inches', + }, + { + val: true, + label: ( +
+ Imperial units (inch) + +
+ ), + desc: 'Pick this if you prefer inches over cm', + }, + ]} + current={imperial} + /> + {t('unitsMustSave')} + + ) : null} + + {/* notes: Control level determines whether or not to show this */} + + {account.control >= conf.account.sets.notes ? ( + + ) : null} + + + ) +} + +/** + * A (helper) component to display a measurements value + * + * @param {object} props - All React props + * @param {string} val - The value + * @param {string} m - The measurement name + * @param {bool} imperial - True for imperial measurements, or metric by default + */ +export const MeasurementValue = ({ val, m, imperial = false }) => + isDegreeMeasurement(m) ? ( + {val}° + ) : ( + + ) + +/* +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'} + /> +
+ +
+
+ ) +} + + +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 ( + + ) + + // 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} + /> + + + ) +} + +*/ diff --git a/packages/react/components/Account/sets.mjs b/packages/react/components/Account/Sets.mjs similarity index 87% rename from packages/react/components/Account/sets.mjs rename to packages/react/components/Account/Sets.mjs index 8a9463e6b5e..37450c53148 100644 --- a/packages/react/components/Account/sets.mjs +++ b/packages/react/components/Account/Sets.mjs @@ -1,66 +1,330 @@ -// __SDEFILE__ - This file is a dependency for the stand-alone environment // Dependencies -import { measurements } from 'config/measurements.mjs' -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' +import { measurements } from '@freesewing/config' +import { cloudflareImageUrl, capitalize } from '@freesewing/utils' // Context -import { LoadingStatusContext } from 'shared/context/loading-status-context.mjs' -import { ModalContext } from 'shared/context/modal-context.mjs' +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 { 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 { Link as WebLink } from '@freesewing/react/components/Link' import { - OkIcon, NoIcon, - TrashIcon, - EditIcon, - UploadIcon, - ResetIcon, + OkIcon, PlusIcon, - WarningIcon, - CameraIcon, - CsetIcon, - BoolYesIcon, - BoolNoIcon, - CloneIcon, -} from 'shared/components/icons.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' + TrashIcon, + UploadIcon, + // EditIcon, + // ResetIcon, + // WarningIcon, + // CameraIcon, + // CsetIcon, + // BoolYesIcon, + // BoolNoIcon, + // CloneIcon, +} from '@freesewing/react/components/Icon' -export const ns = [inputNs, 'account', 'patterns', 'status', 'measurements', 'sets'] +//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' +/* + * The component for the an account/sets page + * + * @param {object} props - All React props + * @param {function} Link - An optional framework-specific Link component + */ +export const Sets = ({ Link = false }) => { + if (!Link) Link = WebLink + + // Hooks + const { control } = useAccount() + const backend = useBackend() + const { setLoadingStatus, LoadingProgress } = useContext(LoadingStatusContext) + + // State + const [sets, setSets] = useState([]) + const [selected, setSelected] = useState({}) + const [refresh, setRefresh] = useState(0) + + // Effects + useEffect(() => { + const getSets = async () => { + const [status, body] = await backend.getSets() + if (status === 200 && body.result === 'success') setSets(body.sets) + } + getSets() + }, [refresh]) + + // Helper var to see how many are selected + const selCount = Object.keys(selected).length + + // Helper method to toggle single selection + const toggleSelect = (id) => { + const newSelected = { ...selected } + if (newSelected[id]) delete newSelected[id] + else newSelected[id] = 1 + setSelected(newSelected) + } + + // Helper method to toggle select all + const toggleSelectAll = () => { + if (selCount === sets.length) setSelected({}) + else { + const newSelected = {} + for (const set of sets) newSelected[set.id] = 1 + setSelected(newSelected) + } + } + + // Helper to delete one or more measurements sets + const removeSelectedSets = async () => { + let i = 0 + for (const id in selected) { + i++ + await backend.removeSet(id) + setLoadingStatus([ + true, + , + ]) + } + setSelected({}) + setRefresh(refresh + 1) + setLoadingStatus([true, 'Nailed it', true, true]) + } + + return ( +
+ {sets.length > 0 ? ( + <> +

+ + + Import Measurements Sets + + + + Create a new Measurements Set + +

+
+ + +
+ + ) : ( + + + Create a new Measurements Set + + )} +
+ {sets.map((set, i) => ( +
+ +
+ +
+
+ ))} +
+
+ ) +} + +/** + * React component to display a (card of a) single measurements set + * + * @param {object} props - All React props + * @param {function} Link - An optional framework-specific Link component + * @param {string} design - The designs for which to check required measurements + * @param {test} href - Where the set should link to + * @param {function} onClick - What to do when clicking on a set + * @param {object} set - The (data of the) measurements set + * @param {string} size - Size of the card + * @param {bool} useA - Whether to use an A tag or not + */ +export const MsetCard = ({ + Link = false, + design = false, + href = false, + onClick = false, + set, + size = 'lg', + useA = false, +}) => { + if (!Link) Link = WebLink + const sizes = { + lg: 96, + md: 52, + sm: 36, + } + const s = sizes[size] + + const wrapperProps = { + className: `bg-base-300 aspect-square h-${s} w-${s} mb-2 grow + mx-auto flex flex-col items-start text-center justify-between rounded-none md:rounded shadow`, + style: { + backgroundImage: `url(${cloudflareImageUrl({ type: 'w500', id: set.img })})`, + backgroundSize: 'cover', + backgroundRepeat: 'no-repeat', + backgroundPosition: '50%', + }, + } + if (!set.img || set.img === 'default-avatar') + wrapperProps.style.backgroundPosition = 'bottom right' + + let icon = + let missingMeasies = '' + let linebreak = '' + const maxLength = 75 + if (design) { + const [hasMeasies, missing] = hasRequiredMeasurements( + designMeasurements[design], + set.measies, + true + ) + const iconClasses = 'w-8 h-8 p-1 rounded-full -mt-2 -ml-2 shadow' + icon = hasMeasies ? ( + + ) : ( + + ) + if (missing.length > 0) { + const translated = missing.map((m) => { + return t(m) + }) + let missingString = t('missing') + ': ' + translated.join(', ') + if (missingString.length > maxLength) { + const lastSpace = missingString.lastIndexOf(', ', maxLength) + missingString = missingString.substring(0, lastSpace) + ', ' + t('andMore') + '...' + } + const measieClasses = 'font-normal text-xs' + missingMeasies = {missingString} + linebreak =
+ } + } + + const inner = ( + <> + {icon} + + {set.name} + {linebreak} + {missingMeasies} + + + ) + + // Is it a button with an onClick handler? + if (onClick) + return ( + + ) + + // 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 NewSet = () => { // Hooks const { setLoadingStatus } = useContext(LoadingStatusContext) @@ -117,106 +381,6 @@ export const MeasieVal = ({ val, m, imperial }) => ) -export const MsetCard = ({ - set, - onClick = false, - href = false, - useA = false, - design = false, - language = false, - size = 'lg', -}) => { - const sizes = { - lg: 96, - md: 52, - sm: 36, - } - const s = sizes[size] - const { t } = useTranslation(ns) - - const wrapperProps = { - className: `bg-base-300 aspect-square h-${s} w-${s} mb-2 - mx-auto flex flex-col items-start text-center justify-between rounded-none md:rounded shadow`, - style: { - backgroundImage: `url(${cloudflareImageUrl({ type: 'w500', id: set.img })})`, - backgroundSize: 'cover', - backgroundRepeat: 'no-repeat', - backgroundPosition: '50%', - }, - } - if (!set.img || set.img === 'default-avatar') - wrapperProps.style.backgroundPosition = 'bottom right' - - let icon = - let missingMeasies = '' - let linebreak = '' - const maxLength = 75 - if (design) { - const [hasMeasies, missing] = hasRequiredMeasurements( - designMeasurements[design], - set.measies, - true - ) - const iconClasses = 'w-8 h-8 p-1 rounded-full -mt-2 -ml-2 shadow' - icon = hasMeasies ? ( - - ) : ( - - ) - if (missing.length > 0) { - const translated = missing.map((m) => { - return t(m) - }) - let missingString = t('missing') + ': ' + translated.join(', ') - if (missingString.length > maxLength) { - const lastSpace = missingString.lastIndexOf(', ', maxLength) - missingString = missingString.substring(0, lastSpace) + ', ' + t('andMore') + '...' - } - const measieClasses = 'font-normal text-xs' - missingMeasies = {missingString} - linebreak =
- } - } - - const inner = ( - <> - {icon} - - {language ? set[`name${capitalize(language)}`] : set.name} - {linebreak} - {missingMeasies} - - - ) - - // Is it a button with an onClick handler? - if (onClick) - return ( - - ) - - // 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 Mset = ({ id, publicOnly = false }) => { // Hooks const { account, control } = useAccount() @@ -630,7 +794,7 @@ export const Mset = ({ id, publicOnly = false }) => {

{t('data')}

- {/* Name is always shown */} + {// Name is always shown //} { docs={docs.name} /> - {/* img: Control level determines whether or not to show this */} + {// img: Control level determines whether or not to show this //} {account.control >= conf.account.sets.img ? ( { /> ) : null} - {/* public: Control level determines whether or not to show this */} + {// public: Control level determines whether or not to show this //} {account.control >= conf.account.sets.public ? ( { /> ) : null} - {/* units: Control level determines whether or not to show this */} + {// units: Control level determines whether or not to show this //} {account.control >= conf.account.sets.units ? ( <> @@ -730,7 +894,7 @@ export const Mset = ({ id, publicOnly = false }) => { ) : null} - {/* notes: Control level determines whether or not to show this */} + {// notes: Control level determines whether or not to show this //} {account.control >= conf.account.sets.notes ? ( { ) } -// Component for the account/sets page -export const Sets = () => { - // Hooks - const { control } = useAccount() - const backend = useBackend() - const { t } = useTranslation(ns) - const { setLoadingStatus, LoadingProgress } = useContext(LoadingStatusContext) - - // State - const [sets, setSets] = useState([]) - const [selected, setSelected] = useState({}) - const [refresh, setRefresh] = useState(0) - - // Effects - useEffect(() => { - const getSets = async () => { - const result = await backend.getSets() - if (result.success) setSets(result.data.sets) - } - getSets() - }, [refresh]) - - // Helper var to see how many are selected - const selCount = Object.keys(selected).length - - // Helper method to toggle single selection - const toggleSelect = (id) => { - const newSelected = { ...selected } - if (newSelected[id]) delete newSelected[id] - else newSelected[id] = 1 - setSelected(newSelected) - } - - // Helper method to toggle select all - const toggleSelectAll = () => { - if (selCount === sets.length) setSelected({}) - else { - const newSelected = {} - for (const set of sets) newSelected[set.id] = 1 - setSelected(newSelected) - } - } - - // Helper to delete one or more measurements sets - const removeSelectedSets = async () => { - let i = 0 - for (const id in selected) { - i++ - await backend.removeSet(id) - setLoadingStatus([ - true, - , - ]) - } - setSelected({}) - setRefresh(refresh + 1) - setLoadingStatus([true, 'nailedIt', true, true]) - } - - return ( -
- {sets.length > 0 ? ( - <> -

- - - {t('account:importSets')} - - - - {t('newSet')} - -

-
- - -
- - ) : ( - - - {t('newSet')} - - )} -
- {sets.map((set, i) => ( -
- -
- -
-
- ))} -
- -
- ) -} - export const SetCard = ({ set, requiredMeasies = [], @@ -1251,3 +1281,5 @@ const SuggestCset = ({ mset, backend, setLoadingStatus, t }) => { ) } + +*/ diff --git a/packages/react/components/Account/index.mjs b/packages/react/components/Account/index.mjs index 6e1aa8bd841..93ff2e4b52f 100644 --- a/packages/react/components/Account/index.mjs +++ b/packages/react/components/Account/index.mjs @@ -1,6 +1,8 @@ import React from 'react' -import { AccountBookmarks } from './AccountBookmarks.mjs' -import { AccountLinks } from './AccountLinks.mjs' +import { Bookmarks, BookmarkButton } from './Bookmarks.mjs' +import { Links } from './Links.mjs' +import { Set } from './Set.mjs' +import { Sets, MsetCard } from './Sets.mjs' -export { AccountBookmarks, AccountLinks } +export { Bookmarks, BookmarkButton, Links, Set, Sets, MsetCard } diff --git a/packages/react/components/Account/shared.mjs b/packages/react/components/Account/shared.mjs index fe5036a7a50..dcdc1de67c3 100644 --- a/packages/react/components/Account/shared.mjs +++ b/packages/react/components/Account/shared.mjs @@ -1,4 +1,18 @@ -// __SDEFILE__ - This file is a dependency for the stand-alone environment +import React from 'react' + +/* + * A component to display a row of data + */ +export const DisplayRow = ({ title, children, keyWidth = 'w-24' }) => ( +
+
+ {title} +
+
{children}
+
+) + +/* import { Spinner } from 'shared/components/spinner.mjs' import Link from 'next/link' import { useTranslation } from 'next-i18next' @@ -126,11 +140,4 @@ export const welcomeSteps = { 5: [''], } -export const DisplayRow = ({ title, children, keyWidth = 'w-24' }) => ( -
-
- {title} -
-
{children}
-
-) +*/ diff --git a/packages/react/components/DocusaurusPage/index.mjs b/packages/react/components/Docusaurus/index.mjs similarity index 78% rename from packages/react/components/DocusaurusPage/index.mjs rename to packages/react/components/Docusaurus/index.mjs index a4aadf57d14..65ec9ed8c6c 100644 --- a/packages/react/components/DocusaurusPage/index.mjs +++ b/packages/react/components/Docusaurus/index.mjs @@ -32,6 +32,23 @@ export const DocusaurusPage = (props) => { ) } +/* + * This component should be the top level of a Docusaurus doc (mdx) + * where you want access to context (typically account pages and so on) + * + * This sets up the various context providers before + * passing all props down to the InnerPageWrapper. + * This is required because the context providers need to + * be setup for the modal and loading state work we do in the InnerPageWrapper + */ +export const DocusaurusDoc = (props) => ( + + + + + +) + /* * This component needs to be a child of the ContextWrapper * diff --git a/packages/react/components/Icon/index.mjs b/packages/react/components/Icon/index.mjs index ce094a3633c..107c7f811ec 100644 --- a/packages/react/components/Icon/index.mjs +++ b/packages/react/components/Icon/index.mjs @@ -92,6 +92,13 @@ export const CircleIcon = (props) => ( ) +// FIXME +export const CloneIcon = (props) => ( + + + +) + // Looks like a X export const CloseIcon = (props) => ( @@ -350,7 +357,7 @@ export const MenuIcon = (props) => ( ) // Looks like a person icon with a + sign -export const NewMsetIcon = (props) => ( +export const NewMeasurementsSetIcon = (props) => ( diff --git a/packages/react/components/Input/index.mjs b/packages/react/components/Input/index.mjs index 712e74e03b7..37ac86f8ddd 100644 --- a/packages/react/components/Input/index.mjs +++ b/packages/react/components/Input/index.mjs @@ -688,7 +688,7 @@ export const ToggleInput = ({ type="checkbox" value={current} onChange={() => update(list.indexOf(current) === 0 ? list[1] : list[0])} - className="toggle my-3 toggle-primary" + className="daisy-toggle my-3 daisy-toggle-primary" checked={list.indexOf(current) === 0 ? true : false} /> diff --git a/packages/react/components/Link/index.mjs b/packages/react/components/Link/index.mjs index c87478663bf..e8525c53aa0 100644 --- a/packages/react/components/Link/index.mjs +++ b/packages/react/components/Link/index.mjs @@ -1,10 +1,5 @@ import React from 'react' - -/* - * These classes are what makes a link a link - */ -export const linkClasses = - 'underline decoration-2 hover:decoration-4 text-secondary hover:text-secondary-focus' +import { linkClasses } from '@freesewing/utils' /** * An anchor link component @@ -28,9 +23,10 @@ export const AnchorLink = ({ children, id = '', title = false }) => ( * @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 Link = ({ href, title = false, children, className = linkClasses }) => ( - +export const Link = ({ href, title = false, children, className = linkClasses, style = {} }) => ( + {children} ) diff --git a/packages/react/components/Modal/index.mjs b/packages/react/components/Modal/index.mjs index b4c45f96c49..ff4907b9ef7 100644 --- a/packages/react/components/Modal/index.mjs +++ b/packages/react/components/Modal/index.mjs @@ -68,7 +68,7 @@ export const ModalWrapper = ({
diff --git a/packages/react/components/New/NewLinks.mjs b/packages/react/components/New/NewLinks.mjs index 16aacec8726..fff1c8da604 100644 --- a/packages/react/components/New/NewLinks.mjs +++ b/packages/react/components/New/NewLinks.mjs @@ -5,7 +5,7 @@ import { useBackend } from '@freesewing/react/hooks/useBackend' // Components import { Link as DefaultLink } from '@freesewing/react/components/Link' import { - NewMsetIcon, + NewMeasurementsSetIcon, NewPatternIcon, ShowcaseIcon, KioskIcon, @@ -43,7 +43,7 @@ const newLinks = { 'Pick a design, add your measurements set, and we will generate a bespoke sewing pattern for you.', }, set: { - Icon: NewMsetIcon, + Icon: NewMeasurementsSetIcon, title: 'Create new measurements set', description: 'Create a new set of measurements which you can then use to generate patterns for.', diff --git a/packages/react/context/LoadingStatus/index.mjs b/packages/react/context/LoadingStatus/index.mjs index ea9b28bc70b..796a408ff1d 100644 --- a/packages/react/context/LoadingStatus/index.mjs +++ b/packages/react/context/LoadingStatus/index.mjs @@ -47,7 +47,7 @@ const LoadingStatus = ({ loadingStatus }) => { } return ( -
+
{ + let negative = '' + let inches = '' + let rest = '' + if (fraction < 0) { + fraction = fraction * -1 + negative = '-' + } + if (Math.abs(fraction) < 1) rest = fraction + else { + inches = Math.floor(fraction) + rest = fraction - inches + } + let fraction128 = Math.round(rest * 128) + if (fraction128 == 0) return formatImperial(negative, inches || fraction128, false, false, format) + + for (let i = 1; i < 7; i++) { + const numoFactor = Math.pow(2, 7 - i) + if (fraction128 % numoFactor === 0) + return formatImperial(negative, inches, fraction128 / numoFactor, Math.pow(2, i), format) + } + + return ( + negative + + Math.round(fraction * 100) / 100 + + (format === 'html' || format === 'notags' ? '"' : '') + ) +} + +/* + * Format an imperial value + * + * @param {bool} neg - Whether or not to render as a negative value + * @param {number} inch - The inches + * @param {number} numo - The fration numerator + * @param {number} deno - The fration denominator + * @param {string} format - One of 'html', 'notags', or anything else for numbers only + * @return {string} formatted - The formatted value + */ +export function formatImperial(neg, inch, numo = false, deno = false, format = 'html') { + if (format === 'html') { + if (numo) return `${neg}${inch} ${numo}/${deno}"` + else return `${neg}${inch}"` + } else if (format === 'notags') { + if (numo) return `${neg}${inch} ${numo}/${deno}"` + else return `${neg}${inch}"` + } else { + if (numo) return `${neg}${inch} ${numo}/${deno}` + else return `${neg}${inch}` + } +} + +/** + * Format a value in mm, taking units into account + * + * @param {number} val - The value to format + * @param {units} units - Both 'imperial' and true will result in imperial, everything else is metric + * @param {string} format - One of 'html', 'notags', or anything else for numbers only + * @return {string} result - The formatted result + */ +export function formatMm(val, units, format = 'html') { + val = roundDistance(val) + if (units === 'imperial' || units === true) { + if (val == 0) return formatImperial('', 0, false, false, format) + + let fraction = val / 25.4 + return formatFraction128(fraction, format) + } else { + if (format === 'html' || format === 'notags') return roundDistance(val / 10) + 'cm' + else return roundDistance(val / 10) + } +} + /** convert a value that may contain a fraction to a decimal */ export function fractionToDecimal(value) { // if it's just a number, return it @@ -118,6 +205,17 @@ export function fractionToDecimal(value) { return total + num / denom } +/* + * Get search parameters from the browser + * + * @param {string} name - Name of the parameter to retrieve + * @return {string} value - Value of the parameter + */ +export function getSearchParam(name = 'id') { + if (typeof window === 'undefined') return undefined + return new URLSearchParams(window.location.search).get(name) // eslint-disable-line +} + /* * Convert a measurement to millimeter * @@ -146,6 +244,14 @@ export function measurementAsUnits(mmValue, units = 'metric') { return round(mmValue / (units === 'imperial' ? 25.4 : 10), 3) } +/* + * A method to ensure input is not empty + * + * @param {string} input - The input + * @return {bool} notEmpty - True if input is not an emtpy strign, false of not + */ +export const notEmpty = (input) => `${input}`.length > 0 + /* * Generic rounding method * @@ -156,3 +262,70 @@ export function measurementAsUnits(mmValue, units = 'metric') { export function round(val, decimals = 1) { return Math.round(val * Math.pow(10, decimals)) / Math.pow(10, decimals) } + +/* + * Rounds a value that is a distance, either mm or inch + * + * @param {number} val - The value to round + * @param {string} units - Use 'imperial' or true for imperial, anything else and you get metric + * @return {number} rounded - The rounded value + */ +export function roundDistance(val, units) { + return units === 'imperial' || units === true + ? Math.round(val * 1000000) / 1000000 + : Math.round(val * 10) / 10 +} + +/* + * A method to render a date in a way that is concise + * + * @param {number} timestamp - The timestamp to render, or current time if none is provided + * @param {bool} withTime - Set this to true to also include time (in addition to date) + * @return {string} date - The formatted date + */ +export function shortDate(timestamp = false, withTime = true) { + const options = { + year: 'numeric', + month: 'short', + day: 'numeric', + } + if (withTime) { + options.hour = '2-digit' + options.minute = '2-digit' + options.hour12 = false + } + const ts = timestamp ? new Date(timestamp) : new Date() + + return ts.toLocaleDateString('en', options) +} + +/* + * We used to use react-timeago but that's too much overhead + * This is a drop-in replacement that does not rerender + * + * @param {string/number} timestamp - The time to parse + * @return {string} timeago - How long ago it was + */ +export function timeAgo(timestamp, terse = true) { + const delta = new Date() - new Date(timestamp) + + const seconds = Math.floor(delta / 1000) + const minutes = Math.floor(seconds / 60) + const hours = Math.floor(minutes / 60) + const days = Math.floor(hours / 24) + const months = Math.floor(days / 30) + const years = Math.floor(days / 365) + const suffix = ' ago' + + if (seconds < 1) return 'Now' + if (seconds === 1) return `${terse ? '1s' : '1 second'}${suffix}` + if (seconds === 60) return `${terse ? '1m' : '1 minute'}${suffix}` + if (seconds < 91) return `${seconds}${terse ? 's' : ' seconds'}${suffix}` + if (minutes === 60) return `${terse ? '1h' : '1 hour'}${suffix}` + if (minutes < 120) return `${minutes}${terse ? 'm' : ' minutes'}${suffix}` + if (hours === 24) return `${terse ? '1d' : '1 day'}${suffix}` + if (hours < 48) return `${hours}${terse ? 'h' : ' hours'}${suffix}` + if (days < 61) return `${days}${terse ? 'd' : ' days'}${suffix}` + if (months < 25) return `${months}${terse ? 'M' : ' months'}${suffix}` + return `${years}${terse ? 'Y' : ' years'}${suffix}` +} diff --git a/sites/org/docs/account/bookmarks/index.mdx b/sites/org/docs/account/bookmarks/index.mdx index e1a4f174479..ff2c31ccefa 100644 --- a/sites/org/docs/account/bookmarks/index.mdx +++ b/sites/org/docs/account/bookmarks/index.mdx @@ -1,11 +1,15 @@ --- title: Your Bookmarks +sidebar_position: 1 --- +import { DocusaurusDoc } from '@freesewing/react/components/Docusaurus' import { RoleBlock } from '@freesewing/react/components/Role' -import { AccountBookmarks } from '@freesewing/react/components/Account' +import { Bookmarks as AccountBookmarks } from '@freesewing/react/components/Account' import Link from '@docusaurus/Link' - - - + + + + + diff --git a/sites/org/docs/account/index.mdx b/sites/org/docs/account/index.mdx index 5c33a458e1a..9585bae11ee 100644 --- a/sites/org/docs/account/index.mdx +++ b/sites/org/docs/account/index.mdx @@ -4,7 +4,7 @@ sidebar_label: Account --- import { RoleBlock } from '@freesewing/react/components/Role' -import { AccountLinks } from '@freesewing/react/components/Account' +import { Links as AccountLinks } from '@freesewing/react/components/Account' import Link from '@docusaurus/Link' diff --git a/sites/org/docs/account/set/index.mdx b/sites/org/docs/account/set/index.mdx new file mode 100644 index 00000000000..336dcfbf7cb --- /dev/null +++ b/sites/org/docs/account/set/index.mdx @@ -0,0 +1,17 @@ +--- +title: Measurement Set +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 { Set } from '@freesewing/react/components/Account' +import Link from '@docusaurus/Link' + + + + + + diff --git a/sites/org/docs/account/sets/index.mdx b/sites/org/docs/account/sets/index.mdx new file mode 100644 index 00000000000..4b679643cff --- /dev/null +++ b/sites/org/docs/account/sets/index.mdx @@ -0,0 +1,15 @@ +--- +title: Your Measurements Sets +sidebar_position: 2 +--- + +import { DocusaurusDoc } from '@freesewing/react/components/Docusaurus' +import { RoleBlock } from '@freesewing/react/components/Role' +import { Sets as AccountSets } from '@freesewing/react/components/Account' +import Link from '@docusaurus/Link' + + + + + + diff --git a/sites/org/docusaurus.config.mjs b/sites/org/docusaurus.config.mjs index d9818e99df6..e1ba653cec9 100644 --- a/sites/org/docusaurus.config.mjs +++ b/sites/org/docusaurus.config.mjs @@ -5,9 +5,12 @@ import tailwindcss from 'tailwindcss' import autoprefixer from 'autoprefixer' /* - * We bundle the options as one page, so keep them out the sidebar + * We customize the sidebar somewhat: + * - We bundle the options as one page, so keep them out the sidebar + * - We hide certain dynamic pages (like for measurements sets, patterns, and so on) */ -function hideDesignOptionsFromSidebar(items) { +function customizeSidebar(items) { + // Filter out design options const docs = items.filter((entry) => entry.label === 'Docs').pop().items for (const item in docs) { if (docs[item].label === 'FreeSewing Designs') { @@ -20,6 +23,7 @@ function hideDesignOptionsFromSidebar(items) { } } } + return items } @@ -190,7 +194,7 @@ const config = { editUrl: 'https://github.com/freesewing/freesewing/tree/v4/sites/org/', async sidebarItemsGenerator({ defaultSidebarItemsGenerator, ...args }) { const sidebarItems = await defaultSidebarItemsGenerator(args) - return hideDesignOptionsFromSidebar(sidebarItems) + return customizeSidebar(sidebarItems) }, }, theme: { diff --git a/sites/org/src/pages/oldcount/index.js b/sites/org/src/pages/oldcount/index.js deleted file mode 100644 index cd1e42e9bea..00000000000 --- a/sites/org/src/pages/oldcount/index.js +++ /dev/null @@ -1,30 +0,0 @@ -import DocusaurusLayout from '@theme/Layout' -import { DocusaurusPage } from '@freesewing/react/components/DocusaurusPage' -import { CustomSidebar } from '@site/src/components/AccountSidebar.js' - -/* - * Some things should never generated as SSR - * So for these, we run a dynamic import and disable SSR rendering -const DynamicAuthWrapper = dynamic( - () => import('shared/components/wrappers/auth/index.mjs').then((mod) => mod.AuthWrapper), - { ssr: false } -) - -const DynamicAccountOverview = dynamic( - () => import('shared/components/account/overview.mjs').then((mod) => mod.AccountOverview), - { ssr: false } -) - */ - -export default function AccountIndexPage() { - return ( - - -
account here
-
- ) -} diff --git a/sites/org/src/pages/signin/index.js b/sites/org/src/pages/signin/index.js index 560f173f920..ff626c2cd5f 100644 --- a/sites/org/src/pages/signin/index.js +++ b/sites/org/src/pages/signin/index.js @@ -1,5 +1,5 @@ import DocusaurusLayout from '@theme/Layout' -import { DocusaurusPage } from '@freesewing/react/components/DocusaurusPage' +import { DocusaurusPage } from '@freesewing/react/components/Docusaurus' import { NoTitleLayout } from '@freesewing/react/components/Layout' import { SignIn } from '@freesewing/react/components/SignIn' import { useHistory } from 'react-router-dom'