1
0
Fork 0

Merge branch 'develop' into i18n

This commit is contained in:
Joost De Cock 2023-09-02 09:35:17 +02:00 committed by GitHub
commit 1fd323e109
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
77 changed files with 2410 additions and 1832 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View 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>
</>
)
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -68,6 +68,7 @@ export const CutView = ({
return (
<PatternWithMenu
noHeader
{...{
settings,
ui,

View file

@ -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,
])}

View file

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

View file

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

View file

@ -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>,
],
]}
/>
</>
)
}

View file

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

View file

@ -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,
}}
/>
</>
),
}}
/>
),
}}
/>
</>
)
}

View file

@ -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,
])}
/>
</>
)
}

View file

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

View file

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

View file

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

View file

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

View file

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

View 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)

View 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>
)