1
0
Fork 0

wip(org): Going through account pages

This commit is contained in:
joostdecock 2023-08-19 18:01:22 +02:00
parent cae9229232
commit 4744759d0b
45 changed files with 765 additions and 538 deletions

View file

@ -60,12 +60,11 @@ export const SlugInput = ({ slug, setSlug, title, slugAvailable }) => {
useEffect(() => {
if (title !== slug) setSlug(slugify(title))
}, [title])
console.log(slugAvailable)
return (
<input
className={`input input-text input-bordered input-lg w-full mb-2 ${
!slugAvailable || slug.length < 4 ? 'input-error' : 'input-success'
true || !slugAvailable || slug.length < 4 ? 'input-error' : 'input-success'
}`}
value={slug}
placeholder="Type your title here"

View file

@ -1,13 +1,16 @@
// Dependencies
import dynamic from 'next/dynamic'
import { serverSideTranslations } from 'next-i18next/serverSideTranslations'
import { nsMerge } from 'shared/utils.mjs'
// Hooks
import { useTranslation } from 'next-i18next'
// Components
import { PageWrapper, ns as pageNs } from 'shared/components/wrappers/page.mjs'
import { ns as authNs } from 'shared/components/wrappers/auth/index.mjs'
import { ns as bioNs } from 'shared/components/account/bio.mjs'
// Translation namespaces used on this page
const namespaces = [...new Set([...bioNs, ...authNs, ...pageNs])]
const ns = nsMerge(bioNs, authNs, pageNs)
/*
* Some things should never generated as SSR
@ -29,20 +32,24 @@ const DynamicBio = dynamic(
* when path and locale come from static props (as here)
* or set them manually.
*/
const AccountBioPage = ({ page }) => (
<PageWrapper {...page}>
<DynamicAuthWrapper>
<DynamicBio title />
</DynamicAuthWrapper>
</PageWrapper>
)
const AccountBioPage = ({ page }) => {
const { t } = useTranslation(ns)
return (
<PageWrapper {...page} title={t('bio')}>
<DynamicAuthWrapper>
<DynamicBio title />
</DynamicAuthWrapper>
</PageWrapper>
)
}
export default AccountBioPage
export async function getStaticProps({ locale }) {
return {
props: {
...(await serverSideTranslations(locale, namespaces)),
...(await serverSideTranslations(locale, ns)),
page: {
locale,
path: ['account', 'bio'],

View file

@ -1,13 +1,16 @@
// Dependencies
import dynamic from 'next/dynamic'
import { serverSideTranslations } from 'next-i18next/serverSideTranslations'
import { nsMerge } from 'shared/utils.mjs'
// Hooks
import { useTranslation } from 'next-i18next'
// Components
import { PageWrapper, ns as pageNs } from 'shared/components/wrappers/page.mjs'
import { ns as authNs } from 'shared/components/wrappers/auth/index.mjs'
import { ns as compareNs } from 'shared/components/account/compare.mjs'
// Translation namespaces used on this page
const namespaces = [...new Set([...compareNs, ...authNs, ...pageNs])]
const ns = nsMerge(compareNs, authNs, pageNs)
/*
* Some things should never generated as SSR
@ -29,13 +32,17 @@ const DynamicCompare = dynamic(
* when path and locale come from static props (as here)
* or set them manually.
*/
const AccountComparePage = ({ page }) => (
<PageWrapper {...page}>
<DynamicAuthWrapper>
<DynamicCompare title />
</DynamicAuthWrapper>
</PageWrapper>
)
const AccountComparePage = ({ page }) => {
const { t } = useTranslation(ns)
return (
<PageWrapper {...page} title={t('compare')}>
<DynamicAuthWrapper>
<DynamicCompare title />
</DynamicAuthWrapper>
</PageWrapper>
)
}
export default AccountComparePage

View file

@ -1,13 +1,16 @@
// Dependencies
import dynamic from 'next/dynamic'
import { serverSideTranslations } from 'next-i18next/serverSideTranslations'
import { nsMerge } from 'shared/utils.mjs'
// Hooks
import { useTranslation } from 'next-i18next'
// Components
import { PageWrapper, ns as pageNs } from 'shared/components/wrappers/page.mjs'
import { ns as authNs } from 'shared/components/wrappers/auth/index.mjs'
import { ns as controlNs } from 'shared/components/account/control.mjs'
// Translation namespaces used on this page
const namespaces = [...new Set([...controlNs, ...authNs, ...pageNs])]
const ns = nsMerge(controlNs, authNs, pageNs)
/*
* Some things should never generated as SSR
@ -29,20 +32,24 @@ const DynamicControl = dynamic(
* when path and locale come from static props (as here)
* or set them manually.
*/
const AccountPage = ({ page }) => (
<PageWrapper {...page}>
<DynamicAuthWrapper>
<DynamicControl title />
</DynamicAuthWrapper>
</PageWrapper>
)
const AccountPage = ({ page }) => {
const { t } = useTranslation(ns)
return (
<PageWrapper {...page} title={t('control')}>
<DynamicAuthWrapper>
<DynamicControl title />
</DynamicAuthWrapper>
</PageWrapper>
)
}
export default AccountPage
export async function getStaticProps({ locale }) {
return {
props: {
...(await serverSideTranslations(locale, namespaces)),
...(await serverSideTranslations(locale, ns)),
page: {
locale,
path: ['account', 'control'],

View file

@ -1,13 +1,16 @@
// Dependencies
import dynamic from 'next/dynamic'
import { serverSideTranslations } from 'next-i18next/serverSideTranslations'
import { nsMerge } from 'shared/utils.mjs'
// Hooks
import { useTranslation } from 'next-i18next'
// Components
import { PageWrapper, ns as pageNs } from 'shared/components/wrappers/page.mjs'
import { ns as authNs } from 'shared/components/wrappers/auth/index.mjs'
import { ns as emailNs } from 'shared/components/account/email.mjs'
// Translation namespaces used on this page
const namespaces = [...new Set([...emailNs, ...authNs, ...pageNs])]
const ns = nsMerge(emailNs, authNs, pageNs)
/*
* Some things should never generated as SSR
@ -29,20 +32,24 @@ const DynamicEmail = dynamic(
* when path and locale come from static props (as here)
* or set them manually.
*/
const AccountEmailPage = ({ page }) => (
<PageWrapper {...page}>
<DynamicAuthWrapper>
<DynamicEmail title />
</DynamicAuthWrapper>
</PageWrapper>
)
const AccountEmailPage = ({ page }) => {
const { t } = useTranslation(ns)
return (
<PageWrapper {...page} title={t('email')}>
<DynamicAuthWrapper>
<DynamicEmail title />
</DynamicAuthWrapper>
</PageWrapper>
)
}
export default AccountEmailPage
export async function getStaticProps({ locale }) {
return {
props: {
...(await serverSideTranslations(locale, namespaces)),
...(await serverSideTranslations(locale, ns)),
page: {
locale,
path: ['account', 'email'],

View file

@ -1,13 +1,16 @@
// Dependencies
import dynamic from 'next/dynamic'
import { serverSideTranslations } from 'next-i18next/serverSideTranslations'
import { nsMerge } from 'shared/utils.mjs'
// Hooks
import { useTranslation } from 'next-i18next'
// Components
import { PageWrapper, ns as pageNs } from 'shared/components/wrappers/page.mjs'
import { ns as authNs } from 'shared/components/wrappers/auth/index.mjs'
import { ns as imgNs } from 'shared/components/account/img.mjs'
// Translation namespaces used on this page
const namespaces = [...new Set([...imgNs, ...authNs, ...pageNs])]
const ns = nsMerge(imgNs, authNs, pageNs)
/*
* Some things should never generated as SSR
@ -29,20 +32,24 @@ const DynamicImg = dynamic(
* when path and locale come from static props (as here)
* or set them manually.
*/
const AccountPage = ({ page }) => (
<PageWrapper {...page}>
<DynamicAuthWrapper>
<DynamicImg title />
</DynamicAuthWrapper>
</PageWrapper>
)
const AccountPage = ({ page }) => {
const { t } = useTranslation(ns)
return (
<PageWrapper {...page} t={t('img')}>
<DynamicAuthWrapper>
<DynamicImg title />
</DynamicAuthWrapper>
</PageWrapper>
)
}
export default AccountPage
export async function getStaticProps({ locale }) {
return {
props: {
...(await serverSideTranslations(locale, namespaces)),
...(await serverSideTranslations(locale, ns)),
page: {
locale,
path: ['account', 'img'],

View file

@ -1,6 +1,7 @@
// Dependencies
import dynamic from 'next/dynamic'
import { serverSideTranslations } from 'next-i18next/serverSideTranslations'
import { nsMerge } from 'shared/utils.mjs'
// Hooks
import { useTranslation } from 'next-i18next'
// Components
@ -8,7 +9,7 @@ import { PageWrapper, ns as pageNs } from 'shared/components/wrappers/page.mjs'
import { ns as authNs } from 'shared/components/wrappers/auth/index.mjs'
// Translation namespaces used on this page
const ns = [...new Set(['account', ...pageNs, ...authNs])]
const ns = nsMerge('account', 'status', pageNs, authNs)
/*
* Some things should never generated as SSR

View file

@ -1,13 +1,16 @@
// Dependencies
import dynamic from 'next/dynamic'
import { serverSideTranslations } from 'next-i18next/serverSideTranslations'
import { nsMerge } from 'shared/utils.mjs'
// Hooks
import { useTranslation } from 'next-i18next'
// Components
import { PageWrapper, ns as pageNs } from 'shared/components/wrappers/page.mjs'
import { ns as authNs } from 'shared/components/wrappers/auth/index.mjs'
import { ns as languageNs } from 'shared/components/account/language.mjs'
// Translation namespaces used on this page
const namespaces = [...new Set([...languageNs, ...authNs, ...pageNs])]
const ns = nsMerge(languageNs, authNs, pageNs)
/*
* Some things should never generated as SSR
@ -29,20 +32,24 @@ const DynamicLanguage = dynamic(
* when path and locale come from static props (as here)
* or set them manually.
*/
const AccountLanguagePage = ({ page }) => (
<PageWrapper {...page}>
<DynamicAuthWrapper>
<DynamicLanguage title />
</DynamicAuthWrapper>
</PageWrapper>
)
const AccountLanguagePage = ({ page }) => {
const { t } = useTranslation(ns)
return (
<PageWrapper {...page} title={t('account:language')}>
<DynamicAuthWrapper>
<DynamicLanguage title />
</DynamicAuthWrapper>
</PageWrapper>
)
}
export default AccountLanguagePage
export async function getStaticProps({ locale }) {
return {
props: {
...(await serverSideTranslations(locale, namespaces)),
...(await serverSideTranslations(locale, ns)),
page: {
locale,
path: ['account', 'language'],

View file

@ -1,13 +1,16 @@
// Dependencies
import dynamic from 'next/dynamic'
import { serverSideTranslations } from 'next-i18next/serverSideTranslations'
import { nsMerge } from 'shared/utils.mjs'
// Hooks
import { useTranslation } from 'next-i18next'
// Components
import { PageWrapper, ns as pageNs } from 'shared/components/wrappers/page.mjs'
import { ns as authNs } from 'shared/components/wrappers/auth/index.mjs'
import { ns as mfaNs } from 'shared/components/account/mfa.mjs'
// Translation namespaces used on this page
const namespaces = [...new Set([...mfaNs, ...authNs, ...pageNs])]
const ns = nsMerge(mfaNs, authNs, pageNs)
/*
* Some things should never generated as SSR
@ -29,20 +32,24 @@ const DynamicMfa = dynamic(
* when path and locale come from static props (as here)
* or set them manually.
*/
const AccountMfaPage = ({ page }) => (
<PageWrapper {...page}>
<DynamicAuthWrapper>
<DynamicMfa title />
</DynamicAuthWrapper>
</PageWrapper>
)
const AccountMfaPage = ({ page }) => {
const { t } = useTranslation(ns)
return (
<PageWrapper {...page} title={t('mfa')}>
<DynamicAuthWrapper>
<DynamicMfa title />
</DynamicAuthWrapper>
</PageWrapper>
)
}
export default AccountMfaPage
export async function getStaticProps({ locale }) {
return {
props: {
...(await serverSideTranslations(locale, namespaces)),
...(await serverSideTranslations(locale, ns)),
page: {
locale,
path: ['account', 'mfa'],

View file

@ -1,13 +1,16 @@
// Dependencies
import dynamic from 'next/dynamic'
import { serverSideTranslations } from 'next-i18next/serverSideTranslations'
import { nsMerge } from 'shared/utils.mjs'
// Hooks
import { useTranslation } from 'next-i18next'
// Components
import { PageWrapper, ns as pageNs } from 'shared/components/wrappers/page.mjs'
import { ns as authNs } from 'shared/components/wrappers/auth/index.mjs'
import { ns as newsletterNs } from 'shared/components/account/newsletter.mjs'
// Translation namespaces used on this page
const namespaces = [...new Set([...newsletterNs, ...authNs, ...pageNs])]
const ns = nsMerge(newsletterNs, authNs, pageNs)
/*
* Some things should never generated as SSR
@ -29,20 +32,24 @@ const DynamicNewsletter = dynamic(
* when path and locale come from static props (as here)
* or set them manually.
*/
const AccountNewsletterPage = ({ page }) => (
<PageWrapper {...page}>
<DynamicAuthWrapper>
<DynamicNewsletter title />
</DynamicAuthWrapper>
</PageWrapper>
)
const AccountNewsletterPage = ({ page }) => {
const { t } = useTranslation(ns)
return (
<PageWrapper {...page}>
<DynamicAuthWrapper>
<DynamicNewsletter title />
</DynamicAuthWrapper>
</PageWrapper>
)
}
export default AccountNewsletterPage
export async function getStaticProps({ locale }) {
return {
props: {
...(await serverSideTranslations(locale, namespaces)),
...(await serverSideTranslations(locale, ns)),
page: {
locale,
path: ['account', 'newsletter'],

View file

@ -1,13 +1,16 @@
// Dependencies
import dynamic from 'next/dynamic'
import { serverSideTranslations } from 'next-i18next/serverSideTranslations'
import { nsMerge } from 'shared/utils.mjs'
// Hooks
import { useTranslation } from 'next-i18next'
// Components
import { PageWrapper, ns as pageNs } from 'shared/components/wrappers/page.mjs'
import { ns as authNs } from 'shared/components/wrappers/auth/index.mjs'
import { ns as passwordNs } from 'shared/components/account/password.mjs'
// Translation namespaces used on this page
const namespaces = [...new Set([...passwordNs, ...authNs, ...pageNs])]
const ns = nsMerge(passwordNs, authNs, pageNs)
/*
* Some things should never generated as SSR
@ -29,20 +32,24 @@ const DynamicPassword = dynamic(
* when path and locale come from static props (as here)
* or set them manually.
*/
const AccountPasswordPage = ({ page }) => (
<PageWrapper {...page}>
<DynamicAuthWrapper>
<DynamicPassword title />
</DynamicAuthWrapper>
</PageWrapper>
)
const AccountPasswordPage = ({ page }) => {
const { t } = useTranslation(ns)
return (
<PageWrapper {...page} title={t('password')}>
<DynamicAuthWrapper>
<DynamicPassword title />
</DynamicAuthWrapper>
</PageWrapper>
)
}
export default AccountPasswordPage
export async function getStaticProps({ locale }) {
return {
props: {
...(await serverSideTranslations(locale, namespaces)),
...(await serverSideTranslations(locale, ns)),
page: {
locale,
path: ['account', 'password'],

View file

@ -1,13 +1,16 @@
// Dependencies
import dynamic from 'next/dynamic'
import { serverSideTranslations } from 'next-i18next/serverSideTranslations'
import { nsMerge } from 'shared/utils.mjs'
// Hooks
import { useTranslation } from 'next-i18next'
// Components
import { PageWrapper, ns as pageNs } from 'shared/components/wrappers/page.mjs'
import { ns as authNs } from 'shared/components/wrappers/auth/index.mjs'
import { ns as reloadNs } from 'shared/components/account/reload.mjs'
// Translation namespaces used on this page
const namespaces = [...new Set([...reloadNs, ...authNs, ...pageNs])]
const ns = nsMerge(reloadNs, authNs, pageNs)
/*
* Some things should never generated as SSR
@ -29,20 +32,24 @@ const DynamicReload = dynamic(
* when path and locale come from static props (as here)
* or set them manually.
*/
const AccountReloadPage = ({ page }) => (
<PageWrapper {...page}>
<DynamicAuthWrapper>
<DynamicReload title />
</DynamicAuthWrapper>
</PageWrapper>
)
const AccountReloadPage = ({ page }) => {
const { t } = useTranslation(ns)
return (
<PageWrapper {...page} title={t('reload')}>
<DynamicAuthWrapper>
<DynamicReload title />
</DynamicAuthWrapper>
</PageWrapper>
)
}
export default AccountReloadPage
export async function getStaticProps({ locale }) {
return {
props: {
...(await serverSideTranslations(locale, namespaces)),
...(await serverSideTranslations(locale, ns)),
page: {
locale,
path: ['account', 'reload'],

View file

@ -1,13 +1,16 @@
// Dependencies
import dynamic from 'next/dynamic'
import { serverSideTranslations } from 'next-i18next/serverSideTranslations'
import { nsMerge } from 'shared/utils.mjs'
// Hooks
import { useTranslation } from 'next-i18next'
// Components
import { PageWrapper, ns as pageNs } from 'shared/components/wrappers/page.mjs'
import { ns as authNs } from 'shared/components/wrappers/auth/index.mjs'
import { ns as unitsNs } from 'shared/components/account/imperial.mjs'
// Translation namespaces used on this page
const namespaces = [...new Set([...unitsNs, ...authNs, ...pageNs])]
const namespaces = nsMerge(unitsNs, authNs, pageNs)
/*
* Some things should never generated as SSR
@ -29,13 +32,17 @@ const DynamicImperial = dynamic(
* when path and locale come from static props (as here)
* or set them manually.
*/
const AccountUnitsPage = ({ page }) => (
<PageWrapper {...page}>
<DynamicAuthWrapper>
<DynamicImperial title />
</DynamicAuthWrapper>
</PageWrapper>
)
const AccountUnitsPage = ({ page }) => {
const { t } = useTranslation(namespaces)
return (
<PageWrapper {...page} title={t('account:units')}>
<DynamicAuthWrapper>
<DynamicImperial title />
</DynamicAuthWrapper>
</PageWrapper>
)
}
export default AccountUnitsPage

View file

@ -1,13 +1,16 @@
// Dependencies
import dynamic from 'next/dynamic'
import { serverSideTranslations } from 'next-i18next/serverSideTranslations'
import { nsMerge } from 'shared/utils.mjs'
// Hooks
import { useTranslation } from 'next-i18next'
// Components
import { PageWrapper, ns as pageNs } from 'shared/components/wrappers/page.mjs'
import { ns as authNs } from 'shared/components/wrappers/auth/index.mjs'
import { ns as usernameNs } from 'shared/components/account/username.mjs'
// Translation namespaces used on this page
const namespaces = [...new Set([...usernameNs, ...authNs, ...pageNs])]
const ns = nsMerge(usernameNs, authNs, pageNs)
/*
* Some things should never generated as SSR
@ -29,20 +32,24 @@ const DynamicUsername = dynamic(
* when path and locale come from static props (as here)
* or set them manually.
*/
const AccountPage = ({ page }) => (
<PageWrapper {...page}>
<DynamicAuthWrapper>
<DynamicUsername title />
</DynamicAuthWrapper>
</PageWrapper>
)
const AccountPage = ({ page }) => {
const { t } = useTranslation(ns)
return (
<PageWrapper {...page} title={t('username')}>
<DynamicAuthWrapper>
<DynamicUsername title />
</DynamicAuthWrapper>
</PageWrapper>
)
}
export default AccountPage
export async function getStaticProps({ locale }) {
return {
props: {
...(await serverSideTranslations(locale, namespaces)),
...(await serverSideTranslations(locale, ns)),
page: {
locale,
path: ['account', 'username'],

View file

@ -4,9 +4,7 @@ import { useTranslation } from 'next-i18next'
// Hooks
import { useAccount } from 'shared/hooks/use-account.mjs'
import { useBackend } from 'shared/hooks/use-backend.mjs'
import { useToast } from 'shared/hooks/use-toast.mjs'
// Context
import { LoadingContext } from 'shared/context/loading-context.mjs'
import { useLoadingStatus } from 'shared/hooks/use-loading-status.mjs'
// Components
import Markdown from 'react-markdown'
import { Icons, welcomeSteps, BackToAccountButton } from './shared.mjs'
@ -14,7 +12,7 @@ import { Popout } from 'shared/components/popout/index.mjs'
import { SaveSettingsButton } from 'shared/components/buttons/save-settings-button.mjs'
import { ContinueButton } from 'shared/components/buttons/continue-button.mjs'
export const ns = ['account', 'toast']
export const ns = ['account', 'status']
export const Tab = ({ id, activeTab, setActiveTab, t }) => (
<button
@ -27,14 +25,11 @@ export const Tab = ({ id, activeTab, setActiveTab, t }) => (
)
export const BioSettings = ({ title = false, welcome = false }) => {
// Context
const { loading, startLoading, stopLoading } = useContext(LoadingContext)
// Hooks
const { account, setAccount, token } = useAccount()
const backend = useBackend(token)
const { t } = useTranslation(ns)
const toast = useToast()
const { setLoadingStatus, LoadingStatus } = useLoadingStatus()
// State
const [bio, setBio] = useState(account.bio)
@ -42,13 +37,12 @@ export const BioSettings = ({ title = false, welcome = false }) => {
// Helper method to save bio
const save = async () => {
startLoading()
setLoadingStatus([true, 'processingUpdate'])
const result = await backend.updateAccount({ bio })
if (result.success) {
setAccount(result.data.account)
toast.for.settingsSaved()
} else toast.for.backendError()
stopLoading()
setLoadingStatus([true, 'settingsSaved', true, true])
} else setLoadingStatus([true, 'backendError', true, true])
}
// Next step in the onboarding
@ -62,6 +56,7 @@ export const BioSettings = ({ title = false, welcome = false }) => {
return (
<div className="max-w-xl xl:pl-4">
<LoadingStatus />
{title ? <h1 className="text-4xl">{t('bioTitle')}</h1> : null}
<div className="tabs w-full">
<Tab id="edit" {...tabProps} />
@ -83,7 +78,7 @@ export const BioSettings = ({ title = false, welcome = false }) => {
)}
</div>
<SaveSettingsButton btnProps={{ onClick: save }} welcome={welcome} />
{!welcome && <BackToAccountButton loading={loading} />}
{!welcome && <BackToAccountButton />}
<Popout tip compact>
{t('mdSupport')}
</Popout>

View file

@ -4,23 +4,18 @@ import { useTranslation } from 'next-i18next'
// Hooks
import { useAccount } from 'shared/hooks/use-account.mjs'
import { useBackend } from 'shared/hooks/use-backend.mjs'
import { useToast } from 'shared/hooks/use-toast.mjs'
// Context
import { LoadingContext } from 'shared/context/loading-context.mjs'
import { useLoadingStatus } from 'shared/hooks/use-loading-status.mjs'
// Components
import { Choice, Icons, welcomeSteps, BackToAccountButton } from './shared.mjs'
import { ContinueButton } from 'shared/components/buttons/continue-button.mjs'
export const ns = ['account', 'toast']
export const ns = ['account', 'status']
export const CompareSettings = ({ title = false, welcome = false }) => {
// Context
const { loading, startLoading, stopLoading } = useContext(LoadingContext)
// Hooks
const { account, setAccount, token } = useAccount()
const backend = useBackend(token)
const toast = useToast()
const { setLoadingStatus, LoadingStatus } = useLoadingStatus()
const { t } = useTranslation(ns)
// State
@ -29,16 +24,15 @@ export const CompareSettings = ({ title = false, welcome = false }) => {
// Helper method to update the account
const update = async (val) => {
if (val !== selection) {
startLoading()
setLoadingStatus([true, 'processingUpdate'])
const result = await backend.updateAccount({
compare: val === 'yes' ? true : false,
})
if (result.success) {
setLoadingStatus([true, 'settingsSaved', true, true])
setAccount(result.data.account)
setSelection(val)
toast.for.settingsSaved()
} else toast.for.backendError()
stopLoading()
} else setLoadingStatus([true, 'backendError', true, true])
}
}
@ -50,6 +44,7 @@ export const CompareSettings = ({ title = false, welcome = false }) => {
return (
<div className="max-w-xl">
<LoadingStatus />
{title ? <h2 className="text-4xl">{t('compareTitle')}</h2> : null}
{['yes', 'no'].map((val) => (
<Choice val={val} t={t} update={update} current={selection} bool key={val}>
@ -85,7 +80,7 @@ export const CompareSettings = ({ title = false, welcome = false }) => {
) : null}
</>
) : (
<BackToAccountButton loading={loading} />
<BackToAccountButton />
)}
</div>
)

View file

@ -1,12 +1,11 @@
// Dependencies
import { useState, useContext } from 'react'
import { useTranslation } from 'next-i18next'
import { nsMerge } from 'shared/utils.mjs'
// Hooks
import { useAccount } from 'shared/hooks/use-account.mjs'
import { useBackend } from 'shared/hooks/use-backend.mjs'
import { useToast } from 'shared/hooks/use-toast.mjs'
// Context
import { LoadingContext } from 'shared/context/loading-context.mjs'
import { useLoadingStatus } from 'shared/hooks/use-loading-status.mjs'
// Components
import Link from 'next/link'
import { Popout } from 'shared/components/popout/index.mjs'
@ -14,7 +13,7 @@ import { BackToAccountButton } from './shared.mjs'
import { SaveSettingsButton } from 'shared/components/buttons/save-settings-button.mjs'
import { GdprAccountDetails, ns as gdprNs } from 'shared/components/gdpr/details.mjs'
export const ns = [...gdprNs, 'account', 'toast']
export const ns = nsMerge(gdprNs, 'account', 'toast')
const Checkbox = ({ value, setter, label, children = null }) => (
<div
@ -37,13 +36,10 @@ const Checkbox = ({ value, setter, label, children = null }) => (
)
export const ConsentSettings = ({ title = false }) => {
// Context
const { loading, startLoading, stopLoading } = useContext(LoadingContext)
// Hooks
const { account, token, setAccount, setToken } = useAccount()
const backend = useBackend(token)
const toast = useToast()
const { setLoadingStatus, LoadingStatus } = useLoadingStatus()
const { t } = useTranslation(ns)
// State
@ -56,27 +52,28 @@ export const ConsentSettings = ({ title = false }) => {
if (consent1) newConsent = 1
if (consent1 && consent2) newConsent = 2
if (newConsent !== account.consent) {
startLoading()
setLoadingStatus([true, 'processingUpdate'])
const result = await backend.updateAccount({ consent: newConsent })
if (result.data?.result === 'success') toast.for.settingsSaved()
else toast.for.backendError()
stopLoading()
if (result.data?.result === 'success') {
setLoadingStatus([true, 'settingsSaved', true, true])
setAccount(result.data.account)
} else setLoadingStatus([true, 'backendError', true, true])
}
}
// Helper method to remove the account
const removeAccount = async () => {
startLoading()
setLoadingStatus([true, 'processingUpdate'])
const result = await backend.removeAccount()
if (result === true) toast.for.settingsSaved()
else toast.for.backendError()
if (result === true) setLoadingStatus([true, 'settingsSaved', true, true])
else setLoadingStatus([true, 'backendError', true, true])
setToken(null)
setAccount({ username: false })
stopLoading()
}
return (
<div className="max-w-xl xl:pl-4">
<LoadingStatus />
{title ? <h2 className="text-4xl">{t('privacyMatters')}</h2> : null}
<p>{t('compliant')}</p>
<p>{t('consentWhyAnswer')}</p>
@ -108,7 +105,7 @@ export const ConsentSettings = ({ title = false }) => {
}}
/>
)}
<BackToAccountButton loading={loading} />
<BackToAccountButton />
<p className="text-center opacity-50 mt-12">
<Link href="/docs/various/privacy" className="hover:text-secondary underline">
FreeSewing Privacy Notice

View file

@ -4,24 +4,19 @@ import { useTranslation } from 'next-i18next'
// Hooks
import { useAccount } from 'shared/hooks/use-account.mjs'
import { useBackend } from 'shared/hooks/use-backend.mjs'
import { useToast } from 'shared/hooks/use-toast.mjs'
// Context
import { LoadingContext } from 'shared/context/loading-context.mjs'
import { useLoadingStatus } from 'shared/hooks/use-loading-status.mjs'
// Components
import { BackToAccountButton, Choice, Icons, welcomeSteps } from './shared.mjs'
import { ContinueButton } from 'shared/components/buttons/continue-button.mjs'
export const ns = ['account', 'toast']
export const ns = ['account', 'status']
/** state handlers for any input that changes the control setting */
export const useControlState = () => {
// Context
const { startLoading, stopLoading } = useContext(LoadingContext)
// Hooks
const { account, setAccount, token } = useAccount()
const backend = useBackend(token)
const toast = useToast()
const { setLoadingStatus, LoadingStatus } = useLoadingStatus()
// State
const [selection, setSelection] = useState(account.control)
@ -30,14 +25,13 @@ export const useControlState = () => {
const update = async (control) => {
if (control !== selection) {
if (token) {
startLoading()
setLoadingStatus([true, 'processingUpdate'])
const result = await backend.updateAccount({ control })
if (result.success) {
setSelection(control)
toast.for.settingsSaved()
setAccount(result.data.account)
} else toast.for.backendError()
stopLoading()
setLoadingStatus([true, 'settingsSaved', true, true])
} else setLoadingStatus([true, 'backendError', true, true])
}
//fallback for guest users
else {
@ -47,13 +41,13 @@ export const useControlState = () => {
}
}
return { selection, update }
return { selection, update, LoadingStatus }
}
export const ControlSettings = ({ title = false, welcome = false, noBack = false }) => {
const { t } = useTranslation(ns)
const { selection, update } = useControlState()
const { selection, update, LoadingStatus } = useControlState()
// Helper to get the link to the next onboarding step
const nextHref = welcome
@ -64,6 +58,7 @@ export const ControlSettings = ({ title = false, welcome = false, noBack = false
return (
<div className="max-w-xl">
<LoadingStatus />
{title ? <h1 className="text-4xl">{t('controlTitle')}</h1> : null}
{[1, 2, 3, 4, 5].map((val) => (
<Choice val={val} t={t} update={update} current={selection} key={val}>

View file

@ -4,26 +4,21 @@ import { useTranslation } from 'next-i18next'
// Hooks
import { useAccount } from 'shared/hooks/use-account.mjs'
import { useBackend } from 'shared/hooks/use-backend.mjs'
import { useToast } from 'shared/hooks/use-toast.mjs'
// Context
import { LoadingContext } from 'shared/context/loading-context.mjs'
import { useLoadingStatus } from 'shared/hooks/use-loading-status.mjs'
// Verification methods
import { validateEmail, validateTld } from 'shared/utils.mjs'
// Components
import { BackToAccountButton } from './shared.mjs'
import { Popout } from 'shared/components/popout/index.mjs'
export const ns = ['account', 'toast']
export const ns = ['account', 'status']
export const EmailSettings = ({ title = false }) => {
// Context
const { loading, startLoading, stopLoading } = useContext(LoadingContext)
// Hooks
const { account, setAccount, token } = useAccount()
const backend = useBackend(token)
const { account, setAccount } = useAccount()
const backend = useBackend()
const { t } = useTranslation(ns)
const toast = useToast()
const { setLoadingStatus, LoadingStatus } = useLoadingStatus()
// State
const [email, setEmail] = useState(account.email)
@ -31,14 +26,13 @@ export const EmailSettings = ({ title = false }) => {
// Helper method to update account
const save = async () => {
startLoading()
setLoadingStatus([true, 'processingUpdate'])
const result = await backend.updateAccount({ email })
if (result.success) {
setAccount(result.data.account)
toast.for.settingsSaved()
} else toast.for.backendError()
setLoadingStatus([true, 'settingsSaved', true, true])
} else setLoadingStatus([true, 'backendError', true, true])
setChanged(true)
stopLoading()
}
// Is email valid?
@ -46,6 +40,7 @@ export const EmailSettings = ({ title = false }) => {
return (
<div className="max-w-xl">
<LoadingStatus />
{title ? <h2 className="text-4xl">{t('emailTitle')}</h2> : null}
{changed ? (
<Popout note>
@ -71,7 +66,7 @@ export const EmailSettings = ({ title = false }) => {
</button>
</>
)}
<BackToAccountButton loading={loading} />
<BackToAccountButton />
</div>
)
}

View file

@ -204,3 +204,6 @@ newBasic: The basics
newAdvanced: Go further
generateANewThing: "Generate a new { thing }"
website: Website
linkedIdentities: Linked Identities

View file

@ -2,27 +2,27 @@
import { useState, useContext, useCallback } from 'react'
import { useTranslation } from 'next-i18next'
import { useDropzone } from 'react-dropzone'
// Context
import { LoadingContext } from 'shared/context/loading-context.mjs'
import { cloudflareImageUrl } from 'shared/utils.mjs'
// Hooks
import { useAccount } from 'shared/hooks/use-account.mjs'
import { useBackend } from 'shared/hooks/use-backend.mjs'
import { useToast } from 'shared/hooks/use-toast.mjs'
import { useLoadingStatus } from 'shared/hooks/use-loading-status.mjs'
// Components
import { Icons, welcomeSteps, BackToAccountButton } from './shared.mjs'
import { ContinueButton } from 'shared/components/buttons/continue-button.mjs'
import { SaveSettingsButton } from 'shared/components/buttons/save-settings-button.mjs'
import { DownloadIcon } from 'shared/components/icons.mjs'
export const ns = ['account', 'toast']
export const ns = ['account', 'status']
export const ImgSettings = ({ title = false, welcome = false }) => {
const { loading, startLoading, stopLoading } = useContext(LoadingContext)
const { account, setAccount, token } = useAccount()
const backend = useBackend(token)
const toast = useToast()
const { setLoadingStatus, LoadingStatus } = useLoadingStatus()
const { t } = useTranslation(ns)
const [img, setImg] = useState(false)
const [url, setUrl] = useState('')
const onDrop = useCallback((acceptedFiles) => {
const reader = new FileReader()
@ -32,26 +32,35 @@ export const ImgSettings = ({ title = false, welcome = false }) => {
acceptedFiles.forEach((file) => reader.readAsDataURL(file))
}, [])
const imageFromUrl = async () => {
const result = await backend.uploadImage({ type, subId, slug, url })
if (result.success) setImg(result.data.imgId)
}
const { getRootProps, getInputProps } = useDropzone({ onDrop })
const save = async () => {
startLoading()
const result = await backend.updateAccount({ img })
setLoadingStatus([true, 'processingUpdate'])
const result = await backend.updateAccount({ img: url ? url : img })
if (result.success) {
setAccount(result.data.account)
toast.for.settingsSaved()
} else toast.for.backendError()
stopLoading()
setLoadingStatus([true, 'settingsSaved', true, true])
} else setLoadingStatus([true, 'backendError', true, false])
}
const nextHref = '/docs/guide'
return (
<div className="max-w-xl">
<LoadingStatus />
{title ? <h2 className="text-4xl">{t('imgTitle')}</h2> : null}
<div>
{!welcome || img !== false ? (
<img alt="img" src={img || account.img} className="shadow mb-4" />
<img
alt="img"
src={img || cloudflareImageUrl({ id: `user-${account.ihash}`, variant: 'public' })}
className="shadow mb-4"
/>
) : null}
<div
{...getRootProps()}
@ -67,6 +76,16 @@ export const ImgSettings = ({ title = false, welcome = false }) => {
{t('imgSelectImage')}
</button>
</div>
<p className="hidden lg:block p-0 my-2 text-center">{t('or')}</p>
<div className="flex flex-row items-center">
<input
type="url"
className="input input-secondary w-full input-bordered"
placeholder="Paste an image URL here"
value={url}
onChange={(evt) => setUrl(evt.target.value)}
/>
</div>
</div>
{welcome ? (
@ -96,7 +115,7 @@ export const ImgSettings = ({ title = false, welcome = false }) => {
) : (
<>
<SaveSettingsButton btnProps={{ onClick: save }} />
<BackToAccountButton loading={loading} />
<BackToAccountButton />
</>
)}
</div>

View file

@ -4,23 +4,18 @@ import { useTranslation } from 'next-i18next'
// Hooks
import { useAccount } from 'shared/hooks/use-account.mjs'
import { useBackend } from 'shared/hooks/use-backend.mjs'
import { useToast } from 'shared/hooks/use-toast.mjs'
// Context
import { LoadingContext } from 'shared/context/loading-context.mjs'
import { useLoadingStatus } from 'shared/hooks/use-loading-status.mjs'
// Components
import { Choice, Icons, welcomeSteps, BackToAccountButton } from './shared.mjs'
import { ContinueButton } from 'shared/components/buttons/continue-button.mjs'
export const ns = ['account', 'toast']
export const ns = ['account', 'status']
export const ImperialSettings = ({ title = false, welcome = false }) => {
// Context
const { loading, startLoading, stopLoading } = useContext(LoadingContext)
// Hooks
const { account, setAccount, token } = useAccount()
const { setLoadingStatus, LoadingStatus } = useLoadingStatus()
const backend = useBackend(token)
const toast = useToast()
const { t } = useTranslation(ns)
// State
@ -29,14 +24,13 @@ export const ImperialSettings = ({ title = false, welcome = false }) => {
// Helper method to update account
const update = async (val) => {
if (val !== selection) {
startLoading()
setLoadingStatus([true, 'processingUpdate'])
const result = await backend.updateAccount({ imperial: val === 'imperial' ? true : false })
if (result.success) {
setAccount(result.data.account)
setSelection(val)
toast.for.settingsSaved()
} else toast.for.backendError()
stopLoading()
setLoadingStatus([true, 'settingsSaved', true, true])
} else setLoadingStatus([true, 'backendError', true, true])
}
}
@ -48,6 +42,7 @@ export const ImperialSettings = ({ title = false, welcome = false }) => {
return (
<div className="max-w-xl">
<LoadingStatus />
{title ? <h1 className="text-4xl">{t('unitsTitle')}</h1> : <h1></h1>}
{['metric', 'imperial'].map((val) => (
<Choice
@ -87,7 +82,7 @@ export const ImperialSettings = ({ title = false, welcome = false }) => {
) : null}
</>
) : (
<BackToAccountButton loading={loading} />
<BackToAccountButton />
)}
</div>
)

View file

@ -4,24 +4,19 @@ import { useTranslation } from 'next-i18next'
// Hooks
import { useAccount } from 'shared/hooks/use-account.mjs'
import { useBackend } from 'shared/hooks/use-backend.mjs'
import { useToast } from 'shared/hooks/use-toast.mjs'
// Context
import { LoadingContext } from 'shared/context/loading-context.mjs'
import { useLoadingStatus, ns as statusNs } from 'shared/hooks/use-loading-status.mjs'
// Components
import { BackToAccountButton, Choice } from './shared.mjs'
// Config
import { siteConfig as conf } from 'site/site.config.mjs'
export const ns = ['account', 'locales', 'toast']
export const ns = ['account', 'locales', statusNs]
export const LanguageSettings = ({ title = false }) => {
// Context
const { loading, startLoading, stopLoading } = useContext(LoadingContext)
// Hooks
const { account, setAccount, token } = useAccount()
const { setLoadingStatus, LoadingStatus } = useLoadingStatus()
const backend = useBackend(token)
const toast = useToast()
const { t } = useTranslation(ns)
// State
@ -30,26 +25,26 @@ export const LanguageSettings = ({ title = false }) => {
// Helper method to update the account
const update = async (lang) => {
if (lang !== language) {
startLoading()
setLoadingStatus([true, 'processingUpdate'])
setLanguage(lang)
const result = await backend.updateAccount({ language: lang })
if (result.success) {
setAccount(result.data.account)
toast.for.settingsSaved()
} else toast.for.backendError()
stopLoading()
setLoadingStatus([true, 'settingsSaved', true, true])
} else setLoadingStatus([true, 'backendError', true, true])
}
}
return (
<div className="max-w-xl">
<LoadingStatus />
{title ? <h2 className="text-4xl">{t('languageTitle')}</h2> : null}
{conf.languages.map((val) => (
<Choice val={val} t={t} update={update} current={language} key={val}>
<span className="block text-lg leading-5">{t(`locales:${val}`)}</span>
</Choice>
))}
<BackToAccountButton loading={loading} />
<BackToAccountButton />
</div>
)
}

View file

@ -1,35 +1,48 @@
import { useState, useEffect } from 'react'
import { useAccount } from 'shared/hooks/use-account.mjs'
import { useBackend } from 'shared/hooks/use-backend.mjs'
import { useTranslation } from 'next-i18next'
import Link from 'next/link'
import { PageLink } from 'shared/components/page-link.mjs'
import { freeSewingConfig as conf } from 'shared/config/freesewing.config.mjs'
import { Fingerprint } from 'shared/components/fingerprint.mjs'
import {
DesignIcon,
MeasieIcon,
SignoutIcon,
UserIcon,
UnitsIcon,
I18nIcon,
ShowcaseIcon,
ChatIcon,
EmailIcon,
KeyIcon,
BookmarkIcon,
CompareIcon,
PrivacyIcon,
ControlIcon,
LockIcon,
NewsletterIcon,
ShieldIcon,
FingerprintIcon,
GitHubIcon,
InstagramIcon,
MastodonIcon,
TwitterIcon,
TwitchIcon,
TikTokIcon,
LinkIcon,
TrashIcon,
RedditIcon,
ExportIcon,
CloseIcon,
ReloadIcon,
OkIcon,
NoIcon,
} from 'shared/components/icons.mjs'
import { cloudflareImageUrl, capitalize } from 'shared/utils.mjs'
import { ControlScore } from 'shared/components/control/score.mjs'
export const ns = ['account']
const Li = ({ children }) => <li className="inline">{children}</li>
const Spacer = () => <li className="inline px-1 opacity-60"> | </li>
const LinkList = ({ items, t, control, first = false }) => {
const output = []
if (first)
output.push(
<li key="first" className="inline pr-2">
<b>{first}:</b>
</li>
)
for (const [item, cscore] of Object.entries(items)) {
if (cscore <= control)
output.push(
<Li key={`${item}-li`}>
<PageLink href={`/account/${item}`} txt={t(item)} className="capitalize" />
</Li>,
<Spacer key={`${item}-spacer`} />
)
}
return output.length > 1 ? <ul className="mt-4">{output.slice(0, -1)}</ul> : null
}
export const ns = ['account', 'i18n']
const actions = {
reload: 4,
@ -39,55 +52,220 @@ const actions = {
remove: 2,
}
const itemIcons = {
bookmarks: <BookmarkIcon />,
sets: <MeasieIcon />,
patterns: <DesignIcon />,
apikeys: <KeyIcon />,
username: <UserIcon />,
email: <EmailIcon />,
bio: <ChatIcon />,
img: <ShowcaseIcon />,
language: <I18nIcon />,
units: <UnitsIcon />,
compare: <CompareIcon />,
consent: <PrivacyIcon />,
control: <ControlIcon />,
mfa: <ShieldIcon />,
newsletter: <NewsletterIcon />,
password: <LockIcon />,
github: <GitHubIcon />,
instagram: <InstagramIcon />,
mastodon: <MastodonIcon />,
twitter: <TwitterIcon />,
twitch: <TwitchIcon />,
tiktok: <TikTokIcon />,
website: <LinkIcon />,
reddit: <RedditIcon />,
}
const itemClasses = 'flex flex-row items-center justify-between bg-opacity-10 p-2 px-4 rounded mb-1'
const AccountLink = ({ href, title, children }) => (
<Link
className={`${itemClasses} bg-secondary hover:bg-opacity-100 hover:text-neutral-content`}
href={href}
title={title}
>
{children}
</Link>
)
const YesNo = ({ check }) =>
check ? (
<OkIcon className="text-success w-6 h-6" stroke={4} />
) : (
<NoIcon className="text-error w-6 h-6" stroke={3} />
)
export const AccountLinks = () => {
const { account, signOut } = useAccount()
const { t } = useTranslation(ns)
const backend = useBackend()
const [bookmarks, setBookmarks] = useState([])
const [sets, setSets] = useState([])
const [patterns, setPatterns] = useState([])
const [apikeys, setApikeys] = useState([])
useEffect(() => {
const getUserData = async () => {
const result = await backend.getUserData(account.id)
if (result.success) {
setApikeys(result.data.data.apikeys)
setBookmarks(result.data.data.bookmarks)
setPatterns(result.data.data.patterns)
setSets(result.data.data.sets)
}
}
getUserData()
}, [account.id])
const lprops = { t, control: account.control }
const btnClasses = 'btn capitalize flex flex-row justify-between'
const linkClasses = 'flex flex-row gap-2 text-lg py-2 items-center font-medium capitalize'
const itemPreviews = {
apikeys: apikeys?.length || 0,
bookmarks: bookmarks?.length || 0,
sets: sets?.length || 0,
patterns: patterns?.length || 0,
username: account.username,
email: account.email,
bio: <span>{account.bio.slice(0, 15)}&hellip;</span>,
img: (
<img
src={cloudflareImageUrl({ type: 'sq100', id: `user-${account.ihash}` })}
className="w-8 h-8 aspect-square rounded-full shadow"
/>
),
language: t(`i18n:${account.language}`),
units: t(account.imperial ? 'imperialUnits' : 'metricUnits'),
newsletter: <YesNo check={account.newsletter} />,
compare: <YesNo check={account.compare} />,
consent: <YesNo check={account.consent} />,
control: <ControlScore control={account.control} />,
github: account.data.githubUsername || account.data.githubEmail || <NoIcon />,
password:
account.passwordType === 'v3' ? (
<OkIcon className="text-success w-6 h-6" stroke={4} />
) : (
<NoIcon />
),
mfa: <YesNo check={false} />,
}
for (const social of Object.keys(conf.account.fields.identities).filter((i) => i !== 'github'))
itemPreviews[social] = account.data[social] || (
<NoIcon className="text-base-content w-6 h-6" stroke={2} />
)
return (
<div className="w-full max-w-md">
<Link className="btn btn-primary mb-2 w-full capitalize" href="/create">
{t('newPattern')}
</Link>
<div className="flex flex-row gap-2">
<Link className="btn btn-secondary grow capitalize" href="/account/sets">
{t('newSet')}
<div className="w-full max-w-7xl">
<div className="grid grid-cols-1 xl:grid-cols-2 gap-4 mb-8">
<div className="">
<h4 className="my-2">{t('data')}</h4>
{Object.keys(conf.account.fields.data).map((item) => (
<AccountLink href={`/account/${item}`} title={t(item)}>
<div className="flex flex-row items-center gap-3 font-medium">
{itemIcons[item]}
{t(`your${capitalize(item)}`)}
</div>
<div className="">{itemPreviews[item]}</div>
</AccountLink>
))}
</div>
<div className="">
<h4 className="my-2">{t('info')}</h4>
{Object.keys(conf.account.fields.info).map((item) => (
<AccountLink href={`/account/${item}`} title={t(item)}>
<div className="flex flex-row items-center gap-3 font-medium">
{itemIcons[item]}
{t(item)}
</div>
<div className="">{itemPreviews[item]}</div>
</AccountLink>
))}
<div className={`${itemClasses} bg-neutral`}>
<div className="flex flex-row items-center gap-3 font-medium">
<FingerprintIcon />
<span>{t('userId')}</span>
</div>
<div className="">{account.id}</div>
</div>
</div>
<div className="">
<h4 className="my-2">{t('settings')}</h4>
{Object.keys(conf.account.fields.settings).map((item) => (
<AccountLink href={`/account/${item}`} title={t(item)}>
<div className="flex flex-row items-center gap-3 font-medium">
{itemIcons[item]}
{t(item)}
</div>
<div className="">{itemPreviews[item]}</div>
</AccountLink>
))}
</div>
<div className="">
<h4 className="my-2">{t('linkedIdentities')}</h4>
{Object.keys(conf.account.fields.identities).map((item) => (
<AccountLink href={`/account/identities/${item}`} title={t(item)}>
<div className="flex flex-row items-center gap-3 font-medium">
{itemIcons[item]}
{t(item)}
</div>
<div className="">{itemPreviews[item]}</div>
</AccountLink>
))}
</div>
<div className="">
<h4 className="my-2">{t('security')}</h4>
{Object.keys(conf.account.fields.security).map((item) => (
<AccountLink href={`/account/${item}`} title={t(item)}>
<div className="flex flex-row items-center gap-3 font-medium">
{itemIcons[item]}
{t(item)}
</div>
<div className="">{itemPreviews[item]}</div>
</AccountLink>
))}
</div>
<div className="">
<h4 className="my-2">{t('actions')}</h4>
<AccountLink href={`/account/reload`} title={t('reload')}>
<ReloadIcon />
{t('reload')}
</AccountLink>
<AccountLink href={`/account/export`} title={t('export')}>
<ExportIcon />
{t('export')}
</AccountLink>
<AccountLink href={`/account/restrict`} title={t('restrict')}>
<CloseIcon />
{t('restrict')}
</AccountLink>
<AccountLink href={`/account/remove`} title={t('remove')}>
<TrashIcon />
{t('remove')}
</AccountLink>
</div>
</div>
<div className="flex flex-row gap-4 justify-end">
<Link className={`${btnClasses} btn-primary w-64`} href="/profile">
<UserIcon />
{t('yourProfile')}
</Link>
<button className="btn btn-warning btnoutline mb-2 capitalize" onClick={() => signOut()}>
<button className={`${btnClasses} btn-warning w-64`} onClick={() => signOut()}>
<SignoutIcon />
{t('signOut')}
</button>
</div>
<ul className="mt-8">
<li className="inline pr-2">
<b>Quick links:</b>
</li>
<Li>
<PageLink href="/profile" txt={t('yourProfile')} />{' '}
</Li>
<Spacer />
<Li>
<PageLink href="/account/patterns" txt={t('yourPatterns')} />{' '}
</Li>
<Spacer />
<Li>
<PageLink href="/account/sets" txt={t('yourSets')} />{' '}
</Li>
</ul>
{Object.keys(conf.account.fields).map((section) => (
<LinkList
key={section}
items={conf.account.fields[section]}
first={t(section)}
{...lprops}
/>
))}
<LinkList items={actions} first={t('actions')} {...lprops} />
<Fingerprint id={account.id} />
</div>
)
}

View file

@ -4,9 +4,7 @@ import { useTranslation } from 'next-i18next'
// Hooks
import { useAccount } from 'shared/hooks/use-account.mjs'
import { useBackend } from 'shared/hooks/use-backend.mjs'
import { useToast } from 'shared/hooks/use-toast.mjs'
// Context
import { LoadingContext } from 'shared/context/loading-context.mjs'
import { useLoadingStatus } from 'shared/hooks/use-loading-status.mjs'
// Components
import { BackToAccountButton } from './shared.mjs'
import { Popout } from 'shared/components/popout/index.mjs'
@ -25,14 +23,11 @@ const CodeInput = ({ code, setCode, t }) => (
)
export const MfaSettings = ({ title = false, welcome = false }) => {
// Context
const { loading, startLoading, stopLoading } = useContext(LoadingContext)
// Hooks
const { account, setAccount, token } = useAccount()
const backend = useBackend(token)
const { t } = useTranslation(ns)
const toast = useToast()
const { setLoadingStatus, LoadingStatus } = useLoadingStatus()
// State
const [enable, setEnable] = useState(false)
@ -42,15 +37,17 @@ export const MfaSettings = ({ title = false, welcome = false }) => {
// Helper method to enable MFA
const enableMfa = async () => {
startLoading()
setLoadingStatus([true, 'processingUpdate'])
const result = await backend.enableMfa()
if (result.success) setEnable(result.data.mfa)
stopLoading()
if (result.success) {
setEnable(result.data.mfa)
setLoadingStatus([true, 'settingsSaved', true, true])
} else setLoadingStatus([true, 'backendError', true, false])
}
// Helper method to disable MFA
const disableMfa = async () => {
startLoading()
setLoadingStatus([true, 'processingUpdate'])
const result = await backend.disableMfa({
mfa: false,
password,
@ -59,19 +56,18 @@ export const MfaSettings = ({ title = false, welcome = false }) => {
if (result) {
if (result.success) {
setAccount(result.data.account)
toast.warning(<span>{t('mfaDisabled')}</span>)
} else toast.for.backendError()
setLoadingStatus([true, 'settingsSaved', true, true])
} else setLoadingStatus([true, 'backendError', true, false])
setDisable(false)
setEnable(false)
setCode('')
setPassword('')
}
stopLoading()
}
// Helper method to confirm MFA
const confirmMfa = async () => {
startLoading()
setLoadingStatus([true, 'processingUpdate'])
const result = await backend.confirmMfa({
mfa: true,
secret: enable.secret,
@ -79,11 +75,10 @@ export const MfaSettings = ({ title = false, welcome = false }) => {
})
if (result.success) {
setAccount(result.data.account)
toast.success(<span>{t('mfaEnabled')}</span>)
} else toast.for.backendError()
setLoadingStatus([true, 'settingsSaved', true, true])
} else setLoadingStatus([true, 'backendError', true, false])
setEnable(false)
setCode('')
stopLoading()
}
// Figure out what title to use
@ -92,6 +87,7 @@ export const MfaSettings = ({ title = false, welcome = false }) => {
return (
<div className="max-w-xl">
<LoadingStatus />
{title ? <h2 className="text-4xl">{titleText}</h2> : null}
{enable ? (
<>
@ -156,7 +152,7 @@ export const MfaSettings = ({ title = false, welcome = false }) => {
</div>
)}
</div>
{!welcome && <BackToAccountButton loading={loading} />}
{!welcome && <BackToAccountButton />}
</div>
)
}

View file

@ -4,39 +4,32 @@ import { useTranslation } from 'next-i18next'
// Hooks
import { useAccount } from 'shared/hooks/use-account.mjs'
import { useBackend } from 'shared/hooks/use-backend.mjs'
import { useToast } from 'shared/hooks/use-toast.mjs'
// Context
import { LoadingContext } from 'shared/context/loading-context.mjs'
import { useLoadingStatus } from 'shared/hooks/use-loading-status.mjs'
// Components
import { BackToAccountButton, Choice, Icons, welcomeSteps } from './shared.mjs'
import { ContinueButton } from 'shared/components/buttons/continue-button.mjs'
export const ns = ['account', 'toast']
export const ns = ['account', 'status']
export const NewsletterSettings = ({ title = false, welcome = false }) => {
// Context
const { loading, startLoading, stopLoading } = useContext(LoadingContext)
// Hooks
const { account, setAccount, token } = useAccount()
const backend = useBackend(token)
const toast = useToast()
const { t } = useTranslation(ns)
const { LoadingStatus, setLoadingStatus } = useLoadingStatus()
// State
const [selection, setSelection] = useState(account?.newsletter ? 'yes' : 'no')
// Helper method to update account
const update = async (val) => {
if (val !== selection) {
startLoading()
setLoadingStatus([true, 'processingUpdate'])
const result = await backend.updateAccount({ newsletter: val === 'yes' ? true : false })
if (result.success) {
setAccount(result.data.account)
setSelection(val)
toast.for.settingsSaved()
} else toast.for.backendError()
stopLoading()
setLoadingStatus([true, 'settingsSaved', true, true])
} else setLoadingStatus([true, 'backendError', true, true])
}
}
@ -48,6 +41,7 @@ export const NewsletterSettings = ({ title = false, welcome = false }) => {
return (
<div className="max-w-xl">
<LoadingStatus />
{title ? <h1 className="text-4xl">{t('newsletterTitle')}</h1> : null}
{['yes', 'no'].map((val) => (
<Choice val={val} t={t} update={update} current={selection} bool key={val}>
@ -83,7 +77,7 @@ export const NewsletterSettings = ({ title = false, welcome = false }) => {
) : null}
</>
) : (
<BackToAccountButton loading={loading} />
<BackToAccountButton />
)}
</div>
)

View file

@ -4,9 +4,7 @@ import { useTranslation } from 'next-i18next'
// Hooks
import { useAccount } from 'shared/hooks/use-account.mjs'
import { useBackend } from 'shared/hooks/use-backend.mjs'
import { useToast } from 'shared/hooks/use-toast.mjs'
// Context
import { LoadingContext } from 'shared/context/loading-context.mjs'
import { useLoadingStatus } from 'shared/hooks/use-loading-status.mjs'
// Components
import Link from 'next/link'
import { BackToAccountButton } from './shared.mjs'
@ -14,17 +12,14 @@ import { SaveSettingsButton } from 'shared/components/buttons/save-settings-butt
import { Popout } from 'shared/components/popout/index.mjs'
import { RightIcon } from 'shared/components/icons.mjs'
export const ns = ['account', 'toast']
export const ns = ['account', 'status']
export const PasswordSettings = ({ title = false, welcome = false }) => {
// Context
const { loading, startLoading, stopLoading } = useContext(LoadingContext)
// Hooks
const { account, setAccount, token } = useAccount()
const backend = useBackend(token)
const { account, setAccount } = useAccount()
const backend = useBackend()
const { t } = useTranslation(ns)
const toast = useToast()
const { setLoadingStatus, LoadingStatus } = useLoadingStatus()
// State
const [password, setPassword] = useState('')
@ -32,17 +27,17 @@ export const PasswordSettings = ({ title = false, welcome = false }) => {
// Helper method to save password to account
const save = async () => {
startLoading()
setLoadingStatus([true, 'processingUpdate'])
const result = await backend.updateAccount({ password })
if (result.success) {
setAccount(result.data.account)
toast.for.settingsSaved()
} else toast.for.backendError()
stopLoading()
setLoadingStatus([true, 'settingsSaved', true, true])
} else setLoadingStatus([true, 'backendError', true, false])
}
return (
<div className="max-w-xl">
<LoadingStatus />
{title ? <h2 className="text-4xl">{t('passwordTitle')}</h2> : null}
<div className="flex flex-row items-center mt-4 gap-2">
<input
@ -62,7 +57,7 @@ export const PasswordSettings = ({ title = false, welcome = false }) => {
</button>
</div>
<SaveSettingsButton btnProps={{ onClick: save, disabled: password.length < 4 }} />
{!welcome && <BackToAccountButton loading={loading} />}
{!welcome && <BackToAccountButton />}
{!account.mfaEnabled && (
<Popout tip>
<h5>{t('mfaTipTitle')}</h5>

View file

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

View file

@ -4,8 +4,7 @@ import { useTranslation } from 'next-i18next'
// Hooks
import { useAccount } from 'shared/hooks/use-account.mjs'
import { useBackend } from 'shared/hooks/use-backend.mjs'
import { useToast } from 'shared/hooks/use-toast.mjs'
// Context
import { useLoadingStatus } from 'shared/hooks/use-loading-status.mjs'
import { LoadingContext } from 'shared/context/loading-context.mjs'
// Components
import { Spinner } from 'shared/components/spinner.mjs'
@ -16,13 +15,10 @@ import { ContinueButton } from 'shared/components/buttons/continue-button.mjs'
export const ns = ['account', 'toast']
export const UsernameSettings = ({ title = false, welcome = false }) => {
// Context
const { loading, startLoading, stopLoading } = useContext(LoadingContext)
// Hooks
const { account, setAccount, token } = useAccount()
const backend = useBackend(token)
const toast = useToast()
const { setLoadingStatus, LoadingStatus } = useLoadingStatus()
const { t } = useTranslation(ns)
const [username, setUsername] = useState(account.username)
const [available, setAvailable] = useState(true)
@ -38,13 +34,12 @@ export const UsernameSettings = ({ title = false, welcome = false }) => {
}
const save = async () => {
startLoading()
setLoadingStatus([true, 'processingUpdate'])
const result = await backend.updateAccount({ username })
if (result.success) {
setAccount(result.data.account)
toast.for.settingsSaved()
} else toast.for.backendError()
stopLoading()
setLoadingStatus([true, 'settingsSaved', true, true])
} else setLoadingStatus([true, 'backendError', true, true])
}
const nextHref =
@ -53,18 +48,12 @@ export const UsernameSettings = ({ title = false, welcome = false }) => {
: '/docs/guide'
let btnClasses = 'btn mt-4 capitalize '
if (welcome) {
btnClasses += 'w-64 '
if (loading) btnClasses += 'btn-accent '
else btnClasses += 'btn-secondary '
} else {
btnClasses += 'w-full '
if (loading) btnClasses += 'btn-accent '
else btnClasses += 'btn-primary '
}
if (welcome) btnClasses += 'w-64 btn-secondary'
else btnClasses += 'w-full btn-primary'
return (
<div className="max-w-xl">
<LoadingStatus />
{title ? <h1 className="text-4xl">{t('usernameTitle')}</h1> : null}
<div className="flex flex-row items-center">
<input
@ -84,16 +73,7 @@ export const UsernameSettings = ({ title = false, welcome = false }) => {
</div>
<button className={btnClasses} disabled={!available} onClick={save}>
<span className="flex flex-row items-center gap-2">
{loading ? (
<>
<Spinner />
<span>{t('processing')}</span>
</>
) : available ? (
t('save')
) : (
t('usernameNotAvailable')
)}
{available ? t('save') : t('usernameNotAvailable')}
</span>
</button>
@ -119,7 +99,7 @@ export const UsernameSettings = ({ title = false, welcome = false }) => {
) : null}
</>
) : (
<BackToAccountButton loading={loading} />
<BackToAccountButton />
)}
</div>
)

View file

@ -0,0 +1,12 @@
import { BulletIcon } from 'shared/components/icons.mjs'
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) => (
<BulletIcon fill={control >= score ? true : false} className="w-6 h-6 -ml-1" />
))}
</div>
) : null

View file

@ -36,6 +36,12 @@ export const BioIcon = (props) => (
</IconWrapper>
)
export const BookmarkIcon = (props) => (
<IconWrapper {...props}>
<path d="M17.593 3.322c1.1.128 1.907 1.077 1.907 2.185V21L12 17.25 4.5 21V5.507c0-1.108.806-2.057 1.907-2.185a48.507 48.507 0 0111.186 0z" />
</IconWrapper>
)
export const BoxIcon = (props) => (
<IconWrapper {...props}>
<path d="M20 7l-8-4-8 4m16 0l-8 4m8-4v10l-8 4m0-10L4 7m8 4v10M4 7v10l8 4" />
@ -327,6 +333,18 @@ export const LinkIcon = (props) => (
</IconWrapper>
)
export const LockIcon = (props) => (
<IconWrapper {...props}>
<path d="M16.5 10.5V6.75a4.5 4.5 0 10-9 0v3.75m-.75 11.25h10.5a2.25 2.25 0 002.25-2.25v-6.75a2.25 2.25 0 00-2.25-2.25H6.75a2.25 2.25 0 00-2.25 2.25v6.75a2.25 2.25 0 002.25 2.25z" />
</IconWrapper>
)
export const MastodonIcon = (props) => (
<IconWrapper {...props} fill stroke={0}>
<path d="m 11.217423,0.1875 c -2.8267978,0.0231106 -5.545964,0.32921539 -7.1306105,1.056962 0,0 -3.14282962,1.4058166 -3.14282962,6.2023445 0,1.0983506 -0.021349,2.4116171 0.013437,3.8043315 0.11412502,4.690743 0.85993502,9.313695 5.19692442,10.461603 1.9996899,0.529281 3.7166529,0.640169 5.0993757,0.564166 2.507534,-0.139021 3.915187,-0.894849 3.915187,-0.894849 l -0.08272,-1.819364 c 0,0 -1.79194,0.564966 -3.804377,0.496111 -1.9938518,-0.06838 -4.0987697,-0.214969 -4.4212502,-2.662908 -0.029782,-0.215025 -0.044673,-0.445024 -0.044673,-0.686494 0,0 1.9573364,0.47844 4.4378282,0.592088 1.516743,0.06957 2.939062,-0.08886 4.383732,-0.261231 2.770451,-0.330816 5.182722,-2.037815 5.485905,-3.597546 0.477704,-2.456993 0.438356,-5.9959075 0.438356,-5.9959075 0,-4.7965279 -3.142655,-6.2023445 -3.142655,-6.2023445 C 16.83453,0.51671539 14.113674,0.21061063 11.286876,0.1875 Z M 8.0182292,3.9352913 c 1.177465,0 2.0690118,0.4525587 2.6585778,1.3578046 l 0.573249,0.9608111 0.573247,-0.9608111 c 0.589448,-0.9052459 1.480995,-1.3578046 2.65858,-1.3578046 1.017594,0 1.837518,0.3577205 2.463657,1.0555661 0.606959,0.6978459 0.909169,1.6411822 0.909169,2.8281631 V 13.626816 H 15.553691 V 7.9896839 c 0,-1.1882914 -0.49996,-1.7914432 -1.500043,-1.7914432 -1.10575,0 -1.659889,0.715401 -1.659889,2.1301529 V 11.413948 H 10.106352 V 8.3283936 c 0,-1.4147519 -0.5543138,-2.1301529 -1.6600628,-2.1301529 -1.000084,0 -1.5000426,0.6031518 -1.5000426,1.7914432 V 13.626816 H 4.6452275 V 7.8190205 c 0,-1.1869809 0.3022656,-2.1303172 0.9093441,-2.8281631 C 6.1805914,4.2930118 7.0005147,3.9352913 8.0182292,3.9352913 Z" />
</IconWrapper>
)
export const MarginIcon = (props) => (
<IconWrapper {...props}>
<path d="m 2.4889452,14.488945 h 7.0221096 v 7.02211 H 2.4889452 Z M 14.488945,2.4889452 h 7.02211 v 7.0221096 h -7.02211 z m -11.9999998,0 H 9.5110548 V 9.5110548 H 2.4889452 Z M 14.488945,14.488945 h 7.02211 v 7.02211 h -7.02211 z" />
@ -441,12 +459,24 @@ export const PrintIcon = (props) => (
</IconWrapper>
)
export const PrivacyIcon = (props) => (
<IconWrapper {...props}>
<path d="M3.98 8.223A10.477 10.477 0 001.934 12C3.226 16.338 7.244 19.5 12 19.5c.993 0 1.953-.138 2.863-.395M6.228 6.228A10.45 10.45 0 0112 4.5c4.756 0 8.773 3.162 10.065 7.498a10.523 10.523 0 01-4.293 5.774M6.228 6.228L3 3m3.228 3.228l3.65 3.65m7.894 7.894L21 21m-3.228-3.228l-3.65-3.65m0 0a3 3 0 10-4.243-4.243m4.242 4.242L9.88 9.88" />
</IconWrapper>
)
export const RedditIcon = (props) => (
<IconWrapper {...props} stroke={0} fill>
<path d="M 11.710829,0.00384705 C 5.0683862,0.16990815 -0.16221405,5.6505729 0.00384705,12.293016 0.16990814,18.686369 5.3178021,23.833614 11.628124,24.082706 18.270567,24.248767 23.833939,19.018167 24,12.375723 V 11.710829 C 23.833939,5.0683862 18.353273,-0.16221404 11.710829,0.00384705 Z m 5.187788,5.10021625 c 0.15698,0.00649 0.313636,0.048326 0.458939,0.1313569 0.581214,0.3321223 0.912687,1.0793971 0.580565,1.660611 C 17.605998,7.4772452 16.858724,7.808718 16.27751,7.4765965 15.862357,7.3105352 15.614238,6.8947339 15.614238,6.3965506 L 13.038995,5.8159854 12.208689,9.55236 c 1.826672,0.08303 3.48858,0.664893 4.651007,1.495199 0.664245,-0.664245 1.826673,-0.664245 2.490917,0 0.332122,0.332121 0.49786,0.747274 0.49786,1.245457 0.249091,0.747275 -0.249092,1.327193 -0.830306,1.576284 v 0.49948 c 0,2.740009 -3.155161,4.897506 -7.057597,4.897506 -3.9024357,0 -7.0575963,-2.157497 -7.0575963,-4.897506 V 13.8693 C 3.9896377,13.454147 3.6578398,12.458754 3.989962,11.545418 c 0.2490916,-0.664245 0.9120387,-1.08037 1.5762832,-0.99734 0.4981831,0 0.9133359,0.167358 1.2454581,0.499481 C 8.2232228,10.134222 9.8848065,9.55236 11.545418,9.55236 l 0.913011,-4.1515273 c 0,-0.083031 0.08271,-0.1654124 0.08271,-0.1654125 0.08303,-0.08303 0.166711,-0.084328 0.249741,-0.084328 l 2.906069,0.664893 C 15.946037,5.3800751 16.427678,5.084603 16.898617,5.1040633 Z M 9.3026198,12.293016 c -0.6642443,0 -1.2454583,0.581214 -1.2454583,1.245458 0,0.664245 0.498183,1.245459 1.2454583,1.245459 0.6642442,0 1.2454582,-0.581214 1.2454582,-1.245459 0,-0.664244 -0.581214,-1.245458 -1.2454582,-1.245458 z m 5.4813132,0 c -0.664245,0 -1.245459,0.581214 -1.245459,1.245458 0,0.664245 0.581214,1.245459 1.245459,1.245459 0.664245,0 1.245458,-0.581214 1.245458,-1.245459 0,-0.664244 -0.581213,-1.245458 -1.245458,-1.245458 z m -5.3872557,3.943952 c -0.072653,0 -0.135249,0.04021 -0.1767645,0.123249 -0.1660605,0.16606 -0.1660605,0.332121 0,0.415152 0.8303052,0.830306 2.4905922,0.914633 2.9887762,0.914633 0.498183,0 2.077061,-0.08433 2.990396,-0.914633 -0.08303,-0.08303 -0.084,-0.249092 -0.167034,-0.415152 -0.166061,-0.166062 -0.332121,-0.166062 -0.415152,0 -0.498183,0.581213 -1.660611,0.747598 -2.490917,0.747598 -0.830305,0 -1.992733,-0.166385 -2.4909165,-0.747598 -0.08303,-0.08303 -0.1657365,-0.123249 -0.2383882,-0.123249 z" />
</IconWrapper>
)
export const ReloadIcon = (props) => (
<IconWrapper {...props}>
<path d="M16.023 9.348h4.992v-.001M2.985 19.644v-4.992m0 0h4.992m-4.993 0l3.181 3.183a8.25 8.25 0 0013.803-3.7M4.031 9.865a8.25 8.25 0 0113.803-3.7l3.181 3.182m0-4.991v4.99" />
</IconWrapper>
)
export const RightIcon = (props) => (
<IconWrapper {...props}>
<path d="M9 5l7 7-7 7" />
@ -493,6 +523,12 @@ export const SettingsIcon = (props) => (
</IconWrapper>
)
export const ShieldIcon = (props) => (
<IconWrapper {...props}>
<path d="M9 12.75L11.25 15 15 9.75m-3-7.036A11.959 11.959 0 013.598 6 11.99 11.99 0 003 9.749c0 5.592 3.824 10.29 9 11.623 5.176-1.332 9-6.03 9-11.622 0-1.31-.21-2.571-.598-3.751h-.152c-3.196 0-6.1-1.248-8.25-3.285z" />
</IconWrapper>
)
export const ShowcaseIcon = (props) => (
<IconWrapper {...props}>
<path d="M3 9a2 2 0 012-2h.93a2 2 0 001.664-.89l.812-1.22A2 2 0 0110.07 4h3.86a2 2 0 011.664.89l.812 1.22A2 2 0 0018.07 7H19a2 2 0 012 2v9a2 2 0 01-2 2H5a2 2 0 01-2-2V9z" />
@ -500,6 +536,18 @@ export const ShowcaseIcon = (props) => (
</IconWrapper>
)
export const SigninIcon = (props) => (
<IconWrapper {...props}>
<path d="M15.75 9V5.25A2.25 2.25 0 0013.5 3h-6a2.25 2.25 0 00-2.25 2.25v13.5A2.25 2.25 0 007.5 21h6a2.25 2.25 0 002.25-2.25V15M12 9l-3 3m0 0l3 3m-3-3h12.75" />
</IconWrapper>
)
export const SignoutIcon = (props) => (
<IconWrapper {...props}>
<path d="M15.75 9V5.25A2.25 2.25 0 0013.5 3h-6a2.25 2.25 0 00-2.25 2.25v13.5A2.25 2.25 0 007.5 21h6a2.25 2.25 0 002.25-2.25V15m3 0l3-3m0 0l-3-3m3 3H9" />
</IconWrapper>
)
export const StarIcon = (props) => (
<IconWrapper {...props}>
<path d="M11.48 3.499a.562.562 0 011.04 0l2.125 5.111a.563.563 0 00.475.345l5.518.442c.499.04.701.663.321.988l-4.204 3.602a.563.563 0 00-.182.557l1.285 5.385a.562.562 0 01-.84.61l-4.725-2.885a.563.563 0 00-.586 0L6.982 20.54a.562.562 0 01-.84-.61l1.285-5.386a.562.562 0 00-.182-.557l-4.204-3.602a.563.563 0 01.321-.988l5.518-.442a.563.563 0 00.475-.345L11.48 3.5z" />
@ -512,6 +560,12 @@ export const ThemeIcon = (props) => (
</IconWrapper>
)
export const TikTokIcon = (props) => (
<IconWrapper {...props}>
<path d="M 21.070629,5.6224629 A 5.7508474,5.7508474 0 0 1 16.547219,0.52913011 V 0 H 12.41376 v 16.404252 a 3.474745,3.474745 0 0 1 -6.2403831,2.091334 l -0.0024,-0.0012 0.0024,0.0012 A 3.4735455,3.4735455 0 0 1 9.9924767,13.084289 V 8.8848362 A 7.5938063,7.5938063 0 0 0 3.5205237,21.713559 7.5950059,7.5950059 0 0 0 16.547219,16.405452 V 8.0233494 a 9.8171151,9.8171151 0 0 0 5.72685,1.8309665 V 5.7472464 A 5.7964413,5.7964413 0 0 1 21.070637,5.6225887 Z" />
</IconWrapper>
)
export const TipIcon = (props) => (
<IconWrapper {...props}>
<path d="M9.663 17h4.673M12 3v1m6.364 1.636l-.707.707M21 12h-1M4 12H3m3.343-5.657l-.707-.707m2.828 9.9a5 5 0 117.072 0l-.548.547A3.374 3.374 0 0014 18.469V19a2 2 0 11-4 0v-.531c0-.895-.356-1.754-.988-2.386l-.548-.547z" />
@ -538,6 +592,12 @@ export const TrophyIcon = (props) => (
</IconWrapper>
)
export const TwitchIcon = (props) => (
<IconWrapper {...props} stroke={0} fill>
<path d="M2.149 0l-1.612 4.119v16.836h5.731v3.045h3.224l3.045-3.045h4.657l6.269-6.269v-14.686h-21.314zm19.164 13.612l-3.582 3.582h-5.731l-3.045 3.045v-3.045h-4.836v-15.045h17.194v11.463zm-3.582-7.343v6.262h-2.149v-6.262h2.149zm-5.731 0v6.262h-2.149v-6.262h2.149z" />
</IconWrapper>
)
export const TwitterIcon = (props) => (
<IconWrapper {...props} stroke={0} fill>
<path d="M23.954 4.569c-.885.389-1.83.654-2.825.775 1.014-.611 1.794-1.574 2.163-2.723-.951.555-2.005.959-3.127 1.184-.896-.959-2.173-1.559-3.591-1.559-2.717 0-4.92 2.203-4.92 4.917 0 .39.045.765.127 1.124C7.691 8.094 4.066 6.13 1.64 3.161c-.427.722-.666 1.561-.666 2.475 0 1.71.87 3.213 2.188 4.096-.807-.026-1.566-.248-2.228-.616v.061c0 2.385 1.693 4.374 3.946 4.827-.413.111-.849.171-1.296.171-.314 0-.615-.03-.916-.086.631 1.953 2.445 3.377 4.604 3.417-1.68 1.319-3.809 2.105-6.102 2.105-.39 0-.779-.023-1.17-.067 2.189 1.394 4.768 2.209 7.557 2.209 9.054 0 13.999-7.496 13.999-13.986 0-.209 0-.42-.015-.63.961-.689 1.8-1.56 2.46-2.548l-.047-.02z" />

View file

@ -1,11 +1,11 @@
import Link from 'next/link'
export const PageLink = ({ href, txt, className = '' }) => (
export const PageLink = ({ href, txt, className = '', children = null }) => (
<Link
href={href}
className={`underline decoration-2 hover:decoration-4 ${className}`}
title={txt}
title={txt ? txt : ''}
>
{txt}
{children ? children : txt}
</Link>
)

View file

@ -11,29 +11,40 @@ export const freeSewingConfig = {
account: {
fields: {
data: {
bookmarks: 1,
sets: 1,
patterns: 1,
apikeys: 4,
},
info: {
bio: 1,
email: 3,
github: 3,
img: 2,
units: 2,
language: 2,
username: 2,
bio: 1,
img: 2,
email: 3,
},
settings: {
compare: 3,
consent: 2,
control: 1,
mfa: 4,
language: 2,
units: 2,
newsletter: 2,
password: 2,
compare: 3,
control: 1,
consent: 2,
},
developer: {
security: {
password: 2,
mfa: 4,
apikeys: 4,
},
identities: {
github: 3,
instagram: 3,
mastodon: 3,
reddit: 3,
twitter: 3,
twitch: 3,
tiktok: 3,
website: 3,
},
},
sets: {
name: 1,

View file

@ -6,6 +6,9 @@
file in it's scans.
-->
<!-- Loading status -->
<div className="fixed top-0 md:top-28 md:max-w-2xl md:px-4 md:mx-auto"></div>
<!-- Classes for the Popout component -->
<p class="border-accent bg-accent text-accent" />
<p class="border-secondary bg-secondary text-secondary" />

View file

@ -164,6 +164,13 @@ Backend.prototype.reloadAccount = async function () {
return responseHandler(await await api.get(`/whoami/jwt`, this.auth))
}
/*
* Load all user data
*/
Backend.prototype.getUserData = async function (uid) {
return responseHandler(await await api.get(`/users/${uid}/jwt`, this.auth))
}
/*
* Load user profile
*/

View file

@ -1,20 +1,80 @@
import { useState } from 'react'
import { useState, useEffect } from 'react'
import { Spinner } from 'shared/components/spinner.mjs'
import { OkIcon, WarningIcon } from 'shared/components/icons.mjs'
import { useTranslation } from 'next-i18next'
const LoadingStatus = ({ loadingStatus }) =>
loadingStatus[0] ? (
<div className="fixed top-28 left-0 w-full z-30">
export const ns = ['status']
/*
* Timeout in seconds before the loading status dissapears
*/
const timeout = 2
const LoadingStatus = ({ loadingStatus }) => {
const { t } = useTranslation(ns)
const [fade, setFade] = useState('opacity-100')
const [timer, setTimer] = useState(false)
useEffect(() => {
if (loadingStatus[2]) {
if (timer) clearTimeout(timer)
setTimer(
window.setTimeout(() => {
setFade('opacity-0')
}, timeout * 1000 - 350)
)
}
}, [loadingStatus[2]])
if (!loadingStatus[0]) return null
let color = 'secondary'
let icon = <Spinner />
if (loadingStatus[2]) {
color = loadingStatus[3] ? 'success' : 'error'
icon = loadingStatus[3] ? (
<OkIcon stroke={4} className="w-8 h-8" />
) : (
<WarningIcon className="w-8 h-8" stroke={2} />
)
}
return (
<div className="fixed top-0 md:top-28 left-0 w-full z-30 md:px-4 md:mx-auto">
<div
className={`w-full max-w-lg m-auto bg-secondary flex flex-row gap-4 p-4 px-4
rounded-lg shadow text-secondary-content text-medium bg-opacity-90`}
className={`w-full md:max-w-2xl m-auto bg-${color} flex flex-row gap-4 p-4 px-4 ${fade}
transition-opacity delay-[${timeout * 1000 - 400}ms] duration-300
md:rounded-lg shadow text-secondary-content text-lg lg:text-xl font-medium md:bg-opacity-90`}
>
<Spinner /> {loadingStatus[1]}
{icon}
{t(loadingStatus[1])}
</div>
</div>
) : null
)
}
export const useLoadingStatus = () => {
/*
* LoadingStatus should hold an array with 1 to 4 elements:
* 0 => Show loading status or not (true or false)
* 1 => Message to show
* 2 => Set this to true to make the loadingStatus dissapear after 2 seconds
* 3 => Set this to true to show success, false to show error (only when 2 is true)
*/
const [loadingStatus, setLoadingStatus] = useState([false])
const [timer, setTimer] = useState(false)
useEffect(() => {
if (loadingStatus[2]) {
if (timer) clearTimeout(timer)
setTimer(
window.setTimeout(() => {
setLoadingStatus([false])
}, timeout * 1000)
)
}
}, [loadingStatus[2]])
return {
setLoadingStatus,

View file

@ -1,100 +0,0 @@
import { useState, useMemo, useCallback } from 'react'
import set from 'lodash.set'
import unset from 'lodash.unset'
import cloneDeep from 'lodash.clonedeep'
import { useLocalStorage } from './useLocalStorage'
import { defaultGist as baseGist } from 'shared/components/workbench/gist.mjs'
// Generates a default design gist to start from
export const defaultGist = (design, locale = 'en') => {
const gist = {
design,
...baseGist,
_state: { view: 'draft' },
}
if (locale) gist.locale = locale
return gist
}
// generate the gist state and its handlers
export function useGist(design, locale) {
// memoize the initial gist for this design so that it doesn't change between renders and cause an infinite loop
const initialGist = useMemo(() => defaultGist(design, locale), [design, locale])
// get the localstorage state and setter
const [gist, _setGist, gistReady] = useLocalStorage(`${design}_gist`, initialGist)
const [gistHistory, setGistHistory] = useState([])
const [gistFuture, setGistFuture] = useState([])
const setGist = useCallback(
(newGist, addToHistory = true) => {
let oldGist
_setGist((gistState) => {
// have to clone it or nested objects will be referenced instead of copied, which defeats the purpose
if (addToHistory) oldGist = cloneDeep(gistState)
return typeof newGist === 'function' ? newGist(cloneDeep(gistState)) : newGist
})
if (addToHistory) {
setGistHistory((history) => {
return [...history, oldGist]
})
setGistFuture([])
}
},
[_setGist, setGistFuture, setGistHistory]
)
/** update a single gist value */
const updateGist = useCallback(
(path, value, addToHistory = true) => {
setGist((gistState) => {
const newGist = { ...gistState }
set(newGist, path, value)
return newGist
}, addToHistory)
},
[setGist]
)
/** unset a single gist value */
const unsetGist = useCallback(
(path, addToHistory = true) => {
setGist((gistState) => {
const newGist = { ...gistState }
unset(newGist, path)
return newGist
}, addToHistory)
},
[setGist]
)
const undoGist = useCallback(() => {
_setGist((gistState) => {
let prevGist
setGistHistory((history) => {
const newHistory = [...history]
prevGist = newHistory.pop() || defaultGist(design, locale)
return newHistory
})
setGistFuture((future) => [gistState, ...future])
return { ...prevGist }
})
}, [_setGist, setGistFuture, setGistHistory])
const redoGist = useCallback(() => {
const newHistory = [...gistHistory, gist]
const newFuture = [...gistFuture]
const newGist = newFuture.shift()
setGistHistory(newHistory)
setGistFuture(newFuture)
_setGist(newGist)
}, [_setGist, setGistFuture, setGistHistory])
const resetGist = useCallback(() => setGist(defaultGist(design, locale)), [setGist])
return { gist, setGist, unsetGist, gistReady, updateGist, undoGist, redoGist, resetGist }
}

View file

@ -1,38 +0,0 @@
import { useState, useEffect } from 'react'
const prefix = 'fs_'
// See: https://usehooks.com/useLocalStorage/
export function useLocalStorage(key, initialValue) {
// use this to track whether it's mounted. useful for doing other effects outside this hook
// and for making sure we don't write the initial value over the current value
const [ready, setReady] = useState(false)
// State to store our value
const [storedValue, setValue] = useState(initialValue)
// set to localstorage every time the storedValue changes
// we do it this way instead of a callback because
// getting the current state inside `useCallback` didn't seem to be working
useEffect(() => {
if (ready) {
window.localStorage.setItem(prefix + key, JSON.stringify(storedValue))
}
}, [storedValue, key, ready])
// read from local storage on mount
useEffect(() => {
try {
// Get from local storage by key
const item = window.localStorage.getItem(prefix + key)
// Parse stored json or if none return initialValue
const valToSet = item ? JSON.parse(item) : initialValue
setValue(valToSet)
setReady(true)
} catch (error) {
console.log(error)
}
}, [setReady, setValue, key, initialValue])
return [storedValue, setValue, ready]
}

View file

@ -1,3 +1,5 @@
updatingSettings: Updating settings
settingsSaved: Settings saved
backendError: Backend returned an error
copiedToClipboard: Copied to clipboard
processingUpdate: Processing update

View file

@ -39,7 +39,7 @@ export const extendSiteNav = async (siteNav, lang) => {
h: 1,
t: t('sections:new'),
apikey: {
c: conf.account.fields.developer.apikeys,
c: conf.account.fields.security.apikeys,
s: 'new/apikey',
t: t('newApikey'),
o: 30,