1
0
Fork 0

[breaking]: FreeSewing v4 (#7297)

Refer to the CHANGELOG for all info.

---------

Co-authored-by: Wouter van Wageningen <wouter.vdub@yahoo.com>
Co-authored-by: Josh Munic <jpmunic@gmail.com>
Co-authored-by: Jonathan Haas <haasjona@gmail.com>
This commit is contained in:
Joost De Cock 2025-04-01 16:15:20 +02:00 committed by GitHub
parent d22fbe78d9
commit 51dc1d9732
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6626 changed files with 142053 additions and 150606 deletions

View file

@ -0,0 +1,103 @@
import useLocalStorageState from 'use-local-storage-state'
import { control as defaultControlLevel } from '@freesewing/config'
/*
* 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 = () => {
console.log('SIGN OUT')
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,
}
}

View file

@ -0,0 +1,830 @@
import { useMemo } from 'react'
import { urls } from '@freesewing/config'
import { RestClient } from '@freesewing/react/lib/RestClient'
import { useAccount } from '@freesewing/react/hooks/useAccount'
/*
* Get backend URL from config
*/
const { backend } = urls
/**
* 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 ? { Authorization: 'Bearer ' + token } : {}
}
/**
* 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.delete = this.restClient.delete
this.get = this.restClient.get
this.patch = this.restClient.patch
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) {
console.log('admin ping called', 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}`)
}
/*
* Newsletter start unsubscribe flow
*
* @param {string} ehash - The email address for which to start the unsubscribe flow
* @return {array} result - The REST response, a [status, data] array
*/
Backend.prototype.newsletterStartUnsubscribe = async function (email) {
return await this.post(`/subscriber/remove`, { email })
}
/*
* 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 () {
console.log('ping called')
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 () {
console.log('reloadAccount called')
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, token }) {
return await this.post(`/signinlink/${id}/${check}`, { token })
}
/**
* 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, language: 'en' })
}
/*
* 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)
}

View file

@ -0,0 +1,46 @@
// Context
import { LoadingStatusContext } from '@freesewing/react/context/LoadingStatus'
// Hooks
import React, { useState, useContext } from 'react'
import { useAccount } from '@freesewing/react/hooks/useAccount'
import { useBackend } from '@freesewing/react/hooks/useBackend'
/**
* Control can be updated from many places in the UI.
* So this shared state handler keeps this DRY
*/
export const useControl = () => {
// Hooks
const backend = useBackend()
const { account, setAccount, token } = useAccount()
const { setLoadingStatus } = useContext(LoadingStatusContext)
// State
const [control, setControl] = useState(account.control)
// Method to update the control setting
const updateControl = async (newControl) => {
if (newControl !== control) {
if (token) {
setLoadingStatus([true, 'Updating preferences'])
const [status, body] = await backend.updateAccount({ control: newControl })
if (status === 200) {
setControl(newControl)
setAccount(body.account)
setLoadingStatus([true, 'Preferences updated', true, true])
} else
setLoadingStatus([true, 'Failed to update preferences. Please report this', true, true])
} else {
/*
* Control is used even when people are not logged in
* So this ensures control is always available, even if people are not authenticated
*/
setAccount({ ...account, control: newControl })
setControl(newControl)
}
}
}
return { control, updateControl }
}

View file

@ -0,0 +1,125 @@
import { Aaron as aaron } from '@freesewing/aaron'
import { Albert as albert } from '@freesewing/albert'
import { Bee as bee } from '@freesewing/bee'
import { Bella as bella } from '@freesewing/bella'
import { Benjamin as benjamin } from '@freesewing/benjamin'
import { Bent as bent } from '@freesewing/bent'
import { Bibi as bibi } from '@freesewing/bibi'
import { Bob as bob } from '@freesewing/bob'
import { Breanna as breanna } from '@freesewing/breanna'
import { Brian as brian } from '@freesewing/brian'
import { Bruce as bruce } from '@freesewing/bruce'
import { Carlita as carlita } from '@freesewing/carlita'
import { Carlton as carlton } from '@freesewing/carlton'
import { Cathrin as cathrin } from '@freesewing/cathrin'
import { Charlie as charlie } from '@freesewing/charlie'
import { Cornelius as cornelius } from '@freesewing/cornelius'
import { Diana as diana } from '@freesewing/diana'
import { Florence as florence } from '@freesewing/florence'
import { Florent as florent } from '@freesewing/florent'
import { Gozer as gozer } from '@freesewing/gozer'
import { Hi as hi } from '@freesewing/hi'
import { Holmes as holmes } from '@freesewing/holmes'
import { Hortensia as hortensia } from '@freesewing/hortensia'
import { Huey as huey } from '@freesewing/huey'
import { Hugo as hugo } from '@freesewing/hugo'
import { Jaeger as jaeger } from '@freesewing/jaeger'
import { Jane as jane } from '@freesewing/jane'
import { Lucy as lucy } from '@freesewing/lucy'
import { Lumina as lumina } from '@freesewing/lumina'
import { Lumira as lumira } from '@freesewing/lumira'
import { Lunetius as lunetius } from '@freesewing/lunetius'
import { Noble as noble } from '@freesewing/noble'
import { Octoplushy as octoplushy } from '@freesewing/octoplushy'
import { Onyx as onyx } from '@freesewing/onyx'
import { Opal as opal } from '@freesewing/opal'
import { Otis as otis } from '@freesewing/otis'
import { Paco as paco } from '@freesewing/paco'
import { Penelope as penelope } from '@freesewing/penelope'
import { Sandy as sandy } from '@freesewing/sandy'
import { Shelly as shelly } from '@freesewing/shelly'
import { Shin as shin } from '@freesewing/shin'
import { Simon as simon } from '@freesewing/simon'
import { Simone as simone } from '@freesewing/simone'
import { Skully as skully } from '@freesewing/skully'
import { Sven as sven } from '@freesewing/sven'
import { Tamiko as tamiko } from '@freesewing/tamiko'
import { Teagan as teagan } from '@freesewing/teagan'
import { Tiberius as tiberius } from '@freesewing/tiberius'
import { Titan as titan } from '@freesewing/titan'
import { Trayvon as trayvon } from '@freesewing/trayvon'
import { Tristan as tristan } from '@freesewing/tristan'
import { Uma as uma } from '@freesewing/uma'
import { Umbra as umbra } from '@freesewing/umbra'
import { Wahid as wahid } from '@freesewing/wahid'
import { Walburga as walburga } from '@freesewing/walburga'
import { Waralee as waralee } from '@freesewing/waralee'
import { Yuri as yuri } from '@freesewing/yuri'
import { Lily as lily } from '@freesewing/lily'
import { Bonny as bonny } from '@freesewing/bonny'
export const designs = {
aaron,
albert,
bee,
bella,
benjamin,
bent,
bibi,
bob,
breanna,
brian,
bruce,
carlita,
carlton,
cathrin,
charlie,
cornelius,
diana,
florence,
florent,
gozer,
hi,
holmes,
hortensia,
huey,
hugo,
jaeger,
jane,
lucy,
lumina,
lumira,
lunetius,
noble,
octoplushy,
onyx,
opal,
otis,
paco,
penelope,
sandy,
shelly,
shin,
simon,
simone,
skully,
sven,
tamiko,
teagan,
tiberius,
titan,
trayvon,
tristan,
uma,
umbra,
wahid,
walburga,
waralee,
yuri,
lily,
bonny,
}
export const useDesign = (design) => (designs[design] ? designs[design] : false)
export const collection = Object.keys(designs)

View file

@ -0,0 +1,122 @@
import { i18n as aaron } from '@freesewing/aaron'
import { i18n as albert } from '@freesewing/albert'
import { i18n as bee } from '@freesewing/bee'
import { i18n as bella } from '@freesewing/bella'
import { i18n as benjamin } from '@freesewing/benjamin'
import { i18n as bent } from '@freesewing/bent'
import { i18n as bibi } from '@freesewing/bibi'
import { i18n as bob } from '@freesewing/bob'
import { i18n as breanna } from '@freesewing/breanna'
import { i18n as brian } from '@freesewing/brian'
import { i18n as bruce } from '@freesewing/bruce'
import { i18n as carlita } from '@freesewing/carlita'
import { i18n as carlton } from '@freesewing/carlton'
import { i18n as cathrin } from '@freesewing/cathrin'
import { i18n as charlie } from '@freesewing/charlie'
import { i18n as cornelius } from '@freesewing/cornelius'
import { i18n as diana } from '@freesewing/diana'
import { i18n as florence } from '@freesewing/florence'
import { i18n as florent } from '@freesewing/florent'
import { i18n as gozer } from '@freesewing/gozer'
import { i18n as hi } from '@freesewing/hi'
import { i18n as holmes } from '@freesewing/holmes'
import { i18n as hortensia } from '@freesewing/hortensia'
import { i18n as huey } from '@freesewing/huey'
import { i18n as hugo } from '@freesewing/hugo'
import { i18n as jaeger } from '@freesewing/jaeger'
import { i18n as jane } from '@freesewing/jane'
import { i18n as lucy } from '@freesewing/lucy'
import { i18n as lumina } from '@freesewing/lumina'
import { i18n as lumira } from '@freesewing/lumira'
import { i18n as lunetius } from '@freesewing/lunetius'
import { i18n as noble } from '@freesewing/noble'
import { i18n as octoplushy } from '@freesewing/octoplushy'
import { i18n as onyx } from '@freesewing/onyx'
import { i18n as opal } from '@freesewing/opal'
import { i18n as otis } from '@freesewing/otis'
import { i18n as paco } from '@freesewing/paco'
import { i18n as penelope } from '@freesewing/penelope'
import { i18n as sandy } from '@freesewing/sandy'
import { i18n as shelly } from '@freesewing/shelly'
import { i18n as shin } from '@freesewing/shin'
import { i18n as simon } from '@freesewing/simon'
import { i18n as simone } from '@freesewing/simone'
import { i18n as skully } from '@freesewing/skully'
import { i18n as sven } from '@freesewing/sven'
import { i18n as tamiko } from '@freesewing/tamiko'
import { i18n as teagan } from '@freesewing/teagan'
import { i18n as tiberius } from '@freesewing/tiberius'
import { i18n as titan } from '@freesewing/titan'
import { i18n as trayvon } from '@freesewing/trayvon'
import { i18n as tristan } from '@freesewing/tristan'
import { i18n as uma } from '@freesewing/uma'
import { i18n as umbra } from '@freesewing/umbra'
import { i18n as wahid } from '@freesewing/wahid'
import { i18n as walburga } from '@freesewing/walburga'
import { i18n as waralee } from '@freesewing/waralee'
import { i18n as yuri } from '@freesewing/yuri'
import { i18n as lily } from '@freesewing/lily'
export const designTranslations = {
aaron,
albert,
bee,
bella,
benjamin,
bent,
bibi,
bob,
breanna,
brian,
bruce,
carlita,
carlton,
cathrin,
charlie,
cornelius,
diana,
florence,
florent,
gozer,
hi,
holmes,
hortensia,
huey,
hugo,
jaeger,
jane,
lucy,
lumina,
lumira,
lunetius,
noble,
octoplushy,
onyx,
opal,
otis,
paco,
penelope,
sandy,
shelly,
shin,
simon,
simone,
skully,
sven,
tamiko,
teagan,
tiberius,
titan,
trayvon,
tristan,
uma,
umbra,
wahid,
walburga,
waralee,
yuri,
lily,
}
export const useDesignTranslation = (design) =>
designTranslations[design] ? designTranslations[design] : false

View file

@ -0,0 +1,40 @@
import React, { useState } from 'react'
export const useSelection = (items) => {
const [selection, setSelection] = useState({})
/*
* This variable keeps track of how many are selected
*/
const count = Object.keys(selection).length
/*
* This method toggles a single item in the selection
*/
const toggle = (id) => {
const newSelection = { ...selection }
if (newSelection[id]) delete newSelection[id]
else newSelection[id] = 1
setSelection(newSelection)
}
/*
* This method toggles all on or off
*/
const toggleAll = () => {
if (count === items.length) setSelection({})
else {
const newSelection = {}
for (const item of items) newSelection[item.id] = 1
setSelection(newSelection)
}
}
return {
count,
selection,
setSelection,
toggle,
toggleAll,
}
}

View file

@ -0,0 +1,48 @@
import React, { useState } from 'react'
import set from 'lodash/set.js'
import unset from 'lodash/unset.js'
/*
* Helper method to handle object updates
*
* This is a wrapper around lodash.set() with extra support for unsetting data.
*
* @param {object} obj - The object to update
* @param {array|string} path - The path to the property to update either as an array or string in dot notation
* @param {mixed} val - The value to set. If the value holds the string 'unset' the property will be removed
*
* @return {object} obj - The mutated object
*/
export const objUpdate = (obj = {}, path, val = undefined) => {
if (val === undefined) {
if (Array.isArray(path) && Array.isArray(path[0])) {
for (const [ipath, ival = undefined] of path) {
if (ival === undefined) unset(obj, ipath)
else set(obj, ipath, ival)
}
} else unset(obj, path)
} else set(obj, path, val)
return obj
}
/*
* This hooks provides an React state update with an
* update method that allows us to set deeply nested
* properties.
*/
export const useStateObject = (dflt = {}) => {
const [obj, setObj] = useState(dflt)
const replace = (val) => setObj(val)
const update = (path, val, altObj = false) => {
const newObj = altObj ? { ...altObj } : { ...obj }
objUpdate(newObj, path, val)
setObj(newObj)
return newObj
}
return [obj, update, replace]
}