wip: More account pages
This commit is contained in:
parent
543be68c1f
commit
c994e3898f
58 changed files with 1419 additions and 29 deletions
|
@ -132,6 +132,7 @@ react:
|
|||
luxon: "^3.5.0"
|
||||
nuqs: "^1.17.6"
|
||||
react-markdown: "^9.0.1"
|
||||
tlds: "^1.255.0"
|
||||
use-local-storage-state: "19.1.0"
|
||||
use-session-storage-state: "^19.0.0"
|
||||
peer:
|
||||
|
|
|
@ -109,6 +109,7 @@ packageJson:
|
|||
# Hooks
|
||||
"./hooks/useAccount": "./hooks/useAccount/index.mjs"
|
||||
"./hooks/useBackend": "./hooks/useBackend/index.mjs"
|
||||
"./hooks/useControl": "./hooks/useControl/index.mjs"
|
||||
"./hooks/useSelection": "./hooks/useSelection/index.mjs"
|
||||
# Lib
|
||||
"./lib/RestClient": "./lib/RestClient/index.mjs"
|
||||
|
|
12
package-lock.json
generated
12
package-lock.json
generated
|
@ -61565,6 +61565,7 @@
|
|||
}
|
||||
},
|
||||
"packages/i18n": {
|
||||
"name": "@freesewing/i18n",
|
||||
"version": "3.3.0-rc.1",
|
||||
"license": "MIT",
|
||||
"devDependencies": {},
|
||||
|
@ -62042,6 +62043,9 @@
|
|||
"name": "@freesewing/utils",
|
||||
"version": "3.3.0-rc.1",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"tlds": "^1.255.0"
|
||||
},
|
||||
"devDependencies": {},
|
||||
"engines": {
|
||||
"node": ">= 20"
|
||||
|
@ -62051,6 +62055,14 @@
|
|||
"url": "https://freesewing.org/patrons/join"
|
||||
}
|
||||
},
|
||||
"packages/utils/node_modules/tlds": {
|
||||
"version": "1.255.0",
|
||||
"resolved": "https://registry.npmjs.org/tlds/-/tlds-1.255.0.tgz",
|
||||
"integrity": "sha512-tcwMRIioTcF/FcxLev8MJWxCp+GUALRhFEqbDoZrnowmKSGqPrl5pqS+Sut2m8BgJ6S4FExCSSpGffZ0Tks6Aw==",
|
||||
"bin": {
|
||||
"tlds": "bin.js"
|
||||
}
|
||||
},
|
||||
"plugins/core-plugins": {
|
||||
"name": "@freesewing/core-plugins",
|
||||
"version": "3.3.0-rc.1",
|
||||
|
|
|
@ -26,7 +26,6 @@ const account = {
|
|||
bookmarks: 2,
|
||||
sets: 1,
|
||||
patterns: 1,
|
||||
apikeys: 4,
|
||||
},
|
||||
info: {
|
||||
username: 2,
|
||||
|
@ -145,3 +144,26 @@ export const control = {
|
|||
views: editor.views,
|
||||
},
|
||||
}
|
||||
|
||||
export const controlDesc = {
|
||||
1: {
|
||||
title: `Keep it as simple as possible`,
|
||||
desc: `Hides all but the most crucial features.`,
|
||||
},
|
||||
2: {
|
||||
title: `Keep it simple, but not too simple`,
|
||||
desc: `Hides the majority of features.`,
|
||||
},
|
||||
3: {
|
||||
title: `Balance simplicity with power`,
|
||||
desc: `Reveals the majority of features, but not all.`,
|
||||
},
|
||||
4: {
|
||||
title: `Give me all powers, but keep me safe`,
|
||||
desc: `Reveals all features, keeps handrails and safety checks`,
|
||||
},
|
||||
5: {
|
||||
title: `Get out of my way`,
|
||||
desc: `Reveals all features, removes handrails and safety checks`,
|
||||
},
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { cloudflare } from './cloudflare.mjs'
|
||||
import { control } from './control.mjs'
|
||||
import { control, controlDesc } from './control.mjs'
|
||||
import { logoPath } from './logo.mjs'
|
||||
import { measurements, degreeMeasurements, isDegreeMeasurement } from './measurements.mjs'
|
||||
import { sewingTechniques } from './sewing.mjs'
|
||||
|
@ -14,6 +14,7 @@ export {
|
|||
apikeyLevels,
|
||||
cloudflare,
|
||||
control,
|
||||
controlDesc,
|
||||
logoPath,
|
||||
measurements,
|
||||
degreeMeasurements,
|
||||
|
|
103
packages/react/components/Account/Avatar.mjs
Normal file
103
packages/react/components/Account/Avatar.mjs
Normal file
|
@ -0,0 +1,103 @@
|
|||
// Dependencies
|
||||
import { welcomeSteps } from './shared.mjs'
|
||||
import { cloudflareImageUrl } from '@freesewing/utils'
|
||||
|
||||
// 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 { PassiveImageInput } from '@freesewing/react/components/Input'
|
||||
|
||||
/*
|
||||
* 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 Avatar = ({ welcome = false, Link = false }) => {
|
||||
if (!Link) Link = WebLink
|
||||
|
||||
// Hooks
|
||||
const { account, setAccount } = useAccount()
|
||||
const backend = useBackend()
|
||||
|
||||
// State
|
||||
const [img, setImg] = useState('')
|
||||
|
||||
// Context
|
||||
const { setLoadingStatus } = useContext(LoadingStatusContext)
|
||||
|
||||
// Save handler
|
||||
const save = async () => {
|
||||
setLoadingStatus([true, 'Uploading image'])
|
||||
const [status, body] = await backend.updateAccount({ img })
|
||||
if (status === 200 && body.result === 'success') {
|
||||
setAccount(body.account)
|
||||
setLoadingStatus([true, 'Avatar saved', true, true])
|
||||
} else setLoadingStatus([true, 'Failed to save avatar image. Please report this', true, false])
|
||||
}
|
||||
|
||||
// Next page in welcome flow
|
||||
const nextHref = '/docs/about/guide'
|
||||
|
||||
return (
|
||||
<div className="w-full">
|
||||
{!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="Avatar image"
|
||||
placeholder={'image'}
|
||||
update={setImg}
|
||||
current={img}
|
||||
valid={(val) => val.length > 0}
|
||||
/>
|
||||
{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}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<p className="text-right">
|
||||
<button className="daisy-btn daisy-btn-primary w-full lg:w-auto mt-8" onClick={save}>
|
||||
<SaveIcon /> Save Avatar
|
||||
</button>
|
||||
</p>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
85
packages/react/components/Account/Bio.mjs
Normal file
85
packages/react/components/Account/Bio.mjs
Normal file
|
@ -0,0 +1,85 @@
|
|||
// Dependencies
|
||||
import { welcomeSteps } from './shared.mjs'
|
||||
|
||||
// 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 { MarkdownInput } from '@freesewing/react/components/Input'
|
||||
|
||||
/*
|
||||
* 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 Bio = ({ welcome = false, Link = false }) => {
|
||||
if (!Link) Link = WebLink
|
||||
|
||||
// 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>
|
||||
)
|
||||
}
|
|
@ -88,7 +88,7 @@ export const Bookmarks = () => {
|
|||
for (const type in types) perType[type] = bookmarks.filter((b) => b.type === type)
|
||||
|
||||
return (
|
||||
<div className="max-w-4xl xl:pl-4">
|
||||
<div className="w-full">
|
||||
<p className="text-center md:text-right">
|
||||
<button
|
||||
className="daisy-btn daisy-btn-primary capitalize w-full md:w-auto hover:text-primary-content hover:no-underline"
|
||||
|
@ -100,7 +100,7 @@ export const Bookmarks = () => {
|
|||
slideFrom="right"
|
||||
keepOpenOnClick
|
||||
>
|
||||
<div className="w-full max-w-xl">
|
||||
<div className="w-full">
|
||||
<h2>New Bookmark</h2>
|
||||
<NewBookmark onCreated={() => setRefresh(refresh + 1)} />
|
||||
</div>
|
||||
|
|
118
packages/react/components/Account/Compare.mjs
Normal file
118
packages/react/components/Account/Compare.mjs
Normal file
|
@ -0,0 +1,118 @@
|
|||
// Dependencies
|
||||
import { welcomeSteps } from './shared.mjs'
|
||||
|
||||
// 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 { NoIcon, OkIcon, SaveIcon } from '@freesewing/react/components/Icon'
|
||||
import { ListInput } from '@freesewing/react/components/Input'
|
||||
|
||||
const strings = {
|
||||
yes: {
|
||||
title: 'Yes, in case it may help me',
|
||||
desc:
|
||||
'Allowing us to compare your measurments to a baseline or others measurements sets ' +
|
||||
'allows us to detect potential problems in your measurements or patterns.',
|
||||
},
|
||||
no: {
|
||||
title: 'No, never compare',
|
||||
desc:
|
||||
'We get it, comparison is the thief of joy. Just be aware that this limits our ability ' +
|
||||
'to warn you about potential problems in your measurements sets or patterns.',
|
||||
},
|
||||
}
|
||||
|
||||
/*
|
||||
* Component for the account/preferences/compare page
|
||||
*
|
||||
* @params {object} props - All React props
|
||||
* @params {bool} props.welcome - Set to true to use this component on the welcome page
|
||||
*/
|
||||
export const Compare = ({ welcome = false }) => {
|
||||
if (!Link) Link = WebLink
|
||||
|
||||
// Hooks
|
||||
const { account, setAccount } = useAccount()
|
||||
const backend = useBackend()
|
||||
|
||||
// State
|
||||
const [selection, setSelection] = useState(account?.compare ? 'yes' : 'no')
|
||||
|
||||
// Context
|
||||
const { setLoadingStatus } = useContext(LoadingStatusContext)
|
||||
|
||||
// Helper method to update the account
|
||||
const update = async (val) => {
|
||||
if (val !== selection) {
|
||||
setLoadingStatus([true, 'Saving preferences'])
|
||||
const [status, body] = await backend.updateAccount({
|
||||
compare: val === 'yes' ? true : false,
|
||||
})
|
||||
if (status === 200) {
|
||||
setLoadingStatus([true, 'Preferences saved', true, true])
|
||||
setAccount(body.account)
|
||||
setSelection(val)
|
||||
} else setLoadingStatus([true, 'An error occured. Please report this.', 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="Are you comfortable with your measurements sets being compared?"
|
||||
list={['yes', 'no'].map((val) => ({
|
||||
val,
|
||||
label: (
|
||||
<div className="flex flex-row items-center w-full justify-between">
|
||||
<span>{strings[val].title}</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: strings[val].desc,
|
||||
}))}
|
||||
current={selection}
|
||||
update={update}
|
||||
/>
|
||||
{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}
|
||||
</>
|
||||
) : null}
|
||||
</div>
|
||||
)
|
||||
}
|
90
packages/react/components/Account/Control.mjs
Normal file
90
packages/react/components/Account/Control.mjs
Normal file
|
@ -0,0 +1,90 @@
|
|||
// Dependencies
|
||||
import { welcomeSteps } from './shared.mjs'
|
||||
import { controlDesc } from '@freesewing/config'
|
||||
|
||||
// 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'
|
||||
import { useControl } from '@freesewing/react/hooks/useControl'
|
||||
|
||||
// Components
|
||||
import { Link as WebLink } from '@freesewing/react/components/Link'
|
||||
import { NoIcon, OkIcon, SaveIcon } from '@freesewing/react/components/Icon'
|
||||
import { ListInput } from '@freesewing/react/components/Input'
|
||||
import { ControlScore } from '@freesewing/react/components/Control'
|
||||
|
||||
const strings = {
|
||||
1: {
|
||||
title: 'Keep it as simple as possible',
|
||||
desc:
|
||||
'Allowing us to compare your measurments to a baseline or others measurements sets ' +
|
||||
'allows us to detect potential problems in your measurements or patterns.',
|
||||
},
|
||||
2: {
|
||||
title: 'No, never compare',
|
||||
desc:
|
||||
'We get it, comparison is the thief of joy. Just be aware that this limits our ability ' +
|
||||
'to warn you about potential problems in your measurements sets or patterns.',
|
||||
},
|
||||
}
|
||||
|
||||
/*
|
||||
* Component for the account/preferences/control page
|
||||
*
|
||||
* @params {object} props - All React props
|
||||
* @params {bool} props.welcome - Set to true to use this component on the welcome page
|
||||
*/
|
||||
export const Control = ({ welcome = false }) => {
|
||||
// Hooks
|
||||
const { control, updateControl } = useControl()
|
||||
|
||||
// Helper to get the link to the next onboarding step
|
||||
const nextHref = welcome
|
||||
? welcomeSteps[control].length > 1
|
||||
? '/welcome/' + welcomeSteps[control][1]
|
||||
: '/docs/about/guide'
|
||||
: false
|
||||
|
||||
return (
|
||||
<div className="w-full">
|
||||
<ListInput
|
||||
id="account-control"
|
||||
label="User Experience"
|
||||
list={[1, 2, 3, 4, 5].map((val) => ({
|
||||
val,
|
||||
label: (
|
||||
<div className="flex flex-row items-center w-full justify-between">
|
||||
<span>{controlDesc[val].title}</span>
|
||||
<ControlScore control={val} />
|
||||
</div>
|
||||
),
|
||||
desc: controlDesc[val].desc,
|
||||
}))}
|
||||
current={control}
|
||||
update={updateControl}
|
||||
/>
|
||||
{welcome ? (
|
||||
<>
|
||||
<ContinueButton btnProps={{ href: nextHref }} link />
|
||||
{welcomeSteps[control].length > 1 ? (
|
||||
<>
|
||||
<progress
|
||||
className="progress progress-primary w-full mt-12"
|
||||
value={100 / welcomeSteps[control].length}
|
||||
max="100"
|
||||
></progress>
|
||||
<span className="pt-4 text-sm font-bold opacity-50">
|
||||
1 / {welcomeSteps[control].length}
|
||||
</span>
|
||||
<Icons done={[]} todo={welcomeSteps[control].slice(1)} current="" />
|
||||
</>
|
||||
) : null}
|
||||
</>
|
||||
) : null}
|
||||
</div>
|
||||
)
|
||||
}
|
87
packages/react/components/Account/Email.mjs
Normal file
87
packages/react/components/Account/Email.mjs
Normal file
|
@ -0,0 +1,87 @@
|
|||
// Dependencies
|
||||
import { welcomeSteps } from './shared.mjs'
|
||||
import { validateEmail, validateTld } from '@freesewing/utils'
|
||||
|
||||
// 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 { EmailInput } from '@freesewing/react/components/Input'
|
||||
import { Popout } from '@freesewing/react/components/Popout'
|
||||
|
||||
/*
|
||||
* 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 Email = ({ welcome = false, Link = false }) => {
|
||||
if (!Link) Link = WebLink
|
||||
|
||||
// Hooks
|
||||
const { account, setAccount } = useAccount()
|
||||
const backend = useBackend()
|
||||
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, 'Updating email address'])
|
||||
const [status, body] = await backend.updateAccount({ email })
|
||||
if (status === 200 && body.result === 'success') {
|
||||
setAccount(body.account)
|
||||
setChanged(true)
|
||||
setLoadingStatus([true, 'Email change initiated', true, true])
|
||||
} else setLoadingStatus([true, 'Something went wrong. Please report this', true, true])
|
||||
}
|
||||
|
||||
// Is email valid?
|
||||
const valid = (validateEmail(email) && validateTld(email)) || false
|
||||
|
||||
return (
|
||||
<div className="w-full">
|
||||
{changed ? (
|
||||
<Popout note>
|
||||
<h3>Please confirm this change</h3>
|
||||
<p>
|
||||
We have sent an E-mail to your new address to confirm this change. Please click the link
|
||||
in that message to finalize this change.
|
||||
</p>
|
||||
</Popout>
|
||||
) : (
|
||||
<>
|
||||
<EmailInput
|
||||
id="account-email"
|
||||
label="Email Address"
|
||||
placeholder="example@freesewing.org"
|
||||
update={setEmail}
|
||||
labelBL="You will need to confirm that you can receive email at this address"
|
||||
current={email}
|
||||
original={account.email}
|
||||
valid={() => valid}
|
||||
/>
|
||||
<p className="text-right">
|
||||
<button
|
||||
className="daisy-btn daisy-btn-primary w-full lg:w-auto mt-8"
|
||||
onClick={save}
|
||||
disabled={!valid || email.toLowerCase() === account.email}
|
||||
>
|
||||
<SaveIcon /> Update Email Address
|
||||
</button>
|
||||
</p>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
61
packages/react/components/Account/Github.mjs
Normal file
61
packages/react/components/Account/Github.mjs
Normal file
|
@ -0,0 +1,61 @@
|
|||
// 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 { SaveIcon } from '@freesewing/react/components/Icon'
|
||||
import { StringInput } from '@freesewing/react/components/Input'
|
||||
|
||||
/*
|
||||
* Component for the account/social/github page
|
||||
*/
|
||||
export const Github = () => {
|
||||
// Hooks
|
||||
const { account, setAccount } = useAccount()
|
||||
const backend = useBackend()
|
||||
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, 'Saving bio'])
|
||||
const [status, body] = await backend.updateAccount({ data: { githubUsername, githubEmail } })
|
||||
if (status === 200 && body.result === 'success') {
|
||||
setAccount(body.account)
|
||||
setLoadingStatus([true, 'GitHub info updated', true, true])
|
||||
} else setLoadingStatus([true, 'Something went wrong. Please report this', true, true])
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="w-full">
|
||||
<StringInput
|
||||
id="account-github-email"
|
||||
label="GitHub Email Address"
|
||||
current={githubEmail}
|
||||
update={setGithubEmail}
|
||||
valid={(val) => val.length > 0}
|
||||
placeholder={'joost@joost.at'}
|
||||
/>
|
||||
<StringInput
|
||||
id="account-github-username"
|
||||
label="GitHub Username"
|
||||
current={githubUsername}
|
||||
update={setGithubUsername}
|
||||
valid={(val) => val.length > 0}
|
||||
placeholder={'joostdecock'}
|
||||
/>
|
||||
<p className="text-right">
|
||||
<button className="daisy-btn daisy-btn-primary w-full lg:w-auto mt-8" onClick={save}>
|
||||
<SaveIcon /> Save
|
||||
</button>
|
||||
</p>
|
||||
</div>
|
||||
)
|
||||
}
|
61
packages/react/components/Account/Instagram.mjs
Normal file
61
packages/react/components/Account/Instagram.mjs
Normal file
|
@ -0,0 +1,61 @@
|
|||
// 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 { SaveIcon } from '@freesewing/react/components/Icon'
|
||||
import { StringInput } from '@freesewing/react/components/Input'
|
||||
|
||||
/*
|
||||
* Component for the account/social/github page
|
||||
*/
|
||||
export const Instagram = () => {
|
||||
// Hooks
|
||||
const { account, setAccount } = useAccount()
|
||||
const backend = useBackend()
|
||||
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, 'Saving bio'])
|
||||
const [status, body] = await backend.updateAccount({ data: { githubUsername, githubEmail } })
|
||||
if (status === 200 && body.result === 'success') {
|
||||
setAccount(body.account)
|
||||
setLoadingStatus([true, 'GitHub info updated', true, true])
|
||||
} else setLoadingStatus([true, 'Something went wrong. Please report this', true, true])
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="w-full">
|
||||
<StringInput
|
||||
id="account-github-email"
|
||||
label="GitHub Email Address"
|
||||
current={githubEmail}
|
||||
update={setGithubEmail}
|
||||
valid={(val) => val.length > 0}
|
||||
placeholder={'joost@joost.at'}
|
||||
/>
|
||||
<StringInput
|
||||
id="account-github-username"
|
||||
label="GitHub Username"
|
||||
current={githubUsername}
|
||||
update={setGithubUsername}
|
||||
valid={(val) => val.length > 0}
|
||||
placeholder={'joostdecock'}
|
||||
/>
|
||||
<p className="text-right">
|
||||
<button className="daisy-btn daisy-btn-primary w-full lg:w-auto mt-8" onClick={save}>
|
||||
<SaveIcon /> Save
|
||||
</button>
|
||||
</p>
|
||||
</div>
|
||||
)
|
||||
}
|
75
packages/react/components/Account/Platform.mjs
Normal file
75
packages/react/components/Account/Platform.mjs
Normal file
|
@ -0,0 +1,75 @@
|
|||
// 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 { SaveIcon } from '@freesewing/react/components/Icon'
|
||||
import { StringInput } from '@freesewing/react/components/Input'
|
||||
|
||||
const labels = {
|
||||
instagram: 'Instagram',
|
||||
mastodon: 'Mastodon',
|
||||
reddit: 'Reddit',
|
||||
twitch: 'Twitch',
|
||||
tiktok: 'TikTok',
|
||||
website: 'Website',
|
||||
}
|
||||
|
||||
export const Instagram = () => <Platform platform="instagram" />
|
||||
export const Mastodon = () => <Platform platform="mastodon" />
|
||||
export const Reddit = () => <Platform platform="reddit" />
|
||||
export const Twitch = () => <Platform platform="twitch" />
|
||||
export const Tiktok = () => <Platform platform="tiktok" />
|
||||
export const Website = () => <Platform platform="website" />
|
||||
|
||||
/*
|
||||
* Component for the account/social/[platform] page
|
||||
*
|
||||
* @param {object} props - All React props
|
||||
* @param {string} platform - One of the keys in the labels object above
|
||||
*/
|
||||
const Platform = ({ platform = false }) => {
|
||||
// Hooks
|
||||
const { account, setAccount } = useAccount()
|
||||
const backend = useBackend()
|
||||
const { setLoadingStatus } = useContext(LoadingStatusContext)
|
||||
|
||||
// State
|
||||
const [platformId, setPlatformId] = useState(account.data[platform] || '')
|
||||
|
||||
if (!labels || !labels[platform]) return <p>Not a supported platform</p>
|
||||
|
||||
// Helper method to save changes
|
||||
const save = async () => {
|
||||
setLoadingStatus([true, 'Saving linked identity'])
|
||||
const data = { data: {} }
|
||||
data.data[platform] = platformId
|
||||
const [status, body] = await backend.updateAccount(data)
|
||||
if (status === 200 && body.result === 'success') {
|
||||
setAccount(body.account)
|
||||
setLoadingStatus([true, `Saved your ${labels[platform]} info`, true, true])
|
||||
} else setLoadingStatus([true, 'Something went wrong. Please report this', true, true])
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="w-full">
|
||||
<StringInput
|
||||
id={`account-${platform}`}
|
||||
label={platform === 'website' ? `Website URL` : `${labels[platform]} account`}
|
||||
current={platformId}
|
||||
update={setPlatformId}
|
||||
valid={(val) => val.length > 0}
|
||||
placeholder={'joostdecock'}
|
||||
/>
|
||||
<p className="text-right">
|
||||
<button className="daisy-btn daisy-btn-primary w-full lg:w-auto mt-8" onClick={save}>
|
||||
<SaveIcon /> Save
|
||||
</button>
|
||||
</p>
|
||||
</div>
|
||||
)
|
||||
}
|
|
@ -200,7 +200,7 @@ export const Set = ({ id, publicOnly = false, Link = false }) => {
|
|||
const [status, body] = await backend.createSet(data)
|
||||
if (status === 201 && body.result === 'created') {
|
||||
setLoadingStatus([true, 'Loading newly created set', true, true])
|
||||
window.location = `/account/set/?id=${body.set.id}`
|
||||
window.location = `/account/data/sets/set?id=${body.set.id}`
|
||||
} else setLoadingStatus([true, 'We failed to create this measurements set', true, false])
|
||||
}
|
||||
|
||||
|
|
|
@ -3,6 +3,7 @@ import { measurements } from '@freesewing/config'
|
|||
import { cloudflareImageUrl, capitalize } from '@freesewing/utils'
|
||||
// Context
|
||||
import { LoadingStatusContext } from '@freesewing/react/context/LoadingStatus'
|
||||
import { ModalContext } from '@freesewing/react/context/Modal'
|
||||
// Hooks
|
||||
import React, { useState, useEffect, Fragment, useContext } from 'react'
|
||||
import { useAccount } from '@freesewing/react/hooks/useAccount'
|
||||
|
@ -10,6 +11,8 @@ import { useBackend } from '@freesewing/react/hooks/useBackend'
|
|||
// Components
|
||||
import { Link as WebLink } from '@freesewing/react/components/Link'
|
||||
import { NoIcon, OkIcon, PlusIcon, TrashIcon, UploadIcon } from '@freesewing/react/components/Icon'
|
||||
import { ModalWrapper } from '@freesewing/react/components/Modal'
|
||||
import { NewSet } from './Set.mjs'
|
||||
|
||||
/*
|
||||
* The component for the an account/sets page
|
||||
|
@ -23,13 +26,16 @@ export const Sets = ({ Link = false }) => {
|
|||
// Hooks
|
||||
const { control } = useAccount()
|
||||
const backend = useBackend()
|
||||
const { setLoadingStatus, LoadingProgress } = useContext(LoadingStatusContext)
|
||||
|
||||
// State
|
||||
const [sets, setSets] = useState([])
|
||||
const [selected, setSelected] = useState({})
|
||||
const [refresh, setRefresh] = useState(0)
|
||||
|
||||
// Context
|
||||
const { setLoadingStatus, LoadingProgress } = useContext(LoadingStatusContext)
|
||||
const { setModal } = useContext(ModalContext)
|
||||
|
||||
// Effects
|
||||
useEffect(() => {
|
||||
const getSets = async () => {
|
||||
|
@ -90,15 +96,19 @@ export const Sets = ({ Link = false }) => {
|
|||
<UploadIcon />
|
||||
Import Measurements Sets
|
||||
</Link>
|
||||
<Link
|
||||
<button
|
||||
className="daisy-btn daisy-btn-primary capitalize w-full md:w-auto hover:no-underline hover:text-primary-content"
|
||||
bottom
|
||||
primary
|
||||
href="/new/set"
|
||||
onClick={() =>
|
||||
setModal(
|
||||
<ModalWrapper keepOpenOnClick>
|
||||
<NewSet />
|
||||
</ModalWrapper>
|
||||
)
|
||||
}
|
||||
>
|
||||
<PlusIcon />
|
||||
Create a new Measurements Set
|
||||
</Link>
|
||||
</button>
|
||||
</p>
|
||||
<div className="flex flex-row gap-2 border-b-2 mb-4 pb-4 mt-8 h-14 items-center">
|
||||
<input
|
||||
|
@ -145,7 +155,12 @@ export const Sets = ({ Link = false }) => {
|
|||
/>
|
||||
</label>
|
||||
<div className="w-full">
|
||||
<MsetCard control={control} href={`/account/set?id=${set.id}`} set={set} size="md" />
|
||||
<MsetCard
|
||||
control={control}
|
||||
href={`/account/data/sets/set?id=${set.id}`}
|
||||
set={set}
|
||||
size="md"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
|
|
117
packages/react/components/Account/Username.mjs
Normal file
117
packages/react/components/Account/Username.mjs
Normal file
|
@ -0,0 +1,117 @@
|
|||
// Dependencies
|
||||
import { welcomeSteps } from './shared.mjs'
|
||||
|
||||
// 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 { NoIcon, OkIcon, SaveIcon } from '@freesewing/react/components/Icon'
|
||||
import { StringInput } from '@freesewing/react/components/Input'
|
||||
|
||||
/*
|
||||
* Component for the account/username 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 Username = ({ welcome = false, Link = false }) => {
|
||||
if (!Link) Link = WebLink
|
||||
|
||||
// Hooks
|
||||
const { account, setAccount } = useAccount()
|
||||
const backend = useBackend()
|
||||
const { setLoadingStatus } = useContext(LoadingStatusContext)
|
||||
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)
|
||||
setAvailable(result.available ? true : false)
|
||||
}
|
||||
}
|
||||
|
||||
const save = async () => {
|
||||
setLoadingStatus([true, 'Saving username'])
|
||||
const [status, body] = await backend.updateAccount({ username })
|
||||
if (status === 200 && body.result === 'success') {
|
||||
setAccount(body.account)
|
||||
setLoadingStatus([true, 'Username updated', true, true])
|
||||
} else setLoadingStatus([true, 'Something went wrong. Please report this', true, true])
|
||||
}
|
||||
|
||||
const nextHref =
|
||||
welcomeSteps[account.control].length > 5
|
||||
? '/welcome/' + welcomeSteps[account.control][5]
|
||||
: '/docs/about/guide'
|
||||
|
||||
let btnClasses = 'daisy-btn mt-4 capitalize '
|
||||
if (welcome) btnClasses += 'w-64 daisy-btn-secondary'
|
||||
else btnClasses += 'w-full daisy-btn-primary'
|
||||
|
||||
return (
|
||||
<div className="w-full">
|
||||
<StringInput
|
||||
id="account-username"
|
||||
label="Username"
|
||||
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} /> Username is available
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<NoIcon className="w-4 h-4 text-error" stroke={3} /> This username is taken
|
||||
</>
|
||||
)}
|
||||
</span>
|
||||
}
|
||||
/>
|
||||
<p className="text-right">
|
||||
<button
|
||||
disabled={!available}
|
||||
className="daisy-btn daisy-btn-primary w-full lg:w-auto mt-8"
|
||||
onClick={save}
|
||||
>
|
||||
<SaveIcon /> Save Username
|
||||
</button>
|
||||
</p>
|
||||
|
||||
{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}
|
||||
</>
|
||||
) : null}
|
||||
</div>
|
||||
)
|
||||
}
|
|
@ -7,6 +7,14 @@ import { Sets, MsetCard } from './Sets.mjs'
|
|||
import { Patterns } from './Patterns.mjs'
|
||||
import { Pattern, PatternCard } from './Pattern.mjs'
|
||||
import { Apikeys } from './Apikeys.mjs'
|
||||
import { Username } from './Username.mjs'
|
||||
import { Bio } from './Bio.mjs'
|
||||
import { Avatar } from './Avatar.mjs'
|
||||
import { Email } from './Email.mjs'
|
||||
import { Github } from './Github.mjs'
|
||||
import { Instagram, Mastodon, Reddit, Twitch, Tiktok, Website } from './Platform.mjs'
|
||||
import { Compare } from './Compare.mjs'
|
||||
import { Control } from './Control.mjs'
|
||||
|
||||
export {
|
||||
Bookmarks,
|
||||
|
@ -20,4 +28,17 @@ export {
|
|||
Pattern,
|
||||
PatternCard,
|
||||
Apikeys,
|
||||
Username,
|
||||
Bio,
|
||||
Avatar,
|
||||
Email,
|
||||
Github,
|
||||
Instagram,
|
||||
Mastodon,
|
||||
Reddit,
|
||||
Twitch,
|
||||
Tiktok,
|
||||
Website,
|
||||
Compare,
|
||||
Control,
|
||||
}
|
||||
|
|
|
@ -12,6 +12,14 @@ export const DisplayRow = ({ title, children, keyWidth = 'w-24' }) => (
|
|||
</div>
|
||||
)
|
||||
|
||||
export const welcomeSteps = {
|
||||
1: [''],
|
||||
2: ['', 'newsletter', 'units'],
|
||||
3: ['', 'newsletter', 'units', 'compare', 'username'],
|
||||
4: ['', 'newsletter', 'units', 'compare', 'username', 'bio', 'img'],
|
||||
5: [''],
|
||||
}
|
||||
|
||||
/*
|
||||
import { Spinner } from 'shared/components/spinner.mjs'
|
||||
import Link from 'next/link'
|
||||
|
@ -132,12 +140,5 @@ const icons = {
|
|||
img: UserIcon,
|
||||
}
|
||||
|
||||
export const welcomeSteps = {
|
||||
1: [''],
|
||||
2: ['', 'newsletter', 'units'],
|
||||
3: ['', 'newsletter', 'units', 'compare', 'username'],
|
||||
4: ['', 'newsletter', 'units', 'compare', 'username', 'bio', 'img'],
|
||||
5: [''],
|
||||
}
|
||||
|
||||
*/
|
||||
|
|
|
@ -51,7 +51,7 @@ export const UsernameSettings = ({ welcome = false }) => {
|
|||
else btnClasses += 'w-full btn-primary'
|
||||
|
||||
return (
|
||||
<div className="max-w-xl">
|
||||
<div className="w-full">
|
||||
<StringInput
|
||||
id="account-username"
|
||||
label={t('usernameTitle')}
|
||||
|
|
|
@ -1,12 +1,11 @@
|
|||
import React from 'react'
|
||||
import { controlDesc } from '@freesewing/config'
|
||||
import { BulletIcon } from '@freesewing/react/components/Icon'
|
||||
|
||||
const scores = [1, 2, 3, 4, 5]
|
||||
|
||||
export const ControlScore = ({ control, color = 'base-content' }) =>
|
||||
control ? (
|
||||
<div className={`flex flex-row items-center text-${color}`}>
|
||||
{scores.map((score) => (
|
||||
{Object.keys(controlDesc).map((score) => (
|
||||
<BulletIcon fill={control >= score ? true : false} className="w-6 h-6 -ml-1" key={score} />
|
||||
))}
|
||||
</div>
|
||||
|
|
|
@ -259,8 +259,12 @@ export const EmailInput = ({
|
|||
placeholder={placeholder}
|
||||
value={current}
|
||||
onChange={(evt) => update(evt.target.value)}
|
||||
className={`input w-full input-bordered ${
|
||||
current === original ? 'input-secondary' : valid(current) ? 'input-success' : 'input-error'
|
||||
className={`daisy-input w-full daisy-input-bordered ${
|
||||
current === original
|
||||
? 'daisy-input-secondary'
|
||||
: valid(current)
|
||||
? 'daisy-input-success'
|
||||
: 'daisy-input-error'
|
||||
}`}
|
||||
/>
|
||||
</FormControl>
|
||||
|
@ -390,7 +394,7 @@ export const ImageInput = ({
|
|||
<input
|
||||
id={id}
|
||||
type="url"
|
||||
className="input input-secondary w-full input-bordered"
|
||||
className="daisy-input daisy-input-secondary w-full daisy-input-bordered"
|
||||
placeholder="Paste an image URL here"
|
||||
value={current}
|
||||
onChange={active ? (evt) => setUrl(evt.target.value) : (evt) => update(evt.target.value)}
|
||||
|
@ -475,7 +479,7 @@ export const MarkdownInput = ({
|
|||
</div>
|
||||
</Tab>
|
||||
<Tab key="preview">
|
||||
<div className="flex flex-row items-center">
|
||||
<div className="mdx markdown">
|
||||
<Markdown>{current}</Markdown>
|
||||
</div>
|
||||
</Tab>
|
||||
|
|
46
packages/react/hooks/useControl/index.mjs
Normal file
46
packages/react/hooks/useControl/index.mjs
Normal file
|
@ -0,0 +1,46 @@
|
|||
// 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'
|
||||
|
||||
/**
|
||||
* Control can be updated from many places in the UI.
|
||||
* So this shared state handler keeps this DRY
|
||||
*/
|
||||
export const useControl = () => {
|
||||
// Hooks
|
||||
const { account, setAccount, token } = useAccount()
|
||||
const backend = useBackend()
|
||||
const { setLoadingStatus } = useContext(LoadingStatusContext)
|
||||
|
||||
// State
|
||||
const [control, setControl] = useState(account.control)
|
||||
|
||||
// Method to update the control setting
|
||||
const updateControl = async (newControl) => {
|
||||
if (newControl !== control) {
|
||||
if (token) {
|
||||
setLoadingStatus([true, 'Updating preferences'])
|
||||
const [status, body] = await backend.updateAccount({ control: newControl })
|
||||
if (status === 200) {
|
||||
setControl(newControl)
|
||||
setAccount(body.account)
|
||||
setLoadingStatus([true, 'Preferences updated', true, true])
|
||||
} else
|
||||
setLoadingStatus([true, 'Failed to update preferences. Please report this', true, true])
|
||||
} else {
|
||||
/*
|
||||
* Control is used even when people are not logged in
|
||||
* So this ensures control is always available, even if people are not authenticated
|
||||
*/
|
||||
setAccount({ ...account, control: newControl })
|
||||
setControl(newControl)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { control, updateControl }
|
||||
}
|
|
@ -60,6 +60,7 @@
|
|||
"./context/Modal": "./context/Modal/index.mjs",
|
||||
"./hooks/useAccount": "./hooks/useAccount/index.mjs",
|
||||
"./hooks/useBackend": "./hooks/useBackend/index.mjs",
|
||||
"./hooks/useControl": "./hooks/useControl/index.mjs",
|
||||
"./hooks/useSelection": "./hooks/useSelection/index.mjs",
|
||||
"./lib/RestClient": "./lib/RestClient/index.mjs",
|
||||
"./lib/logoPath": "./components/Logo/path.mjs"
|
||||
|
@ -78,6 +79,7 @@
|
|||
"luxon": "^3.5.0",
|
||||
"nuqs": "^1.17.6",
|
||||
"react-markdown": "^9.0.1",
|
||||
"tlds": "^1.255.0",
|
||||
"use-local-storage-state": "19.1.0",
|
||||
"use-session-storage-state": "^19.0.0"
|
||||
},
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import tlds from 'tlds/index.json' with { type: 'json' }
|
||||
import { cloudflare as cloudflareConfig } from '@freesewing/config'
|
||||
|
||||
/*
|
||||
|
@ -343,3 +344,29 @@ export function timeAgo(timestamp, terse = true) {
|
|||
if (months < 25) return `${months}${terse ? 'M' : ' months'}${suffix}`
|
||||
return `${years}${terse ? 'Y' : ' years'}${suffix}`
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates an email address for correct syntax
|
||||
*
|
||||
* @param {string} email - The email input to check
|
||||
* @return {bool} valid - True if it's a valid email address
|
||||
*/
|
||||
export function validateEmail(email) {
|
||||
/* eslint-disable */
|
||||
const re =
|
||||
/^(([^<>()[\]\\.,;:\s@\"]+(\.[^<>()[\]\\.,;:\s@\"]+)*)|(\".+\"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/
|
||||
/* eslint-enable */
|
||||
return re.test(email)
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates the top level domain (TLT) for an email address
|
||||
*
|
||||
* @param {string} email - The email input to check
|
||||
* @return {bool} valid - True if it's a valid email address
|
||||
*/
|
||||
export function validateTld(email) {
|
||||
const tld = email.split('@').pop().split('.').pop().toLowerCase()
|
||||
if (tlds.indexOf(tld) === -1) return tld
|
||||
else return true
|
||||
}
|
||||
|
|
14
sites/org/docs/account/about/avatar/index.mdx
Normal file
14
sites/org/docs/account/about/avatar/index.mdx
Normal file
|
@ -0,0 +1,14 @@
|
|||
---
|
||||
title: Avatar
|
||||
---
|
||||
|
||||
import { DocusaurusDoc } from '@freesewing/react/components/Docusaurus'
|
||||
import { RoleBlock } from '@freesewing/react/components/Role'
|
||||
import { Avatar } from '@freesewing/react/components/Account'
|
||||
import Link from '@docusaurus/Link'
|
||||
|
||||
<DocusaurusDoc>
|
||||
<RoleBlock user>
|
||||
<Avatar Link={Link} />
|
||||
</RoleBlock>
|
||||
</DocusaurusDoc>
|
14
sites/org/docs/account/about/bio/index.mdx
Normal file
14
sites/org/docs/account/about/bio/index.mdx
Normal file
|
@ -0,0 +1,14 @@
|
|||
---
|
||||
title: Bio
|
||||
---
|
||||
|
||||
import { DocusaurusDoc } from '@freesewing/react/components/Docusaurus'
|
||||
import { RoleBlock } from '@freesewing/react/components/Role'
|
||||
import { Bio } from '@freesewing/react/components/Account'
|
||||
import Link from '@docusaurus/Link'
|
||||
|
||||
<DocusaurusDoc>
|
||||
<RoleBlock user>
|
||||
<Bio Link={Link} />
|
||||
</RoleBlock>
|
||||
</DocusaurusDoc>
|
14
sites/org/docs/account/about/email/index.mdx
Normal file
14
sites/org/docs/account/about/email/index.mdx
Normal file
|
@ -0,0 +1,14 @@
|
|||
---
|
||||
title: Email Address
|
||||
---
|
||||
|
||||
import { DocusaurusDoc } from '@freesewing/react/components/Docusaurus'
|
||||
import { RoleBlock } from '@freesewing/react/components/Role'
|
||||
import { Email } from '@freesewing/react/components/Account'
|
||||
import Link from '@docusaurus/Link'
|
||||
|
||||
<DocusaurusDoc>
|
||||
<RoleBlock user>
|
||||
<Email Link={Link} />
|
||||
</RoleBlock>
|
||||
</DocusaurusDoc>
|
32
sites/org/docs/account/about/index.mdx
Normal file
32
sites/org/docs/account/about/index.mdx
Normal file
|
@ -0,0 +1,32 @@
|
|||
---
|
||||
title: About you
|
||||
sidebar_position: 2
|
||||
---
|
||||
|
||||
import { DocusaurusDoc } from '@freesewing/react/components/Docusaurus'
|
||||
import { RoleBlock } from '@freesewing/react/components/Role'
|
||||
import { Avatar, Bio, Email, Username } from '@freesewing/react/components/Account'
|
||||
import Link from '@docusaurus/Link'
|
||||
|
||||
<DocusaurusDoc>
|
||||
<RoleBlock user>
|
||||
These fields are tied to your identity:
|
||||
|
||||
## Avatar
|
||||
|
||||
<Avatar />
|
||||
|
||||
## Bio
|
||||
|
||||
<Bio />
|
||||
|
||||
## Email Address
|
||||
|
||||
<Email />
|
||||
|
||||
## Username
|
||||
|
||||
<Username />
|
||||
|
||||
</RoleBlock>
|
||||
</DocusaurusDoc>
|
14
sites/org/docs/account/about/username/index.mdx
Normal file
14
sites/org/docs/account/about/username/index.mdx
Normal file
|
@ -0,0 +1,14 @@
|
|||
---
|
||||
title: Username
|
||||
---
|
||||
|
||||
import { DocusaurusDoc } from '@freesewing/react/components/Docusaurus'
|
||||
import { RoleBlock } from '@freesewing/react/components/Role'
|
||||
import { Username } from '@freesewing/react/components/Account'
|
||||
import Link from '@docusaurus/Link'
|
||||
|
||||
<DocusaurusDoc>
|
||||
<RoleBlock user>
|
||||
<Username Link={Link} />
|
||||
</RoleBlock>
|
||||
</DocusaurusDoc>
|
15
sites/org/docs/account/actions/import/index.mdx
Normal file
15
sites/org/docs/account/actions/import/index.mdx
Normal file
|
@ -0,0 +1,15 @@
|
|||
---
|
||||
title: Import data
|
||||
sidebar_position: 43
|
||||
---
|
||||
|
||||
import { DocusaurusDoc } from '@freesewing/react/components/Docusaurus'
|
||||
import { RoleBlock } from '@freesewing/react/components/Role'
|
||||
import { Bio } from '@freesewing/react/components/Account'
|
||||
import Link from '@docusaurus/Link'
|
||||
|
||||
<DocusaurusDoc>
|
||||
<RoleBlock user>
|
||||
<Bio Link={Link} />
|
||||
</RoleBlock>
|
||||
</DocusaurusDoc>
|
4
sites/org/docs/account/actions/index.mdx
Normal file
4
sites/org/docs/account/actions/index.mdx
Normal file
|
@ -0,0 +1,4 @@
|
|||
---
|
||||
title: Actions
|
||||
sidebar_position: 6
|
||||
---
|
15
sites/org/docs/account/actions/reload/index.mdx
Normal file
15
sites/org/docs/account/actions/reload/index.mdx
Normal file
|
@ -0,0 +1,15 @@
|
|||
---
|
||||
title: Reload account data
|
||||
sidebar_position: 43
|
||||
---
|
||||
|
||||
import { DocusaurusDoc } from '@freesewing/react/components/Docusaurus'
|
||||
import { RoleBlock } from '@freesewing/react/components/Role'
|
||||
import { Bio } from '@freesewing/react/components/Account'
|
||||
import Link from '@docusaurus/Link'
|
||||
|
||||
<DocusaurusDoc>
|
||||
<RoleBlock user>
|
||||
<Bio Link={Link} />
|
||||
</RoleBlock>
|
||||
</DocusaurusDoc>
|
15
sites/org/docs/account/actions/remove/index.mdx
Normal file
15
sites/org/docs/account/actions/remove/index.mdx
Normal file
|
@ -0,0 +1,15 @@
|
|||
---
|
||||
title: Remove your account
|
||||
sidebar_position: 43
|
||||
---
|
||||
|
||||
import { DocusaurusDoc } from '@freesewing/react/components/Docusaurus'
|
||||
import { RoleBlock } from '@freesewing/react/components/Role'
|
||||
import { Bio } from '@freesewing/react/components/Account'
|
||||
import Link from '@docusaurus/Link'
|
||||
|
||||
<DocusaurusDoc>
|
||||
<RoleBlock user>
|
||||
<Bio Link={Link} />
|
||||
</RoleBlock>
|
||||
</DocusaurusDoc>
|
15
sites/org/docs/account/actions/restrict/index.mdx
Normal file
15
sites/org/docs/account/actions/restrict/index.mdx
Normal file
|
@ -0,0 +1,15 @@
|
|||
---
|
||||
title: Restrict processing of your data
|
||||
sidebar_position: 43
|
||||
---
|
||||
|
||||
import { DocusaurusDoc } from '@freesewing/react/components/Docusaurus'
|
||||
import { RoleBlock } from '@freesewing/react/components/Role'
|
||||
import { Bio } from '@freesewing/react/components/Account'
|
||||
import Link from '@docusaurus/Link'
|
||||
|
||||
<DocusaurusDoc>
|
||||
<RoleBlock user>
|
||||
<Bio Link={Link} />
|
||||
</RoleBlock>
|
||||
</DocusaurusDoc>
|
28
sites/org/docs/account/data/index.mdx
Normal file
28
sites/org/docs/account/data/index.mdx
Normal file
|
@ -0,0 +1,28 @@
|
|||
---
|
||||
title: Your Data
|
||||
sidebar_position: 1
|
||||
---
|
||||
|
||||
import { DocusaurusDoc } from '@freesewing/react/components/Docusaurus'
|
||||
import { RoleBlock } from '@freesewing/react/components/Role'
|
||||
import { Bookmarks, Patterns, Sets } from '@freesewing/react/components/Account'
|
||||
import Link from '@docusaurus/Link'
|
||||
|
||||
<DocusaurusDoc>
|
||||
<RoleBlock user>
|
||||
This is the main data stored in your FreeSewing account:
|
||||
|
||||
## Your Bookmarks
|
||||
|
||||
<Bookmarks Link={Link} />
|
||||
|
||||
## Your Measurements Sets
|
||||
|
||||
<Sets Link={Link} />
|
||||
|
||||
## Your Patterns
|
||||
|
||||
<Patterns Link={Link} />
|
||||
|
||||
</RoleBlock>
|
||||
</DocusaurusDoc>
|
|
@ -1,7 +1,6 @@
|
|||
---
|
||||
title: Measurement Set
|
||||
sidebar_label: ' '
|
||||
sidebar_position: 99
|
||||
---
|
||||
|
||||
import { getSearchParam } from '@freesewing/utils'
|
13
sites/org/docs/account/preferences/compare/index.mdx
Normal file
13
sites/org/docs/account/preferences/compare/index.mdx
Normal file
|
@ -0,0 +1,13 @@
|
|||
---
|
||||
title: Measurements Sets Comparison
|
||||
---
|
||||
|
||||
import { DocusaurusDoc } from '@freesewing/react/components/Docusaurus'
|
||||
import { RoleBlock } from '@freesewing/react/components/Role'
|
||||
import { Compare } from '@freesewing/react/components/Account'
|
||||
|
||||
<DocusaurusDoc>
|
||||
<RoleBlock user>
|
||||
<Compare />
|
||||
</RoleBlock>
|
||||
</DocusaurusDoc>
|
14
sites/org/docs/account/preferences/consent/index.mdx
Normal file
14
sites/org/docs/account/preferences/consent/index.mdx
Normal file
|
@ -0,0 +1,14 @@
|
|||
---
|
||||
title: Consent & Privacy
|
||||
---
|
||||
|
||||
import { DocusaurusDoc } from '@freesewing/react/components/Docusaurus'
|
||||
import { RoleBlock } from '@freesewing/react/components/Role'
|
||||
import { Bio } from '@freesewing/react/components/Account'
|
||||
import Link from '@docusaurus/Link'
|
||||
|
||||
<DocusaurusDoc>
|
||||
<RoleBlock user>
|
||||
<Bio Link={Link} />
|
||||
</RoleBlock>
|
||||
</DocusaurusDoc>
|
13
sites/org/docs/account/preferences/control/index.mdx
Normal file
13
sites/org/docs/account/preferences/control/index.mdx
Normal file
|
@ -0,0 +1,13 @@
|
|||
---
|
||||
title: User Experience
|
||||
---
|
||||
|
||||
import { DocusaurusDoc } from '@freesewing/react/components/Docusaurus'
|
||||
import { RoleBlock } from '@freesewing/react/components/Role'
|
||||
import { Control } from '@freesewing/react/components/Account'
|
||||
|
||||
<DocusaurusDoc>
|
||||
<RoleBlock user>
|
||||
<Control />
|
||||
</RoleBlock>
|
||||
</DocusaurusDoc>
|
12
sites/org/docs/account/preferences/index.mdx
Normal file
12
sites/org/docs/account/preferences/index.mdx
Normal file
|
@ -0,0 +1,12 @@
|
|||
---
|
||||
title: Preferences
|
||||
sidebar_position: 3
|
||||
---
|
||||
|
||||
Here are the personal preferences stored in your account:
|
||||
|
||||
- [Measurements Sets Comparison](/account/preferences/compare)
|
||||
- [Consent & Privacy](/account/preferences/consent)
|
||||
- [User Experience](/account/preferences/control)
|
||||
- [Newsletter Subscription](/account/preferences/newsletter)
|
||||
- [Units](/account/preferences/units)
|
14
sites/org/docs/account/preferences/newsletter/index.mdx
Normal file
14
sites/org/docs/account/preferences/newsletter/index.mdx
Normal file
|
@ -0,0 +1,14 @@
|
|||
---
|
||||
title: Newsletter Subscription
|
||||
---
|
||||
|
||||
import { DocusaurusDoc } from '@freesewing/react/components/Docusaurus'
|
||||
import { RoleBlock } from '@freesewing/react/components/Role'
|
||||
import { Bio } from '@freesewing/react/components/Account'
|
||||
import Link from '@docusaurus/Link'
|
||||
|
||||
<DocusaurusDoc>
|
||||
<RoleBlock user>
|
||||
<Bio Link={Link} />
|
||||
</RoleBlock>
|
||||
</DocusaurusDoc>
|
14
sites/org/docs/account/preferences/units/index.mdx
Normal file
14
sites/org/docs/account/preferences/units/index.mdx
Normal file
|
@ -0,0 +1,14 @@
|
|||
---
|
||||
title: Units
|
||||
---
|
||||
|
||||
import { DocusaurusDoc } from '@freesewing/react/components/Docusaurus'
|
||||
import { RoleBlock } from '@freesewing/react/components/Role'
|
||||
import { Bio } from '@freesewing/react/components/Account'
|
||||
import Link from '@docusaurus/Link'
|
||||
|
||||
<DocusaurusDoc>
|
||||
<RoleBlock user>
|
||||
<Bio Link={Link} />
|
||||
</RoleBlock>
|
||||
</DocusaurusDoc>
|
|
@ -1,6 +1,5 @@
|
|||
---
|
||||
title: Your API Keys
|
||||
sidebar_position: 3
|
||||
---
|
||||
|
||||
import { DocusaurusDoc } from '@freesewing/react/components/Docusaurus'
|
4
sites/org/docs/account/security/index.mdx
Normal file
4
sites/org/docs/account/security/index.mdx
Normal file
|
@ -0,0 +1,4 @@
|
|||
---
|
||||
title: Security
|
||||
sidebar_position: 5
|
||||
---
|
14
sites/org/docs/account/security/mfa/index.mdx
Normal file
14
sites/org/docs/account/security/mfa/index.mdx
Normal file
|
@ -0,0 +1,14 @@
|
|||
---
|
||||
title: Multi-Factor Authentication
|
||||
---
|
||||
|
||||
import { DocusaurusDoc } from '@freesewing/react/components/Docusaurus'
|
||||
import { RoleBlock } from '@freesewing/react/components/Role'
|
||||
import { Bio } from '@freesewing/react/components/Account'
|
||||
import Link from '@docusaurus/Link'
|
||||
|
||||
<DocusaurusDoc>
|
||||
<RoleBlock user>
|
||||
<Bio Link={Link} />
|
||||
</RoleBlock>
|
||||
</DocusaurusDoc>
|
14
sites/org/docs/account/security/password/index.mdx
Normal file
14
sites/org/docs/account/security/password/index.mdx
Normal file
|
@ -0,0 +1,14 @@
|
|||
---
|
||||
title: Bio
|
||||
---
|
||||
|
||||
import { DocusaurusDoc } from '@freesewing/react/components/Docusaurus'
|
||||
import { RoleBlock } from '@freesewing/react/components/Role'
|
||||
import { Bio } from '@freesewing/react/components/Account'
|
||||
import Link from '@docusaurus/Link'
|
||||
|
||||
<DocusaurusDoc>
|
||||
<RoleBlock user>
|
||||
<Bio Link={Link} />
|
||||
</RoleBlock>
|
||||
</DocusaurusDoc>
|
14
sites/org/docs/account/social/github/index.mdx
Normal file
14
sites/org/docs/account/social/github/index.mdx
Normal file
|
@ -0,0 +1,14 @@
|
|||
---
|
||||
title: GitHub
|
||||
---
|
||||
|
||||
import { DocusaurusDoc } from '@freesewing/react/components/Docusaurus'
|
||||
import { RoleBlock } from '@freesewing/react/components/Role'
|
||||
import { Github } from '@freesewing/react/components/Account'
|
||||
import Link from '@docusaurus/Link'
|
||||
|
||||
<DocusaurusDoc>
|
||||
<RoleBlock user>
|
||||
<Github Link={Link} />
|
||||
</RoleBlock>
|
||||
</DocusaurusDoc>
|
53
sites/org/docs/account/social/index.mdx
Normal file
53
sites/org/docs/account/social/index.mdx
Normal file
|
@ -0,0 +1,53 @@
|
|||
---
|
||||
title: Linked Identities
|
||||
sidebar_position: 4
|
||||
---
|
||||
|
||||
import { DocusaurusDoc } from '@freesewing/react/components/Docusaurus'
|
||||
import { RoleBlock } from '@freesewing/react/components/Role'
|
||||
import {
|
||||
Github,
|
||||
Instagram,
|
||||
Mastodon,
|
||||
Reddit,
|
||||
Tiktok,
|
||||
Twitch,
|
||||
Website,
|
||||
} from '@freesewing/react/components/Account'
|
||||
import Link from '@docusaurus/Link'
|
||||
|
||||
<DocusaurusDoc>
|
||||
<RoleBlock user>
|
||||
|
||||
Listing your other online identities allows visitors to follow you elsewhere.
|
||||
|
||||
## GitHub
|
||||
|
||||
<Github />
|
||||
|
||||
## Instagram
|
||||
|
||||
<Instagram />
|
||||
|
||||
## Mastodon
|
||||
|
||||
<Mastodon />
|
||||
|
||||
## Reddit
|
||||
|
||||
<Reddit />
|
||||
|
||||
## TikTok
|
||||
|
||||
<Tiktok />
|
||||
|
||||
## Twitch
|
||||
|
||||
<Twitch />
|
||||
|
||||
## Website
|
||||
|
||||
<Website />
|
||||
|
||||
</RoleBlock>
|
||||
</DocusaurusDoc>
|
14
sites/org/docs/account/social/instagram/index.mdx
Normal file
14
sites/org/docs/account/social/instagram/index.mdx
Normal file
|
@ -0,0 +1,14 @@
|
|||
---
|
||||
title: Instagram
|
||||
---
|
||||
|
||||
import { DocusaurusDoc } from '@freesewing/react/components/Docusaurus'
|
||||
import { RoleBlock } from '@freesewing/react/components/Role'
|
||||
import { Instagram } from '@freesewing/react/components/Account'
|
||||
import Link from '@docusaurus/Link'
|
||||
|
||||
<DocusaurusDoc>
|
||||
<RoleBlock user>
|
||||
<Instagram />
|
||||
</RoleBlock>
|
||||
</DocusaurusDoc>
|
14
sites/org/docs/account/social/mastodon/index.mdx
Normal file
14
sites/org/docs/account/social/mastodon/index.mdx
Normal file
|
@ -0,0 +1,14 @@
|
|||
---
|
||||
title: Mastodon
|
||||
---
|
||||
|
||||
import { DocusaurusDoc } from '@freesewing/react/components/Docusaurus'
|
||||
import { RoleBlock } from '@freesewing/react/components/Role'
|
||||
import { Mastodon } from '@freesewing/react/components/Account'
|
||||
import Link from '@docusaurus/Link'
|
||||
|
||||
<DocusaurusDoc>
|
||||
<RoleBlock user>
|
||||
<Mastodon />
|
||||
</RoleBlock>
|
||||
</DocusaurusDoc>
|
14
sites/org/docs/account/social/reddit/index.mdx
Normal file
14
sites/org/docs/account/social/reddit/index.mdx
Normal file
|
@ -0,0 +1,14 @@
|
|||
---
|
||||
title: Reddit
|
||||
---
|
||||
|
||||
import { DocusaurusDoc } from '@freesewing/react/components/Docusaurus'
|
||||
import { RoleBlock } from '@freesewing/react/components/Role'
|
||||
import { Reddit } from '@freesewing/react/components/Account'
|
||||
import Link from '@docusaurus/Link'
|
||||
|
||||
<DocusaurusDoc>
|
||||
<RoleBlock user>
|
||||
<Reddit />
|
||||
</RoleBlock>
|
||||
</DocusaurusDoc>
|
14
sites/org/docs/account/social/tiktok/index.mdx
Normal file
14
sites/org/docs/account/social/tiktok/index.mdx
Normal file
|
@ -0,0 +1,14 @@
|
|||
---
|
||||
title: TikTok
|
||||
---
|
||||
|
||||
import { DocusaurusDoc } from '@freesewing/react/components/Docusaurus'
|
||||
import { RoleBlock } from '@freesewing/react/components/Role'
|
||||
import { Tiktok } from '@freesewing/react/components/Account'
|
||||
import Link from '@docusaurus/Link'
|
||||
|
||||
<DocusaurusDoc>
|
||||
<RoleBlock user>
|
||||
<Tiktok />
|
||||
</RoleBlock>
|
||||
</DocusaurusDoc>
|
14
sites/org/docs/account/social/twitch/index.mdx
Normal file
14
sites/org/docs/account/social/twitch/index.mdx
Normal file
|
@ -0,0 +1,14 @@
|
|||
---
|
||||
title: Twitch
|
||||
---
|
||||
|
||||
import { DocusaurusDoc } from '@freesewing/react/components/Docusaurus'
|
||||
import { RoleBlock } from '@freesewing/react/components/Role'
|
||||
import { Twitch } from '@freesewing/react/components/Account'
|
||||
import Link from '@docusaurus/Link'
|
||||
|
||||
<DocusaurusDoc>
|
||||
<RoleBlock user>
|
||||
<Twitch />
|
||||
</RoleBlock>
|
||||
</DocusaurusDoc>
|
14
sites/org/docs/account/social/website/index.mdx
Normal file
14
sites/org/docs/account/social/website/index.mdx
Normal file
|
@ -0,0 +1,14 @@
|
|||
---
|
||||
title: Website
|
||||
---
|
||||
|
||||
import { DocusaurusDoc } from '@freesewing/react/components/Docusaurus'
|
||||
import { RoleBlock } from '@freesewing/react/components/Role'
|
||||
import { Website } from '@freesewing/react/components/Account'
|
||||
import Link from '@docusaurus/Link'
|
||||
|
||||
<DocusaurusDoc>
|
||||
<RoleBlock user>
|
||||
<Website />
|
||||
</RoleBlock>
|
||||
</DocusaurusDoc>
|
Loading…
Add table
Add a link
Reference in a new issue