1
0
Fork 0

wip: More account pages

This commit is contained in:
joostdecock 2024-12-24 11:13:02 +01:00
parent c994e3898f
commit 373e5d078d
15 changed files with 531 additions and 59 deletions

View file

@ -1 +1 @@
yarn lint-staged
npx lint-staged

View file

@ -28,17 +28,17 @@ const account = {
patterns: 1,
},
info: {
username: 2,
bio: 2,
img: 2,
bio: 2,
email: 3,
username: 2,
},
settings: {
units: 2,
newsletter: 2,
compare: 3,
control: 1,
consent: 2,
compare: 3,
newsletter: 2,
units: 2,
control: 1,
},
security: {
password: 2,

View file

@ -36,8 +36,6 @@ const strings = {
* @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()

View file

@ -0,0 +1,195 @@
// Dependencies
import { welcomeSteps } from './shared.mjs'
import { linkClasses } 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 { NoIcon, OkIcon, SaveIcon } from '@freesewing/react/components/Icon'
import { ListInput } from '@freesewing/react/components/Input'
import { Popout } from '@freesewing/react/components/Popout'
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/newsletter page
*
* @params {object} props - All React props
* @params {bool} props.welcome - Set to true to use this component on the welcome page
* @param {function} props.Link - An optional framework-specific Link component
*/
export const Consent = ({ welcome = false, Link = false, title = false }) => {
if (!Link) Link = WebLink
// Hooks
const backend = useBackend()
const { account, setAccount, setToken } = useAccount()
const { setLoadingStatus } = useContext(LoadingStatusContext)
// State
const [consent1, setConsent1] = useState(account?.consent > 0)
const [consent2, setConsent2] = useState(account?.consent > 1)
// Helper method to update the account
const update = async () => {
let newConsent = 0
if (consent1) newConsent = 1
if (consent1 && consent2) newConsent = 2
if (newConsent !== account.consent) {
setLoadingStatus([true, 'Updating your account'])
const [status, body] = await backend.updateAccount({ consent: newConsent })
if (status === 200) {
setLoadingStatus([true, 'Account updated', true, true])
setAccount(body.account)
} else setLoadingStatus([true, 'An error occured, please report this', true, true])
}
}
// Helper method to remove the account
const removeAccount = async () => {
setLoadingStatus([true, 'One moment please'])
const [status, body] = await backend.removeAccount()
if (status === 200) {
setLoadingStatus([true, 'All done, farewell', true, true])
setToken(null)
setAccount({ username: false })
} else setLoadingStatus([true, 'Something went wrong. Please report this.', true, true])
}
return (
<div className="w-full mdx">
{title ? <h2 className="text-4xl">Privacy Matters</h2> : null}
{text.intro}
<h5 className="mt-8">Do you give your consent to process your account data?</h5>
{text.account}
{consent1 ? (
<Checkbox value={consent1} setter={setConsent1} label="Yes, I do" />
) : (
<button
className="daisy-btn daisy-btn-primary daisy-btn-lg w-full mt-4"
onClick={() => setConsent1(!consent1)}
>
Click here to give your consent
</button>
)}
{consent1 ? (
<>
<h5 className="mt-8">Do you give your consent to share your anonymized measurements</h5>
<Checkbox
value={consent2}
setter={setConsent2}
label="Share anonymized measurements as open data"
/>
{!consent2 ? <Popout note>{text.opendata}</Popout> : null}
</>
) : null}
{!consent1 && <Popout warning>This consent is required for a FreeSewing account.</Popout>}
{consent1 ? (
<button className="daisy-btn daisy-btn-primary w-full mt-4" onClick={update}>
Save
</button>
) : (
<button
className="daisy-btn mt-4 capitalize w-full daisy-btn-error"
onClick={removeAccount}
>
Remove your account
</button>
)}
<p className="text-center opacity-50 mt-12">
<Link href="/docs/about/privacy" className="hover:text-secondary underline">
FreeSewing Privacy Notice
</Link>
</p>
</div>
)
}
const Checkbox = ({ value, setter, label, children = null }) => (
<div
className={`form-control p-4 hover:cursor-pointer rounded border-l-8 my-2
${value ? 'border-success bg-success' : 'border-error bg-error'}
bg-opacity-10 shadow`}
onClick={() => setter(value ? false : true)}
>
<div className="form-control flex flex-row items-center gap-2">
<input
type="checkbox"
className="daisy-checkbox"
checked={value ? 'checked' : ''}
onChange={() => setter(value ? false : true)}
/>
<span className="label-text">{label}</span>
</div>
{children}
</div>
)
const text = {
intro: (
<>
<p>
FreeSewing respects your privacy and your rights. We adhere to the toughest privacy and
security law in the world: the{' '}
<a href="https://en.wikipedia.org/wiki/General_Data_Protection_Regulation">
General Data Protection Regulation
</a>{' '}
(GDPR) of the European Union (EU).
</p>
<p>
Under the GDPR, processing of your personal data requires granular consent in other words,{' '}
<b>we need your permission for the various ways we handle your data</b>.
</p>
</>
),
account: (
<div className="border-l-4 ml-1 pl-4 my-2 opacity-80">
<h6>What is account data?</h6>
<p>
Your <b>email address</b>, <b>username</b>, and <b>password</b>, and any <b>measurements</b>{' '}
you add to your account.
</p>
<h6>Why do we need it?</h6>
<p>
To <b>authenticate</b> you, <b>contact</b> you when needed, and generate <b>bespoke</b>{' '}
sewing patterns.
</p>
<h6>How long do we keep it?</h6>
<p>
<b>12 months</b> after the last time your connected to our backend, or until you{' '}
<b>remove</b> your account or <b>revoke</b> this consent.
</p>
<h6>Do we share it with others?</h6>
<p>
<b>No</b>, never.
</p>
<p className="text-sm italic">
Note: Freesewing publishes anonymized measurements as open data for scientific research. You
have the right to object to this.
</p>
</div>
),
opendata: `This data is used to study and understand the human form in all its shapes,
so we can get better sewing patterns, and better fitting garments.
Even though this data is anonymized, you have the right to object to this.`,
}

View file

@ -74,12 +74,12 @@ const itemIcons = {
const btnClasses = 'daisy-btn capitalize flex flex-row justify-between'
const itemClasses = 'flex flex-row items-center justify-between bg-opacity-10 p-2 px-4 rounded mb-1'
const linkClasses = `hover:bg-secondary hover:bg-opacity-10 max-w-md hover:no-underline`
const titles = {
apikeys: 'Your API Keys',
bookmarks: 'Your Bookmarks',
sets: 'Your Measurements Sets',
patterns: 'Your Patterns',
apikeys: 'API Keys',
sets: 'Measurements Sets',
patterns: 'Patterns',
img: 'Avatar',
email: 'E-mail Address',
newsletter: 'Newsletter Subscription',
@ -90,22 +90,12 @@ const titles = {
mfa: 'Multi-Factor Authentication',
}
const AccountLink = ({ item, children, Link }) => (
<Link
className={`${itemClasses} hover:bg-secondary hover:bg-opacity-10 max-w-md`}
href={`/account/${item}/`}
title={titles[item] ? titles.item : capitalize(item)}
>
{children}
</Link>
)
const YesNo = ({ check }) => (check ? <BoolYesIcon /> : <BoolNoIcon />)
const t = (input) => input
/**
* The AccountLinks component shows all of the links to manage your account
* The Links component shows all of the links to manage your account
*
* @param {object} props - All the React props
* @param {function} Link - A custom Link component, typically the Docusaurus one, but it's optional
@ -177,13 +167,18 @@ export const Links = ({ Link = false }) => {
<h4 className="my-2">Your Data</h4>
{Object.keys(controlConfig.account.fields.data).map((item) =>
controlConfig.flat[item] > control ? null : (
<AccountLink {...{ item, Link }} key={item}>
<Link
key={item}
title={titles[item]}
href={`/account/data/${item}/`}
className={`${itemClasses} ${linkClasses}`}
>
<div className="flex flex-row items-center gap-3 font-medium">
{itemIcons[item]}
{titles[item] ? titles[item] : capitalize(item)}
</div>
<div className="">{itemPreviews[item]}</div>
</AccountLink>
</Link>
)
)}
</div>
@ -193,13 +188,18 @@ export const Links = ({ Link = false }) => {
<h4 className="my-2">About You</h4>
{Object.keys(controlConfig.account.fields.info).map((item) =>
controlConfig.flat[item] > control ? null : (
<AccountLink {...{ item, Link }} key={item}>
<Link
key={item}
title={titles[item] || capitalize(item)}
href={`/account/about/${item}/`}
className={`${itemClasses} ${linkClasses}`}
>
<div className="flex flex-row items-center gap-3 font-medium">
{itemIcons[item]}
{titles[item] ? titles[item] : capitalize(item)}
</div>
<div className="">{itemPreviews[item]}</div>
</AccountLink>
</Link>
)
)}
<div className={`${itemClasses} opacity-60 max-w-md`}>
@ -223,13 +223,18 @@ export const Links = ({ Link = false }) => {
<h4 className="my-2">Preferences</h4>
{Object.keys(controlConfig.account.fields.settings).map((item) =>
controlConfig.flat[item] > control ? null : (
<AccountLink {...{ item, Link }} key={item}>
<Link
key={item}
title={titles[item] || capitalize(item)}
href={`/account/preferences/${item}/`}
className={`${itemClasses} ${linkClasses}`}
>
<div className="flex flex-row items-center gap-3 font-medium">
{itemIcons[item]}
{titles[item] ? titles[item] : capitalize(item)}
</div>
<div className="">{itemPreviews[item]}</div>
</AccountLink>
</Link>
)
)}
</div>
@ -239,13 +244,18 @@ export const Links = ({ Link = false }) => {
<h4 className="my-2">Linked Identities</h4>
{Object.keys(controlConfig.account.fields.identities).map((item) =>
controlConfig.flat[item] > control ? null : (
<AccountLink {...{ item, Link }} key={item}>
<Link
key={item}
title={titles[item] || capitalize(item)}
href={`/account/social/${item}/`}
className={`${itemClasses} ${linkClasses}`}
>
<div className="flex flex-row items-center gap-3 font-medium">
{itemIcons[item]}
{titles[item] ? titles[item] : capitalize(item)}
</div>
<div className="">{itemPreviews[item]}</div>
</AccountLink>
</Link>
)
)}
</div>
@ -256,13 +266,18 @@ export const Links = ({ Link = false }) => {
<h4 className="my-2">Security</h4>
{Object.keys(controlConfig.account.fields.security).map((item) =>
controlConfig.flat[item] > control ? null : (
<AccountLink {...{ item, Link }} key={item}>
<Link
key={item}
title={titles[item] || capitalize(item)}
href={`/account/security/${item}/`}
className={`${itemClasses} ${linkClasses}`}
>
<div className="flex flex-row items-center gap-3 font-medium">
{itemIcons[item]}
{titles[item] ? titles[item] : capitalize(item)}
</div>
<div className="">{itemPreviews[item]}</div>
</AccountLink>
</Link>
)
)}
</div>
@ -272,33 +287,53 @@ export const Links = ({ Link = false }) => {
<div className="">
<h4 className="my-2">Actions</h4>
{control > 2 && (
<AccountLink item="import" Link={Link}>
<Link
className={`${itemClasses} ${linkClasses}`}
title="Import data"
href="/account/actions/import/"
>
<UploadIcon />
Import data
</AccountLink>
<span className="font-medium">Import data</span>
</Link>
)}
{control > 2 && (
<AccountLink item="export" Link={Link}>
<Link
className={`${itemClasses} ${linkClasses}`}
title="Export your data"
href="/account/actions/export/"
>
<DownloadIcon />
Export your data
</AccountLink>
<span className="font-medium">Export your data</span>
</Link>
)}
{control > 2 && (
<AccountLink item="reload" Link={Link}>
<Link
className={`${itemClasses} ${linkClasses}`}
title="Reload account data"
href="/account/actions/reload/"
>
<ReloadIcon />
Reload account data
</AccountLink>
<span className="font-medium">Reload account data</span>
</Link>
)}
{control > 3 && (
<AccountLink item="restrict" Link={Link}>
<Link
className={`${itemClasses} ${linkClasses}`}
title="Restrict processing of your data"
href="/account/actions/restrict/"
>
<CloseIcon className="w-6 h-6 text-warning" stroke={3} />
Restrict processing of your data
</AccountLink>
<span className="font-medium">Restrict processing of your data</span>
</Link>
)}
<AccountLink item="remove" Link={Link}>
<Link
className={`${itemClasses} ${linkClasses}`}
title="Remove your account"
href="/account/actions/remove/"
>
<TrashIcon className="w-6 h-6 text-warning" />
Remove your account
</AccountLink>
<span className="font-medium">Remove your account</span>
</Link>
</div>
)}
</div>

View file

@ -0,0 +1,139 @@
// Dependencies
import { welcomeSteps } from './shared.mjs'
import { linkClasses } 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 { NoIcon, OkIcon, SaveIcon } from '@freesewing/react/components/Icon'
import { ListInput } from '@freesewing/react/components/Input'
import { Popout } from '@freesewing/react/components/Popout'
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/newsletter page
*
* @params {object} props - All React props
* @params {bool} props.welcome - Set to true to use this component on the welcome page
* @param {function} props.Link - An optional framework-specific Link component
*/
export const Newsletter = ({ welcome = false, Link = false }) => {
if (!Link) Link = WebLink
// Hooks
const { account, setAccount } = useAccount()
const backend = useBackend()
const { setLoadingStatus } = useContext(LoadingStatusContext)
// State
const [selection, setSelection] = useState(account?.newsletter ? 'yes' : 'no')
// Helper method to update account
const update = async (val) => {
if (val !== selection) {
setLoadingStatus([true, 'Saving preferences'])
const [status, body] = await backend.updateAccount({
newsletter: 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])
}
}
// Next step for onboarding
const nextHref =
welcomeSteps[account?.control].length > 2
? '/welcome/' + welcomeSteps[account?.control][2]
: '/docs/about/guide'
return (
<div className="w-full">
<ListInput
id="account-newsletter"
label="Would you like to receive the FreeSewing newsletter?"
list={['yes', 'no'].map((val) => ({
val,
label: (
<div className="flex flex-row items-center w-full justify-between">
<span>
{val === 'yes' ? 'Yes, I would like to receive the newsletter' : 'No thanks'}
</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:
val === 'yes'
? `Once every 3 months you'll receive an email from us with honest wholesome content. No tracking, no ads, no nonsense.`
: `You can always change your mind later. But until you do, we will not send you any newsletters.`,
}))}
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={200 / welcomeSteps[account?.control].length}
max="100"
></progress>
<span className="pt-4 text-sm font-bold opacity-50">
2 / {welcomeSteps[account?.control].length}
</span>
<Icons
done={welcomeSteps[account?.control].slice(0, 1)}
todo={welcomeSteps[account?.control].slice(2)}
current="newsletter"
/>
</>
) : null}
</>
) : null}
<Popout tip>
<h5>You can unsubscribe at any time with the link below</h5>
<p>
This unsubscribe link will also be included at the bottom of every newsletter we send you,
so you do not need to bookmark it, but you can if you want to.
</p>
<p>
<Link href={`/newsletter/unsubscribe?x=${account?.ehash}`} className={linkClasses}>
Unsubscribe link
</Link>
</p>
<p className="text-sm">
This link is to unsubscribe you specifically, do not share it with other subscribers.
</p>
</Popout>
</div>
)
}

View file

@ -112,7 +112,7 @@ export const Pattern = ({ id, Link }) => {
const [status, body] = await backend.createPattern(data)
if (status === 201 && body.result === 'created') {
setLoadingStatus([true, 'Loading newly created pattern', true, true])
window.location = `/account/pattern/?id=${body.pattern.id}`
window.location = `/account/data/patterns/pattern?id=${body.pattern.id}`
} else setLoadingStatus([true, 'We failed to create this pattern', true, false])
}

View file

@ -200,7 +200,7 @@ export const Patterns = ({ Link = false }) => {
<td className="text-base font-medium">{pattern.id}</td>
<td className="text-base font-medium">
<PatternCard
href={`/account/pattern?id=${pattern.id}`}
href={`/account/data/patterns/pattern?id=${pattern.id}`}
pattern={pattern}
size="xs"
Link={Link}
@ -208,7 +208,7 @@ export const Patterns = ({ Link = false }) => {
</td>
<td className="text-base font-medium">
<Link
href={`/account/pattern?id=${pattern.id}`}
href={`/account/data/patterns/pattern?id=${pattern.id}`}
className="text-secondary underline decoration-2 hover:decoration-4"
>
{pattern.name}

View file

@ -748,7 +748,7 @@ export const NewSet = () => {
const [status, body] = await backend.createSet({ name, imperial })
if (status === 201 && body.result === 'created') {
setLoadingStatus([true, 'Nailed it', true, true])
window.location = `/account/set?id=${body.set.id}`
window.location = `/account/data/sets/set?id=${body.set.id}`
} else
setLoadingStatus([
true,

View file

@ -0,0 +1,99 @@
// 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 { ListInput } from '@freesewing/react/components/Input'
import { NumberCircle } from '@freesewing/react/components/Number'
/*
* Component for the account/preferences/units page
*
* @params {object} props - All React props
* @params {bool} props.welcome - Set to true to use this component on the welcome page
*/
export const Units = ({ welcome = false }) => {
// Hooks
const { account, setAccount } = useAccount()
const { setLoadingStatus } = useContext(LoadingStatusContext)
const backend = useBackend()
// State
const [selection, setSelection] = useState(account?.imperial === true ? 'imperial' : 'metric')
// Helper method to update account
const update = async (val) => {
if (val !== selection) {
setLoadingStatus([true, 'Saving units'])
const [status, body] = await backend.updateAccount({
imperial: val === 'imperial' ? true : false,
})
if (status === 200 && body.result === 'success') {
setAccount(body.account)
setSelection(body.account.imperial ? 'imperial' : 'metric')
setLoadingStatus([true, 'Units 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 > 3
? '/welcome/' + welcomeSteps[account?.control][3]
: '/docs/about/guide'
return (
<div className="w-full">
<ListInput
id="account-units"
label="Units"
list={['metric', 'imperial'].map((val) => ({
val,
label: (
<div className="flex flex-row items-center w-full justify-between">
<span>{val === 'metric' ? 'Metric units (cm)' : 'Imperial units (inch)'}</span>
<NumberCircle nr={val === 'imperial' ? '″' : 'cm'} color="secondary" />
</div>
),
desc:
val === 'metric'
? 'Pick this if you prefere centimeters over inches'
: 'Pick this if you prefer inches over centimeters',
}))}
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={300 / welcomeSteps[account?.control].length}
max="100"
></progress>
<span className="pt-4 text-sm font-bold opacity-50">
3 / {welcomeSteps[account?.control].length}
</span>
<Icons
done={welcomeSteps[account?.control].slice(0, 2)}
todo={welcomeSteps[account?.control].slice(3)}
current="units"
/>
</>
) : null}
</>
) : null}
</div>
)
}

View file

@ -15,6 +15,9 @@ 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'
import { Units } from './Units.mjs'
import { Newsletter } from './Newsletter.mjs'
import { Consent } from './Consent.mjs'
export {
Bookmarks,
@ -41,4 +44,7 @@ export {
Website,
Compare,
Control,
Units,
Newsletter,
Consent,
}

View file

@ -4,11 +4,11 @@ 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 { Consent } from '@freesewing/react/components/Account'
import Link from '@docusaurus/Link'
<DocusaurusDoc>
<RoleBlock user>
<Bio Link={Link} />
<Consent Link={Link} />
</RoleBlock>
</DocusaurusDoc>

View file

@ -4,11 +4,11 @@ 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 { Newsletter } from '@freesewing/react/components/Account'
import Link from '@docusaurus/Link'
<DocusaurusDoc>
<RoleBlock user>
<Bio Link={Link} />
<Newsletter Link={Link} />
</RoleBlock>
</DocusaurusDoc>

View file

@ -4,11 +4,11 @@ title: Units
import { DocusaurusDoc } from '@freesewing/react/components/Docusaurus'
import { RoleBlock } from '@freesewing/react/components/Role'
import { Bio } from '@freesewing/react/components/Account'
import { Units } from '@freesewing/react/components/Account'
import Link from '@docusaurus/Link'
<DocusaurusDoc>
<RoleBlock user>
<Bio Link={Link} />
<Units />
</RoleBlock>
</DocusaurusDoc>