1
0
Fork 0

wip: More account pages

This commit is contained in:
joostdecock 2024-12-23 18:25:48 +01:00
parent 543be68c1f
commit c994e3898f
58 changed files with 1419 additions and 29 deletions

View file

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

View file

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

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

View file

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

View file

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

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

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

View file

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

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

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

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

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

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

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

View file

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

View file

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

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

View file

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

View file

@ -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: [''],
}
*/

View file

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

View file

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

View file

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

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

View file

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

View file

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

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

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

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

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

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

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

View file

@ -0,0 +1,4 @@
---
title: Actions
sidebar_position: 6
---

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

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

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

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

View file

@ -1,7 +1,6 @@
---
title: Measurement Set
sidebar_label: ' '
sidebar_position: 99
---
import { getSearchParam } from '@freesewing/utils'

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

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

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

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

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

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

View file

@ -1,6 +1,5 @@
---
title: Your API Keys
sidebar_position: 3
---
import { DocusaurusDoc } from '@freesewing/react/components/Docusaurus'

View file

@ -0,0 +1,4 @@
---
title: Security
sidebar_position: 5
---

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

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

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

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

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

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

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

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

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

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