1
0
Fork 0

wip[org]: More work on account pages

This commit is contained in:
joostdecock 2024-12-15 17:54:25 +01:00
parent bc584399e2
commit 3733f93e45
25 changed files with 1823 additions and 437 deletions

View file

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

View file

@ -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,
<LoadingProgress val={i} max={selCount} msg={t('removingBookmarks')} key="linter" />,
<LoadingProgress val={i} max={selCount} msg="Removing Bookmarks" key="linter" />,
])
}
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 (
<div className="max-w-4xl xl:pl-4">
<p className="text-center md:text-right">
<Link
<button
className="daisy-btn daisy-btn-primary capitalize w-full md:w-auto hover:text-primary-content hover:no-underline"
href="/new/bookmark"
onClick={() =>
setModal(
<ModalWrapper
flex="col"
justify="top lg:justify-center"
slideFrom="right"
keepOpenOnClick
>
<div className="w-full max-w-xl">
<h2>New Bookmark</h2>
<NewBookmark onCreated={() => setRefresh(refresh + 1)} />
</div>
</ModalWrapper>
)
}
>
<PlusIcon />
New Bookmark
</Link>
</button>
</p>
{bookmarks.length > 0 ? (
<button
@ -129,9 +153,7 @@ export const AccountBookmarks = ({ Link = false }) => {
onClick={() => toggleSelect(bookmark.id)}
/>
</td>
<td className="text-base font-medium">
<Link href={`/account/bookmark?id=${bookmark.id}`}>{bookmark.title}</Link>
</td>
<td className="text-base font-medium">{bookmark.title}</td>
<td className="text-base font-medium">
<WebLink href={bookmark.url}>
{bookmark.url.length > 30
@ -150,84 +172,58 @@ export const AccountBookmarks = ({ Link = false }) => {
)
}
const types = {
design: 'Designs',
pattern: 'Patterns',
set: 'Measurements Sets',
cset: 'Curated Measurements Sets',
doc: 'Documentation',
custom: 'Custom Bookmarks',
}
export const Bookmark = ({ bookmark }) => {
const { t } = useTranslation(ns)
return bookmark ? (
<div>
<DisplayRow title={t('title')}>{bookmark.title}</DisplayRow>
<DisplayRow title={t('url')}>
{bookmark.url.length > 30 ? bookmark.url.slice(0, 30) + '...' : bookmark.url}
</DisplayRow>
<DisplayRow title={t('type')}>{t(`${bookmark.type}Bookmark`)}</DisplayRow>
<div className="flex flex-row flex-wrap md:gap-2 md:items-center md:justify-between mt-8">
<Link
href="/account/bookmarks"
className="w-full md:w-auto daisy-btn daisy-btn-secondary pr-6 flex flex-row items-center gap-2"
>
<LeftIcon />
{t('bookmarks')}
</Link>
</div>
</div>
) : null
}
// Component for the 'new/bookmark' page
export const NewBookmark = () => {
/*
* Component to create a new bookmark
*
* @param {object} props - All the React props
* @param {function} onCreated - An optional method to call when the bookmark is created
*/
export const NewBookmark = ({ onCreated = false }) => {
// Hooks
const { setLoadingStatus } = useContext(LoadingStatusContext)
const router = useRouter()
const { clearModal } = useContext(ModalContext)
const backend = useBackend()
const { t, i18n } = useTranslation(ns)
const docs = {}
for (const option of ['title', 'location', 'type']) {
docs[option] = (
<DynamicMdx language={i18n.language} slug={`docs/about/site/bookmarks/${option}`} />
)
}
// State
const [title, setTitle] = useState('')
const [url, setUrl] = useState('')
// This method will create the bookmark
const createBookmark = async () => {
setLoadingStatus([true, 'processingUpdate'])
const result = await backend.createBookmark({
setLoadingStatus([true, 'Processing update'])
const [status, body] = await backend.createBookmark({
title,
url,
type: 'custom',
})
if (result.success) {
setLoadingStatus([true, 'nailedIt', true, true])
router.push('/account/bookmarks')
} else setLoadingStatus([true, 'backendError', true, false])
if (status === 201) setLoadingStatus([true, 'Bookmark created', true, true])
else
setLoadingStatus([
true,
'An error occured, the bookmark was not created. Please report this.',
true,
false,
])
if (typeof onCreated === 'function') onCreated()
clearModal()
}
// Render the form
return (
<div className="max-w-2xl xl:pl-4">
<div className="max-w-2xl w-full">
<StringInput
id="bookmark-title"
label={t('title')}
docs={docs.title}
label="Title"
labelBL="The title/name of your bookmark"
update={setTitle}
current={title}
valid={(val) => val.length > 0}
placeholder={t('account')}
placeholder="Bookmark title"
/>
<StringInput
id="bookmark-url"
label={t('location')}
docs={docs.location}
label="Location"
labelBL="The location/url of your bookmark"
update={setUrl}
current={url}
valid={(val) => val.length > 0}
@ -239,11 +235,82 @@ export const NewBookmark = () => {
disabled={!(title.length > 0 && url.length > 0)}
onClick={createBookmark}
>
{t('newBookmark')}
New bookmark
</button>
</div>
</div>
)
}
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 (
<button
className={`daisy-btn daisy-btn-secondary daisy-btn-outline ${horFlexClasses}`}
onClick={() =>
setModal(
<ModalWrapper flex="col" justify="top lg:justify-center" slideFrom="right">
<CreateBookmark {...{ type, title, slug }} />
</ModalWrapper>
)
}
>
<BookmarkIcon />
<span>Bookmark this {typeTitles[type] ? typeTitles[type] : type}</span>
</button>
)
}
/*
* 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 (
<div className="mt-12">
<h2>New bookmark</h2>
<StringInput label="Title" current={name} update={setName} valid={notEmpty} labelBL={url} />
<button className="daisy-btn daisy-btn-primary w-full mt-4" onClick={bookmark}>
Create bookmark
</button>
</div>
)
}

View file

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

File diff suppressed because it is too large Load diff

View file

@ -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,
<LoadingProgress val={i} max={selCount} msg="Removing measurements sets" key="linter" />,
])
}
setSelected({})
setRefresh(refresh + 1)
setLoadingStatus([true, 'Nailed it', true, true])
}
return (
<div className="max-w-7xl xl:pl-4">
{sets.length > 0 ? (
<>
<p className="text-center md:text-right">
<Link
className="daisy-btn daisy-btn-primary daisy-btn-outline capitalize w-full md:w-auto mr-2 mb-2 hover:no-underline hover:text-primary-content"
bottom
primary
href="/account/import"
>
<UploadIcon />
Import Measurements Sets
</Link>
<Link
className="daisy-btn daisy-btn-primary capitalize w-full md:w-auto hover:no-underline hover:text-primary-content"
bottom
primary
href="/new/set"
>
<PlusIcon />
Create a new Measurements Set
</Link>
</p>
<div className="flex flex-row gap-2 border-b-2 mb-4 pb-4 mt-8 h-14 items-center">
<input
type="checkbox"
className="checkbox checkbox-secondary"
onClick={toggleSelectAll}
checked={sets.length === selCount}
/>
<button
className="daisy-btn daisy-btn-error"
onClick={removeSelectedSets}
disabled={selCount < 1}
>
<TrashIcon /> {selCount} Measurements Sets
</button>
</div>
</>
) : (
<Link
className="daisy-btn daisy-btn-primary capitalize w-full md:w-auto btn-lg hover:no-underline hover:text-primary-content"
bottom
primary
href="/new/set"
>
<PlusIcon />
Create a new Measurements Set
</Link>
)}
<div className="grid grid-cols-2 lg:grid-cols-4 gap-2">
{sets.map((set, i) => (
<div
key={i}
className={`flex flex-row items-start gap-1 border-2
${
selected[set.id] ? 'border-solid border-secondary' : 'border-dotted border-base-300'
} rounded-lg p-2`}
>
<label className="w-8 h-full shrink-0">
<input
type="checkbox"
checked={selected[set.id] ? true : false}
className="daisy-checkbox daisy-checkbox-secondary"
onClick={() => toggleSelect(set.id)}
/>
</label>
<div className="w-full">
<MsetCard control={control} href={`/account/set?id=${set.id}`} set={set} size="md" />
</div>
</div>
))}
</div>
</div>
)
}
/**
* 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 = <span></span>
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 ? (
<OkIcon className={`${iconClasses} bg-success text-success-content`} stroke={4} />
) : (
<NoIcon className={`${iconClasses} bg-error text-error-content`} stroke={3} />
)
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 = <span className={`${measieClasses}`}>{missingString}</span>
linebreak = <br />
}
}
const inner = (
<>
{icon}
<span className="bg-neutral text-neutral-content px-4 w-full bg-opacity-50 py-2 rounded rounded-t-none font-bold leading-5">
{set.name}
{linebreak}
{missingMeasies}
</span>
</>
)
// Is it a button with an onClick handler?
if (onClick)
return (
<button {...wrapperProps} onClick={() => onClick(set)}>
{inner}
</button>
)
// Returns a link to an internal page
if (href && !useA)
return (
<Link {...wrapperProps} href={href}>
{inner}
</Link>
)
// Returns a link to an external page
if (href && useA)
return (
<a {...wrapperProps} href={href}>
{inner}
</a>
)
// Returns a div
return <div {...wrapperProps}>{inner}</div>
}
/*
export const NewSet = () => {
// Hooks
const { setLoadingStatus } = useContext(LoadingStatusContext)
@ -117,106 +381,6 @@ export const MeasieVal = ({ val, m, imperial }) =>
<span dangerouslySetInnerHTML={{ __html: formatMm(val, imperial) }}></span>
)
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 = <span></span>
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 ? (
<OkIcon className={`${iconClasses} bg-success text-success-content`} stroke={4} />
) : (
<NoIcon className={`${iconClasses} bg-error text-error-content`} stroke={3} />
)
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 = <span className={`${measieClasses}`}>{missingString}</span>
linebreak = <br />
}
}
const inner = (
<>
{icon}
<span className="bg-neutral text-neutral-content px-4 w-full bg-opacity-50 py-2 rounded rounded-t-none font-bold leading-5">
{language ? set[`name${capitalize(language)}`] : set.name}
{linebreak}
{missingMeasies}
</span>
</>
)
// Is it a button with an onClick handler?
if (onClick)
return (
<button {...wrapperProps} onClick={() => onClick(set)}>
{inner}
</button>
)
// Returns a link to an internal page
if (href && !useA)
return (
<Link {...wrapperProps} href={href}>
{inner}
</Link>
)
// Returns a link to an external page
if (href && useA)
return (
<a {...wrapperProps} href={href}>
{inner}
</a>
)
// Returns a div
return <div {...wrapperProps}>{inner}</div>
}
export const Mset = ({ id, publicOnly = false }) => {
// Hooks
const { account, control } = useAccount()
@ -630,7 +794,7 @@ export const Mset = ({ id, publicOnly = false }) => {
<h2 id="data">{t('data')}</h2>
{/* Name is always shown */}
{// Name is always shown //}
<span id="name"></span>
<StringInput
id="set-name"
@ -643,7 +807,7 @@ export const Mset = ({ id, publicOnly = false }) => {
docs={docs.name}
/>
{/* img: Control level determines whether or not to show this */}
{// img: Control level determines whether or not to show this //}
<span id="image"></span>
{account.control >= conf.account.sets.img ? (
<PassiveImageInput
@ -656,7 +820,7 @@ export const Mset = ({ id, publicOnly = false }) => {
/>
) : null}
{/* public: Control level determines whether or not to show this */}
{// public: Control level determines whether or not to show this //}
<span id="public"></span>
{account.control >= conf.account.sets.public ? (
<ListInput
@ -693,7 +857,7 @@ export const Mset = ({ id, publicOnly = false }) => {
/>
) : null}
{/* units: Control level determines whether or not to show this */}
{// units: Control level determines whether or not to show this //}
<span id="units"></span>
{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 //}
<span id="notes"></span>
{account.control >= conf.account.sets.notes ? (
<MarkdownInput
@ -753,140 +917,6 @@ export const Mset = ({ id, publicOnly = false }) => {
)
}
// 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,
<LoadingProgress val={i} max={selCount} msg={t('removingSets')} key="linter" />,
])
}
setSelected({})
setRefresh(refresh + 1)
setLoadingStatus([true, 'nailedIt', true, true])
}
return (
<div className="max-w-7xl xl:pl-4">
{sets.length > 0 ? (
<>
<p className="text-center md:text-right">
<Link
className="btn btn-primary btn-outline capitalize w-full md:w-auto mr-2"
bottom
primary
href="/account/import"
>
<UploadIcon />
{t('account:importSets')}
</Link>
<Link
className="btn btn-primary capitalize w-full md:w-auto"
bottom
primary
href="/new/set"
>
<PlusIcon />
{t('newSet')}
</Link>
</p>
<div className="flex flex-row gap-2 border-b-2 mb-4 pb-4 mt-8 h-14 items-center">
<input
type="checkbox"
className="checkbox checkbox-secondary"
onClick={toggleSelectAll}
checked={sets.length === selCount}
/>
<button className="btn btn-error" onClick={removeSelectedSets} disabled={selCount < 1}>
<TrashIcon /> {selCount} {t('sets')}
</button>
</div>
</>
) : (
<Link
className="btn btn-primary capitalize w-full md:w-auto btn-lg"
bottom
primary
href="/new/set"
>
<PlusIcon />
{t('newSet')}
</Link>
)}
<div className="flex flex-row flex-wrap gap-2">
{sets.map((set, i) => (
<div
key={i}
className={`flex flex-row items-start gap-1 border-2
${
selected[set.id] ? 'border-solid border-secondary' : 'border-dotted border-base-300'
} rounded-lg p-2`}
>
<label className="w-8 h-full shrink-0">
<input
type="checkbox"
checked={selected[set.id] ? true : false}
className="checkbox checkbox-secondary"
onClick={() => toggleSelect(set.id)}
/>
</label>
<div className="w-full">
<MsetCard control={control} href={`/account/set?id=${set.id}`} set={set} size="md" />
</div>
</div>
))}
</div>
<BackToAccountButton />
</div>
)
}
export const SetCard = ({
set,
requiredMeasies = [],
@ -1251,3 +1281,5 @@ const SuggestCset = ({ mset, backend, setLoadingStatus, t }) => {
</>
)
}
*/

View file

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

View file

@ -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' }) => (
<div className="flex flex-row flex-wrap items-center lg:gap-4 my-2 w-full">
<div className={`${keyWidth} text-left md:text-right block md:inline font-bold pr-4 shrink-0`}>
{title}
</div>
<div className="grow">{children}</div>
</div>
)
/*
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' }) => (
<div className="flex flex-row flex-wrap items-center lg:gap-4 my-2 w-full">
<div className={`${keyWidth} text-left md:text-right block md:inline font-bold pr-4 shrink-0`}>
{title}
</div>
<div className="grow">{children}</div>
</div>
)
*/

View file

@ -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) => (
<ModalContextProvider>
<LoadingStatusContextProvider>
<InnerDocusaurusPage {...props} Layout={false} />
</LoadingStatusContextProvider>
</ModalContextProvider>
)
/*
* This component needs to be a child of the ContextWrapper
*

View file

@ -92,6 +92,13 @@ export const CircleIcon = (props) => (
</IconWrapper>
)
// FIXME
export const CloneIcon = (props) => (
<IconWrapper {...props}>
<path d="M15.75 17.25v3.375c0 .621-.504 1.125-1.125 1.125h-9.75a1.125 1.125 0 01-1.125-1.125V7.875c0-.621.504-1.125 1.125-1.125H6.75a9.06 9.06 0 011.5.124m7.5 10.376h3.375c.621 0 1.125-.504 1.125-1.125V11.25c0-4.46-3.243-8.161-7.5-8.876a9.06 9.06 0 00-1.5-.124H9.375c-.621 0-1.125.504-1.125 1.125v3.5m7.5 10.375H9.375a1.125 1.125 0 01-1.125-1.125v-9.25m12 6.625v-1.875a3.375 3.375 0 00-3.375-3.375h-1.5a1.125 1.125 0 01-1.125-1.125v-1.5a3.375 3.375 0 00-3.375-3.375H9.75" />
</IconWrapper>
)
// Looks like a X
export const CloseIcon = (props) => (
<IconWrapper {...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) => (
<IconWrapper {...props}>
<path d="M19 7.5v3m0 0v3m0-3h3m-3 0h-3m-2.25-4.125a3.375 3.375 0 11-6.75 0 3.375 3.375 0 016.75 0zM4 19.235v-.11a6.375 6.375 0 0112.75 0v.109A12.318 12.318 0 0110.374 21c-2.331 0-4.512-.645-6.374-1.766z" />
</IconWrapper>

View file

@ -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}
/>
</FormControl>

View file

@ -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 }) => (
<a href={href} className={className} title={title ? title : ''}>
export const Link = ({ href, title = false, children, className = linkClasses, style = {} }) => (
<a href={href} className={className} title={title ? title : ''} style={style}>
{children}
</a>
)

View file

@ -68,7 +68,7 @@ export const ModalWrapper = ({
<div
className={`fixed top-0 left-0 m-0 p-0 shadow w-full h-screen
transform-all duration-150 ${animation}
bg-${bg} bg-opacity-${bgOpacity} z-50 hover:cursor-pointer
bg-${bg} bg-opacity-${bgOpacity} z-40 hover:cursor-pointer
flex flex-${flex} justify-${justify} items-${items} lg:p-12`}
onClick={close}
>

View file

@ -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.',

View file

@ -47,7 +47,7 @@ const LoadingStatus = ({ loadingStatus }) => {
}
return (
<div className="fixed top-0 md:top-28 left-0 w-full z-30 md:px-4 md:mx-auto">
<div className="fixed bottom-14 md:top-28 left-0 w-full z-50 md:px-4 md:mx-auto">
<div
className={`w-full md:max-w-2xl m-auto bg-${color} flex flex-row items-center gap-4 p-4 px-4 ${fade}
transition-opacity delay-[${timeout * 1000 - 400}ms] duration-300

View file

@ -114,7 +114,9 @@ function Backend(token) {
this.token = token
this.headers = authenticationHeaders(token)
this.restClient = new RestClient(backend, this.headers)
this.delete = this.restClient.delete
this.get = this.restClient.get
this.patch = this.restClient.patch
this.put = this.restClient.put
this.post = this.restClient.post
}

View file

@ -27,6 +27,10 @@ export function RestClient(baseUrl = '', baseHeaders = {}) {
return withoutBody('HEAD', baseUrl + url, { ...baseHeaders, ...headers }, raw, log)
}
this.patch = async function (url, data, headers, raw, log) {
return withBody('PATCH', baseUrl + url, data, { ...baseHeaders, ...headers }, raw, log)
}
this.post = async function (url, data, headers, raw, log) {
return withBody('POST', baseUrl + url, data, { ...baseHeaders, ...headers }, raw, log)
}

View file

@ -30,7 +30,7 @@
"./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",

View file

@ -15,6 +15,12 @@ export const horFlexClasses = 'flex flex-row items-center justify-between gap-4
export const horFlexClassesNoSm =
'md:flex md:flex-row md:items-center md:justify-between md:gap-4 md-w-full'
/*
* These classes are what makes a link a link
*/
export const linkClasses =
'underline decoration-2 hover:decoration-4 text-secondary hover:text-secondary-focus'
/*
* FUNCTIONS
*/
@ -81,6 +87,87 @@ export function distanceAsMm(val = false, imperial = false) {
return isNaN(val) ? false : Number(val)
}
/**
* Format a number using fractions, typically used for imperial
*
* @param {number} fraction - the value to process
* @param {string} format - One of
* fraction: the value to process
* format: one of the the type of formatting to apply. html, notags, or anything else which will only return numbers
*/
export const formatFraction128 = (fraction, format = 'html') => {
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}&nbsp;<sup>${numo}</sup>/<sub>${deno}</sub>"`
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}`
}

View file

@ -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'
<RoleBlock user>
<DocusaurusDoc>
<RoleBlock user>
<AccountBookmarks Link={Link} />
</RoleBlock>
</RoleBlock>
</DocusaurusDoc>

View file

@ -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'
<RoleBlock user>

View file

@ -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'
<DocusaurusDoc>
<RoleBlock user>
<Set Link={Link} id={getSearchParam('id')} />
</RoleBlock>
</DocusaurusDoc>

View file

@ -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'
<DocusaurusDoc>
<RoleBlock user>
<AccountSets Link={Link} />
</RoleBlock>
</DocusaurusDoc>

View file

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

View file

@ -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 (
<DocusaurusPage
DocusaurusLayout={DocusaurusLayout}
title="Account"
description="Sign In to your FreeSewing account to unlock all features"
>
<CustomSidebar />
<pre>account here</pre>
</DocusaurusPage>
)
}

View file

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