wip(org): Going through account pages
This commit is contained in:
parent
cae9229232
commit
4744759d0b
45 changed files with 765 additions and 538 deletions
|
@ -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"
|
||||
|
|
|
@ -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'],
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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'],
|
||||
|
|
|
@ -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'],
|
||||
|
|
|
@ -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'],
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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'],
|
||||
|
|
|
@ -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'],
|
||||
|
|
|
@ -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'],
|
||||
|
|
|
@ -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'],
|
||||
|
|
|
@ -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'],
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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'],
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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}>
|
||||
|
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -204,3 +204,6 @@ newBasic: The basics
|
|||
newAdvanced: Go further
|
||||
|
||||
generateANewThing: "Generate a new { thing }"
|
||||
|
||||
website: Website
|
||||
linkedIdentities: Linked Identities
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
)
|
||||
|
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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)}…</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>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
)
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
)
|
||||
|
|
12
sites/shared/components/control/score.mjs
Normal file
12
sites/shared/components/control/score.mjs
Normal 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
|
|
@ -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" />
|
||||
|
|
|
@ -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>
|
||||
)
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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" />
|
||||
|
|
|
@ -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
|
||||
*/
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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 }
|
||||
}
|
|
@ -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]
|
||||
}
|
|
@ -1,3 +1,5 @@
|
|||
updatingSettings: Updating settings
|
||||
settingsSaved: Settings saved
|
||||
backendError: Backend returned an error
|
||||
copiedToClipboard: Copied to clipboard
|
||||
processingUpdate: Processing update
|
|
@ -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,
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue