chore: Rename react-components to react
This commit is contained in:
parent
03f5eb75f6
commit
2357fdfaca
132 changed files with 1599 additions and 70 deletions
|
@ -225,12 +225,6 @@
|
||||||
|
|
||||||
- Calling `Path.join` with a second parameter to indicate that the resulting paths most be closed is now deprecated and will be removed in FreeSewing v4.
|
- Calling `Path.join` with a second parameter to indicate that the resulting paths most be closed is now deprecated and will be removed in FreeSewing v4.
|
||||||
|
|
||||||
### react-components
|
|
||||||
|
|
||||||
#### Added
|
|
||||||
|
|
||||||
- This Pattern component now supports translation of nested arrays of strings, giving you more flexibility to concatenate translated parts of strings
|
|
||||||
|
|
||||||
|
|
||||||
## 3.1.0 (2023-12-26)
|
## 3.1.0 (2023-12-26)
|
||||||
|
|
||||||
|
|
|
@ -124,7 +124,7 @@ plugintest:
|
||||||
'@freesewing/plugin-sprinkle': *freesewing
|
'@freesewing/plugin-sprinkle': *freesewing
|
||||||
'@freesewing/plugin-svgattr': *freesewing
|
'@freesewing/plugin-svgattr': *freesewing
|
||||||
'@freesewing/plugin-theme': *freesewing
|
'@freesewing/plugin-theme': *freesewing
|
||||||
react-components:
|
react:
|
||||||
_:
|
_:
|
||||||
axios: *axios
|
axios: *axios
|
||||||
html-react-parser: "^5.0.7"
|
html-react-parser: "^5.0.7"
|
||||||
|
@ -399,7 +399,7 @@ sde:
|
||||||
"@freesewing/plugin-theme": *freesewing
|
"@freesewing/plugin-theme": *freesewing
|
||||||
"@freesewing/plugin-i18n": *freesewing
|
"@freesewing/plugin-i18n": *freesewing
|
||||||
"@freesewing/snapseries": *freesewing
|
"@freesewing/snapseries": *freesewing
|
||||||
"@freesewing/react-components": *freesewing
|
"@freesewing/react": *freesewing
|
||||||
'@mdx-js/mdx': *mdx
|
'@mdx-js/mdx': *mdx
|
||||||
'@mdx-js/react': *mdx
|
'@mdx-js/react': *mdx
|
||||||
'@mdx-js/runtime': *mdxRuntime
|
'@mdx-js/runtime': *mdxRuntime
|
||||||
|
|
|
@ -6,7 +6,7 @@ customBuild:
|
||||||
- new-design
|
- new-design
|
||||||
- prettier-config
|
- prettier-config
|
||||||
- core-plugins
|
- core-plugins
|
||||||
- react-components
|
- react
|
||||||
- rehype-jargon
|
- rehype-jargon
|
||||||
- rehype-highlight-lines
|
- rehype-highlight-lines
|
||||||
skipTests:
|
skipTests:
|
||||||
|
@ -67,13 +67,14 @@ packageJson:
|
||||||
prettier-config:
|
prettier-config:
|
||||||
type: 'module'
|
type: 'module'
|
||||||
exports: { .: './index.json' }
|
exports: { .: './index.json' }
|
||||||
scripts: { build: 'echo "prettier-config: No build required" && exit 0' }
|
scripts:
|
||||||
|
build: 'echo "prettier-config: No build required" && exit 0'
|
||||||
keywords:
|
keywords:
|
||||||
- prettier
|
- prettier
|
||||||
files:
|
files:
|
||||||
- index.json
|
- index.json
|
||||||
- package.json
|
- package.json
|
||||||
react-components:
|
react:
|
||||||
exports:
|
exports:
|
||||||
".":
|
".":
|
||||||
"internal": "./src/index.mjs"
|
"internal": "./src/index.mjs"
|
||||||
|
@ -81,6 +82,22 @@ packageJson:
|
||||||
"./linedrawings": "./src/linedrawings/index.mjs"
|
"./linedrawings": "./src/linedrawings/index.mjs"
|
||||||
"./pattern": "./src/pattern/index.mjs"
|
"./pattern": "./src/pattern/index.mjs"
|
||||||
"./xray": "./src/pattern-xray/index.mjs"
|
"./xray": "./src/pattern-xray/index.mjs"
|
||||||
|
"./config/freesewing": "./config/freesewing/index.mjs"
|
||||||
|
# Context
|
||||||
|
"./context/loadingStatus": "./context/loadingStatus/index.mjs"
|
||||||
|
# Hooks
|
||||||
|
"./hooks/useBackend": "./hooks/useBackend/index.mjs"
|
||||||
|
# Lib
|
||||||
|
"./lib/RestClient": "./lib/RestClient/index.mjs"
|
||||||
|
files:
|
||||||
|
- "components/**"
|
||||||
|
- "hooks/**"
|
||||||
|
- "lib/**"
|
||||||
|
- "README.md"
|
||||||
|
scripts:
|
||||||
|
lint: "eslint 'components/**/*.mjs' 'hooks/**/*.mjs' 'lib/**/*.mjs' 'context/**/*.mjs' 'config/**/*.mjs'"
|
||||||
|
prettier: "npx prettier --write 'components/**/*.mjs' 'hooks/**/*.mjs' 'lib/**/*.mjs' 'context/**/*.mjs' 'config/**/*.mjs'"
|
||||||
|
|
||||||
rehype-hightlight-lines:
|
rehype-hightlight-lines:
|
||||||
private: true
|
private: true
|
||||||
sandy:
|
sandy:
|
||||||
|
|
|
@ -46,7 +46,7 @@ rehype-jargon:
|
||||||
lint: "npx eslint 'src/*.mjs'"
|
lint: "npx eslint 'src/*.mjs'"
|
||||||
snapseries:
|
snapseries:
|
||||||
lint: "npx eslint 'src/*.mjs'"
|
lint: "npx eslint 'src/*.mjs'"
|
||||||
react-components:
|
react:
|
||||||
lint: "eslint 'src/**/*.mjs'"
|
lint: "eslint 'src/**/*.mjs'"
|
||||||
|
|
||||||
# Sites go here
|
# Sites go here
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
"models": "Body measurements data for a range of default sizes",
|
"models": "Body measurements data for a range of default sizes",
|
||||||
"new-design": "Initializer package for a new FreeSewing design: npx @freesewing/new-design",
|
"new-design": "Initializer package for a new FreeSewing design: npx @freesewing/new-design",
|
||||||
"prettier-config": "FreeSewing's shared configuration for prettier",
|
"prettier-config": "FreeSewing's shared configuration for prettier",
|
||||||
"react-components": "React components by/for FreeSewing",
|
"react": "React components, hooks and context by/for FreeSewing",
|
||||||
"snapseries": "A series of common sizes for elastics and other series to be used with snapped percentage options",
|
"snapseries": "A series of common sizes for elastics and other series to be used with snapped percentage options",
|
||||||
"rehype-jargon": "A Rehype plugin for jargon terms",
|
"rehype-jargon": "A Rehype plugin for jargon terms",
|
||||||
"rehype-highlight-lines": "A Rehype plugin to add highlighted lines to code blocks"
|
"rehype-highlight-lines": "A Rehype plugin to add highlighted lines to code blocks"
|
||||||
|
|
|
@ -1,12 +1,6 @@
|
||||||
# Change log for: @freesewing/react-components
|
# Change log for: @freesewing/react
|
||||||
|
|
||||||
|
|
||||||
## 3.2.0 (2024-02-11)
|
|
||||||
|
|
||||||
### Added
|
|
||||||
|
|
||||||
- This Pattern component now supports translation of nested arrays of strings, giving you more flexibility to concatenate translated parts of strings
|
|
||||||
|
|
||||||
## 3.0.0 (2023-09-30)
|
## 3.0.0 (2023-09-30)
|
||||||
|
|
||||||
### Changed
|
### Changed
|
|
@ -1,12 +1,12 @@
|
||||||
<p align='center'><a
|
<p align='center'><a
|
||||||
href="https://www.npmjs.com/package/@freesewing/react-components"
|
href="https://www.npmjs.com/package/@freesewing/react"
|
||||||
title="@freesewing/react-components on NPM"
|
title="@freesewing/react on NPM"
|
||||||
><img src="https://img.shields.io/npm/v/@freesewing/react-components.svg"
|
><img src="https://img.shields.io/npm/v/@freesewing/react.svg"
|
||||||
alt="@freesewing/react-components on NPM"/>
|
alt="@freesewing/react on NPM"/>
|
||||||
</a><a
|
</a><a
|
||||||
href="https://opensource.org/licenses/MIT"
|
href="https://opensource.org/licenses/MIT"
|
||||||
title="License: MIT"
|
title="License: MIT"
|
||||||
><img src="https://img.shields.io/npm/l/@freesewing/react-components.svg?label=License"
|
><img src="https://img.shields.io/npm/l/@freesewing/react.svg?label=License"
|
||||||
alt="License: MIT"/>
|
alt="License: MIT"/>
|
||||||
</a><a
|
</a><a
|
||||||
href="https://deepscan.io/dashboard#view=project&tid=2114&pid=2993&bid=23256"
|
href="https://deepscan.io/dashboard#view=project&tid=2114&pid=2993&bid=23256"
|
||||||
|
@ -14,10 +14,10 @@
|
||||||
><img src="https://deepscan.io/api/teams/2114/projects/2993/branches/23256/badge/grade.svg"
|
><img src="https://deepscan.io/api/teams/2114/projects/2993/branches/23256/badge/grade.svg"
|
||||||
alt="Code quality on DeepScan"/>
|
alt="Code quality on DeepScan"/>
|
||||||
</a><a
|
</a><a
|
||||||
href="https://github.com/freesewing/freesewing/issues?q=is%3Aissue+is%3Aopen+label%3Apkg%3Areact-components"
|
href="https://github.com/freesewing/freesewing/issues?q=is%3Aissue+is%3Aopen+label%3Apkg%3Areact"
|
||||||
title="Open issues tagged pkg:react-components"
|
title="Open issues tagged pkg:react"
|
||||||
><img src="https://img.shields.io/github/issues/freesewing/freesewing/pkg:react-components.svg?label=Issues"
|
><img src="https://img.shields.io/github/issues/freesewing/freesewing/pkg:react.svg?label=Issues"
|
||||||
alt="Open issues tagged pkg:react-components"/>
|
alt="Open issues tagged pkg:react"/>
|
||||||
</a><a
|
</a><a
|
||||||
href="#contributors-"
|
href="#contributors-"
|
||||||
title="All Contributors"
|
title="All Contributors"
|
||||||
|
@ -45,9 +45,9 @@
|
||||||
alt="Follow @freesewing_org on Twitter"/>
|
alt="Follow @freesewing_org on Twitter"/>
|
||||||
</a></p>
|
</a></p>
|
||||||
|
|
||||||
# @freesewing/react-components
|
# @freesewing/react
|
||||||
|
|
||||||
React components by/for FreeSewing
|
React components, hooks and context by/for FreeSewing
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@ -65,7 +65,7 @@ hardship, then you should [join us and become a patron](https://freesewing.org/c
|
||||||
|
|
||||||
This repository is the FreeSewing *monorepo* holding all FreeSewing's websites, documentation, designs, plugins, and other NPM packages.
|
This repository is the FreeSewing *monorepo* holding all FreeSewing's websites, documentation, designs, plugins, and other NPM packages.
|
||||||
|
|
||||||
This folder holds: @freesewing/react-components
|
This folder holds: @freesewing/react
|
||||||
|
|
||||||
If you're not entirely sure what to do or how to start, type this command:
|
If you're not entirely sure what to do or how to start, type this command:
|
||||||
|
|
210
packages/react/components/AuthWrapper/index.mjs
Normal file
210
packages/react/components/AuthWrapper/index.mjs
Normal file
|
@ -0,0 +1,210 @@
|
||||||
|
import Link from 'next/link'
|
||||||
|
import { useTranslation } from 'next-i18next'
|
||||||
|
import { useAccount } from 'shared/hooks/use-account.mjs'
|
||||||
|
import { useBackend } from 'shared/hooks/use-backend.mjs'
|
||||||
|
import { roles } from 'config/roles.mjs'
|
||||||
|
import { useEffect, useState } from 'react'
|
||||||
|
import { Loading } from 'shared/components/spinner.mjs'
|
||||||
|
import { horFlexClasses } from 'shared/utils.mjs'
|
||||||
|
import { LockIcon, PlusIcon } from 'shared/components/icons.mjs'
|
||||||
|
import { ConsentForm, ns as gdprNs } from 'shared/components/gdpr/form.mjs'
|
||||||
|
|
||||||
|
export const ns = ['auth', gdprNs]
|
||||||
|
|
||||||
|
const Wrap = ({ children }) => (
|
||||||
|
<div className="m-auto max-w-xl text-center mt-8 p-8">{children}</div>
|
||||||
|
)
|
||||||
|
|
||||||
|
const ContactSupport = ({ t }) => (
|
||||||
|
<div className="flex flex-row items-center justify-center gap-4 mt-8">
|
||||||
|
<Link href="/support" className="btn btn-success w-full">
|
||||||
|
{t('contactSupport')}
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
|
||||||
|
const AuthRequired = ({ t, banner }) => (
|
||||||
|
<Wrap>
|
||||||
|
{banner}
|
||||||
|
<h2>{t('authRequired')}</h2>
|
||||||
|
<p>{t('membersOnly')}</p>
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-2 mt-8">
|
||||||
|
<Link href="/signup" className={`${horFlexClasses} btn btn-secondary w-full`}>
|
||||||
|
<PlusIcon />
|
||||||
|
{t('signUp')}
|
||||||
|
</Link>
|
||||||
|
<Link href="/signin" className={`${horFlexClasses} btn btn-secondary btn-outline w-full`}>
|
||||||
|
<LockIcon />
|
||||||
|
{t('signIn')}
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</Wrap>
|
||||||
|
)
|
||||||
|
|
||||||
|
const AccountInactive = ({ t, banner }) => (
|
||||||
|
<Wrap>
|
||||||
|
{banner}
|
||||||
|
<h1>{t('accountInactive')}</h1>
|
||||||
|
<p>{t('accountInactiveMsg')}</p>
|
||||||
|
<p>{t('signupAgain')}</p>
|
||||||
|
<div className="flex flex-row items-center justify-center gap-4 mt-8">
|
||||||
|
<Link href="/signup" className="btn btn-primary w-full">
|
||||||
|
{t('signUp')}
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</Wrap>
|
||||||
|
)
|
||||||
|
|
||||||
|
const AccountDisabled = ({ t, banner }) => (
|
||||||
|
<Wrap>
|
||||||
|
{banner}
|
||||||
|
<h1>{t('accountDisabled')}</h1>
|
||||||
|
<p>{t('accountDisabledMsg')}</p>
|
||||||
|
<ContactSupport t={t} />
|
||||||
|
</Wrap>
|
||||||
|
)
|
||||||
|
|
||||||
|
const AccountProhibited = ({ t, banner }) => (
|
||||||
|
<Wrap>
|
||||||
|
{banner}
|
||||||
|
<h1>{t('accountProhibited')}</h1>
|
||||||
|
<p>{t('accountProhibitedMsg')}</p>
|
||||||
|
<ContactSupport t={t} />
|
||||||
|
</Wrap>
|
||||||
|
)
|
||||||
|
|
||||||
|
const AccountStatusUnknown = ({ t, banner }) => (
|
||||||
|
<Wrap>
|
||||||
|
{banner}
|
||||||
|
<h1>{t('statusUnknown')}</h1>
|
||||||
|
<p>{t('statusUnknownMsg')}</p>
|
||||||
|
<ContactSupport t={t} />
|
||||||
|
</Wrap>
|
||||||
|
)
|
||||||
|
|
||||||
|
const RoleLacking = ({ t, requiredRole, role, banner }) => (
|
||||||
|
<Wrap>
|
||||||
|
{banner}
|
||||||
|
<h1>{t('roleLacking')}</h1>
|
||||||
|
<p dangerouslySetInnerHTML={{ __html: t('roleLackingMsg', { requiredRole, role }) }} />
|
||||||
|
<ContactSupport t={t} />
|
||||||
|
</Wrap>
|
||||||
|
)
|
||||||
|
|
||||||
|
const ConsentLacking = ({ banner, refresh }) => {
|
||||||
|
const { setAccount, setToken, setSeenUser } = useAccount()
|
||||||
|
const backend = useBackend()
|
||||||
|
|
||||||
|
const updateConsent = async ({ consent1, consent2 }) => {
|
||||||
|
let consent = 0
|
||||||
|
if (consent1) consent = 1
|
||||||
|
if (consent1 && consent2) consent = 2
|
||||||
|
if (consent > 0) {
|
||||||
|
const result = await backend.updateConsent(consent)
|
||||||
|
console.log({ result })
|
||||||
|
if (result.success) {
|
||||||
|
setToken(result.data.token)
|
||||||
|
setAccount(result.data.account)
|
||||||
|
setSeenUser(result.data.account.username)
|
||||||
|
refresh()
|
||||||
|
} else {
|
||||||
|
console.log('something went wrong', result)
|
||||||
|
refresh()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Wrap>
|
||||||
|
<div className="text-left">
|
||||||
|
{banner}
|
||||||
|
<ConsentForm submit={updateConsent} />
|
||||||
|
</div>
|
||||||
|
</Wrap>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const AuthWrapper = ({ children, requiredRole = 'user' }) => {
|
||||||
|
const { t } = useTranslation(ns)
|
||||||
|
const { account, setAccount, token, admin, stopImpersonating, signOut } = useAccount()
|
||||||
|
const backend = useBackend()
|
||||||
|
|
||||||
|
const [ready, setReady] = useState(false)
|
||||||
|
const [impersonating, setImpersonating] = useState(false)
|
||||||
|
const [error, setError] = useState(false)
|
||||||
|
const [refreshCount, setRefreshCount] = useState(0)
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Avoid hydration errors
|
||||||
|
*/
|
||||||
|
useEffect(() => {
|
||||||
|
const verifyAdmin = async () => {
|
||||||
|
const result = await backend.adminPing(admin.token)
|
||||||
|
if (result.success && result.data.account.role === 'admin') {
|
||||||
|
setImpersonating({
|
||||||
|
admin: result.data.account.username,
|
||||||
|
user: account.username,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
setReady(true)
|
||||||
|
}
|
||||||
|
const verifyUser = async () => {
|
||||||
|
const result = await backend.ping()
|
||||||
|
if (result.success) {
|
||||||
|
// Refresh account in local storage
|
||||||
|
setAccount({
|
||||||
|
...account,
|
||||||
|
...result.data.account,
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
if (result.data?.error?.error) setError(result.data.error.error)
|
||||||
|
else signOut()
|
||||||
|
}
|
||||||
|
setReady(true)
|
||||||
|
}
|
||||||
|
if (admin && admin.token) verifyAdmin()
|
||||||
|
if (token) verifyUser()
|
||||||
|
else setReady(true)
|
||||||
|
}, [admin, token, refreshCount, account, setAccount, backend, signOut])
|
||||||
|
|
||||||
|
const refresh = () => {
|
||||||
|
setRefreshCount(refreshCount + 1)
|
||||||
|
setError(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!ready)
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<p>not ready</p>
|
||||||
|
<Loading />
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
|
||||||
|
const banner = impersonating ? (
|
||||||
|
<div className="bg-warning rounded-lg shadow py-4 px-6 flex flex-row items-center gap-4 justify-between">
|
||||||
|
<span className="text-base-100 text-left">
|
||||||
|
Hi <b>{impersonating.admin}</b>, you are currently impersonating <b>{impersonating.user}</b>
|
||||||
|
</span>
|
||||||
|
<button className="btn btn-neutral" onClick={stopImpersonating}>
|
||||||
|
Stop Impersonating
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
) : null
|
||||||
|
|
||||||
|
const childProps = { t, banner }
|
||||||
|
|
||||||
|
if (!token || !account.username) return <AuthRequired {...childProps} />
|
||||||
|
if (error) {
|
||||||
|
if (error === 'accountInactive') return <AccountInactive {...childProps} />
|
||||||
|
if (error === 'accountDisabled') return <AccountDisabled {...childProps} />
|
||||||
|
if (error === 'accountBlocked') return <AccountProhibited {...childProps} />
|
||||||
|
if (error === 'consentLacking') return <ConsentLacking {...childProps} refresh={refresh} />
|
||||||
|
return <AccountStatusUnknown {...childProps} />
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!roles.levels[account.role] || roles.levels[account.role] < roles.levels[requiredRole]) {
|
||||||
|
return <RoleLacking {...childProps} role={account.role} requiredRole={requiredRole} />
|
||||||
|
}
|
||||||
|
|
||||||
|
return children
|
||||||
|
}
|
170
packages/react/config/freesewing/index.mjs
Normal file
170
packages/react/config/freesewing/index.mjs
Normal file
|
@ -0,0 +1,170 @@
|
||||||
|
/*
|
||||||
|
* This configuration file has the following named exports:
|
||||||
|
*
|
||||||
|
* account: Fields and control levels, and other account related settings
|
||||||
|
* backend: URL of the FreeSewing backend
|
||||||
|
* editor: Configuration for the pattern editor
|
||||||
|
* monorepo: URL of the FreeSewing monorepo
|
||||||
|
* social : Social media and other account links for FreeSewing
|
||||||
|
* controlLevels: Consolidated object holding all control levels
|
||||||
|
*/
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Structure of the various account fields and their control levels
|
||||||
|
*/
|
||||||
|
export const account = {
|
||||||
|
fields: {
|
||||||
|
data: {
|
||||||
|
bookmarks: 2,
|
||||||
|
sets: 1,
|
||||||
|
patterns: 1,
|
||||||
|
apikeys: 4,
|
||||||
|
},
|
||||||
|
info: {
|
||||||
|
username: 2,
|
||||||
|
bio: 2,
|
||||||
|
img: 2,
|
||||||
|
email: 3,
|
||||||
|
},
|
||||||
|
settings: {
|
||||||
|
language: 2,
|
||||||
|
units: 2,
|
||||||
|
newsletter: 2,
|
||||||
|
compare: 3,
|
||||||
|
control: 1,
|
||||||
|
consent: 2,
|
||||||
|
},
|
||||||
|
security: {
|
||||||
|
password: 2,
|
||||||
|
mfa: 3,
|
||||||
|
apikeys: 4,
|
||||||
|
},
|
||||||
|
identities: {
|
||||||
|
github: 3,
|
||||||
|
instagram: 3,
|
||||||
|
mastodon: 3,
|
||||||
|
reddit: 3,
|
||||||
|
twitch: 3,
|
||||||
|
tiktok: 3,
|
||||||
|
website: 3,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
sets: {
|
||||||
|
name: 1,
|
||||||
|
img: 1,
|
||||||
|
public: 3,
|
||||||
|
units: 1,
|
||||||
|
notes: 2,
|
||||||
|
createdAt: 2,
|
||||||
|
updatedAt: 2,
|
||||||
|
id: 4,
|
||||||
|
},
|
||||||
|
patterns: {
|
||||||
|
name: 1,
|
||||||
|
img: 1,
|
||||||
|
public: 3,
|
||||||
|
notes: 2,
|
||||||
|
createdAt: 2,
|
||||||
|
updatedAt: 2,
|
||||||
|
id: 4,
|
||||||
|
},
|
||||||
|
statuses: {
|
||||||
|
0: {
|
||||||
|
name: 'inactive',
|
||||||
|
color: 'neutral',
|
||||||
|
},
|
||||||
|
1: {
|
||||||
|
name: 'active',
|
||||||
|
color: 'success',
|
||||||
|
},
|
||||||
|
'-1': {
|
||||||
|
name: 'paused',
|
||||||
|
color: 'warning',
|
||||||
|
},
|
||||||
|
'-2': {
|
||||||
|
name: 'disabled',
|
||||||
|
color: 'error',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* URL of the FreeSewing backend
|
||||||
|
*/
|
||||||
|
export const backend = 'https://backend3.freesewing.org'
|
||||||
|
|
||||||
|
/*
|
||||||
|
* The default control level in case we have nothing more specific
|
||||||
|
*/
|
||||||
|
export const defaultControlLevel = 3
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Editor control levels
|
||||||
|
*/
|
||||||
|
export const editor = {
|
||||||
|
control: {
|
||||||
|
core: {
|
||||||
|
sa: 2,
|
||||||
|
paperless: 2,
|
||||||
|
locale: 3,
|
||||||
|
units: 1,
|
||||||
|
complete: 4,
|
||||||
|
expand: 4,
|
||||||
|
only: 4,
|
||||||
|
scale: 4,
|
||||||
|
margin: 4,
|
||||||
|
},
|
||||||
|
ui: {
|
||||||
|
renderer: 4,
|
||||||
|
kiosk: 2,
|
||||||
|
},
|
||||||
|
views: {
|
||||||
|
draft: 1,
|
||||||
|
measies: 1,
|
||||||
|
test: 3,
|
||||||
|
time: 3,
|
||||||
|
print: 1,
|
||||||
|
export: 1,
|
||||||
|
save: 1,
|
||||||
|
edit: 4,
|
||||||
|
logs: 2,
|
||||||
|
inspect: 4,
|
||||||
|
docs: 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* URL of the FreeSewing monorepo
|
||||||
|
*/
|
||||||
|
export const monorepo = 'https://github.com/freesewing/freesewing'
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Social media and other account links for FreeSewing
|
||||||
|
*/
|
||||||
|
export const social = {
|
||||||
|
YouTube: 'https://www.youtube.com/@freesewing',
|
||||||
|
Discord: 'https://discord.freesewing.org/',
|
||||||
|
Instagram: 'https://instagram.com/freesewing_org',
|
||||||
|
Facebook: 'https://www.facebook.com/groups/627769821272714/',
|
||||||
|
GitHub: 'https://github.com/freesewing',
|
||||||
|
Reddit: 'https://www.reddit.com/r/freesewing/',
|
||||||
|
Mastodon: 'https://freesewing.social/@freesewing',
|
||||||
|
Bluesky: 'https://bsky.app/profile/freesewing.org',
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Consolidated object holding all control levels
|
||||||
|
*/
|
||||||
|
export const controlLevels = {
|
||||||
|
...account.fields.data,
|
||||||
|
...account.fields.info,
|
||||||
|
...account.fields.settings,
|
||||||
|
...account.fields.security,
|
||||||
|
...account.fields.identities,
|
||||||
|
sets: account.sets,
|
||||||
|
patterns: editor.account.patterns,
|
||||||
|
core: editor.control.core,
|
||||||
|
ui: editor.control.ui,
|
||||||
|
views: editor.control.views,
|
||||||
|
}
|
115
packages/react/context/loadingStatus/index.mjs
Normal file
115
packages/react/context/loadingStatus/index.mjs
Normal file
|
@ -0,0 +1,115 @@
|
||||||
|
// __SDEFILE__ - This file is a dependency for the stand-alone environment
|
||||||
|
/* eslint-disable */
|
||||||
|
// Not sure why but eslint does not seem to understand this file
|
||||||
|
// and I don't have time to hold its hand.
|
||||||
|
import { useState, useEffect, createContext } from 'react'
|
||||||
|
import { Spinner } from 'shared/components/spinner.mjs'
|
||||||
|
import { OkIcon, WarningIcon } from 'shared/components/icons.mjs'
|
||||||
|
import { useTranslation } from 'next-i18next'
|
||||||
|
|
||||||
|
export const ns = ['status']
|
||||||
|
|
||||||
|
export const LoadingStatusContext = createContext([false])
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Timeout in seconds before the loading status dissapears
|
||||||
|
*/
|
||||||
|
const timeout = 2
|
||||||
|
|
||||||
|
const LoadingStatus = ({ loadingStatus }) => {
|
||||||
|
const { t } = useTranslation(ns)
|
||||||
|
|
||||||
|
const [fade, setFade] = useState('opacity-100')
|
||||||
|
const [timer, setTimer] = useState(false)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (loadingStatus[2]) {
|
||||||
|
if (timer) clearTimeout(timer)
|
||||||
|
setTimer(
|
||||||
|
window.setTimeout(() => {
|
||||||
|
setFade('opacity-0')
|
||||||
|
}, timeout * 1000 - 350)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}, [loadingStatus[2]])
|
||||||
|
|
||||||
|
if (!loadingStatus[0]) return null
|
||||||
|
|
||||||
|
let color = 'secondary'
|
||||||
|
let icon = <Spinner />
|
||||||
|
if (loadingStatus[2]) {
|
||||||
|
color = loadingStatus[3] ? 'success' : 'error'
|
||||||
|
icon = loadingStatus[3] ? (
|
||||||
|
<OkIcon stroke={4} className="w-8 h-8" />
|
||||||
|
) : (
|
||||||
|
<WarningIcon className="w-8 h-8" stroke={2} />
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="fixed top-0 md:top-28 left-0 w-full z-30 md:px-4 md:mx-auto">
|
||||||
|
<div
|
||||||
|
className={`w-full md:max-w-2xl m-auto bg-${color} flex flex-row items-center gap-4 p-4 px-4 ${fade}
|
||||||
|
transition-opacity delay-[${timeout * 1000 - 400}ms] duration-300
|
||||||
|
md:rounded-lg shadow text-secondary-content text-lg lg:text-xl font-medium md:bg-opacity-90`}
|
||||||
|
>
|
||||||
|
<span className="shrink-0">{icon}</span>
|
||||||
|
{typeof loadingStatus[1] === 'object' && loadingStatus[1].props
|
||||||
|
? loadingStatus[1]
|
||||||
|
: t(loadingStatus[1])}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const LoadingProgress = ({ val = 0, max = 1, msg }) => (
|
||||||
|
<div className="flex flex-col gap-2 w-full grow-0">
|
||||||
|
{msg}
|
||||||
|
<progress className="progress progress-white" value={val} max={max}></progress>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
|
||||||
|
export const LoadingStatusContextProvider = ({ children }) => {
|
||||||
|
/*
|
||||||
|
* LoadingStatus should hold an array with 1 to 4 elements:
|
||||||
|
* 0 => Show loading status or not (true or false)
|
||||||
|
* 1 => Message to show
|
||||||
|
* 2 => Set this to true to make the loadingStatus dissapear after 2 seconds
|
||||||
|
* 3 => Set this to true to show success, false to show error (only when 2 is true)
|
||||||
|
*/
|
||||||
|
const [timer, setTimer] = useState(false)
|
||||||
|
|
||||||
|
const [__loadingStatus, __setLoadingStatus] = useState({
|
||||||
|
status: [false],
|
||||||
|
setLoadingStatus,
|
||||||
|
loading: false,
|
||||||
|
LoadingStatus: () => <LoadingStatus loadingStatus={[false]} />,
|
||||||
|
LoadingProgress,
|
||||||
|
})
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (__loadingStatus.status[2]) {
|
||||||
|
if (timer) clearTimeout(timer)
|
||||||
|
setTimer(
|
||||||
|
window.setTimeout(() => {
|
||||||
|
setLoadingStatus([false])
|
||||||
|
}, timeout * 1000)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}, [__loadingStatus.status[2]])
|
||||||
|
|
||||||
|
function setLoadingStatus(newStatus) {
|
||||||
|
__setLoadingStatus({
|
||||||
|
...__loadingStatus,
|
||||||
|
status: newStatus,
|
||||||
|
loading: newStatus[0] || false,
|
||||||
|
LoadingStatus: () => <LoadingStatus loadingStatus={newStatus} />,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<LoadingStatusContext.Provider value={__loadingStatus}>
|
||||||
|
{children}
|
||||||
|
</LoadingStatusContext.Provider>
|
||||||
|
)
|
||||||
|
}
|
|
@ -1,4 +1,4 @@
|
||||||
// This file is auto-generated | All changes you make will be overwritten.
|
// This file is auto-generated | All changes you make will be overwritten.
|
||||||
export const name = '@freesewing/react-components'
|
export const name = '@freesewing/react'
|
||||||
export const version = '3.3.0-rc.1'
|
export const version = '3.3.0-rc.1'
|
||||||
export const data = { name, version }
|
export const data = { name, version }
|
102
packages/react/hooks/useAccount/index.mjs
Normal file
102
packages/react/hooks/useAccount/index.mjs
Normal file
|
@ -0,0 +1,102 @@
|
||||||
|
import useLocalStorageState from 'use-local-storage-state'
|
||||||
|
import { defaultControlLevel } from '@freesewing/react-components/config/freesewing'
|
||||||
|
|
||||||
|
/*
|
||||||
|
* When there is no account, we use this making it easy to check for username
|
||||||
|
* or control
|
||||||
|
*/
|
||||||
|
const noAccount = { username: false, control: defaultControlLevel }
|
||||||
|
|
||||||
|
/*
|
||||||
|
* FreeSewing's useAccount hook. Grants access to the (data in the) user's account
|
||||||
|
*/
|
||||||
|
export function useAccount() {
|
||||||
|
/*
|
||||||
|
* Account is stored in persisted state (saved to local storage)
|
||||||
|
*/
|
||||||
|
const [account, setAccount] = useLocalStorageState('fs-account', { defaultValue: noAccount })
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Admins are allowed to inpersonate a user.
|
||||||
|
* When that happens, we store the admin's account in this admin field (saved in local storage).
|
||||||
|
* When the admin ends the impersonation, we copy the account data under the admin key back into
|
||||||
|
* the regular account key.
|
||||||
|
* With this, admins would have to re-login after impersonating a user.
|
||||||
|
*/
|
||||||
|
const [admin, setAdmin] = useLocalStorageState('fs-admin', { defaultValue: noAccount })
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This holds the JSON Web Token (JWT) returned from the backend after authentication
|
||||||
|
*/
|
||||||
|
const [token, setToken] = useLocalStorageState('fs-token', { defaultValue: null })
|
||||||
|
|
||||||
|
/*
|
||||||
|
* We use this to allow 'welcome back' style UI, asking for password, not username
|
||||||
|
*/
|
||||||
|
const [seenUser, setSeenUser] = useLocalStorageState('fs-seen-user', { defaultValue: false })
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Clear user data when signing out
|
||||||
|
*/
|
||||||
|
const signOut = () => {
|
||||||
|
setAccount(noAccount)
|
||||||
|
setToken(null)
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Impersonate a user.
|
||||||
|
* Only admins can do this but that is enforced at the backend.
|
||||||
|
*/
|
||||||
|
const impersonate = (data) => {
|
||||||
|
/*
|
||||||
|
* Store token and account data in admin, so we can restore them later
|
||||||
|
*/
|
||||||
|
setAdmin({ token, account })
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Create new account object based on the data passed in
|
||||||
|
*/
|
||||||
|
const newAccount = {
|
||||||
|
...data.account,
|
||||||
|
impersonatingAdmin: { id: account.id, username: account.username },
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Now set the new account and token
|
||||||
|
*/
|
||||||
|
setAccount(newAccount)
|
||||||
|
setToken(data.token)
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* When impersonation ends, restore the original admin account
|
||||||
|
*/
|
||||||
|
const stopImpersonating = () => {
|
||||||
|
setAccount(admin.account)
|
||||||
|
setToken(admin.token)
|
||||||
|
clearAdmin()
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Don't keep account data lingering in the admin key
|
||||||
|
*/
|
||||||
|
const clearAdmin = () => setAdmin(noAccount)
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Return everything this hook provides
|
||||||
|
*/
|
||||||
|
return {
|
||||||
|
account,
|
||||||
|
setAccount,
|
||||||
|
token,
|
||||||
|
setToken,
|
||||||
|
seenUser,
|
||||||
|
setSeenUser,
|
||||||
|
signOut,
|
||||||
|
admin,
|
||||||
|
clearAdmin,
|
||||||
|
impersonate,
|
||||||
|
stopImpersonating,
|
||||||
|
control: account?.control || 2,
|
||||||
|
}
|
||||||
|
}
|
814
packages/react/hooks/useBackend/index.mjs
Normal file
814
packages/react/hooks/useBackend/index.mjs
Normal file
|
@ -0,0 +1,814 @@
|
||||||
|
import { useMemo } from 'react'
|
||||||
|
import { backend } from '@freesewing/react-components/config/freesewing'
|
||||||
|
import { RestClient } from '@freesewing/react-components/lib/restClient'
|
||||||
|
import { useAccount } from '@freesewing/react-components/hooks/useAccount'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The useBackend hook
|
||||||
|
*
|
||||||
|
* This hook provides access to the FreeSewing backend
|
||||||
|
*/
|
||||||
|
export function useBackend() {
|
||||||
|
/*
|
||||||
|
* Load the token via the useAccount hook
|
||||||
|
*/
|
||||||
|
const { token } = useAccount()
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Memoize this call for efficiency
|
||||||
|
*/
|
||||||
|
const backend = useMemo(() => new Backend(token), [token])
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This backend object rovides the following methods:
|
||||||
|
*
|
||||||
|
* - backend.adminSearchUsers
|
||||||
|
* - backend.adminLoadUser
|
||||||
|
* - backend.adminUpdateUser
|
||||||
|
* - backend.adminImpersonateUser
|
||||||
|
* - backend.adminLoadSubscribers
|
||||||
|
* - backend.adminPing
|
||||||
|
* - backend.acceptCset
|
||||||
|
* - backend.confirmMfa
|
||||||
|
* - backend.confirmSignup
|
||||||
|
* - backend.createApikey
|
||||||
|
* - backend.createBookmark
|
||||||
|
* - backend.createSocialImage
|
||||||
|
* - backend.createDiscussion
|
||||||
|
* - backend.createIssue
|
||||||
|
* - backend.createPattern
|
||||||
|
* - backend.createPostPr
|
||||||
|
* - backend.createSet
|
||||||
|
* - backend.disableMfa
|
||||||
|
* - backend.enableMfa
|
||||||
|
* - backend.exportAccount
|
||||||
|
* - backend.getApikey
|
||||||
|
* - backend.getApikeys
|
||||||
|
* - backend.getBookmark
|
||||||
|
* - backend.getBookmarks
|
||||||
|
* - backend.getConfirmation
|
||||||
|
* - backend.getCuratedSet
|
||||||
|
* - backend.getCuratedSets
|
||||||
|
* - backend.getPattern
|
||||||
|
* - backend.getPatterns
|
||||||
|
* - backend.getPublicPattern
|
||||||
|
* - backend.getPublicSet
|
||||||
|
* - backend.getSet
|
||||||
|
* - backend.getSets
|
||||||
|
* - backend.getStats
|
||||||
|
* - backend.getSuggestedPacks
|
||||||
|
* - backend.getSuggestedSets
|
||||||
|
* - backend.getUserCount
|
||||||
|
* - backend.getUserData
|
||||||
|
* - backend.getUserProfile
|
||||||
|
* - backend.isPostSlugAvailable
|
||||||
|
* - backend.isUsernameAvailable
|
||||||
|
* - backend.newsletterConfirmSubscribe
|
||||||
|
* - backend.newsletterSubscribe
|
||||||
|
* - backend.newsletterUnsubscribe
|
||||||
|
* - backend.oauthInit
|
||||||
|
* - backend.oauthSignIn
|
||||||
|
* - backend.ping
|
||||||
|
* - backend.reloadAccount
|
||||||
|
* - backend.removeAccount
|
||||||
|
* - backend.removeApikey
|
||||||
|
* - backend.removeBookmark
|
||||||
|
* - backend.removeCuratedSet
|
||||||
|
* - backend.removeImage
|
||||||
|
* - backend.removePattern
|
||||||
|
* - backend.removeSet
|
||||||
|
* - backend.removeSuggestedSet
|
||||||
|
* - backend.restrictAccount
|
||||||
|
* - backend.signIn
|
||||||
|
* - backend.signInFromLink
|
||||||
|
* - backend.signUp
|
||||||
|
* - backend.suggestCset
|
||||||
|
* - backend.suggestOpack
|
||||||
|
* - backend.updateAccount
|
||||||
|
* - backend.updateCuratedSet
|
||||||
|
* - backend.updateConsent
|
||||||
|
* - backend.updatePattern
|
||||||
|
* - backend.updateSet
|
||||||
|
* - backend.uploadImage
|
||||||
|
* - backend.uploadImageAnon
|
||||||
|
*/
|
||||||
|
return backend
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This helper function creates the authentication headers
|
||||||
|
*
|
||||||
|
* @param {string} token - The JSON Web Token to authenticate to the backend
|
||||||
|
* @return {object} headers - An object holding headers for the REST API call
|
||||||
|
*/
|
||||||
|
function authenticationHeaders(token) {
|
||||||
|
return token
|
||||||
|
? {
|
||||||
|
headers: { Authorization: 'Bearer ' + token },
|
||||||
|
}
|
||||||
|
: { headers: {} }
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This creates a backend instance and stores the authentication data
|
||||||
|
*
|
||||||
|
* @param {string} token - The JWT token to use for authentication to the backend
|
||||||
|
*/
|
||||||
|
function Backend(token) {
|
||||||
|
this.token = token
|
||||||
|
this.headers = authenticationHeaders(token)
|
||||||
|
this.restClient = new RestClient(backend, this.headers)
|
||||||
|
this.get = this.restClient.get
|
||||||
|
this.put = this.restClient.put
|
||||||
|
this.post = this.restClient.post
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Admin: Search user
|
||||||
|
*
|
||||||
|
* @param {string} q - The search query
|
||||||
|
* @return {array} result - The REST response, a [status, data] array
|
||||||
|
*/
|
||||||
|
Backend.prototype.adminSearchUsers = async function (q) {
|
||||||
|
return await this.post('/admin/search/users/jwt', { q })
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Admin: Load user
|
||||||
|
*
|
||||||
|
* @param {number} id - The user ID to load
|
||||||
|
* @return {array} result - The REST response, a [status, data] array
|
||||||
|
*/
|
||||||
|
Backend.prototype.adminLoadUser = async function (id) {
|
||||||
|
return await this.get(`/admin/user/${id}/jwt`)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Admin: Update user
|
||||||
|
*
|
||||||
|
* @param {object} data
|
||||||
|
* @param {number} data.id - The user ID to update
|
||||||
|
* @param {object} data.data - The data for the API request
|
||||||
|
* @return {array} result - The REST response, a [status, data] array
|
||||||
|
*/
|
||||||
|
Backend.prototype.adminUpdateUser = async function ({ id, data }) {
|
||||||
|
return await this.patch(`/admin/user/${id}/jwt`, data)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Admin: Impersonate user
|
||||||
|
*
|
||||||
|
* @param {number} id - The user ID to impersonate
|
||||||
|
* @return {array} result - The REST response, a [status, data] array
|
||||||
|
*/
|
||||||
|
Backend.prototype.adminImpersonateUser = async function (id) {
|
||||||
|
return await this.get(`/admin/impersonate/${id}/jwt`)
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Load newsletter subscribers (admin method)
|
||||||
|
*/
|
||||||
|
Backend.prototype.adminLoadSubscribers = async function () {
|
||||||
|
return await this.get(`/admin/subscribers/jwt`)
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Verify an admin account while impersonating another user
|
||||||
|
*/
|
||||||
|
Backend.prototype.adminPing = async function (token) {
|
||||||
|
return await this.get(`/whoami/jwt`, { Authorization: `Bearer: ${token}` })
|
||||||
|
}
|
||||||
|
/*
|
||||||
|
* Create a curated set from a suggested measurements set
|
||||||
|
*/
|
||||||
|
Backend.prototype.acceptCset = async function (id) {
|
||||||
|
return await this.post(`/curated-sets/from/${id}/jwt`)
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Confirm MFA
|
||||||
|
*
|
||||||
|
* @param {object} data for the API call
|
||||||
|
* @return {array} result - The REST response, a [status, data] array
|
||||||
|
*/
|
||||||
|
Backend.prototype.confirmMfa = async function (data) {
|
||||||
|
return await this.post(`/account/mfa/jwt`, { ...data, mfa: true })
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Confirm a signup
|
||||||
|
*
|
||||||
|
* @param {object} data
|
||||||
|
* @param {string} data.id - The confirmation ID
|
||||||
|
* @param {string} data.consent - The consent data
|
||||||
|
* @return {array} result - The REST response, a [status, data] array
|
||||||
|
*/
|
||||||
|
Backend.prototype.confirmSignup = async function ({ id, consent }) {
|
||||||
|
return await this.post(`/confirm/signup/${id}`, { consent })
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create API key
|
||||||
|
*
|
||||||
|
* @param {object} data - The data for the API call
|
||||||
|
* @return {array} result - The REST response, a [status, data] array
|
||||||
|
*/
|
||||||
|
Backend.prototype.createApikey = async function (data) {
|
||||||
|
return await this.post(`/apikeys/jwt`, data)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create bookmark
|
||||||
|
*
|
||||||
|
* @param {object} data - Data for the API call
|
||||||
|
* @return {array} result - The REST response, a [status, data] array
|
||||||
|
*/
|
||||||
|
Backend.prototype.createBookmark = async function (data) {
|
||||||
|
return await this.post(`/bookmarks/jwt`, data)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate a social media image
|
||||||
|
*
|
||||||
|
* @param {object} data - Data for the API call
|
||||||
|
* @return {array} result - The REST response, a [status, data] array
|
||||||
|
*/
|
||||||
|
Backend.prototype.createSocialImage = async function (data) {
|
||||||
|
return await this.post('/img', data)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create GitHub discussion
|
||||||
|
*
|
||||||
|
* @param {object} data - Data for the API call
|
||||||
|
* @return {array} result - The REST response, a [status, data] array
|
||||||
|
*/
|
||||||
|
Backend.prototype.createDiscussion = async function (data) {
|
||||||
|
return await this.post(`/discussions`, data)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create GitHub issue
|
||||||
|
*
|
||||||
|
* @param {object} data - Data for the API call
|
||||||
|
* @return {array} result - The REST response, a [status, data] array
|
||||||
|
*/
|
||||||
|
Backend.prototype.createIssue = async function (data) {
|
||||||
|
return await this.post(`/issues`, data)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create pattern
|
||||||
|
*
|
||||||
|
* @param {object} data - Data for the API call
|
||||||
|
* @return {array} result - The REST response, a [status, data] array
|
||||||
|
*/
|
||||||
|
Backend.prototype.createPattern = async function (data) {
|
||||||
|
return await this.post(`/patterns/jwt`, data)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a pull request for a showcase or blog post
|
||||||
|
*
|
||||||
|
* @param {object} data - Data for the API call
|
||||||
|
* @return {array} result - The REST response, a [status, data] array
|
||||||
|
*/
|
||||||
|
Backend.prototype.createPostPr = async function (type, data) {
|
||||||
|
return await this.post(`/flows/pr/${type}/jwt`, data)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create measurements set
|
||||||
|
*
|
||||||
|
* @param {object} data - Data for the API call
|
||||||
|
* @return {array} result - The REST response, a [status, data] array
|
||||||
|
*/
|
||||||
|
Backend.prototype.createSet = async function (data) {
|
||||||
|
return await this.post(`/sets/jwt`, data)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Disable MFA for the current user
|
||||||
|
*
|
||||||
|
* @param {object} data for the API call
|
||||||
|
* @return {array} result - The REST response, a [status, data] array
|
||||||
|
*/
|
||||||
|
Backend.prototype.disableMfa = async function (data) {
|
||||||
|
return await this.post(`/account/mfa/jwt`, { ...data, mfa: false })
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enable MFA for the current user
|
||||||
|
*
|
||||||
|
* @return {array} result - The REST response, a [status, data] array
|
||||||
|
*/
|
||||||
|
Backend.prototype.enableMfa = async function () {
|
||||||
|
return await this.post(`/account/mfa/jwt`, { mfa: true })
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Export account data for the current user
|
||||||
|
*
|
||||||
|
* @return {array} result - The REST response, a [status, data] array
|
||||||
|
*/
|
||||||
|
Backend.prototype.exportAccount = async function () {
|
||||||
|
return await this.get(`/account/export/jwt`)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get API key
|
||||||
|
*
|
||||||
|
* @param {string} id - The API Key ID
|
||||||
|
* @return {array} result - The REST response, a [status, data] array
|
||||||
|
*/
|
||||||
|
Backend.prototype.getApikey = async function (id) {
|
||||||
|
return await this.get(`/apikeys/${id}/jwt`)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get API keys for the current user
|
||||||
|
*/
|
||||||
|
Backend.prototype.getApikeys = async function () {
|
||||||
|
return await this.get(`/apikeys/jwt`)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get bookmark
|
||||||
|
*
|
||||||
|
* @param {string} id - The bookmark ID
|
||||||
|
* @return {array} result - The REST response, a [status, data] array
|
||||||
|
*/
|
||||||
|
Backend.prototype.getBookmark = async function (id) {
|
||||||
|
return await this.get(`/bookmarks/${id}/jwt`)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get bookmarks (for the current user)
|
||||||
|
*/
|
||||||
|
Backend.prototype.getBookmarks = async function () {
|
||||||
|
return await this.get(`/bookmarks/jwt`)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get data for a confirmation
|
||||||
|
*
|
||||||
|
* @param {object} data
|
||||||
|
* @param {string} data.id - The confirmation ID
|
||||||
|
* @param {string} data.check - The confirmation check value
|
||||||
|
* @return {array} result - The REST response, a [status, data] array
|
||||||
|
*/
|
||||||
|
Backend.prototype.getConfirmation = async function ({ id, check }) {
|
||||||
|
return await this.get(`/confirmations/${id}/${check}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get curated measurements set
|
||||||
|
*
|
||||||
|
* @param {number} id - The curated measurements set ID
|
||||||
|
* @return {array} result - The REST response, a [status, data] array
|
||||||
|
*/
|
||||||
|
Backend.prototype.getCuratedSet = async function (id) {
|
||||||
|
return await this.get(`/curated-sets/${id}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get curated measurements sets
|
||||||
|
*
|
||||||
|
* @return {array} result - The REST response, a [status, data] array
|
||||||
|
*/
|
||||||
|
Backend.prototype.getCuratedSets = async function () {
|
||||||
|
return await this.get(`/curated-sets`)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get pattern
|
||||||
|
*
|
||||||
|
* @param {number} id - The pattern ID
|
||||||
|
* @return {array} result - The REST response, a [status, data] array
|
||||||
|
*/
|
||||||
|
Backend.prototype.getPattern = async function (id) {
|
||||||
|
return await this.get(`/patterns/${id}/jwt`)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get patterns for the current user
|
||||||
|
*
|
||||||
|
* @return {array} result - The REST response, a [status, data] array
|
||||||
|
*/
|
||||||
|
Backend.prototype.getPatterns = async function () {
|
||||||
|
return await this.get(`/patterns/jwt`)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get public pattern
|
||||||
|
*
|
||||||
|
* @param {number} id - The pattern ID
|
||||||
|
* @return {array} result - The REST response, a [status, data] array
|
||||||
|
*/
|
||||||
|
Backend.prototype.getPublicPattern = async function (id) {
|
||||||
|
return await this.get(`/patterns/${id}.json`)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get public measurements set
|
||||||
|
*
|
||||||
|
* @param {number} id - The public measurements set ID
|
||||||
|
* @return {array} result - The REST response, a [status, data] array
|
||||||
|
*/
|
||||||
|
Backend.prototype.getPublicSet = async function (id) {
|
||||||
|
return await this.get(`/sets/${id}.json`)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get measurements set
|
||||||
|
*
|
||||||
|
* @param {number} id - The measurements set ID
|
||||||
|
* @return {array} result - The REST response, a [status, data] array
|
||||||
|
*/
|
||||||
|
Backend.prototype.getSet = async function (id) {
|
||||||
|
return await this.get(`/sets/${id}/jwt`)
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Get measurements sets for the current user
|
||||||
|
*
|
||||||
|
* @return {array} result - The REST response, a [status, data] array
|
||||||
|
*/
|
||||||
|
Backend.prototype.getSets = async function () {
|
||||||
|
return await this.get(`/sets/jwt`)
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Get stats (info about how many users, patterns, and so on)
|
||||||
|
*
|
||||||
|
* @return {array} result - The REST response, a [status, data] array
|
||||||
|
*/
|
||||||
|
Backend.prototype.getStats = async function () {
|
||||||
|
return await this.get(`/info/stats`)
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Get option packs suggested for curation
|
||||||
|
*
|
||||||
|
* @return {array} result - The REST response, a [status, data] array
|
||||||
|
*/
|
||||||
|
Backend.prototype.getSuggestedPacks = async function () {
|
||||||
|
return await this.get(`/suggested-packs/jwt`)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get measurements sets suggested for curation
|
||||||
|
*
|
||||||
|
* @return {array} result - The REST response, a [status, data] array
|
||||||
|
*/
|
||||||
|
Backend.prototype.getSuggestedSets = async function () {
|
||||||
|
return await this.get(`/suggested-sets/jwt`)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get user count (how many users FreeSewing has)
|
||||||
|
*
|
||||||
|
* @return {array} result - The REST response, a [status, data] array
|
||||||
|
*/
|
||||||
|
Backend.prototype.getUserCount = async function () {
|
||||||
|
return await this.get(`/info/users`)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get user data
|
||||||
|
*
|
||||||
|
* @param {number} uid - The user ID
|
||||||
|
* @return {array} result - The REST response, a [status, data] array
|
||||||
|
*/
|
||||||
|
Backend.prototype.getUserData = async function (uid) {
|
||||||
|
return await this.get(`/users/${uid}/jwt`)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get user profile
|
||||||
|
*
|
||||||
|
* @param {number} uid - The user ID
|
||||||
|
* @return {array} result - The REST response, a [status, data] array
|
||||||
|
*/
|
||||||
|
Backend.prototype.getUserProfile = async function (uid) {
|
||||||
|
return await this.get(`/users/${uid}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Check whether a slug for a blog or showcase post is available
|
||||||
|
*
|
||||||
|
* @param {object} data
|
||||||
|
* @param {string} data.slug - The slug to check
|
||||||
|
* @param {string} data.type - One of 'blog' or 'showcase'
|
||||||
|
* @return {array} result - The REST response, a [status, data] array
|
||||||
|
*/
|
||||||
|
Backend.prototype.isPostSlugAvailable = async function ({ slug, type }) {
|
||||||
|
const response = await this.get(`/slugs/${type}/${slug}/jwt`, this.auth)
|
||||||
|
|
||||||
|
// 404 means username is available, which is success in this case
|
||||||
|
return response[0] === 404
|
||||||
|
? { success: true, data: false, available: true, response }
|
||||||
|
: { success: false, available: false, response }
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks whether a username is available
|
||||||
|
*
|
||||||
|
* @param {string} username - The username to check
|
||||||
|
* @return {array} result - The REST response, a [status, data] array
|
||||||
|
*/
|
||||||
|
Backend.prototype.isUsernameAvailable = async function (username) {
|
||||||
|
const response = await this.post(`/available/username/jwt`, { username })
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Status 404 means username is available, which is success in this case
|
||||||
|
*/
|
||||||
|
return response[0] === 404
|
||||||
|
? { success: true, data: false, available: true, response }
|
||||||
|
: { success: false, available: false, response }
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
/*
|
||||||
|
* Confirm newsletter subscribe
|
||||||
|
*
|
||||||
|
* @param {object} data
|
||||||
|
* @param {string} data.id - The confirmation ID
|
||||||
|
* @param {string} data.ehash - The ehash value
|
||||||
|
* @return {array} result - The REST response, a [status, data] array
|
||||||
|
*/
|
||||||
|
Backend.prototype.newsletterConfirmSubscribe = async function ({ id, ehash }) {
|
||||||
|
return await this.put('/subscriber', { id, ehash })
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Subscribe to newsletter
|
||||||
|
*
|
||||||
|
* @param {string} email - The email to subscribe
|
||||||
|
* @return {array} result - The REST response, a [status, data] array
|
||||||
|
*/
|
||||||
|
Backend.prototype.newsletterSubscribe = async function (email) {
|
||||||
|
return await this.post('/subscriber', { email, language: 'en' })
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Newsletter unsubscribe
|
||||||
|
*
|
||||||
|
* @param {string} ehash - The ehash to unsubscribe
|
||||||
|
* @return {array} result - The REST response, a [status, data] array
|
||||||
|
*/
|
||||||
|
Backend.prototype.newsletterUnsubscribe = async function (ehash) {
|
||||||
|
return await this.delete(`/subscriber/${ehash}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Init Oauth flow for oauth provider
|
||||||
|
*
|
||||||
|
* @param {object} data
|
||||||
|
* @param {string} data.provider - ID of the OAuth provider
|
||||||
|
* @return {array} result - The REST response, a [status, data] array
|
||||||
|
*/
|
||||||
|
Backend.prototype.oauthInit = async function (provider) {
|
||||||
|
return await this.post('/signin/oauth/init', { provider })
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* User sign in via oauth provider
|
||||||
|
*
|
||||||
|
* @param {object} data
|
||||||
|
* @param {string} data.state - The Oath state
|
||||||
|
* @param {code} data.code - The OAuth code
|
||||||
|
* @param {string} data.provider - ID of the OAuth provider
|
||||||
|
* @return {array} result - The REST response, a [status, data] array
|
||||||
|
*/
|
||||||
|
Backend.prototype.oauthSignIn = async function ({ state, code, provider }) {
|
||||||
|
return await this.post('/signin/oauth', { state, code, provider })
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ping backend to see if current token is still valid
|
||||||
|
*
|
||||||
|
* @return {array} result - The REST response, a [status, data] array
|
||||||
|
*/
|
||||||
|
Backend.prototype.ping = async function () {
|
||||||
|
return await this.get(`/whoami/jwt`)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reload account - Useful for when local state gets out of sync
|
||||||
|
*
|
||||||
|
* @return {array} result - The REST response, a [status, data] array
|
||||||
|
*/
|
||||||
|
Backend.prototype.reloadAccount = async function () {
|
||||||
|
return await this.get(`/whoami/jwt`)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove account (the current logged in user)
|
||||||
|
*
|
||||||
|
* @return {array} result - The REST response, a [status, data] array
|
||||||
|
*/
|
||||||
|
Backend.prototype.removeAccount = async function () {
|
||||||
|
return await this.delete(`/account/jwt`)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove API key
|
||||||
|
*
|
||||||
|
* @return {array} result - The REST response, a [status, data] array
|
||||||
|
*/
|
||||||
|
Backend.prototype.removeApikey = async function (id) {
|
||||||
|
return await this.delete(`/apikeys/${id}/jwt`, this.auth)
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Remove bookmark
|
||||||
|
*
|
||||||
|
* @param {string} id - The bookmark ID
|
||||||
|
* @return {array} result - The REST response, a [status, data] array
|
||||||
|
*/
|
||||||
|
Backend.prototype.removeBookmark = async function (id) {
|
||||||
|
return await this.delete(`/bookmarks/${id}/jwt`)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove curated measurements set
|
||||||
|
*
|
||||||
|
* @return {array} result - The REST response, a [status, data] array
|
||||||
|
*/
|
||||||
|
Backend.prototype.removeCuratedSet = async function (id) {
|
||||||
|
return await this.delete(`/curated-sets/${id}/jwt`)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove an uploaded image
|
||||||
|
*
|
||||||
|
* @param {string} id - The image ID
|
||||||
|
* @return {array} result - The REST response, a [status, data] array
|
||||||
|
*/
|
||||||
|
Backend.prototype.removeImage = async function (id) {
|
||||||
|
return await this.delete(`/images/${id}/jwt`)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove pattern
|
||||||
|
*
|
||||||
|
* @return {array} result - The REST response, a [status, data] array
|
||||||
|
*/
|
||||||
|
Backend.prototype.removePattern = async function (id) {
|
||||||
|
return await this.delete(`/patterns/${id}/jwt`, this.auth)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove measurements set
|
||||||
|
*
|
||||||
|
* @param {string} id - The measurements set ID
|
||||||
|
* @return {array} result - The REST response, a [status, data] array
|
||||||
|
*/
|
||||||
|
Backend.prototype.removeSet = async function (id) {
|
||||||
|
return await this.delete(`/sets/${id}/jwt`)
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Remove suggested measurements set
|
||||||
|
*/
|
||||||
|
Backend.prototype.removeSuggestedSet = async function (id) {
|
||||||
|
return await this.delete(`/suggested-sets/${id}/jwt`)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Restrict processing of account data
|
||||||
|
*
|
||||||
|
* @return {array} result - The REST response, a [status, data] array
|
||||||
|
*/
|
||||||
|
Backend.prototype.restrictAccount = async function () {
|
||||||
|
return await this.get(`/account/restrict/jwt`)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* User signin/login
|
||||||
|
*
|
||||||
|
* @param {object} data
|
||||||
|
* @param {string} data.username - The account username
|
||||||
|
* @param {string} data.password - The account password
|
||||||
|
* @param {string} data.token - The (optional) MFA token
|
||||||
|
* @return {array} result - The REST response, a [status, data] array
|
||||||
|
*/
|
||||||
|
Backend.prototype.signIn = async function ({ username, password = false, token = false }) {
|
||||||
|
return password === false
|
||||||
|
? await this.post('/signinlink', { username })
|
||||||
|
: await this.post('/signin', { username, password, token })
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Trade in sign-in link id & check for JWT token
|
||||||
|
*
|
||||||
|
* @param {object} data
|
||||||
|
* @param {string} data.id - The confirmation ID
|
||||||
|
* @param {string} data.check - The confirmation check value
|
||||||
|
* @return {array} result - The REST response, a [status, data] array
|
||||||
|
*/
|
||||||
|
Backend.prototype.signInFromLink = async function ({ id, check }) {
|
||||||
|
return await this.post(`/signinlink/${id}/${check}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* User signup
|
||||||
|
*
|
||||||
|
* @param {object} data
|
||||||
|
* @param {string} data.email - The Email address to sign up
|
||||||
|
* @return {array} result - The REST response, a [status, data] array
|
||||||
|
*/
|
||||||
|
Backend.prototype.signUp = async function ({ email }) {
|
||||||
|
return await this.post('/signup', { email })
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Suggest a measurements set for curation
|
||||||
|
*
|
||||||
|
* @param {object} data - Data for the API call
|
||||||
|
* @return {array} result - The REST response, a [status, data] array
|
||||||
|
*/
|
||||||
|
Backend.prototype.suggestCset = async function (data) {
|
||||||
|
return await this.post(`/curated-sets/suggest/jwt`, data)
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Suggest an option pack for curation
|
||||||
|
*
|
||||||
|
* @param {object} data - Data for the API call
|
||||||
|
* @return {array} result - The REST response, a [status, data] array
|
||||||
|
*/
|
||||||
|
Backend.prototype.suggestOpack = async function (data) {
|
||||||
|
return await this.post(`/option-packs/suggest/jwt`, data)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generic update account method
|
||||||
|
*
|
||||||
|
* @param {object} data - Data for the API call
|
||||||
|
* @return {array} result - The REST response, a [status, data] array
|
||||||
|
*/
|
||||||
|
Backend.prototype.updateAccount = async function (data) {
|
||||||
|
return await this.patch(`/account/jwt`, data)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generic update curated measurements set method
|
||||||
|
*
|
||||||
|
* @param {object} consent
|
||||||
|
* @return {array} result - The REST response, a [status, data] array
|
||||||
|
*/
|
||||||
|
Backend.prototype.updateCuratedSet = async function (id, data) {
|
||||||
|
return await this.patch(`/curated-sets/${id}/jwt`, data)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update consent (uses the jwt-guest middleware)
|
||||||
|
*
|
||||||
|
* @param {object} consent
|
||||||
|
* @return {array} result - The REST response, a [status, data] array
|
||||||
|
*/
|
||||||
|
Backend.prototype.updateConsent = async function (consent) {
|
||||||
|
return await this.patch(`/consent/jwt`, { consent })
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generic update pattern set method
|
||||||
|
*
|
||||||
|
* @param {object} data - Data for the API call
|
||||||
|
* @return {array} result - The REST response, a [status, data] array
|
||||||
|
*/
|
||||||
|
Backend.prototype.updatePattern = async function (id, data) {
|
||||||
|
return await this.patch(`/patterns/${id}/jwt`, data)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generic update measurements set method
|
||||||
|
*
|
||||||
|
* @param {object} data - Data for the API call
|
||||||
|
* @return {array} result - The REST response, a [status, data] array
|
||||||
|
*/
|
||||||
|
Backend.prototype.updateSet = async function (id, data) {
|
||||||
|
return await this.patch(`/sets/${id}/jwt`, data)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Upload an image
|
||||||
|
*
|
||||||
|
* @param {object} data - Data for the API call
|
||||||
|
* @return {array} result - The REST response, a [status, data] array
|
||||||
|
*/
|
||||||
|
Backend.prototype.uploadImage = async function (data) {
|
||||||
|
return await this.post('/images/jwt', data)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Upload an image anonymously
|
||||||
|
*
|
||||||
|
* @param {object} data - Data for the API call
|
||||||
|
* @return {array} result - The REST response, a [status, data] array
|
||||||
|
*/
|
||||||
|
Backend.prototype.uploadImageAnon = async function (data) {
|
||||||
|
return await this.post('/images', data)
|
||||||
|
}
|
120
packages/react/lib/RestClient/index.mjs
Normal file
120
packages/react/lib/RestClient/index.mjs
Normal file
|
@ -0,0 +1,120 @@
|
||||||
|
/**
|
||||||
|
* A simple HTTP REST client
|
||||||
|
*
|
||||||
|
* We use this in the useBackend hook, but it's also possible to use this with
|
||||||
|
* any other REST API.
|
||||||
|
* This uses the fetch API, so it has no dependencies.
|
||||||
|
* However, that means you cannot use this with a REST API using a non-trusted X.509 certificate, since fetch does not provide an escape hatch for that.
|
||||||
|
* If that's your use-case, use Axios instead.
|
||||||
|
*
|
||||||
|
* @param {string} baseUrl - The base URL of the API that will be used as a prefix for all calls
|
||||||
|
* @param {string} baseHeaders - Any headers to add to each request. Eg for authentication
|
||||||
|
* @return {object} client - An object with get, post, and put methods
|
||||||
|
*/
|
||||||
|
export function RestClient(baseUrl = '', baseHeaders = {}) {
|
||||||
|
this.baseUrl = baseUrl
|
||||||
|
this.baseHeaders = baseHeaders
|
||||||
|
|
||||||
|
this.delete = async function (url, headers, raw, log) {
|
||||||
|
return withoutBody('DELETE', baseUrl + url, { ...baseHeaders, ...headers }, raw, log)
|
||||||
|
}
|
||||||
|
|
||||||
|
this.get = async function (url, headers, raw, log) {
|
||||||
|
return withoutBody('GET', baseUrl + url, { ...baseHeaders, ...headers }, raw, log)
|
||||||
|
}
|
||||||
|
|
||||||
|
this.head = async function (url, headers, raw, log) {
|
||||||
|
return withoutBody('HEAD', baseUrl + url, { ...baseHeaders, ...headers }, raw, log)
|
||||||
|
}
|
||||||
|
|
||||||
|
this.post = async function (url, data, headers, raw, log) {
|
||||||
|
return withBody('POST', baseUrl + url, data, { ...baseHeaders, ...headers }, raw, log)
|
||||||
|
}
|
||||||
|
|
||||||
|
this.put = async function (url, data, headers, raw, log) {
|
||||||
|
return withBody('PUT', baseUrl + url, data, { ...baseHeaders, headers }, raw, log)
|
||||||
|
}
|
||||||
|
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* General purpose method to call a REST API without a body (eg: GET or DELETE requests)
|
||||||
|
*
|
||||||
|
* @param {string} url - The URL to call
|
||||||
|
* @param {object} headers - Any request headers to add
|
||||||
|
* @param {bool} raw - Set this to something truthy to not parse the result as JSON
|
||||||
|
* @param {function} log - Optional custom logging method to log errors
|
||||||
|
* @return {response} array - An array with status code followed by either the result parse as JSON, the raw result, or false in case of trouble
|
||||||
|
*/
|
||||||
|
async function withoutBody(method = 'GET', url, headers = {}, raw = false, log = console.log) {
|
||||||
|
let response
|
||||||
|
try {
|
||||||
|
response = await fetch(url, { method, headers })
|
||||||
|
} catch (err) {
|
||||||
|
if (log) console.log({ url, err })
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!response) return [false, false]
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Can we parse the response as JSON?
|
||||||
|
*/
|
||||||
|
let body
|
||||||
|
try {
|
||||||
|
body = raw ? await response.text() : await response.json()
|
||||||
|
} catch (err) {
|
||||||
|
try {
|
||||||
|
body = await response.text()
|
||||||
|
} catch (err) {
|
||||||
|
body = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return [response.status || false, body]
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* General purpose method to call a REST API with a body (eg: PATCH, PUT, or POST requests)
|
||||||
|
*
|
||||||
|
* @param {url} string - The URL to call
|
||||||
|
* @param {data} string - The data to send
|
||||||
|
* @param {object} headers - Any request headers to add
|
||||||
|
* @param {raw} string - Set this to something truthy to not parse the result as JSON
|
||||||
|
* @param {function} log - Optional logging method to log errors
|
||||||
|
* @return {response} array - An array with status code followed by either the result parse as JSON, the raw result, or false in case of trouble
|
||||||
|
*/
|
||||||
|
async function withBody(method = 'POST', url, data, headers, raw = false, log = console.log) {
|
||||||
|
const request = { method, headers }
|
||||||
|
if (data && typeof data === 'object' && Object.keys(data).length > 0) {
|
||||||
|
request.body = JSON.stringify(data)
|
||||||
|
request.headers['Content-Type'] = 'application/json'
|
||||||
|
}
|
||||||
|
|
||||||
|
let response
|
||||||
|
try {
|
||||||
|
response = await fetch(url, request)
|
||||||
|
} catch (err) {
|
||||||
|
if (log) log(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Some status codes have no response body
|
||||||
|
*/
|
||||||
|
if (response?.status && [204].includes(response.status)) return [response.status, {}]
|
||||||
|
else if (response?.status && response.status < 400) {
|
||||||
|
let data
|
||||||
|
try {
|
||||||
|
data = raw ? await response.text() : await response.json()
|
||||||
|
} catch (err) {
|
||||||
|
if (log) log(err)
|
||||||
|
return raw ? [response.status, { err }] : [response.status, data]
|
||||||
|
}
|
||||||
|
return [response.status, data]
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* If we end up here, status code is 400 or higher so it's an error
|
||||||
|
*/
|
||||||
|
return [response?.status || 500, false]
|
||||||
|
}
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"name": "@freesewing/react-components",
|
"name": "@freesewing/react",
|
||||||
"version": "3.3.0-rc.1",
|
"version": "3.3.0-rc.1",
|
||||||
"description": "React components by/for FreeSewing",
|
"description": "React components, hooks and context by/for FreeSewing",
|
||||||
"author": "Joost De Cock <joost@joost.at> (https://github.com/joostdecock)",
|
"author": "Joost De Cock <joost@joost.at> (https://github.com/joostdecock)",
|
||||||
"homepage": "https://freesewing.org/",
|
"homepage": "https://freesewing.org/",
|
||||||
"repository": "github:freesewing/freesewing",
|
"repository": "github:freesewing/freesewing",
|
||||||
|
@ -26,21 +26,15 @@
|
||||||
},
|
},
|
||||||
"./linedrawings": "./src/linedrawings/index.mjs",
|
"./linedrawings": "./src/linedrawings/index.mjs",
|
||||||
"./pattern": "./src/pattern/index.mjs",
|
"./pattern": "./src/pattern/index.mjs",
|
||||||
"./xray": "./src/pattern-xray/index.mjs"
|
"./xray": "./src/pattern-xray/index.mjs",
|
||||||
|
"./config/freesewing": "./config/freesewing/index.mjs",
|
||||||
|
"./context/loadingStatus": "./context/loadingStatus/index.mjs",
|
||||||
|
"./hooks/useBackend": "./hooks/useBackend/index.mjs",
|
||||||
|
"./lib/RestClient": "./lib/RestClient/index.mjs"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "node build.mjs",
|
"lint": "eslint 'components/**/*.mjs' 'hooks/**/*.mjs' 'lib/**/*.mjs' 'context/**/*.mjs' 'config/**/*.mjs'",
|
||||||
"build:all": "yarn build",
|
"prettier": "npx prettier --write 'components/**/*.mjs' 'hooks/**/*.mjs' 'lib/**/*.mjs' 'context/**/*.mjs' 'config/**/*.mjs'"
|
||||||
"clean": "rimraf dist",
|
|
||||||
"mbuild": "NO_MINIFY=1 node build.mjs",
|
|
||||||
"symlink": "mkdir -p ./node_modules/@freesewing && cd ./node_modules/@freesewing && ln -s -f ../../../* . && cd -",
|
|
||||||
"test": "echo \"react-components: No tests configured. Perhaps you could write some?\" && exit 0",
|
|
||||||
"vbuild": "VERBOSE=1 node build.mjs",
|
|
||||||
"lab": "cd ../../sites/lab && yarn start",
|
|
||||||
"tips": "node ../../scripts/help.mjs",
|
|
||||||
"lint": "eslint 'src/**/*.mjs'",
|
|
||||||
"wbuild": "node build.mjs",
|
|
||||||
"wbuild:all": "yarn wbuild"
|
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"react": "^18.2.0"
|
"react": "^18.2.0"
|
||||||
|
@ -55,7 +49,9 @@
|
||||||
},
|
},
|
||||||
"devDependencies": {},
|
"devDependencies": {},
|
||||||
"files": [
|
"files": [
|
||||||
"dist/*",
|
"components/**",
|
||||||
|
"hooks/**",
|
||||||
|
"lib/**",
|
||||||
"README.md"
|
"README.md"
|
||||||
],
|
],
|
||||||
"publishConfig": {
|
"publishConfig": {
|
|
@ -247,8 +247,8 @@ export const MeasurementInput = ({
|
||||||
typeof original === 'undefined'
|
typeof original === 'undefined'
|
||||||
? original
|
? original
|
||||||
: isDegree
|
: isDegree
|
||||||
? Number(original)
|
? Number(original)
|
||||||
: Swizzled.methods.measurementAsUnits(original, units)
|
: Swizzled.methods.measurementAsUnits(original, units)
|
||||||
)
|
)
|
||||||
const [validatedVal, setValidatedVal] = useState(
|
const [validatedVal, setValidatedVal] = useState(
|
||||||
Swizzled.methods.measurementAsUnits(original, units)
|
Swizzled.methods.measurementAsUnits(original, units)
|
|
@ -164,10 +164,10 @@ export const MenuItemGroup = ({
|
||||||
const ItemIcon = item.icon
|
const ItemIcon = item.icon
|
||||||
? item.icon
|
? item.icon
|
||||||
: item.isGroup
|
: item.isGroup
|
||||||
? Swizzled.components.GroupIcon
|
? Swizzled.components.GroupIcon
|
||||||
: Icon
|
: Icon
|
||||||
? Icon
|
? Icon
|
||||||
: () => <span role="img">fixme-icon</span>
|
: () => <span role="img">fixme-icon</span>
|
||||||
const Value = item.isGroup
|
const Value = item.isGroup
|
||||||
? () => (
|
? () => (
|
||||||
<div className="flex flex-row gap-2 items-center font-medium">
|
<div className="flex flex-row gap-2 items-center font-medium">
|
||||||
|
@ -176,10 +176,10 @@ export const MenuItemGroup = ({
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
: isDesignOptionsGroup
|
: isDesignOptionsGroup
|
||||||
? values[Swizzled.methods.designOptionType(item)]
|
? values[Swizzled.methods.designOptionType(item)]
|
||||||
: values[itemName]
|
: values[itemName]
|
||||||
? values[itemName]
|
? values[itemName]
|
||||||
: () => <span>¯\_(ツ)_/¯</span>
|
: () => <span>¯\_(ツ)_/¯</span>
|
||||||
|
|
||||||
return [
|
return [
|
||||||
<div className="flex flex-row items-center justify-between w-full" key="a">
|
<div className="flex flex-row items-center justify-between w-full" key="a">
|
|
@ -80,8 +80,8 @@ export const MenuListInput = ({
|
||||||
const titleKey = config.choiceTitles
|
const titleKey = config.choiceTitles
|
||||||
? config.choiceTitles[entry]
|
? config.choiceTitles[entry]
|
||||||
: isDesignOption
|
: isDesignOption
|
||||||
? `${design}:${name}.${entry}`
|
? `${design}:${name}.${entry}`
|
||||||
: `${name}.o.${entry}`
|
: `${name}.o.${entry}`
|
||||||
const title = config.titleMethod ? config.titleMethod(entry, t) : t(`${titleKey}.t`)
|
const title = config.titleMethod ? config.titleMethod(entry, t) : t(`${titleKey}.t`)
|
||||||
const desc = config.valueMethod ? config.valueMethod(entry, t) : t(`${titleKey}.d`)
|
const desc = config.valueMethod ? config.valueMethod(entry, t) : t(`${titleKey}.d`)
|
||||||
const sideBySide = config.sideBySide || desc.length + title.length < 42
|
const sideBySide = config.sideBySide || desc.length + title.length < 42
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue