wip[org]: More work on account pages
This commit is contained in:
parent
bc584399e2
commit
3733f93e45
25 changed files with 1823 additions and 437 deletions
|
@ -93,7 +93,7 @@ packageJson:
|
||||||
"./components/Account": "./components/Account/index.mjs"
|
"./components/Account": "./components/Account/index.mjs"
|
||||||
"./components/Breadcrumbs": "./components/Breadcrumbs/index.mjs"
|
"./components/Breadcrumbs": "./components/Breadcrumbs/index.mjs"
|
||||||
"./components/Control": "./components/Control/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/Editor": "./components/Editor/index.mjs"
|
||||||
"./components/Icon": "./components/Icon/index.mjs"
|
"./components/Icon": "./components/Icon/index.mjs"
|
||||||
"./components/Input": "./components/Input/index.mjs"
|
"./components/Input": "./components/Input/index.mjs"
|
||||||
|
|
|
@ -1,26 +1,36 @@
|
||||||
|
// Dependencies
|
||||||
|
import { horFlexClasses, notEmpty } from '@freesewing/utils'
|
||||||
// Hooks
|
// Hooks
|
||||||
import React, { useState, useEffect, Fragment, useContext } from 'react'
|
import React, { useState, useEffect, Fragment, useContext } from 'react'
|
||||||
import { useBackend } from '@freesewing/react/hooks/useBackend'
|
import { useBackend } from '@freesewing/react/hooks/useBackend'
|
||||||
// Context
|
// Context
|
||||||
import { LoadingStatusContext } from '@freesewing/react/context/LoadingStatus'
|
import { LoadingStatusContext } from '@freesewing/react/context/LoadingStatus'
|
||||||
|
import { ModalContext } from '@freesewing/react/context/Modal'
|
||||||
// Components
|
// 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 { Link as WebLink } from '@freesewing/react/components/Link'
|
||||||
//import { DisplayRow } from './shared.mjs'
|
import { ModalWrapper } from '@freesewing/react/components/Modal'
|
||||||
//import { StringInput } from 'shared/components/inputs.mjs'
|
import { StringInput } from '@freesewing/react/components/Input'
|
||||||
//import { DynamicMdx } from 'shared/components/mdx/dynamic.mjs'
|
|
||||||
|
/*
|
||||||
|
* 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
|
* 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 }) => {
|
export const Bookmarks = () => {
|
||||||
if (!Link) Link = WebLink
|
// Hooks & Context
|
||||||
|
|
||||||
// Hooks
|
|
||||||
const backend = useBackend()
|
const backend = useBackend()
|
||||||
|
const { setModal, clearModal } = useContext(ModalContext)
|
||||||
const { setLoadingStatus, LoadingProgress } = useContext(LoadingStatusContext)
|
const { setLoadingStatus, LoadingProgress } = useContext(LoadingStatusContext)
|
||||||
|
|
||||||
// State
|
// State
|
||||||
|
@ -66,12 +76,12 @@ export const AccountBookmarks = ({ Link = false }) => {
|
||||||
await backend.removeBookmark(id)
|
await backend.removeBookmark(id)
|
||||||
setLoadingStatus([
|
setLoadingStatus([
|
||||||
true,
|
true,
|
||||||
<LoadingProgress val={i} max={selCount} msg={t('removingBookmarks')} key="linter" />,
|
<LoadingProgress val={i} max={selCount} msg="Removing Bookmarks" key="linter" />,
|
||||||
])
|
])
|
||||||
}
|
}
|
||||||
setSelected({})
|
setSelected({})
|
||||||
setRefresh(refresh + 1)
|
setRefresh(refresh + 1)
|
||||||
setLoadingStatus([true, 'nailedIt', true, true])
|
setLoadingStatus([true, 'Nailed it', true, true])
|
||||||
}
|
}
|
||||||
|
|
||||||
const perType = {}
|
const perType = {}
|
||||||
|
@ -80,13 +90,27 @@ export const AccountBookmarks = ({ Link = false }) => {
|
||||||
return (
|
return (
|
||||||
<div className="max-w-4xl xl:pl-4">
|
<div className="max-w-4xl xl:pl-4">
|
||||||
<p className="text-center md:text-right">
|
<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"
|
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 />
|
<PlusIcon />
|
||||||
New Bookmark
|
New Bookmark
|
||||||
</Link>
|
</button>
|
||||||
</p>
|
</p>
|
||||||
{bookmarks.length > 0 ? (
|
{bookmarks.length > 0 ? (
|
||||||
<button
|
<button
|
||||||
|
@ -129,9 +153,7 @@ export const AccountBookmarks = ({ Link = false }) => {
|
||||||
onClick={() => toggleSelect(bookmark.id)}
|
onClick={() => toggleSelect(bookmark.id)}
|
||||||
/>
|
/>
|
||||||
</td>
|
</td>
|
||||||
<td className="text-base font-medium">
|
<td className="text-base font-medium">{bookmark.title}</td>
|
||||||
<Link href={`/account/bookmark?id=${bookmark.id}`}>{bookmark.title}</Link>
|
|
||||||
</td>
|
|
||||||
<td className="text-base font-medium">
|
<td className="text-base font-medium">
|
||||||
<WebLink href={bookmark.url}>
|
<WebLink href={bookmark.url}>
|
||||||
{bookmark.url.length > 30
|
{bookmark.url.length > 30
|
||||||
|
@ -150,84 +172,58 @@ export const AccountBookmarks = ({ Link = false }) => {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const types = {
|
/*
|
||||||
design: 'Designs',
|
* Component to create a new bookmark
|
||||||
pattern: 'Patterns',
|
*
|
||||||
set: 'Measurements Sets',
|
* @param {object} props - All the React props
|
||||||
cset: 'Curated Measurements Sets',
|
* @param {function} onCreated - An optional method to call when the bookmark is created
|
||||||
doc: 'Documentation',
|
*/
|
||||||
custom: 'Custom Bookmarks',
|
export const NewBookmark = ({ onCreated = false }) => {
|
||||||
}
|
|
||||||
|
|
||||||
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 = () => {
|
|
||||||
// Hooks
|
// Hooks
|
||||||
const { setLoadingStatus } = useContext(LoadingStatusContext)
|
const { setLoadingStatus } = useContext(LoadingStatusContext)
|
||||||
const router = useRouter()
|
const { clearModal } = useContext(ModalContext)
|
||||||
const backend = useBackend()
|
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
|
// State
|
||||||
const [title, setTitle] = useState('')
|
const [title, setTitle] = useState('')
|
||||||
const [url, setUrl] = useState('')
|
const [url, setUrl] = useState('')
|
||||||
|
|
||||||
|
// This method will create the bookmark
|
||||||
const createBookmark = async () => {
|
const createBookmark = async () => {
|
||||||
setLoadingStatus([true, 'processingUpdate'])
|
setLoadingStatus([true, 'Processing update'])
|
||||||
const result = await backend.createBookmark({
|
const [status, body] = await backend.createBookmark({
|
||||||
title,
|
title,
|
||||||
url,
|
url,
|
||||||
type: 'custom',
|
type: 'custom',
|
||||||
})
|
})
|
||||||
if (result.success) {
|
if (status === 201) setLoadingStatus([true, 'Bookmark created', true, true])
|
||||||
setLoadingStatus([true, 'nailedIt', true, true])
|
else
|
||||||
router.push('/account/bookmarks')
|
setLoadingStatus([
|
||||||
} else setLoadingStatus([true, 'backendError', true, false])
|
true,
|
||||||
|
'An error occured, the bookmark was not created. Please report this.',
|
||||||
|
true,
|
||||||
|
false,
|
||||||
|
])
|
||||||
|
if (typeof onCreated === 'function') onCreated()
|
||||||
|
clearModal()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Render the form
|
||||||
return (
|
return (
|
||||||
<div className="max-w-2xl xl:pl-4">
|
<div className="max-w-2xl w-full">
|
||||||
<StringInput
|
<StringInput
|
||||||
id="bookmark-title"
|
id="bookmark-title"
|
||||||
label={t('title')}
|
label="Title"
|
||||||
docs={docs.title}
|
labelBL="The title/name of your bookmark"
|
||||||
update={setTitle}
|
update={setTitle}
|
||||||
current={title}
|
current={title}
|
||||||
valid={(val) => val.length > 0}
|
valid={(val) => val.length > 0}
|
||||||
placeholder={t('account')}
|
placeholder="Bookmark title"
|
||||||
/>
|
/>
|
||||||
<StringInput
|
<StringInput
|
||||||
id="bookmark-url"
|
id="bookmark-url"
|
||||||
label={t('location')}
|
label="Location"
|
||||||
docs={docs.location}
|
labelBL="The location/url of your bookmark"
|
||||||
update={setUrl}
|
update={setUrl}
|
||||||
current={url}
|
current={url}
|
||||||
valid={(val) => val.length > 0}
|
valid={(val) => val.length > 0}
|
||||||
|
@ -239,11 +235,82 @@ export const NewBookmark = () => {
|
||||||
disabled={!(title.length > 0 && url.length > 0)}
|
disabled={!(title.length > 0 && url.length > 0)}
|
||||||
onClick={createBookmark}
|
onClick={createBookmark}
|
||||||
>
|
>
|
||||||
{t('newBookmark')}
|
New bookmark
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</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>
|
||||||
|
)
|
||||||
|
}
|
|
@ -110,7 +110,7 @@ const t = (input) => input
|
||||||
* @param {object} props - All the React props
|
* @param {object} props - All the React props
|
||||||
* @param {function} Link - A custom Link component, typically the Docusaurus one, but it's optional
|
* @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
|
// Use custom Link component if available
|
||||||
if (!Link) Link = DefaultLink
|
if (!Link) Link = DefaultLink
|
||||||
|
|
1069
packages/react/components/Account/Set.mjs
Normal file
1069
packages/react/components/Account/Set.mjs
Normal file
File diff suppressed because it is too large
Load diff
|
@ -1,66 +1,330 @@
|
||||||
// __SDEFILE__ - This file is a dependency for the stand-alone environment
|
|
||||||
// Dependencies
|
// Dependencies
|
||||||
import { measurements } from 'config/measurements.mjs'
|
import { measurements } from '@freesewing/config'
|
||||||
import { measurements as designMeasurements } from 'shared/prebuild/data/design-measurements.mjs'
|
import { cloudflareImageUrl, capitalize } from '@freesewing/utils'
|
||||||
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
|
// Context
|
||||||
import { LoadingStatusContext } from 'shared/context/loading-status-context.mjs'
|
import { LoadingStatusContext } from '@freesewing/react/context/LoadingStatus'
|
||||||
import { ModalContext } from 'shared/context/modal-context.mjs'
|
//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
|
// Components
|
||||||
import { Popout } from 'shared/components/popout/index.mjs'
|
import { Link as WebLink } from '@freesewing/react/components/Link'
|
||||||
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 {
|
import {
|
||||||
OkIcon,
|
|
||||||
NoIcon,
|
NoIcon,
|
||||||
TrashIcon,
|
OkIcon,
|
||||||
EditIcon,
|
|
||||||
UploadIcon,
|
|
||||||
ResetIcon,
|
|
||||||
PlusIcon,
|
PlusIcon,
|
||||||
WarningIcon,
|
TrashIcon,
|
||||||
CameraIcon,
|
UploadIcon,
|
||||||
CsetIcon,
|
// EditIcon,
|
||||||
BoolYesIcon,
|
// ResetIcon,
|
||||||
BoolNoIcon,
|
// WarningIcon,
|
||||||
CloneIcon,
|
// CameraIcon,
|
||||||
} from 'shared/components/icons.mjs'
|
// CsetIcon,
|
||||||
import { ModalWrapper } from 'shared/components/wrappers/modal.mjs'
|
// BoolYesIcon,
|
||||||
import { Mdx } from 'shared/components/mdx/dynamic.mjs'
|
// BoolNoIcon,
|
||||||
import Timeago from 'react-timeago'
|
// CloneIcon,
|
||||||
import { DisplayRow } from './shared.mjs'
|
} from '@freesewing/react/components/Icon'
|
||||||
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'
|
|
||||||
|
|
||||||
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 = () => {
|
export const NewSet = () => {
|
||||||
// Hooks
|
// Hooks
|
||||||
const { setLoadingStatus } = useContext(LoadingStatusContext)
|
const { setLoadingStatus } = useContext(LoadingStatusContext)
|
||||||
|
@ -117,106 +381,6 @@ export const MeasieVal = ({ val, m, imperial }) =>
|
||||||
<span dangerouslySetInnerHTML={{ __html: formatMm(val, imperial) }}></span>
|
<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 }) => {
|
export const Mset = ({ id, publicOnly = false }) => {
|
||||||
// Hooks
|
// Hooks
|
||||||
const { account, control } = useAccount()
|
const { account, control } = useAccount()
|
||||||
|
@ -630,7 +794,7 @@ export const Mset = ({ id, publicOnly = false }) => {
|
||||||
|
|
||||||
<h2 id="data">{t('data')}</h2>
|
<h2 id="data">{t('data')}</h2>
|
||||||
|
|
||||||
{/* Name is always shown */}
|
{// Name is always shown //}
|
||||||
<span id="name"></span>
|
<span id="name"></span>
|
||||||
<StringInput
|
<StringInput
|
||||||
id="set-name"
|
id="set-name"
|
||||||
|
@ -643,7 +807,7 @@ export const Mset = ({ id, publicOnly = false }) => {
|
||||||
docs={docs.name}
|
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>
|
<span id="image"></span>
|
||||||
{account.control >= conf.account.sets.img ? (
|
{account.control >= conf.account.sets.img ? (
|
||||||
<PassiveImageInput
|
<PassiveImageInput
|
||||||
|
@ -656,7 +820,7 @@ export const Mset = ({ id, publicOnly = false }) => {
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : 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>
|
<span id="public"></span>
|
||||||
{account.control >= conf.account.sets.public ? (
|
{account.control >= conf.account.sets.public ? (
|
||||||
<ListInput
|
<ListInput
|
||||||
|
@ -693,7 +857,7 @@ export const Mset = ({ id, publicOnly = false }) => {
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : 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>
|
<span id="units"></span>
|
||||||
{account.control >= conf.account.sets.units ? (
|
{account.control >= conf.account.sets.units ? (
|
||||||
<>
|
<>
|
||||||
|
@ -730,7 +894,7 @@ export const Mset = ({ id, publicOnly = false }) => {
|
||||||
</>
|
</>
|
||||||
) : null}
|
) : 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>
|
<span id="notes"></span>
|
||||||
{account.control >= conf.account.sets.notes ? (
|
{account.control >= conf.account.sets.notes ? (
|
||||||
<MarkdownInput
|
<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 = ({
|
export const SetCard = ({
|
||||||
set,
|
set,
|
||||||
requiredMeasies = [],
|
requiredMeasies = [],
|
||||||
|
@ -1251,3 +1281,5 @@ const SuggestCset = ({ mset, backend, setLoadingStatus, t }) => {
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
*/
|
|
@ -1,6 +1,8 @@
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
|
|
||||||
import { AccountBookmarks } from './AccountBookmarks.mjs'
|
import { Bookmarks, BookmarkButton } from './Bookmarks.mjs'
|
||||||
import { AccountLinks } from './AccountLinks.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 }
|
||||||
|
|
|
@ -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 { Spinner } from 'shared/components/spinner.mjs'
|
||||||
import Link from 'next/link'
|
import Link from 'next/link'
|
||||||
import { useTranslation } from 'next-i18next'
|
import { useTranslation } from 'next-i18next'
|
||||||
|
@ -126,11 +140,4 @@ export const welcomeSteps = {
|
||||||
5: [''],
|
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>
|
|
||||||
)
|
|
||||||
|
|
|
@ -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
|
* This component needs to be a child of the ContextWrapper
|
||||||
*
|
*
|
|
@ -92,6 +92,13 @@ export const CircleIcon = (props) => (
|
||||||
</IconWrapper>
|
</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
|
// Looks like a X
|
||||||
export const CloseIcon = (props) => (
|
export const CloseIcon = (props) => (
|
||||||
<IconWrapper {...props}>
|
<IconWrapper {...props}>
|
||||||
|
@ -350,7 +357,7 @@ export const MenuIcon = (props) => (
|
||||||
)
|
)
|
||||||
|
|
||||||
// Looks like a person icon with a + sign
|
// Looks like a person icon with a + sign
|
||||||
export const NewMsetIcon = (props) => (
|
export const NewMeasurementsSetIcon = (props) => (
|
||||||
<IconWrapper {...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" />
|
<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>
|
</IconWrapper>
|
||||||
|
|
|
@ -688,7 +688,7 @@ export const ToggleInput = ({
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
value={current}
|
value={current}
|
||||||
onChange={() => update(list.indexOf(current) === 0 ? list[1] : list[0])}
|
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}
|
checked={list.indexOf(current) === 0 ? true : false}
|
||||||
/>
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
|
|
|
@ -1,10 +1,5 @@
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
|
import { linkClasses } from '@freesewing/utils'
|
||||||
/*
|
|
||||||
* These classes are what makes a link a link
|
|
||||||
*/
|
|
||||||
export const linkClasses =
|
|
||||||
'underline decoration-2 hover:decoration-4 text-secondary hover:text-secondary-focus'
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* An anchor link component
|
* 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.href - The target to link to
|
||||||
* @param {array} props.title - An optional link title
|
* @param {array} props.title - An optional link title
|
||||||
* @param {string} props.className - Any non-default CSS classes to apply
|
* @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 = {} }) => (
|
||||||
<a href={href} className={className} title={title ? title : ''}>
|
<a href={href} className={className} title={title ? title : ''} style={style}>
|
||||||
{children}
|
{children}
|
||||||
</a>
|
</a>
|
||||||
)
|
)
|
||||||
|
|
|
@ -68,7 +68,7 @@ export const ModalWrapper = ({
|
||||||
<div
|
<div
|
||||||
className={`fixed top-0 left-0 m-0 p-0 shadow w-full h-screen
|
className={`fixed top-0 left-0 m-0 p-0 shadow w-full h-screen
|
||||||
transform-all duration-150 ${animation}
|
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`}
|
flex flex-${flex} justify-${justify} items-${items} lg:p-12`}
|
||||||
onClick={close}
|
onClick={close}
|
||||||
>
|
>
|
||||||
|
|
|
@ -5,7 +5,7 @@ import { useBackend } from '@freesewing/react/hooks/useBackend'
|
||||||
// Components
|
// Components
|
||||||
import { Link as DefaultLink } from '@freesewing/react/components/Link'
|
import { Link as DefaultLink } from '@freesewing/react/components/Link'
|
||||||
import {
|
import {
|
||||||
NewMsetIcon,
|
NewMeasurementsSetIcon,
|
||||||
NewPatternIcon,
|
NewPatternIcon,
|
||||||
ShowcaseIcon,
|
ShowcaseIcon,
|
||||||
KioskIcon,
|
KioskIcon,
|
||||||
|
@ -43,7 +43,7 @@ const newLinks = {
|
||||||
'Pick a design, add your measurements set, and we will generate a bespoke sewing pattern for you.',
|
'Pick a design, add your measurements set, and we will generate a bespoke sewing pattern for you.',
|
||||||
},
|
},
|
||||||
set: {
|
set: {
|
||||||
Icon: NewMsetIcon,
|
Icon: NewMeasurementsSetIcon,
|
||||||
title: 'Create new measurements set',
|
title: 'Create new measurements set',
|
||||||
description:
|
description:
|
||||||
'Create a new set of measurements which you can then use to generate patterns for.',
|
'Create a new set of measurements which you can then use to generate patterns for.',
|
||||||
|
|
|
@ -47,7 +47,7 @@ const LoadingStatus = ({ loadingStatus }) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
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
|
<div
|
||||||
className={`w-full md:max-w-2xl m-auto bg-${color} flex flex-row items-center gap-4 p-4 px-4 ${fade}
|
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
|
transition-opacity delay-[${timeout * 1000 - 400}ms] duration-300
|
||||||
|
|
|
@ -114,7 +114,9 @@ function Backend(token) {
|
||||||
this.token = token
|
this.token = token
|
||||||
this.headers = authenticationHeaders(token)
|
this.headers = authenticationHeaders(token)
|
||||||
this.restClient = new RestClient(backend, this.headers)
|
this.restClient = new RestClient(backend, this.headers)
|
||||||
|
this.delete = this.restClient.delete
|
||||||
this.get = this.restClient.get
|
this.get = this.restClient.get
|
||||||
|
this.patch = this.restClient.patch
|
||||||
this.put = this.restClient.put
|
this.put = this.restClient.put
|
||||||
this.post = this.restClient.post
|
this.post = this.restClient.post
|
||||||
}
|
}
|
||||||
|
|
|
@ -27,6 +27,10 @@ export function RestClient(baseUrl = '', baseHeaders = {}) {
|
||||||
return withoutBody('HEAD', baseUrl + url, { ...baseHeaders, ...headers }, raw, log)
|
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) {
|
this.post = async function (url, data, headers, raw, log) {
|
||||||
return withBody('POST', baseUrl + url, data, { ...baseHeaders, ...headers }, raw, log)
|
return withBody('POST', baseUrl + url, data, { ...baseHeaders, ...headers }, raw, log)
|
||||||
}
|
}
|
||||||
|
|
|
@ -30,7 +30,7 @@
|
||||||
"./components/Account": "./components/Account/index.mjs",
|
"./components/Account": "./components/Account/index.mjs",
|
||||||
"./components/Breadcrumbs": "./components/Breadcrumbs/index.mjs",
|
"./components/Breadcrumbs": "./components/Breadcrumbs/index.mjs",
|
||||||
"./components/Control": "./components/Control/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/Editor": "./components/Editor/index.mjs",
|
||||||
"./components/Icon": "./components/Icon/index.mjs",
|
"./components/Icon": "./components/Icon/index.mjs",
|
||||||
"./components/Input": "./components/Input/index.mjs",
|
"./components/Input": "./components/Input/index.mjs",
|
||||||
|
|
|
@ -15,6 +15,12 @@ export const horFlexClasses = 'flex flex-row items-center justify-between gap-4
|
||||||
export const horFlexClassesNoSm =
|
export const horFlexClassesNoSm =
|
||||||
'md:flex md:flex-row md:items-center md:justify-between md:gap-4 md-w-full'
|
'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
|
* FUNCTIONS
|
||||||
*/
|
*/
|
||||||
|
@ -81,6 +87,87 @@ export function distanceAsMm(val = false, imperial = false) {
|
||||||
return isNaN(val) ? false : Number(val)
|
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} <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 */
|
/** convert a value that may contain a fraction to a decimal */
|
||||||
export function fractionToDecimal(value) {
|
export function fractionToDecimal(value) {
|
||||||
// if it's just a number, return it
|
// if it's just a number, return it
|
||||||
|
@ -118,6 +205,17 @@ export function fractionToDecimal(value) {
|
||||||
return total + num / denom
|
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
|
* Convert a measurement to millimeter
|
||||||
*
|
*
|
||||||
|
@ -146,6 +244,14 @@ export function measurementAsUnits(mmValue, units = 'metric') {
|
||||||
return round(mmValue / (units === 'imperial' ? 25.4 : 10), 3)
|
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
|
* Generic rounding method
|
||||||
*
|
*
|
||||||
|
@ -156,3 +262,70 @@ export function measurementAsUnits(mmValue, units = 'metric') {
|
||||||
export function round(val, decimals = 1) {
|
export function round(val, decimals = 1) {
|
||||||
return Math.round(val * Math.pow(10, decimals)) / Math.pow(10, decimals)
|
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}`
|
||||||
|
}
|
||||||
|
|
|
@ -1,11 +1,15 @@
|
||||||
---
|
---
|
||||||
title: Your Bookmarks
|
title: Your Bookmarks
|
||||||
|
sidebar_position: 1
|
||||||
---
|
---
|
||||||
|
|
||||||
|
import { DocusaurusDoc } from '@freesewing/react/components/Docusaurus'
|
||||||
import { RoleBlock } from '@freesewing/react/components/Role'
|
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'
|
import Link from '@docusaurus/Link'
|
||||||
|
|
||||||
|
<DocusaurusDoc>
|
||||||
<RoleBlock user>
|
<RoleBlock user>
|
||||||
<AccountBookmarks Link={Link} />
|
<AccountBookmarks Link={Link} />
|
||||||
</RoleBlock>
|
</RoleBlock>
|
||||||
|
</DocusaurusDoc>
|
||||||
|
|
|
@ -4,7 +4,7 @@ sidebar_label: Account
|
||||||
---
|
---
|
||||||
|
|
||||||
import { RoleBlock } from '@freesewing/react/components/Role'
|
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'
|
import Link from '@docusaurus/Link'
|
||||||
|
|
||||||
<RoleBlock user>
|
<RoleBlock user>
|
||||||
|
|
17
sites/org/docs/account/set/index.mdx
Normal file
17
sites/org/docs/account/set/index.mdx
Normal 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>
|
15
sites/org/docs/account/sets/index.mdx
Normal file
15
sites/org/docs/account/sets/index.mdx
Normal 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>
|
|
@ -5,9 +5,12 @@ import tailwindcss from 'tailwindcss'
|
||||||
import autoprefixer from 'autoprefixer'
|
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
|
const docs = items.filter((entry) => entry.label === 'Docs').pop().items
|
||||||
for (const item in docs) {
|
for (const item in docs) {
|
||||||
if (docs[item].label === 'FreeSewing Designs') {
|
if (docs[item].label === 'FreeSewing Designs') {
|
||||||
|
@ -20,6 +23,7 @@ function hideDesignOptionsFromSidebar(items) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return items
|
return items
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -190,7 +194,7 @@ const config = {
|
||||||
editUrl: 'https://github.com/freesewing/freesewing/tree/v4/sites/org/',
|
editUrl: 'https://github.com/freesewing/freesewing/tree/v4/sites/org/',
|
||||||
async sidebarItemsGenerator({ defaultSidebarItemsGenerator, ...args }) {
|
async sidebarItemsGenerator({ defaultSidebarItemsGenerator, ...args }) {
|
||||||
const sidebarItems = await defaultSidebarItemsGenerator(args)
|
const sidebarItems = await defaultSidebarItemsGenerator(args)
|
||||||
return hideDesignOptionsFromSidebar(sidebarItems)
|
return customizeSidebar(sidebarItems)
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
theme: {
|
theme: {
|
||||||
|
|
|
@ -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>
|
|
||||||
)
|
|
||||||
}
|
|
|
@ -1,5 +1,5 @@
|
||||||
import DocusaurusLayout from '@theme/Layout'
|
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 { NoTitleLayout } from '@freesewing/react/components/Layout'
|
||||||
import { SignIn } from '@freesewing/react/components/SignIn'
|
import { SignIn } from '@freesewing/react/components/SignIn'
|
||||||
import { useHistory } from 'react-router-dom'
|
import { useHistory } from 'react-router-dom'
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue