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
|
@ -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>
|
||||
)
|
||||
}
|
|
@ -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
|
||||
|
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
|
||||
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 }) => {
|
|||
</>
|
||||
)
|
||||
}
|
||||
|
||||
*/
|
|
@ -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 }
|
||||
|
|
|
@ -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>
|
||||
)
|
||||
*/
|
||||
|
|
|
@ -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
|
||||
*
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
)
|
||||
|
|
|
@ -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}
|
||||
>
|
||||
|
|
|
@ -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.',
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue