1
0
Fork 0

wip: More account logic

This commit is contained in:
joostdecock 2024-12-24 15:09:46 +01:00
parent 132efebe5b
commit f548e1ed8f
39 changed files with 523 additions and 2952 deletions

View file

@ -76,6 +76,7 @@ packageJson:
# Components # Components
"./components/Account": "./components/Account/index.mjs" "./components/Account": "./components/Account/index.mjs"
"./components/Breadcrumbs": "./components/Breadcrumbs/index.mjs" "./components/Breadcrumbs": "./components/Breadcrumbs/index.mjs"
"./components/Button": "./components/Button/index.mjs"
"./components/Control": "./components/Control/index.mjs" "./components/Control": "./components/Control/index.mjs"
"./components/CopyToClipboard": "./components/CopyToClipboard/index.mjs" "./components/CopyToClipboard": "./components/CopyToClipboard/index.mjs"
"./components/Docusaurus": "./components/Docusaurus/index.mjs" "./components/Docusaurus": "./components/Docusaurus/index.mjs"

View file

@ -0,0 +1,53 @@
// Context
import { LoadingStatusContext } from '@freesewing/react/context/LoadingStatus'
// Hooks
import React, { useState, useContext } from 'react'
import { useAccount } from '@freesewing/react/hooks/useAccount'
import { useBackend } from '@freesewing/react/hooks/useBackend'
// Components
import { Link as WebLink } from '@freesewing/react/components/Link'
import { DownloadIcon } from '@freesewing/react/components/Icon'
import { Popout } from '@freesewing/react/components/Popout'
import { IconButton } from '@freesewing/react/components/Button'
/*
* Component for the account/actions/export page
*/
export const Export = () => {
// Hooks
const backend = useBackend()
const { setLoadingStatus } = useContext(LoadingStatusContext)
// State
const [link, setLink] = useState()
// Helper method to export account
const exportData = async () => {
setLoadingStatus([true, 'Contacting backend'])
const [status, body] = await backend.exportAccount()
if (status === 200) {
setLink(body.data)
setLoadingStatus([true, 'All done', true, true])
} else setLoadingStatus([true, 'Something went wrong, please report this', true, false])
}
return (
<div className="max-w-xl">
{link ? (
<Popout link>
<h5>Your data was exported and is available for download at the following location:</h5>
<p className="text-lg">
<WebLink href={link}>{link}</WebLink>
</p>
</Popout>
) : null}
<p>Click below to export your personal FreeSewing data</p>
<IconButton onClick={exportData} title="Export your data">
<DownloadIcon />
Export Your Data
</IconButton>
</div>
)
}

View file

@ -0,0 +1,181 @@
// Context
import { LoadingStatusContext } from '@freesewing/react/context/LoadingStatus'
// Hooks
import React, { useState, useContext } from 'react'
import { useAccount } from '@freesewing/react/hooks/useAccount'
import { useBackend } from '@freesewing/react/hooks/useBackend'
// Components
import { Link as WebLink } from '@freesewing/react/components/Link'
import { SaveIcon } from '@freesewing/react/components/Icon'
import { FileInput } from '@freesewing/react/components/Input'
import { Popout } from '@freesewing/react/components/Popout'
import { Yaml } from '@freesewing/react/components/Yaml'
/*
* Component for the account/bio page
*
* @params {object} props - All React props
* @params {bool} props.welcome - Set to true to use this component on the welcome page
* @params {function} props.Link - A framework specific Link component for client-side routing
*/
export const ImportSet = () => {
// Hooks
const backend = useBackend()
const { account } = useAccount()
const { setLoadingStatus } = useContext(LoadingStatusContext)
// Helper method to upload/save a set
const uploadSet = async (upload) => {
setLoadingStatus([true, 'Uploading data'])
let data
try {
const chunks = upload.split(',')
if (chunks[0].includes('json')) data = JSON.parse(atob(chunks[1]))
else data = yaml.parse(atob(chunks[1]))
if (!Array.isArray(data)) data = [data]
/*
* Treat each set
*/
for (const set of data) {
if (set.measurements || set.measies) {
const name = set.name || 'J. Doe'
setLoadingStatus([true, `Importing ${name}`])
const [status, body] = await backend.createSet({
name: set.name || 'J. Doe',
units: set.units || 'metric',
notes: set.notes || '',
measies: set.measurements || set.measies,
userId: account.id,
})
if (status === 200) setLoadingStatus([true, `Imported ${name}`, true, true])
else setLoadingStatus([true, `Import of ${name} failed`, true, false])
} else {
setLoadingStatus([true, `Invalid format`, true, false])
}
}
} catch (err) {
console.log(err)
setLoadingStatus([true, `Import of ${name || 'file'} failed`, true, false])
}
}
return (
<div className="w-full">
<FileInput
label="Measurements file (YAML / JSON)"
update={uploadSet}
current=""
id="file"
dropzoneConfig={{
accept: {
'application/json': ['.json'],
'application/yaml': ['.yaml', '.yml'],
},
maxFiles: 1,
multiple: false,
}}
/>
<Popout tip>
<p>
To import a measurement set, you should have a JSON or YAML file that has the following
structure:
</p>
<Yaml
js={{
name: 'Joost',
units: 'metric',
notes: 'These are my notes',
measurements: { biceps: 335, wrist: 190 },
}}
title="measurements.yaml"
/>
<p>
Your file can either contain a single measurements set, or an array/list of multiple
measurements sets.
</p>
</Popout>
</div>
)
}
/*
// Hooks
const { account, setAccount } = useAccount()
const backend = useBackend()
const { setLoadingStatus } = useContext(LoadingStatusContext)
// State
const [bio, setBio] = useState(account.bio)
// Helper method to save bio
const save = async () => {
setLoadingStatus([true, 'Saving bio'])
const [status, body] = await backend.updateAccount({ bio })
if (status === 200 && body.result === 'success') {
setAccount(body.account)
setLoadingStatus([true, 'Bio updated', true, true])
} else setLoadingStatus([true, 'Something went wrong. Please report this', true, true])
}
// Next step in the onboarding
const nextHref =
welcomeSteps[account.control].length > 5
? '/welcome/' + welcomeSteps[account.control][6]
: '/docs/about/guide'
return (
<div className="w-full">
<h6>Tell people a little bit about yourself.</h6>
<MarkdownInput id="account-bio" label="Bio" update={setBio} current={bio} placeholder="Bio" />
<p className="text-right">
<button className="daisy-btn daisy-btn-primary w-full lg:w-auto mt-8" onClick={save}>
<SaveIcon /> Save Bio
</button>
</p>
{welcome ? (
<>
<ContinueButton btnProps={{ href: nextHref }} link />
{welcomeSteps[account.control].length > 0 ? (
<>
<progress
className="progress progress-primary w-full mt-12"
value={600 / welcomeSteps[account.control].length}
max="100"
></progress>
<span className="pt-4 text-sm font-bold opacity-50">
6 / {welcomeSteps[account.control].length}
</span>
<Icons
done={welcomeSteps[account.control].slice(0, 5)}
todo={welcomeSteps[account.control].slice(6)}
current="bio"
/>
</>
) : null}
</>
) : null}
</div>
)
}
// Dependencies
import { useContext } from 'react'
import { useTranslation } from 'next-i18next'
// Context
import { LoadingStatusContext } from 'shared/context/loading-status-context.mjs'
// Hooks
import { useAccount } from 'shared/hooks/use-account.mjs'
import { useBackend } from 'shared/hooks/use-backend.mjs'
// Components
import { FileInput } from 'shared/components/inputs.mjs'
import { Popout } from 'shared/components/popout/index.mjs'
import { linkClasses } from 'shared/components/link.mjs'
import yaml from 'yaml'
export const ns = ['account', 'status']
export const Importer = () => {
// Hooks
*/

View file

@ -0,0 +1,50 @@
// Context
import { LoadingStatusContext } from '@freesewing/react/context/LoadingStatus'
// Hooks
import React, { useState, useContext } from 'react'
import { useAccount } from '@freesewing/react/hooks/useAccount'
import { useBackend } from '@freesewing/react/hooks/useBackend'
// Components
import { Link as WebLink } from '@freesewing/react/components/Link'
import { ReloadIcon } from '@freesewing/react/components/Icon'
import { Popout } from '@freesewing/react/components/Popout'
import { IconButton } from '@freesewing/react/components/Button'
/*
* Component for the account/actions/export page
*/
export const Reload = () => {
// Hooks
const backend = useBackend()
const { setAccount } = useAccount()
const { setLoadingStatus } = useContext(LoadingStatusContext)
// Helper method to reload account
const reload = async () => {
setLoadingStatus([true, 'Contacting backend'])
const [status, body] = await backend.reloadAccount()
if (status === 200) {
setAccount(body.account)
setLoadingStatus([true, 'All done', true, true])
} else setLoadingStatus([true, 'This did not go as planned. Please report this.', true, false])
}
return (
<div className="w-full">
<p>
The data stored in your browser can sometimes get out of sync with the data stored in our
backend.
</p>
<p>
This lets you reload your account data from the backend. It has the same effect as signing
out, and then signing in again.
</p>
<IconButton onClick={reload}>
<ReloadIcon />
Reload Account Data
</IconButton>
</div>
)
}

View file

@ -0,0 +1,81 @@
// Context
import { LoadingStatusContext } from '@freesewing/react/context/LoadingStatus'
import { ModalContext } from '@freesewing/react/context/Modal'
// Hooks
import React, { useState, useContext } from 'react'
import { useAccount } from '@freesewing/react/hooks/useAccount'
import { useBackend } from '@freesewing/react/hooks/useBackend'
// Components
import { Link as WebLink } from '@freesewing/react/components/Link'
import { BackIcon as ExitIcon, TrashIcon } from '@freesewing/react/components/Icon'
import { Popout } from '@freesewing/react/components/Popout'
import { IconButton } from '@freesewing/react/components/Button'
import { ModalWrapper } from '@freesewing/react/components/Modal'
/*
* Component for the account/actions/remove page
*/
export const Remove = () => {
// Hooks
const backend = useBackend()
const { signOut, account } = useAccount()
// Context
const { setLoadingStatus } = useContext(LoadingStatusContext)
const { setModal, clearModal } = useContext(ModalContext)
// Helper method to remove the account
const removeAccount = async () => {
setLoadingStatus([true, 'Talking to the backend'])
const result = await backend.removeAccount()
if (result.success) {
setLoadingStatus([true, 'Done. Or rather, gone.', true, true])
signOut()
} else setLoadingStatus([true, 'An error occured. Please report this', true, false])
}
if (account.control === 5)
return (
<>
<p>This button is red for a reason.</p>
<IconButton onClick={removeAccount} color="error">
<TrashIcon />
Remove your FreeSewing account
</IconButton>
</>
)
return (
<div className="w-full">
<IconButton
onClick={() =>
setModal(
<ModalWrapper keepOpenOnClick>
<div className="text-center w-full">
<h2>There is no way back from this</h2>
<p>If this is what you want, then go ahead.</p>
<IconButton onClick={removeAccount} color="error" className="mx-auto">
<TrashIcon />
Remove your FreeSewing account
</IconButton>
<IconButton
onClick={clearModal}
color="primary"
className="mx-auto daisy-btn-outline mt-4"
>
<ExitIcon />
Back to safety
</IconButton>
</div>
</ModalWrapper>
)
}
>
<TrashIcon />
Remove your FreeSewing account
</IconButton>
</div>
)
}

View file

@ -0,0 +1,98 @@
// Dependencies
import { linkClasses } from '@freesewing/utils'
// Context
import { LoadingStatusContext } from '@freesewing/react/context/LoadingStatus'
import { ModalContext } from '@freesewing/react/context/Modal'
// Hooks
import React, { useState, useContext } from 'react'
import { useAccount } from '@freesewing/react/hooks/useAccount'
import { useBackend } from '@freesewing/react/hooks/useBackend'
// Components
import { Link as WebLink } from '@freesewing/react/components/Link'
import { BackIcon, NoIcon } from '@freesewing/react/components/Icon'
import { Popout } from '@freesewing/react/components/Popout'
import { IconButton } from '@freesewing/react/components/Button'
import { ModalWrapper } from '@freesewing/react/components/Modal'
/*
* Component for the account/actions/restrict page
*/
export const Restrict = ({ Link = false }) => {
if (!Link) Link = WebLink
// Hooks
const backend = useBackend()
const { signOut, account } = useAccount()
// Context
const { setLoadingStatus } = useContext(LoadingStatusContext)
const { setModal, clearModal } = useContext(ModalContext)
// Helper method to restrict the account
const restrictAccount = async () => {
setLoadingStatus([true, 'Talking to the backend'])
const [status, body] = await backend.restrictAccount()
if (status === 200) {
setLoadingStatus([true, 'Done. Consider yourself restrcited.', true, true])
signOut()
} else setLoadingStatus([true, 'An error occured. Please report this', true, false])
}
if (account.control === 5)
return (
<>
<p>This button is red for a reason.</p>
<IconButton onClick={restrictAccount} color="error">
<Nocon />
Remove your FreeSewing account
</IconButton>
</>
)
return (
<div className="w-full">
<p>
The GDPR guarantees{' '}
<Link href="/docs/about/rights/#the-right-to-restrict-processing" className={linkClasses}>
your right to restrict processing
</Link>{' '}
of your personal data.
</p>
<p>This will disable your account, but not remove any data.</p>
<IconButton
onClick={() =>
setModal(
<ModalWrapper keepOpenOnClick>
<div className="text-center w-full max-w-xl">
<h2>Proceed with caution</h2>
<p>
While no data will be removed, this will disable your account. Furthermore, you
can not undo this on your own, but will have to contact support when you want to
restore access to your account.
</p>
<IconButton onClick={restrictAccount} color="error" className="mx-auto">
<NoIcon stroke={3} />
Restrict processing of your FreeSewing data
</IconButton>
<IconButton
onClick={clearModal}
color="primary"
className="mx-auto daisy-btn-outline mt-4"
>
<BackIcon />
Back to safety
</IconButton>
</div>
</ModalWrapper>
)
}
>
<NoIcon stroke={3} />
Restrict processing of your FreeSewing data
</IconButton>
</div>
)
}

View file

@ -1,408 +0,0 @@
// Dependencies
import { useState, useEffect, useContext } from 'react'
import { useTranslation } from 'next-i18next'
import { DateTime } from 'luxon'
import { CopyToClipboard } from 'react-copy-to-clipboard'
import { shortDate, formatNumber } from 'shared/utils.mjs'
// Context
import { LoadingStatusContext } from 'shared/context/loading-status-context.mjs'
// Hooks
import { useAccount } from 'shared/hooks/use-account.mjs'
import { useBackend } from 'shared/hooks/use-backend.mjs'
import { useRouter } from 'next/router'
// Components
import { BackToAccountButton, DisplayRow, NumberBullet } from './shared.mjs'
import { Popout } from 'shared/components/popout/index.mjs'
import { LeftIcon, PlusIcon, CopyIcon, RightIcon, TrashIcon } from 'shared/components/icons.mjs'
import { Link, linkClasses } from 'shared/components/link.mjs'
import { StringInput, ListInput, FormControl } from 'shared/components/inputs.mjs'
import { DynamicMdx } from 'shared/components/mdx/dynamic.mjs'
export const ns = ['account', 'status']
const ExpiryPicker = ({ t, expires, setExpires }) => {
const router = useRouter()
const { locale } = router
const [months, setMonths] = useState(1)
// Run update when component mounts
useEffect(() => update(months), [])
const update = (evt) => {
const value = typeof evt === 'number' ? evt : evt.target.value
setExpires(DateTime.now().plus({ months: value }))
setMonths(value)
}
return (
<>
<div className="flex flex-row gap-2 items-center">
<input
type="range"
min="0"
max={24}
value={months}
className="range range-secondary"
onChange={update}
/>
</div>
<Popout note compact>
{t('keyExpiresDesc')}
<b> {shortDate(locale, expires)}</b>
</Popout>
</>
)
}
const CopyInput = ({ text }) => {
const { t } = useTranslation(['status'])
const { setLoadingStatus } = useContext(LoadingStatusContext)
const [copied, setCopied] = useState(false)
const showCopied = () => {
setCopied(true)
setLoadingStatus([true, t('copiedToClipboard'), true, true])
window.setTimeout(() => setCopied(false), 2000)
}
return (
<div className="flex flez-row gap-2 items-center w-full">
<input
readOnly
value={text}
className="input w-full input-bordered flex flex-row"
type="text"
/>
<CopyToClipboard text={text} onCopy={showCopied}>
<button className={`btn ${copied ? 'btn-success' : 'btn-secondary'}`}>
<CopyIcon />
</button>
</CopyToClipboard>
</div>
)
}
export const Apikey = ({ apikey, setId }) => {
const { t } = useTranslation(ns)
const router = useRouter()
const { locale } = router
return apikey ? (
<div>
<DisplayRow title={t('keyName')}>{apikey.name}</DisplayRow>
<DisplayRow title={t('created')}>{shortDate(locale, apikey.createdAt)}</DisplayRow>
<DisplayRow title={t('expires')}>{shortDate(locale, apikey.expiresAt)}</DisplayRow>
<DisplayRow title="Key ID">{apikey.key}</DisplayRow>
<div className="flex flex-row flex-wrap md:gap-2 md:items-center md:justify-between mt-8">
<button
className="w-full md:w-auto btn btn-secondary pr-6 flex flex-row items-center gap-2"
onClick={() => setId(null)}
>
<LeftIcon />
{t('apikeys')}
</button>
</div>
</div>
) : null
}
const ShowKey = ({ apikey, t, clear }) => {
const router = useRouter()
const { locale } = router
return (
<div>
<Popout warning compact>
{t('keySecretWarning')}
</Popout>
<DisplayRow title={t('keyName')}>{apikey.name}</DisplayRow>
<DisplayRow title={t('created')}>{shortDate(locale, apikey.createdAt)}</DisplayRow>
<DisplayRow title={t('created')}>{shortDate(locale, apikey.expiresAt)}</DisplayRow>
<DisplayRow title="Key ID">
<CopyInput text={apikey.key} />
</DisplayRow>
<DisplayRow title="Key Secret">
<CopyInput text={apikey.secret} />
</DisplayRow>
<div className="flex flex-row flex-wrap md:gap-2 md:items-center md:justify-between mt-8">
<button
className="w-full md:w-auto btn btn-secondary pr-6 flex flex-row items-center gap-2"
onClick={() => router.push('/account/apikeys')}
>
<LeftIcon />
{t('apikeys')}
</button>
<button className="btn btn-primary w-full mt-2 md:w-auto md:mt-0" onClick={clear}>
<PlusIcon />
{t('newApikey')}
</button>
</div>
</div>
)
}
const NewKey = ({ account, setGenerate, backend }) => {
const [name, setName] = useState('')
const [level, setLevel] = useState(1)
const [expires, setExpires] = useState(Date.now())
const [apikey, setApikey] = useState(false)
const { setLoadingStatus } = useContext(LoadingStatusContext)
const { t, i18n } = useTranslation(ns)
const docs = {}
for (const option of ['name', 'expiry', 'level']) {
docs[option] = (
<DynamicMdx language={i18n.language} slug={`docs/about/site/apikeys/${option}`} />
)
}
const levels = account.role === 'admin' ? [0, 1, 2, 3, 4, 5, 6, 7, 8] : [0, 1, 2, 3, 4]
const createKey = async () => {
setLoadingStatus([true, 'processingUpdate'])
const result = await backend.createApikey({
name,
level,
expiresIn: Math.floor((expires.valueOf() - Date.now().valueOf()) / 1000),
})
if (result.success) {
setLoadingStatus([true, 'nailedIt', true, true])
setApikey(result.data.apikey)
} else setLoadingStatus([true, 'backendError', true, false])
}
const clear = () => {
setApikey(false)
setGenerate(false)
setName('')
setLevel(1)
}
return (
<div>
{apikey ? (
<ShowKey {...{ apikey, t, clear }} />
) : (
<>
<StringInput
id="apikey-name"
label={t('keyName')}
docs={docs.name}
current={name}
update={setName}
valid={(val) => val.length > 0}
placeholder={'Alicia Key'}
/>
<FormControl label={t('keyExpires')} docs={docs.expiry}>
<ExpiryPicker {...{ t, expires, setExpires }} />
</FormControl>
<ListInput
id="apikey-level"
label={t('keyLevel')}
docs={docs.level}
list={levels.map((l) => ({
val: l,
label: (
<div className="flex flex-row items-center w-full justify-between">
<span>{t(`keyLevel${l}`)}</span>
<NumberBullet nr={l} color="secondary" />
</div>
),
}))}
current={level}
update={setLevel}
/>
<div className="flex flex-row gap-2 items-center w-full my-8">
<button
className="btn btn-primary capitalize w-full md:w-auto"
disabled={name.length < 1}
onClick={createKey}
>
{t('newApikey')}
</button>
</div>
</>
)}
</div>
)
}
// Component for the 'new/apikey' page
export const NewApikey = () => {
// Hooks
const { account } = useAccount()
const backend = useBackend()
// State
const [generate, setGenerate] = useState(false)
const [added, setAdded] = useState(0)
// Helper method to force refresh
const keyAdded = () => setAdded(added + 1)
return (
<div className="max-w-2xl xl:pl-4">
<NewKey
{...{
account,
generate,
setGenerate,
backend,
keyAdded,
}}
/>
</div>
)
}
// Component for the account/apikeys page
export const Apikeys = ({ setId }) => {
const router = useRouter()
const { locale } = router
// Hooks
const { account } = useAccount()
const backend = useBackend()
const { t } = useTranslation(ns)
const { setLoadingStatus, LoadingProgress } = useContext(LoadingStatusContext)
// State
const [keys, setKeys] = useState([])
const [selected, setSelected] = useState({})
const [refresh, setRefresh] = useState(0)
// Helper var to see how many are selected
const selCount = Object.keys(selected).length
// Effects
useEffect(() => {
const getApikeys = async () => {
const result = await backend.getApikeys()
if (result.success) setKeys(result.data.apikeys)
}
getApikeys()
}, [refresh])
// Helper method to toggle single selection
const toggleSelect = (id) => {
const newSelected = { ...selected }
if (newSelected[id]) delete newSelected[id]
else newSelected[id] = 1
setSelected(newSelected)
}
// Helper method to toggle select all
const toggleSelectAll = () => {
if (selCount === keys.length) setSelected({})
else {
const newSelected = {}
for (const key of keys) newSelected[key.id] = 1
setSelected(newSelected)
}
}
// Helper to delete one or more apikeys
const removeSelectedApikeys = async () => {
let i = 0
for (const key in selected) {
i++
await backend.removeApikey(key)
setLoadingStatus([
true,
<LoadingProgress val={i} max={selCount} msg={t('removingApikeys')} key="linter" />,
])
}
setSelected({})
setRefresh(refresh + 1)
setLoadingStatus([true, 'nailedIt', true, true])
}
return (
<div className="max-w-4xl xl:pl-4">
<p className="text-center md:text-right">
<Link
className="btn btn-primary capitalize w-full md:w-auto"
bottom
primary
href="/new/apikey"
>
<PlusIcon />
{t('newApikey')}
</Link>
</p>
<button className="btn btn-error" onClick={removeSelectedApikeys} disabled={selCount < 1}>
<TrashIcon /> {selCount} {t('apikeys')}
</button>
<table className="table table-auto">
<thead className="border border-base-300 border-b-2 border-t-0 border-x-0">
<tr className="b">
<th className="text-base-300 text-base">
<input
type="checkbox"
className="checkbox checkbox-secondary"
onClick={toggleSelectAll}
checked={keys.length === selCount}
/>
</th>
<th className="text-base-300 text-base">{t('keyName')}</th>
<th className="text-base-300 text-base">
<span className="hidden md:inline">{t('keyLevel')}</span>
<span role="img" className="inline md:hidden">
🔐
</span>
</th>
<th className="text-base-300 text-base">{t('keyExpires')}</th>
<th className="text-base-300 text-base hidden md:block">{t('apiCalls')}</th>
</tr>
</thead>
<tbody>
{keys.map((apikey, i) => (
<tr key={i}>
<td className="text-base font-medium">
<input
type="checkbox"
checked={selected[apikey.id] ? true : false}
className="checkbox checkbox-secondary"
onClick={() => toggleSelect(apikey.id)}
/>
</td>
<td className="text-base font-medium">
<button className={linkClasses} onClick={() => setId(apikey.id)}>
{apikey.name}
</button>
</td>
<td className="text-base font-medium">
{apikey.level}
<small className="hidden md:inline pl-2 text-base-300 italic">
({t(`keyLevel${apikey.level}`)})
</small>
</td>
<td className="text-base font-medium">
{shortDate(locale, apikey.expiresAt, false)}
</td>
<td className="text-base font-medium hidden md:block">
{formatNumber(apikey.calls)}
</td>
</tr>
))}
</tbody>
</table>
<BackToAccountButton />
{account.control < 5 ? (
<Popout link>
<h5>{t('keyDocsTitle')}</h5>
<p>{t('keyDocsMsg')}</p>
<p className="text-right">
<a
className="btn btn-secondary mt-2"
href="https://freesewing.dev/reference/backend/apikeys"
>
FreeSewing.dev
<RightIcon />
</a>
</p>
</Popout>
) : null}
</div>
)
}

View file

@ -1,98 +0,0 @@
// Dependencies
import { useState, useContext } from 'react'
import { useTranslation } from 'next-i18next'
// Context
import { LoadingStatusContext } from 'shared/context/loading-status-context.mjs'
// Hooks
import { useAccount } from 'shared/hooks/use-account.mjs'
import { useBackend } from 'shared/hooks/use-backend.mjs'
// Components
import { Icons, welcomeSteps, BackToAccountButton } from './shared.mjs'
import { SaveSettingsButton } from 'shared/components/buttons/save-settings-button.mjs'
import { ContinueButton } from 'shared/components/buttons/continue-button.mjs'
import { MarkdownInput } from 'shared/components/inputs.mjs'
import { DynamicMdx } from 'shared/components/mdx/dynamic.mjs'
import { TipIcon } from 'shared/components/icons.mjs'
export const ns = ['account', 'status']
export const Tab = ({ id, activeTab, setActiveTab, t }) => (
<button
className={`text-xl font-bold capitalize tab tab-bordered grow
${activeTab === id ? 'tab-active' : ''}`}
onClick={() => setActiveTab(id)}
>
{t(id)}
</button>
)
export const BioSettings = ({ welcome = false }) => {
// Hooks
const { account, setAccount } = useAccount()
const backend = useBackend()
const { t, i18n } = useTranslation(ns)
const { setLoadingStatus } = useContext(LoadingStatusContext)
// State
const [bio, setBio] = useState(account.bio)
// Helper method to save bio
const save = async () => {
setLoadingStatus([true, 'processingUpdate'])
const result = await backend.updateAccount({ bio })
if (result.success) {
setAccount(result.data.account)
setLoadingStatus([true, 'settingsSaved', true, true])
} else setLoadingStatus([true, 'backendError', true, false])
}
// Next step in the onboarding
const nextHref =
welcomeSteps[account.control].length > 5
? '/welcome/' + welcomeSteps[account.control][6]
: '/docs/about/guide'
return (
<div className="max-w-xl xl:pl-4">
<MarkdownInput
id="account-bio"
label={t('bioTitle')}
update={setBio}
current={bio}
placeholder={t('bioTitle')}
docs={<DynamicMdx language={i18n.language} slug={`docs/about/site/account/bio`} />}
labelBL={
<span className="flex flex-row items-center gap-1">
<TipIcon className="w-6 h-6 text-success" />
{t('mdSupport')}
</span>
}
/>
<SaveSettingsButton btnProps={{ onClick: save }} welcome={welcome} />
{!welcome && <BackToAccountButton />}
{welcome ? (
<>
<ContinueButton btnProps={{ href: nextHref }} link />
{welcomeSteps[account.control].length > 0 ? (
<>
<progress
className="progress progress-primary w-full mt-12"
value={600 / welcomeSteps[account.control].length}
max="100"
></progress>
<span className="pt-4 text-sm font-bold opacity-50">
6 / {welcomeSteps[account.control].length}
</span>
<Icons
done={welcomeSteps[account.control].slice(0, 5)}
todo={welcomeSteps[account.control].slice(6)}
current="bio"
/>
</>
) : null}
</>
) : null}
</div>
)
}

View file

@ -1,98 +0,0 @@
// Dependencies
import { useState, useContext } from 'react'
import { useTranslation } from 'next-i18next'
// Context
import { LoadingStatusContext } from 'shared/context/loading-status-context.mjs'
// Hooks
import { useAccount } from 'shared/hooks/use-account.mjs'
import { useBackend } from 'shared/hooks/use-backend.mjs'
// Components
import { Icons, welcomeSteps, BackToAccountButton } from './shared.mjs'
import { ContinueButton } from 'shared/components/buttons/continue-button.mjs'
import { ListInput } from 'shared/components/inputs.mjs'
import { OkIcon, NoIcon } from 'shared/components/icons.mjs'
import { DynamicMdx } from 'shared/components/mdx/dynamic.mjs'
export const ns = ['account', 'status']
export const CompareSettings = ({ welcome = false }) => {
// Hooks
const { account, setAccount } = useAccount()
const backend = useBackend()
const { setLoadingStatus } = useContext(LoadingStatusContext)
const { t, i18n } = useTranslation(ns)
// State
const [selection, setSelection] = useState(account?.compare ? 'yes' : 'no')
// Helper method to update the account
const update = async (val) => {
if (val !== selection) {
setLoadingStatus([true, 'processingUpdate'])
const result = await backend.updateAccount({
compare: val === 'yes' ? true : false,
})
if (result.success) {
setLoadingStatus([true, 'settingsSaved', true, true])
setAccount(result.data.account)
setSelection(val)
} else setLoadingStatus([true, 'backendError', true, true])
}
}
// Link to the next onboarding step
const nextHref =
welcomeSteps[account?.control].length > 3
? '/welcome/' + welcomeSteps[account?.control][4]
: '/docs/about/guide'
return (
<div className="max-w-xl">
<ListInput
id="account-compare"
label={t('compareTitle')}
list={['yes', 'no'].map((val) => ({
val,
label: (
<div className="flex flex-row items-center w-full justify-between">
<span>{t(val === 'yes' ? 'compareYes' : 'compareNo')}</span>
{val === 'yes' ? (
<OkIcon className="w-8 h-8 text-success" stroke={4} />
) : (
<NoIcon className="w-8 h-8 text-error" stroke={3} />
)}
</div>
),
desc: t(val === 'yes' ? 'compareYesd' : 'compareNod'),
}))}
current={selection}
update={update}
docs={<DynamicMdx language={i18n.language} slug={`docs/about/site/account/compare`} />}
/>
{welcome ? (
<>
<ContinueButton btnProps={{ href: nextHref }} link />
{welcomeSteps[account?.control].length > 0 ? (
<>
<progress
className="progress progress-primary w-full mt-12"
value={400 / welcomeSteps[account?.control].length}
max="100"
></progress>
<span className="pt-4 text-sm font-bold opacity-50">
4 / {welcomeSteps[account?.control].length}
</span>
<Icons
done={welcomeSteps[account?.control].slice(0, 3)}
todo={welcomeSteps[account?.control].slice(4)}
current="compare"
/>
</>
) : null}
</>
) : (
<BackToAccountButton />
)}
</div>
)
}

View file

@ -1,116 +0,0 @@
// Dependencies
import { useState, useContext } from 'react'
import { useTranslation } from 'next-i18next'
import { nsMerge } from 'shared/utils.mjs'
// Context
import { LoadingStatusContext } from 'shared/context/loading-status-context.mjs'
// Hooks
import { useAccount } from 'shared/hooks/use-account.mjs'
import { useBackend } from 'shared/hooks/use-backend.mjs'
// Components
import Link from 'next/link'
import { Popout } from 'shared/components/popout/index.mjs'
import { BackToAccountButton } from './shared.mjs'
import { SaveSettingsButton } from 'shared/components/buttons/save-settings-button.mjs'
import { GdprAccountDetails, ns as gdprNs } from 'shared/components/gdpr/details.mjs'
export const ns = nsMerge(gdprNs, 'account', 'status')
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 ConsentSettings = ({ title = false }) => {
// Hooks
const { account, setAccount, setToken } = useAccount()
const backend = useBackend()
const { setLoadingStatus } = useContext(LoadingStatusContext)
const { t } = useTranslation(ns)
// State
const [consent1, setConsent1] = useState(account?.consent > 0)
const [consent2, setConsent2] = useState(account?.consent > 1)
// Helper method to update the account
const update = async () => {
let newConsent = 0
if (consent1) newConsent = 1
if (consent1 && consent2) newConsent = 2
if (newConsent !== account.consent) {
setLoadingStatus([true, 'processingUpdate'])
const result = await backend.updateAccount({ consent: newConsent })
if (result.data?.result === 'success') {
setLoadingStatus([true, 'settingsSaved', true, true])
setAccount(result.data.account)
} else setLoadingStatus([true, 'backendError', true, true])
}
}
// Helper method to remove the account
const removeAccount = async () => {
setLoadingStatus([true, 'processingUpdate'])
const result = await backend.removeAccount()
if (result === true) setLoadingStatus([true, 'settingsSaved', true, true])
else setLoadingStatus([true, 'backendError', true, true])
setToken(null)
setAccount({ username: false })
}
return (
<div className="max-w-xl xl:pl-4">
{title ? <h2 className="text-4xl">{t('privacyMatters')}</h2> : null}
<p>{t('compliant')}</p>
<p>{t('consentWhyAnswer')}</p>
<h5 className="mt-8">{t('accountQuestion')}</h5>
<GdprAccountDetails />
{consent1 ? (
<Checkbox value={consent1} setter={setConsent1} label={t('yesIDo')} />
) : (
<button
className="btn btn-primary btn-lg w-full mt-4"
onClick={() => setConsent1(!consent1)}
>
{t('clickHere')}
</button>
)}
{consent1 ? (
<Checkbox value={consent2} setter={setConsent2} label={t('openDataQuestion')} />
) : null}
{consent1 && !consent2 ? <Popout note>{t('openDataInfo')}</Popout> : null}
{!consent1 && <Popout warning>{t('noConsentNoAccount')}</Popout>}
{consent1 ? (
<SaveSettingsButton btnProps={{ onClick: update }} />
) : (
<SaveSettingsButton
label={t('account:removeAccount')}
btnProps={{
onClick: removeAccount,
className: 'btn mt-4 capitalize w-full btn-error',
}}
/>
)}
<BackToAccountButton />
<p className="text-center opacity-50 mt-12">
<Link href="/docs/about/privacy" className="hover:text-secondary underline">
FreeSewing Privacy Notice
</Link>
</p>
</div>
)
}

View file

@ -1,105 +0,0 @@
// __SDEFILE__ - This file is a dependency for the stand-alone environment
// Dependencies
import { useState, useContext } from 'react'
import { useTranslation } from 'next-i18next'
// Context
import { LoadingStatusContext } from 'shared/context/loading-status-context.mjs'
// Hooks
import { useAccount } from 'shared/hooks/use-account.mjs'
import { useBackend } from 'shared/hooks/use-backend.mjs'
// Components
import { BackToAccountButton, Icons, welcomeSteps } from './shared.mjs'
import { ContinueButton } from 'shared/components/buttons/continue-button.mjs'
import { ListInput } from 'shared/components/inputs.mjs'
import { ControlScore } from 'shared/components/control/score.mjs'
import { DynamicMdx } from 'shared/components/mdx/dynamic.mjs'
export const ns = ['account', 'status']
/** state handlers for any input that changes the control setting */
export const useControlState = () => {
// Hooks
const { account, setAccount, token } = useAccount()
const backend = useBackend()
const { setLoadingStatus } = useContext(LoadingStatusContext)
// State
const [selection, setSelection] = useState(account.control)
// Method to update the control setting
const update = async (control) => {
if (control !== selection) {
if (token) {
setLoadingStatus([true, 'processingUpdate'])
const result = await backend.updateAccount({ control })
if (result.success) {
setSelection(control)
setAccount(result.data.account)
setLoadingStatus([true, 'settingsSaved', true, true])
} else setLoadingStatus([true, 'backendError', true, true])
}
//fallback for guest users
else {
setAccount({ ...account, control })
setSelection(control)
}
}
}
return { selection, update }
}
export const ControlSettings = ({ welcome = false, noBack = false }) => {
const { t, i18n } = useTranslation(ns)
const { selection, update } = useControlState()
// Helper to get the link to the next onboarding step
const nextHref = welcome
? welcomeSteps[selection].length > 1
? '/welcome/' + welcomeSteps[selection][1]
: '/docs/about/guide'
: false
return (
<div className="max-w-xl">
<ListInput
id="account-control"
label={t('controlTitle')}
list={[1, 2, 3, 4, 5].map((val) => ({
val,
label: (
<div className="flex flex-row items-center w-full justify-between">
<span>{t(`control${val}.t`)}</span>
<ControlScore control={val} />
</div>
),
desc: t(`control${val}.d`),
}))}
current={selection}
update={update}
docs={<DynamicMdx language={i18n.language} slug="docs/about/site/account/control" />}
/>
{welcome ? (
<>
<ContinueButton btnProps={{ href: nextHref }} link />
{welcomeSteps[selection].length > 1 ? (
<>
<progress
className="progress progress-primary w-full mt-12"
value={100 / welcomeSteps[selection].length}
max="100"
></progress>
<span className="pt-4 text-sm font-bold opacity-50">
1 / {welcomeSteps[selection].length}
</span>
<Icons done={[]} todo={welcomeSteps[selection].slice(1)} current="" />
</>
) : null}
</>
) : noBack ? null : (
<BackToAccountButton />
)}
</div>
)
}

View file

@ -1,76 +0,0 @@
// Dependencies
import { useState, useContext } from 'react'
import { useTranslation } from 'next-i18next'
// Context
import { LoadingStatusContext } from 'shared/context/loading-status-context.mjs'
// Hooks
import { useAccount } from 'shared/hooks/use-account.mjs'
import { useBackend } from 'shared/hooks/use-backend.mjs'
// Verification methods
import { validateEmail, validateTld } from 'shared/utils.mjs'
// Components
import { BackToAccountButton } from './shared.mjs'
import { Popout } from 'shared/components/popout/index.mjs'
import { EmailInput } from 'shared/components/inputs.mjs'
import { DynamicMdx } from 'shared/components/mdx/dynamic.mjs'
export const ns = ['account', 'status']
export const EmailSettings = () => {
// Hooks
const { account, setAccount } = useAccount()
const backend = useBackend()
const { t, i18n } = useTranslation(ns)
const { setLoadingStatus } = useContext(LoadingStatusContext)
// State
const [email, setEmail] = useState(account.email)
const [changed, setChanged] = useState(false)
// Helper method to update account
const save = async () => {
setLoadingStatus([true, 'processingUpdate'])
const result = await backend.updateAccount({ email })
if (result.success) {
setAccount(result.data.account)
setLoadingStatus([true, 'settingsSaved', true, true])
} else setLoadingStatus([true, 'backendError', true, true])
setChanged(true)
}
// Is email valid?
const valid = (validateEmail(email) && validateTld(email)) || false
return (
<div className="max-w-xl">
{changed ? (
<Popout note>
<h3>{t('oneMoreThing')}</h3>
<p>{t('emailChangeConfirmation')}</p>
</Popout>
) : (
<>
<EmailInput
id="account-email"
label={t('account:email')}
placeholder={t('account:email')}
update={setEmail}
labelBL={t('emailTitle')}
current={email}
original={account.email}
valid={() => valid}
docs={<DynamicMdx language={i18n.language} slug={`docs/about/site/account/email`} />}
/>
<button
className="btn mt-4 btn-primary w-full"
onClick={save}
disabled={!valid || email.toLowerCase() === account.email}
>
{t('save')}
</button>
</>
)}
<BackToAccountButton />
</div>
)
}

View file

@ -1,49 +0,0 @@
// Context
import { LoadingStatusContext } from 'shared/context/loading-status-context.mjs'
// Hooks
import { useTranslation } from 'next-i18next'
import { useState, useContext } from 'react'
import { useBackend } from 'shared/hooks/use-backend.mjs'
// Components
import { BackToAccountButton } from './shared.mjs'
import { Popout } from 'shared/components/popout/index.mjs'
import { WebLink } from 'shared/components/link.mjs'
export const ns = ['account', 'status']
export const ExportAccount = () => {
// Hooks
const backend = useBackend()
const { t } = useTranslation(ns)
const { setLoadingStatus } = useContext(LoadingStatusContext)
const [link, setLink] = useState()
// Helper method to export account
const exportData = async () => {
setLoadingStatus([true, 'processingUpdate'])
const result = await backend.exportAccount()
if (result.success) {
setLink(result.data.data)
setLoadingStatus([true, 'nailedIt', true, true])
} else setLoadingStatus([true, 'backendError', true, false])
}
return (
<div className="max-w-xl">
{link ? (
<Popout link>
<h5>{t('exportDownload')}</h5>
<p className="text-lg">
<WebLink href={link} txt={link} />
</p>
</Popout>
) : null}
<p>{t('exportMsg')}</p>
<button className="btn btn-primary capitalize w-full my-2" onClick={exportData}>
{t('export')}
</button>
<BackToAccountButton />
</div>
)
}

View file

@ -1,35 +0,0 @@
// Dependencies
import { useState, useEffect } from 'react'
// Hooks
import { useAccount } from 'shared/hooks/use-account.mjs'
import { useBackend } from 'shared/hooks/use-backend.mjs'
export const ForceAccountCheck = ({ trigger = null }) => {
// Hooks
const { account, setAccount, signOut } = useAccount()
const backend = useBackend()
// State
const [lastCheck, setLastCheck] = useState(Date.now())
// The actual check
useEffect(() => {
const age = Date.now() - lastCheck
if (account.status && age < 500) {
const checkAccount = async () => {
const result = await backend.reloadAccount()
if (result.success) {
setAccount(result.data.account)
} else {
// Login expired. Logout user.
signOut()
}
setLastCheck(Date.now())
}
checkAccount()
}
}, [trigger])
// Don't return anything. This is all about the useEffect hook.
return null
}

View file

@ -1,62 +0,0 @@
// Context
import { LoadingStatusContext } from 'shared/context/loading-status-context.mjs'
// Hooks
import { useState, useContext } from 'react'
import { useTranslation } from 'next-i18next'
import { useAccount } from 'shared/hooks/use-account.mjs'
import { useBackend } from 'shared/hooks/use-backend.mjs'
// Components
import { BackToAccountButton } from './shared.mjs'
import { SaveSettingsButton } from 'shared/components/buttons/save-settings-button.mjs'
import { StringInput } from 'shared/components/inputs.mjs'
import { DynamicMdx } from 'shared/components/mdx/dynamic.mjs'
export const ns = ['account', 'status']
export const GithubSettings = () => {
// Hooks
const { account, setAccount } = useAccount()
const backend = useBackend()
const { t, i18n } = useTranslation(ns)
const { setLoadingStatus } = useContext(LoadingStatusContext)
// State
const [githubUsername, setGithubUsername] = useState(account.data.githubUsername || '')
const [githubEmail, setGithubEmail] = useState(account.data.githubEmail || '')
// Helper method to save changes
const save = async () => {
setLoadingStatus([true, 'processingUpdate'])
const result = await backend.updateAccount({ data: { githubUsername, githubEmail } })
if (result.success) {
setAccount(result.data.account)
setLoadingStatus([true, 'settingsSaved', true, true])
} else setLoadingStatus([true, 'backendError', true, false])
}
return (
<div className="max-w-xl">
<h2 className="text-4xl">{t('githubTitle')}</h2>
<StringInput
id="account-github-email"
label={t('email')}
current={githubEmail}
update={setGithubEmail}
valid={(val) => val.length > 0}
placeholder={'joost@joost.at'}
docs={<DynamicMdx language={i18n.language} slug={`docs/about/site/account/github`} />}
/>
<StringInput
id="account-github-username"
label={t('username')}
current={githubUsername}
update={setGithubUsername}
valid={(val) => val.length > 0}
placeholder={'joostdecock'}
docs={<DynamicMdx language={i18n.language} slug={`docs/about/site/account/github`} />}
/>
<SaveSettingsButton btnProps={{ onClick: save }} />
<BackToAccountButton />
</div>
)
}

View file

@ -1,88 +0,0 @@
// Dependencies
import { cloudflareImageUrl } from 'shared/utils.mjs'
// Context
import { LoadingStatusContext } from 'shared/context/loading-status-context.mjs'
// Hooks
import { useState, useContext } from 'react'
import { useTranslation } from 'next-i18next'
import { useAccount } from 'shared/hooks/use-account.mjs'
import { useBackend } from 'shared/hooks/use-backend.mjs'
// Components
import { Icons, welcomeSteps, BackToAccountButton } from './shared.mjs'
import { ContinueButton } from 'shared/components/buttons/continue-button.mjs'
import { SaveSettingsButton } from 'shared/components/buttons/save-settings-button.mjs'
import { PassiveImageInput } from 'shared/components/inputs.mjs'
import { DynamicMdx } from 'shared/components/mdx/dynamic.mjs'
export const ns = ['account', 'status']
export const ImgSettings = ({ welcome = false }) => {
const { account, setAccount } = useAccount()
const backend = useBackend()
const { setLoadingStatus } = useContext(LoadingStatusContext)
const { t, i18n } = useTranslation(ns)
const [img, setImg] = useState('')
const save = async () => {
setLoadingStatus([true, 'processingUpdate'])
const result = await backend.updateAccount({ img })
if (result.success) {
setAccount(result.data.account)
setLoadingStatus([true, 'settingsSaved', true, true])
} else setLoadingStatus([true, 'backendError', true, false])
}
const nextHref = '/docs/about/guide'
return (
<div className="max-w-xl">
{!welcome || img !== false ? (
<img
alt="img"
src={img || cloudflareImageUrl({ id: `uid-${account.ihash}`, variant: 'public' })}
className="shadow mb-4"
/>
) : null}
<PassiveImageInput
id="account-img"
label={t('image')}
placeholder={'image'}
update={setImg}
current={img}
valid={(val) => val.length > 0}
docs={<DynamicMdx language={i18n.language} slug={`docs/about/site/account/img`} />}
/>
{welcome ? (
<>
<button className={`btn btn-secondary mt-4 px-8`} onClick={save} disabled={!img}>
{t('save')}
</button>
<ContinueButton btnProps={{ href: nextHref }} link />
{welcomeSteps[account.control].length > 0 ? (
<>
<progress
className="progress progress-primary w-full mt-12"
value={700 / welcomeSteps[account.control].length}
max="100"
></progress>
<span className="pt-4 text-sm font-bold opacity-50">
7 / {welcomeSteps[account.control].length}
</span>
<Icons
done={welcomeSteps[account.control].slice(0, 6)}
todo={welcomeSteps[account.control].slice(7)}
current="img"
/>
</>
) : null}
</>
) : (
<>
<SaveSettingsButton btnProps={{ onClick: save }} />
<BackToAccountButton />
</>
)}
</div>
)
}

View file

@ -1,90 +0,0 @@
// Context
import { LoadingStatusContext } from 'shared/context/loading-status-context.mjs'
// Hooks
import { useState, useContext } from 'react'
import { useTranslation } from 'next-i18next'
import { useAccount } from 'shared/hooks/use-account.mjs'
import { useBackend } from 'shared/hooks/use-backend.mjs'
// Components
import { Icons, welcomeSteps, BackToAccountButton, NumberBullet } from './shared.mjs'
import { ContinueButton } from 'shared/components/buttons/continue-button.mjs'
import { ListInput } from 'shared/components/inputs.mjs'
import { DynamicMdx } from 'shared/components/mdx/dynamic.mjs'
export const ns = ['account', 'status']
export const ImperialSettings = ({ welcome = false }) => {
// Hooks
const { account, setAccount } = useAccount()
const { setLoadingStatus } = useContext(LoadingStatusContext)
const backend = useBackend()
const { t, i18n } = useTranslation(ns)
// State
const [selection, setSelection] = useState(account?.imperial === true ? 'imperial' : 'metric')
// Helper method to update account
const update = async (val) => {
if (val !== selection) {
setLoadingStatus([true, 'processingUpdate'])
const result = await backend.updateAccount({ imperial: val === 'imperial' ? true : false })
if (result.success) {
setAccount(result.data.account)
setSelection(val)
setLoadingStatus([true, 'settingsSaved', true, true])
} else setLoadingStatus([true, 'backendError', true, true])
}
}
// Next step in the onboarding
const nextHref =
welcomeSteps[account?.control].length > 3
? '/welcome/' + welcomeSteps[account?.control][3]
: '/docs/about/guide'
return (
<div className="max-w-xl">
<ListInput
id="account-units"
label={t('unitsTitle')}
list={['metric', 'imperial'].map((val) => ({
val,
label: (
<div className="flex flex-row items-center w-full justify-between">
<span>{t(`${val}Units`)}</span>
<NumberBullet nr={val === 'imperial' ? '″' : 'cm'} color="secondary" />
</div>
),
desc: t(`${val}Unitsd`),
}))}
current={selection}
update={update}
docs={<DynamicMdx language={i18n.language} slug={`docs/about/site/account/units`} />}
/>
{welcome ? (
<>
<ContinueButton btnProps={{ href: nextHref }} link />
{welcomeSteps[account?.control].length > 0 ? (
<>
<progress
className="progress progress-primary w-full mt-12"
value={300 / welcomeSteps[account?.control].length}
max="100"
></progress>
<span className="pt-4 text-sm font-bold opacity-50">
3 / {welcomeSteps[account?.control].length}
</span>
<Icons
done={welcomeSteps[account?.control].slice(0, 2)}
todo={welcomeSteps[account?.control].slice(3)}
current="units"
/>
</>
) : null}
</>
) : (
<BackToAccountButton />
)}
</div>
)
}

View file

@ -1,101 +0,0 @@
// Dependencies
import { useContext } from 'react'
import { useTranslation } from 'next-i18next'
// Context
import { LoadingStatusContext } from 'shared/context/loading-status-context.mjs'
// Hooks
import { useAccount } from 'shared/hooks/use-account.mjs'
import { useBackend } from 'shared/hooks/use-backend.mjs'
// Components
import { FileInput } from 'shared/components/inputs.mjs'
import { Yaml } from 'shared/components/yaml.mjs'
import { Popout } from 'shared/components/popout/index.mjs'
import { linkClasses } from 'shared/components/link.mjs'
import yaml from 'yaml'
export const ns = ['account', 'status']
export const Importer = () => {
// Hooks
const { account } = useAccount()
const backend = useBackend()
const { t } = useTranslation(ns)
const { setLoadingStatus } = useContext(LoadingStatusContext)
// Helper method to upload/save a set
const uploadSet = async (upload) => {
setLoadingStatus([true, 'processingUpdate'])
let data
try {
const chunks = upload.split(',')
if (chunks[0].includes('json')) data = JSON.parse(atob(chunks[1]))
else data = yaml.parse(atob(chunks[1]))
if (!Array.isArray(data)) data = [data]
/*
* Treat each set
*/
for (const set of data) {
if (set.measurements || set.measies) {
const name = set.name || 'J. Doe'
setLoadingStatus([true, `Importing ${name}`])
const result = await backend.createSet({
name: set.name || 'J. Doe',
units: set.units || 'metric',
notes: set.notes || '',
measies: set.measurements || set.measies,
userId: account.id,
})
if (result.success) setLoadingStatus([true, `Imported ${name}`, true, true])
else setLoadingStatus([true, `Import of ${name} failed`, true, false])
} else {
setLoadingStatus([true, `Invalid format`, true, false])
}
}
} catch (err) {
console.log(err)
setLoadingStatus([true, `Import of ${name || 'file'} failed`, true, false])
}
}
return (
<div className="max-w-xl xl:pl-4">
<p>{t('account:importHere')}</p>
<p>{t('account:importSupported')}</p>
<ul className="list list-inside list-disc ml-4">
<li>
<a href="#set" className={linkClasses}>
{t('account:sets')}
</a>
</li>
</ul>
<h2 id="set">{t('account:importSets')}</h2>
<FileInput
label={`${t('account:measieFile')} (YAML / JSON)`}
update={uploadSet}
current=""
id="file"
dropzoneConfig={{
accept: {
'application/json': ['.json'],
'application/yaml': ['.yaml', '.yml'],
},
maxFiles: 1,
multiple: false,
}}
/>
<Popout tip>
<p>{t('account:importSetTip1')}</p>
<Yaml
js={{
name: 'Joost',
units: 'metric',
notes: '',
measurements: { biceps: 335, wrist: 190 },
}}
title="measurements.yaml"
/>
<p>{t('account:importSetTip2')}</p>
</Popout>
</div>
)
}

View file

@ -20,6 +20,11 @@ import { Newsletter } from './Newsletter.mjs'
import { Consent } from './Consent.mjs' import { Consent } from './Consent.mjs'
import { Password } from './Password.mjs' import { Password } from './Password.mjs'
import { Mfa } from './Mfa.mjs' import { Mfa } from './Mfa.mjs'
import { ImportSet } from './Import.mjs'
import { Export } from './Export.mjs'
import { Reload } from './Reload.mjs'
import { Remove } from './Remove.mjs'
import { Restrict } from './Restrict.mjs'
export { export {
Bookmarks, Bookmarks,
@ -51,4 +56,9 @@ export {
Consent, Consent,
Password, Password,
Mfa, Mfa,
ImportSet,
Export,
Reload,
Remove,
Restrict,
} }

View file

@ -1,66 +0,0 @@
// Context
import { LoadingStatusContext } from 'shared/context/loading-status-context.mjs'
// Hooks
import { useState, useContext } from 'react'
import { useTranslation } from 'next-i18next'
import { useAccount } from 'shared/hooks/use-account.mjs'
import { useBackend } from 'shared/hooks/use-backend.mjs'
// Components
import { BackToAccountButton, NumberBullet } from './shared.mjs'
import { ListInput } from 'shared/components/inputs.mjs'
import { DynamicMdx } from 'shared/components/mdx/dynamic.mjs'
// Config
import { siteConfig as conf } from 'site/site.config.mjs'
export const ns = ['account', 'locales', 'status']
export const LanguageSettings = () => {
// Hooks
const { account, setAccount } = useAccount()
const { setLoadingStatus } = useContext(LoadingStatusContext)
const backend = useBackend()
const { t, i18n } = useTranslation(ns)
// State
const [language, setLanguage] = useState(account.language || 'en')
// Helper method to update the account
const update = async (lang) => {
if (lang !== language) {
setLoadingStatus([true, 'processingUpdate'])
setLanguage(lang)
const result = await backend.updateAccount({ language: lang })
if (result.success) {
setAccount(result.data.account)
setLoadingStatus([true, 'settingsSaved', true, true])
} else setLoadingStatus([true, 'backendError', true, true])
}
}
return (
<div className="max-w-xl">
<ListInput
id="account-language"
label={t('languageTitle')}
list={conf.languages.map((val) => ({
val,
label: (
<div className="flex flex-row items-center w-full justify-between">
<span>
{t(`locales:${val}`)}
<span className="px-2 opacity-50">|</span>
{t(`locales:${val}`, { lng: val })}
</span>
<NumberBullet nr={val} color="secondary" />
</div>
),
desc: t('languageTitle', { lng: val }),
}))}
current={language}
update={update}
docs={<DynamicMdx language={i18n.language} slug={`docs/about/site/account/language`} />}
/>
<BackToAccountButton />
</div>
)
}

View file

@ -1,187 +0,0 @@
// Context
import { LoadingStatusContext } from 'shared/context/loading-status-context.mjs'
// Hooks
import { useState, useContext } from 'react'
import { useTranslation } from 'next-i18next'
import { useAccount } from 'shared/hooks/use-account.mjs'
import { useBackend } from 'shared/hooks/use-backend.mjs'
// Components
import { BackToAccountButton } from './shared.mjs'
import { Popout } from 'shared/components/popout/index.mjs'
import { Bullet } from 'shared/components/bullet.mjs'
import { PasswordInput } from 'shared/components/inputs.mjs'
import { CopyToClipboard } from 'shared/components/copy-to-clipboard.mjs'
export const ns = ['account']
const CodeInput = ({ code, setCode, t }) => (
<input
value={code}
onChange={(evt) => setCode(evt.target.value)}
className="input w-full text-4xl input-bordered input-lg flex flex-row text-center mb-8 tracking-widest"
type="text"
placeholder={t('000000')}
/>
)
export const MfaSettings = ({ title = false, welcome = false }) => {
// Hooks
const { account, setAccount } = useAccount()
const backend = useBackend()
const { t } = useTranslation(ns)
const { setLoadingStatus } = useContext(LoadingStatusContext)
// State
const [enable, setEnable] = useState(false)
const [disable, setDisable] = useState(false)
const [code, setCode] = useState('')
const [password, setPassword] = useState('')
const [scratchCodes, setScratchCodes] = useState(false)
// Helper method to enable MFA
const enableMfa = async () => {
setLoadingStatus([true, 'processingUpdate'])
const result = await backend.enableMfa()
if (result.success) {
setEnable(result.data.mfa)
setLoadingStatus([true, 'settingsSaved', true, true])
} else setLoadingStatus([true, 'backendError', true, false])
}
// Helper method to disable MFA
const disableMfa = async () => {
setLoadingStatus([true, 'processingUpdate'])
const result = await backend.disableMfa({
mfa: false,
password,
token: code,
})
if (result) {
if (result.success) {
setAccount(result.data.account)
setLoadingStatus([true, 'settingsSaved', true, true])
} else setLoadingStatus([true, 'backendError', true, false])
setDisable(false)
setEnable(false)
setCode('')
setPassword('')
}
}
// Helper method to confirm MFA
const confirmMfa = async () => {
setLoadingStatus([true, 'processingUpdate'])
const result = await backend.confirmMfa({
mfa: true,
secret: enable.secret,
token: code,
})
if (result.success) {
setAccount(result.data.account)
setScratchCodes(result.data.scratchCodes)
setLoadingStatus([true, 'settingsSaved', true, true])
} else setLoadingStatus([true, 'backendError', true, false])
setEnable(false)
setCode('')
}
// Figure out what title to use
let titleText = account.mfaEnabled ? t('mfaEnabled') : t('mfaDisabled')
if (enable) titleText = t('mfaSetup')
return (
<div className="max-w-xl">
{title ? <h2 className="text-4xl">{titleText}</h2> : null}
{enable ? (
<>
<div className="flex flex-row items-center justify-center px-8 lg:px-36">
<div dangerouslySetInnerHTML={{ __html: enable.qrcode }} />
</div>
<p className="flex flex-row items-center justify-center">{enable.secret}</p>
<Bullet num="1">{t('mfaAdd')}</Bullet>
<Bullet num="2">{t('confirmWithMfa')}</Bullet>
<input
value={code}
onChange={(evt) => setCode(evt.target.value)}
className="input w-64 m-auto text-4xl input-bordered input-lg flex flex-row text-center mb-8 tracking-widest"
type="text"
inputMode="numeric"
pattern="[0-9]{6}"
placeholder={t('000000')}
/>
<button className="btn btn-success btn-lg block w-full" onClick={confirmMfa}>
{t('enableMfa')}
</button>
</>
) : null}
{disable ? (
<div className="my-8">
<Bullet num="1">
<h5>{t('confirmWithPassword')}</h5>
<PasswordInput
current={password}
update={setPassword}
placeholder={t('passwordPlaceholder')}
valid={() => true}
/>
</Bullet>
<Bullet num="2">
<h5>{t('confirmWithMfa')}</h5>
<CodeInput code={code} setCode={setCode} t={t} />
</Bullet>
<button
className="btn btn-error btn-lg block w-full"
onClick={disableMfa}
disabled={code.length < 4 || password.length < 3}
>
{t('disableMfa')}
</button>
</div>
) : null}
{scratchCodes ? (
<>
<h3>{t('account:mfaScratchCodes')}</h3>
<p>{t('account:mfaScratchCodesMsg1')}</p>
<p>{t('account:mfaScratchCodesMsg2')}</p>
<div className="hljs my-4">
<div className=" flex flex-row justify-between items-center text-xs font-medium text-warning mt-1 border-b border-neutral-content border-opacity-25 px-4 py-1 mb-2 lg:text-sm">
<span>{t('account:mfaScratchCodes')}</span>
<CopyToClipboard
content={
'FreeSewing ' +
t('account:mfaScratchCodes') +
':\n' +
scratchCodes.map((code) => code + '\n').join('')
}
/>
</div>
<pre className="language-shell hljs text-base lg:text-lg whitespace-break-spaces overflow-scroll pr-4">
{scratchCodes.map((code) => code + '\n')}
</pre>
</div>
</>
) : (
<div className="flex flex-row items-center mt-4">
{account.mfaEnabled ? (
disable ? null : (
<button className="btn btn-primary block w-full" onClick={() => setDisable(true)}>
{t('disableMfa')}
</button>
)
) : enable ? null : (
<div>
<button className="btn btn-primary block w-full" onClick={enableMfa}>
{t('mfaSetup')}
</button>
<Popout tip>
<h5>{t('mfaTipTitle')}</h5>
<p>{t('mfaTipMsg')}</p>
</Popout>
</div>
)}
</div>
)}
{!welcome && <BackToAccountButton />}
</div>
)
}

View file

@ -1,107 +0,0 @@
// Context
import { LoadingStatusContext } from 'shared/context/loading-status-context.mjs'
// Hooks
import { useState, useContext } from 'react'
import { useTranslation } from 'next-i18next'
import { useAccount } from 'shared/hooks/use-account.mjs'
import { useBackend } from 'shared/hooks/use-backend.mjs'
// Components
import { BackToAccountButton, Icons, welcomeSteps } from './shared.mjs'
import { ContinueButton } from 'shared/components/buttons/continue-button.mjs'
import { ListInput } from 'shared/components/inputs.mjs'
import { DynamicMdx } from 'shared/components/mdx/dynamic.mjs'
import { OkIcon, NoIcon } from 'shared/components/icons.mjs'
import { Popout } from 'shared/components/popout/index.mjs'
import { PageLink } from 'shared/components/link.mjs'
export const ns = ['account', 'status', 'newsletter']
export const NewsletterSettings = ({ welcome = false, bare = false }) => {
// Hooks
const { account, setAccount } = useAccount()
const backend = useBackend()
const { t, i18n } = useTranslation(ns)
const { setLoadingStatus } = useContext(LoadingStatusContext)
// State
const [selection, setSelection] = useState(account?.newsletter ? 'yes' : 'no')
// Helper method to update account
const update = async (val) => {
if (val !== selection) {
setLoadingStatus([true, 'processingUpdate'])
const result = await backend.updateAccount({ newsletter: val === 'yes' ? true : false })
if (result.success) {
setAccount(result.data.account)
setSelection(val)
setLoadingStatus([true, 'settingsSaved', true, true])
} else setLoadingStatus([true, 'backendError', true, true])
}
}
// Next step for onboarding
const nextHref =
welcomeSteps[account?.control].length > 2
? '/welcome/' + welcomeSteps[account?.control][2]
: '/docs/about/guide'
return (
<div className="max-w-xl">
<ListInput
id="account-newsletter"
label={t('newsletterTitle')}
list={['yes', 'no'].map((val) => ({
val,
label: (
<div className="flex flex-row items-center w-full justify-between">
<span>{t(val === 'yes' ? 'newsletterYes' : 'noThanks')}</span>
{val === 'yes' ? (
<OkIcon className="w-8 h-8 text-success" stroke={4} />
) : (
<NoIcon className="w-8 h-8 text-error" stroke={3} />
)}
</div>
),
desc: t(val === 'yes' ? 'newsletterYesd' : 'newsletterNod'),
}))}
current={selection}
update={update}
docs={<DynamicMdx language={i18n.language} slug={`account/site/account/newsletter`} />}
/>
{welcome ? (
<>
<ContinueButton btnProps={{ href: nextHref }} link />
{welcomeSteps[account?.control].length > 0 ? (
<>
<progress
className="progress progress-primary w-full mt-12"
value={200 / welcomeSteps[account?.control].length}
max="100"
></progress>
<span className="pt-4 text-sm font-bold opacity-50">
2 / {welcomeSteps[account?.control].length}
</span>
<Icons
done={welcomeSteps[account?.control].slice(0, 1)}
todo={welcomeSteps[account?.control].slice(2)}
current="newsletter"
/>
</>
) : null}
</>
) : bare ? null : (
<BackToAccountButton />
)}
<Popout tip>
<p>{t('newsletter:subscribePs')}</p>
<p>
<PageLink
href={`/newsletter/unsubscribe?x=${account?.ehash}`}
txt={t('newsletter:unsubscribeLink')}
/>
</p>
</Popout>
</div>
)
}
export default NewsletterSettings

View file

@ -1,3 +0,0 @@
import { AccountLinks } from './links.mjs'
export const AccountOverview = ({ app }) => <AccountLinks app={app} />

View file

@ -1,66 +0,0 @@
// Dependencies
import { useState, useContext } from 'react'
import { useTranslation } from 'next-i18next'
// Context
import { LoadingStatusContext } from 'shared/context/loading-status-context.mjs'
// Hooks
import { useAccount } from 'shared/hooks/use-account.mjs'
import { useBackend } from 'shared/hooks/use-backend.mjs'
// Components
import Link from 'next/link'
import { BackToAccountButton } from './shared.mjs'
import { SaveSettingsButton } from 'shared/components/buttons/save-settings-button.mjs'
import { Popout } from 'shared/components/popout/index.mjs'
import { RightIcon } from 'shared/components/icons.mjs'
import { PasswordInput } from 'shared/components/inputs.mjs'
import { DynamicMdx } from 'shared/components/mdx/dynamic.mjs'
export const ns = ['account', 'status']
export const PasswordSettings = ({ welcome = false }) => {
// Hooks
const { account, setAccount } = useAccount()
const backend = useBackend()
const { t, i18n } = useTranslation(ns)
const { setLoadingStatus } = useContext(LoadingStatusContext)
// State
const [password, setPassword] = useState('')
// Helper method to save password to account
const save = async () => {
setLoadingStatus([true, 'processingUpdate'])
const result = await backend.updateAccount({ password })
if (result.success) {
setAccount(result.data.account)
setLoadingStatus([true, 'settingsSaved', true, true])
} else setLoadingStatus([true, 'backendError', true, false])
}
return (
<div className="max-w-xl">
<PasswordInput
id="account-password"
label={t('passwordTitle')}
current={password}
update={setPassword}
valid={(val) => val.length > 0}
placeholder={t('passwordTitle')}
docs={<DynamicMdx language={i18n.language} slug={`docs/about/site/account/password`} />}
/>
<SaveSettingsButton btnProps={{ onClick: save, disabled: password.length < 4 }} />
{!welcome && <BackToAccountButton />}
{!account.mfaEnabled && (
<Popout tip>
<h5>{t('mfaTipTitle')}</h5>
<p>{t('mfaTipMsg')}</p>
<p className="text-right m-0 pt-0">
<Link className="btn btn-secondary btn-accent" href="/account/mfa">
{t('mfa')} <RightIcon className="h-6 w-6 ml-2" />
</Link>
</p>
</Popout>
)}
</div>
)
}

View file

@ -1,725 +0,0 @@
// Dependencies
import { useState, useEffect, useContext } from 'react'
import { useTranslation } from 'next-i18next'
import {
capitalize,
shortDate,
cloudflareImageUrl,
horFlexClasses,
newPatternUrl,
} from 'shared/utils.mjs'
import orderBy from 'lodash.orderby'
import { freeSewingConfig as conf, controlLevels } from 'shared/config/freesewing.config.mjs'
// Context
import { LoadingStatusContext } from 'shared/context/loading-status-context.mjs'
// Hooks
import { useAccount } from 'shared/hooks/use-account.mjs'
import { useBackend } from 'shared/hooks/use-backend.mjs'
import { useRouter } from 'next/router'
// Context
import { ModalContext } from 'shared/context/modal-context.mjs'
// Components
import { PageLink, Link, AnchorLink } from 'shared/components/link.mjs'
import { BackToAccountButton } from './shared.mjs'
import { Popout } from 'shared/components/popout/index.mjs'
import {
StringInput,
MarkdownInput,
PassiveImageInput,
ListInput,
} from 'shared/components/inputs.mjs'
import {
OkIcon,
NoIcon,
TrashIcon,
PlusIcon,
CameraIcon,
EditIcon,
ResetIcon,
RightIcon,
UploadIcon,
FreeSewingIcon,
CloneIcon,
BoolYesIcon,
BoolNoIcon,
LockIcon,
PatternIcon,
BookmarkIcon,
} from 'shared/components/icons.mjs'
import { DisplayRow } from './shared.mjs'
import { ModalWrapper } from 'shared/components/wrappers/modal.mjs'
import { Mdx } from 'shared/components/mdx/dynamic.mjs'
import Timeago from 'react-timeago'
import { TableWrapper } from 'shared/components/wrappers/table.mjs'
import { PatternReactPreview } from 'shared/components/pattern/preview.mjs'
import { Lightbox } from 'shared/components/lightbox.mjs'
import { DynamicMdx } from 'shared/components/mdx/dynamic.mjs'
export const ns = ['account', 'patterns', 'status']
export const ShowPattern = ({ id }) => {
// Hooks
const { setLoadingStatus } = useContext(LoadingStatusContext)
const backend = useBackend()
const { t, i18n } = useTranslation(ns)
const { account } = useAccount()
// State
const [pattern, setPattern] = useState()
const [isOwn, setIsOwn] = useState(false)
// Effect
useEffect(() => {
const getPattern = async () => {
setLoadingStatus([true, t('backendLoadingStarted')])
let result
try {
result = await backend.getPattern(id)
if (result.success) {
setPattern(result.data.pattern)
if (result.data.pattern.userId === account.userId) setIsOwn(true)
setLoadingStatus([true, 'backendLoadingCompleted', true, true])
} else {
result = await backend.getPublicPattern(id)
if (result.success) {
setPattern({ ...result.data, public: true })
setLoadingStatus([true, 'backendLoadingCompleted', true, true])
} else setLoadingStatus([true, 'backendError', true, false])
}
} catch (err) {
console.log(err)
setLoadingStatus([true, 'backendError', true, false])
}
}
if (id) getPattern()
}, [id])
const bookmarkPattern = async () => {
setLoadingStatus([true, 'creatingBookmark'])
const result = await backend.createBookmark({
type: 'pattern',
title: pattern.name,
url: `/patterns?id=${pattern.id}`,
})
if (result.success) {
const id = result.data.bookmark.id
setLoadingStatus([
true,
<>
{t('status:bookmarkCreated')} <small>[#{id}]</small>
</>,
true,
true,
])
} else setLoadingStatus([true, 'backendError', true, false])
}
if (!pattern) return <p>loading</p>
return (
<>
<div className="flex flex-row flex-wrap gap-4 max-w-7xl w-full">
<div className="max-w-lg grow w-full">
<Lightbox buttonClasses="w-full" boxClasses="" modalProps={{ fullWidth: 1 }}>
<PatternReactPreview {...pattern} />
</Lightbox>
</div>
<div className="w-full md:max-w-lg">
<DisplayRow title={t('name')}>{pattern.name}</DisplayRow>
<DisplayRow title="#">{pattern.id}</DisplayRow>
<DisplayRow title={t('account:publicView')}>
<PageLink href={`/pattern?id=${pattern.id}`} txt={`/pattern?id=${pattern.id}`} />
</DisplayRow>
<DisplayRow title={t('account:privateView')}>
<PageLink
href={`/account/pattern?id=${pattern.id}`}
txt={`/account/pattern?id=${pattern.id}`}
/>
</DisplayRow>
<DisplayRow title={t('created')}>
<Timeago date={pattern.createdAt} />
<span className="px-2 opacity-50">|</span>
{shortDate(i18n.language, pattern.createdAt, false)}
</DisplayRow>
<DisplayRow title={t('updated')}>
<Timeago date={pattern.createdAt} />
<span className="px-2 opacity-50">|</span>
{shortDate(i18n.language, pattern.updatedAt, false)}
</DisplayRow>
<DisplayRow title={t('public')}>
{pattern.public ? <BoolYesIcon /> : <BoolNoIcon />}
</DisplayRow>
<DisplayRow title={t('img')}>
<Lightbox buttonClasses="mask mask-squircle w-36 h-35">
<img src={cloudflareImageUrl({ id: pattern.img, variant: 'sq500' })} />
</Lightbox>
</DisplayRow>
{account.id ? (
<button className="btn btn-primary btn-outline mb-2 w-full" onClick={bookmarkPattern}>
<div className="flex flex-row items-center justify-between w-full">
<BookmarkIcon /> {t('bookmark')}
</div>
</button>
) : null}
<Link
href={newPatternUrl({ design: pattern.design, settings: pattern.settings })}
className={`btn btn-primary ${horFlexClasses}`}
>
<CloneIcon /> {t('clonePattern')}
</Link>
{isOwn ? (
<>
<Popout tip noP>
<p>{t('account:ownPublicPattern')}</p>
<Link
href={`/account/pattern?id=${pattern.id}/`}
className={`btn btn-secondary ${horFlexClasses} mt-2`}
>
<LockIcon /> {t('account:privateView')}
</Link>
</Popout>
</>
) : null}
</div>
</div>
<h2>{t('account:notes')}</h2>
{isOwn ? 'is own' : 'is not own'}
<Mdx md={pattern.notes} />
</>
)
}
export const Pattern = ({ id }) => {
// Hooks
const { account, control } = useAccount()
const { setLoadingStatus } = useContext(LoadingStatusContext)
const backend = useBackend()
const { t, i18n } = useTranslation(ns)
// Context
const { setModal } = useContext(ModalContext)
const [edit, setEdit] = useState(false)
const [pattern, setPattern] = useState()
// Set fields for editing
const [name, setName] = useState(pattern?.name)
const [image, setImage] = useState(pattern?.image)
const [isPublic, setIsPublic] = useState(pattern?.public ? true : false)
const [notes, setNotes] = useState(pattern?.notes || '')
// Effect
useEffect(() => {
const getPattern = async () => {
setLoadingStatus([true, t('backendLoadingStarted')])
const result = await backend.getPattern(id)
if (result.success) {
setPattern(result.data.pattern)
setName(result.data.pattern.name)
setImage(result.data.pattern.image)
setIsPublic(result.data.pattern.public ? true : false)
setNotes(result.data.pattern.notes)
setLoadingStatus([true, 'backendLoadingCompleted', true, true])
} else setLoadingStatus([true, 'backendError', true, false])
}
if (id) getPattern()
}, [id])
const save = async () => {
setLoadingStatus([true, 'gatheringInfo'])
// Compile data
const data = {}
if (name || name !== pattern.name) data.name = name
if (image || image !== pattern.image) data.img = image
if (notes || notes !== pattern.notes) data.notes = notes
if ([true, false].includes(isPublic) && isPublic !== pattern.public) data.public = isPublic
setLoadingStatus([true, 'savingPattern'])
const result = await backend.updatePattern(pattern.id, data)
if (result.success) {
setPattern(result.data.pattern)
setEdit(false)
setLoadingStatus([true, 'nailedIt', true, true])
} else setLoadingStatus([true, 'backendError', true, false])
}
if (!pattern) return null
const heading = (
<>
<div className="flex flex-wrap md:flex-nowrap flex-row gap-2 w-full">
<div className="w-full md:w-96 shrink-0">
<PatternCard pattern={pattern} size="md" />
</div>
<div className="flex flex-col justify-end gap-2 mb-2 grow">
{account.control > 3 && pattern?.public ? (
<div className="flex flex-row gap-2 items-center">
<a
className="badge badge-secondary font-bold badge-lg"
href={`${conf.backend}/patterns/${pattern.id}.json`}
>
JSON
</a>
<a
className="badge badge-success font-bold badge-lg"
href={`${conf.backend}/patterns/${pattern.id}.yaml`}
>
YAML
</a>
</div>
) : (
<span></span>
)}
<button
onClick={() =>
setModal(
<ModalWrapper flex="col" justify="top lg:justify-center" slideFrom="right">
<img src={cloudflareImageUrl({ type: 'public', id: pattern.img })} />
</ModalWrapper>
)
}
className={`btn btn-secondary btn-outline ${horFlexClasses}`}
>
<CameraIcon />
{t('showImage')}
</button>
{pattern.userId === account.id && (
<>
{edit ? (
<>
<button
onClick={() => setEdit(false)}
className={`btn btn-primary btn-outline ${horFlexClasses}`}
>
<ResetIcon />
{t('cancel')}
</button>
<button onClick={save} className={`btn btn-primary ${horFlexClasses}`}>
<UploadIcon />
{t('saveThing', { thing: t('account:pattern') })}
</button>
</>
) : (
<>
<Link
href={`/account/patterns/${pattern.design}/edit?id=${pattern.id}`}
className={`btn btn-primary btn-outline ${horFlexClasses}`}
>
<FreeSewingIcon /> {t('updatePattern')}
</Link>
<Link
href={newPatternUrl({ design: pattern.design, settings: pattern.settings })}
className={`btn btn-primary btn-outline ${horFlexClasses}`}
>
<CloneIcon /> {t('clonePattern')}
</Link>
<button
onClick={() => setEdit(true)}
className={`btn btn-primary ${horFlexClasses}`}
>
<EditIcon /> {t('editThing', { thing: t('account:patternMetadata') })}
</button>
</>
)}
</>
)}
</div>
</div>
<div className="flex flex-row flex-wrap gap-4 text-sm items-center justify-between mb-2"></div>
</>
)
if (!edit)
return (
<div className="max-w-2xl">
{heading}
<DisplayRow title={t('name')}>{pattern.name}</DisplayRow>
{control >= controlLevels.sets.notes && (
<DisplayRow title={t('notes')}>
<Mdx md={pattern.notes} />
</DisplayRow>
)}
{control >= controlLevels.patterns.public && (
<>
<DisplayRow title={t('public')}>
{pattern.public ? <BoolYesIcon /> : <BoolNoIcon />}
</DisplayRow>
{pattern.public && (
<DisplayRow title={t('permalink')}>
<PageLink href={`/pattern?id=${pattern.id}`} txt={`/patternid=?${pattern.id}`} />
</DisplayRow>
)}
</>
)}
{control >= controlLevels.sets.createdAt && (
<DisplayRow title={t('created')}>
<Timeago date={pattern.createdAt} />
<span className="px-2 opacity-50">|</span>
{shortDate(i18n.language, pattern.createdAt, false)}
</DisplayRow>
)}
{control >= controlLevels.patterns.updatedAt && (
<DisplayRow title={t('updated')}>
<Timeago date={pattern.updatedAt} />
<span className="px-2 opacity-50">|</span>
{shortDate(i18n.language, pattern.updatedAt, false)}
</DisplayRow>
)}
{control >= controlLevels.patterns.id && (
<DisplayRow title={t('id')}>{pattern.id}</DisplayRow>
)}
<Popout tip noP>
<p>{t('account:ownPrivatePattern')}</p>
<Link
className={`btn btn-secondary ${horFlexClasses}`}
href={`/pattern?id=${pattern.id}`}
>
<PatternIcon />
{t('account:publicView')}
</Link>
</Popout>
</div>
)
return (
<div className="max-w-2xl">
{heading}
<ul className="list list-disc list-inside ml-4">
<li>
<AnchorLink id="name" txt={t('name')} />
</li>
{account.control >= conf.account.sets.img ? (
<li>
<AnchorLink id="image" txt={t('image')} />
</li>
) : null}
{['public', 'units', 'notes'].map((id) =>
account.control >= conf.account.sets[id] ? (
<li key={id}>
<AnchorLink id="units" txt={t(id)} />
</li>
) : null
)}
</ul>
{/* Name is always shown */}
<span id="name"></span>
<StringInput
id="pattern-name"
label={t('name')}
update={setName}
current={name}
original={pattern.name}
placeholder="Maurits Cornelis Escher"
valid={(val) => val && val.length > 0}
docs={<DynamicMdx language={i18n.language} slug="docs/about/site/patterns/name" />}
/>
{/* img: Control level determines whether or not to show this */}
<span id="image"></span>
{account.control >= conf.account.sets.img ? (
<PassiveImageInput
id="pattern-img"
label={t('image')}
update={setImage}
current={image}
valid={(val) => val.length > 0}
docs={<DynamicMdx language={i18n.language} slug="docs/about/site/patterns/image" />}
/>
) : null}
{/* public: Control level determines whether or not to show this */}
<span id="public"></span>
{account.control >= conf.account.patterns.public ? (
<ListInput
id="pattern-public"
label={t('public')}
update={setIsPublic}
list={[
{
val: true,
label: (
<div className="flex flex-row items-center flex-wrap justify-between w-full">
<span>{t('publicPattern')}</span>
<OkIcon
className="w-8 h-8 text-success bg-base-100 rounded-full p-1"
stroke={4}
/>
</div>
),
desc: t('publicPatternDesc'),
},
{
val: false,
label: (
<div className="flex flex-row items-center flex-wrap justify-between w-full">
<span>{t('privatePattern')}</span>
<NoIcon className="w-8 h-8 text-error bg-base-100 rounded-full p-1" stroke={3} />
</div>
),
desc: t('privatePatternDesc'),
},
]}
current={isPublic}
docs={<DynamicMdx language={i18n.language} slug="docs/about/site/patterns/public" />}
/>
) : null}
{/* notes: Control level determines whether or not to show this */}
<span id="notes"></span>
{account.control >= conf.account.patterns.notes ? (
<MarkdownInput
id="pattern-notes"
label={t('notes')}
update={setNotes}
current={notes}
placeholder={t('mdSupport')}
docs={<DynamicMdx language={i18n.language} slug="docs/about/site/patterns/notes" />}
/>
) : null}
<button
onClick={save}
className="btn btn-primary btn-lg flex flex-row items-center gap-4 mx-auto mt-8"
>
<UploadIcon />
{t('saveThing', { thing: t('account:pattern') })}
</button>
</div>
)
}
export const PatternCard = ({
pattern,
href = false,
onClick = false,
useA = false,
size = 'md',
}) => {
const sizes = {
lg: 96,
md: 52,
sm: 36,
xs: 20,
}
const s = sizes[size]
const wrapperProps = {
className: `bg-base-300 w-full mb-2 mx-auto flex flex-col items-start text-center justify-center rounded shadow py-4 h-${s} w-${s}`,
style: {
backgroundImage: `url(${cloudflareImageUrl({ type: 'w1000', id: pattern.img })})`,
backgroundSize: 'cover',
backgroundRepeat: 'no-repeat',
backgroundPosition: '50%',
},
}
if (pattern.img === 'default-avatar') wrapperProps.style.backgroundPosition = 'bottom right'
const inner = null
// Is it a button with an onClick handler?
if (onClick)
return (
<button {...wrapperProps} onClick={onClick}>
{inner}
</button>
)
// Returns a link to an internal page
if (href && !useA)
return (
<Link {...wrapperProps} href={href}>
{inner}
</Link>
)
// Returns a link to an external page
if (href && useA)
return (
<a {...wrapperProps} href={href}>
{inner}
</a>
)
// Returns a div
return <div {...wrapperProps}>{inner}</div>
}
// Component to show the sort header in the pattern table
const SortButton = ({ field, label, order, orderAsc, updateOrder }) => (
<button
onClick={() => updateOrder(field)}
className="btn-link text-secondary flex flex-row gap-2 items-center decoration-0 no-underline"
>
{label}
{order === field ? (
<RightIcon className={`w-5 h-5 ${orderAsc ? '-rotate-90' : 'rotate-90'}`} stroke={3} />
) : null}
</button>
)
// Component for the account/patterns page
export const Patterns = () => {
const router = useRouter()
const { locale } = router
// Hooks
const backend = useBackend()
const { t } = useTranslation(ns)
const { setLoadingStatus, LoadingProgress } = useContext(LoadingStatusContext)
// State
const [patterns, setPatterns] = useState([])
const [selected, setSelected] = useState({})
const [refresh, setRefresh] = useState(0)
const [order, setOrder] = useState('id')
const [orderAsc, setOrderAsc] = useState(true)
// Helper var to see how many are selected
const selCount = Object.keys(selected).length
// Effects
useEffect(() => {
const getPatterns = async () => {
const result = await backend.getPatterns()
if (result.success) setPatterns(result.data.patterns)
}
getPatterns()
}, [refresh])
// Helper method to toggle single selection
const toggleSelect = (id) => {
const newSelected = { ...selected }
if (newSelected[id]) delete newSelected[id]
else newSelected[id] = 1
setSelected(newSelected)
}
// Helper method to toggle select all
const toggleSelectAll = () => {
if (selCount === patterns.length) setSelected({})
else {
const newSelected = {}
for (const pattern of patterns) newSelected[pattern.id] = 1
setSelected(newSelected)
}
}
// Helper to delete one or more patterns
const removeSelectedPatterns = async () => {
let i = 0
for (const pattern in selected) {
i++
await backend.removePattern(pattern)
setLoadingStatus([
true,
<LoadingProgress val={i} max={selCount} msg={t('removingPatterns')} key="linter" />,
])
}
setSelected({})
setRefresh(refresh + 1)
setLoadingStatus([true, 'nailedIt', true, true])
}
// Helper method to update the order state
const updateOrder = (field) => {
if (order !== field) {
setOrder(field)
setOrderAsc(true)
} else setOrderAsc(!orderAsc)
}
return (
<div className="max-w-4xl xl:pl-4">
<p className="text-center md:text-right">
<Link className="btn btn-primary capitalize w-full md:w-auto" href="/new/pattern">
<PlusIcon />
{t('patternNew')}
</Link>
</p>
<button className="btn btn-error" onClick={removeSelectedPatterns} disabled={selCount < 1}>
<TrashIcon /> {selCount} {t('patterns')}
</button>
<TableWrapper>
<table className="table table-auto">
<thead className="border border-base-300 border-b-2 border-t-0 border-x-0">
<tr className="">
<th className="">
<input
type="checkbox"
className="checkbox checkbox-secondary"
onClick={toggleSelectAll}
checked={patterns.length === selCount}
/>
</th>
<th>
<SortButton field="id" label="#" {...{ order, orderAsc, updateOrder }} />
</th>
<th>{t('account:img')}</th>
<th>
<SortButton
field="name"
label={t('account:name')}
{...{ order, orderAsc, updateOrder }}
/>
</th>
<th>
<SortButton
field="design"
label={t('account:design')}
{...{ order, orderAsc, updateOrder }}
/>
</th>
<th>
<SortButton
field="createdAt"
label={t('account:createdAt')}
{...{ order, orderAsc, updateOrder }}
/>
</th>
<th>
<SortButton
field="public"
label={t('account:public')}
{...{ order, orderAsc, updateOrder }}
/>
</th>
</tr>
</thead>
<tbody>
{orderBy(patterns, order, orderAsc ? 'asc' : 'desc').map((pattern, i) => (
<tr key={i}>
<td className="text-base font-medium">
<input
type="checkbox"
checked={selected[pattern.id] ? true : false}
className="checkbox checkbox-secondary"
onClick={() => toggleSelect(pattern.id)}
/>
</td>
<td className="text-base font-medium">{pattern.id}</td>
<td className="text-base font-medium">
<PatternCard
href={`/account/pattern?id=${pattern.id}`}
pattern={pattern}
size="xs"
/>
</td>
<td className="text-base font-medium">
<PageLink href={`/account/pattern?id=${pattern.id}`} txt={pattern.name} />
</td>
<td className="text-base font-medium">
<PageLink href={`/designs/${pattern.design}`} txt={capitalize(pattern.design)} />
</td>
<td className="text-base font-medium">
{shortDate(locale, pattern.createdAt, false)}
</td>
<td className="text-base font-medium">
{pattern.public ? <BoolYesIcon /> : <BoolNoIcon />}
</td>
</tr>
))}
</tbody>
</table>
</TableWrapper>
<BackToAccountButton />
</div>
)
}

View file

@ -1,55 +0,0 @@
// Context
import { LoadingStatusContext } from 'shared/context/loading-status-context.mjs'
// Hooks
import { useState, useContext } from 'react'
import { useTranslation } from 'next-i18next'
import { useAccount } from 'shared/hooks/use-account.mjs'
import { useBackend } from 'shared/hooks/use-backend.mjs'
// Components
import { BackToAccountButton } from './shared.mjs'
import { SaveSettingsButton } from 'shared/components/buttons/save-settings-button.mjs'
import { StringInput } from 'shared/components/inputs.mjs'
import { DynamicMdx } from 'shared/components/mdx/dynamic.mjs'
export const ns = ['account', 'status']
export const PlatformSettings = ({ platform }) => {
// Hooks
const { account, setAccount } = useAccount()
const backend = useBackend()
const { t, i18n } = useTranslation(ns)
const { setLoadingStatus } = useContext(LoadingStatusContext)
// State
const [platformId, setPlatformId] = useState(account.data[platform] || '')
// Helper method to save changes
const save = async () => {
setLoadingStatus([true, 'processingUpdate'])
const data = { data: {} }
data.data[platform] = platformId
const result = await backend.updateAccount(data)
if (result.success) {
setAccount(result.data.account)
setLoadingStatus([true, 'settingsSaved', true, true])
} else setLoadingStatus([true, 'backendError', true, false])
}
return (
<div className="max-w-xl">
<StringInput
id={`account-${platform}`}
label={t(platform === 'website' ? 'account:websiteTitle' : 'account:platformTitle', {
platform: platform,
})}
current={platformId}
update={setPlatformId}
valid={(val) => val.length > 0}
placeholder={'joostdecock'}
docs={<DynamicMdx language={i18n.language} slug={`docs/about/site/account/platform`} />}
/>
<SaveSettingsButton btnProps={{ onClick: save }} />
<BackToAccountButton />
</div>
)
}

View file

@ -1,40 +0,0 @@
// Context
import { LoadingStatusContext } from 'shared/context/loading-status-context.mjs'
// Hooks
import { useTranslation } from 'next-i18next'
import { useContext } from 'react'
import { useAccount } from 'shared/hooks/use-account.mjs'
import { useBackend } from 'shared/hooks/use-backend.mjs'
// Components
import { BackToAccountButton } from './shared.mjs'
export const ns = ['account', 'status']
export const ReloadAccount = ({ title = false }) => {
// Hooks
const { setAccount } = useAccount()
const backend = useBackend()
const { t } = useTranslation(ns)
const { setLoadingStatus } = useContext(LoadingStatusContext)
// Helper method to reload account
const reload = async () => {
setLoadingStatus([true, 'processingUpdate'])
const result = await backend.reloadAccount()
if (result.success) {
setAccount(result.data.account)
setLoadingStatus([true, 'nailedIt', true, true])
} else setLoadingStatus([true, 'backendError', true, false])
}
return (
<div className="max-w-xl">
{title ? <h2>{t('reloadMsg1')}</h2> : null}
<p>{t('reloadMsg2')}</p>
<button className="btn btn-primary capitalize w-full my-2" onClick={reload}>
{t('reload')}
</button>
<BackToAccountButton />
</div>
)
}

View file

@ -1,42 +0,0 @@
// Context
import { LoadingStatusContext } from 'shared/context/loading-status-context.mjs'
// Hooks
import { useContext } from 'react'
import { useTranslation } from 'next-i18next'
import { useAccount } from 'shared/hooks/use-account.mjs'
import { useBackend } from 'shared/hooks/use-backend.mjs'
// Components
import { BackToAccountButton } from './shared.mjs'
import { Popout } from 'shared/components/popout/index.mjs'
export const ns = ['account', 'status']
export const RemoveAccount = () => {
// Hooks
const { signOut } = useAccount()
const backend = useBackend()
const { t } = useTranslation(ns)
const { setLoadingStatus } = useContext(LoadingStatusContext)
// Helper method to export account
const removeAccount = async () => {
setLoadingStatus([true, 'processingUpdate'])
const result = await backend.removeAccount()
if (result.success) {
setLoadingStatus([true, 'nailedIt', true, true])
signOut()
} else setLoadingStatus([true, 'backendError', true, false])
}
return (
<div className="max-w-xl">
<Popout warning>
<h3>{t('noWayBack')}</h3>
<button className="btn btn-error capitalize w-full my-2" onClick={removeAccount}>
{t('remove')}
</button>
</Popout>
<BackToAccountButton />
</div>
)
}

View file

@ -1,43 +0,0 @@
// Context
import { LoadingStatusContext } from 'shared/context/loading-status-context.mjs'
// Hooks
import { useContext } from 'react'
import { useTranslation } from 'next-i18next'
import { useAccount } from 'shared/hooks/use-account.mjs'
import { useBackend } from 'shared/hooks/use-backend.mjs'
// Components
import { BackToAccountButton } from './shared.mjs'
import { Popout } from 'shared/components/popout/index.mjs'
export const ns = ['account', 'status']
export const RestrictAccount = () => {
// Hooks
const { signOut } = useAccount()
const backend = useBackend()
const { t } = useTranslation(ns)
const { setLoadingStatus } = useContext(LoadingStatusContext)
// Helper method to export account
const restrictAccount = async () => {
setLoadingStatus([true, 'processingUpdate'])
const result = await backend.restrictAccount()
if (result.success) {
setLoadingStatus([true, 'nailedIt', true, true])
signOut()
} else setLoadingStatus([true, 'backendError', true, false])
}
return (
<div className="max-w-xl">
<Popout warning>
<h5>{t('proceedWithCaution')}</h5>
<p className="text-lg">{t('restrictWarning')}</p>
<button className="btn btn-error capitalize w-full my-2" onClick={restrictAccount}>
{t('restrict')}
</button>
</Popout>
<BackToAccountButton />
</div>
)
}

View file

@ -1,28 +0,0 @@
export const ns = ['roles']
const colors = {
user: 'primary',
curator: 'secondary',
bughunter: 'accent',
support: 'warning',
admin: 'error',
}
export const AccountRole = ({ role }) => {
const color = colors[role]
return (
<span className={``}>
<span
className={`text-xs uppercase bg-${color} rounded-l-lg pl-1 font-bold text-base-100 border border-2 border-solid border-${color}`}
>
role
</span>
<span
className={`text-xs uppercase bg-base-100 text-${color} rounded-r-lg px-1 font-bold border border-2 border-solid border-${color}`}
>
{role}
</span>
</span>
)
}

View file

@ -19,126 +19,3 @@ export const welcomeSteps = {
4: ['', 'newsletter', 'units', 'compare', 'username', 'bio', 'img'], 4: ['', 'newsletter', 'units', 'compare', 'username', 'bio', 'img'],
5: [''], 5: [''],
} }
/*
import { Spinner } from 'shared/components/spinner.mjs'
import Link from 'next/link'
import { useTranslation } from 'next-i18next'
import {
CogIcon,
FingerprintIcon as ControlIcon,
NewsletterIcon,
UnitsIcon,
CompareIcon,
LabelIcon,
BioIcon,
UserIcon,
LeftIcon,
OkIcon,
NoIcon,
} from 'shared/components/icons.mjs'
const btnClasses = {
dflt:
'btn w-full mt-2 btn-secondary ' +
'flex flex-row flex-nowrap items-center gap-4 py-4 h-auto ' +
'border border-secondary justify-start text-left bg-opacity-30',
active:
'btn-ghost bg-secondary hover:bg-secondary ' + 'hover:bg-opacity-30 hover:border-secondary',
inactive:
'hover:bg-opacity-20 hover:bg-secondary btn-ghost ' +
'border border-secondary hover:border hover:border-secondary',
}
export const NumberBullet = ({ nr, color = 'secondary' }) => (
<span
className={`p-2 w-8 h-8 flex flex-col items-center justify-center shrink-0 rounded-full text-center p-0 py-2 bg-${color} text-${color}-content border-2 border-base-100`}
>
{nr}
</span>
)
export const BackToAccountButton = ({ loading = false }) => {
const { t } = useTranslation(['account'])
return (
<Link className={`btn ${loading ? 'btn-accent' : 'btn-secondary'} mt-4 pr-6`} href="/account">
<span className="flex flex-row items-center gap-2">
{loading ? <Spinner /> : <LeftIcon />}
{t('yourAccount')}
</span>
</Link>
)
}
export const Choice = ({
val,
update,
current,
children,
bool = false,
boolChoices = {
yes: <OkIcon className="w-6 h-6 text-success shrink-0" stroke={4} />,
no: <NoIcon className="w-6 h-6 text-error shrink-0" stroke={3} />,
},
}) => {
const active = val === current
return (
<button
className={`${btnClasses.dflt} ${active ? btnClasses.active : btnClasses.inactive}`}
onClick={() => update(val)}
>
{bool ? boolChoices[val] : <NumberBullet nr={val} />}
<div className={`normal-case text-base-content`}>{children}</div>
</button>
)
}
export const DoneIcon = ({ href }) => (
<Link href={`/welcome/${href}`} className="text-success hover:text-secondary">
<TopicIcon href={href} />
</Link>
)
export const TodoIcon = ({ href }) => (
<Link href={`/welcome/${href}`} className="text-secondary w-6 h-6 opacity-50 hover:opacity-100">
<TopicIcon href={href} />
</Link>
)
const TopicIcon = (props) => {
const Icon =
props.href === '' || props.href === 'control'
? ControlIcon
: icons[props.href]
? icons[props.href]
: CogIcon
return <Icon {...props} />
}
const DoingIcon = ({ href }) => <TopicIcon href={href} className="w-6 h-6 text-base-content" />
export const Icons = ({ done = [], todo = [], current = '' }) => (
<div className="m-auto flex flex-row items-center justify-center gap-2">
{done.map((href) => (
<DoneIcon href={href} key={href} />
))}
<DoingIcon href={current} />
{todo.map((href) => (
<TodoIcon href={href} key={href} />
))}
</div>
)
const icons = {
newsletter: NewsletterIcon,
units: UnitsIcon,
compare: CompareIcon,
username: LabelIcon,
bio: BioIcon,
img: UserIcon,
}
*/

View file

@ -1,22 +0,0 @@
import { freeSewingConfig } from 'shared/config/freesewing.config.mjs'
export const ns = ['status']
export const AccountStatus = ({ status }) => {
const { name, color } = freeSewingConfig.statuses[status]
return (
<span className={``}>
<span
className={`text-xs uppercase bg-${color} rounded-l-lg pl-1 font-bold text-base-100 border border-2 border-solid border-${color}`}
>
status
</span>
<span
className={`text-xs uppercase bg-base-100 text-${color} rounded-r-lg px-1 font-bold border border-2 border-solid border-${color}`}
>
{name}
</span>
</span>
)
}

View file

@ -1,109 +0,0 @@
// Context
import { LoadingStatusContext } from 'shared/context/loading-status-context.mjs'
// Hooks
import { useState, useContext } from 'react'
import { useTranslation } from 'next-i18next'
import { useAccount } from 'shared/hooks/use-account.mjs'
import { useBackend } from 'shared/hooks/use-backend.mjs'
// Components
import { Icons, welcomeSteps, BackToAccountButton } from './shared.mjs'
import { OkIcon, NoIcon } from 'shared/components/icons.mjs'
import { ContinueButton } from 'shared/components/buttons/continue-button.mjs'
import { StringInput } from 'shared/components/inputs.mjs'
import { DynamicMdx } from 'shared/components/mdx/dynamic.mjs'
export const ns = ['account', 'status']
export const UsernameSettings = ({ welcome = false }) => {
// Hooks
const { account, setAccount } = useAccount()
const backend = useBackend()
const { setLoadingStatus } = useContext(LoadingStatusContext)
const { t, i18n } = useTranslation(ns)
const [username, setUsername] = useState(account.username)
const [available, setAvailable] = useState(true)
const update = async (value) => {
if (value !== username) {
setUsername(value)
const result = await backend.isUsernameAvailable(value)
if (result?.response?.response?.status === 404) setAvailable(true)
else setAvailable(false)
}
}
const save = async () => {
setLoadingStatus([true, 'processingUpdate'])
const result = await backend.updateAccount({ username })
if (result.success) {
setAccount(result.data.account)
setLoadingStatus([true, 'settingsSaved', true, true])
} else setLoadingStatus([true, 'backendError', true, true])
}
const nextHref =
welcomeSteps[account.control].length > 5
? '/welcome/' + welcomeSteps[account.control][5]
: '/docs/about/guide'
let btnClasses = 'btn mt-4 capitalize '
if (welcome) btnClasses += 'w-64 btn-secondary'
else btnClasses += 'w-full btn-primary'
return (
<div className="w-full">
<StringInput
id="account-username"
label={t('usernameTitle')}
current={username}
update={update}
valid={() => available}
placeholder={'Sorcha Ni Dhubghaill'}
labelBL={
<span className="flex flex-row gap-1 items-center">
{available ? (
<>
<OkIcon className="w-4 h-4 text-success" stroke={4} /> {t('usernameAvailable')}
</>
) : (
<>
<NoIcon className="w-4 h-4 text-error" stroke={3} /> {t('usernameNotAvailable')}
</>
)}
</span>
}
docs={<DynamicMdx language={i18n.language} slug={`docs/about/site/account/username`} />}
/>
<button className={btnClasses} disabled={!available} onClick={save}>
<span className="flex flex-row items-center gap-2">
{available ? t('save') : t('usernameNotAvailable')}
</span>
</button>
{welcome ? (
<>
<ContinueButton btnProps={{ href: nextHref }} link />
{welcomeSteps[account.control].length > 0 ? (
<>
<progress
className="progress progress-primary w-full mt-12"
value={500 / welcomeSteps[account.control].length}
max="100"
></progress>
<span className="pt-4 text-sm font-bold opacity-50">
5 / {welcomeSteps[account.control].length}
</span>
<Icons
done={welcomeSteps[account.control].slice(0, 4)}
todo={welcomeSteps[account.control].slice(5)}
current="username"
/>
</>
) : null}
</>
) : (
<BackToAccountButton />
)}
</div>
)
}

View file

@ -0,0 +1,30 @@
import React from 'react'
/**
* A button with an icon and a label. Common across our UI
*
* @param {object} props - All React props
* @param {string} title - The button title
* @param {string} className - Any EXTRA classes to add
* @param {string} color - The main button color
* @param {string} href - Set this to make it a link
*/
export const IconButton = ({
title = '',
className = '',
onClick = false,
href = false,
color = 'primary',
children = [],
btnProps = {},
}) => {
const allProps = {
className: `flex flex-row gap-2 lg:gap-12 items-center justify-between w-full lg:w-auto daisy-btn daisy-btn-${color} capitalize my-2 ${className}`,
title: title,
...btnProps,
}
if (onClick) allProps.onClick = onClick
else if (href) allProps.href = href
return onClick ? <button {...allProps}>{children}</button> : <a {...allProps}>{children}</a>
}

View file

@ -29,6 +29,7 @@
"./xray": "./src/pattern-xray/index.mjs", "./xray": "./src/pattern-xray/index.mjs",
"./components/Account": "./components/Account/index.mjs", "./components/Account": "./components/Account/index.mjs",
"./components/Breadcrumbs": "./components/Breadcrumbs/index.mjs", "./components/Breadcrumbs": "./components/Breadcrumbs/index.mjs",
"./components/Button": "./components/Button/index.mjs",
"./components/Control": "./components/Control/index.mjs", "./components/Control": "./components/Control/index.mjs",
"./components/CopyToClipboard": "./components/CopyToClipboard/index.mjs", "./components/CopyToClipboard": "./components/CopyToClipboard/index.mjs",
"./components/Docusaurus": "./components/Docusaurus/index.mjs", "./components/Docusaurus": "./components/Docusaurus/index.mjs",

View file

@ -5,11 +5,20 @@ sidebar_position: 43
import { DocusaurusDoc } from '@freesewing/react/components/Docusaurus' import { DocusaurusDoc } from '@freesewing/react/components/Docusaurus'
import { RoleBlock } from '@freesewing/react/components/Role' import { RoleBlock } from '@freesewing/react/components/Role'
import { Bio } from '@freesewing/react/components/Account' import { ImportSet } from '@freesewing/react/components/Account'
import Link from '@docusaurus/Link'
<DocusaurusDoc> <DocusaurusDoc>
<RoleBlock user> <RoleBlock user>
<Bio Link={Link} />
This page allows you to import data into your FreeSewing account.
Currently, we support importing the following types of data:
- Measurements Sets
## Import Measurements Sets {#sets}
<ImportSet />
</RoleBlock> </RoleBlock>
</DocusaurusDoc> </DocusaurusDoc>

View file

@ -5,11 +5,10 @@ sidebar_position: 43
import { DocusaurusDoc } from '@freesewing/react/components/Docusaurus' import { DocusaurusDoc } from '@freesewing/react/components/Docusaurus'
import { RoleBlock } from '@freesewing/react/components/Role' import { RoleBlock } from '@freesewing/react/components/Role'
import { Bio } from '@freesewing/react/components/Account' import { Reload } from '@freesewing/react/components/Account'
import Link from '@docusaurus/Link'
<DocusaurusDoc> <DocusaurusDoc>
<RoleBlock user> <RoleBlock user>
<Bio Link={Link} /> <Reload />
</RoleBlock> </RoleBlock>
</DocusaurusDoc> </DocusaurusDoc>

View file

@ -5,11 +5,11 @@ sidebar_position: 43
import { DocusaurusDoc } from '@freesewing/react/components/Docusaurus' import { DocusaurusDoc } from '@freesewing/react/components/Docusaurus'
import { RoleBlock } from '@freesewing/react/components/Role' import { RoleBlock } from '@freesewing/react/components/Role'
import { Bio } from '@freesewing/react/components/Account' import { Remove } from '@freesewing/react/components/Account'
import Link from '@docusaurus/Link' import Link from '@docusaurus/Link'
<DocusaurusDoc> <DocusaurusDoc>
<RoleBlock user> <RoleBlock user>
<Bio Link={Link} /> <Remove />
</RoleBlock> </RoleBlock>
</DocusaurusDoc> </DocusaurusDoc>

View file

@ -5,11 +5,11 @@ sidebar_position: 43
import { DocusaurusDoc } from '@freesewing/react/components/Docusaurus' import { DocusaurusDoc } from '@freesewing/react/components/Docusaurus'
import { RoleBlock } from '@freesewing/react/components/Role' import { RoleBlock } from '@freesewing/react/components/Role'
import { Bio } from '@freesewing/react/components/Account' import { Restrict } from '@freesewing/react/components/Account'
import Link from '@docusaurus/Link' import Link from '@docusaurus/Link'
<DocusaurusDoc> <DocusaurusDoc>
<RoleBlock user> <RoleBlock user>
<Bio Link={Link} /> <Restrict Link={Link} />
</RoleBlock> </RoleBlock>
</DocusaurusDoc> </DocusaurusDoc>