Merge branch 'develop' into i18n
This commit is contained in:
commit
1fd323e109
77 changed files with 2410 additions and 1832 deletions
|
@ -1,60 +1,50 @@
|
|||
import { useState } from 'react'
|
||||
|
||||
/*
|
||||
* DaisyUI's accordion seems rather unreliable.
|
||||
* So instead, we handle this in React state
|
||||
*/
|
||||
export const Accordion = ({
|
||||
const getProps = (active, i) => ({
|
||||
className: `p-2 px-4 rounded-lg bg-transparent shadow
|
||||
w-full mt-2 py-4 h-auto content-start text-left bg-opacity-20
|
||||
${active === i ? 'hover:bg-transparent' : 'hover:bg-secondary hover:bg-opacity-10'}`,
|
||||
})
|
||||
|
||||
const getSubProps = (active, i) => ({
|
||||
className: ` p-2 px-4 rounded bg-transparent w-full mt-2 py-4 h-auto
|
||||
content-start bg-secondary text-left bg-opacity-20
|
||||
${
|
||||
active === i
|
||||
? 'bg-opacity-100 hover:bg-transparent shadow'
|
||||
: 'hover:bg-opacity-10 hover:bg-secondary '
|
||||
}`,
|
||||
})
|
||||
|
||||
const BaseAccordion = ({
|
||||
items, // Items in the accordion
|
||||
propsGetter = getProps, // Method to get the relevant props
|
||||
}) => {
|
||||
const [active, setActive] = useState()
|
||||
|
||||
return (
|
||||
<nav>
|
||||
{items.map((item, i) => (
|
||||
<button
|
||||
key={i}
|
||||
className={`
|
||||
p-2 px-4 rounded-lg bg-transparent shadow
|
||||
w-full mt-2 py-4 h-auto content-start text-left bg-opacity-20
|
||||
${active === i ? 'hover:bg-transparent' : 'hover:bg-secondary hover:bg-opacity-10'}
|
||||
`}
|
||||
onClick={() => setActive(i)}
|
||||
>
|
||||
{item[0]}
|
||||
{active === i ? item[1] : null}
|
||||
</button>
|
||||
))}
|
||||
{items.map((item, i) =>
|
||||
active === i ? (
|
||||
<div key={i} {...propsGetter(active, i)}>
|
||||
<button onClick={setActive} className="w-full">
|
||||
{item[0]}
|
||||
</button>
|
||||
{item[1]}
|
||||
</div>
|
||||
) : (
|
||||
<button key={i} {...getProps(active, i)} onClick={() => setActive(i)}>
|
||||
{item[0]}
|
||||
</button>
|
||||
)
|
||||
)}
|
||||
</nav>
|
||||
)
|
||||
}
|
||||
|
||||
export const SubAccordion = ({
|
||||
items, // Items in the accordion
|
||||
}) => {
|
||||
const [active, setActive] = useState()
|
||||
|
||||
return (
|
||||
<nav>
|
||||
{items.map((item, i) => (
|
||||
<button
|
||||
key={i}
|
||||
className={`
|
||||
p-2 px-4 rounded
|
||||
bg-transparent
|
||||
w-full mt-2 py-4 h-auto content-start bg-secondary
|
||||
text-left bg-opacity-20
|
||||
${
|
||||
active === i
|
||||
? 'bg-opacity-100 hover:bg-transparent shadow'
|
||||
: 'hover:bg-opacity-10 hover:bg-secondary '
|
||||
}
|
||||
`}
|
||||
onClick={() => setActive(i)}
|
||||
>
|
||||
{item[0]}
|
||||
{active === i ? item[1] : null}
|
||||
</button>
|
||||
))}
|
||||
</nav>
|
||||
)
|
||||
}
|
||||
export const SubAccordion = (props) => <BaseAccordion {...props} propsGetter={getProps} />
|
||||
export const Accordion = (props) => <BaseAccordion {...props} propsGetter={getSubProps} />
|
||||
|
|
|
@ -23,7 +23,9 @@ export const Bookmark = ({ bookmark }) => {
|
|||
return bookmark ? (
|
||||
<div>
|
||||
<DisplayRow title={t('title')}>{bookmark.title}</DisplayRow>
|
||||
<DisplayRow title={t('url')}>{bookmark.url}</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
|
||||
|
|
|
@ -5,7 +5,7 @@ import { useTranslation } from 'next-i18next'
|
|||
import Link from 'next/link'
|
||||
import { freeSewingConfig as conf, controlLevels } from 'shared/config/freesewing.config.mjs'
|
||||
import {
|
||||
MeasieIcon,
|
||||
MsetIcon,
|
||||
SignoutIcon,
|
||||
UserIcon,
|
||||
UnitsIcon,
|
||||
|
@ -34,9 +34,10 @@ import {
|
|||
ExportIcon,
|
||||
CloseIcon,
|
||||
ReloadIcon,
|
||||
OkIcon,
|
||||
NoIcon,
|
||||
PageIcon,
|
||||
PatternIcon,
|
||||
BoolYesIcon,
|
||||
BoolNoIcon,
|
||||
} from 'shared/components/icons.mjs'
|
||||
import { cloudflareImageUrl, capitalize } from 'shared/utils.mjs'
|
||||
import { ControlScore } from 'shared/components/control/score.mjs'
|
||||
|
@ -45,8 +46,8 @@ export const ns = ['account', 'i18n']
|
|||
|
||||
const itemIcons = {
|
||||
bookmarks: <BookmarkIcon />,
|
||||
sets: <MeasieIcon />,
|
||||
patterns: <PageIcon />,
|
||||
sets: <MsetIcon />,
|
||||
patterns: <PatternIcon />,
|
||||
apikeys: <KeyIcon />,
|
||||
username: <UserIcon />,
|
||||
email: <EmailIcon />,
|
||||
|
@ -74,7 +75,7 @@ const itemClasses = 'flex flex-row items-center justify-between bg-opacity-10 p-
|
|||
|
||||
const AccountLink = ({ href, title, children }) => (
|
||||
<Link
|
||||
className={`${itemClasses} hover:bg-secondary hover:bg-opacity-10`}
|
||||
className={`${itemClasses} hover:bg-secondary hover:bg-opacity-10 max-w-md`}
|
||||
href={href}
|
||||
title={title}
|
||||
>
|
||||
|
@ -82,12 +83,7 @@ const AccountLink = ({ href, title, children }) => (
|
|||
</Link>
|
||||
)
|
||||
|
||||
const YesNo = ({ check }) =>
|
||||
check ? (
|
||||
<OkIcon className="text-success w-6 h-6" stroke={4} />
|
||||
) : (
|
||||
<NoIcon className="text-error w-6 h-6" stroke={3} />
|
||||
)
|
||||
const YesNo = ({ check }) => (check ? <BoolYesIcon /> : <BoolNoIcon />)
|
||||
|
||||
export const AccountLinks = () => {
|
||||
const { account, signOut, control } = useAccount()
|
||||
|
@ -137,12 +133,7 @@ export const AccountLinks = () => {
|
|||
consent: <YesNo check={account.consent} />,
|
||||
control: <ControlScore control={account.control} />,
|
||||
github: account.data.githubUsername || account.data.githubEmail || <NoIcon />,
|
||||
password:
|
||||
account.passwordType === 'v3' ? (
|
||||
<OkIcon className="text-success w-6 h-6" stroke={4} />
|
||||
) : (
|
||||
<NoIcon />
|
||||
),
|
||||
password: account.passwordType === 'v3' ? <BoolYesIcon /> : <NoIcon />,
|
||||
mfa: <YesNo check={false} />,
|
||||
}
|
||||
for (const social of Object.keys(conf.account.fields.identities).filter((i) => i !== 'github'))
|
||||
|
@ -182,7 +173,7 @@ export const AccountLinks = () => {
|
|||
</AccountLink>
|
||||
)
|
||||
)}
|
||||
<div className={`${itemClasses} bg-neutral`}>
|
||||
<div className={`${itemClasses} bg-neutral max-w-md`}>
|
||||
<div className="flex flex-row items-center gap-3 font-medium">
|
||||
<FingerprintIcon />
|
||||
<span>{t('userId')}</span>
|
||||
|
|
|
@ -1,606 +0,0 @@
|
|||
// Dependencies
|
||||
import { useState, useEffect, useContext, useCallback } from 'react'
|
||||
import { useTranslation } from 'next-i18next'
|
||||
import orderBy from 'lodash.orderby'
|
||||
import { capitalize } from 'shared/utils.mjs'
|
||||
import { freeSewingConfig as conf } from 'shared/config/freesewing.config.mjs'
|
||||
// Hooks
|
||||
import { useDropzone } from 'react-dropzone'
|
||||
import { useAccount } from 'shared/hooks/use-account.mjs'
|
||||
import { useBackend } from 'shared/hooks/use-backend.mjs'
|
||||
import { useToast } from 'shared/hooks/use-toast.mjs'
|
||||
import { useRouter } from 'next/router'
|
||||
// Context
|
||||
import { LoadingContext } from 'shared/context/loading-context.mjs'
|
||||
import { ModalContext } from 'shared/context/modal-context.mjs'
|
||||
// Components
|
||||
import { PageLink, Link } from 'shared/components/link.mjs'
|
||||
import { Collapse, MimicCollapseLink } from 'shared/components/collapse.mjs'
|
||||
import { BackToAccountButton, Choice } from './shared.mjs'
|
||||
import {
|
||||
OkIcon,
|
||||
NoIcon,
|
||||
TrashIcon,
|
||||
SettingsIcon,
|
||||
DownloadIcon,
|
||||
PageIcon,
|
||||
} from 'shared/components/icons.mjs'
|
||||
import { ModalWrapper } from 'shared/components/wrappers/modal.mjs'
|
||||
import Markdown from 'react-markdown'
|
||||
import { Tab } from './bio.mjs'
|
||||
import Timeago from 'react-timeago'
|
||||
import { Spinner } from 'shared/components/spinner.mjs'
|
||||
|
||||
export const ns = ['account', 'patterns', 'toast']
|
||||
|
||||
export const StandAloneNewSet = () => {
|
||||
const { t } = useTranslation(['account'])
|
||||
const toast = useToast()
|
||||
const { account } = useAccount()
|
||||
const backend = useBackend()
|
||||
|
||||
return (
|
||||
<div className="max-w-xl">
|
||||
<NewSet {...{ t, account, backend, toast }} title={false} standalone={true} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export const NewSet = ({
|
||||
t,
|
||||
refresh,
|
||||
closeCollapseButton,
|
||||
backend,
|
||||
toast,
|
||||
title = true,
|
||||
standalone = false,
|
||||
}) => {
|
||||
// Context
|
||||
const { startLoading, stopLoading } = useContext(LoadingContext)
|
||||
|
||||
// Hooks
|
||||
const router = useRouter()
|
||||
|
||||
// State
|
||||
const [name, setName] = useState('')
|
||||
|
||||
// Helper method to create a new set
|
||||
const createSet = async () => {
|
||||
startLoading()
|
||||
const result = await backend.createSet({
|
||||
name,
|
||||
})
|
||||
if (result.success) {
|
||||
toast.success(<span>{t('nailedIt')}</span>)
|
||||
if (standalone) router.push('/account/sets/')
|
||||
else {
|
||||
refresh()
|
||||
closeCollapseButton()
|
||||
}
|
||||
} else toast.for.backendError()
|
||||
stopLoading()
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
{title ? <h2>{t('newSet')}</h2> : null}
|
||||
<h5>{t('name')}</h5>
|
||||
<p>{t('setNameDesc')}</p>
|
||||
<input
|
||||
autoFocus
|
||||
value={name}
|
||||
onChange={(evt) => setName(evt.target.value)}
|
||||
className="input w-full input-bordered flex flex-row"
|
||||
type="text"
|
||||
placeholder={'Georg Cantor'}
|
||||
/>
|
||||
<div className="flex flex-row gap-2 items-center w-full mt-8 mb-2">
|
||||
<button
|
||||
className="btn btn-primary grow capitalize"
|
||||
disabled={name.length < 1}
|
||||
onClick={createSet}
|
||||
>
|
||||
{t('newSet')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const EditField = (props) => {
|
||||
if (props.field === 'name') return <EditName {...props} />
|
||||
if (props.field === 'notes') return <EditNotes {...props} />
|
||||
if (props.field === 'public') return <EditPublic {...props} />
|
||||
if (props.field === 'img') return <EditImg {...props} />
|
||||
|
||||
return <p>FIXME: No edit component for this field</p>
|
||||
}
|
||||
|
||||
export const EditRow = (props) => (
|
||||
<Collapse
|
||||
color="secondary"
|
||||
openTitle={props.title}
|
||||
title={
|
||||
<>
|
||||
<div className="w-24 text-left md:text-right block md:inline font-bold pr-4">
|
||||
{props.title}
|
||||
</div>
|
||||
<div className="grow">{props.children}</div>
|
||||
</>
|
||||
}
|
||||
>
|
||||
<EditField field="name" {...props} />
|
||||
</Collapse>
|
||||
)
|
||||
|
||||
const EditImg = ({ t, pattern, account, backend, toast, refresh }) => {
|
||||
const [img, setImg] = useState(pattern.img)
|
||||
const { startLoading, stopLoading } = useContext(LoadingContext)
|
||||
|
||||
const onDrop = useCallback((acceptedFiles) => {
|
||||
const reader = new FileReader()
|
||||
reader.onload = () => {
|
||||
setImg(reader.result)
|
||||
}
|
||||
acceptedFiles.forEach((file) => reader.readAsDataURL(file))
|
||||
}, [])
|
||||
|
||||
const { getRootProps, getInputProps } = useDropzone({ onDrop })
|
||||
|
||||
const save = async () => {
|
||||
startLoading()
|
||||
const result = await backend.updatePattern(pattern.id, { img })
|
||||
if (result.success) {
|
||||
toast.for.settingsSaved()
|
||||
refresh()
|
||||
} else toast.for.backendError()
|
||||
stopLoading()
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div>
|
||||
<img
|
||||
alt="img"
|
||||
src={img || account.img}
|
||||
className="shadow mb-4 mask mask-squircle bg-neutral aspect-square"
|
||||
/>
|
||||
<div
|
||||
{...getRootProps()}
|
||||
className={`
|
||||
flex rounded-lg w-full flex-col items-center justify-center
|
||||
lg:h-64 lg:border-4 lg:border-secondary lg:border-dashed
|
||||
`}
|
||||
>
|
||||
<input {...getInputProps()} />
|
||||
<p className="hidden lg:block p-0 m-0">{t('imgDragAndDropImageHere')}</p>
|
||||
<p className="hidden lg:block p-0 my-2">{t('or')}</p>
|
||||
<button className={`btn btn-secondary btn-outline mt-4 px-8`}>
|
||||
{t('imgSelectImage')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-row gap-2 items-center justify-center mt-2">
|
||||
<button className="btn btn-secondary" onClick={save}>
|
||||
{t('save')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const EditName = ({ t, pattern, backend, toast, refresh }) => {
|
||||
const [value, setValue] = useState(pattern.name)
|
||||
const { loading, startLoading, stopLoading } = useContext(LoadingContext)
|
||||
|
||||
const update = async (evt) => {
|
||||
evt.preventDefault()
|
||||
if (evt.target.value !== value) {
|
||||
setValue(evt.target.value)
|
||||
}
|
||||
}
|
||||
|
||||
const save = async () => {
|
||||
startLoading()
|
||||
const result = await backend.updatePattern(pattern.id, { name: value })
|
||||
if (result.success) {
|
||||
refresh()
|
||||
toast.for.settingsSaved()
|
||||
} else toast.for.backendError()
|
||||
stopLoading()
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-col lg:flex-row gap-2">
|
||||
<input
|
||||
value={value}
|
||||
onChange={update}
|
||||
className="input w-full input-bordered flex flex-row"
|
||||
type="text"
|
||||
placeholder={t('name')}
|
||||
/>
|
||||
<button className="btn btn-secondary" onClick={save} disabled={value === pattern.name}>
|
||||
<span className="flex flex-row items-center gap-2">
|
||||
{loading ? (
|
||||
<>
|
||||
<Spinner />
|
||||
<span>{t('processing')}</span>
|
||||
</>
|
||||
) : (
|
||||
t('save')
|
||||
)}
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const EditNotes = ({ t, pattern, backend, toast, refresh }) => {
|
||||
const [value, setValue] = useState(pattern.notes)
|
||||
const [activeTab, setActiveTab] = useState('edit')
|
||||
const { loading, startLoading, stopLoading } = useContext(LoadingContext)
|
||||
|
||||
const update = async (evt) => {
|
||||
evt.preventDefault()
|
||||
if (evt.target.value !== value) {
|
||||
setValue(evt.target.value)
|
||||
}
|
||||
}
|
||||
|
||||
const save = async () => {
|
||||
startLoading()
|
||||
const result = await backend.updatePattern(pattern.id, { notes: value })
|
||||
if (result.success) {
|
||||
refresh()
|
||||
toast.for.settingsSaved()
|
||||
} else toast.for.backendError()
|
||||
stopLoading()
|
||||
}
|
||||
|
||||
// Shared props for tabs
|
||||
const tabProps = { activeTab, setActiveTab, t }
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="tabs w-full">
|
||||
<Tab id="edit" {...tabProps} />
|
||||
<Tab id="preview" {...tabProps} />
|
||||
</div>
|
||||
<div className="flex flex-row items-center mt-4">
|
||||
{activeTab === 'edit' ? (
|
||||
<textarea
|
||||
rows="5"
|
||||
className="textarea textarea-bordered textarea-lg w-full"
|
||||
placeholder={t('placeholder')}
|
||||
onChange={update}
|
||||
value={value}
|
||||
/>
|
||||
) : (
|
||||
<div className="text-left px-4 border w-full">
|
||||
<Markdown>{value}</Markdown>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="my-2 flex gap-2 items-center justify-center">
|
||||
<button className="btn btn-secondary" onClick={save} disabled={value === pattern.notes}>
|
||||
<span className="flex flex-row items-center gap-2">
|
||||
{loading ? (
|
||||
<>
|
||||
<Spinner />
|
||||
<span>{t('processing')}</span>
|
||||
</>
|
||||
) : (
|
||||
t('save')
|
||||
)}
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const EditPublic = ({ t, pattern, backend, toast, refresh }) => {
|
||||
const [selection, setSelection] = useState(pattern.public)
|
||||
const { startLoading, stopLoading } = useContext(LoadingContext)
|
||||
|
||||
const update = async (val) => {
|
||||
setSelection(val)
|
||||
if (val !== pattern.public) {
|
||||
startLoading()
|
||||
const result = await backend.updatePattern(pattern.id, { public: val })
|
||||
if (result.success) {
|
||||
refresh()
|
||||
toast.for.settingsSaved()
|
||||
} else toast.for.backendError()
|
||||
stopLoading()
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{[true, false].map((val) => (
|
||||
<Choice val={val} t={t} update={update} current={selection} bool key={val}>
|
||||
<div className="flex flex-row gap-2 text-lg leading-5 items-center">
|
||||
{val ? (
|
||||
<>
|
||||
<OkIcon className="w-6 h-6 text-success" /> <span>{t('publicPattern')}</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<NoIcon className="w-6 h-6 text-error" /> <span>{t('privatePattern')}</span>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex flex-row gap-2 text-normal font-light normal-case pt-1 items-center">
|
||||
{val ? t('publicPatternDesc') : t('privatePatternDesc')}
|
||||
</div>
|
||||
</Choice>
|
||||
))}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export const EditSectionTitle = ({ title }) => (
|
||||
<h5 className="border border-solid border-b-2 border-r-0 border-l-0 border-t-0 border-primary mt-4 mb-2">
|
||||
{title}
|
||||
</h5>
|
||||
)
|
||||
|
||||
const EditPattern = (props) => {
|
||||
const { account, pattern, t } = props
|
||||
|
||||
return (
|
||||
<div className="p-2 lg:p-4">
|
||||
{/* Meta info */}
|
||||
{account.control > 2 ? (
|
||||
<div className="flex flex-row gap-2 text-sm items-center justify-center mb-2">
|
||||
<div className="flex flex-row gap-2 items-center">
|
||||
<b>{t('permalink')}:</b>
|
||||
{pattern.public ? (
|
||||
<PageLink href={`/patterns/${pattern.id}`} txt={`/patterns/${pattern.id}`} />
|
||||
) : (
|
||||
<NoIcon className="w-4 h-4 text-error" />
|
||||
)}
|
||||
</div>
|
||||
<div>
|
||||
<b>{t('created')}</b>: <Timeago date={pattern.createdAt} />
|
||||
</div>
|
||||
<div>
|
||||
<b>{t('updated')}</b>: <Timeago date={pattern.updatedAt} />
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{/* JSON & YAML links */}
|
||||
{account.control > 3 ? (
|
||||
<div className="flex flex-row gap-2 text-sm items-center justify-center">
|
||||
<a
|
||||
className="badge badge-secondary font-bold"
|
||||
href={`${conf.backend}/patterns/${pattern.id}.json`}
|
||||
>
|
||||
JSON
|
||||
</a>
|
||||
<a
|
||||
className="badge badge-success font-bold"
|
||||
href={`${conf.backend}/patterns/${pattern.id}.yaml`}
|
||||
>
|
||||
YAML
|
||||
</a>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<EditSectionTitle title={t('data')} />
|
||||
|
||||
{/* Name is always shown */}
|
||||
<EditRow title={t('name')} field="name" {...props}>
|
||||
{pattern.name}
|
||||
</EditRow>
|
||||
|
||||
{/* img: Control level determines whether or not to show this */}
|
||||
{account.control >= conf.account.patterns.img ? (
|
||||
<EditRow title={t('image')} field="img" {...props}>
|
||||
<img src={pattern.img} className="w-10 mask mask-squircle bg-neutral aspect-square" />
|
||||
</EditRow>
|
||||
) : null}
|
||||
|
||||
{/* public: Control level determines whether or not to show this */}
|
||||
{account.control >= conf.account.patterns.public ? (
|
||||
<EditRow title={t('public')} field="public" {...props}>
|
||||
<div className="flex flex-row gap-2">
|
||||
{pattern.public ? (
|
||||
<>
|
||||
<OkIcon className="h-6 w-6 text-success" /> <span>{t('publicPattern')}</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<NoIcon className="h-6 w-6 text-error" /> <span>{t('privatePattern')}</span>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</EditRow>
|
||||
) : null}
|
||||
|
||||
{/* notes: Control level determines whether or not to show this */}
|
||||
{account.control >= conf.account.patterns.notes ? (
|
||||
<EditRow title={t('notes')} field="notes" {...props}>
|
||||
<Markdown>{pattern.notes}</Markdown>
|
||||
</EditRow>
|
||||
) : null}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const Pattern = ({ pattern, t, account, backend, refresh }) => {
|
||||
// Context
|
||||
const { startLoading, stopLoading } = useContext(LoadingContext)
|
||||
const { setModal } = useContext(ModalContext)
|
||||
|
||||
// Hooks
|
||||
const toast = useToast()
|
||||
|
||||
const remove = async () => {
|
||||
startLoading()
|
||||
const result = await backend.removePattern(pattern.id)
|
||||
if (result) toast.success(t('gone'))
|
||||
else toast.for.backendError()
|
||||
// This just forces a refresh of the list from the server
|
||||
refresh()
|
||||
stopLoading()
|
||||
}
|
||||
|
||||
const removeModal = () => {
|
||||
setModal(
|
||||
<ModalWrapper slideFrom="top">
|
||||
<h2>{t('areYouCertain')}</h2>
|
||||
<p>{t('deleteSetWarning')}</p>
|
||||
<p className="flex flex-row gap-4 items-center justify-center">
|
||||
<button className="btn btn-neutral btn-outline px-8">{t('cancel')}</button>
|
||||
<button className="btn btn-error px-8" onClick={remove}>
|
||||
{t('delete')}
|
||||
</button>
|
||||
</p>
|
||||
</ModalWrapper>
|
||||
)
|
||||
}
|
||||
|
||||
const title = (
|
||||
<>
|
||||
<img src={pattern.img} className="w-10 mask mask-squircle bg-neutral aspect-square" />
|
||||
<div className="flex flex-col lg:flex-row lg:flex-wrap lg:gap-2 lg:justify-between w-full">
|
||||
{pattern.set?.img && (
|
||||
<img src={pattern.set.img} className="w-10 mask mask-squircle bg-neutral aspect-square" />
|
||||
)}
|
||||
{pattern.cset?.img && (
|
||||
<img
|
||||
src={pattern.cset.img}
|
||||
className="w-10 mask mask-squircle bg-neutral aspect-square"
|
||||
/>
|
||||
)}
|
||||
<b>{capitalize(pattern.design)}</b>
|
||||
<span>{pattern.name}</span>
|
||||
{pattern.set?.name && <span>{pattern.set.name}</span>}
|
||||
{pattern.cset?.nameEn && <span>{pattern.cset.nameEn}</span>}
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
|
||||
return (
|
||||
<>
|
||||
<MimicCollapseLink
|
||||
href={`/patterns/${pattern.id}`}
|
||||
className="block lg:hidden"
|
||||
title={title}
|
||||
/>
|
||||
<Collapse
|
||||
primary
|
||||
top
|
||||
className="hidden lg:flex"
|
||||
title={title}
|
||||
openTitle={`${capitalize(pattern.design)} | ${pattern.name}`}
|
||||
buttons={[
|
||||
<Link
|
||||
key="view"
|
||||
className="btn btn-success hover:text-success-content border-0 hidden lg:flex"
|
||||
href={`/patterns/${pattern.id}`}
|
||||
title={t('showPattern')}
|
||||
>
|
||||
<PageIcon />
|
||||
</Link>,
|
||||
<Link
|
||||
key="edit"
|
||||
className="btn btn-secondary hover:text-secondary-content border-0 hidden lg:flex"
|
||||
href={`/patterns/${pattern.id}/edit#view="draft"`}
|
||||
title={t('removePattern')}
|
||||
>
|
||||
<SettingsIcon />
|
||||
</Link>,
|
||||
<Link
|
||||
key="export"
|
||||
className="btn btn-primary hover:text-primary-content border-0 hidden lg:flex"
|
||||
href={`/patterns/${pattern.id}/edit#view="export"`}
|
||||
title={t('removePattern')}
|
||||
>
|
||||
<DownloadIcon />
|
||||
</Link>,
|
||||
<button
|
||||
key="rm"
|
||||
className="btn btn-error hover:text-error-content border-0"
|
||||
onClick={account.control > 4 ? remove : removeModal}
|
||||
title={t('removePattern')}
|
||||
>
|
||||
<TrashIcon />
|
||||
</button>,
|
||||
]}
|
||||
>
|
||||
<EditPattern {...{ t, pattern, account, backend, toast, refresh, setModal }} />
|
||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-2">
|
||||
<Link
|
||||
key="view"
|
||||
className="btn btn-success w-full"
|
||||
href={`/patterns/${pattern.id}`}
|
||||
title={t('showPattern')}
|
||||
>
|
||||
<PageIcon />
|
||||
<span className="pl-2">{t('showPattern')}</span>
|
||||
</Link>
|
||||
<Link
|
||||
key="draft"
|
||||
className="btn btn-secondary w-full"
|
||||
href={`/patterns/${pattern.id}/edit#view="draft"`}
|
||||
title={t('draftPattern')}
|
||||
>
|
||||
<SettingsIcon />
|
||||
<span className="pl-2">{t('draftPattern')}</span>
|
||||
</Link>
|
||||
<Link
|
||||
key="download"
|
||||
className="btn btn-primary w-full"
|
||||
href={`/patterns/${pattern.id}/edit#view="export"`}
|
||||
title={t('exportPattern')}
|
||||
>
|
||||
<DownloadIcon />
|
||||
<span className="pl-2">{t('exportPattern')}</span>
|
||||
</Link>
|
||||
</div>
|
||||
</Collapse>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
// Component for the account/patterns page
|
||||
export const Patterns = ({ standAlone = false }) => {
|
||||
// Hooks
|
||||
const { account } = useAccount()
|
||||
const backend = useBackend()
|
||||
const { t } = useTranslation(ns)
|
||||
|
||||
// State
|
||||
const [patterns, setPatterns] = useState([])
|
||||
const [changes, setChanges] = useState(0)
|
||||
|
||||
// Effects
|
||||
useEffect(() => {
|
||||
const getPatterns = async () => {
|
||||
const result = await backend.getPatterns()
|
||||
if (result.success) setPatterns(result.data.patterns)
|
||||
}
|
||||
getPatterns()
|
||||
}, [changes])
|
||||
|
||||
// Helper method to force a refresh
|
||||
const refresh = () => {
|
||||
setChanges(changes + 1)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="max-w-4xl xl:pl-4">
|
||||
{orderBy(patterns, ['name'], ['asc']).map((pattern) => (
|
||||
<Pattern {...{ account, pattern, t, backend, refresh }} key={pattern.id} />
|
||||
))}
|
||||
<Link href="/new/pattern" className="btn btn-primary w-full capitalize mt-4">
|
||||
{t('createANewPattern')}
|
||||
</Link>
|
||||
{standAlone ? null : <BackToAccountButton />}
|
||||
</div>
|
||||
)
|
||||
}
|
File diff suppressed because it is too large
Load diff
|
@ -289,6 +289,7 @@ export const Mset = ({ id, publicOnly = false }) => {
|
|||
setLoadingStatus([true, 'nailedIt', true, true])
|
||||
} else setLoadingStatus([true, 'backendError', true, false])
|
||||
}
|
||||
|
||||
const heading = (
|
||||
<>
|
||||
<LoadingStatus />
|
||||
|
@ -410,11 +411,11 @@ export const Mset = ({ id, publicOnly = false }) => {
|
|||
{shortDate(i18n.language, mset.createdAt, false)}
|
||||
</DisplayRow>
|
||||
)}
|
||||
{control >= controlLevels.sets.createdAt && (
|
||||
{control >= controlLevels.sets.updatedAt && (
|
||||
<DisplayRow title={t('updated')}>
|
||||
<Timeago date={mset.updatedAt} />
|
||||
<span className="px-2 opacity-50">|</span>
|
||||
{shortDate(i18n.language, mset.createdAt, false)}
|
||||
{shortDate(i18n.language, mset.updatedAt, false)}
|
||||
</DisplayRow>
|
||||
)}
|
||||
{control >= controlLevels.sets.id && <DisplayRow title={t('id')}>{mset.id}</DisplayRow>}
|
||||
|
@ -695,7 +696,7 @@ export const Sets = () => {
|
|||
</button>
|
||||
) : null}
|
||||
</div>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 2xl:grid-cols-3 gap-2">
|
||||
<div className="flex flex-row flex-wrap gap-2">
|
||||
{sets.map((set, i) => (
|
||||
<div
|
||||
key={i}
|
||||
|
@ -713,7 +714,7 @@ export const Sets = () => {
|
|||
/>
|
||||
</label>
|
||||
<div className="w-full">
|
||||
<MsetCard control={control} href={`/account/sets/${set.id}`} set={set} />
|
||||
<MsetCard control={control} href={`/account/sets/${set.id}`} set={set} size="md" />
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
|
@ -883,10 +884,11 @@ export const UserSetPicker = ({ design, t, href, clickHandler, size = 'lg' }) =>
|
|||
)
|
||||
}
|
||||
|
||||
export const CuratedSetPicker = ({ design, language, href, clickHandler, size }) => {
|
||||
export const CuratedSetPicker = ({ design, href, clickHandler, size }) => {
|
||||
// Hooks
|
||||
const backend = useBackend()
|
||||
const { t, i18n } = useTranslation('sets')
|
||||
const { language } = i18n
|
||||
const { control } = useAccount()
|
||||
|
||||
// State
|
||||
|
|
|
@ -4,6 +4,7 @@ import { Spinner } from 'shared/components/spinner.mjs'
|
|||
import { MdxWrapper } from './wrapper.mjs'
|
||||
import { components } from 'shared/components/mdx/index.mjs'
|
||||
|
||||
const orgComponents = components()
|
||||
export const loaders = {
|
||||
en: (path) => import(`orgmarkdown/docs/${path}/en.md`),
|
||||
de: (path) => import(`orgmarkdown/docs/${path}/de.md`),
|
||||
|
@ -34,7 +35,7 @@ function DynamicDocs({ path, lang }) {
|
|||
|
||||
return (
|
||||
<MdxWrapper {...frontmatter} path={path} language={lang}>
|
||||
<MDX components={components} />
|
||||
<MDX components={orgComponents} />
|
||||
</MdxWrapper>
|
||||
)
|
||||
}
|
||||
|
|
84
sites/shared/components/gdpr/form.mjs
Normal file
84
sites/shared/components/gdpr/form.mjs
Normal file
|
@ -0,0 +1,84 @@
|
|||
// Hooks
|
||||
import { useState } from 'react'
|
||||
import { useTranslation } from 'next-i18next'
|
||||
// Components
|
||||
import { Popout } from 'shared/components/popout/index.mjs'
|
||||
import { Link } from 'shared/components/link.mjs'
|
||||
import { GdprAccountDetails, ns as gdprNs } from 'shared/components/gdpr/details.mjs'
|
||||
|
||||
export const ns = ['gdpr', gdprNs]
|
||||
|
||||
const Checkbox = ({ value, setter, label, children = null }) => (
|
||||
<div
|
||||
className={`form-control p-4 hover:cursor-pointer rounded border-l-8 my-2
|
||||
${value ? 'border-success bg-success' : 'border-error bg-error'}
|
||||
bg-opacity-10 shadow`}
|
||||
onClick={() => setter(value ? false : true)}
|
||||
>
|
||||
<div className="form-control flex flex-row items-center gap-2">
|
||||
<input
|
||||
type="checkbox"
|
||||
className="checkbox"
|
||||
checked={value ? 'checked' : ''}
|
||||
onChange={() => setter(value ? false : true)}
|
||||
/>
|
||||
<span className="label-text">{label}</span>
|
||||
</div>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
|
||||
export const ConsentForm = ({ submit }) => {
|
||||
// State
|
||||
const [details, setDetails] = useState(false)
|
||||
const [consent1, setConsent1] = useState(false)
|
||||
const [consent2, setConsent2] = useState(false)
|
||||
|
||||
// Hooks
|
||||
const { t } = useTranslation(ns)
|
||||
|
||||
const giveConsent = () => {
|
||||
setConsent1(true)
|
||||
setConsent2(true)
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<h1>{t('gdpr:privacyMatters')}</h1>
|
||||
<p>{t('gdpr:compliant')}</p>
|
||||
<p>{t('gdpr:consentWhyAnswer')}</p>
|
||||
<h5 className="mt-8">{t('gdpr:accountQuestion')}</h5>
|
||||
{details ? <GdprAccountDetails /> : null}
|
||||
{consent1 ? (
|
||||
<>
|
||||
<Checkbox value={consent1} setter={setConsent1} label={t('gdpr:yesIDo')} />
|
||||
<Checkbox value={consent2} setter={setConsent2} label={t('gdpr:openDataQuestion')} />
|
||||
</>
|
||||
) : (
|
||||
<button className="btn btn-primary btn-lg w-full mt-4" onClick={giveConsent}>
|
||||
{t('gdpr:clickHere')}
|
||||
</button>
|
||||
)}
|
||||
{consent1 && !consent2 ? <Popout note>{t('openDataInfo')}</Popout> : null}
|
||||
<p className="text-center">
|
||||
<button className="btn btn-neutral btn-ghost btn-sm" onClick={() => setDetails(!details)}>
|
||||
{t(details ? 'gdpr:hideDetails' : 'gdpr:showDetails')}
|
||||
</button>
|
||||
</p>
|
||||
{!consent1 && <Popout note>{t('gdpr:noConsentNoAccountCreation')}</Popout>}
|
||||
{consent1 && (
|
||||
<button
|
||||
onClick={() => submit({ consent1, consent2 })}
|
||||
className="btn btn-lg w-full mt-8 btn-primary"
|
||||
>
|
||||
<span>{t('gdpr:createAccount')}</span>
|
||||
</button>
|
||||
)}
|
||||
<p className="text-center opacity-50 mt-12">
|
||||
<Link href="/docs/various/privacy" className="hover:text-secondary underline">
|
||||
FreeSewing Privacy Notice
|
||||
</Link>
|
||||
</p>
|
||||
</>
|
||||
)
|
||||
}
|
|
@ -1,6 +1,6 @@
|
|||
import Link from 'next/link'
|
||||
|
||||
export const iconSize = 'h-10 w-10 lg:h-12 lg:w-12'
|
||||
export const iconSize = 'h-8 w-8 md:h-10 md:w-10 lg:h-12 lg:w-12'
|
||||
|
||||
export const NavButton = ({
|
||||
href,
|
||||
|
|
|
@ -24,6 +24,10 @@ export const IconWrapper = ({
|
|||
<> {children} </>
|
||||
)
|
||||
|
||||
// Used in several icons
|
||||
const page =
|
||||
'M19.5 14.25v-2.625a3.375 3.375 0 00-3.375-3.375h-1.5A1.125 1.125 0 0113.5 7.125v-1.5a3.375 3.375 0 00-3.375-3.375H8.25m2.25 0H5.625c-.621 0-1.125.504-1.125 1.125v17.25c0 .621.504 1.125 1.125 1.125h12.75c.621 0 1.125-.504 1.125-1.125V11.25a9 9 0 00-9-9z'
|
||||
|
||||
export const BeakerIcon = (props) => (
|
||||
<IconWrapper {...props}>
|
||||
<path d="M9.75 3.104v5.714a2.25 2.25 0 01-.659 1.591L5 14.5M9.75 3.104c-.251.023-.501.05-.75.082m.75-.082a24.301 24.301 0 014.5 0m0 0v5.714c0 .597.237 1.17.659 1.591L19.8 15.3M14.25 3.104c.251.023.501.05.75.082M19.8 15.3l-1.57.393A9.065 9.065 0 0112 15a9.065 9.065 0 00-6.23-.693L5 14.5m14.8.8l1.402 1.402c1.232 1.232.65 3.318-1.067 3.611A48.309 48.309 0 0112 21c-2.773 0-5.491-.235-8.135-.687-1.718-.293-2.3-2.379-1.067-3.61L5 14.5" />
|
||||
|
@ -42,6 +46,13 @@ export const BookmarkIcon = (props) => (
|
|||
</IconWrapper>
|
||||
)
|
||||
|
||||
export const BoolNoIcon = ({ size = 6 }) => (
|
||||
<NoIcon className={`w-${size} h-${size} text-error`} stroke={3} />
|
||||
)
|
||||
export const BoolYesIcon = ({ size = 6 }) => (
|
||||
<OkIcon className={`w-${size} h-${size} text-success`} stroke={4} />
|
||||
)
|
||||
|
||||
export const BoxIcon = (props) => (
|
||||
<IconWrapper {...props}>
|
||||
<path d="M20 7l-8-4-8 4m16 0l-8 4m8-4v10l-8 4m0-10L4 7m8 4v10M4 7v10l8 4" />
|
||||
|
@ -119,6 +130,12 @@ export const ClearIcon = (props) => (
|
|||
</IconWrapper>
|
||||
)
|
||||
|
||||
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>
|
||||
)
|
||||
|
||||
export const CloseIcon = (props) => (
|
||||
<IconWrapper {...props}>
|
||||
<path d="M6 18L18 6M6 6l12 12" />
|
||||
|
@ -161,11 +178,21 @@ export const CopyIcon = (props) => (
|
|||
</IconWrapper>
|
||||
)
|
||||
|
||||
export const CoverPageIcon = (props) => (
|
||||
<IconWrapper {...props}>
|
||||
<path d={page} />
|
||||
<circle cx="9" cy="12" r="1" />
|
||||
<circle cx="14" cy="12" r="1" />
|
||||
<path d="M 9 16 C 11 18 12 18 14 16" />
|
||||
</IconWrapper>
|
||||
)
|
||||
|
||||
export const CsetIcon = (props) => (
|
||||
<IconWrapper {...props}>
|
||||
<path d="M12 21v-8.25M15.75 21v-8.25M8.25 21v-8.25M3 9l9-6 9 6m-1.5 12V10.332A48.36 48.36 0 0012 9.75c-2.551 0-5.056.2-7.5.582V21M3 21h18M12 6.75h.008v.008H12V6.75z" />
|
||||
</IconWrapper>
|
||||
)
|
||||
export const CuratedMeasurementsSetIcon = CsetIcon
|
||||
|
||||
export const CutIcon = (props) => (
|
||||
<IconWrapper {...props}>
|
||||
|
@ -173,6 +200,16 @@ export const CutIcon = (props) => (
|
|||
</IconWrapper>
|
||||
)
|
||||
|
||||
export const CuttingLayoutIcon = (props) => (
|
||||
<IconWrapper {...props}>
|
||||
<path d={page} />
|
||||
<path
|
||||
d="M7.848 8.25l1.536.887M7.848 8.25a3 3 0 11-5.196-3 3 3 0 015.196 3zm1.536.887a2.165 2.165 0 011.083 1.839c.005.351.054.695.14 1.024M9.384 9.137l2.077 1.199M7.848 15.75l1.536-.887m-1.536.887a3 3 0 11-5.196 3 3 3 0 015.196-3zm1.536-.887a2.165 2.165 0 001.083-1.838c.005-.352.054-.695.14-1.025m-1.223 2.863l2.077-1.199m0-3.328a4.323 4.323 0 012.068-1.379l5.325-1.628a4.5 4.5 0 012.48-.044l.803.215-7.794 4.5m-2.882-1.664A4.331 4.331 0 0010.607 12m3.736 0l7.794 4.5-.802.215a4.5 4.5 0 01-2.48-.043l-5.326-1.629a4.324 4.324 0 01-2.068-1.379M14.343 12l-2.882 1.664"
|
||||
transform="rotate(-90) scale(0.6) translate(-35 8)"
|
||||
/>
|
||||
</IconWrapper>
|
||||
)
|
||||
|
||||
export const DesignIcon = (props) => (
|
||||
<IconWrapper {...props} stroke={0} fill>
|
||||
<path d="m11.975 2.9104c-1.5285 0-2.7845 1.2563-2.7845 2.7848 0 0.7494 0.30048 1.4389 0.78637 1.9394a0.79437 0.79437 0 0 0 0.0084 0.00839c0.38087 0.38087 0.74541 0.62517 0.94538 0.82483 0.19998 0.19966 0.25013 0.2645 0.25013 0.51907v0.65964l-9.1217 5.2665c-0.28478 0.16442-0.83603 0.46612-1.3165 0.9611-0.48047 0.49498-0.92451 1.3399-0.66684 2.2585 0.22026 0.78524 0.7746 1.3486 1.3416 1.5878 0.56697 0.23928 1.0982 0.23415 1.4685 0.23415h18.041c0.37033 0 0.90158 0.0051 1.4686-0.23415 0.56697-0.23928 1.1215-0.80261 1.3418-1.5878 0.25767-0.91859-0.18662-1.7636-0.66709-2.2585-0.48046-0.49498-1.0315-0.79669-1.3162-0.9611l-8.9844-5.1873v-0.73889c0-0.70372-0.35623-1.2837-0.71653-1.6435-0.35778-0.3572-0.70316-0.58503-0.93768-0.81789-0.20864-0.21601-0.33607-0.50298-0.33607-0.83033 0-0.67 0.52595-1.1962 1.1959-1.1962 0.67001 0 1.1962 0.5262 1.1962 1.1962a0.79429 0.79429 0 0 0 0.79434 0.79427 0.79429 0.79429 0 0 0 0.79427-0.79427c0-1.5285-1.2563-2.7848-2.7848-2.7848zm-0.06859 8.2927 8.9919 5.1914c0.28947 0.16712 0.69347 0.41336 0.94393 0.67138 0.25046 0.25803 0.31301 0.3714 0.24754 0.60483-0.10289 0.36677-0.19003 0.40213-0.35969 0.47373-0.16967 0.07161-0.47013 0.09952-0.80336 0.09952h-18.041c-0.33323 0-0.6337-0.02792-0.80336-0.09952-0.16967-0.07161-0.25675-0.10696-0.35963-0.47373-0.06548-0.23342-0.00303-0.3468 0.24748-0.60483 0.25046-0.25803 0.65471-0.50426 0.94418-0.67138z" />
|
||||
|
@ -349,6 +386,12 @@ export const LeftIcon = (props) => (
|
|||
</IconWrapper>
|
||||
)
|
||||
|
||||
export const LeftRightIcon = (props) => (
|
||||
<IconWrapper {...props}>
|
||||
<path d="M7.5 21L3 16.5m0 0L7.5 12M3 16.5h13.5m0-13.5L21 7.5m0 0L16.5 12M21 7.5H7.5" />
|
||||
</IconWrapper>
|
||||
)
|
||||
|
||||
export const LinkIcon = (props) => (
|
||||
<IconWrapper {...props}>
|
||||
<path d="M13.19 8.688a4.5 4.5 0 011.242 7.244l-4.5 4.5a4.5 4.5 0 01-6.364-6.364l1.757-1.757m13.35-.622l1.757-1.757a4.5 4.5 0 00-6.364-6.364l-4.5 4.5a4.5 4.5 0 001.242 7.244" />
|
||||
|
@ -391,11 +434,18 @@ export const MenuIcon = (props) => (
|
|||
</IconWrapper>
|
||||
)
|
||||
|
||||
export const MenuAltIcon = (props) => (
|
||||
<IconWrapper {...props}>
|
||||
<path d="M12 6.75a.75.75 0 110-1.5.75.75 0 010 1.5zM12 12.75a.75.75 0 110-1.5.75.75 0 010 1.5zM12 18.75a.75.75 0 110-1.5.75.75 0 010 1.5z" />
|
||||
</IconWrapper>
|
||||
)
|
||||
|
||||
export const MsetIcon = (props) => (
|
||||
<IconWrapper {...props}>
|
||||
<path d="M15 19.128a9.38 9.38 0 002.625.372 9.337 9.337 0 004.121-.952 4.125 4.125 0 00-7.533-2.493M15 19.128v-.003c0-1.113-.285-2.16-.786-3.07M15 19.128v.106A12.318 12.318 0 018.624 21c-2.331 0-4.512-.645-6.374-1.766l-.001-.109a6.375 6.375 0 0111.964-3.07M12 6.375a3.375 3.375 0 11-6.75 0 3.375 3.375 0 016.75 0zm8.25 2.25a2.625 2.625 0 11-5.25 0 2.625 2.625 0 015.25 0z" />
|
||||
</IconWrapper>
|
||||
)
|
||||
export const MeasurementsSetIcon = MsetIcon
|
||||
|
||||
export const MsfIcon = (props) => (
|
||||
<IconWrapper {...props}>
|
||||
|
@ -407,6 +457,12 @@ export const MsfIcon = (props) => (
|
|||
</IconWrapper>
|
||||
)
|
||||
|
||||
export const NewMsetIcon = (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>
|
||||
)
|
||||
|
||||
export const NewsletterIcon = (props) => (
|
||||
<IconWrapper {...props}>
|
||||
<path d="M12 7.5h1.5m-1.5 3h1.5m-7.5 3h7.5m-7.5 3h7.5m3-9h3.375c.621 0 1.125.504 1.125 1.125V18a2.25 2.25 0 01-2.25 2.25M16.5 7.5V18a2.25 2.25 0 002.25 2.25M16.5 7.5V4.875c0-.621-.504-1.125-1.125-1.125H4.125C3.504 3.75 3 4.254 3 4.875V18a2.25 2.25 0 002.25 2.25h13.5M6 7.5h3v3H6v-3z" />
|
||||
|
@ -431,6 +487,13 @@ export const OkIcon = (props) => (
|
|||
</IconWrapper>
|
||||
)
|
||||
|
||||
export const OpackIcon = (props) => (
|
||||
<IconWrapper {...props}>
|
||||
<path d="M9.813 15.904L9 18.75l-.813-2.846a4.5 4.5 0 00-3.09-3.09L2.25 12l2.846-.813a4.5 4.5 0 003.09-3.09L9 5.25l.813 2.846a4.5 4.5 0 003.09 3.09L15.75 12l-2.846.813a4.5 4.5 0 00-3.09 3.09zM18.259 8.715L18 9.75l-.259-1.035a3.375 3.375 0 00-2.455-2.456L14.25 6l1.036-.259a3.375 3.375 0 002.455-2.456L18 2.25l.259 1.035a3.375 3.375 0 002.456 2.456L21.75 6l-1.035.259a3.375 3.375 0 00-2.456 2.456zM16.894 20.567L16.5 21.75l-.394-1.183a2.25 2.25 0 00-1.423-1.423L13.5 18.75l1.183-.394a2.25 2.25 0 001.423-1.423l.394-1.183.394 1.183a2.25 2.25 0 001.423 1.423l1.183.394-1.183.394a2.25 2.25 0 00-1.423 1.423z" />
|
||||
</IconWrapper>
|
||||
)
|
||||
export const OptionPackIcon = OpackIcon
|
||||
|
||||
export const OpenSourceIcon = (props) => (
|
||||
<IconWrapper {...props}>
|
||||
<path
|
||||
|
@ -447,15 +510,30 @@ export const OptionsIcon = (props) => (
|
|||
</IconWrapper>
|
||||
)
|
||||
|
||||
export const PageIcon = (props) => (
|
||||
export const PageMarginIcon = (props) => (
|
||||
<IconWrapper {...props}>
|
||||
<path d="M19.5 14.25v-2.625a3.375 3.375 0 00-3.375-3.375h-1.5A1.125 1.125 0 0113.5 7.125v-1.5a3.375 3.375 0 00-3.375-3.375H8.25m2.25 0H5.625c-.621 0-1.125.504-1.125 1.125v17.25c0 .621.504 1.125 1.125 1.125h12.75c.621 0 1.125-.504 1.125-1.125V11.25a9 9 0 00-9-9z" />
|
||||
<path
|
||||
d="M 4.5 2.5 v 19.2 h 15 v -13.3 h-3 v 10.3 h-9 v-13.2 h 6 v-3 z"
|
||||
strokeWidth={0.1}
|
||||
stroke="none"
|
||||
fill="currentColor"
|
||||
fillOpacity="0.666"
|
||||
/>
|
||||
<path d={page} />
|
||||
</IconWrapper>
|
||||
)
|
||||
|
||||
export const PageOrientationIcon = (props) => (
|
||||
<IconWrapper {...props}>
|
||||
<path d={page} transform="scale(-1 1) translate(-21 0)" />
|
||||
<path d="M 16.5 7.75 h 5 v 14 h-5" />
|
||||
</IconWrapper>
|
||||
)
|
||||
|
||||
export const PageSizeIcon = (props) => (
|
||||
<IconWrapper {...props}>
|
||||
<path d="M8 7v8a2 2 0 002 2h6M8 7V5a2 2 0 012-2h4.586a1 1 0 01.707.293l4.414 4.414a1 1 0 01.293.707V15a2 2 0 01-2 2h-2M8 7H6a2 2 0 00-2 2v10a2 2 0 002 2h8a2 2 0 002-2v-2" />
|
||||
<path d={page} />
|
||||
<path d={page} transform="scale(0.666) translate(3, 11)" />
|
||||
</IconWrapper>
|
||||
)
|
||||
|
||||
|
@ -469,6 +547,18 @@ export const PaperlessIcon = (props) => (
|
|||
</IconWrapper>
|
||||
)
|
||||
|
||||
export const PatternIcon = (props) => (
|
||||
<IconWrapper {...props}>
|
||||
<path d="M19.5 14.25v-2.625a3.375 3.375 0 00-3.375-3.375h-1.5A1.125 1.125 0 0113.5 7.125v-1.5a3.375 3.375 0 00-3.375-3.375H8.25m2.25 0H5.625c-.621 0-1.125.504-1.125 1.125v17.25c0 .621.504 1.125 1.125 1.125h12.75c.621 0 1.125-.504 1.125-1.125V11.25a9 9 0 00-9-9z" />
|
||||
</IconWrapper>
|
||||
)
|
||||
|
||||
export const NewPatternIcon = (props) => (
|
||||
<IconWrapper {...props}>
|
||||
<path d="M19.5 14.25v-2.625a3.375 3.375 0 00-3.375-3.375h-1.5A1.125 1.125 0 0113.5 7.125v-1.5a3.375 3.375 0 00-3.375-3.375H8.25m3.75 9v6m3-3H9m1.5-12H5.625c-.621 0-1.125.504-1.125 1.125v17.25c0 .621.504 1.125 1.125 1.125h12.75c.621 0 1.125-.504 1.125-1.125V11.25a9 9 0 00-9-9z" />
|
||||
</IconWrapper>
|
||||
)
|
||||
|
||||
export const PluginIcon = (props) => (
|
||||
<IconWrapper {...props}>
|
||||
<path d="M14.25 6.087c0-.355.186-.676.401-.959.221-.29.349-.634.349-1.003 0-1.036-1.007-1.875-2.25-1.875s-2.25.84-2.25 1.875c0 .369.128.713.349 1.003.215.283.401.604.401.959v0a.64.64 0 01-.657.643 48.39 48.39 0 01-4.163-.3c.186 1.613.293 3.25.315 4.907a.656.656 0 01-.658.663v0c-.355 0-.676-.186-.959-.401a1.647 1.647 0 00-1.003-.349c-1.036 0-1.875 1.007-1.875 2.25s.84 2.25 1.875 2.25c.369 0 .713-.128 1.003-.349.283-.215.604-.401.959-.401v0c.31 0 .555.26.532.57a48.039 48.039 0 01-.642 5.056c1.518.19 3.058.309 4.616.354a.64.64 0 00.657-.643v0c0-.355-.186-.676-.401-.959a1.647 1.647 0 01-.349-1.003c0-1.035 1.008-1.875 2.25-1.875 1.243 0 2.25.84 2.25 1.875 0 .369-.128.713-.349 1.003-.215.283-.4.604-.4.959v0c0 .333.277.599.61.58a48.1 48.1 0 005.427-.63 48.05 48.05 0 00.582-4.717.532.532 0 00-.533-.57v0c-.355 0-.676.186-.959.401-.29.221-.634.349-1.003.349-1.035 0-1.875-1.007-1.875-2.25s.84-2.25 1.875-2.25c.37 0 .713.128 1.003.349.283.215.604.401.96.401v0a.656.656 0 00.658-.663 48.422 48.422 0 00-.37-5.36c-1.886.342-3.81.574-5.766.689a.578.578 0 01-.61-.58v0z" />
|
||||
|
@ -689,3 +779,15 @@ export const YouTubeIcon = (props) => (
|
|||
<path d="M 18.723199,4.1090377 H 5.2768074 C 2.638649,4.1090377 0.5,6.2476867 0.5,8.8858457 v 6.7217683 c 0,2.638165 2.138649,4.776807 4.7768074,4.776807 H 18.723199 c 2.638159,0 4.776801,-2.138642 4.776801,-4.776807 V 8.8858457 c 0,-2.638159 -2.138642,-4.776808 -4.776801,-4.776808 z M 15.492674,12.57377 9.2033594,15.573394 C 9.0357741,15.653314 8.8421952,15.531134 8.8421952,15.345486 V 9.1587477 c 0,-0.188291 0.1986681,-0.310321 0.3666026,-0.22521 l 6.2893152,3.1871143 c 0.186996,0.09475 0.18375,0.36291 -0.0054,0.453118 z" />
|
||||
</IconWrapper>
|
||||
)
|
||||
|
||||
export const ZoomInIcon = (props) => (
|
||||
<IconWrapper {...props}>
|
||||
<path d="M21 21l-5.197-5.197m0 0A7.5 7.5 0 105.196 5.196a7.5 7.5 0 0010.607 10.607zM10.5 7.5v6m3-3h-6" />
|
||||
</IconWrapper>
|
||||
)
|
||||
|
||||
export const ZoomOutIcon = (props) => (
|
||||
<IconWrapper {...props}>
|
||||
<path d="M21 21l-5.197-5.197m0 0A7.5 7.5 0 105.196 5.196a7.5 7.5 0 0010.607 10.607zM13.5 10.5h-6" />
|
||||
</IconWrapper>
|
||||
)
|
||||
|
|
|
@ -10,7 +10,7 @@ import {
|
|||
ShowcaseIcon,
|
||||
UserIcon,
|
||||
MeasieIcon,
|
||||
PageIcon,
|
||||
PatternIcon,
|
||||
CodeIcon,
|
||||
I18nIcon,
|
||||
WrenchIcon,
|
||||
|
@ -43,7 +43,7 @@ export const icons = {
|
|||
showcase: (className = '') => <ShowcaseIcon className={className} />,
|
||||
community: (className = '') => <CommunityIcon className={className} />,
|
||||
sets: (className = '') => <MeasieIcon className={className} />,
|
||||
patterns: (className = '') => <PageIcon className={className} />,
|
||||
patterns: (className = '') => <PatternIcon className={className} />,
|
||||
new: (className = '') => <PlusIcon className={className} />,
|
||||
|
||||
// Lab
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
accountBlocked: Your account is blocked
|
||||
accountBlockedMsg: This is highly unusual but it seems your account is administratively blocked. Your only recourse is to contact support.
|
||||
alreadyHaveAnAccount: Already have an account?
|
||||
back: Back
|
||||
backToSignIn: Back to sign in
|
||||
|
@ -5,7 +7,11 @@ backToSignUp: Back to sign up
|
|||
checkYourInbox: Go check your inbox for an email from
|
||||
clickSigninLink: Click the sign-in link in that email to sign in to your FreeSewing account.
|
||||
clickSignupLink: Click your personal signup link in that email to create your FreeSewing account.
|
||||
consentLacking: We lack consent to process your data
|
||||
consentLackingMsg: Getting your consent is part of sign up process. Look for the email you received when you signed up for instracutions. You can sign up again with the same email address to receive the email again.
|
||||
contact: Contact support
|
||||
contactingGithub: Contacting GitHub
|
||||
contactingGoogle: Contacting Google
|
||||
createAFreeSewingAccount: Create a FreeSewing account
|
||||
dontHaveAV2Account: Don't have a v2 account?
|
||||
dontHaveAnAccount: Don't have an account yet?
|
||||
|
@ -17,6 +23,7 @@ emailSigninLink: Email me a sign-in link
|
|||
emailUsernameId: "Your Email address, Username, or User #"
|
||||
err2: Unfortunately, we cannot recover from this error, we need a human being to look into this.
|
||||
err3: Feel free to try again, or reach out to support so we can assist you.
|
||||
fewerOptions: Fewer options
|
||||
haveAV2Account: Have a v2 account?
|
||||
joinFreeSewing: Join FreeSewing
|
||||
migrateItHere: Migrate it here
|
||||
|
@ -25,6 +32,11 @@ migrateV2Account: Migrate your v2 account
|
|||
migrateV2Desc: Enter your v2 username & password to migrate your account.
|
||||
migrateV2Info: Your v2 account will not be changed, this will only create a v3 account with the v2 account data.
|
||||
migrateV3UserAlreadyExists: Cannot migrate over an existing v3 account. Perhaps just sign in instead?
|
||||
moreOptions: More options
|
||||
noMagicFound: No such magic (link) found
|
||||
noMagicFoundMsg: The magic link you used is either expired, or invalid. Note that each magic link can only be used once.
|
||||
noSuchUser: User not found
|
||||
noSuchUserMsg: We tried to find the user account you requested, but were unable to find it.
|
||||
notFound: No such user was found
|
||||
oneMomentPlease: One moment please
|
||||
password: Your Password
|
||||
|
@ -35,6 +47,7 @@ regainAccess: Re-gain access
|
|||
signIn: Sign in
|
||||
signInAsOtherUser: Sign in as a different user
|
||||
signInFailed: Sign in failed
|
||||
signInFailedMsg: Not entirely certain why, but it did not work as expected.
|
||||
signInHere: Sign in here
|
||||
signInToThing: "Sign in to { thing }"
|
||||
signInWithProvider: Sign in with { provider }
|
||||
|
@ -43,6 +56,9 @@ signUpWithProvider: Sign up with {provider}
|
|||
signupAgain: Sign up again
|
||||
signupLinkExpired: Signup link expired
|
||||
somethingWentWrong: Something went wrong
|
||||
sorry: Sorry
|
||||
statusLacking: Your account is in a non-active status
|
||||
statusLackingMsg: The current status of your account prohibits us from proceeding. The most common reason for this is that you did not complete the onboarding process and thus your account was never activated. You can sign up again with the same email address to remediate this.
|
||||
toReceiveSignupLink: To receive a sign-up link, enter your email address
|
||||
tryAgain: Try again
|
||||
usePassword: Use your password
|
||||
|
@ -50,3 +66,4 @@ usernameMissing: Please provide your username
|
|||
welcome: Welcome
|
||||
welcomeBackName: "Welcome back { name }"
|
||||
welcomeMigrateName: Welcome to FreeSewing v3 {name}. Please note that this is still alpha code.
|
||||
|
||||
|
|
|
@ -5,7 +5,7 @@ import { useTranslation } from 'next-i18next'
|
|||
import { useBackend } from 'shared/hooks/use-backend.mjs'
|
||||
import { useRouter } from 'next/router'
|
||||
import { useLoadingStatus } from 'shared/hooks/use-loading-status.mjs'
|
||||
import { horFlexClasses, horFlexClassesNoSm } from 'shared/utils.mjs'
|
||||
import { horFlexClasses, horFlexClassesNoSm, capitalize } from 'shared/utils.mjs'
|
||||
// Components
|
||||
import Link from 'next/link'
|
||||
import {
|
||||
|
@ -25,7 +25,7 @@ export const ns = ['susi', 'errors', 'status']
|
|||
|
||||
export const SignIn = () => {
|
||||
const { setAccount, setToken, seenUser, setSeenUser } = useAccount()
|
||||
const { t } = useTranslation(ns)
|
||||
const { t, i18n } = useTranslation(ns)
|
||||
const backend = useBackend()
|
||||
const router = useRouter()
|
||||
const { setLoadingStatus, LoadingStatus } = useLoadingStatus()
|
||||
|
@ -94,6 +94,15 @@ export const SignIn = () => {
|
|||
}
|
||||
}
|
||||
|
||||
const initOauth = async (provider) => {
|
||||
setLoadingStatus([true, t(`susi:contactingBackend`)])
|
||||
const result = await backend.oauthInit({ provider, language: i18n.language })
|
||||
if (result.success) {
|
||||
setLoadingStatus([true, t(`susi:contacting${capitalize(provider)}`)])
|
||||
window.location.href = result.data.authUrl
|
||||
}
|
||||
}
|
||||
|
||||
const btnClasses = `btn capitalize w-full mt-4 ${
|
||||
signInFailed ? 'btn-warning' : 'btn-primary'
|
||||
} transition-colors ease-in-out duration-300 ${horFlexClasses}`
|
||||
|
@ -197,7 +206,12 @@ export const SignIn = () => {
|
|||
</button>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-2 items-center mt-2">
|
||||
{['Google', 'Github'].map((provider) => (
|
||||
<button key={provider} id={provider} className={`${horFlexClasses} btn btn-secondary`}>
|
||||
<button
|
||||
key={provider}
|
||||
id={provider}
|
||||
className={`${horFlexClasses} btn btn-secondary`}
|
||||
onClick={() => initOauth(provider)}
|
||||
>
|
||||
{provider === 'Google' ? <GoogleIcon stroke={0} /> : <GitHubIcon />}
|
||||
<span>{t('susi:signInWithProvider', { provider })}</span>
|
||||
</button>
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
import { useState, useContext } from 'react'
|
||||
import { useBackend } from 'shared/hooks/use-backend.mjs'
|
||||
import { useTranslation } from 'next-i18next'
|
||||
import { useLoadingStatus } from 'shared/hooks/use-loading-status.mjs'
|
||||
// Context
|
||||
import { ModalContext } from 'shared/context/modal-context.mjs'
|
||||
// Dependencies
|
||||
|
@ -17,6 +18,7 @@ import {
|
|||
KeyIcon,
|
||||
SettingsIcon,
|
||||
EmailIcon,
|
||||
DownIcon,
|
||||
} from 'shared/components/icons.mjs'
|
||||
import { ModalWrapper } from 'shared/components/wrappers/modal.mjs'
|
||||
import { EmailInput } from 'shared/components/inputs.mjs'
|
||||
|
@ -30,11 +32,12 @@ export const SignUp = () => {
|
|||
|
||||
const backend = useBackend()
|
||||
const { t, i18n } = useTranslation(ns)
|
||||
const { setLoadingStatus, LoadingStatus } = useLoadingStatus()
|
||||
|
||||
const [email, setEmail] = useState('')
|
||||
const [emailValid, setEmailValid] = useState(false)
|
||||
const [result, setResult] = useState(false)
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [showAll, setShowAll] = useState(false)
|
||||
|
||||
const updateEmail = (value) => {
|
||||
setEmail(value)
|
||||
|
@ -44,8 +47,10 @@ export const SignUp = () => {
|
|||
|
||||
const signupHandler = async (evt) => {
|
||||
evt.preventDefault()
|
||||
setLoading(true)
|
||||
if (!emailValid) return
|
||||
if (!emailValid) {
|
||||
setLoadingStatus([true, t('susi:pleaseProvideValidEmail'), true, false])
|
||||
return
|
||||
}
|
||||
let res
|
||||
try {
|
||||
res = await backend.signUp({
|
||||
|
@ -80,14 +85,21 @@ export const SignUp = () => {
|
|||
</ModalWrapper>
|
||||
)
|
||||
}
|
||||
setLoading(false)
|
||||
}
|
||||
|
||||
const loadingClasses = loading ? 'opacity-50' : ''
|
||||
const initOauth = async (provider) => {
|
||||
setLoadingStatus([true, t(`status:contactingBackend`)])
|
||||
const result = await backend.oauthInit({ provider, language: i18n.language })
|
||||
if (result.success) {
|
||||
setLoadingStatus([true, t(`status:contacting${provider}`)])
|
||||
window.location.href = result.data.authUrl
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="w-full">
|
||||
<h2 className={`text-inherit ${loadingClasses}`}>
|
||||
<LoadingStatus />
|
||||
<h2 className="text-inherit">
|
||||
{result ? (
|
||||
result === 'success' ? (
|
||||
<span>{t('susi:emailSent')}!</span>
|
||||
|
@ -132,58 +144,81 @@ export const SignUp = () => {
|
|||
)
|
||||
) : (
|
||||
<>
|
||||
<p className={`text-inherit ${loadingClasses}`}>{t('toReceiveSignupLink')}:</p>
|
||||
<p className="text-inherit">{t('toReceiveSignupLink')}:</p>
|
||||
<form onSubmit={signupHandler}>
|
||||
<EmailInput
|
||||
id="signup-email"
|
||||
label={t('susi:emailAddress')}
|
||||
current={email}
|
||||
original={''}
|
||||
valid={() => emailValid}
|
||||
placeholder={t('susi:emailAddress')}
|
||||
update={updateEmail}
|
||||
/>
|
||||
<button
|
||||
className={`btn btn-primary btn-lg mt-2 w-full ${horFlexClasses} disabled:bg-neutral disabled:text-neutral-content disabled:opacity-50`}
|
||||
type="submit"
|
||||
disabled={!emailValid}
|
||||
>
|
||||
<span className="hidden md:block">
|
||||
<EmailIcon />
|
||||
</span>
|
||||
{emailValid ? t('susi:emailSignupLink') : t('susi:pleaseProvideValidEmail')}
|
||||
<span className="hidden md:block">
|
||||
<EmailIcon />
|
||||
</span>
|
||||
<EmailIcon />
|
||||
{t('susi:emailSignupLink')}
|
||||
</button>
|
||||
</form>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-2 items-center mt-4">
|
||||
{['Google', 'Github'].map((provider) => (
|
||||
<button
|
||||
key={provider}
|
||||
id={provider}
|
||||
className={`${horFlexClasses} btn btn-secondary`}
|
||||
{showAll ? (
|
||||
<>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-2 items-center mt-4">
|
||||
{['Google', 'GitHub'].map((provider) => (
|
||||
<button
|
||||
key={provider}
|
||||
id={provider}
|
||||
className={`${horFlexClasses} btn btn-secondary`}
|
||||
onClick={() => initOauth(provider)}
|
||||
>
|
||||
{provider === 'Google' ? <GoogleIcon stroke={0} /> : <GitHubIcon />}
|
||||
<span>{t('susi:signUpWithProvider', { provider })}</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
<Link
|
||||
className={`${horFlexClassesNoSm} w-full btn btn-lg btn-neutral mt-2`}
|
||||
href="/signup"
|
||||
>
|
||||
{provider === 'Google' ? <GoogleIcon stroke={0} /> : <GitHubIcon />}
|
||||
<span>{t('susi:signUpWithProvider', { provider })}</span>
|
||||
<span className="hidden md:block">
|
||||
<KeyIcon className="h-10 w-10" />
|
||||
</span>
|
||||
{t('susi:signInHere')}
|
||||
</Link>
|
||||
<Link
|
||||
className={`${horFlexClassesNoSm} w-full btn btn-neutral btn-outline mt-2`}
|
||||
href="/migrate"
|
||||
>
|
||||
<span className="hidden md:block">
|
||||
<SettingsIcon />
|
||||
</span>
|
||||
{t('susi:migrateV2Account')}
|
||||
</Link>
|
||||
<div className="flex flex-row justify-center mt-2">
|
||||
<button
|
||||
onClick={() => setShowAll(false)}
|
||||
className={`btn btn-ghost ${horFlexClasses}`}
|
||||
>
|
||||
<DownIcon className="w-6 h-6 rotate-180" />
|
||||
{t('susi:fewerOptions')}
|
||||
<DownIcon className="w-6 h-6 rotate-180" />
|
||||
</button>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<div className="flex flex-row justify-center mt-2">
|
||||
<button
|
||||
onClick={() => setShowAll(true)}
|
||||
className={`btn btn-ghost ${horFlexClasses}`}
|
||||
>
|
||||
<DownIcon />
|
||||
{t('susi:moreOptions')}
|
||||
<DownIcon />
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
<Link
|
||||
className={`${horFlexClassesNoSm} w-full btn btn-lg btn-neutral mt-2`}
|
||||
href="/signup"
|
||||
>
|
||||
<span className="hidden md:block">
|
||||
<KeyIcon className="h-10 w-10" />
|
||||
</span>
|
||||
{t('susi:signInHere')}
|
||||
</Link>
|
||||
<Link
|
||||
className={`${horFlexClassesNoSm} w-full btn btn-neutral btn-outline mt-2`}
|
||||
href="/migrate"
|
||||
>
|
||||
<span className="hidden md:block">
|
||||
<SettingsIcon />
|
||||
</span>
|
||||
{t('susi:migrateV2Account')}
|
||||
</Link>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
|
|
@ -19,6 +19,7 @@ import {
|
|||
} from 'shared/components/icons.mjs'
|
||||
import Link from 'next/link'
|
||||
import { MenuWrapper } from 'shared/components/workbench/menus/shared/menu-wrapper.mjs'
|
||||
import { isProduction } from 'shared/config/freesewing.config.mjs'
|
||||
|
||||
export const ns = ['workbench', 'sections']
|
||||
|
||||
|
@ -46,7 +47,7 @@ export const NavButton = ({
|
|||
const className = `w-full flex flex-row items-center px-4 py-2 ${extraClasses} ${
|
||||
active ? 'text-secondary' : ''
|
||||
}`
|
||||
const span = <span className="font-bold block grow text-left">{label}</span>
|
||||
const span = <span className="font-normal block grow text-left">{label}</span>
|
||||
|
||||
return onClick ? (
|
||||
<button {...{ onClick, className }} title={label}>
|
||||
|
@ -70,7 +71,7 @@ const NavIcons = ({ setView, setDense, dense, view }) => {
|
|||
<NavButton
|
||||
onClick={() => setDense(!dense)}
|
||||
label={t('workbench:viewMenu')}
|
||||
extraClasses="hidden lg:flex text-success bg-neutral hover:bg-success hover:text-neutral"
|
||||
extraClasses="hidden lg:flex text-accent bg-neutral hover:bg-accent hover:text-neutral-content"
|
||||
>
|
||||
{dense ? (
|
||||
<RightIcon
|
||||
|
@ -109,13 +110,15 @@ const NavIcons = ({ setView, setDense, dense, view }) => {
|
|||
>
|
||||
<PrintIcon className={iconSize} />
|
||||
</NavButton>
|
||||
<NavButton
|
||||
onClick={() => setView('cut')}
|
||||
label={t('workbench:cutLayout')}
|
||||
active={view === 'cut'}
|
||||
>
|
||||
<CutIcon className={iconSize} />
|
||||
</NavButton>
|
||||
{!isProduction && (
|
||||
<NavButton
|
||||
onClick={() => setView('cut')}
|
||||
label={t('workbench:cutLayout')}
|
||||
active={view === 'cut'}
|
||||
>
|
||||
<CutIcon className={iconSize} />
|
||||
</NavButton>
|
||||
)}
|
||||
<NavButton
|
||||
onClick={() => setView('save')}
|
||||
label={t('workbench:savePattern')}
|
||||
|
|
|
@ -4,6 +4,7 @@ import { ModalWrapper } from 'shared/components/wrappers/modal.mjs'
|
|||
import { CloseIcon } from 'shared/components/icons.mjs'
|
||||
import { MobileMenubarContext } from 'shared/context/mobile-menubar-context.mjs'
|
||||
import { shownHeaderSelector } from 'shared/components/wrappers/header.mjs'
|
||||
import { MenuAltIcon } from 'shared/components/icons.mjs'
|
||||
|
||||
/**
|
||||
* A component to display menu buttons and actions in mobile.
|
||||
|
@ -35,6 +36,7 @@ export const MobileMenubar = () => {
|
|||
slideFrom="right"
|
||||
keepOpenOnClick={selectedMenu.keepOpenOnClick}
|
||||
keepOpenOnSwipe
|
||||
fullWidth
|
||||
>
|
||||
<div className="mb-16">{selectedMenu.menuContent}</div>
|
||||
<button
|
||||
|
@ -62,10 +64,10 @@ export const MobileMenubar = () => {
|
|||
<div
|
||||
className={`
|
||||
lg:hidden
|
||||
${shownHeaderSelector('bottom-16')}
|
||||
${shownHeaderSelector('bottom-16')}
|
||||
sticky bottom-0 w-20 -ml-20 self-end
|
||||
duration-300 transition-all
|
||||
flex flex-col-reverse gap-4
|
||||
duration-300 transition-all
|
||||
flex flex-col-reverse gap-2 mb-2
|
||||
z-20
|
||||
mobile-menubar
|
||||
`}
|
||||
|
@ -73,11 +75,11 @@ export const MobileMenubar = () => {
|
|||
{Object.keys(menus)
|
||||
.sort((a, b) => menus[a].order - menus[b].order)
|
||||
.map((m) => {
|
||||
const Icon = menus[m].Icon
|
||||
const Icon = m === 'nav' ? MenuAltIcon : menus[m].Icon
|
||||
return (
|
||||
<button
|
||||
key={m}
|
||||
className="btn btn-accent btn-circle mx-4"
|
||||
className={`btn ${m === 'nav' ? 'btn-neutral' : 'btn-primary'} btn-circle mx-4`}
|
||||
onClick={() => setSelectedModal(m)}
|
||||
>
|
||||
<Icon />
|
||||
|
|
|
@ -234,7 +234,7 @@ export const ListInput = ({ name, config, current, updateFunc, compact = false,
|
|||
const titleKey = config.choiceTitles ? config.choiceTitles[entry] : `${name}.o.${entry}`
|
||||
const title = t(`${titleKey}.t`)
|
||||
const desc = t(`${titleKey}.d`)
|
||||
const sideBySide = desc.length + title.length < 70
|
||||
const sideBySide = desc.length + title.length < 60
|
||||
|
||||
return (
|
||||
<ButtonFrame
|
||||
|
|
|
@ -216,10 +216,10 @@ export const MenuItemGroup = ({
|
|||
: () => <span>¯\_(ツ)_/¯</span>
|
||||
|
||||
return [
|
||||
<div className="flex flex-row items-center justify-between" key="a">
|
||||
<div className="flex flex-row items-center gap-4">
|
||||
<div className="flex flex-row items-center justify-between w-full" key="a">
|
||||
<div className="flex flex-row items-center gap-4 w-full">
|
||||
<ItemIcon />
|
||||
<h6>{t([`${itemName}.t`, itemName])}</h6>
|
||||
<span className="font-medium">{t([`${itemName}.t`, itemName])}</span>
|
||||
</div>
|
||||
<div className="font-bold">
|
||||
<Value
|
||||
|
@ -280,33 +280,5 @@ export const MenuItemGroup = ({
|
|||
]
|
||||
})
|
||||
|
||||
// if it should be wrapped in a collapsible
|
||||
/*
|
||||
if (collapsible) {
|
||||
// props to give to the group title
|
||||
const titleProps = {
|
||||
name,
|
||||
t,
|
||||
emoji: emojis[name] || emojis.groupDflt,
|
||||
}
|
||||
return (
|
||||
<Collapse
|
||||
bottom
|
||||
color={topLevel ? 'primary' : 'secondary'}
|
||||
title={
|
||||
<ItemTitle
|
||||
{...titleProps}
|
||||
current={Icon ? <Icon className="w-6 h-6 text-primary" /> : ''}
|
||||
/>
|
||||
}
|
||||
openTitle={<ItemTitle open {...titleProps} />}
|
||||
>
|
||||
{content}
|
||||
</Collapse>
|
||||
)
|
||||
}
|
||||
*/
|
||||
|
||||
//otherwise just return the content
|
||||
return <SubAccordion items={content.filter((item) => item !== null)} />
|
||||
}
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import { formatMm } from 'shared/utils.mjs'
|
||||
import { BoolYesIcon, BoolNoIcon } from 'shared/components/icons.mjs'
|
||||
|
||||
/*********************************************************************************************************
|
||||
* This file contains the base components to be used for displaying values in menu titles in the workbench
|
||||
|
@ -38,10 +39,10 @@ export const ListValue = ({ current, t, config, changed }) => {
|
|||
// if not, is the value a string
|
||||
else if (typeof val === 'string') key = val
|
||||
// otherwise stringify booleans
|
||||
else if (val) key = 'yes'
|
||||
else key = 'no'
|
||||
else if (val) key = <BoolYesIcon />
|
||||
else key = <BoolNoIcon />
|
||||
|
||||
const translated = config.doNotTranslate ? key : t(key)
|
||||
const translated = config.doNotTranslate || typeof key !== 'string' ? key : t(key)
|
||||
|
||||
return <HighlightedValue changed={changed}>{translated}</HighlightedValue>
|
||||
}
|
||||
|
|
|
@ -68,6 +68,7 @@ export const CutView = ({
|
|||
|
||||
return (
|
||||
<PatternWithMenu
|
||||
noHeader
|
||||
{...{
|
||||
settings,
|
||||
ui,
|
||||
|
|
|
@ -69,7 +69,7 @@ export const DraftMenu = ({
|
|||
<span>{t(`${section.ns}:${section.name}.t`)}</span>
|
||||
{section.icon}
|
||||
</h5>
|
||||
<p>{t(`${section.ns}:${section.name}.d`)}</p>
|
||||
<p className="text-left">{t(`${section.ns}:${section.name}.d`)}</p>
|
||||
</>,
|
||||
section.menu,
|
||||
])}
|
||||
|
|
|
@ -20,6 +20,8 @@ import { MsetIcon, BookmarkIcon, CsetIcon, EditIcon } from 'shared/components/ic
|
|||
|
||||
export const ns = nsMerge(authNs, setsNs)
|
||||
|
||||
const iconClasses = { className: 'w-8 h-8 md:w-10 md:h-10 lg:w-12 lg:h-12 shrink-0', stroke: 1.5 }
|
||||
|
||||
export const MeasiesView = ({ design, Design, settings, update, missingMeasurements, setView }) => {
|
||||
const { t } = useTranslation(['workbench'])
|
||||
const { setLoadingStatus, LoadingStatus } = useLoadingStatus()
|
||||
|
@ -61,7 +63,7 @@ export const MeasiesView = ({ design, Design, settings, update, missingMeasureme
|
|||
<Fragment key={1}>
|
||||
<div className={horFlexClasses}>
|
||||
<h5 id="ownsets">{t('workbench:chooseFromOwnSets')}</h5>
|
||||
<MsetIcon className="w-6 h-6 shrink-0" />
|
||||
<MsetIcon {...iconClasses} />
|
||||
</div>
|
||||
<p>{t('workbench:chooseFromOwnSetsDesc')}</p>
|
||||
</Fragment>,
|
||||
|
@ -77,7 +79,7 @@ export const MeasiesView = ({ design, Design, settings, update, missingMeasureme
|
|||
<Fragment key={1}>
|
||||
<div className={horFlexClasses}>
|
||||
<h5 id="bookmarkedsets">{t('workbench:chooseFromBookmarkedSets')}</h5>
|
||||
<BookmarkIcon className="w-6 h-6 shrink-0" />
|
||||
<BookmarkIcon {...iconClasses} />
|
||||
</div>
|
||||
<p>{t('workbench:chooseFromBookmarkedSetsDesc')}</p>
|
||||
</Fragment>,
|
||||
|
@ -93,7 +95,7 @@ export const MeasiesView = ({ design, Design, settings, update, missingMeasureme
|
|||
<Fragment key={1}>
|
||||
<div className={horFlexClasses}>
|
||||
<h5 id="curatedsets">{t('workbench:chooseFromCuratedSets')}</h5>
|
||||
<CsetIcon />
|
||||
<CsetIcon {...iconClasses} />
|
||||
</div>
|
||||
<p>{t('workbench:chooseFromCuratedSetsDesc')}</p>
|
||||
</Fragment>,
|
||||
|
@ -103,7 +105,7 @@ export const MeasiesView = ({ design, Design, settings, update, missingMeasureme
|
|||
<Fragment key={1}>
|
||||
<div className={horFlexClasses}>
|
||||
<h5 id="editmeasies">{t('workbench:editMeasiesByHand')}</h5>
|
||||
<EditIcon />
|
||||
<EditIcon {...iconClasses} />
|
||||
</div>
|
||||
<p>{t('workbench:editMeasiesByHandDesc')}</p>
|
||||
</Fragment>,
|
||||
|
|
|
@ -16,20 +16,23 @@ export const PatternWithMenu = ({
|
|||
pattern,
|
||||
menu,
|
||||
setSettings,
|
||||
noHeader = false,
|
||||
}) => (
|
||||
<PanZoomContextProvider>
|
||||
<div className="flex flex-col h-full">
|
||||
<ViewHeader
|
||||
{...{
|
||||
settings,
|
||||
ui,
|
||||
update,
|
||||
control,
|
||||
account,
|
||||
design,
|
||||
setSettings,
|
||||
}}
|
||||
/>
|
||||
{noHeader ? null : (
|
||||
<ViewHeader
|
||||
{...{
|
||||
settings,
|
||||
ui,
|
||||
update,
|
||||
control,
|
||||
account,
|
||||
design,
|
||||
setSettings,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<div className="flex lg:flex-row grow lg:max-h-[90vh] max-h-[calc(100vh-3rem)] h-full py-4 lg:mt-6">
|
||||
<div className="lg:w-2/3 flex flex-col h-full grow px-4">
|
||||
{title}
|
||||
|
|
|
@ -1,28 +1,79 @@
|
|||
import { Fragment } from 'react'
|
||||
import { useTranslation } from 'next-i18next'
|
||||
import { ClearIcon, ExportIcon } from 'shared/components/icons.mjs'
|
||||
import { ShowButtonsToggle } from 'shared/components/workbench/pattern/movable/transform-buttons.mjs'
|
||||
import { SubAccordion } from 'shared/components/accordion.mjs'
|
||||
import {
|
||||
WarningIcon,
|
||||
ResetIcon,
|
||||
LeftRightIcon,
|
||||
BoolYesIcon,
|
||||
BoolNoIcon,
|
||||
} from 'shared/components/icons.mjs'
|
||||
import { ListInput } from 'shared/components/inputs.mjs'
|
||||
import { horFlexClasses } from 'shared/utils.mjs'
|
||||
|
||||
export const ns = ['workbench', 'print']
|
||||
|
||||
export const PrintActions = ({ update, ui, exportIt }) => {
|
||||
export const PrintActions = ({ update, ui }) => {
|
||||
// get translation for the menu
|
||||
const { t } = useTranslation(ns)
|
||||
|
||||
const resetLayout = () => update.ui(['layouts', 'print'])
|
||||
|
||||
return (
|
||||
<div className="mt-2 mb-4">
|
||||
<div className="flex justify-evenly flex-col lg:flex-row">
|
||||
<ShowButtonsToggle update={update} ui={ui} />
|
||||
<button className="btn btn-primary btn-outline" onClick={resetLayout}>
|
||||
<ClearIcon />
|
||||
<span className="ml-2">{t('reset')}</span>
|
||||
</button>
|
||||
<button className="btn btn-primary" onClick={exportIt}>
|
||||
<ExportIcon />
|
||||
<span className="ml-2">{t('export')}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<>
|
||||
<SubAccordion
|
||||
items={[
|
||||
[
|
||||
<div className="w-full flex flex-row gap2 justify-between" key={1}>
|
||||
<div className="flex flex-row items-center gap-2">
|
||||
<LeftRightIcon />
|
||||
<span>{t('workbench:partTransfo')}</span>
|
||||
</div>
|
||||
{ui.hideMovableButtons ? <BoolNoIcon /> : <BoolYesIcon />}
|
||||
</div>,
|
||||
<ListInput
|
||||
key={2}
|
||||
update={() => update.ui('hideMovableButtons', ui.hideMovableButtons ? false : true)}
|
||||
label={
|
||||
<span className="text-base font-normal">{t('workbench:partTransfoDesc')}</span>
|
||||
}
|
||||
list={[
|
||||
{
|
||||
val: true,
|
||||
label: t('workbench:partTransfoNo'),
|
||||
desc: t('workbench:partTransfoNoDesc'),
|
||||
},
|
||||
{
|
||||
val: false,
|
||||
label: t('workbench:partTransfoYes'),
|
||||
desc: t('workbench:partTransfoYesDesc'),
|
||||
},
|
||||
]}
|
||||
current={ui.hideMovableButtons ? true : false}
|
||||
/>,
|
||||
],
|
||||
[
|
||||
<div className="w-full flex flex-row gap2 justify-between" key={1}>
|
||||
<div className="flex flex-row items-center gap-2">
|
||||
<ResetIcon />
|
||||
<span>{t('workbench:resetPrintLayout')}</span>
|
||||
</div>
|
||||
<WarningIcon />
|
||||
</div>,
|
||||
|
||||
<Fragment key={2}>
|
||||
<p>{t('workbench:resetPrintLayoutDesc')}</p>
|
||||
<button
|
||||
className={`${horFlexClasses} btn btn-warning btn-outline w-full`}
|
||||
onClick={resetLayout}
|
||||
>
|
||||
<ResetIcon />
|
||||
<span>{t('workbench:resetPrintLayout')}</span>
|
||||
</button>
|
||||
</Fragment>,
|
||||
],
|
||||
]}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -1,11 +1,19 @@
|
|||
import { measurementAsMm } from 'shared/utils.mjs'
|
||||
import {
|
||||
PageSizeIcon,
|
||||
PageOrientationIcon,
|
||||
PageMarginIcon,
|
||||
CoverPageIcon,
|
||||
CuttingLayoutIcon,
|
||||
} from 'shared/components/icons.mjs'
|
||||
import { isProduction } from 'shared/config/freesewing.config.mjs'
|
||||
|
||||
export const printSettingsPath = ['print', 'pages']
|
||||
|
||||
export const defaultPrintSettings = (units, inMm = true) => {
|
||||
const margin = units === 'imperial' ? 0.5 : 1
|
||||
return {
|
||||
size: 'a4',
|
||||
size: units === 'imperial' ? 'letter' : 'a4',
|
||||
orientation: 'portrait',
|
||||
margin: inMm ? measurementAsMm(margin, units) : margin,
|
||||
coverPage: true,
|
||||
|
@ -22,6 +30,7 @@ export const loadPrintConfig = (units) => {
|
|||
dflt: defaults.size,
|
||||
choiceTitles: {},
|
||||
valueTitles: {},
|
||||
icon: PageSizeIcon,
|
||||
},
|
||||
orientation: {
|
||||
control: 2,
|
||||
|
@ -35,6 +44,7 @@ export const loadPrintConfig = (units) => {
|
|||
landscape: 'landscape',
|
||||
},
|
||||
dflt: defaults.orientation,
|
||||
icon: PageOrientationIcon,
|
||||
},
|
||||
margin: {
|
||||
control: 2,
|
||||
|
@ -42,14 +52,17 @@ export const loadPrintConfig = (units) => {
|
|||
max: 2.5,
|
||||
step: units === 'imperial' ? 0.125 : 0.1,
|
||||
dflt: defaults.margin,
|
||||
icon: PageMarginIcon,
|
||||
},
|
||||
coverPage: {
|
||||
control: 3,
|
||||
dflt: defaults.coverPage,
|
||||
icon: CoverPageIcon,
|
||||
},
|
||||
cutlist: {
|
||||
control: 3,
|
||||
dflt: defaults.cutlist,
|
||||
icon: CuttingLayoutIcon,
|
||||
},
|
||||
}
|
||||
|
||||
|
@ -58,5 +71,10 @@ export const loadPrintConfig = (units) => {
|
|||
config.size.valueTitles[s] = s
|
||||
})
|
||||
|
||||
/*
|
||||
* Don't include cutlist in production until it's ready to go
|
||||
*/
|
||||
if (isProduction) delete config.cutlist
|
||||
|
||||
return config
|
||||
}
|
||||
|
|
|
@ -5,38 +5,17 @@ import {
|
|||
handleExport,
|
||||
ns as exportNs,
|
||||
} from 'shared/components/workbench/exporting/export-handler.mjs'
|
||||
import { useLoadingStatus } from 'shared/hooks/use-loading-status.mjs'
|
||||
import get from 'lodash.get'
|
||||
import { MovablePattern } from 'shared/components/workbench/pattern/movable/index.mjs'
|
||||
import { PrintMenu, ns as menuNs } from './menu.mjs'
|
||||
import { defaultPrintSettings, printSettingsPath } from './config.mjs'
|
||||
import { PrintIcon, RightIcon } from 'shared/components/icons.mjs'
|
||||
import { LoadingContext } from 'shared/context/loading-context.mjs'
|
||||
import { useToast } from 'shared/hooks/use-toast.mjs'
|
||||
import { PatternWithMenu, ns as wrapperNs } from '../pattern-with-menu.mjs'
|
||||
import { V3Wip } from 'shared/components/v3-wip.mjs'
|
||||
import { nsMerge } from 'shared/utils.mjs'
|
||||
|
||||
const viewNs = ['print', ...exportNs]
|
||||
export const ns = [...viewNs, ...menuNs, ...wrapperNs]
|
||||
export const ns = nsMerge(menuNs, wrapperNs, exportNs, 'print', 'status')
|
||||
|
||||
const PageCounter = ({ pattern }) => {
|
||||
const pages = pattern.setStores[0].get('pages', {})
|
||||
const { cols, rows, count } = pages
|
||||
|
||||
return (
|
||||
<div className="flex flex-row font-bold items-center text-2xl justify-center ">
|
||||
<PrintIcon />
|
||||
<span className="ml-2">{count}</span>
|
||||
<span className="mx-6 opacity-50">|</span>
|
||||
<RightIcon />
|
||||
<span className="ml-2">{cols}</span>
|
||||
<span className="mx-6 opacity-50">|</span>
|
||||
<div className="rotate-90">
|
||||
<RightIcon />
|
||||
</div>
|
||||
<span className="ml-2">{rows}</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export const PrintView = ({
|
||||
design,
|
||||
pattern,
|
||||
|
@ -52,7 +31,7 @@ export const PrintView = ({
|
|||
}) => {
|
||||
const { t } = useTranslation(ns)
|
||||
const loading = useContext(LoadingContext)
|
||||
const toast = useToast()
|
||||
const { setLoadingStatus, LoadingStatus } = useLoadingStatus()
|
||||
|
||||
const defaultSettings = defaultPrintSettings(settings.units)
|
||||
// add the pages plugin to the draft
|
||||
|
@ -73,6 +52,7 @@ export const PrintView = ({
|
|||
}
|
||||
|
||||
const exportIt = () => {
|
||||
setLoadingStatus([true, 'generatingPdf'])
|
||||
handleExport({
|
||||
format: pageSettings.size,
|
||||
settings,
|
||||
|
@ -82,43 +62,44 @@ export const PrintView = ({
|
|||
ui,
|
||||
startLoading: loading.startLoading,
|
||||
stopLoading: loading.stopLoading,
|
||||
onComplete: () => {},
|
||||
onError: (err) => toast.error(err.message),
|
||||
onComplete: () => {
|
||||
setLoadingStatus([true, 'pdfReady', true, true])
|
||||
},
|
||||
onError: (err) => {
|
||||
setLoadingStatus([true, 'pdfFailed', true, true])
|
||||
console.log(err)
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<PatternWithMenu
|
||||
{...{
|
||||
settings,
|
||||
ui,
|
||||
update,
|
||||
control: account.control,
|
||||
account,
|
||||
design,
|
||||
setSettings,
|
||||
title: (
|
||||
<div className="flex lg:justify-between items-baseline flex-wrap px-2">
|
||||
<h2 className="text-center lg:text-left capitalize">
|
||||
{t('layoutThing', { thing: design }) + ' ' + t('forPrinting')}
|
||||
</h2>
|
||||
<PageCounter pattern={pattern} />
|
||||
</div>
|
||||
),
|
||||
pattern: (
|
||||
<MovablePattern
|
||||
{...{
|
||||
renderProps,
|
||||
update,
|
||||
immovable: ['pages'],
|
||||
layoutPath: ['layouts', 'print'],
|
||||
showButtons: !ui.hideMovableButtons,
|
||||
}}
|
||||
/>
|
||||
),
|
||||
menu: (
|
||||
<>
|
||||
<V3Wip />
|
||||
<>
|
||||
<LoadingStatus />
|
||||
<PatternWithMenu
|
||||
noHeader
|
||||
{...{
|
||||
settings,
|
||||
ui,
|
||||
update,
|
||||
control: account.control,
|
||||
account,
|
||||
design,
|
||||
setSettings,
|
||||
title: (
|
||||
<h2 className="text-center lg:text-left capitalize">{t('workbench:printLayout')}</h2>
|
||||
),
|
||||
pattern: (
|
||||
<MovablePattern
|
||||
{...{
|
||||
renderProps,
|
||||
update,
|
||||
immovable: ['pages'],
|
||||
layoutPath: ['layouts', 'print'],
|
||||
showButtons: !ui.hideMovableButtons,
|
||||
}}
|
||||
/>
|
||||
),
|
||||
menu: (
|
||||
<PrintMenu
|
||||
{...{
|
||||
design,
|
||||
|
@ -134,9 +115,9 @@ export const PrintView = ({
|
|||
exportIt,
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -1,21 +1,45 @@
|
|||
import {
|
||||
DesignOptions,
|
||||
ns as designMenuNs,
|
||||
} from 'shared/components/workbench/menus/design-options/index.mjs'
|
||||
import {
|
||||
CoreSettings,
|
||||
ClearAllButton,
|
||||
ns as coreMenuNs,
|
||||
} from 'shared/components/workbench/menus/core-settings/index.mjs'
|
||||
import { PrintSettings, ns as printMenuNs } from './settings.mjs'
|
||||
import { PrintActions } from './actions.mjs'
|
||||
import { PrintIcon, CompareIcon } from 'shared/components/icons.mjs'
|
||||
import { Accordion } from 'shared/components/accordion.mjs'
|
||||
import { useTranslation } from 'next-i18next'
|
||||
import { horFlexClasses, capitalize } from 'shared/utils.mjs'
|
||||
|
||||
export const ns = [...coreMenuNs, ...designMenuNs, ...printMenuNs]
|
||||
export const ns = printMenuNs
|
||||
|
||||
const PageCounter = ({ pattern, t, ui, settings }) => {
|
||||
const pages = pattern.setStores[0].get('pages', {})
|
||||
const format = ui.print?.pages?.size
|
||||
? ui.print.pages.size
|
||||
: settings.units === 'imperial'
|
||||
? 'letter'
|
||||
: 'a4'
|
||||
const { cols, rows, count } = pages
|
||||
|
||||
return (
|
||||
<div className="flex flex-row flex-wrap items-center gap-1 mb-2 py-2">
|
||||
<b>{t('workbench:currentPrintLayout')}:</b>
|
||||
<div className="flex flex-row flex-wrap items-center gap-1">
|
||||
<span>
|
||||
{count} {capitalize(format)} {t('workbench:pages')},
|
||||
</span>
|
||||
<span>
|
||||
{cols} {t('workbench:columns')},
|
||||
</span>
|
||||
<span>
|
||||
{rows} {t('workbench:rows')}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex flex-row flex-wrap items-center italic">
|
||||
({t('workbench:xTotalPagesSomeBlank', { total: cols * rows, blank: cols * rows - count })})
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export const PrintMenu = ({
|
||||
design,
|
||||
patternConfig,
|
||||
setSettings,
|
||||
settings,
|
||||
ui,
|
||||
update,
|
||||
|
@ -23,8 +47,9 @@ export const PrintMenu = ({
|
|||
account,
|
||||
DynamicDocs,
|
||||
exportIt,
|
||||
pattern,
|
||||
}) => {
|
||||
const control = account.control
|
||||
const { t } = useTranslation()
|
||||
const menuProps = {
|
||||
design,
|
||||
patternConfig,
|
||||
|
@ -33,15 +58,43 @@ export const PrintMenu = ({
|
|||
language,
|
||||
account,
|
||||
DynamicDocs,
|
||||
control,
|
||||
control: account.control,
|
||||
}
|
||||
|
||||
const sections = [
|
||||
{
|
||||
name: 'printSettings',
|
||||
ns: 'workbench',
|
||||
icon: <PrintIcon className="w-8 h-8" />,
|
||||
menu: <PrintSettings {...menuProps} ui={ui} />,
|
||||
},
|
||||
{
|
||||
name: 'layoutSettings',
|
||||
ns: 'workbench',
|
||||
icon: <CompareIcon className="w-8 h-8" />,
|
||||
menu: <PrintActions {...menuProps} ui={ui} />,
|
||||
},
|
||||
]
|
||||
console.log(ui)
|
||||
return (
|
||||
<nav>
|
||||
<PrintActions {...menuProps} ui={ui} exportIt={exportIt} />
|
||||
<PrintSettings {...menuProps} ui={ui} />
|
||||
<DesignOptions {...menuProps} isFirst={false} />
|
||||
<CoreSettings {...menuProps} />
|
||||
<ClearAllButton setSettings={setSettings} />
|
||||
</nav>
|
||||
<>
|
||||
<PageCounter {...{ pattern, t, ui, settings }} />
|
||||
<button className={`${horFlexClasses} btn btn-primary btn-lg`} onClick={exportIt}>
|
||||
<PrintIcon className="w-8 h-8" />
|
||||
{t('workbench:generatePdf')}
|
||||
</button>
|
||||
<Accordion
|
||||
items={sections.map((section) => [
|
||||
<>
|
||||
<h5 className="flex flex-row gap-2 items-center justify-between w-full">
|
||||
<span>{t(`${section.ns}:${section.name}.t`)}</span>
|
||||
{section.icon}
|
||||
</h5>
|
||||
<p className="text-left">{t(`${section.ns}:${section.name}.d`)}</p>
|
||||
</>,
|
||||
section.menu,
|
||||
])}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -1,9 +1,15 @@
|
|||
import { PrintIcon } from 'shared/components/icons.mjs'
|
||||
import { WorkbenchMenu } from 'shared/components/workbench/menus/shared/index.mjs'
|
||||
import { ListInput, BoolInput, MmInput } from 'shared/components/workbench/menus/shared/inputs.mjs'
|
||||
import { ListValue, BoolValue, MmValue } from 'shared/components/workbench/menus/shared/values.mjs'
|
||||
import {
|
||||
BoolValue,
|
||||
MmValue,
|
||||
HighlightedValue,
|
||||
} from 'shared/components/workbench/menus/shared/values.mjs'
|
||||
import { loadPrintConfig, printSettingsPath } from './config.mjs'
|
||||
import { capitalize } from 'shared/utils.mjs'
|
||||
import get from 'lodash.get'
|
||||
import { PatternIcon } from 'shared/components/icons.mjs'
|
||||
|
||||
const inputs = {
|
||||
size: ListInput,
|
||||
|
@ -14,8 +20,18 @@ const inputs = {
|
|||
}
|
||||
|
||||
const values = {
|
||||
size: ListValue,
|
||||
orientation: ListValue,
|
||||
size: ({ current, changed, config }) => (
|
||||
<HighlightedValue changed={changed}>
|
||||
{capitalize(current ? current : config.dflt)}
|
||||
</HighlightedValue>
|
||||
),
|
||||
orientation: ({ current, changed }) => (
|
||||
<HighlightedValue changed={changed}>
|
||||
<PatternIcon
|
||||
className={`w-6 h-6 text-inherit ${current === 'landscape' ? '-rotate-90' : ''}`}
|
||||
/>
|
||||
</HighlightedValue>
|
||||
),
|
||||
margin: MmValue,
|
||||
coverPage: BoolValue,
|
||||
cutlist: BoolValue,
|
||||
|
|
|
@ -2,6 +2,7 @@ import { useTranslation } from 'next-i18next'
|
|||
import { PanZoomPattern } from 'shared/components/workbench/pan-zoom-pattern.mjs'
|
||||
import { TestMenu, ns as menuNs } from './menu.mjs'
|
||||
import { PatternWithMenu, ns as wrapperNs } from '../pattern-with-menu.mjs'
|
||||
import { Popout } from 'shared/components/popout/index.mjs'
|
||||
|
||||
export const ns = [...menuNs, wrapperNs]
|
||||
|
||||
|
@ -23,24 +24,35 @@ export const TestView = ({
|
|||
|
||||
const renderProps = pattern.getRenderProps()
|
||||
const patternConfig = pattern.getConfig()
|
||||
let placeholder = false
|
||||
|
||||
/*
|
||||
* Translation of the title needs some work
|
||||
*/
|
||||
let title = t('workbench:testDesignOption', {
|
||||
design,
|
||||
option: t(`${design}:${settings.sample?.option}.t`),
|
||||
})
|
||||
let title = t('workbench:chooseATest')
|
||||
if (settings.sample?.type === 'measurement')
|
||||
title = t('workbench:testDesignMeasurement', {
|
||||
design,
|
||||
measurement: t(`measurements:${settings.sample?.measurement}`),
|
||||
})
|
||||
else if (settings.sample?.type === 'option')
|
||||
title = t('workbench:testDesignOption', {
|
||||
design,
|
||||
option: t(`${design}:${settings.sample?.option}.t`),
|
||||
})
|
||||
else if (settings.sample?.type === 'sets')
|
||||
title = t('workbench:testDesignSets', {
|
||||
design,
|
||||
thing: 'fixme views/test/index.mjs',
|
||||
})
|
||||
else
|
||||
placeholder = (
|
||||
<Popout tip>
|
||||
<p>{t('workbench:chooseATestDesc')}</p>
|
||||
<p className="hidden md:block">{t('workbench:chooseATestMenuMsg')}</p>
|
||||
<p className="block md:hidden">{t('workbench:chooseATestMenuMobileMsg')}</p>
|
||||
</Popout>
|
||||
)
|
||||
|
||||
return (
|
||||
<PatternWithMenu
|
||||
|
@ -53,7 +65,7 @@ export const TestView = ({
|
|||
design,
|
||||
setSettings,
|
||||
title: <h2>{title}</h2>,
|
||||
pattern: <PanZoomPattern {...{ renderProps }} />,
|
||||
pattern: placeholder ? placeholder : <PanZoomPattern {...{ renderProps }} />,
|
||||
menu: (
|
||||
<TestMenu
|
||||
{...{
|
||||
|
|
|
@ -10,11 +10,11 @@ import {
|
|||
BulletIcon,
|
||||
UnitsIcon,
|
||||
DetailIcon,
|
||||
IconWrapper,
|
||||
ClearIcon,
|
||||
ResetIcon,
|
||||
UploadIcon,
|
||||
BookmarkIcon,
|
||||
ZoomInIcon,
|
||||
ZoomOutIcon,
|
||||
} from 'shared/components/icons.mjs'
|
||||
import { shownHeaderSelector } from 'shared/components/wrappers/header.mjs'
|
||||
import { useLoadingStatus } from 'shared/hooks/use-loading-status.mjs'
|
||||
|
@ -22,18 +22,6 @@ import { capitalize, shortDate } from 'shared/utils.mjs'
|
|||
|
||||
export const ns = ['common', 'core-settings', 'ui-settings']
|
||||
|
||||
const ZoomInIcon = (props) => (
|
||||
<IconWrapper {...props}>
|
||||
<path d="M21 21l-5.197-5.197m0 0A7.5 7.5 0 105.196 5.196a7.5 7.5 0 0010.607 10.607zM10.5 7.5v6m3-3h-6" />
|
||||
</IconWrapper>
|
||||
)
|
||||
|
||||
const ZoomOutIcon = (props) => (
|
||||
<IconWrapper {...props}>
|
||||
<path d="M21 21l-5.197-5.197m0 0A7.5 7.5 0 105.196 5.196a7.5 7.5 0 0010.607 10.607zM13.5 10.5h-6" />
|
||||
</IconWrapper>
|
||||
)
|
||||
|
||||
const IconButton = ({ Icon, onClick, dflt = true, title, hide = false, extraClasses = '' }) => (
|
||||
<div className="tooltip tooltip-bottom tooltip-primary flex items-center" data-tip={title}>
|
||||
<button
|
||||
|
@ -49,13 +37,13 @@ const IconButton = ({ Icon, onClick, dflt = true, title, hide = false, extraClas
|
|||
)
|
||||
|
||||
const smZoomClasses =
|
||||
'[.mobile-menubar_&]:btn [.mobile-menubar_&]:btn-secondary [.mobile-menubar_&]:btn-circle [.mobile-menubar_&]:my-2'
|
||||
'[.mobile-menubar_&]:btn [.mobile-menubar_&]:btn-secondary [.mobile-menubar_&]:btn-circle [.mobile-menubar_&]:my-1'
|
||||
const ZoomButtons = ({ t, zoomFunctions, zoomed }) => {
|
||||
if (!zoomFunctions) return null
|
||||
return (
|
||||
<div className="flex flex-col lg:flex-row items-center lg:content-center lg:gap-4">
|
||||
<IconButton
|
||||
Icon={ClearIcon}
|
||||
Icon={ResetIcon}
|
||||
onClick={zoomFunctions.reset}
|
||||
title={t('resetZoom')}
|
||||
hide={!zoomed}
|
||||
|
|
|
@ -5,11 +5,14 @@ import { useBackend } from 'shared/hooks/use-backend.mjs'
|
|||
import { roles } from 'config/roles.mjs'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { Loading } from 'shared/components/spinner.mjs'
|
||||
import { horFlexClasses } from 'shared/utils.mjs'
|
||||
import { LockIcon, PlusIcon } from 'shared/components/icons.mjs'
|
||||
import { ConsentForm, ns as gdprNs } from 'shared/components/gdpr/form.mjs'
|
||||
|
||||
export const ns = ['auth']
|
||||
export const ns = ['auth', gdprNs]
|
||||
|
||||
const Wrap = ({ children }) => (
|
||||
<div className="m-auto max-w-xl text-center mt-24 p-8">{children}</div>
|
||||
<div className="m-auto max-w-xl text-center mt-8 p-8">{children}</div>
|
||||
)
|
||||
|
||||
const ContactSupport = ({ t }) => (
|
||||
|
@ -23,13 +26,15 @@ const ContactSupport = ({ t }) => (
|
|||
const AuthRequired = ({ t, banner }) => (
|
||||
<Wrap>
|
||||
{banner}
|
||||
<h1>{t('authRequired')}</h1>
|
||||
<h2>{t('authRequired')}</h2>
|
||||
<p>{t('membersOnly')}</p>
|
||||
<div className="flex flex-row items-center justify-center gap-4 mt-8">
|
||||
<Link href="/signup" className="btn btn-primary w-32">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-2 mt-8">
|
||||
<Link href="/signup" className={`${horFlexClasses} btn btn-secondary w-full`}>
|
||||
<PlusIcon />
|
||||
{t('signUp')}
|
||||
</Link>
|
||||
<Link href="/signin" className="btn btn-primary btn-outline w-32">
|
||||
<Link href="/signin" className={`${horFlexClasses} btn btn-secondary btn-outline w-full`}>
|
||||
<LockIcon />
|
||||
{t('signIn')}
|
||||
</Link>
|
||||
</div>
|
||||
|
@ -86,21 +91,38 @@ const RoleLacking = ({ t, requiredRole, role, banner }) => (
|
|||
</Wrap>
|
||||
)
|
||||
|
||||
const ConsentLacking = ({ t, banner }) => (
|
||||
<Wrap>
|
||||
{banner}
|
||||
<h1>{t('consentLacking')}</h1>
|
||||
<p>{t('membersOnly')}</p>
|
||||
<div className="flex flex-row items-center justify-center gap-4 mt-8">
|
||||
<Link href="/signup" className="btn btn-primary w-32">
|
||||
{t('signUp')}
|
||||
</Link>
|
||||
<Link href="/signin" className="btn btn-primary btn-outline w-32">
|
||||
{t('signIn')}
|
||||
</Link>
|
||||
</div>
|
||||
</Wrap>
|
||||
)
|
||||
const ConsentLacking = ({ banner, refresh }) => {
|
||||
const { setAccount, setToken, setSeenUser } = useAccount()
|
||||
const backend = useBackend()
|
||||
|
||||
const updateConsent = async ({ consent1, consent2 }) => {
|
||||
let consent = 0
|
||||
if (consent1) consent = 1
|
||||
if (consent1 && consent2) consent = 2
|
||||
if (consent > 0) {
|
||||
const result = await backend.updateConsent(consent)
|
||||
console.log({ result })
|
||||
if (result.success) {
|
||||
setToken(result.data.token)
|
||||
setAccount(result.data.account)
|
||||
setSeenUser(result.data.account.username)
|
||||
refresh()
|
||||
} else {
|
||||
console.log('something went wrong', result)
|
||||
refresh()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Wrap>
|
||||
<div className="text-left">
|
||||
{banner}
|
||||
<ConsentForm submit={updateConsent} />
|
||||
</div>
|
||||
</Wrap>
|
||||
)
|
||||
}
|
||||
|
||||
export const AuthWrapper = ({ children, requiredRole = 'user' }) => {
|
||||
const { t } = useTranslation(ns)
|
||||
|
@ -109,6 +131,8 @@ export const AuthWrapper = ({ children, requiredRole = 'user' }) => {
|
|||
|
||||
const [ready, setReady] = useState(false)
|
||||
const [impersonating, setImpersonating] = useState(false)
|
||||
const [error, setError] = useState(false)
|
||||
const [refreshCount, setRefreshCount] = useState(0)
|
||||
|
||||
/*
|
||||
* Avoid hydration errors
|
||||
|
@ -122,17 +146,33 @@ export const AuthWrapper = ({ children, requiredRole = 'user' }) => {
|
|||
user: account.username,
|
||||
})
|
||||
}
|
||||
setReady(true)
|
||||
}
|
||||
const verifyUser = async () => {
|
||||
const result = await backend.ping()
|
||||
if (!result.success) signOut()
|
||||
if (!result.success) {
|
||||
if (result.data?.error?.error) setError(result.data.error.error)
|
||||
else signOut()
|
||||
}
|
||||
setReady(true)
|
||||
}
|
||||
if (admin && admin.token) verifyAdmin()
|
||||
if (token) verifyUser()
|
||||
setReady(true)
|
||||
}, [admin, token])
|
||||
else setReady(true)
|
||||
}, [admin, token, refreshCount])
|
||||
|
||||
if (!ready) return <Loading />
|
||||
const refresh = () => {
|
||||
setRefreshCount(refreshCount + 1)
|
||||
setError(false)
|
||||
}
|
||||
|
||||
if (!ready)
|
||||
return (
|
||||
<>
|
||||
<p>not ready</p>
|
||||
<Loading />
|
||||
</>
|
||||
)
|
||||
|
||||
const banner = impersonating ? (
|
||||
<div className="bg-warning rounded-lg shadow py-4 px-6 flex flex-row items-center gap-4 justify-between">
|
||||
|
@ -148,13 +188,13 @@ export const AuthWrapper = ({ children, requiredRole = 'user' }) => {
|
|||
const childProps = { t, banner }
|
||||
|
||||
if (!token || !account.username) return <AuthRequired {...childProps} />
|
||||
if (account.status !== 1) {
|
||||
if (account.status === 0) return <AccountInactive {...childProps} />
|
||||
if (account.status === -1) return <AccountDisabled {...childProps} />
|
||||
if (account.status === -2) return <AccountProhibited {...childProps} />
|
||||
if (error) {
|
||||
if (error === 'accountInactive') return <AccountInactive {...childProps} />
|
||||
if (error === 'accountDisabled') return <AccountDisabled {...childProps} />
|
||||
if (error === 'accountBlocked') return <AccountProhibited {...childProps} />
|
||||
if (error === 'consentLacking') return <ConsentLacking {...childProps} refresh={refresh} />
|
||||
return <AccountStatusUnknown {...childProps} />
|
||||
}
|
||||
if (account.consent < 1) return <ConsentLacking {...childProps} />
|
||||
|
||||
if (!roles.levels[account.role] || roles.levels[account.role] < roles.levels[requiredRole]) {
|
||||
return <RoleLacking {...childProps} role={account.role} requiredRole={requiredRole} />
|
||||
|
|
|
@ -20,6 +20,7 @@ export const ModalWrapper = ({
|
|||
keepOpenOnClick = false,
|
||||
slideFrom = 'left',
|
||||
keepOpenOnSwipe = false,
|
||||
fullWidth = false,
|
||||
}) => {
|
||||
const { clearModal } = useContext(ModalContext)
|
||||
const [animate, setAnimate] = useState('in')
|
||||
|
@ -68,7 +69,9 @@ export const ModalWrapper = ({
|
|||
children
|
||||
) : (
|
||||
<div
|
||||
className={`bg-base-100 p-4 lg:px-8 lg:rounded-lg lg:shadow-lg max-h-full overflow-auto`}
|
||||
className={`bg-base-100 p-4 lg:px-8 lg:rounded-lg lg:shadow-lg max-h-full overflow-auto ${
|
||||
fullWidth ? 'w-full' : ''
|
||||
}`}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
|
|
4
sites/shared/components/wrappers/production.mjs
Normal file
4
sites/shared/components/wrappers/production.mjs
Normal file
|
@ -0,0 +1,4 @@
|
|||
import { isProduction } from 'shared/freesewing.config.mjs'
|
||||
|
||||
export const NotInProduction = ({ children }) => (isProduction ? null : children)
|
||||
export const OnlyInProduction = ({ children }) => (isProduction ? children : null)
|
7
sites/shared/components/wrappers/table.mjs
Normal file
7
sites/shared/components/wrappers/table.mjs
Normal file
|
@ -0,0 +1,7 @@
|
|||
/*
|
||||
* Tables on mobile will almost always break the layout
|
||||
* unless we set the overflow behaviour explicitly
|
||||
*/
|
||||
export const TableWrapper = ({ children }) => (
|
||||
<div className="max-w-full overflow-x-auto">{children}</div>
|
||||
)
|
Loading…
Add table
Add a link
Reference in a new issue