1
0
Fork 0

wip(shared): Removed useApp hook in favor of contexts

This removes the useApp hook from all org pages in favor of various
context. This means there is no longer global state that gets passed
around, instead each component that requires access to something shared
(like account, or navigation) can just use the context instead.

This is a first step, as a lot of shared components stil rely on app not
to mention the dev and lab sites.
This commit is contained in:
Joost De Cock 2023-04-28 21:23:06 +02:00
parent e717457454
commit e47c18177b
84 changed files with 1457 additions and 1296 deletions

View file

@ -237,7 +237,7 @@ dev:
'@mdx-js/mdx': *mdx
'@mdx-js/react': *mdx
'@mdx-js/runtime': &mdxRuntime '2.0.0-next.9'
'@next/bundle-analyzer': &next '13.3.0'
'@next/bundle-analyzer': &next '13.3.1'
'@tailwindcss/typography': &tailwindTypography '0.5.9'
'algoliasearch': '4.17.0'
'daisyui': &daisyui '2.51.5'

View file

@ -29,14 +29,14 @@
"@mdx-js/mdx": "2.3.0",
"@mdx-js/react": "2.3.0",
"@mdx-js/runtime": "2.0.0-next.9",
"@next/bundle-analyzer": "13.3.0",
"@next/bundle-analyzer": "13.3.1",
"@tailwindcss/typography": "0.5.9",
"algoliasearch": "4.17.0",
"daisyui": "2.51.5",
"lodash.get": "4.4.2",
"lodash.orderby": "4.6.0",
"lodash.set": "4.3.2",
"next": "13.3.0",
"next": "13.3.1",
"react": "18.2.0",
"react-copy-to-clipboard": "5.1.0",
"react-dom": "18.2.0",
@ -55,7 +55,7 @@
},
"devDependencies": {
"autoprefixer": "10.4.14",
"eslint-config-next": "13.3.0",
"eslint-config-next": "13.3.1",
"js-yaml": "4.1.0",
"postcss": "8.4.21",
"remark-extract-frontmatter": "3.2.0",

View file

@ -40,7 +40,7 @@
"lodash.get": "4.4.2",
"lodash.orderby": "4.6.0",
"lodash.set": "4.3.2",
"next": "13.3.0",
"next": "13.3.1",
"next-i18next": "13.2.2",
"react": "18.2.0",
"react-copy-to-clipboard": "5.1.0",
@ -60,7 +60,7 @@
},
"devDependencies": {
"autoprefixer": "10.4.14",
"eslint-config-next": "13.3.0",
"eslint-config-next": "13.3.1",
"js-yaml": "4.1.0",
"postcss": "8.4.21",
"remark-extract-frontmatter": "3.2.0",

View file

@ -1,16 +1,26 @@
// Hooks
import { useContext } from 'react'
import { useTranslation } from 'next-i18next'
// Context
import { LoadingContext } from 'shared/context/loading-context.mjs'
// Components
import { Spinner } from 'shared/components/spinner.mjs'
import Link from 'next/link'
import { useTranslation } from 'next-i18next'
export const ContinueButton = ({ app, btnProps = {}, link = false }) => {
// Context
const { loading } = useContext(LoadingContext)
// Hooks
const { t } = useTranslation(['account'])
let classes = 'btn mt-8 capitalize w-full '
if (app.state.loading) classes += 'btn-accent '
if (loading) classes += 'btn-accent '
else classes += 'btn-primary '
const children = (
<span className="flex flex-row items-center gap-2">
{app.state.loading ? (
{loading ? (
<>
<Spinner />
<span>{t('processing')}</span>

View file

@ -1,23 +1,29 @@
import { Spinner } from 'shared/components/spinner.mjs'
// Hooks
import { useContext } from 'react'
import { useTranslation } from 'next-i18next'
// Context
import { LoadingContext } from 'shared/context/loading-context.mjs'
// Components
import { Spinner } from 'shared/components/spinner.mjs'
export const SaveSettingsButton = ({ app, btnProps = {}, welcome = false, label = false }) => {
export const SaveSettingsButton = ({ btnProps = {}, welcome = false, label = false }) => {
const { loading } = useContext(LoadingContext)
const { t } = useTranslation(['account'])
let classes = 'btn mt-4 capitalize '
if (welcome) {
classes += 'w-64 '
if (app.state.loading) classes += 'btn-accent '
if (loading) classes += 'btn-accent '
else classes += 'btn-secondary '
} else {
classes += 'w-full '
if (app.state.loading) classes += 'btn-accent '
if (loading) classes += 'btn-accent '
else classes += 'btn-primary '
}
return (
<button className={classes} tabIndex="-1" role="button" {...btnProps}>
<span className="flex flex-row items-center gap-2">
{app.state.loading ? (
{loading ? (
<>
<Spinner />
<span>{t('processing')}</span>

View file

@ -0,0 +1,11 @@
menu: Menu
designs: Collectie
showcase: Voorbeelden
docs: Documentatie
blog: Blog
community: Gemeenschap
account: Account
theme: Thema
language: Taal
search: Zoeken

View file

@ -3,6 +3,7 @@ import { useState, useEffect, useContext } from 'react'
import { useTranslation } from 'next-i18next'
// Context
import { ModalContext } from 'shared/context/modal-context.mjs'
import { LoadingContext } from 'shared/context/loading-context.mjs'
// Components
import {
CommunityIcon,
@ -60,14 +61,14 @@ export const colors = {
account: 'purple',
}
const NavIcons = ({ app, setModal, setSearch }) => {
const NavIcons = ({ setModal, setSearch }) => {
const { t } = useTranslation(['header'])
const iconSize = 'h-6 w-6 lg:h-12 lg:w-12'
return (
<>
<NavButton
onClick={() => setModal(<ModalMenu app={app} />)}
onClick={() => setModal(<ModalMenu />)}
label={t('header:menu')}
color={colors.menu}
>
@ -111,14 +112,14 @@ const NavIcons = ({ app, setModal, setSearch }) => {
</NavButton>
<NavSpacer />
<NavButton
onClick={() => setModal(<ModalThemePicker app={app} />)}
onClick={() => setModal(<ModalThemePicker />)}
label={t('header:theme')}
color={colors.theme}
>
<ThemeIcon className={iconSize} />
</NavButton>
<NavButton
onClick={() => setModal(<ModalLocalePicker app={app} />)}
onClick={() => setModal(<ModalLocalePicker />)}
label={t('header:language')}
color={colors.language}
>
@ -135,8 +136,9 @@ const NavIcons = ({ app, setModal, setSearch }) => {
)
}
export const Header = ({ app, setSearch }) => {
export const Header = ({ setSearch }) => {
const { setModal } = useContext(ModalContext)
const { loading } = useContext(LoadingContext)
const [prevScrollPos, setPrevScrollPos] = useState(0)
const [show, setShow] = useState(true)
@ -162,11 +164,7 @@ export const Header = ({ app, setSearch }) => {
w-full
z-30
transition-transform
${
show || app.state.loading
? ''
: 'fixed bottom-0 lg:top-0 left-0 translate-y-36 lg:-translate-y-36'
}
${show || loading ? '' : 'fixed bottom-0 lg:top-0 left-0 translate-y-36 lg:-translate-y-36'}
drop-shadow-xl
`}
>
@ -174,16 +172,16 @@ export const Header = ({ app, setSearch }) => {
<div className="p-0 flex flex-row gap-2 justify-between text-neutral-content items-center">
{/* Non-mobile content */}
<div className="hidden lg:flex lg:px-2 flex-row items-center justify-center w-full">
<NavIcons app={app} setModal={setModal} setSearch={setSearch} />
<NavIcons setModal={setModal} setSearch={setSearch} />
</div>
{/* Mobile content */}
<div className="flex lg:hidden flex-row items-center justify-between w-full">
<NavIcons app={app} setModal={setModal} setSearch={setSearch} />
<NavIcons setModal={setModal} setSearch={setSearch} />
</div>
</div>
</div>
<Ribbon loading={app.state.loading} theme={app.theme} />
<Ribbon />
</header>
)
}

View file

@ -2,9 +2,9 @@ import { AsideNavigation } from 'shared/components/navigation/aside.mjs'
export const ns = []
export const BareLayout = ({ app, children = [] }) => (
export const BareLayout = (props) => (
<>
<AsideNavigation app={app} mobileOnly />
{children}
<AsideNavigation mobileOnly />
{props.children}
</>
)

View file

@ -1,20 +1,26 @@
import { useContext } from 'react'
// Components
import { AsideNavigation, ns as navNs } from 'shared/components/navigation/aside.mjs'
import { Breadcrumbs } from 'shared/components/breadcrumbs.mjs'
import { NavigationContext } from 'shared/context/navigation-context.mjs'
export const ns = navNs
export const DocsLayout = ({ app, children = [], title }) => (
<div className="grid grid-cols-4 m-auto justify-center place-items-stretch lg:mt-16">
<AsideNavigation app={app} />
<section className="col-span-4 lg:col-span-3 py-24 px-4 lg:pl-8 bg-base-50">
{title && (
<div className="xl:pl-4">
<Breadcrumbs crumbs={app.state.crumbs} title={title} />
<h1 className="break-words">{title}</h1>
</div>
)}
<div className="xl:pl-4">{children}</div>
</section>
</div>
)
export const DocsLayout = ({ children = [] }) => {
const { title, crumbs } = useContext(NavigationContext)
return (
<div className="grid grid-cols-4 m-auto justify-center place-items-stretch lg:mt-16">
<AsideNavigation />
<section className="col-span-4 lg:col-span-3 py-24 px-4 lg:pl-8 bg-base-50">
{title && (
<div className="xl:pl-4">
<Breadcrumbs crumbs={crumbs} title={title} />
<h1 className="break-words">{title}</h1>
</div>
)}
<div className="xl:pl-4">{children}</div>
</section>
</div>
)
}

View file

@ -1,3 +1,5 @@
import { useContext, useEffect } from 'react'
import { NavigationContext } from 'shared/context/navigation-context.mjs'
import get from 'lodash.get'
import { prebuildNavigation as pbn } from 'site/prebuild/navigation.mjs'
import { useTranslation } from 'next-i18next'
@ -81,6 +83,10 @@ const sitePages = (t = false, control = 99) => {
}
}
}
pages.account.reload = {
t: t(`account:reload`),
s: `account/reload`,
}
return pages
}
@ -106,7 +112,7 @@ const createSections = (nav) => {
return orderBy(sections, ['o', 't'])
}
export const useNavigation = (path = [], locale = 'en') => {
export const useNavigation = ({ path, locale }) => {
const { t } = useTranslation(ns)
const { account } = useAccount()
@ -117,10 +123,9 @@ export const useNavigation = (path = [], locale = 'en') => {
const sections = createSections(nav)
return {
path,
slug: path.join('/'),
crumbs,
sections,
slug: path.join('/'),
nav: path.length > 1 ? get(nav, path[0]) : path.length === 0 ? sections : nav[path[0]],
title: crumbs.length > 0 ? crumbs.slice(-1)[0].t : '',
}

View file

@ -40,7 +40,7 @@
"lodash.orderby": "4.6.0",
"lodash.set": "4.3.2",
"luxon": "3.3.0",
"next": "13.3.0",
"next": "13.3.1",
"react-dropzone": "14.2.3",
"react-hotkeys-hook": "4.3.8",
"react-instantsearch-dom": "6.39.1",
@ -60,7 +60,7 @@
},
"devDependencies": {
"autoprefixer": "10.4.14",
"eslint-config-next": "13.3.0",
"eslint-config-next": "13.3.1",
"js-yaml": "4.1.0",
"postcss": "8.4.21",
"remark-extract-frontmatter": "3.2.0",

View file

@ -5,6 +5,7 @@ import Bugsnag from '@bugsnag/js'
import BugsnagPluginReact from '@bugsnag/plugin-react'
import { freeSewingConfig } from 'site/freesewing.config.mjs'
import { Toaster as DefaultToaster } from 'react-hot-toast'
import { ContextWrapper } from 'shared/components/wrappers/context.mjs'
Bugsnag.start({
apiKey: freeSewingConfig.bugsnag.key,
@ -16,25 +17,27 @@ const ErrorBoundary = Bugsnag.getPlugin('react').createErrorBoundary(React)
const FreeSewingOrg = ({ Component, pageProps }) => (
<ErrorBoundary>
<Component {...pageProps} />
<DefaultToaster
position="bottom-right"
toastOptions={{
className: 'bg-base-100 text-base-content',
success: {
className: 'bg-success text-success-content',
},
error: {
className: 'bg-error text-error-content',
},
loading: {
className: 'bg-warning text-warning-content',
},
custom: {
className: 'bg-accent text-accent-content',
},
}}
/>
<ContextWrapper>
<Component {...pageProps} />
<DefaultToaster
position="bottom-right"
toastOptions={{
className: 'bg-base-100 text-base-content',
success: {
className: 'bg-success text-success-content',
},
error: {
className: 'bg-error text-error-content',
},
loading: {
className: 'bg-warning text-warning-content',
},
custom: {
className: 'bg-accent text-accent-content',
},
}}
/>
</ContextWrapper>
</ErrorBoundary>
)

View file

@ -1,5 +1,3 @@
// Hooks
import { useApp } from 'shared/hooks/use-app.mjs'
// Dependencies
import dynamic from 'next/dynamic'
import { serverSideTranslations } from 'next-i18next/serverSideTranslations'
@ -25,25 +23,28 @@ const DynamicApikeys = dynamic(
{ ssr: false }
)
const AccountPage = (props) => {
const app = useApp(props)
/*
* Each page MUST be wrapped in the PageWrapper component.
* You also MUST spread props.page into this wrapper component
* when path and locale come from static props (as here)
* or set them manually.
*/
const AccountApikeysPage = ({ page }) => (
<PageWrapper {...page}>
<DynamicAuthWrapper>
<DynamicApikeys />
</DynamicAuthWrapper>
</PageWrapper>
)
return (
<PageWrapper app={app}>
<DynamicAuthWrapper app={app}>
<DynamicApikeys app={app} />
</DynamicAuthWrapper>
</PageWrapper>
)
}
export default AccountPage
export default AccountApikeysPage
export async function getStaticProps({ locale }) {
return {
props: {
...(await serverSideTranslations(locale, namespaces)),
page: {
locale,
path: ['account', 'apikeys'],
},
},

View file

@ -1,5 +1,3 @@
// Hooks
import { useApp } from 'shared/hooks/use-app.mjs'
// Dependencies
import dynamic from 'next/dynamic'
import { serverSideTranslations } from 'next-i18next/serverSideTranslations'
@ -25,25 +23,28 @@ const DynamicBio = dynamic(
{ ssr: false }
)
const AccountPage = (props) => {
const app = useApp(props)
/*
* Each page MUST be wrapped in the PageWrapper component.
* You also MUST spread props.page into this wrapper component
* when path and locale come from static props (as here)
* or set them manually.
*/
const AccountBioPage = ({ page }) => (
<PageWrapper {...page}>
<DynamicAuthWrapper>
<DynamicBio title />
</DynamicAuthWrapper>
</PageWrapper>
)
return (
<PageWrapper app={app}>
<DynamicAuthWrapper app={app}>
<DynamicBio app={app} title />
</DynamicAuthWrapper>
</PageWrapper>
)
}
export default AccountPage
export default AccountBioPage
export async function getStaticProps({ locale }) {
return {
props: {
...(await serverSideTranslations(locale, namespaces)),
page: {
locale,
path: ['account', 'bio'],
},
},

View file

@ -1,5 +1,3 @@
// Hooks
import { useApp } from 'shared/hooks/use-app.mjs'
// Dependencies
import dynamic from 'next/dynamic'
import { serverSideTranslations } from 'next-i18next/serverSideTranslations'
@ -25,25 +23,28 @@ const DynamicCompare = dynamic(
{ ssr: false }
)
const AccountPage = (props) => {
const app = useApp(props)
/*
* Each page MUST be wrapped in the PageWrapper component.
* You also MUST spread props.page into this wrapper component
* when path and locale come from static props (as here)
* or set them manually.
*/
const AccountComparePage = ({ page }) => (
<PageWrapper {...page}>
<DynamicAuthWrapper>
<DynamicCompare title />
</DynamicAuthWrapper>
</PageWrapper>
)
return (
<PageWrapper app={app}>
<DynamicAuthWrapper app={app}>
<DynamicCompare app={app} title />
</DynamicAuthWrapper>
</PageWrapper>
)
}
export default AccountPage
export default AccountComparePage
export async function getStaticProps({ locale }) {
return {
props: {
...(await serverSideTranslations(locale, namespaces)),
page: {
locale,
path: ['account', 'compare'],
},
},

View file

@ -1,5 +1,3 @@
// Hooks
import { useApp } from 'shared/hooks/use-app.mjs'
// Dependencies
import dynamic from 'next/dynamic'
import { serverSideTranslations } from 'next-i18next/serverSideTranslations'
@ -25,25 +23,28 @@ const DynamicConsent = dynamic(
{ ssr: false }
)
const AccountPage = (props) => {
const app = useApp(props)
/*
* Each page MUST be wrapped in the PageWrapper component.
* You also MUST spread props.page into this wrapper component
* when path and locale come from static props (as here)
* or set them manually.
*/
const AccountConsentPage = ({ page }) => (
<PageWrapper {...page}>
<DynamicAuthWrapper>
<DynamicConsent title />
</DynamicAuthWrapper>
</PageWrapper>
)
return (
<PageWrapper app={app}>
<DynamicAuthWrapper app={app}>
<DynamicConsent app={app} title />
</DynamicAuthWrapper>
</PageWrapper>
)
}
export default AccountPage
export default AccountConsentPage
export async function getStaticProps({ locale }) {
return {
props: {
...(await serverSideTranslations(locale, namespaces)),
page: {
locale,
path: ['account', 'consent'],
},
},

View file

@ -1,5 +1,3 @@
// Hooks
import { useApp } from 'shared/hooks/use-app.mjs'
// Dependencies
import dynamic from 'next/dynamic'
import { serverSideTranslations } from 'next-i18next/serverSideTranslations'
@ -25,17 +23,19 @@ const DynamicControl = dynamic(
{ ssr: false }
)
const AccountPage = (props) => {
const app = useApp(props)
return (
<PageWrapper app={app}>
<DynamicAuthWrapper app={app}>
<DynamicControl app={app} title />
</DynamicAuthWrapper>
</PageWrapper>
)
}
/*
* Each page MUST be wrapped in the PageWrapper component.
* You also MUST spread props.page into this wrapper component
* when path and locale come from static props (as here)
* or set them manually.
*/
const AccountPage = ({ page }) => (
<PageWrapper {...page}>
<DynamicAuthWrapper>
<DynamicControl title />
</DynamicAuthWrapper>
</PageWrapper>
)
export default AccountPage
@ -44,6 +44,7 @@ export async function getStaticProps({ locale }) {
props: {
...(await serverSideTranslations(locale, namespaces)),
page: {
locale,
path: ['account', 'control'],
},
},

View file

@ -1,5 +1,3 @@
// Hooks
import { useApp } from 'shared/hooks/use-app.mjs'
// Dependencies
import dynamic from 'next/dynamic'
import { serverSideTranslations } from 'next-i18next/serverSideTranslations'
@ -25,25 +23,28 @@ const DynamicEmail = dynamic(
{ ssr: false }
)
const AccountPage = (props) => {
const app = useApp(props)
/*
* Each page MUST be wrapped in the PageWrapper component.
* You also MUST spread props.page into this wrapper component
* when path and locale come from static props (as here)
* or set them manually.
*/
const AccountEmailPage = ({ page }) => (
<PageWrapper {...page}>
<DynamicAuthWrapper>
<DynamicEmail title />
</DynamicAuthWrapper>
</PageWrapper>
)
return (
<PageWrapper app={app}>
<DynamicAuthWrapper app={app}>
<DynamicEmail app={app} title />
</DynamicAuthWrapper>
</PageWrapper>
)
}
export default AccountPage
export default AccountEmailPage
export async function getStaticProps({ locale }) {
return {
props: {
...(await serverSideTranslations(locale, namespaces)),
page: {
locale,
path: ['account', 'email'],
},
},

View file

@ -1,5 +1,3 @@
// Hooks
import { useApp } from 'shared/hooks/use-app.mjs'
// Dependencies
import dynamic from 'next/dynamic'
import { serverSideTranslations } from 'next-i18next/serverSideTranslations'
@ -25,17 +23,19 @@ const DynamicGithub = dynamic(
{ ssr: false }
)
const AccountPage = (props) => {
const app = useApp(props)
return (
<PageWrapper app={app}>
<DynamicAuthWrapper app={app}>
<DynamicGithub app={app} title />
</DynamicAuthWrapper>
</PageWrapper>
)
}
/*
* Each page MUST be wrapped in the PageWrapper component.
* You also MUST spread props.page into this wrapper component
* when path and locale come from static props (as here)
* or set them manually.
*/
const AccountPage = ({ page }) => (
<PageWrapper {...page}>
<DynamicAuthWrapper>
<DynamicGithub title />
</DynamicAuthWrapper>
</PageWrapper>
)
export default AccountPage
@ -44,6 +44,7 @@ export async function getStaticProps({ locale }) {
props: {
...(await serverSideTranslations(locale, namespaces)),
page: {
locale,
path: ['account', 'github'],
},
},

View file

@ -1,5 +1,3 @@
// Hooks
import { useApp } from 'shared/hooks/use-app.mjs'
// Dependencies
import dynamic from 'next/dynamic'
import { serverSideTranslations } from 'next-i18next/serverSideTranslations'
@ -25,17 +23,19 @@ const DynamicImg = dynamic(
{ ssr: false }
)
const AccountPage = (props) => {
const app = useApp(props)
return (
<PageWrapper app={app}>
<DynamicAuthWrapper app={app}>
<DynamicImg app={app} title />
</DynamicAuthWrapper>
</PageWrapper>
)
}
/*
* Each page MUST be wrapped in the PageWrapper component.
* You also MUST spread props.page into this wrapper component
* when path and locale come from static props (as here)
* or set them manually.
*/
const AccountPage = ({ page }) => (
<PageWrapper {...page}>
<DynamicAuthWrapper>
<DynamicImg title />
</DynamicAuthWrapper>
</PageWrapper>
)
export default AccountPage
@ -44,6 +44,7 @@ export async function getStaticProps({ locale }) {
props: {
...(await serverSideTranslations(locale, namespaces)),
page: {
locale,
path: ['account', 'img'],
},
},

View file

@ -1,5 +1,3 @@
// Hooks
import { useApp } from 'shared/hooks/use-app.mjs'
// Dependencies
import dynamic from 'next/dynamic'
import { serverSideTranslations } from 'next-i18next/serverSideTranslations'
@ -24,25 +22,22 @@ const DynamicAccountOverview = dynamic(
{ ssr: false }
)
const AccountPage = (props) => {
const app = useApp(props)
const AccountIndexPage = ({ page }) => (
<PageWrapper {...page}>
<DynamicAuthWrapper>
<DynamicAccountOverview />
</DynamicAuthWrapper>
</PageWrapper>
)
return (
<PageWrapper app={app}>
<DynamicAuthWrapper app={app}>
<DynamicAccountOverview app={app} />
</DynamicAuthWrapper>
</PageWrapper>
)
}
export default AccountPage
export default AccountIndexPage
export async function getStaticProps({ locale }) {
return {
props: {
...(await serverSideTranslations(locale, ns)),
page: {
locale,
path: ['account'],
},
},

View file

@ -1,5 +1,3 @@
// Hooks
import { useApp } from 'shared/hooks/use-app.mjs'
// Dependencies
import dynamic from 'next/dynamic'
import { serverSideTranslations } from 'next-i18next/serverSideTranslations'
@ -25,25 +23,28 @@ const DynamicLanguage = dynamic(
{ ssr: false }
)
const AccountPage = (props) => {
const app = useApp(props)
/*
* Each page MUST be wrapped in the PageWrapper component.
* You also MUST spread props.page into this wrapper component
* when path and locale come from static props (as here)
* or set them manually.
*/
const AccountLanguagePage = ({ page }) => (
<PageWrapper {...page}>
<DynamicAuthWrapper>
<DynamicLanguage title />
</DynamicAuthWrapper>
</PageWrapper>
)
return (
<PageWrapper app={app}>
<DynamicAuthWrapper app={app}>
<DynamicLanguage app={app} title />
</DynamicAuthWrapper>
</PageWrapper>
)
}
export default AccountPage
export default AccountLanguagePage
export async function getStaticProps({ locale }) {
return {
props: {
...(await serverSideTranslations(locale, namespaces)),
page: {
locale,
path: ['account', 'language'],
},
},

View file

@ -1,5 +1,3 @@
// Hooks
import { useApp } from 'shared/hooks/use-app.mjs'
// Dependencies
import dynamic from 'next/dynamic'
import { serverSideTranslations } from 'next-i18next/serverSideTranslations'
@ -25,25 +23,28 @@ const DynamicMfa = dynamic(
{ ssr: false }
)
const AccountPage = (props) => {
const app = useApp(props)
/*
* Each page MUST be wrapped in the PageWrapper component.
* You also MUST spread props.page into this wrapper component
* when path and locale come from static props (as here)
* or set them manually.
*/
const AccountMfaPage = ({ page }) => (
<PageWrapper {...page}>
<DynamicAuthWrapper>
<DynamicMfa title />
</DynamicAuthWrapper>
</PageWrapper>
)
return (
<PageWrapper app={app}>
<DynamicAuthWrapper app={app}>
<DynamicMfa app={app} title />
</DynamicAuthWrapper>
</PageWrapper>
)
}
export default AccountPage
export default AccountMfaPage
export async function getStaticProps({ locale }) {
return {
props: {
...(await serverSideTranslations(locale, namespaces)),
page: {
locale,
path: ['account', 'mfa'],
},
},

View file

@ -1,5 +1,3 @@
// Hooks
import { useApp } from 'shared/hooks/use-app.mjs'
// Dependencies
import dynamic from 'next/dynamic'
import { serverSideTranslations } from 'next-i18next/serverSideTranslations'
@ -25,25 +23,28 @@ const DynamicNewsletter = dynamic(
{ ssr: false }
)
const AccountPage = (props) => {
const app = useApp(props)
/*
* Each page MUST be wrapped in the PageWrapper component.
* You also MUST spread props.page into this wrapper component
* when path and locale come from static props (as here)
* or set them manually.
*/
const AccountNewsletterPage = ({ page }) => (
<PageWrapper {...page}>
<DynamicAuthWrapper>
<DynamicNewsletter title />
</DynamicAuthWrapper>
</PageWrapper>
)
return (
<PageWrapper app={app}>
<DynamicAuthWrapper app={app}>
<DynamicNewsletter app={app} title />
</DynamicAuthWrapper>
</PageWrapper>
)
}
export default AccountPage
export default AccountNewsletterPage
export async function getStaticProps({ locale }) {
return {
props: {
...(await serverSideTranslations(locale, namespaces)),
page: {
locale,
path: ['account', 'newsletter'],
},
},

View file

@ -1,5 +1,3 @@
// Hooks
import { useApp } from 'shared/hooks/use-app.mjs'
// Dependencies
import dynamic from 'next/dynamic'
import { serverSideTranslations } from 'next-i18next/serverSideTranslations'
@ -25,25 +23,28 @@ const DynamicPassword = dynamic(
{ ssr: false }
)
const AccountPage = (props) => {
const app = useApp(props)
/*
* Each page MUST be wrapped in the PageWrapper component.
* You also MUST spread props.page into this wrapper component
* when path and locale come from static props (as here)
* or set them manually.
*/
const AccountPasswordPage = ({ page }) => (
<PageWrapper {...page}>
<DynamicAuthWrapper>
<DynamicPassword title />
</DynamicAuthWrapper>
</PageWrapper>
)
return (
<PageWrapper app={app}>
<DynamicAuthWrapper app={app}>
<DynamicPassword app={app} title />
</DynamicAuthWrapper>
</PageWrapper>
)
}
export default AccountPage
export default AccountPasswordPage
export async function getStaticProps({ locale }) {
return {
props: {
...(await serverSideTranslations(locale, namespaces)),
page: {
locale,
path: ['account', 'password'],
},
},

View file

@ -1,5 +1,3 @@
// Hooks
import { useApp } from 'shared/hooks/use-app.mjs'
// Dependencies
import dynamic from 'next/dynamic'
import { serverSideTranslations } from 'next-i18next/serverSideTranslations'
@ -25,25 +23,28 @@ const DynamicReload = dynamic(
{ ssr: false }
)
const AccountPage = (props) => {
const app = useApp(props)
/*
* Each page MUST be wrapped in the PageWrapper component.
* You also MUST spread props.page into this wrapper component
* when path and locale come from static props (as here)
* or set them manually.
*/
const AccountReloadPage = ({ page }) => (
<PageWrapper {...page}>
<DynamicAuthWrapper>
<DynamicReload title />
</DynamicAuthWrapper>
</PageWrapper>
)
return (
<PageWrapper app={app}>
<DynamicAuthWrapper app={app}>
<DynamicReload app={app} title />
</DynamicAuthWrapper>
</PageWrapper>
)
}
export default AccountPage
export default AccountReloadPage
export async function getStaticProps({ locale }) {
return {
props: {
...(await serverSideTranslations(locale, namespaces)),
page: {
locale,
path: ['account', 'reload'],
},
},

View file

@ -1,5 +1,3 @@
// Hooks
import { useApp } from 'shared/hooks/use-app.mjs'
// Dependencies
import dynamic from 'next/dynamic'
import { serverSideTranslations } from 'next-i18next/serverSideTranslations'
@ -25,25 +23,28 @@ const DynamicSets = dynamic(
{ ssr: false }
)
const AccountPage = (props) => {
const app = useApp(props)
/*
* Each page MUST be wrapped in the PageWrapper component.
* You also MUST spread props.page into this wrapper component
* when path and locale come from static props (as here)
* or set them manually.
*/
const AccountSetsPage = ({ page }) => (
<PageWrapper {...page}>
<DynamicAuthWrapper>
<DynamicSets />
</DynamicAuthWrapper>
</PageWrapper>
)
return (
<PageWrapper app={app}>
<DynamicAuthWrapper app={app}>
<DynamicSets app={app} />
</DynamicAuthWrapper>
</PageWrapper>
)
}
export default AccountPage
export default AccountSetsPage
export async function getStaticProps({ locale }) {
return {
props: {
...(await serverSideTranslations(locale, namespaces)),
page: {
locale,
path: ['account', 'sets'],
},
},

View file

@ -1,5 +1,3 @@
// Hooks
import { useApp } from 'shared/hooks/use-app.mjs'
// Dependencies
import dynamic from 'next/dynamic'
import { serverSideTranslations } from 'next-i18next/serverSideTranslations'
@ -25,25 +23,28 @@ const DynamicImperial = dynamic(
{ ssr: false }
)
const AccountPage = (props) => {
const app = useApp(props)
/*
* Each page MUST be wrapped in the PageWrapper component.
* You also MUST spread props.page into this wrapper component
* when path and locale come from static props (as here)
* or set them manually.
*/
const AccountUnitsPage = ({ page }) => (
<PageWrapper {...page}>
<DynamicAuthWrapper>
<DynamicImperial title />
</DynamicAuthWrapper>
</PageWrapper>
)
return (
<PageWrapper app={app}>
<DynamicAuthWrapper app={app}>
<DynamicImperial app={app} title />
</DynamicAuthWrapper>
</PageWrapper>
)
}
export default AccountPage
export default AccountUnitsPage
export async function getStaticProps({ locale }) {
return {
props: {
...(await serverSideTranslations(locale, namespaces)),
page: {
locale,
path: ['account', 'units'],
},
},

View file

@ -1,5 +1,3 @@
// Hooks
import { useApp } from 'shared/hooks/use-app.mjs'
// Dependencies
import dynamic from 'next/dynamic'
import { serverSideTranslations } from 'next-i18next/serverSideTranslations'
@ -25,17 +23,19 @@ const DynamicUsername = dynamic(
{ ssr: false }
)
const AccountPage = (props) => {
const app = useApp(props)
return (
<PageWrapper app={app}>
<DynamicAuthWrapper app={app}>
<DynamicUsername app={app} title />
</DynamicAuthWrapper>
</PageWrapper>
)
}
/*
* Each page MUST be wrapped in the PageWrapper component.
* You also MUST spread props.page into this wrapper component
* when path and locale come from static props (as here)
* or set them manually.
*/
const AccountPage = ({ page }) => (
<PageWrapper {...page}>
<DynamicAuthWrapper>
<DynamicUsername title />
</DynamicAuthWrapper>
</PageWrapper>
)
export default AccountPage
@ -44,6 +44,7 @@ export async function getStaticProps({ locale }) {
props: {
...(await serverSideTranslations(locale, namespaces)),
page: {
locale,
path: ['account', 'username'],
},
},

View file

@ -1,5 +1,3 @@
// Hooks
import { useApp } from 'shared/hooks/use-app.mjs'
// Dependencies
import { serverSideTranslations } from 'next-i18next/serverSideTranslations'
// Components
@ -9,25 +7,28 @@ import { V3Wip } from 'shared/components/v3-wip.mjs'
// Translation namespaces used on this page
const namespaces = [...new Set(['designs', ...pageNs])]
const DesignsPage = (props) => {
const app = useApp(props)
/*
* Each page MUST be wrapped in the PageWrapper component.
* You also MUST spread props.page into this wrapper component
* when path and locale come from static props (as here)
* or set them manually.
*/
const BlogIndexPage = ({ page }) => (
<PageWrapper {...page}>
<div className="max-w-2xl">
<V3Wip />
</div>
</PageWrapper>
)
return (
<PageWrapper app={app}>
<div className="max-w-2xl">
<V3Wip />
</div>
</PageWrapper>
)
}
export default DesignsPage
export default BlogIndexPage
export async function getStaticProps({ locale }) {
return {
props: {
...(await serverSideTranslations(locale, namespaces)),
page: {
locale,
path: ['blog'],
},
},

View file

@ -1,5 +1,3 @@
// Hooks
import { useApp } from 'shared/hooks/use-app.mjs'
// Dependencies
import { serverSideTranslations } from 'next-i18next/serverSideTranslations'
// Components
@ -9,25 +7,28 @@ import { V3Wip } from 'shared/components/v3-wip.mjs'
// Translation namespaces used on this page
const namespaces = [...new Set(['community', ...pageNs])]
const CommunityPage = (props) => {
const app = useApp(props)
/*
* Each page MUST be wrapped in the PageWrapper component.
* You also MUST spread props.page into this wrapper component
* when path and locale come from static props (as here)
* or set them manually.
*/
const CommunityIndexPage = ({ page }) => (
<PageWrapper {...page}>
<div className="max-w-2xl">
<V3Wip />
</div>
</PageWrapper>
)
return (
<PageWrapper app={app}>
<div className="max-w-2xl">
<V3Wip />
</div>
</PageWrapper>
)
}
export default CommunityPage
export default CommunityIndexPage
export async function getStaticProps({ locale }) {
return {
props: {
...(await serverSideTranslations(locale, namespaces)),
page: {
locale,
path: ['community'],
},
},

View file

@ -1,11 +1,12 @@
// Hooks
import { useEffect, useState } from 'react'
import { useApp } from 'shared/hooks/use-app.mjs'
import { useEffect, useState, useContext } from 'react'
import { useBackend } from 'shared/hooks/use-backend.mjs'
import { useAccount } from 'shared/hooks/use-account.mjs'
import { useToast } from 'shared/hooks/use-toast.mjs'
import { useRouter } from 'next/router'
import { useTranslation } from 'next-i18next'
// Context
import { LoadingContext } from 'shared/context/loading-context.mjs'
// Dependencies
import { serverSideTranslations } from 'next-i18next/serverSideTranslations'
import Link from 'next/link'
@ -20,28 +21,27 @@ import { HelpIcon } from 'shared/components/icons.mjs'
// Translation namespaces used on this page
const ns = Array.from(new Set([...pageNs, 'account']))
const ConfirmSignUpPage = (props) => {
const router = useRouter()
// Get confirmation ID and check from url
const [id, check] = router.asPath.slice(1).split('/').slice(2)
const ConfirmSignUpPage = ({ page }) => {
// Context
const { loading, startLoading, stopLoading } = useContext(LoadingContext)
const app = useApp({
...props,
page: {
path: ['confirm', 'emailchange', id],
},
})
// Hooks
const { setAccount, setToken, token } = useAccount()
const backend = useBackend()
const toast = useToast()
const { t } = useTranslation(ns)
const router = useRouter()
// Get confirmation ID and check from url
const [id, check] = router.asPath.slice(1).split('/').slice(2)
// State
const [error, setError] = useState(false)
// Effects
useEffect(() => {
// Async inside useEffect requires this approach
const confirmEmail = async () => {
app.startLoading()
startLoading()
const confirmation = await backend.loadConfirmation({ id, check })
if (confirmation?.result === 'success' && confirmation.confirmation) {
const result = await backend.updateAccount({
@ -52,16 +52,16 @@ const ConfirmSignUpPage = (props) => {
if (result.success) {
setAccount(result.data.account)
setToken(result.data.token)
app.stopLoading()
stopLoading()
setError(false)
toast.for.settingsSaved()
router.push('/account')
} else {
app.stopLoading()
stopLoading()
setError(true)
}
} else {
app.stopLoading()
stopLoading()
setError(true)
}
}
@ -69,10 +69,14 @@ const ConfirmSignUpPage = (props) => {
if (token) confirmEmail()
}, [id, check, token])
// Update path with dynamic ID
if (!page) return null
if (page) page.path = ['confirm', 'emailchange', id]
// Short-circuit errors
if (error)
return (
<PageWrapper app={app} title={t('account:politeOhCrap')} layout={BareLayout} footer={false}>
<PageWrapper {...page} title={t('account:politeOhCrap')} layout={BareLayout} footer={false}>
<div className="max-w-md flex flex-col items-center m-auto justify-center h-screen text-center">
<h1 className="text-center">{t('account:politeOhCrap')}</h1>
<Robot pose="ohno" className="w-48" embed />
@ -88,7 +92,7 @@ const ConfirmSignUpPage = (props) => {
)
return (
<PageWrapper app={app} title={t('account:oneMomentPlease')} layout={BareLayout} footer={false}>
<PageWrapper {...page} title={t('account:oneMomentPlease')} layout={BareLayout} footer={false}>
<div className="max-w-md flex flex-col items-center m-auto justify-center h-screen text-center">
<h1 className="text-center">{t('account:oneMomentPlease')}</h1>
<p className="text-center">
@ -105,6 +109,7 @@ export async function getStaticProps({ locale }) {
return {
props: {
...(await serverSideTranslations(locale, ns)),
page: { locale },
},
}
}

View file

@ -1,6 +1,5 @@
// Hooks
import { useEffect, useState } from 'react'
import { useApp } from 'shared/hooks/use-app.mjs'
import { useAccount } from 'shared/hooks/use-account.mjs'
import { useBackend } from 'shared/hooks/use-backend.mjs'
import { useRouter } from 'next/router'
@ -37,25 +36,23 @@ export const SigninLinkExpired = () => {
)
}
const Wrapper = ({ app, t, children }) => (
<PageWrapper app={app} title={t('signin:oneMomentPlease')} layout={BareLayout} footer={false}>
const Wrapper = ({ page, t, children }) => (
<PageWrapper {...page} title={t('signin:oneMomentPlease')} layout={BareLayout} footer={false}>
<section className="m-0 p-0 w-full">
<div className="mt-4 lg:mt-32 max-w-xl m-auto">{children}</div>
</section>
</PageWrapper>
)
const ConfirmSignInPage = (props) => {
/*
* Each page MUST be wrapped in the PageWrapper component.
* You also MUST spread props.page into this wrapper component
* and set path and locale when it's dynamic (like on this page)
*/
const ConfirmSignInPage = ({ page }) => {
const router = useRouter()
// Get confirmation ID and check from url
const [confirmationId, confirmationCheck] = router.asPath.slice(1).split('/').slice(2)
const app = useApp({
...props,
page: {
path: ['confirm', 'emailchange', confirmationId],
},
})
const { setAccount, setToken } = useAccount()
const backend = useBackend()
const { t } = useTranslation(ns)
@ -90,16 +87,18 @@ const ConfirmSignInPage = (props) => {
getConfirmation()
}, [])
if (page) page.path = ['confirm', 'emailchange', confirmationId]
// Short-circuit errors
if (error)
return (
<Wrapper app={app} t={t}>
<Wrapper page={page} t={t}>
<SigninLinkExpired />
</Wrapper>
)
return (
<Wrapper app={app} t={t}>
<Wrapper page={page} t={t}>
<h1>{t('oneMomentPlease')}</h1>
<Spinner className="w-8 h-8 m-auto animate-spin" />
</Wrapper>

View file

@ -1,6 +1,5 @@
// Hooks
import { useEffect, useState } from 'react'
import { useApp } from 'shared/hooks/use-app.mjs'
import { useAccount } from 'shared/hooks/use-account.mjs'
import { useBackend } from 'shared/hooks/use-backend.mjs'
import { useRouter } from 'next/router'
@ -24,215 +23,8 @@ import {
// Translation namespaces used on this page
const ns = Array.from(new Set([...pageNs, ...layoutNs, ...gdprNs, 'confirm', 'locales', 'themes']))
export const SignupLinkExpired = () => {
const { t } = useTranslation('confirm')
return (
<div className="p-8 max-w-md">
<h1 className="text-center">{t('signupLinkExpired')}</h1>
<Robot pose="shrug" className="w-full" embed />
<Link className="btn btn-primary btn-lg w-full" href="/signup">
{t('signupAgain')}
</Link>
</div>
)
}
const Checkbox = ({ value, setter, label, children = null }) => (
<div
className={`form-control p-4 hover:cursor-pointer rounded border-l-8 my-2
${value ? 'border-success bg-success' : 'border-error bg-error'}
bg-opacity-10 shadow`}
onClick={() => setter(value ? false : true)}
>
<div className="form-control flex flex-row items-center gap-2">
<input
type="checkbox"
className="checkbox"
checked={value ? 'checked' : ''}
onChange={() => setter(value ? false : true)}
/>
<span className="label-text">{label}</span>
</div>
{children}
</div>
)
const ConfirmSignUpPage = (props) => {
const router = useRouter()
// Get confirmation ID and check from url
const [confirmationId, confirmationCheck] = router.asPath.slice(1).split('/').slice(2)
const app = useApp({
...props,
page: {
path: ['confirm', 'emailchange', confirmationId],
},
})
const { setAccount, setToken } = useAccount()
const backend = useBackend()
const { t } = useTranslation(ns)
const [id, setId] = useState(false)
const [pDetails, setPDetails] = useState(false)
const [mDetails, setMDetails] = useState(false)
const [profile, setProfile] = useState(false)
const [measurements, setMeasurements] = useState(false)
const [openData, setOpenData] = useState(true)
const [ready, setReady] = useState(false)
const [error, setError] = useState(false)
const createAccount = async () => {
let consent = 0
if (profile) consent = 1
if (profile && measurements) consent = 2
if (profile && measurements && openData) consent = 3
if (consent > 0 && id) {
const result = await backend.confirmSignup({ consent, id })
if (result.success) {
setToken(result.data.token)
setAccount(result.data.account)
router.push('/welcome')
} else {
// Something went wrong
console.log('something went wrong')
}
}
}
useEffect(() => {
// Async inside useEffect requires this approach
const getConfirmation = async () => {
// Reach out to backend
const data = await backend.loadConfirmation({
id: confirmationId,
check: confirmationCheck,
})
if (data instanceof Error) setError(true)
setReady(true)
setId(confirmationId)
}
// Call async method
getConfirmation()
}, [])
// Short-circuit errors
if (error)
return (
<PageWrapper app={app} title={t('joinFreeSewing')} layout={BareLayout} footer={false}>
<WelcomeWrapper theme={app.theme}>
<SignupLinkExpired />
</WelcomeWrapper>
</PageWrapper>
)
const partA = (
<>
<h5 className="mt-8">{t('profileQuestion')}</h5>
{pDetails ? <GdprProfileDetails /> : null}
{profile ? (
<Checkbox value={profile} setter={setProfile} label={t('yesIDo')} />
) : (
<>
<button
className="btn btn-primary btn-lg w-full mt-4 flex flex-row items-center justify-between"
onClick={() => setProfile(!profile)}
>
{t('clickHere')}
<span>1/2</span>
</button>
<p className="text-center">
<button
className="btn btn-neutral btn-ghost btn-sm"
onClick={() => setPDetails(!pDetails)}
>
{t(pDetails ? 'hideDetails' : 'showDetails')}
</button>
</p>
<Popout warning>{t('noConsentNoAccount')}</Popout>
</>
)}
</>
)
const partB = (
<>
<h5 className="mt-8">{t('peopleQuestion')}</h5>
{mDetails ? <GdprMeasurementsDetails /> : null}
{measurements ? (
<Checkbox value={measurements} setter={setMeasurements} label={t('yesIDo')} />
) : (
<button
className="btn btn-primary btn-lg w-full mt-4 flex flex-row items-center justify-between"
onClick={() => setMeasurements(!measurements)}
>
{t('clickHere')}
<span>2/2</span>
</button>
)}
{mDetails && measurements ? (
<Checkbox value={openData} setter={setOpenData} label={t('openDataQuestion')} />
) : null}
{measurements && !openData ? <Popout note>{t('openDataInfo')}</Popout> : null}
{!measurements && (
<>
<p className="text-center">
<button
className="btn btn-neutral btn-ghost btn-sm"
onClick={() => setMDetails(!mDetails)}
>
{t(mDetails ? 'hideDetails' : 'showDetails')}
</button>
</p>
<Popout warning>{t('noConsentNoPatterns')}</Popout>
</>
)}
</>
)
return (
<PageWrapper app={app} title={t('joinFreeSewing')} layout={BareLayout} footer={false}>
<WelcomeWrapper theme={app.theme}>
{ready ? (
<>
<h1>{t('privacyMatters')}</h1>
<p>{t('compliant')}</p>
<p>{t('consentWhyAnswer')}</p>
{partA}
{profile && partB}
</>
) : (
<Spinner className="w-8 h-8 m-auto" />
)}
{profile && !measurements && (
<button
className="btn btn-primary btn-outline btn-lg w-full mt-4"
onClick={createAccount}
>
{t('createLimitedAccount')}
</button>
)}
{profile && measurements && (
<button
onClick={createAccount}
className={`btn btn-lg w-full mt-8 ${app.state.loading ? 'btn-accent' : 'btn-primary'}`}
>
{app.state.loading ? (
<>
<Spinner />
<span>{t('processing')}</span>
</>
) : (
<span>{t('createAccount')}</span>
)}
</button>
)}
<p className="text-center opacity-50 mt-12">
<Link href="/docs/various/privacy" className="hover:text-secondary underline">
FreeSewing Privacy Notice
</Link>
</p>
</WelcomeWrapper>
</PageWrapper>
)
return <PageWrapper></PageWrapper>
}
export default ConfirmSignUpPage

View file

@ -1,5 +1,3 @@
// Hooks
import { useApp } from 'shared/hooks/use-app.mjs'
// Dependencies
import { serverSideTranslations } from 'next-i18next/serverSideTranslations'
// Components
@ -9,17 +7,19 @@ import { V3Wip } from 'shared/components/v3-wip.mjs'
// Translation namespaces used on this page
const namespaces = [...new Set(['designs', ...pageNs])]
const DesignsPage = (props) => {
const app = useApp(props)
return (
<PageWrapper app={app}>
<div className="max-w-2xl">
<V3Wip />
</div>
</PageWrapper>
)
}
/*
* Each page MUST be wrapped in the PageWrapper component.
* You also MUST spread props.page into this wrapper component
* when path and locale come from static props (as here)
* or set them manually.
*/
const DesignsPage = ({ page }) => (
<PageWrapper {...page}>
<div className="max-w-2xl">
<V3Wip />
</div>
</PageWrapper>
)
export default DesignsPage
@ -28,6 +28,7 @@ export async function getStaticProps({ locale }) {
props: {
...(await serverSideTranslations(locale, namespaces)),
page: {
locale,
path: ['designs'],
},
},

View file

@ -1,5 +1,3 @@
// Hooks
import { useApp } from 'shared/hooks/use-app.mjs'
// Dependencies
import Head from 'next/head'
import { serverSideTranslations } from 'next-i18next/serverSideTranslations'
@ -13,52 +11,44 @@ import { TocWrapper } from 'shared/components/wrappers/toc.mjs'
import { PageWrapper } from 'shared/components/wrappers/page.mjs'
import { components } from 'site/components/mdx/index.mjs'
const MdxPage = (props) => {
// This hook is used for shared code and global state
const app = useApp(props)
const title = props.page.title
const fullTitle = title + ' - FreeSewing.org'
/*
* Each page should be wrapped in the Page wrapper component
* You MUST pass it the result of useApp() as the `app` prop
* and for MDX pages you can spread the props.page props to it
* to automatically set the title and navigation
*
* Like breadcrumbs and updating the primary navigation with
* active state
*/
return (
<PageWrapper app={app} {...props.page}>
<Head>
<meta property="og:title" content={props.page.title} key="title" />
<meta property="og:type" content="article" key="type" />
<meta property="og:description" content={props.intro} key="type" />
<meta property="og:article:author" content="Joost De Cock" key="author" />
<meta
property="og:image"
content={`https://canary.backend.freesewing.org/og-img/en/dev/${props.page.slug}`}
key="image"
/>
<meta property="og:image:type" content="image/png" />
<meta property="og:image:width" content="1200" />
<meta property="og:image:height" content="630" />
<meta property="og:url" content={`https://freesewing.dev/${props.page.slug}`} key="url" />
<meta property="og:locale" content="en_US" key="locale" />
<meta property="og:site_name" content="freesewing.dev" key="site" />
<title>{fullTitle}</title>
</Head>
<div className="flex flex-row-reverse flex-wrap xl:flex-nowrap justify-end">
{props.toc && (
<div className="mb-8 w-full xl:w-80 2xl:w-96 xl:pl-8 2xl:pl-16">
<TocWrapper toc={props.toc} app={app} />
</div>
)}
<MdxWrapper mdx={props.mdx} app={app} components={components} />
/*
* Each page MUST be wrapped in the PageWrapper component.
* You also MUST spread props.page into this wrapper component
* when path and locale come from static props (as here)
* or set them manually.
*/
const MdxPage = ({ page, mdx, toc, intro }) => (
<PageWrapper {...page}>
<Head>
<meta property="og:title" content={page.t} key="title" />
<meta property="og:type" content="article" key="type" />
<meta property="og:description" content={intro} key="type" />
<meta property="og:article:author" content="Joost De Cock" key="author" />
<meta
property="og:image"
content={`https://canary.backend.freesewing.org/og-img/en/dev/${page.slug}`}
key="image"
/>
<meta property="og:image:type" content="image/png" />
<meta property="og:image:width" content="1200" />
<meta property="og:image:height" content="630" />
<meta property="og:url" content={`https://freesewing.dev/${page.slug}`} key="url" />
<meta property="og:locale" content="en_US" key="locale" />
<meta property="og:site_name" content="freesewing.dev" key="site" />
<title>{typeof page.t === 'string' ? page.t : ''} - FreeSewing.org</title>
</Head>
<div className="flex flex-row-reverse flex-wrap xl:flex-nowrap justify-end">
{toc && (
<div className="mb-8 w-full xl:w-80 2xl:w-96 xl:pl-8 2xl:pl-16">
<TocWrapper toc={toc} />
</div>
)}
<div className="max-w-prose">
<MdxWrapper mdx={mdx} components={components} />
</div>
</PageWrapper>
)
}
</div>
</PageWrapper>
)
/*
* Default export is the NextJS page object
@ -92,6 +82,7 @@ export async function getStaticProps({ params, locale }) {
intro: intro.join(' '),
page: {
path, // path to page as array
locale,
...mdxMeta[locale][path.join('/')],
},
params,

View file

@ -1,5 +1,3 @@
// Hooks
import { useApp } from 'shared/hooks/use-app.mjs'
// Dependencies
import Head from 'next/head'
import { serverSideTranslations } from 'next-i18next/serverSideTranslations'
@ -9,60 +7,55 @@ import mdxMeta from 'site/prebuild/mdx.js'
// Components
import { MdxWrapper } from 'shared/components/wrappers/mdx.mjs'
import { TocWrapper } from 'shared/components/wrappers/toc.mjs'
import { PageWrapper } from 'shared/components/wrappers/page.mjs'
import { PageWrapper, ns as pageNs } from 'shared/components/wrappers/page.mjs'
import { components } from 'site/components/mdx/index.mjs'
const MdxPage = (props) => {
// This hook is used for shared code and global state
const app = useApp(props)
const title = props.page.title
const fullTitle = title + ' - FreeSewing.org'
// Translation namespaces used on this page
const namespaces = [...new Set(['docs', ...pageNs])]
/*
* Each page should be wrapped in the Page wrapper component
* You MUST pass it the result of useApp() as the `app` prop
* and for MDX pages you can spread the props.page props to it
* to automatically set the title and navigation
*
* Like breadcrumbs and updating the primary navigation with
* active state
*/
return (
<PageWrapper app={app} {...props.page}>
<Head>
<meta property="og:title" content={props.page.title} key="title" />
<meta property="og:type" content="article" key="type" />
<meta property="og:description" content={props.intro} key="type" />
<meta property="og:article:author" content="Joost De Cock" key="author" />
<meta
property="og:image"
content={`https://canary.backend.freesewing.org/og-img/en/dev/${props.page.slug}`}
key="image"
/>
<meta property="og:image:type" content="image/png" />
<meta property="og:image:width" content="1200" />
<meta property="og:image:height" content="630" />
<meta property="og:url" content={`https://freesewing.dev/${props.page.slug}`} key="url" />
<meta property="og:locale" content="en_US" key="locale" />
<meta property="og:site_name" content="freesewing.dev" key="site" />
<title>{fullTitle}</title>
</Head>
<div className="flex flex-row-reverse flex-wrap xl:flex-nowrap justify-end">
{props.toc && (
<div className="mb-8 w-full xl:w-80 2xl:w-96 xl:pl-8 2xl:pl-16">
<TocWrapper toc={props.toc} app={app} />
</div>
)}
<MdxWrapper mdx={props.mdx} app={app} components={components} />
/*
* Each page MUST be wrapped in the PageWrapper component.
* You also MUST spread props.page into this wrapper component
* when path and locale come from static props (as here)
* or set them manually.
*/
const DocsIndexPage = ({ page, mdx, toc, intro }) => (
<PageWrapper {...page}>
<Head>
<meta property="og:title" content={page.t} key="title" />
<meta property="og:type" content="article" key="type" />
<meta property="og:description" content={intro} key="type" />
<meta property="og:article:author" content="Joost De Cock" key="author" />
<meta
property="og:image"
content={`https://canary.backend.freesewing.org/og-img/en/dev/${page.slug}`}
key="image"
/>
<meta property="og:image:type" content="image/png" />
<meta property="og:image:width" content="1200" />
<meta property="og:image:height" content="630" />
<meta property="og:url" content={`https://freesewing.dev/${page.slug}`} key="url" />
<meta property="og:locale" content="en_US" key="locale" />
<meta property="og:site_name" content="freesewing.dev" key="site" />
<title>{typeof page.t === 'string' ? page.t : ''} - FreeSewing.org</title>
</Head>
<div className="flex flex-row-reverse flex-wrap xl:flex-nowrap justify-end">
{toc && (
<div className="mb-8 w-full xl:w-80 2xl:w-96 xl:pl-8 2xl:pl-16">
<TocWrapper toc={toc} />
</div>
)}
<div className="max-w-prose">
<MdxWrapper mdx={mdx} components={components} />
</div>
</PageWrapper>
)
}
</div>
</PageWrapper>
)
/*
* Default export is the NextJS page object
*/
export default MdxPage
export default DocsIndexPage
/*
* getStaticProps() is used to fetch data at build-time.
@ -85,9 +78,10 @@ export async function getStaticProps({ locale }) {
intro: intro.join(' '),
page: {
path: ['docs'],
locale,
...mdxMeta[locale]['docs'],
},
...(await serverSideTranslations(locale)),
...(await serverSideTranslations(locale, namespaces)),
},
}
}

View file

@ -1,5 +1,3 @@
// Hooks
import { useApp } from 'shared/hooks/use-app.mjs'
// Dependencies
import { serverSideTranslations } from 'next-i18next/serverSideTranslations'
//import { useTranslation } from 'next-i18next'
@ -10,52 +8,50 @@ import { Popout } from 'shared/components/popout.mjs'
import { BareLayout } from 'site/components/layouts/bare.mjs'
import { PageLink } from 'shared/components/page-link.mjs'
const HomePage = (props) => {
const app = useApp(props)
const title = 'Welcome to FreeSewing.org'
// Not using translation for now
// const { t } = useTranslation(['homepage', 'ograph'])
return (
<PageWrapper app={app} title={title} layout={BareLayout}>
<Head>
<title>{title}</title>
</Head>
<div>
<div className="max-w-xl m-auto my-32 px-6">
<Popout fixme>
Create homepage. Meanwhile check <PageLink href="/signup" txt="the signup flow" />
</Popout>
<h2>What is FreeSewing?</h2>
<small>(by ChatGPT)</small>
<p>
Freesewing is an open-source pattern making software that allows users to generate
custom sewing patterns based on their own measurements. It is designed to be flexible
and customizable, and can be used to create a wide range of garments, from simple
t-shirts and skirts to more complex dresses and jackets.
</p>
<p>
Freesewing is available for free, and users can access a wide range of pre-made patterns
or create their own from scratch. The software is designed to be easy to use, with an
intuitive interface that guides users through the process of creating a pattern
step-by-step.
</p>
<p>
In addition to the pattern making software, freesewing also has an active online
community of sewists and pattern makers who share tips, techniques, and advice on all
aspects of sewing. The community also collaborates on creating new patterns and
improving existing ones, and users can contribute their own patterns to the project as
well.
</p>
<p>
Overall, freesewing is a powerful tool for anyone interested in sewing and pattern
making, whether they are seasoned professionals or beginners just starting out.
</p>
</div>
/*
* Each page MUST be wrapped in the PageWrapper component.
* You also MUST spread props.page into this wrapper component
* when path and locale come from static props (as here)
* or set them manually.
*/
const HomePage = ({ page }) => (
<PageWrapper {...page} layout={BareLayout}>
<Head>
<title>Welcome to FreeSewing.org</title>
</Head>
<div>
<div className="max-w-xl m-auto my-32 px-6">
<Popout fixme>
Create homepage. Meanwhile check <PageLink href="/signup" txt="the signup flow" />
</Popout>
<h2>What is FreeSewing?</h2>
<small>(by ChatGPT)</small>
<p>
Freesewing is an open-source pattern making software that allows users to generate custom
sewing patterns based on their own measurements. It is designed to be flexible and
customizable, and can be used to create a wide range of garments, from simple t-shirts and
skirts to more complex dresses and jackets.
</p>
<p>
Freesewing is available for free, and users can access a wide range of pre-made patterns
or create their own from scratch. The software is designed to be easy to use, with an
intuitive interface that guides users through the process of creating a pattern
step-by-step.
</p>
<p>
In addition to the pattern making software, freesewing also has an active online community
of sewists and pattern makers who share tips, techniques, and advice on all aspects of
sewing. The community also collaborates on creating new patterns and improving existing
ones, and users can contribute their own patterns to the project as well.
</p>
<p>
Overall, freesewing is a powerful tool for anyone interested in sewing and pattern making,
whether they are seasoned professionals or beginners just starting out.
</p>
</div>
</PageWrapper>
)
}
</div>
</PageWrapper>
)
export default HomePage
@ -64,6 +60,7 @@ export async function getStaticProps({ locale }) {
props: {
...(await serverSideTranslations(locale)),
page: {
locale,
path: [],
},
},

View file

@ -1,5 +1,3 @@
// Hooks
import { useApp } from 'shared/hooks/use-app.mjs'
// Dependencies
import dynamic from 'next/dynamic'
import { serverSideTranslations } from 'next-i18next/serverSideTranslations'
@ -25,25 +23,28 @@ const DynamicNewApikey = dynamic(
{ ssr: false }
)
const AccountPage = (props) => {
const app = useApp(props)
/*
* Each page MUST be wrapped in the PageWrapper component.
* You also MUST spread props.page into this wrapper component
* when path and locale come from static props (as here)
* or set them manually.
*/
const NewApikeyPage = ({ page }) => (
<PageWrapper {...page}>
<DynamicAuthWrapper>
<DynamicNewApikey standAlone />
</DynamicAuthWrapper>
</PageWrapper>
)
return (
<PageWrapper app={app}>
<DynamicAuthWrapper app={app}>
<DynamicNewApikey app={app} standAlone />
</DynamicAuthWrapper>
</PageWrapper>
)
}
export default AccountPage
export default NewApikeyPage
export async function getStaticProps({ locale }) {
return {
props: {
...(await serverSideTranslations(locale, namespaces)),
page: {
locale,
path: ['new', 'apikey'],
},
},

View file

@ -1,5 +1,3 @@
// Hooks
import { useApp } from 'shared/hooks/use-app.mjs'
// Dependencies
import { serverSideTranslations } from 'next-i18next/serverSideTranslations'
// Components
@ -10,30 +8,33 @@ import { Popout } from 'shared/components/popout.mjs'
// Note that we include the account namespace here for the 'new' keyword
const namespaces = [...pageNs, 'account']
const AccountPage = (props) => {
const app = useApp(props)
/*
* Each page MUST be wrapped in the PageWrapper component.
* You also MUST spread props.page into this wrapper component
* when path and locale come from static props (as here)
* or set them manually.
*/
const NewIndexPage = ({ page }) => (
<PageWrapper {...page}>
<div className="max-w-lg">
<Popout fixme>
<h5>This needs an umbrella page</h5>
<p>
We need to create content here linking to all the <em>new something</em> pages
</p>
</Popout>
</div>
</PageWrapper>
)
return (
<PageWrapper app={app}>
<div className="max-w-lg">
<Popout fixme>
<h5>This needs an umbrella page</h5>
<p>
We need to create content here linking to all the <em>new something</em> pages
</p>
</Popout>
</div>
</PageWrapper>
)
}
export default AccountPage
export default NewIndexPage
export async function getStaticProps({ locale }) {
return {
props: {
...(await serverSideTranslations(locale, namespaces)),
page: {
locale,
path: ['new'],
},
},

View file

@ -1,5 +1,4 @@
// Hooks
import { useApp } from 'shared/hooks/use-app.mjs'
import { useAccount } from 'shared/hooks/use-account.mjs'
// Dependencies
import dynamic from 'next/dynamic'
@ -28,14 +27,18 @@ const DynamicAccountProfile = dynamic(
{ ssr: false }
)
const AccountPage = (props) => {
const app = useApp(props)
/*
* Each page MUST be wrapped in the PageWrapper component.
* You also MUST spread props.page into this wrapper component
* when path and locale come from static props (as here)
* or set them manually.
*/
const ProfilePage = ({ page }) => {
const { account } = useAccount()
return (
<PageWrapper app={app}>
<DynamicAuthWrapper app={app}>
<DynamicAccountProfile app={app} />
<PageWrapper {...page}>
<DynamicAuthWrapper>
<DynamicAccountProfile />
<Popout link compact>
<PageLink href={`/users/${account.username}`} txt={`/users/${account.username}`} />
</Popout>
@ -45,13 +48,14 @@ const AccountPage = (props) => {
)
}
export default AccountPage
export default ProfilePage
export async function getStaticProps({ locale }) {
return {
props: {
...(await serverSideTranslations(locale, namespaces)),
page: {
locale,
path: ['profile'],
},
},

View file

@ -1,5 +1,3 @@
// Hooks
import { useApp } from 'shared/hooks/use-app.mjs'
// Dependencies
import { serverSideTranslations } from 'next-i18next/serverSideTranslations'
// Components
@ -7,19 +5,21 @@ import { PageWrapper, ns as pageNs } from 'shared/components/wrappers/page.mjs'
import { V3Wip } from 'shared/components/v3-wip.mjs'
// Translation namespaces used on this page
const namespaces = [...new Set(['designs', ...pageNs])]
const namespaces = [...new Set(['showcase', ...pageNs])]
const DesignsPage = (props) => {
const app = useApp(props)
return (
<PageWrapper app={app}>
<div className="max-w-2xl">
<V3Wip />
</div>
</PageWrapper>
)
}
/*
* Each page MUST be wrapped in the PageWrapper component.
* You also MUST spread props.page into this wrapper component
* when path and locale come from static props (as here)
* or set them manually.
*/
const DesignsPage = ({ page }) => (
<PageWrapper {...page}>
<div className="max-w-2xl">
<V3Wip />
</div>
</PageWrapper>
)
export default DesignsPage
@ -28,6 +28,7 @@ export async function getStaticProps({ locale }) {
props: {
...(await serverSideTranslations(locale, namespaces)),
page: {
locale,
path: ['showcase'],
},
},

View file

@ -1,11 +1,12 @@
// Hooks
import { useState, useEffect } from 'react'
import { useApp } from 'shared/hooks/use-app.mjs'
import { useState, useEffect, useContext } from 'react'
import { useAccount } from 'shared/hooks/use-account.mjs'
import { useTranslation } from 'next-i18next'
import { useBackend } from 'shared/hooks/use-backend.mjs'
import { useToast } from 'shared/hooks/use-toast.mjs'
import { useRouter } from 'next/router'
// Context
import { LoadingContext } from 'shared/context/loading-context.mjs'
// Dependencies
import { serverSideTranslations } from 'next-i18next/serverSideTranslations'
// Components
@ -55,11 +56,17 @@ export const ButtonText = ({ children }) => (
<div className="flex flex-row items-center justify-between w-full">{children}</div>
)
const SignInPage = (props) => {
const app = useApp(props)
const { setAccount, setToken } = useAccount()
/*
* Each page MUST be wrapped in the PageWrapper component.
* You also MUST spread props.page into this wrapper component
* when path and locale come from static props (as here)
* or set them manually.
*/
const SignInPage = ({ page }) => {
const { startLoading, stopLoading } = useContext(LoadingContext)
const { setAccount, setToken, seenUser, setSeenUser, clear } = useAccount()
const { t } = useTranslation(['signin', 'signup', 'toast'])
const backend = useBackend(app)
const backend = useBackend()
const toast = useToast()
const router = useRouter()
@ -69,8 +76,9 @@ const SignInPage = (props) => {
const [magicLink, setMagicLink] = useState(true)
const [signInFailed, setSignInFailed] = useState(false)
const [magicLinkSent, setMagicLinkSent] = useState(false)
const [seenBefore, setSeenBefore] = useState(false)
const clearUsername = () => app.setUsername(false)
const clearUsername = () => setUsername(false)
useEffect(() => {
if (typeof window !== 'undefined' && signInFailed) {
@ -78,9 +86,20 @@ const SignInPage = (props) => {
}
}, [signInFailed])
// Avoid SSR rendering mismatch by setting this in effect
useEffect(() => {
if (seenUser) {
setSeenBefore(seenUser)
setUsername(seenUser)
} else {
setSeenBefore(false)
setUsername('')
}
}, [seenUser])
const signinHandler = async (evt) => {
evt.preventDefault()
app.startLoading()
startLoading()
const result = magicLink
? await backend.signIn({ username, password: false })
: await backend.signIn({ username, password })
@ -93,7 +112,9 @@ const SignInPage = (props) => {
} else {
setAccount(result.data.account)
setToken(result.data.token)
msg = t('signin:welcomeName', { name: result.data.account.username })
setSeenUser(result.data.account.username)
msg = t('signin:welcomeBackName', { name: result.data.account.username })
stopLoading()
router.push('/account')
}
return toast.success(<b>{msg}</b>)
@ -116,7 +137,7 @@ const SignInPage = (props) => {
setSignInFailed(msg)
return toast.warning(<b>{msg}</b>)
}
app.stopLoading()
stopLoading()
}
const btnClasses = `btn capitalize w-full mt-4 ${
@ -132,8 +153,8 @@ const SignInPage = (props) => {
if (magicLinkSent)
return (
<PageWrapper app={app} title={t('welcomeBack')} layout={BareLayout} footer={false}>
<SusiWrapper theme={app.theme}>
<PageWrapper {...page} title={t('welcomeBack')} layout={BareLayout} footer={false}>
<SusiWrapper>
<h1 className="text-neutral-content font-light text-3xl mb-4 pb-0 text-center">
{t('signup:emailSent')}
</h1>
@ -154,15 +175,15 @@ const SignInPage = (props) => {
)
return (
<PageWrapper app={app} title={t('welcomeBack')} layout={BareLayout} footer={false}>
<SusiWrapper theme={app.theme}>
<PageWrapper {...page} title={t('welcomeBack')} layout={BareLayout} footer={false}>
<SusiWrapper>
<h1 className="text-neutral-content font-light text-3xl mb-4 pb-0 text-center">
{t('signin:welcomeName', { name: app.username || '' })}
{seenBefore ? t('signin:welcomeBackName', { name: seenUser }) : t('signin:welcome')}
</h1>
<p className="text-neutral-content text-center">
{t('signin:signInToThing', { thing: 'FreeSewing.org' })}
</p>
{!app.username && <UsernameField {...{ username, setUsername, t }} />}
{!seenBefore && <UsernameField {...{ username, setUsername, t }} />}
{magicLink ? (
<button className={btnClasses} tabIndex="-1" role="button" onClick={signinHandler}>
{signInFailed ? (
@ -190,29 +211,31 @@ const SignInPage = (props) => {
</button>
</>
)}
<ul className="mt-4 flex flex-row gap-2 text-sm items-center justify-center">
<ul className="mt-4 mb-2 flex flex-row gap-2 text-sm items-center justify-center">
<li>
<button className={darkLinkClasses} onClick={() => setMagicLink(!magicLink)}>
{magicLink ? t('signin:signInWith') : t('signin:emailSignInLink')}
{magicLink ? t('signin:usePassword') : t('signin:emailSignInLink')}
</button>
</li>
{app.username ? (
{seenBefore ? (
<>
<li>|</li>
<li>
<button className={darkLinkClasses} onClick={clearUsername}>
<button className={darkLinkClasses} onClick={() => setSeenUser(false)}>
Sign in as another user
</button>
</li>
</>
) : null}
</ul>
<p className="text-neutral-content text-sm mt-4 opacity-80 text-center">
{t('signin:dontHaveAnAccount')}{' '}
<Link className={darkLinkClasses} href="/signup">
{t('signin:signUpHere')}
</Link>
</p>
{!seenBefore ? (
<p className="text-neutral-content text-sm mt-4 opacity-80 text-center">
{t('signin:dontHaveAnAccount')}{' '}
<Link className={darkLinkClasses} href="/signup">
{t('signin:signUpHere')}
</Link>
</p>
) : null}
</SusiWrapper>
</PageWrapper>
)
@ -225,6 +248,7 @@ export async function getStaticProps({ locale }) {
props: {
...(await serverSideTranslations(locale)),
page: {
locale,
path: ['signin'],
},
},

View file

@ -1,10 +1,11 @@
welcomeName: "Welcome { name }"
welcome: Welcome
welcomeBackName: "Welcome back { name }"
emailUsernameId: "Your Email address, Username, or User #"
password: Your Password
dontHaveAnAccount: Don't have an account yet?
signIn: Sign in
signInToThing: "Sign in to { thing }"
signInWith: Sign in with username and password
usePassword: Use your password
signUpHere: Sign up here
processing: Processing
signInFailed: Sign in failed

View file

@ -1,8 +1,9 @@
// Hooks
import { useState } from 'react'
import { useApp } from 'shared/hooks/use-app.mjs'
import { useState, useContext } from 'react'
import { useBackend } from 'shared/hooks/use-backend.mjs'
import { useTranslation } from 'next-i18next'
// Context
import { ModalContext } from 'shared/context/modal-context.mjs'
// Dependencies
import { serverSideTranslations } from 'next-i18next/serverSideTranslations'
import { validateEmail, validateTld } from 'site/utils.mjs'
@ -25,8 +26,10 @@ const DarkLink = ({ href, txt }) => (
</Link>
)
const SignUpPage = (props) => {
const app = useApp(props)
const SignUpPage = ({ page }) => {
// Context
const { setModal } = useContext(ModalContext)
const backend = useBackend()
const { t, i18n } = useTranslation(namespaces)
@ -34,6 +37,7 @@ const SignUpPage = (props) => {
const [emailValid, setEmailValid] = useState(false)
const [result, setResult] = useState(false)
const [error, setError] = useState(null)
const [loading, setLoading] = useState(false)
const updateEmail = (evt) => {
const value = evt.target.value
@ -44,6 +48,7 @@ const SignUpPage = (props) => {
const signupHandler = async (evt) => {
evt.preventDefault()
setLoading(true)
if (!emailValid) return
let res
try {
@ -53,14 +58,13 @@ const SignUpPage = (props) => {
setResult,
})
} catch (err) {
setError(app.error(err))
// Here to keep the stupid linter happy
console.log(err)
}
if (res.result === 'success') setResult('success')
else {
app.setModal(
<ModalWrapper app={app} bg="base-100 lg:bg-base-300">
setModal(
<ModalWrapper bg="base-100 lg:bg-base-300">
<div className="bg-base-100 rounded-lg p-4 lg:px-8 max-w-xl lg:shadow-lg">
<h3>An error occured while trying to process your request</h3>
<Robot pose="ohno" className="m-auto w-56" embed />
@ -80,13 +84,14 @@ const SignUpPage = (props) => {
</ModalWrapper>
)
}
setLoading(false)
}
const loadingClasses = app.state.loading ? 'opacity-50' : ''
const loadingClasses = loading ? 'opacity-50' : ''
return (
<PageWrapper app={app} title={t('joinFreeSewing')} layout={BareLayout} footer={false}>
<SusiWrapper theme={app.theme} error={result && result !== 'success'}>
<PageWrapper {...page} title={t('joinFreeSewing')} layout={BareLayout} footer={false}>
<SusiWrapper error={result && result !== 'success'}>
<h1 className={`text-neutral-content font-light text-3xl mb-0 pb-0 ${loadingClasses}`}>
{result ? (
result === 'success' ? (
@ -145,7 +150,6 @@ const SignUpPage = (props) => {
/>
<EmailValidButton
email={email}
app={app}
t={t}
validText={t('emailSignupLink')}
invalidText={t('pleaseProvideValidEmail')}

View file

@ -1,5 +1,3 @@
// Hooks
import { useApp } from 'shared/hooks/use-app.mjs'
// Dependencies
import { serverSideTranslations } from 'next-i18next/serverSideTranslations'
// Components
@ -9,91 +7,88 @@ import { Popout } from 'shared/components/popout.mjs'
// Translation namespaces used on this page
const namespaces = [...new Set(pageNs)]
const TypographyPage = (props) => {
const app = useApp(props)
// Re-use this
const p = (
<p>
This paragraph is here to show the vertical spacing between headings and paragraphs. In
addition, let&apos;s make it a bit longer so we can see the line height as the text wraps.
</p>
)
const p = (
<p>
This paragraph is here to show the vertical spacing between headings and paragraphs. In
addition, let&apos;s make it a bit longer so we can see the line height as the text wraps.
</p>
)
return (
<PageWrapper app={app}>
<div className="text-primary mdx max-w-prose text-base-content max-w-prose text-base xl:pl-4">
<p>This typography page shows an overview of different elements and how they are styled.</p>
<p>It&apos;s a good starting point for theme development.</p>
<h2>Headings (this is h2)</h2>
{p} {p}
<h3>This is h3</h3>
{p} {p}
<h4>This is h4</h4>
{p} {p}
<h5>This is h5</h5>
{p} {p}
<h6>This is h6</h6>
{p} {p}
<h2>Links and buttons</h2>
<p>
A regular link <a href="#">looks like this</a>, whereas buttons look like this:
</p>
<h3>Main button styles</h3>
<div className="flex flex-row gap-2 flex-wrap">
<button className="btn btn-neutral">Neutral button</button>
<button className="btn btn-primary">Primary button</button>
<button className="btn btn-secondary">Secondary button</button>
<button className="btn btn-accent">Accent button</button>
</div>
<h3>State button styles</h3>
<div className="flex flex-row gap-2 flex-wrap">
<button className="btn btn-info">Info button</button>
<button className="btn btn-success">Success button</button>
<button className="btn btn-warning">Warning button</button>
<button className="btn btn-error">Error button</button>
</div>
<h3>Other button styles</h3>
<div className="flex flex-row gap-2 flex-wrap">
<button className="btn btn-ghost">Ghost button</button>
<button className="btn btn-link">Link button</button>
</div>
<h3>Outlined button styles</h3>
<div className="flex flex-row gap-2 flex-wrap">
<button className="btn btn-outline btn-neutral">Neutral button</button>
<button className="btn btn-outline btn-primary">Primary button</button>
<button className="btn btn-outline btn-secondary">Secondary button</button>
<button className="btn btn-outline btn-accent">Accent button</button>
</div>
<h3>Button sizes</h3>
<div className="flex flex-row gap-2 flex-wrap">
<button className="btn btn-primary btn-lg">Large</button>
<button className="btn btn-primary">Normal</button>
<button className="btn btn-primary btn-sm">Small</button>
<button className="btn btn-primary btn-xs">Tiny</button>
<button className="btn btn-primary btn-lg btn-wide">Large wide</button>
<button className="btn btn-primary btn-wide">Normal wide</button>
<button className="btn btn-primary btn-sm btn-wide">Small wide</button>
<button className="btn btn-primary btn-xs bnt-wide">Tiny wide</button>
</div>
<h2>Popouts</h2>
<p>The Popout component is what powers various custom MDX components under the hood:</p>
{['note', 'tip', 'warning', 'fixme', 'link', 'related', 'none'].map((type) => {
const props = {}
props[type] = true
return (
<div key={type}>
<h3 className="capitalize">{type}</h3>
<Popout {...props}>
<h5>I am the {type} title</h5>
{p}
</Popout>
</div>
)
})}
const TypographyPage = ({ page }) => (
<PageWrapper {...page}>
<div className="text-primary mdx max-w-prose text-base-content max-w-prose text-base xl:pl-4">
<p>This typography page shows an overview of different elements and how they are styled.</p>
<p>It&apos;s a good starting point for theme development.</p>
<h2>Headings (this is h2)</h2>
{p} {p}
<h3>This is h3</h3>
{p} {p}
<h4>This is h4</h4>
{p} {p}
<h5>This is h5</h5>
{p} {p}
<h6>This is h6</h6>
{p} {p}
<h2>Links and buttons</h2>
<p>
A regular link <a href="#">looks like this</a>, whereas buttons look like this:
</p>
<h3>Main button styles</h3>
<div className="flex flex-row gap-2 flex-wrap">
<button className="btn btn-neutral">Neutral button</button>
<button className="btn btn-primary">Primary button</button>
<button className="btn btn-secondary">Secondary button</button>
<button className="btn btn-accent">Accent button</button>
</div>
</PageWrapper>
)
}
<h3>State button styles</h3>
<div className="flex flex-row gap-2 flex-wrap">
<button className="btn btn-info">Info button</button>
<button className="btn btn-success">Success button</button>
<button className="btn btn-warning">Warning button</button>
<button className="btn btn-error">Error button</button>
</div>
<h3>Other button styles</h3>
<div className="flex flex-row gap-2 flex-wrap">
<button className="btn btn-ghost">Ghost button</button>
<button className="btn btn-link">Link button</button>
</div>
<h3>Outlined button styles</h3>
<div className="flex flex-row gap-2 flex-wrap">
<button className="btn btn-outline btn-neutral">Neutral button</button>
<button className="btn btn-outline btn-primary">Primary button</button>
<button className="btn btn-outline btn-secondary">Secondary button</button>
<button className="btn btn-outline btn-accent">Accent button</button>
</div>
<h3>Button sizes</h3>
<div className="flex flex-row gap-2 flex-wrap">
<button className="btn btn-primary btn-lg">Large</button>
<button className="btn btn-primary">Normal</button>
<button className="btn btn-primary btn-sm">Small</button>
<button className="btn btn-primary btn-xs">Tiny</button>
<button className="btn btn-primary btn-lg btn-wide">Large wide</button>
<button className="btn btn-primary btn-wide">Normal wide</button>
<button className="btn btn-primary btn-sm btn-wide">Small wide</button>
<button className="btn btn-primary btn-xs bnt-wide">Tiny wide</button>
</div>
<h2>Popouts</h2>
<p>The Popout component is what powers various custom MDX components under the hood:</p>
{['note', 'tip', 'warning', 'fixme', 'link', 'related', 'none'].map((type) => {
const props = {}
props[type] = true
return (
<div key={type}>
<h3 className="capitalize">{type}</h3>
<Popout {...props}>
<h5>I am the {type} title</h5>
{p}
</Popout>
</div>
)
})}
</div>
</PageWrapper>
)
export default TypographyPage
@ -102,6 +97,7 @@ export async function getStaticProps({ locale }) {
props: {
...(await serverSideTranslations(locale, namespaces)),
page: {
locale,
path: ['typography'],
},
},

View file

@ -1,5 +1,4 @@
// Hooks
import { useApp } from 'shared/hooks/use-app.mjs'
import { useTranslation } from 'next-i18next'
// Dependencies
import dynamic from 'next/dynamic'
@ -26,28 +25,34 @@ const DynamicBio = dynamic(
{ ssr: false }
)
const BioPage = (props) => {
const app = useApp(props)
/*
* Each page MUST be wrapped in the PageWrapper component.
* You also MUST spread props.page into this wrapper component
* when path and locale come from static props (as here)
* or set them manually.
*/
const WelcomeBioPage = ({ page }) => {
const { t } = useTranslation(namespaces)
return (
<PageWrapper app={app} title={t('title')} layout={BareLayout} footer={false}>
<DynamicAuthWrapper app={app}>
<PageWrapper {...page} layout={BareLayout} footer={false}>
<DynamicAuthWrapper>
<div className="m-auto max-w-lg text-center lg:mt-24 p-8">
<DynamicBio app={app} title welcome />
<DynamicBio title welcome />
</div>
</DynamicAuthWrapper>
</PageWrapper>
)
}
export default BioPage
export default WelcomeBioPage
export async function getStaticProps({ locale }) {
return {
props: {
...(await serverSideTranslations(locale, namespaces)),
page: {
locale,
path: ['welcome', 'bio'],
},
},

View file

@ -1,5 +1,4 @@
// Hooks
import { useApp } from 'shared/hooks/use-app.mjs'
import { useTranslation } from 'next-i18next'
// Dependencies
import dynamic from 'next/dynamic'
@ -26,28 +25,34 @@ const DynamicCompare = dynamic(
{ ssr: false }
)
const ComparePage = (props) => {
const app = useApp(props)
/*
* Each page MUST be wrapped in the PageWrapper component.
* You also MUST spread props.page into this wrapper component
* when path and locale come from static props (as here)
* or set them manually.
*/
const WelcomeComparePage = ({ page }) => {
const { t } = useTranslation(namespaces)
return (
<PageWrapper app={app} title={t('title')} layout={BareLayout} footer={false}>
<DynamicAuthWrapper app={app}>
<PageWrapper {...page} layout={BareLayout} footer={false}>
<DynamicAuthWrapper>
<div className="m-auto max-w-lg text-center lg:mt-24 p-8">
<DynamicCompare app={app} title welcome />
<DynamicCompare title welcome />
</div>
</DynamicAuthWrapper>
</PageWrapper>
)
}
export default ComparePage
export default WelcomeComparePage
export async function getStaticProps({ locale }) {
return {
props: {
...(await serverSideTranslations(locale, namespaces)),
page: {
locale,
path: ['welcome', 'compare'],
},
},

View file

@ -1,5 +1,4 @@
// Hooks
import { useApp } from 'shared/hooks/use-app.mjs'
import { useTranslation } from 'next-i18next'
// Dependencies
import dynamic from 'next/dynamic'
@ -26,28 +25,34 @@ const DynamicImg = dynamic(
{ ssr: false }
)
const ImgPage = (props) => {
const app = useApp(props)
/*
* Each page MUST be wrapped in the PageWrapper component.
* You also MUST spread props.page into this wrapper component
* when path and locale come from static props (as here)
* or set them manually.
*/
const WelcomeImgPage = ({ page }) => {
const { t } = useTranslation(namespaces)
return (
<PageWrapper app={app} title={t('imgTitle')} layout={BareLayout} footer={false}>
<DynamicAuthWrapper app={app}>
<PageWrapper {...page} title={t('imgTitle')} layout={BareLayout} footer={false}>
<DynamicAuthWrapper>
<div className="m-auto max-w-lg text-center lg:mt-24 p-8">
<DynamicImg app={app} title welcome />
<DynamicImg title welcome />
</div>
</DynamicAuthWrapper>
</PageWrapper>
)
}
export default ImgPage
export default WelcomeImgPage
export async function getStaticProps({ locale }) {
return {
props: {
...(await serverSideTranslations(locale, namespaces)),
page: {
locale,
path: ['welcome', 'img'],
},
},

View file

@ -1,5 +1,4 @@
// Hooks
import { useApp } from 'shared/hooks/use-app.mjs'
import { useTranslation } from 'next-i18next'
// Dependencies
import dynamic from 'next/dynamic'
@ -26,15 +25,20 @@ const DynamicControl = dynamic(
{ ssr: false }
)
const WelcomePage = (props) => {
const app = useApp(props)
/*
* Each page MUST be wrapped in the PageWrapper component.
* You also MUST spread props.page into this wrapper component
* when path and locale come from static props (as here)
* or set them manually.
*/
const WelcomePage = ({ page }) => {
const { t } = useTranslation(namespaces)
return (
<PageWrapper app={app} title={t('title')} layout={BareLayout} footer={false}>
<DynamicAuthWrapper app={app}>
<PageWrapper {...page} layout={BareLayout} footer={false}>
<DynamicAuthWrapper>
<div className="m-auto max-w-lg text-center lg:mt-24 p-8">
<DynamicControl app={app} title welcome />
<DynamicControl title welcome />
</div>
</DynamicAuthWrapper>
</PageWrapper>

View file

@ -1,5 +1,4 @@
// Hooks
import { useApp } from 'shared/hooks/use-app.mjs'
import { useTranslation } from 'next-i18next'
// Dependencies
import dynamic from 'next/dynamic'
@ -27,28 +26,34 @@ const DynamicNewsletter = dynamic(
{ ssr: false }
)
const WelcomePage = (props) => {
const app = useApp(props)
/*
* Each page MUST be wrapped in the PageWrapper component.
* You also MUST spread props.page into this wrapper component
* when path and locale come from static props (as here)
* or set them manually.
*/
const WelcomeNewsletterPage = ({ page }) => {
const { t } = useTranslation(namespaces)
return (
<PageWrapper app={app} title={t('title')} layout={BareLayout} footer={false}>
<DynamicAuthWrapper app={app}>
<PageWrapper {...page} title={t('title')} layout={BareLayout} footer={false}>
<DynamicAuthWrapper>
<div className="m-auto max-w-lg text-center lg:mt-24 p-8">
<DynamicNewsletter app={app} title welcome />
<DynamicNewsletter title welcome />
</div>
</DynamicAuthWrapper>
</PageWrapper>
)
}
export default WelcomePage
export default WelcomeNewsletterPage
export async function getStaticProps({ locale }) {
return {
props: {
...(await serverSideTranslations(locale, namespaces)),
page: {
locale,
path: ['welcome', 'newsletter'],
},
},

View file

@ -1,5 +1,4 @@
// Hooks
import { useApp } from 'shared/hooks/use-app.mjs'
import { useTranslation } from 'next-i18next'
// Dependencies
import dynamic from 'next/dynamic'
@ -26,28 +25,34 @@ const DynamicImperial = dynamic(
{ ssr: false }
)
const UnitsPage = (props) => {
const app = useApp(props)
/*
* Each page MUST be wrapped in the PageWrapper component.
* You also MUST spread props.page into this wrapper component
* when path and locale come from static props (as here)
* or set them manually.
*/
const WelcomeUnitsPage = ({ page }) => {
const { t } = useTranslation(namespaces)
return (
<PageWrapper app={app} title={t('title')} layout={BareLayout} footer={false}>
<DynamicAuthWrapper app={app}>
<PageWrapper {...page} layout={BareLayout} footer={false}>
<DynamicAuthWrapper>
<div className="m-auto max-w-lg text-center lg:mt-24 p-8">
<DynamicImperial app={app} title welcome />
<DynamicImperial title welcome />
</div>
</DynamicAuthWrapper>
</PageWrapper>
)
}
export default UnitsPage
export default WelcomeUnitsPage
export async function getStaticProps({ locale }) {
return {
props: {
...(await serverSideTranslations(locale, namespaces)),
page: {
locale,
path: ['welcome', 'units'],
},
},

View file

@ -1,5 +1,4 @@
// Hooks
import { useApp } from 'shared/hooks/use-app.mjs'
import { useTranslation } from 'next-i18next'
// Dependencies
import dynamic from 'next/dynamic'
@ -26,28 +25,34 @@ const DynamicUsername = dynamic(
{ ssr: false }
)
const UsernamePage = (props) => {
const app = useApp(props)
/*
* Each page MUST be wrapped in the PageWrapper component.
* You also MUST spread props.page into this wrapper component
* when path and locale come from static props (as here)
* or set them manually.
*/
const WelcomeUsernamePage = ({ page }) => {
const { t } = useTranslation(namespaces)
return (
<PageWrapper app={app} title={t('title')} layout={BareLayout} footer={false}>
<DynamicAuthWrapper app={app}>
<PageWrapper {...page} title={t('title')} layout={BareLayout} footer={false}>
<DynamicAuthWrapper>
<div className="m-auto max-w-lg text-center lg:mt-24 p-8">
<DynamicUsername app={app} title welcome />
<DynamicUsername title welcome />
</div>
</DynamicAuthWrapper>
</PageWrapper>
)
}
export default UsernamePage
export default WelcomeUsernamePage
export async function getStaticProps({ locale }) {
return {
props: {
...(await serverSideTranslations(locale, namespaces)),
page: {
locale,
path: ['welcome', 'username'],
},
},

View file

@ -1,8 +1,11 @@
// Dependencies
import { useState, useEffect } from 'react'
import { useState, useEffect, useContext } from 'react'
import { useTranslation } from 'next-i18next'
import { DateTime } from 'luxon'
import { CopyToClipboard } from 'react-copy-to-clipboard'
// Context
import { LoadingContext } from 'shared/context/loading-context.mjs'
import { ModalContext } from 'shared/context/modal-context.mjs'
// Hooks
import { useAccount } from 'shared/hooks/use-account.mjs'
import { useBackend } from 'shared/hooks/use-backend.mjs'
@ -111,7 +114,17 @@ const ShowKey = ({ apikey, t, clear }) => (
</div>
)
const NewKey = ({ app, t, account, setGenerate, keyAdded, backend, toast, standAlone = false }) => {
const NewKey = ({
t,
account,
setGenerate,
keyAdded,
backend,
toast,
startLoading,
stopLoading,
standAlone = false,
}) => {
const router = useRouter()
const [name, setName] = useState('')
const [level, setLevel] = useState(1)
@ -121,7 +134,7 @@ const NewKey = ({ app, t, account, setGenerate, keyAdded, backend, toast, standA
const levels = account.role === 'admin' ? [0, 1, 2, 3, 4, 5, 6, 7, 8] : [0, 1, 2, 3, 4]
const createKey = async () => {
app.startLoading()
startLoading()
const result = await backend.createApikey({
name,
level,
@ -132,7 +145,7 @@ const NewKey = ({ app, t, account, setGenerate, keyAdded, backend, toast, standA
setApikey(result.data.apikey)
keyAdded()
} else toast.for.backendError()
app.stopLoading()
stopLoading()
}
const clear = () => {
@ -187,7 +200,9 @@ const NewKey = ({ app, t, account, setGenerate, keyAdded, backend, toast, standA
)
}
const Apikey = ({ apikey, t, account, backend, keyAdded, app }) => {
const Apikey = ({ apikey, t, account, backend, keyAdded }) => {
//const { startLoading, stopLoading } = useContext(LoadingContext)
const { setModal } = useContext(ModalContext)
const toast = useToast()
const fields = {
@ -201,19 +216,19 @@ const Apikey = ({ apikey, t, account, backend, keyAdded, app }) => {
const expired = DateTime.fromISO(apikey.expiresAt).valueOf() < DateTime.now().valueOf()
const remove = async () => {
app.startLoading()
startLoading()
const result = await backend.removeApikey(apikey.id)
if (result) toast.success(t('gone'))
else toast.for.backendError()
// This just forces a refresh of the list from the server
// We obviously did not add a key here, but rather removed one
keyAdded()
app.stopLoading()
stopLoading()
}
const removeModal = () => {
app.setModal(
<ModalWrapper app={app} slideFrom="top">
setModal(
<ModalWrapper slideFrom="top">
<h2>{t('areYouCertain')}</h2>
<p>{t('deleteKeyWarning')}</p>
<p className="flex flex-row gap-4 items-center justify-center">
@ -268,36 +283,60 @@ const Apikey = ({ apikey, t, account, backend, keyAdded, app }) => {
}
// Component for the 'new/apikey' page
export const NewApikey = ({ app, standAlone = false }) => {
export const NewApikey = ({ standAlone = false }) => {
// Context
const { startLoading, stopLoading } = useContext(LoadingContext)
// Hooks
const { account, token } = useAccount()
const backend = useBackend(token)
const { t } = useTranslation(ns)
const toast = useToast()
// State
const [keys, setKeys] = useState([])
const [generate, setGenerate] = useState(false)
const [added, setAdded] = useState(0)
// Helper method to force refresh
const keyAdded = () => setAdded(added + 1)
return (
<div className="max-w-xl xl:pl-4">
<NewKey {...{ app, t, account, setGenerate, backend, toast, keyAdded, standAlone }} />
<NewKey
{...{
t,
account,
setGenerate,
backend,
toast,
keyAdded,
standAlone,
startLoading,
stopLoading,
}}
/>
</div>
)
}
// Component for the account/apikeys page
export const Apikeys = ({ app }) => {
export const Apikeys = () => {
// Context
const { startLoading, stopLoading, loading } = useContext(LoadingContext)
// Hooks
const { account, token } = useAccount()
const backend = useBackend(token)
const { t } = useTranslation(ns)
const toast = useToast()
// State
const [keys, setKeys] = useState([])
const [generate, setGenerate] = useState(false)
const [added, setAdded] = useState(0)
// Effects
useEffect(() => {
const getApikeys = async () => {
const result = await backend.getApikeys()
@ -306,12 +345,24 @@ export const Apikeys = ({ app }) => {
getApikeys()
}, [added])
// Helper method to force refresh
const keyAdded = () => setAdded(added + 1)
return (
<div className="max-w-xl xl:pl-4">
{generate ? (
<NewKey {...{ app, t, account, setGenerate, backend, toast, keyAdded }} />
<NewKey
{...{
t,
account,
setGenerate,
backend,
toast,
keyAdded,
startLoading,
stopLoading,
}}
/>
) : (
<>
<h2>{t('apikeys')}</h2>
@ -324,7 +375,7 @@ export const Apikeys = ({ app }) => {
>
{t('newApikey')}
</button>
<BackToAccountButton loading={app.state.loading} />
<BackToAccountButton loading={loading} />
{account.control < 5 ? (
<Popout tip>
<h5>Refer to FreeSewing.dev for details (English only)</h5>

View file

@ -1,10 +1,12 @@
// Dependencies
import { useState } from 'react'
import { useState, useContext } from 'react'
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'
// Components
import Markdown from 'react-markdown'
import { Icons, welcomeSteps, BackToAccountButton } from './shared.mjs'
@ -24,29 +26,38 @@ export const Tab = ({ id, activeTab, setActiveTab, t }) => (
</button>
)
export const BioSettings = ({ app, title = false, welcome = false }) => {
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()
// State
const [bio, setBio] = useState(account.bio)
const [activeTab, setActiveTab] = useState('edit')
// Helper method to save bio
const save = async () => {
app.startLoading()
startLoading()
const result = await backend.updateAccount({ bio })
if (result.success) {
setAccount(result.data.account)
toast.for.settingsSaved()
} else toast.for.backendError()
app.stopLoading()
stopLoading()
}
// Next step in the onboarding
const nextHref =
welcomeSteps[account.control].length > 5
? '/welcome/' + welcomeSteps[account.control][6]
: '/docs/guide'
// Shared props for tabs
const tabProps = { activeTab, setActiveTab, t }
return (
@ -71,15 +82,15 @@ export const BioSettings = ({ app, title = false, welcome = false }) => {
</div>
)}
</div>
<SaveSettingsButton app={app} btnProps={{ onClick: save }} welcome={welcome} />
{!welcome && <BackToAccountButton loading={app.state.loading} />}
<SaveSettingsButton btnProps={{ onClick: save }} welcome={welcome} />
{!welcome && <BackToAccountButton loading={loading} />}
<Popout tip compact>
{t('mdSupport')}
</Popout>
{welcome ? (
<>
<ContinueButton app={app} btnProps={{ href: nextHref }} link />
<ContinueButton btnProps={{ href: nextHref }} link />
{welcomeSteps[account.control].length > 0 ? (
<>
<progress

View file

@ -1,36 +1,48 @@
// Dependencies
import { useState } from 'react'
import { useState, useContext } from 'react'
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'
// Components
import { Choice, Icons, welcomeSteps, BackToAccountButton } from './shared.mjs'
import { ContinueButton } from 'site/components/buttons/continue-button.mjs'
export const ns = ['account', 'toast']
export const CompareSettings = ({ app, title = false, welcome = false }) => {
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 { t } = useTranslation(ns)
// State
const [selection, setSelection] = useState(account?.compare ? 'yes' : 'no')
// Helper method to update the account
const update = async (val) => {
if (val !== selection) {
app.startLoading()
const result = await backend.updateAccount({ compare: val === 'yes' ? true : false })
startLoading()
const result = await backend.updateAccount({
compare: val === 'yes' ? true : false,
})
if (result.success) {
setAccount(result.data.account)
setSelection(val)
toast.for.settingsSaved()
} else toast.for.backendError()
app.stopLoading()
stopLoading()
}
}
// Link to the next onboarding step
const nextHref =
welcomeSteps[account?.control].length > 3
? '/welcome/' + welcomeSteps[account?.control][4]
@ -53,7 +65,7 @@ export const CompareSettings = ({ app, title = false, welcome = false }) => {
))}
{welcome ? (
<>
<ContinueButton app={app} btnProps={{ href: nextHref }} link />
<ContinueButton btnProps={{ href: nextHref }} link />
{welcomeSteps[account?.control].length > 0 ? (
<>
<progress
@ -73,10 +85,8 @@ export const CompareSettings = ({ app, title = false, welcome = false }) => {
) : null}
</>
) : (
<BackToAccountButton loading={app.state.loading} />
<BackToAccountButton loading={loading} />
)}
</div>
)
}
export default CompareSettings

View file

@ -1,10 +1,12 @@
// Dependencies
import { useState } from 'react'
import { useState, useContext } from 'react'
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'
// Components
import Link from 'next/link'
import { Popout } from 'shared/components/popout.mjs'
@ -38,40 +40,48 @@ const Checkbox = ({ value, setter, label, children = null }) => (
</div>
)
export const ConsentSettings = ({ app, title = false }) => {
const { account, token } = useAccount()
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 { t } = useTranslation(ns)
// State
const [profile, setProfile] = useState(account?.consent > 0)
const [measurements, setMeasurements] = useState(account?.consent > 1)
const [openData, setOpenData] = useState(account?.consent > 2)
// Helper method to update the account
const update = async () => {
let newConsent = 0
if (profile) newConsent = 1
if (profile && measurements) newConsent = 2
if (profile && measurements && openData) newConsent = 3
if (newConsent !== account.consent) {
app.startLoading()
startLoading()
const result = await backend.updateAccount({ consent: newConsent })
if (result === true) toast.for.settingsSaved()
else toast.for.backendError()
app.stopLoading()
stopLoading()
}
}
// Helper method to remove the account
const removeAccount = async () => {
app.startLoading()
startLoading()
const result = await backend.removeAccount()
if (result === true) toast.for.settingsSaved()
else toast.for.backendError()
app.setToken(null)
app.setAccount({ username: false })
app.stopLoading()
setToken(null)
setAccount({ username: false })
stopLoading()
}
// Part A of the consent screen
const partA = (
<>
<h5 className="mt-8">{t('profileQuestion')}</h5>
@ -92,6 +102,7 @@ export const ConsentSettings = ({ app, title = false }) => {
)}
</>
)
// Part B of the consent screen
const partB = (
<>
<h5 className="mt-8">{t('setQuestion')}</h5>
@ -122,12 +133,9 @@ export const ConsentSettings = ({ app, title = false }) => {
<p>{t('consentWhyAnswer')}</p>
{partA}
{profile && partB}
{profile && measurements ? (
<SaveSettingsButton app={app} btnProps={{ onClick: update }} />
) : null}
{profile && measurements ? <SaveSettingsButton btnProps={{ onClick: update }} /> : null}
{profile && !measurements ? (
<SaveSettingsButton
app={app}
label={t('revokeConsent')}
btnProps={{
onClick: update,
@ -137,7 +145,6 @@ export const ConsentSettings = ({ app, title = false }) => {
) : null}
{!profile ? (
<SaveSettingsButton
app={app}
label={t('account:removeAccount')}
btnProps={{
onClick: removeAccount,
@ -146,7 +153,7 @@ export const ConsentSettings = ({ app, title = false }) => {
/>
) : null}
<BackToAccountButton loading={app.state.loading} />
<BackToAccountButton loading={loading} />
<p className="text-center opacity-50 mt-12">
<Link href="/docs/various/privacy" className="hover:text-secondary underline">
FreeSewing Privacy Notice

View file

@ -1,38 +1,51 @@
// Dependencies
import { useState } from 'react'
import { useState, useContext } from 'react'
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'
// Components
import { BackToAccountButton, Choice, Icons, welcomeSteps } from './shared.mjs'
import { ContinueButton } from 'site/components/buttons/continue-button.mjs'
export const ns = ['account', 'toast']
export const ControlSettings = ({ app, title = false, welcome = false }) => {
export const ControlSettings = ({ 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)
// State
const [selection, setSelection] = useState(account.control || 2)
// Method to update the control setting
const update = async (control) => {
if (control !== selection) {
app.startLoading()
startLoading()
const result = await backend.updateAccount({ control })
if (result.success) {
setSelection(control)
toast.for.settingsSaved()
setAccount(result.data.account)
} else toast.for.backendError()
app.stopLoading()
stopLoading()
}
}
const nextHref =
welcomeSteps[selection].length > 1 ? '/welcome/' + welcomeSteps[selection][1] : '/docs/guide'
// Helper to get the link to the next onboarding step
const nextHref = welcome
? welcomeSteps[selection].length > 1
? '/welcome/' + welcomeSteps[selection][1]
: '/docs/guide'
: false
return (
<div className="max-w-xl">
@ -58,7 +71,7 @@ export const ControlSettings = ({ app, title = false, welcome = false }) => {
})}
{welcome ? (
<>
<ContinueButton app={app} btnProps={{ href: nextHref }} link />
<ContinueButton btnProps={{ href: nextHref }} link />
{welcomeSteps[selection].length > 1 ? (
<>
<progress
@ -74,7 +87,7 @@ export const ControlSettings = ({ app, title = false, welcome = false }) => {
) : null}
</>
) : (
<BackToAccountButton loading={app.state.loading} />
<BackToAccountButton />
)}
</div>
)

View file

@ -1,10 +1,12 @@
// Dependencies
import { useState } from 'react'
import { useState, useContext } from 'react'
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'
// Verification methods
import { validateEmail, validateTld } from 'site/utils.mjs'
// Components
@ -13,23 +15,33 @@ import { Popout } from 'shared/components/popout.mjs'
export const ns = ['account', 'toast']
export const EmailSettings = ({ app, title = false }) => {
export const EmailSettings = ({ title = 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()
// State
const [email, setEmail] = useState(account.email)
const [changed, setChanged] = useState(false)
// Helper method to update account
const save = async () => {
startLoading()
const result = await backend.updateAccount({ email })
if (result.success) {
setAccount(result.data.account)
toast.for.settingsSaved()
} else toast.for.backendError()
setChanged(true)
stopLoading()
}
// Is email valid?
const valid = (validateEmail(email) && validateTld(email)) || false
return (
@ -59,7 +71,7 @@ export const EmailSettings = ({ app, title = false }) => {
</button>
</>
)}
<BackToAccountButton loading={app.state.loading} />
<BackToAccountButton loading={loading} />
</div>
)
}

View file

@ -1,31 +1,40 @@
// Dependencies
import { useState } from 'react'
import { useState, useContext } from 'react'
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'
// Components
import { BackToAccountButton } from './shared.mjs'
import { SaveSettingsButton } from 'site/components/buttons/save-settings-button.mjs'
export const ns = ['account', 'toast']
export const GithubSettings = ({ app, title = false, welcome = false }) => {
export const GithubSettings = ({ 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()
// State
const [github, setGithub] = useState(account.github || '')
// Helper method to save changes
const save = async () => {
app.startLoading()
startLoading()
const result = await backend.updateAccount({ github })
if (result.success) {
setAccount(result.data.account)
toast.for.settingsSaved()
} else toast.for.backendError()
app.stopLoading()
stopLoading()
}
return (
@ -40,8 +49,8 @@ export const GithubSettings = ({ app, title = false, welcome = false }) => {
placeholder={t('github')}
/>
</div>
<SaveSettingsButton app={app} btnProps={{ onClick: save }} />
{!welcome && <BackToAccountButton loading={app.state.loading} />}
<SaveSettingsButton btnProps={{ onClick: save }} />
{!welcome && <BackToAccountButton loading={loading} />}
</div>
)
}

View file

@ -1,7 +1,9 @@
// Dependencies
import { useState, useCallback } from 'react'
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'
// Hooks
import { useAccount } from 'shared/hooks/use-account.mjs'
import { useBackend } from 'shared/hooks/use-backend.mjs'
@ -13,7 +15,8 @@ import { SaveSettingsButton } from 'site/components/buttons/save-settings-button
export const ns = ['account', 'toast']
export const ImgSettings = ({ app, title = false, welcome = false }) => {
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()
@ -32,13 +35,13 @@ export const ImgSettings = ({ app, title = false, welcome = false }) => {
const { getRootProps, getInputProps } = useDropzone({ onDrop })
const save = async () => {
app.startLoading()
startLoading()
const result = await backend.updateAccount({ img })
if (result.success) {
setAccount(result.data.account)
toast.for.settingsSaved()
} else toast.for.backendError()
app.stopLoading()
stopLoading()
}
const nextHref = '/docs/guide'
@ -71,7 +74,7 @@ export const ImgSettings = ({ app, title = false, welcome = false }) => {
<button className={`btn btn-secondary mt-4 w-64`} onClick={save} disabled={!img}>
{t('save')}
</button>
<ContinueButton app={app} btnProps={{ href: nextHref }} link />
<ContinueButton btnProps={{ href: nextHref }} link />
{welcomeSteps[account.control].length > 0 ? (
<>
<progress
@ -92,8 +95,8 @@ export const ImgSettings = ({ app, title = false, welcome = false }) => {
</>
) : (
<>
<SaveSettingsButton app={app} btnProps={{ onClick: save }} />
<BackToAccountButton loading={app.state.loading} />
<SaveSettingsButton btnProps={{ onClick: save }} />
<BackToAccountButton loading={loading} />
</>
)}
</div>

View file

@ -1,36 +1,46 @@
// Dependencies
import { useState } from 'react'
import { useState, useContext } from 'react'
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'
// Components
import { Choice, Icons, welcomeSteps, BackToAccountButton } from './shared.mjs'
import { ContinueButton } from 'site/components/buttons/continue-button.mjs'
export const ns = ['account', 'toast']
export const ImperialSettings = ({ app, title = false, welcome = false }) => {
export const ImperialSettings = ({ 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)
// State
const [selection, setSelection] = useState(account?.imperial === true ? 'imperial' : 'metric')
// Helper method to update account
const update = async (val) => {
if (val !== selection) {
app.startLoading()
startLoading()
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()
app.stopLoading()
stopLoading()
}
}
// Next step in the onboarding
const nextHref =
welcomeSteps[account?.control].length > 3
? '/welcome/' + welcomeSteps[account?.control][3]
@ -57,7 +67,7 @@ export const ImperialSettings = ({ app, title = false, welcome = false }) => {
))}
{welcome ? (
<>
<ContinueButton app={app} btnProps={{ href: nextHref }} link />
<ContinueButton btnProps={{ href: nextHref }} link />
{welcomeSteps[account?.control].length > 0 ? (
<>
<progress
@ -77,7 +87,7 @@ export const ImperialSettings = ({ app, title = false, welcome = false }) => {
) : null}
</>
) : (
<BackToAccountButton loading={app.state.loading} />
<BackToAccountButton loading={loading} />
)}
</div>
)

View file

@ -1,10 +1,12 @@
// Dependencies
import { useState } from 'react'
import { useState, useContext } from 'react'
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'
// Components
import { BackToAccountButton, Choice } from './shared.mjs'
// Config
@ -12,23 +14,30 @@ import { freeSewingConfig as conf } from 'site/freesewing.config.mjs'
export const ns = ['account', 'locales', 'toast']
export const LanguageSettings = ({ app, title = false }) => {
export const LanguageSettings = ({ title = 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)
// State
const [language, setLanguage] = useState(account.language || 'en')
// Helper method to update the account
const update = async (lang) => {
if (lang !== language) {
app.startLoading()
startLoading()
setLanguage(lang)
const result = await backend.updateAccount({ language: lang })
if (result.success) {
setAccount(result.data.account)
toast.for.settingsSaved()
} else toast.for.backendError()
app.stopLoading()
stopLoading()
}
}
@ -40,7 +49,7 @@ export const LanguageSettings = ({ app, title = false }) => {
<span className="block text-lg leading-5">{t(`locales:${val}`)}</span>
</Choice>
))}
<BackToAccountButton loading={app.state.loading} />
<BackToAccountButton loading={loading} />
</div>
)
}

View file

@ -38,8 +38,8 @@ const actions = {
remove: 2,
}
export const AccountLinks = ({ app }) => {
const { account } = useAccount()
export const AccountLinks = () => {
const { account, clear } = useAccount()
const { t } = useTranslation(ns)
const lprops = { t, control: account.control }
@ -53,7 +53,7 @@ export const AccountLinks = ({ app }) => {
<Link className="btn btn-secondary grow capitalize" href="/account/sets">
{t('newSet')}
</Link>
<button className="btn btn-warning btnoutline mb-2 capitalize" onClick={app.clear}>
<button className="btn btn-warning btnoutline mb-2 capitalize" onClick={clear}>
{t('signOut')}
</button>
</div>

View file

@ -1,10 +1,12 @@
// Dependencies
import { useState } from 'react'
import { useState, useContext } from 'react'
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'
// Components
import { BackToAccountButton } from './shared.mjs'
import { Popout } from 'shared/components/popout.mjs'
@ -22,26 +24,33 @@ const CodeInput = ({ code, setCode, t }) => (
/>
)
export const MfaSettings = ({ app, title = false, welcome = false }) => {
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()
// State
const [enable, setEnable] = useState(false)
const [disable, setDisable] = useState(false)
const [code, setCode] = useState('')
const [password, setPassword] = useState('')
// Helper method to enable MFA
const enableMfa = async () => {
app.startLoading()
startLoading()
const result = await backend.enableMfa()
if (result.success) setEnable(result.data.mfa)
app.stopLoading()
stopLoading()
}
// Helper method to disable MFA
const disableMfa = async () => {
app.startLoading()
startLoading()
const result = await backend.disableMfa({
mfa: false,
password,
@ -57,11 +66,12 @@ export const MfaSettings = ({ app, title = false, welcome = false }) => {
setCode('')
setPassword('')
}
app.stopLoading()
stopLoading()
}
// Helper method to confirm MFA
const confirmMfa = async () => {
app.startLoading()
startLoading()
const result = await backend.confirmMfa({
mfa: true,
secret: enable.secret,
@ -73,9 +83,10 @@ export const MfaSettings = ({ app, title = false, welcome = false }) => {
} else toast.for.backendError()
setEnable(false)
setCode('')
app.stopLoading()
stopLoading()
}
// Figure out what title to use
let titleText = account.mfaEnabled ? t('mfaEnabled') : t('mfaDisabled')
if (enable) titleText = t('mfaSetup')
@ -145,7 +156,7 @@ export const MfaSettings = ({ app, title = false, welcome = false }) => {
</div>
)}
</div>
{!welcome && <BackToAccountButton loading={app.state.loading} />}
{!welcome && <BackToAccountButton loading={loading} />}
</div>
)
}

View file

@ -1,36 +1,46 @@
// Dependencies
import { useState } from 'react'
import { useState, useContext } from 'react'
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'
// Components
import { BackToAccountButton, Choice, Icons, welcomeSteps } from './shared.mjs'
import { ContinueButton } from 'site/components/buttons/continue-button.mjs'
export const ns = ['account', 'toast']
export const NewsletterSettings = ({ app, title = false, welcome = false }) => {
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)
// State
const [selection, setSelection] = useState(account?.newsletter ? 'yes' : 'no')
// Helper method to update account
const update = async (val) => {
if (val !== selection) {
app.startLoading()
startLoading()
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()
app.stopLoading()
stopLoading()
}
}
// Next step for onboarding
const nextHref =
welcomeSteps[account?.control].length > 2
? '/welcome/' + welcomeSteps[account?.control][2]
@ -53,7 +63,7 @@ export const NewsletterSettings = ({ app, title = false, welcome = false }) => {
))}
{welcome ? (
<>
<ContinueButton app={app} btnProps={{ href: nextHref }} link />
<ContinueButton btnProps={{ href: nextHref }} link />
{welcomeSteps[account?.control].length > 0 ? (
<>
<progress
@ -73,7 +83,7 @@ export const NewsletterSettings = ({ app, title = false, welcome = false }) => {
) : null}
</>
) : (
<BackToAccountButton loading={app.state.loading} />
<BackToAccountButton loading={loading} />
)}
</div>
)

View file

@ -1,10 +1,12 @@
// Dependencies
import { useState } from 'react'
import { useState, useContext } from 'react'
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'
// Components
import Link from 'next/link'
import { BackToAccountButton } from './shared.mjs'
@ -14,22 +16,29 @@ import { RightIcon } from 'shared/components/icons.mjs'
export const ns = ['account', 'toast']
export const PasswordSettings = ({ app, title = false, welcome = false }) => {
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 { t } = useTranslation(ns)
const toast = useToast()
// State
const [password, setPassword] = useState('')
const [reveal, setReveal] = useState(false)
// Helper method to save password to account
const save = async () => {
app.startLoading()
startLoading()
const result = await backend.updateAccount({ password })
if (result.success) {
setAccount(result.data.account)
toast.for.settingsSaved()
} else toast.for.backendError()
app.stopLoading()
stopLoading()
}
return (
@ -52,8 +61,8 @@ export const PasswordSettings = ({ app, title = false, welcome = false }) => {
</span>
</button>
</div>
<SaveSettingsButton app={app} btnProps={{ onClick: save, disabled: password.length < 4 }} />
{!welcome && <BackToAccountButton loading={app.state.loading} />}
<SaveSettingsButton btnProps={{ onClick: save, disabled: password.length < 4 }} />
{!welcome && <BackToAccountButton loading={loading} />}
{!account.mfaEnabled && (
<Popout tip>
<h5>{t('mfaTipTitle')}</h5>

View file

@ -1,38 +1,46 @@
// 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'
// Components
import { BackToAccountButton } from './shared.mjs'
export const ns = ['account', 'toast']
export const ReloadAccount = ({ app, title = false }) => {
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()
// Helper method to reload account
const reload = async () => {
app.startLoading()
startLoading()
const result = await backend.reloadAccount()
if (result.success) {
setAccount(result.data.account)
toast.success(<span>{t('nailedIt')}</span>)
} else toast.for.backendError()
app.stopLoading()
stopLoading()
}
return (
<>
<div className="max-w-xl">
{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={app.state.loading} />
</>
<BackToAccountButton loading={loading} />
</div>
)
}

View file

@ -1,5 +1,5 @@
// Dependencies
import { useState, useEffect } from 'react'
import { useState, useEffect, useContext } from 'react'
import { useTranslation } from 'next-i18next'
import { DateTime } from 'luxon'
import { CopyToClipboard } from 'react-copy-to-clipboard'
@ -8,6 +8,9 @@ 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 { useRouter } from 'next/router'
// Context
import { LoadingContext } from 'shared/context/loading-context.mjs'
import { ModalContext } from 'shared/context/modal-context.mjs'
// Components
import { BackToAccountButton, Choice } from './shared.mjs'
import { Popout } from 'shared/components/popout.mjs'
@ -22,15 +25,22 @@ import { Tab } from './bio.mjs'
export const ns = ['account', 'toast']
const NewSet = ({ app, t, account, setGenerate, oneAdded, backend, toast, standAlone = false }) => {
const NewSet = ({ t, account, setGenerate, oneAdded, backend, toast, standAlone = false }) => {
// Context
const { loading, startLoading, stopLoading } = useContext(LoadingContext)
// Hooks
const router = useRouter()
// State
const [name, setName] = useState('')
const [notes, setNotes] = useState('')
const [set, setSet] = useState(false)
const [activeTab, setActiveTab] = useState('edit')
// Helper method to create a new set
const createSet = async () => {
app.startLoading()
startLoading()
const result = await backend.createSet({
name,
})
@ -39,13 +49,16 @@ const NewSet = ({ app, t, account, setGenerate, oneAdded, backend, toast, standA
setSet(result.data.set)
oneAdded()
} else toast.for.backendError()
app.stopLoading()
stopLoading()
}
// Helper method to clear inputs
const clear = () => {
setSet(false)
setGenerate(false)
}
// Shared props for tabs
const tabProps = { activeTab, setActiveTab, t }
return (
@ -104,7 +117,12 @@ const NewSet = ({ app, t, account, setGenerate, oneAdded, backend, toast, standA
)
}
const MeasurementsSet = ({ apikey, t, account, backend, oneAdded, app }) => {
const MeasurementsSet = ({ apikey, t, account, backend, oneAdded }) => {
// Context
const { loading, startLoading, stopLoading } = useContext(LoadingContext)
const { setModal } = useContext(ModalContext)
// Hooks
const toast = useToast()
const fields = {
@ -118,19 +136,19 @@ const MeasurementsSet = ({ apikey, t, account, backend, oneAdded, app }) => {
const expired = DateTime.fromISO(apikey.expiresAt).valueOf() < DateTime.now().valueOf()
const remove = async () => {
app.startLoading()
startLoading()
const result = await backend.removeApikey(apikey.id)
if (result) toast.success(t('gone'))
else toast.for.backendError()
// This just forces a refresh of the list from the server
// We obviously did not add a key here, but rather removed one
oneAdded()
app.stopLoading()
stopLoading()
}
const removeModal = () => {
app.setModal(
<ModalWrapper app={app} slideFrom="top">
setModal(
<ModalWrapper slideFrom="top">
<h2>{t('areYouCertain')}</h2>
<p>{t('deleteKeyWarning')}</p>
<p className="flex flex-row gap-4 items-center justify-center">
@ -205,16 +223,22 @@ const MeasurementsSet = ({ apikey, t, account, backend, oneAdded, app }) => {
//}
// Component for the account/sets page
export const Sets = ({ app }) => {
export const Sets = () => {
// Context
const { loading, startLoading, stopLoading } = useContext(LoadingContext)
// Hooks
const { account, token } = useAccount()
const backend = useBackend(token)
const { t } = useTranslation(ns)
const toast = useToast()
// State
const [sets, setSets] = useState([])
const [generate, setGenerate] = useState(false)
const [added, setAdded] = useState(0)
// Effects
useEffect(() => {
const getSets = async () => {
const result = await backend.getSets()
@ -223,17 +247,18 @@ export const Sets = ({ app }) => {
getSets()
}, [added])
// Helper method to force a refresh
const oneAdded = () => setAdded(added + 1)
return (
<div className="max-w-xl xl:pl-4">
{generate ? (
<NewSet {...{ app, t, account, setGenerate, backend, toast, oneAdded }} />
<NewSet {...{ t, account, setGenerate, backend, toast, oneAdded }} />
) : (
<>
<h2>{t('sets')}</h2>
{sets.map((set) => (
<Set {...{ app, account, apikey, t, backend, oneAdded }} key={apikey.id} />
<Set {...{ account, apikey, t, backend, oneAdded }} key={apikey.id} />
))}
<button
className="btn btn-primary w-full capitalize mt-4"
@ -241,7 +266,7 @@ export const Sets = ({ app }) => {
>
{t('newSet')}
</button>
<BackToAccountButton loading={app.state.loading} />
<BackToAccountButton loading={loading} />
</>
)}
</div>

View file

@ -1,10 +1,12 @@
// Dependencies
import { useState } from 'react'
import { useState, useContext } from 'react'
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'
// Components
import { Spinner } from 'shared/components/spinner.mjs'
import { Icons, welcomeSteps, BackToAccountButton } from './shared.mjs'
@ -13,7 +15,11 @@ import { ContinueButton } from 'site/components/buttons/continue-button.mjs'
export const ns = ['account', 'toast']
export const UsernameSettings = ({ app, title = false, welcome = false }) => {
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()
@ -31,13 +37,13 @@ export const UsernameSettings = ({ app, title = false, welcome = false }) => {
}
const save = async () => {
app.startLoading()
startLoading()
const result = await backend.updateAccount({ username })
if (result.success) {
setAccount(result.data.account)
toast.for.settingsSaved()
} else toast.for.backendError()
app.stopLoading()
stopLoading()
}
const nextHref =
@ -48,11 +54,11 @@ export const UsernameSettings = ({ app, title = false, welcome = false }) => {
let btnClasses = 'btn mt-4 capitalize '
if (welcome) {
btnClasses += 'w-64 '
if (app.state.loading) btnClasses += 'btn-accent '
if (loading) btnClasses += 'btn-accent '
else btnClasses += 'btn-secondary '
} else {
btnClasses += 'w-full '
if (app.state.loading) btnClasses += 'btn-accent '
if (loading) btnClasses += 'btn-accent '
else btnClasses += 'btn-primary '
}
@ -77,7 +83,7 @@ export const UsernameSettings = ({ app, title = false, welcome = false }) => {
</div>
<button className={btnClasses} disabled={!available} onClick={save}>
<span className="flex flex-row items-center gap-2">
{app.state.loading ? (
{loading ? (
<>
<Spinner />
<span>{t('processing')}</span>
@ -92,7 +98,7 @@ export const UsernameSettings = ({ app, title = false, welcome = false }) => {
{welcome ? (
<>
<ContinueButton app={app} btnProps={{ href: nextHref }} link />
<ContinueButton btnProps={{ href: nextHref }} link />
{welcomeSteps[account.control].length > 0 ? (
<>
<progress
@ -112,7 +118,7 @@ export const UsernameSettings = ({ app, title = false, welcome = false }) => {
) : null}
</>
) : (
<BackToAccountButton loading={app.state.loading} />
<BackToAccountButton loading={loading} />
)}
</div>
)

View file

@ -7,8 +7,7 @@ export const ns = ['footer', ...sponsorsNs]
export const Footer = ({ app }) => (
<footer className="bg-neutral">
<Ribbon loading={app.loading} theme={app.theme} />
<Ribbon />
<div className="w-full sm:w-auto flex flex-col gap-2 items-center justify-center pt-12">
<FreeSewingIcon className="w-24 lg:w-40 m-auto m-auto text-neutral-content" />
<div className="mt-4">

View file

@ -9,7 +9,7 @@ import { HttpMethod, HttpStatusCode } from './http.mjs'
const Fixme = () => <p>FIXME</p>
export const MdxComponents = (app = false) => ({
export const MdxComponents = () => ({
// Custom components
Method: HttpMethod,
StatusCode: HttpStatusCode,
@ -17,7 +17,7 @@ export const MdxComponents = (app = false) => ({
Fixme: (props) => <Popout {...props} fixme />,
Link: (props) => <Popout {...props} link />,
Note: (props) => <Popout {...props} note />,
ReadMore: (props) => <ReadMore {...props} app={app} slug={app.state.slug} />,
ReadMore,
Related: (props) => <Popout {...props} related />,
Tip: (props) => <Popout {...props} tip />,
Warning: (props) => <Popout {...props} warning />,
@ -31,7 +31,7 @@ export const MdxComponents = (app = false) => ({
),
Tab,
Tabs,
Example: (props) => <Example {...props} app={app} />,
Example,
PatternDocs: Fixme,
PatternOptions: Fixme,
})

View file

@ -2,13 +2,12 @@ import { MainSections, ActiveSection, ns as navNs } from './primary.mjs'
export const ns = navNs
export const AsideNavigation = ({ app, mobileOnly = false, before = [], after = [] }) => (
export const AsideNavigation = ({ mobileOnly = false, before = [], after = [] }) => (
<aside
className={`
fixed top-0 right-0 h-screen
overflow-y-auto z-20
bg-base-100 text-base-content
${app.state?.menu?.main ? '' : 'translate-x-[-120%]'} transition-transform
px-0 pb-20 pt-8 shrink-0
lg:w-auto
@ -22,9 +21,9 @@ export const AsideNavigation = ({ app, mobileOnly = false, before = [], after =
>
<div className="w-screen lg:w-auto">
{before}
<MainSections app={app} />
<div className=" border border-l-0 border-r-0 border-b-0 border-dashed border-base-300 mt-4 pt-4">
<ActiveSection app={app} />
<MainSections />
<div className="border border-l-0 border-r-0 border-b-0 border-dashed border-base-300 mt-4 pt-4">
<ActiveSection />
</div>
{after}
</div>

View file

@ -1,3 +1,5 @@
import { NavigationContext } from 'shared/context/navigation-context.mjs'
import { useContext } from 'react'
import Link from 'next/link'
import orderBy from 'lodash.orderby'
import {
@ -195,13 +197,14 @@ export const Icons = ({
return <ul className={ulClasses}>{output}</ul>
}
export const MainSections = ({ app }) => {
if (!app.state.sections) return null
export const MainSections = () => {
const { sections = false, slug } = useContext(NavigationContext)
if (!sections) return null
// Ensure each page as an `o` key so we can put them in order
const sortableSections = app.state.sections.map((s) => ({ ...s, o: s.o ? s.o : s.t }))
const sortableSections = sections.map((s) => ({ ...s, o: s.o ? s.o : s.t }))
const output = []
for (const page of orderBy(sortableSections, ['o', 't'])) {
const act = isActive(page.s, app.state.slug)
const act = isActive(page.s, slug)
const txt = (
<>
{icons[page.s] ? (
@ -252,45 +255,47 @@ export const MainSections = ({ app }) => {
return <ul>{output}</ul>
}
const getCrumb = (index, app) => app.state.crumbs[index].s.split('/').pop()
const getCrumb = (index, crumbs) => crumbs[index].s.split('/').pop()
export const ActiveSection = () => {
// Get navigation context
const { sections = false, crumbs = [], nav = {}, slug } = useContext(NavigationContext)
export const ActiveSection = ({ app }) => {
// Don't bother if we don't know where we are
if (!app.state.crumbs || !Array.isArray(app.state.crumbs) || app.state.crumbs.length < 1)
return null
if (!crumbs || !Array.isArray(crumbs) || crumbs.length < 1) return null
let slice = 1
let nodes = app.state.nav
let nodes = nav
// Some sections are further trimmed
if (app.state.crumbs && app.state.crumbs[0].s === 'docs') {
if (app.state.crumbs.length > 1 && app.state.crumbs[1].s === 'docs/faq') {
if (crumbs && crumbs[0].s === 'docs') {
if (crumbs.length > 1 && crumbs[1].s === 'docs/faq') {
slice = 2
nodes = app.state.nav[getCrumb(1, app)]
} else if (app.state.crumbs.length === 2) {
nodes = nav[getCrumb(1, crumbs)]
} else if (crumbs.length === 2) {
slice = 2
nodes = app.state.nav[getCrumb(1, app)]
nodes = nav[getCrumb(1, crumbs)]
} else if (
app.state.crumbs.length === 4 &&
app.state.crumbs[1].s === 'docs/patterns' &&
app.state.crumbs[3].s.split('/').pop() === 'options'
crumbs.length === 4 &&
crumbs[1].s === 'docs/patterns' &&
crumbs[3].s.split('/').pop() === 'options'
) {
slice = 4
nodes = app.state.nav[getCrumb(1, app)][getCrumb(2, app)][getCrumb(3, app)]
} else if (app.state.crumbs.length > 2 && app.state.crumbs[1].s === 'docs/patterns') {
nodes = nav[getCrumb(1, crumbs)][getCrumb(2, crumbs)][getCrumb(3, crumbs)]
} else if (crumbs.length > 2 && crumbs[1].s === 'docs/patterns') {
slice = 3
nodes = app.state.nav[getCrumb(1, app)][getCrumb(2, app)]
nodes = nav[getCrumb(1, crumbs)][getCrumb(2, crumbs)]
}
}
return (
<div>
{app.state.crumbs ? (
{crumbs ? (
<div className="pl-4 my-2">
<Breadcrumbs crumbs={app.state.crumbs.slice(0, slice)} />
<Breadcrumbs crumbs={crumbs.slice(0, slice)} />
</div>
) : null}
<div className="pr-2">
<SubLevel hasChildren={1} nodes={nodes} active={app.state.slug} />
<SubLevel hasChildren={1} nodes={nodes} active={slug} />
</div>
</div>
)

View file

@ -1,7 +1,16 @@
export const Ribbon = ({ loading = false }) => (
<div
className={`flex flex-col justify-between p-0 transition-transform
${loading ? 'theme-gradient loading h-1' : 'h-0 -translate-y-1'}
`}
></div>
)
// Hooks
import { useContext } from 'react'
// Context
import { LoadingContext } from 'shared/context/loading-context.mjs'
export const Ribbon = () => {
const { loading } = useContext(LoadingContext)
return (
<div
className={`flex flex-col justify-between p-0 transition-transform
${loading ? 'theme-gradient loading h-1' : 'h-0 -translate-y-1'}
`}
></div>
)
}

View file

@ -0,0 +1,11 @@
import { ModalContextProvider } from 'shared/context/modal-context.mjs'
import { LoadingContextProvider } from 'shared/context/loading-context.mjs'
import { NavigationContextProvider } from 'shared/context/navigation-context.mjs'
export const ContextWrapper = ({ children }) => (
<ModalContextProvider>
<LoadingContextProvider>
<NavigationContextProvider>{children}</NavigationContextProvider>
</LoadingContextProvider>
</ModalContextProvider>
)

View file

@ -1,5 +1,5 @@
// Dependencies
import React, { useState, useEffect } from 'react'
import React, { useState, useEffect, useContext } from 'react'
import { useHotkeys } from 'react-hotkeys-hook'
// Hooks
import { useTheme } from 'shared/hooks/use-theme.mjs'
@ -9,19 +9,20 @@ import { LayoutWrapper, ns as layoutNs } from 'site/components/wrappers/layout.m
import { DocsLayout, ns as docsNs } from 'site/components/layouts/docs.mjs'
import { Feeds } from 'site/components/feeds.mjs'
import { Spinner } from 'shared/components/spinner.mjs'
import { ModalContextProvider, ModalContextConsumer } from 'shared/context/modal-context.mjs'
import { ModalContext } from 'shared/context/modal-context.mjs'
import { NavigationContext } from 'shared/context/navigation-context.mjs'
export const ns = [...new Set([...layoutNs, ...docsNs])]
/* This component should wrap all page content */
export const PageWrapper = ({
noSearch = false,
app = false,
layout = DocsLayout,
footer = true,
children = [],
title = <Spinner className="h-12 w-12 animate-spin text-primary" />,
}) => {
export const PageWrapper = (props) => {
/*
* Deconstruct props
*/
const { layout = DocsLayout, footer = true, children = [], path = [], locale = 'en' } = props
// Title is typically set in props.t but check props.title too
const pageTitle = props.t ? props.t : props.title ? props.title : null
/*
* This forces a re-render upon initial bootstrap of the app
* This is needed to avoid hydration errors because theme can't be set reliably in SSR
@ -30,6 +31,23 @@ export const PageWrapper = ({
const [currentTheme, setCurrentTheme] = useState()
useEffect(() => setCurrentTheme(theme), [currentTheme, theme])
/*
* Contexts
*/
const { modalContent } = useContext(ModalContext)
const { title, setTitle, setNavigation } = useContext(NavigationContext)
/*
* Update navigation context with title and path
*/
useEffect(() => {
setNavigation({
title: pageTitle,
locale,
path,
})
}, [path, title, pageTitle])
/*
* Hotkeys (keyboard actions)
*/
@ -43,34 +61,24 @@ export const PageWrapper = ({
const [search, setSearch] = useState(false)
// Helper object to pass props down (keeps things DRY)
const childProps = {
app: app,
footer,
search,
setSearch,
toggleSearch: () => setSearch(!search),
noSearch: noSearch,
title: app.state.title ? app.state.title : title,
}
const childProps = { footer, title: pageTitle }
// Make layout prop into a (uppercase) component
const Layout = layout
// Return wrapper
return (
<ModalContextProvider>
<SwipeWrapper app={app}>
<div
data-theme={currentTheme} // This facilitates CSS selectors
key={currentTheme} // This forces the data-theme update
>
<Feeds />
<LayoutWrapper {...childProps}>
{Layout ? <Layout {...childProps}>{children}</Layout> : children}
</LayoutWrapper>
<ModalContextConsumer />
</div>
</SwipeWrapper>
</ModalContextProvider>
<SwipeWrapper>
<div
data-theme={currentTheme} // This facilitates CSS selectors
key={currentTheme} // This forces the data-theme update
>
<Feeds />
<LayoutWrapper {...childProps}>
{Layout ? <Layout {...childProps}>{children}</Layout> : children}
</LayoutWrapper>
{modalContent}
</div>
</SwipeWrapper>
)
}

View file

@ -0,0 +1,24 @@
import React, { useState } from 'react'
export const LoadingContext = React.createContext(false)
export const LoadingContextProvider = ({ children }) => {
function stopLoading() {
__setLoading({ ...__loading, loading: false })
}
function startLoading() {
__setLoading({ ...__loading, loading: true })
}
function setLoading(loading) {
__setLoading({ ...__loading, loading })
}
const [__loading, __setLoading] = useState({
setLoading,
startLoading,
stopLoading,
loading: false,
})
return <LoadingContext.Provider value={__loading}>{children}</LoadingContext.Provider>
}

View file

@ -19,9 +19,3 @@ export const ModalContextProvider = ({ children }) => {
return <ModalContext.Provider value={__modal}>{children}</ModalContext.Provider>
}
export const ModalContextConsumer = ({}) => {
const { modalContent } = useContext(ModalContext)
return modalContent ? modalContent : null
}

View file

@ -0,0 +1,37 @@
import React, { useState, useContext } from 'react'
import { useNavigation } from 'site/hooks/use-navigation.mjs'
const defaultNavigationContext = {
path: [],
title: 'FIXME: No title (default)',
locale: 'en',
crumbs: [],
}
export const NavigationContext = React.createContext(defaultNavigationContext)
export const NavigationContextProvider = ({ children }) => {
function setNavigation(newValues) {
setValue({
...value,
...newValues,
setNavigation,
})
}
const [value, setValue] = useState({
...defaultNavigationContext,
setNavigation,
})
const navState = useNavigation({
path: value.path,
locale: value.locale,
})
return (
<NavigationContext.Provider value={{ ...value, ...navState }}>
{children}
</NavigationContext.Provider>
)
}

View file

@ -5,6 +5,7 @@ import createPersistedState from 'use-persisted-state'
*/
const usePersistedAccount = createPersistedState('fs-account')
const usePersistedToken = createPersistedState('fs-token')
const usePersistedSeenUser = createPersistedState('fs-seen-user')
/*
* Make it possible to always check for account.username
@ -18,9 +19,10 @@ export function useAccount() {
// (persisted) State (saved to local storage)
const [account, setAccount] = usePersistedAccount(noAccount)
const [token, setToken] = usePersistedToken(null)
const [seenUser, setSeenUser] = usePersistedSeenUser(false)
// Clear user data. This gets called when signing out
const clear = () => {
const logout = () => {
setAccount(noAccount)
setToken(null)
}
@ -30,6 +32,8 @@ export function useAccount() {
setAccount,
token,
setToken,
clear,
seenUser,
setSeenUser,
logout,
}
}

View file

@ -1,66 +0,0 @@
// Hooks
import { useState, useEffect } from 'react'
import { useNavigation } from 'site/hooks/use-navigation.mjs'
// Dependencies
import get from 'lodash.get'
import set from 'lodash.set'
import unset from 'lodash.unset'
const defaultState = {
loading: false,
modal: null,
menu: {
main: null,
},
}
/*
* The useApp hook
*/
export function useApp(props = {}) {
const { page = {}, loadState = {} } = props
const { path = false } = page
if (!path) throw 'You MUST pass a page.path prop to the useApp hook'
const navState = useNavigation(path)
// React state
const [state, setState] = useState(() => ({ ...defaultState, ...loadState }))
useEffect(() => {
// Force update of navigation info (nav, title, crumbs) on each page change
if (path) setState({ ...state, ...navState })
}, [path, state.slug, state.title])
/*
* Helper methods for partial state updates
*/
const updateState = (path, value) => setState(set({ ...state }, path, value))
const unsetState = (path) => setState(unset({ ...state }, path))
/*
* Helper methods for specific state updates
*/
const setModal = (content) => updateState('modal', content)
const closeModal = () => updateState('modal', null)
const closeMenu = (name) =>
get(state, `menu.${name}`, false) ? updateState(`menu.${name}`, false) : null
const startLoading = () => updateState('loading', true)
const stopLoading = () => updateState('loading', false)
return {
// All-purpose React state object
state,
setState,
updateState,
unsetState,
// Helper methods
setModal,
closeModal,
closeMenu,
startLoading,
stopLoading,
}
}

View file

@ -0,0 +1,14 @@
import { useEffect, useContext } from 'react'
import { NavigationContext } from 'shared/context/navigation-context.mjs'
export const useNavigation = ({ page }) => {
const all = useContext(NavigationContext)
useEffect(() => {}, [page.path, page.t])
return {
title: page.t,
slug: page.s,
order: page.o,
}
}

150
yarn.lock
View file

@ -3211,17 +3211,17 @@
hey-listen "^1.0.8"
tslib "^2.3.1"
"@next/bundle-analyzer@13.3.0":
version "13.3.0"
resolved "https://registry.yarnpkg.com/@next/bundle-analyzer/-/bundle-analyzer-13.3.0.tgz#3dc6224d2020ee7198423b9989a173d19a069eff"
integrity sha512-MNtsx3MFh/kqq2DDa4ladjzfUV8McncEowNvt/s7Z9UjvX6DkuJGFRdjq/RYtxWvjQAsDgiOsgyIOLzDbvQo3g==
"@next/bundle-analyzer@13.3.1":
version "13.3.1"
resolved "https://registry.yarnpkg.com/@next/bundle-analyzer/-/bundle-analyzer-13.3.1.tgz#df6c8c969646456b694dc1cd41d2847eae331248"
integrity sha512-Qkd0ENOczIyJlKjjN7ENwQ2S5B6wqrTnVlzfWqAImw38VNgaVqZRgaM9Ph8JM43E6iLPjsMOHh8EN/7FxHyQ/g==
dependencies:
webpack-bundle-analyzer "4.7.0"
"@next/env@13.3.0":
version "13.3.0"
resolved "https://registry.yarnpkg.com/@next/env/-/env-13.3.0.tgz#cc2e49f03060a4684ce7ec7fd617a21bc5b9edba"
integrity sha512-AjppRV4uG3No7L1plinoTQETH+j2F10TEnrMfzbTUYwze5sBUPveeeBAPZPm8OkJZ1epq9OyYKhZrvbD6/9HCQ==
"@next/env@13.3.1":
version "13.3.1"
resolved "https://registry.yarnpkg.com/@next/env/-/env-13.3.1.tgz#589707043065f6b71d411ed9b8f1ffd057c0fd4a"
integrity sha512-EDtCoedIZC7JlUQ3uaQpSc4aVmyhbLHmQVALg7pFfQgOTjgSnn7mKtA0DiCMkYvvsx6aFb5octGMtWrOtGXW9A==
"@next/eslint-plugin-next@13.3.0":
version "13.3.0"
@ -3230,50 +3230,57 @@
dependencies:
glob "7.1.7"
"@next/swc-darwin-arm64@13.3.0":
version "13.3.0"
resolved "https://registry.yarnpkg.com/@next/swc-darwin-arm64/-/swc-darwin-arm64-13.3.0.tgz#38f18e0639cd4c7edc6a38d4b83fe00f38eea4f2"
integrity sha512-DmIQCNq6JtccLPPBzf0dgh2vzMWt5wjxbP71pCi5EWpWYE3MsP6FcRXi4MlAmFNDQOfcFXR2r7kBeG1LpZUh1w==
"@next/eslint-plugin-next@13.3.1":
version "13.3.1"
resolved "https://registry.yarnpkg.com/@next/eslint-plugin-next/-/eslint-plugin-next-13.3.1.tgz#aa08601f1fec5e1ffbb5850761585734f110345a"
integrity sha512-Hpd74UrYGF+bq9bBSRDXRsRfaWkPpcwjhvachy3sr/R/5fY6feC0T0s047pUthyqcaeNsqKOY1nUGQQJNm4WyA==
dependencies:
glob "7.1.7"
"@next/swc-darwin-x64@13.3.0":
version "13.3.0"
resolved "https://registry.yarnpkg.com/@next/swc-darwin-x64/-/swc-darwin-x64-13.3.0.tgz#b670ed1fd1d231aa21279173ec52e3ad56dc6aeb"
integrity sha512-oQoqFa88OGgwnYlnAGHVct618FRI/749se0N3S8t9Bzdv5CRbscnO0RcX901+YnNK4Q6yeiizfgO3b7kogtsZg==
"@next/swc-darwin-arm64@13.3.1":
version "13.3.1"
resolved "https://registry.yarnpkg.com/@next/swc-darwin-arm64/-/swc-darwin-arm64-13.3.1.tgz#2c9719dd10a9cdf63bf50a7576b05dcf78999fe8"
integrity sha512-UXPtriEc/pBP8luSLSCZBcbzPeVv+SSjs9cH/KygTbhmACye8/OOXRZO13Z2Wq1G0gLmEAIHQAOuF+vafPd2lw==
"@next/swc-linux-arm64-gnu@13.3.0":
version "13.3.0"
resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-13.3.0.tgz#b114935f6b4c94c123f6cac55a4823d483209ba5"
integrity sha512-Wzz2p/WqAJUqTVoLo6H18WMeAXo3i+9DkPDae4oQG8LMloJ3if4NEZTnOnTUlro6cq+S/W4pTGa97nWTrOjbGw==
"@next/swc-darwin-x64@13.3.1":
version "13.3.1"
resolved "https://registry.yarnpkg.com/@next/swc-darwin-x64/-/swc-darwin-x64-13.3.1.tgz#0be90342c89e53a390ccd9bece15f7f5cd480049"
integrity sha512-lT36yYxosCfLtplFzJWgo0hrPu6/do8+msgM7oQkPeohDNdhjtjFUgOOwdSnPublLR6Mo2Ym4P/wl5OANuD2bw==
"@next/swc-linux-arm64-musl@13.3.0":
version "13.3.0"
resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-13.3.0.tgz#67a57309f8761c7d00d629d6785d56ed0567a0d2"
integrity sha512-xPVrIQOQo9WXJYgmoTlMnAD/HlR/1e1ZIWGbwIzEirXBVBqMARUulBEIKdC19zuvoJ477qZJgBDCKtKEykCpyQ==
"@next/swc-linux-arm64-gnu@13.3.1":
version "13.3.1"
resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-13.3.1.tgz#a7353265839f8b8569a346a444dc3ab3770d297e"
integrity sha512-wRb76nLWJhonH8s3kxC/1tFguEkeOPayIwe9mkaz1G/yeS3OrjeyKMJsb4+Kdg0zbTo53bNCOl59NNtDM7yyyw==
"@next/swc-linux-x64-gnu@13.3.0":
version "13.3.0"
resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-13.3.0.tgz#11bd2bea7c00b40be111c0dd16e71171f3792086"
integrity sha512-jOFlpGuPD7W2tuXVJP4wt9a3cpNxWAPcloq5EfMJRiXsBBOjLVFZA7boXYxEBzSVgUiVVr1V9T0HFM7pULJ1qA==
"@next/swc-linux-arm64-musl@13.3.1":
version "13.3.1"
resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-13.3.1.tgz#24552e6102c350e372f83f505a1d93c880551a50"
integrity sha512-qz3BzjJRZ16Iq/jrp+pjiYOc0jTjHlfmxQmZk9x/+5uhRP6/eWQSTAPVJ33BMo6oK5O5N4644OgTAbzXzorecg==
"@next/swc-linux-x64-musl@13.3.0":
version "13.3.0"
resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-13.3.0.tgz#d57e99f85890799b78719c3ea32a4624de8d701b"
integrity sha512-2OwKlzaBgmuet9XYHc3KwsEilzb04F540rlRXkAcjMHL7eCxB7uZIGtsVvKOnQLvC/elrUegwSw1+5f7WmfyOw==
"@next/swc-linux-x64-gnu@13.3.1":
version "13.3.1"
resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-13.3.1.tgz#5f335a683b6eafa52307b12af97782993b6c45ff"
integrity sha512-6mgkLmwlyWlomQmpl21I3hxgqE5INoW4owTlcLpNsd1V4wP+J46BlI/5zV5KWWbzjfncIqzXoeGs5Eg+1GHODA==
"@next/swc-win32-arm64-msvc@13.3.0":
version "13.3.0"
resolved "https://registry.yarnpkg.com/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-13.3.0.tgz#0c209aa35d1c88b01e78259a89cd68f4139b5093"
integrity sha512-OeHiA6YEvndxT46g+rzFK/MQTfftKxJmzslERMu9LDdC6Kez0bdrgEYed5eXFK2Z1viKZJCGRlhd06rBusyztA==
"@next/swc-linux-x64-musl@13.3.1":
version "13.3.1"
resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-13.3.1.tgz#58e5aad6f97203a0788783f66324456c8f9cdb50"
integrity sha512-uqm5sielhQmKJM+qayIhgZv1KlS5pqTdQ99b+Z7hMWryXS96qE0DftTmMZowBcUL6x7s2vSXyH5wPtO1ON7LBg==
"@next/swc-win32-ia32-msvc@13.3.0":
version "13.3.0"
resolved "https://registry.yarnpkg.com/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-13.3.0.tgz#52ae74da1dd6d840c3743923367d27ed013803dd"
integrity sha512-4aB7K9mcVK1lYEzpOpqWrXHEZympU3oK65fnNcY1Qc4HLJFLJj8AViuqQd4jjjPNuV4sl8jAwTz3gN5VNGWB7w==
"@next/swc-win32-arm64-msvc@13.3.1":
version "13.3.1"
resolved "https://registry.yarnpkg.com/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-13.3.1.tgz#f8ed1badab57ed4503969758754e6fb0cf326753"
integrity sha512-WomIiTj/v3LevltlibNQKmvrOymNRYL+a0dp5R73IwPWN5FvXWwSELN/kiNALig/+T3luc4qHNTyvMCp9L6U5Q==
"@next/swc-win32-x64-msvc@13.3.0":
version "13.3.0"
resolved "https://registry.yarnpkg.com/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-13.3.0.tgz#db7b55fee834dc8c2c484c696469e65bae2ee770"
integrity sha512-Reer6rkLLcoOvB0dd66+Y7WrWVFH7sEEkF/4bJCIfsSKnTStTYaHtwIJAwbqnt9I392Tqvku0KkoqZOryWV9LQ==
"@next/swc-win32-ia32-msvc@13.3.1":
version "13.3.1"
resolved "https://registry.yarnpkg.com/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-13.3.1.tgz#7f599c8975b09ee5527cc49b9e5a4d13be50635a"
integrity sha512-M+PoH+0+q658wRUbs285RIaSTYnGBSTdweH/0CdzDgA6Q4rBM0sQs4DHmO3BPP0ltCO/vViIoyG7ks66XmCA5g==
"@next/swc-win32-x64-msvc@13.3.1":
version "13.3.1"
resolved "https://registry.yarnpkg.com/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-13.3.1.tgz#192d43ab44ebb98bd4f5865d0e1d7ce62703182f"
integrity sha512-Sl1F4Vp5Z1rNXWZYqJwMuWRRol4bqOB6+/d7KqkgQ4AcafKPN1PZmpkCoxv4UFHtFNIB7EotnuIhtXu3zScicQ==
"@nicolo-ribaudo/eslint-scope-5-internals@5.1.1-v1":
version "5.1.1-v1"
@ -4277,10 +4284,10 @@
resolved "https://registry.yarnpkg.com/@socket.io/component-emitter/-/component-emitter-3.1.0.tgz#96116f2a912e0c02817345b3c10751069920d553"
integrity sha512-+9jVqKhRSpsc591z5vX+X5Yyw+he/HCB4iQ/RYxw35CEPaY1gnsNE43nf9n9AaYjAQrTiI/mOwKUKdUs9vf7Xg==
"@swc/helpers@0.4.14":
version "0.4.14"
resolved "https://registry.yarnpkg.com/@swc/helpers/-/helpers-0.4.14.tgz#1352ac6d95e3617ccb7c1498ff019654f1e12a74"
integrity sha512-4C7nX/dvpzB7za4Ql9K81xK3HPxCpHMgwTZVyf+9JQ6VUbn9jjZVN7/Nkdz/Ugzs2CSjqnL/UPXroiVBVHUWUw==
"@swc/helpers@0.5.0":
version "0.5.0"
resolved "https://registry.yarnpkg.com/@swc/helpers/-/helpers-0.5.0.tgz#bf1d807b60f7290d0ec763feea7ccdeda06e85f1"
integrity sha512-SjY/p4MmECVVEWspzSRpQEM3sjR17sP8PbGxELWrT+YZMBfiUyt1MRUNjMV23zohwlG2HYtCQOsCwsTHguXkyg==
dependencies:
tslib "^2.4.0"
@ -8307,7 +8314,22 @@ escodegen@^2.0.0:
optionalDependencies:
source-map "~0.6.1"
eslint-config-next@13.3.0, eslint-config-next@^13.0.6:
eslint-config-next@13.3.1:
version "13.3.1"
resolved "https://registry.yarnpkg.com/eslint-config-next/-/eslint-config-next-13.3.1.tgz#a4441b9b8a708383e9444492c21bab0099851d90"
integrity sha512-DieA5djybeE3Q0IqnDXihmhgRSp44x1ywWBBpVRA9pSx+m5Icj8hFclx7ffXlAvb9MMLN6cgj/hqJ4lka/QmvA==
dependencies:
"@next/eslint-plugin-next" "13.3.1"
"@rushstack/eslint-patch" "^1.1.3"
"@typescript-eslint/parser" "^5.42.0"
eslint-import-resolver-node "^0.3.6"
eslint-import-resolver-typescript "^3.5.2"
eslint-plugin-import "^2.26.0"
eslint-plugin-jsx-a11y "^6.5.1"
eslint-plugin-react "^7.31.7"
eslint-plugin-react-hooks "^4.5.0"
eslint-config-next@^13.0.6:
version "13.3.0"
resolved "https://registry.yarnpkg.com/eslint-config-next/-/eslint-config-next-13.3.0.tgz#c302fbecfe2b976ea306f7622af637ef9d9e3802"
integrity sha512-6YEwmFBX0VjBd3ODGW9df0Is0FLaRFdMN8eAahQG9CN6LjQ28J8AFr19ngxqMSg7Qv6Uca/3VeeBosJh1bzu0w==
@ -13844,27 +13866,27 @@ next-i18next@13.2.2:
hoist-non-react-statics "^3.3.2"
i18next-fs-backend "^2.1.1"
next@13.3.0:
version "13.3.0"
resolved "https://registry.yarnpkg.com/next/-/next-13.3.0.tgz#40632d303d74fc8521faa0a5bf4a033a392749b1"
integrity sha512-OVTw8MpIPa12+DCUkPqRGPS3thlJPcwae2ZL4xti3iBff27goH024xy4q2lhlsdoYiKOi8Kz6uJoLW/GXwgfOA==
next@13.3.1:
version "13.3.1"
resolved "https://registry.yarnpkg.com/next/-/next-13.3.1.tgz#17625f7423db2e059d71b41bd9031756cf2b33bc"
integrity sha512-eByWRxPzKHs2oQz1yE41LX35umhz86ZSZ+mYyXBqn2IBi2hyUqxBA88avywdr4uyH+hCJczegGsDGWbzQA5Rqw==
dependencies:
"@next/env" "13.3.0"
"@swc/helpers" "0.4.14"
"@next/env" "13.3.1"
"@swc/helpers" "0.5.0"
busboy "1.6.0"
caniuse-lite "^1.0.30001406"
postcss "8.4.14"
styled-jsx "5.1.1"
optionalDependencies:
"@next/swc-darwin-arm64" "13.3.0"
"@next/swc-darwin-x64" "13.3.0"
"@next/swc-linux-arm64-gnu" "13.3.0"
"@next/swc-linux-arm64-musl" "13.3.0"
"@next/swc-linux-x64-gnu" "13.3.0"
"@next/swc-linux-x64-musl" "13.3.0"
"@next/swc-win32-arm64-msvc" "13.3.0"
"@next/swc-win32-ia32-msvc" "13.3.0"
"@next/swc-win32-x64-msvc" "13.3.0"
"@next/swc-darwin-arm64" "13.3.1"
"@next/swc-darwin-x64" "13.3.1"
"@next/swc-linux-arm64-gnu" "13.3.1"
"@next/swc-linux-arm64-musl" "13.3.1"
"@next/swc-linux-x64-gnu" "13.3.1"
"@next/swc-linux-x64-musl" "13.3.1"
"@next/swc-win32-arm64-msvc" "13.3.1"
"@next/swc-win32-ia32-msvc" "13.3.1"
"@next/swc-win32-x64-msvc" "13.3.1"
nise@^5.1.4:
version "5.1.4"