diff --git a/.husky/pre-commit b/.husky/pre-commit index 37236231717..2312dc587f6 100755 --- a/.husky/pre-commit +++ b/.husky/pre-commit @@ -1 +1 @@ -yarn lint-staged +npx lint-staged diff --git a/packages/config/src/control.mjs b/packages/config/src/control.mjs index 1898719b7d9..7dd9ca90ed4 100644 --- a/packages/config/src/control.mjs +++ b/packages/config/src/control.mjs @@ -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, diff --git a/packages/react/components/Account/Compare.mjs b/packages/react/components/Account/Compare.mjs index 1bbdaf58a74..131a171e382 100644 --- a/packages/react/components/Account/Compare.mjs +++ b/packages/react/components/Account/Compare.mjs @@ -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() diff --git a/packages/react/components/Account/Consent.mjs b/packages/react/components/Account/Consent.mjs new file mode 100644 index 00000000000..c2ef0875e32 --- /dev/null +++ b/packages/react/components/Account/Consent.mjs @@ -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 ( +
+ {title ?

Privacy Matters

: null} + {text.intro} +
Do you give your consent to process your account data?
+ {text.account} + {consent1 ? ( + + ) : ( + + )} + {consent1 ? ( + <> +
Do you give your consent to share your anonymized measurements
+ + {!consent2 ? {text.opendata} : null} + + ) : null} + {!consent1 && This consent is required for a FreeSewing account.} + {consent1 ? ( + + ) : ( + + )} +

+ + FreeSewing Privacy Notice + +

+
+ ) +} + +const Checkbox = ({ value, setter, label, children = null }) => ( +
setter(value ? false : true)} + > +
+ setter(value ? false : true)} + /> + {label} +
+ {children} +
+) + +const text = { + intro: ( + <> +

+ FreeSewing respects your privacy and your rights. We adhere to the toughest privacy and + security law in the world: the{' '} + + General Data Protection Regulation + {' '} + (GDPR) of the European Union (EU). +

+

+ Under the GDPR, processing of your personal data requires granular consent — in other words,{' '} + we need your permission for the various ways we handle your data. +

+ + ), + account: ( +
+
What is account data?
+

+ Your email address, username, and password, and any measurements{' '} + you add to your account. +

+
Why do we need it?
+

+ To authenticate you, contact you when needed, and generate bespoke{' '} + sewing patterns. +

+
How long do we keep it?
+

+ 12 months after the last time your connected to our backend, or until you{' '} + remove your account or revoke this consent. +

+
Do we share it with others?
+

+ No, never. +

+

+ Note: Freesewing publishes anonymized measurements as open data for scientific research. You + have the right to object to this. +

+
+ ), + 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.`, +} diff --git a/packages/react/components/Account/Links.mjs b/packages/react/components/Account/Links.mjs index 26e1ccfb940..7faca7858fa 100644 --- a/packages/react/components/Account/Links.mjs +++ b/packages/react/components/Account/Links.mjs @@ -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 }) => ( - - {children} - -) - const YesNo = ({ check }) => (check ? : ) 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 }) => {

Your Data

{Object.keys(controlConfig.account.fields.data).map((item) => controlConfig.flat[item] > control ? null : ( - +
{itemIcons[item]} {titles[item] ? titles[item] : capitalize(item)}
{itemPreviews[item]}
-
+ ) )} @@ -193,13 +188,18 @@ export const Links = ({ Link = false }) => {

About You

{Object.keys(controlConfig.account.fields.info).map((item) => controlConfig.flat[item] > control ? null : ( - +
{itemIcons[item]} {titles[item] ? titles[item] : capitalize(item)}
{itemPreviews[item]}
-
+ ) )}
@@ -223,13 +223,18 @@ export const Links = ({ Link = false }) => {

Preferences

{Object.keys(controlConfig.account.fields.settings).map((item) => controlConfig.flat[item] > control ? null : ( - +
{itemIcons[item]} {titles[item] ? titles[item] : capitalize(item)}
{itemPreviews[item]}
-
+ ) )}
@@ -239,13 +244,18 @@ export const Links = ({ Link = false }) => {

Linked Identities

{Object.keys(controlConfig.account.fields.identities).map((item) => controlConfig.flat[item] > control ? null : ( - +
{itemIcons[item]} {titles[item] ? titles[item] : capitalize(item)}
{itemPreviews[item]}
-
+ ) )} @@ -256,13 +266,18 @@ export const Links = ({ Link = false }) => {

Security

{Object.keys(controlConfig.account.fields.security).map((item) => controlConfig.flat[item] > control ? null : ( - +
{itemIcons[item]} {titles[item] ? titles[item] : capitalize(item)}
{itemPreviews[item]}
-
+ ) )} @@ -272,33 +287,53 @@ export const Links = ({ Link = false }) => {

Actions

{control > 2 && ( - + - Import data - + Import data + )} {control > 2 && ( - + - Export your data - + Export your data + )} {control > 2 && ( - + - Reload account data - + Reload account data + )} {control > 3 && ( - + - Restrict processing of your data - + Restrict processing of your data + )} - + - Remove your account - + Remove your account +
)} diff --git a/packages/react/components/Account/Newsletter.mjs b/packages/react/components/Account/Newsletter.mjs new file mode 100644 index 00000000000..3d446ca0011 --- /dev/null +++ b/packages/react/components/Account/Newsletter.mjs @@ -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 ( +
+ ({ + val, + label: ( +
+ + {val === 'yes' ? 'Yes, I would like to receive the newsletter' : 'No thanks'} + + {val === 'yes' ? ( + + ) : ( + + )} +
+ ), + 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 ? ( + <> + + {welcomeSteps[account?.control].length > 0 ? ( + <> + + + 2 / {welcomeSteps[account?.control].length} + + + + ) : null} + + ) : null} + +
You can unsubscribe at any time with the link below
+

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

+

+ + Unsubscribe link + +

+

+ This link is to unsubscribe you specifically, do not share it with other subscribers. +

+
+
+ ) +} diff --git a/packages/react/components/Account/Pattern.mjs b/packages/react/components/Account/Pattern.mjs index f84c1d9ce04..397f9d5b28a 100644 --- a/packages/react/components/Account/Pattern.mjs +++ b/packages/react/components/Account/Pattern.mjs @@ -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]) } diff --git a/packages/react/components/Account/Patterns.mjs b/packages/react/components/Account/Patterns.mjs index 16dc2a463c8..693a95a5d48 100644 --- a/packages/react/components/Account/Patterns.mjs +++ b/packages/react/components/Account/Patterns.mjs @@ -200,7 +200,7 @@ export const Patterns = ({ Link = false }) => { {pattern.id} { {pattern.name} diff --git a/packages/react/components/Account/Set.mjs b/packages/react/components/Account/Set.mjs index 0c47b4562be..00400b10658 100644 --- a/packages/react/components/Account/Set.mjs +++ b/packages/react/components/Account/Set.mjs @@ -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, diff --git a/packages/react/components/Account/Units.mjs b/packages/react/components/Account/Units.mjs new file mode 100644 index 00000000000..d181763f18a --- /dev/null +++ b/packages/react/components/Account/Units.mjs @@ -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 ( +
+ ({ + val, + label: ( +
+ {val === 'metric' ? 'Metric units (cm)' : 'Imperial units (inch)'} + +
+ ), + desc: + val === 'metric' + ? 'Pick this if you prefere centimeters over inches' + : 'Pick this if you prefer inches over centimeters', + }))} + current={selection} + update={update} + /> + {welcome ? ( + <> + + {welcomeSteps[account?.control].length > 0 ? ( + <> + + + 3 / {welcomeSteps[account?.control].length} + + + + ) : null} + + ) : null} +
+ ) +} diff --git a/packages/react/components/Account/index.mjs b/packages/react/components/Account/index.mjs index a8913f42ac6..4a62becd92f 100644 --- a/packages/react/components/Account/index.mjs +++ b/packages/react/components/Account/index.mjs @@ -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, } diff --git a/sites/org/docs/account/pattern/index.mdx b/sites/org/docs/account/data/patterns/pattern.mdx similarity index 100% rename from sites/org/docs/account/pattern/index.mdx rename to sites/org/docs/account/data/patterns/pattern.mdx diff --git a/sites/org/docs/account/preferences/consent/index.mdx b/sites/org/docs/account/preferences/consent/index.mdx index 8a349e73b8d..1f829a31f42 100644 --- a/sites/org/docs/account/preferences/consent/index.mdx +++ b/sites/org/docs/account/preferences/consent/index.mdx @@ -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' - + diff --git a/sites/org/docs/account/preferences/newsletter/index.mdx b/sites/org/docs/account/preferences/newsletter/index.mdx index e86255e1519..8d38214f0ca 100644 --- a/sites/org/docs/account/preferences/newsletter/index.mdx +++ b/sites/org/docs/account/preferences/newsletter/index.mdx @@ -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' - + diff --git a/sites/org/docs/account/preferences/units/index.mdx b/sites/org/docs/account/preferences/units/index.mdx index bf9e3ab2fef..cb13326b721 100644 --- a/sites/org/docs/account/preferences/units/index.mdx +++ b/sites/org/docs/account/preferences/units/index.mdx @@ -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' - +