Merge pull request #4777 from freesewing/joost
wip(org): Improved account pages
This commit is contained in:
commit
319ac98e3a
89 changed files with 2165 additions and 789 deletions
|
@ -18,4 +18,4 @@ dist
|
|||
node_modules
|
||||
yarn.lock
|
||||
package.json
|
||||
.html
|
||||
*.html
|
||||
|
|
|
@ -20,6 +20,15 @@ model Apikey {
|
|||
userId Int
|
||||
}
|
||||
|
||||
model Bookmark {
|
||||
id Int @id @default(autoincrement())
|
||||
type String @default("")
|
||||
title String @default("")
|
||||
url String @default("")
|
||||
user User @relation(fields: [userId], references: [id])
|
||||
userId Int
|
||||
}
|
||||
|
||||
model Confirmation {
|
||||
id String @id @default(uuid())
|
||||
createdAt DateTime @default(now())
|
||||
|
@ -43,6 +52,7 @@ model User {
|
|||
id Int @id @default(autoincrement())
|
||||
apikeys Apikey[]
|
||||
bio String @default("")
|
||||
bookmarks Bookmark[]
|
||||
compare Boolean @default(true)
|
||||
confirmations Confirmation[]
|
||||
consent Int @default(0)
|
||||
|
|
|
@ -85,6 +85,9 @@ const baseConfig = {
|
|||
db: {
|
||||
url: process.env.BACKEND_DB_URL || './db.sqlite',
|
||||
},
|
||||
bookmarks: {
|
||||
types: ['set', 'cset', 'pattern', 'design', 'doc', 'custom'],
|
||||
},
|
||||
encryption: {
|
||||
key: encryptionKey,
|
||||
},
|
||||
|
@ -98,6 +101,10 @@ const baseConfig = {
|
|||
newsletter: [true, false],
|
||||
},
|
||||
},
|
||||
exports: {
|
||||
dir: process.env.BACKEND_EXPORTS_DIR || '/tmp',
|
||||
url: process.env.BACKEND_EXPORTS_URL || 'https://static3.freesewing.org/export/',
|
||||
},
|
||||
github: {
|
||||
token: process.env.BACKEND_GITHUB_TOKEN,
|
||||
},
|
||||
|
@ -234,6 +241,7 @@ export const forwardmx = config.forwardmx || {}
|
|||
export const website = config.website
|
||||
export const githubToken = config.github.token
|
||||
export const instance = config.instance
|
||||
export const exports = config.exports
|
||||
|
||||
const vars = {
|
||||
BACKEND_DB_URL: ['required', 'db.url'],
|
||||
|
|
61
sites/backend/src/controllers/bookmarks.mjs
Normal file
61
sites/backend/src/controllers/bookmarks.mjs
Normal file
|
@ -0,0 +1,61 @@
|
|||
import { BookmarkModel } from '../models/bookmark.mjs'
|
||||
|
||||
export function BookmarksController() {}
|
||||
|
||||
/*
|
||||
* Create a bookmark for the authenticated user
|
||||
* See: https://freesewing.dev/reference/backend/api
|
||||
*/
|
||||
BookmarksController.prototype.create = async (req, res, tools) => {
|
||||
const Bookmark = new BookmarkModel(tools)
|
||||
await Bookmark.guardedCreate(req)
|
||||
|
||||
return Bookmark.sendResponse(res)
|
||||
}
|
||||
|
||||
/*
|
||||
* Read a bookmark
|
||||
* See: https://freesewing.dev/reference/backend/api
|
||||
*/
|
||||
BookmarksController.prototype.read = async (req, res, tools) => {
|
||||
const Bookmark = new BookmarkModel(tools)
|
||||
await Bookmark.guardedRead(req)
|
||||
|
||||
return Bookmark.sendResponse(res)
|
||||
}
|
||||
|
||||
/*
|
||||
* Get a list of bookmarks
|
||||
* See: https://freesewing.dev/reference/backend/api
|
||||
*/
|
||||
BookmarksController.prototype.list = async (req, res, tools) => {
|
||||
const Bookmark = new BookmarkModel(tools)
|
||||
const bookmarks = await Bookmark.userBookmarks(req.user.uid)
|
||||
|
||||
if (bookmarks) Bookmark.setResponse(200, 'success', { bookmarks })
|
||||
else Bookmark.setResponse(404, 'notFound')
|
||||
|
||||
return Bookmark.sendResponse(res)
|
||||
}
|
||||
|
||||
/*
|
||||
* Update a bookmark
|
||||
* See: https://freesewing.dev/reference/backend/api
|
||||
*/
|
||||
BookmarksController.prototype.update = async (req, res, tools) => {
|
||||
const Bookmark = new BookmarkModel(tools)
|
||||
await Bookmark.guardedUpdate(req)
|
||||
|
||||
return Bookmark.sendResponse(res)
|
||||
}
|
||||
|
||||
/*
|
||||
* Remove a bookmark
|
||||
* See: https://freesewing.dev/reference/backend/api
|
||||
*/
|
||||
BookmarksController.prototype.delete = async (req, res, tools) => {
|
||||
const Bookmark = new BookmarkModel(tools)
|
||||
await Bookmark.guardedDelete(req)
|
||||
|
||||
return Bookmark.sendResponse(res)
|
||||
}
|
|
@ -117,6 +117,54 @@ UsersController.prototype.profile = async (req, res, tools) => {
|
|||
return User.sendResponse(res)
|
||||
}
|
||||
|
||||
/*
|
||||
* Returns all user data
|
||||
*
|
||||
* See: https://freesewing.dev/reference/backend/api
|
||||
*/
|
||||
UsersController.prototype.allData = async (req, res, tools) => {
|
||||
const User = new UserModel(tools)
|
||||
await User.allData(req)
|
||||
|
||||
return User.sendResponse(res)
|
||||
}
|
||||
|
||||
/*
|
||||
* Exports all account data
|
||||
*
|
||||
* See: https://freesewing.dev/reference/backend/api
|
||||
*/
|
||||
UsersController.prototype.exportAccount = async (req, res, tools) => {
|
||||
const User = new UserModel(tools)
|
||||
await User.exportAccount(req)
|
||||
|
||||
return User.sendResponse(res)
|
||||
}
|
||||
|
||||
/*
|
||||
* Restricts processing of account data
|
||||
*
|
||||
* See: https://freesewing.dev/reference/backend/api
|
||||
*/
|
||||
UsersController.prototype.restrictAccount = async (req, res, tools) => {
|
||||
const User = new UserModel(tools)
|
||||
await User.restrictAccount(req)
|
||||
|
||||
return User.sendResponse(res)
|
||||
}
|
||||
|
||||
/*
|
||||
* Remove account
|
||||
*
|
||||
* See: https://freesewing.dev/reference/backend/api
|
||||
*/
|
||||
UsersController.prototype.removeAccount = async (req, res, tools) => {
|
||||
const User = new UserModel(tools)
|
||||
await User.removeAccount(req)
|
||||
|
||||
return User.sendResponse(res)
|
||||
}
|
||||
|
||||
/*
|
||||
* Checks whether a submitted username is available
|
||||
*
|
||||
|
|
|
@ -61,11 +61,6 @@ ApikeyModel.prototype.guardedRead = async function ({ params, user }) {
|
|||
*/
|
||||
if (!this.rbac.readSome(user)) return this.setResponse(403, 'insufficientAccessLevel')
|
||||
|
||||
/*
|
||||
* Ensure the account is active
|
||||
*/
|
||||
if (user.iss && user.status < 1) return this.setResponse(403, 'accountStatusLacking')
|
||||
|
||||
/*
|
||||
* Attempt to read record from database
|
||||
*/
|
||||
|
@ -113,10 +108,6 @@ ApikeyModel.prototype.guardedDelete = async function ({ params, user }) {
|
|||
* Enforece RBAC
|
||||
*/
|
||||
if (!this.rbac.user(user)) return this.setResponse(403, 'insufficientAccessLevel')
|
||||
/*
|
||||
* Ensure the account is active
|
||||
*/
|
||||
if (user.iss && user.status < 1) return this.setResponse(403, 'accountStatusLacking')
|
||||
|
||||
/*
|
||||
* Attempt to read record from database
|
||||
|
@ -176,12 +167,18 @@ ApikeyModel.prototype.userApikeys = async function (uid) {
|
|||
/*
|
||||
* Keys are an array, remove sercrets with map() and decrypt prior to returning
|
||||
*/
|
||||
return keys.map((key) => {
|
||||
delete key.secret
|
||||
key.name = this.decrypt(key.name)
|
||||
return keys.map((key) => this.asKeyData(key))
|
||||
}
|
||||
|
||||
return key
|
||||
})
|
||||
/*
|
||||
* Takes non-instatiated key data and prepares it so it can be returned
|
||||
*/
|
||||
ApikeyModel.prototype.asKeyData = async function (key) {
|
||||
delete key.secret
|
||||
delete key.aud
|
||||
key.name = this.decrypt(key.name)
|
||||
|
||||
return key
|
||||
}
|
||||
|
||||
/*
|
||||
|
|
203
sites/backend/src/models/bookmark.mjs
Normal file
203
sites/backend/src/models/bookmark.mjs
Normal file
|
@ -0,0 +1,203 @@
|
|||
import { log } from '../utils/log.mjs'
|
||||
import { decorateModel } from '../utils/model-decorator.mjs'
|
||||
|
||||
/*
|
||||
* This model handles all bookmarks
|
||||
*/
|
||||
export function BookmarkModel(tools) {
|
||||
return decorateModel(this, tools, { name: 'bookmark' })
|
||||
}
|
||||
|
||||
/*
|
||||
* Creates a bookmark - Uses user input so we need to validate it
|
||||
*
|
||||
* @params {body} object - The request body
|
||||
* @params {user} object - The user as provided by the auth middleware
|
||||
* @returns {BookmarkModel} object - The BookmarkModel
|
||||
*/
|
||||
BookmarkModel.prototype.guardedCreate = async function ({ body, user }) {
|
||||
/*
|
||||
* Enforce RBAC
|
||||
*/
|
||||
if (!this.rbac.writeSome(user)) return this.BookmarkResponse(403, 'insufficientAccessLevel')
|
||||
|
||||
/*
|
||||
* Do we have a POST body?
|
||||
*/
|
||||
if (Object.keys(body).length < 1) return this.BookmarkResponse(400, 'postBodyMissing')
|
||||
|
||||
/*
|
||||
* Is title and url set?
|
||||
*/
|
||||
for (const field of ['title', 'url']) {
|
||||
if (!body[field] || typeof body[field] !== 'string' || body[field].length < 1)
|
||||
return this.setResponse(400, `${field}Missing`)
|
||||
}
|
||||
|
||||
/*
|
||||
* Is type set and valid?
|
||||
*/
|
||||
if (!body.type || !this.config.bookmarks.types.includes(body.type))
|
||||
return this.setResponse(400, body.type ? 'typeInvalid' : 'typeMissing')
|
||||
|
||||
/*
|
||||
* Create the initial record
|
||||
*/
|
||||
await this.createRecord({
|
||||
type: body.type,
|
||||
title: body.title,
|
||||
url: body.url,
|
||||
userId: user.uid,
|
||||
})
|
||||
|
||||
//await this.read({ id: this.record.id })
|
||||
|
||||
/*
|
||||
* Now return 201 and the data
|
||||
*/
|
||||
return this.setResponse201({ bookmark: this.asBookmark() })
|
||||
}
|
||||
|
||||
/*
|
||||
* Loads a bookmark from the database based on the where clause you pass it
|
||||
* In addition prepares it for returning the bookmark data
|
||||
*
|
||||
* @params {params} object - The request (URL) parameters
|
||||
* @params {user} object - The user as provided by the auth middleware
|
||||
* @returns {BookmarkModel} object - The BoolkmarkModel
|
||||
*/
|
||||
BookmarkModel.prototype.guardedRead = async function ({ params, user }) {
|
||||
/*
|
||||
* Enforce RBAC
|
||||
*/
|
||||
if (!this.rbac.readSome(user)) return this.setResponse(403, 'insufficientAccessLevel')
|
||||
|
||||
/*
|
||||
* Attempt to read the record from the database
|
||||
*/
|
||||
await this.read({ id: parseInt(params.id) })
|
||||
|
||||
/*
|
||||
* If it does not exist, send a 404
|
||||
*/
|
||||
if (!this.record) return this.setResponse(404)
|
||||
|
||||
/*
|
||||
* You cannot read other people's bookmarks
|
||||
*/
|
||||
if (this.record.userId !== user.uid) return this.setResponse(403, 'insufficientAccessLevel')
|
||||
|
||||
/*
|
||||
* Return 200 and send the bookmark data
|
||||
*/
|
||||
return this.setResponse(200, false, {
|
||||
result: 'success',
|
||||
bookmark: this.asBookmark(),
|
||||
})
|
||||
}
|
||||
|
||||
/*
|
||||
* Updates the bookmark data - Used when we pass through user-provided data
|
||||
* so we can't be certain it's safe
|
||||
*
|
||||
* @params {params} object - The request (URL) parameters
|
||||
* @params {body} object - The request body
|
||||
* @returns {BookmarkModel} object - The BookmarkModel
|
||||
*/
|
||||
BookmarkModel.prototype.guardedUpdate = async function ({ params, body, user }) {
|
||||
/*
|
||||
* Enforce RBAC
|
||||
*/
|
||||
if (!this.rbac.writeSome(user)) return this.setResponse(403, 'insufficientAccessLevel')
|
||||
|
||||
/*
|
||||
* Attempt to read record from database
|
||||
*/
|
||||
await this.read({ id: parseInt(params.id) })
|
||||
|
||||
/*
|
||||
* You cannot update other user's bookmarks
|
||||
*/
|
||||
if (this.record.userId !== user.uid) return this.setResponse(403, 'insufficientAccessLevel')
|
||||
|
||||
/*
|
||||
* Prepare data to update the record
|
||||
*/
|
||||
const data = {}
|
||||
|
||||
/*
|
||||
* title & url are all you can update (you cannot update the type)
|
||||
*/
|
||||
for (const field of ['title', 'url']) {
|
||||
if (body[field] && typeof body[field] === 'string' && body[field].length > 0)
|
||||
data[field] = body[field]
|
||||
}
|
||||
|
||||
/*
|
||||
* Now update the database record
|
||||
*/
|
||||
await this.update(data)
|
||||
|
||||
/*
|
||||
* Return 200 and the record data
|
||||
*/
|
||||
return this.setResponse200({ bookmark: this.asBookmark() })
|
||||
}
|
||||
|
||||
/*
|
||||
* Removes the bookmark
|
||||
*
|
||||
* @params {params} object - The request (URL) parameters
|
||||
* @params {user} object - The user as provided by the auth middleware
|
||||
* @returns {BookmarkModel} object - The BookmarkModel
|
||||
*/
|
||||
BookmarkModel.prototype.guardedDelete = async function ({ params, user }) {
|
||||
/*
|
||||
* Enforce RBAC
|
||||
*/
|
||||
if (!this.rbac.writeSome(user)) return this.setResponse(403, 'insufficientAccessLevel')
|
||||
|
||||
/*
|
||||
* Attempt to read the record from the database
|
||||
*/
|
||||
await this.read({ id: parseInt(params.id) })
|
||||
|
||||
/*
|
||||
* You cannot remove other user's data
|
||||
*/
|
||||
if (this.record.userId !== user.uid) return this.setResponse(403, 'insufficientAccessLevel')
|
||||
|
||||
/*
|
||||
* Delete the record
|
||||
*/
|
||||
await this.delete()
|
||||
|
||||
/*
|
||||
* Return 204
|
||||
*/
|
||||
return this.setResponse(204, false)
|
||||
}
|
||||
|
||||
/*
|
||||
* Returns a list of bookmarks for the user making the API call
|
||||
*/
|
||||
BookmarkModel.prototype.userBookmarks = async function (uid) {
|
||||
if (!uid) return false
|
||||
let bookmarks
|
||||
try {
|
||||
bookmarks = await this.prisma.bookmark.findMany({ where: { userId: uid } })
|
||||
} catch (err) {
|
||||
log.warn(`Failed to search bookmarks for user ${uid}: ${err}`)
|
||||
}
|
||||
const list = []
|
||||
for (const bookmark of bookmarks) list.push(bookmark)
|
||||
|
||||
return list
|
||||
}
|
||||
|
||||
/*
|
||||
* Returns record data
|
||||
*/
|
||||
BookmarkModel.prototype.asBookmark = function () {
|
||||
return { ...this.record }
|
||||
}
|
|
@ -172,11 +172,6 @@ CuratedSetModel.prototype.guardedClone = async function ({ params, user, body },
|
|||
*/
|
||||
if (!this.rbac.writeSome(user)) return this.setResponse(403, 'insufficientAccessLevel')
|
||||
|
||||
/*
|
||||
* Verify JWT
|
||||
*/
|
||||
if (user.iss && user.status < 1) return this.setResponse(403, 'accountStatusLacking')
|
||||
|
||||
/*
|
||||
* Is language set?
|
||||
*/
|
||||
|
@ -222,11 +217,6 @@ CuratedSetModel.prototype.guardedUpdate = async function ({ params, body, user }
|
|||
*/
|
||||
if (!this.rbac.curator(user)) return this.setResponse(403, 'insufficientAccessLevel')
|
||||
|
||||
/*
|
||||
* Verify JWT token for user status
|
||||
*/
|
||||
if (user.iss && user.status < 1) return this.setResponse(403, 'accountStatusLacking')
|
||||
|
||||
/*
|
||||
* Attempt to read database record
|
||||
*/
|
||||
|
@ -297,11 +287,6 @@ CuratedSetModel.prototype.guardedDelete = async function ({ params, user }) {
|
|||
*/
|
||||
if (!this.rbac.curator(user)) return this.setResponse(403, 'insufficientAccessLevel')
|
||||
|
||||
/*
|
||||
* Make sure the account is ok by checking the JWT
|
||||
*/
|
||||
if (user.iss && user.status < 1) return this.setResponse(403, 'accountStatusLacking')
|
||||
|
||||
/*
|
||||
* Find the database record
|
||||
*/
|
||||
|
|
|
@ -173,11 +173,6 @@ PatternModel.prototype.guardedRead = async function ({ params, user }) {
|
|||
*/
|
||||
if (!this.rbac.readSome(user)) return this.setResponse(403, 'insufficientAccessLevel')
|
||||
|
||||
/*
|
||||
* Check JWT for status
|
||||
*/
|
||||
if (user.iss && user.status < 1) return this.setResponse(403, 'accountStatusLacking')
|
||||
|
||||
/*
|
||||
* Is the id set?
|
||||
*/
|
||||
|
@ -221,11 +216,6 @@ PatternModel.prototype.guardedClone = async function ({ params, user }) {
|
|||
*/
|
||||
if (!this.rbac.writeSome(user)) return this.setResponse(403, 'insufficientAccessLevel')
|
||||
|
||||
/*
|
||||
* Check JWT
|
||||
*/
|
||||
if (user.iss && user.status < 1) return this.setResponse(403, 'accountStatusLacking')
|
||||
|
||||
/*
|
||||
* Attempt to read record from database
|
||||
*/
|
||||
|
@ -277,11 +267,6 @@ PatternModel.prototype.guardedUpdate = async function ({ params, body, user }) {
|
|||
*/
|
||||
if (!this.rbac.writeSome(user)) return this.setResponse(403, 'insufficientAccessLevel')
|
||||
|
||||
/*
|
||||
* Check JWT
|
||||
*/
|
||||
if (user.iss && user.status < 1) return this.setResponse(403, 'accountStatusLacking')
|
||||
|
||||
/*
|
||||
* Attempt to read record from the database
|
||||
*/
|
||||
|
@ -356,11 +341,6 @@ PatternModel.prototype.guardedDelete = async function ({ params, user }) {
|
|||
*/
|
||||
if (!this.rbac.writeSome(user)) return this.setResponse(403, 'insufficientAccessLevel')
|
||||
|
||||
/*
|
||||
* Check JWT
|
||||
*/
|
||||
if (user.iss && user.status < 1) return this.setResponse(403, 'accountStatusLacking')
|
||||
|
||||
/*
|
||||
* Attempt to read record from database
|
||||
*/
|
||||
|
|
|
@ -3,7 +3,7 @@ import { replaceImage, storeImage, importImage } from '../utils/cloudflare-image
|
|||
import { decorateModel } from '../utils/model-decorator.mjs'
|
||||
|
||||
/*
|
||||
* This model handles all flows (typically that involves sending out emails)
|
||||
* This model handles all set updates (typically that involves sending out emails)
|
||||
*/
|
||||
export function SetModel(tools) {
|
||||
return decorateModel(this, tools, {
|
||||
|
@ -94,11 +94,6 @@ SetModel.prototype.guardedRead = async function ({ params, user }) {
|
|||
*/
|
||||
if (!this.rbac.readSome(user)) return this.setResponse(403, 'insufficientAccessLevel')
|
||||
|
||||
/*
|
||||
* Check JWT
|
||||
*/
|
||||
if (user.iss && user.status < 1) return this.setResponse(403, 'accountStatusLacking')
|
||||
|
||||
/*
|
||||
* Attempt to read the record from the database
|
||||
*/
|
||||
|
@ -164,11 +159,6 @@ SetModel.prototype.guardedClone = async function ({ params, user }) {
|
|||
*/
|
||||
if (!this.rbac.writeSome(user)) return this.setResponse(403, 'insufficientAccessLevel')
|
||||
|
||||
/*
|
||||
* Check the JWT
|
||||
*/
|
||||
if (user.iss && user.status < 1) return this.setResponse(403, 'accountStatusLacking')
|
||||
|
||||
/*
|
||||
* Attempt to read the record from the database
|
||||
*/
|
||||
|
@ -234,11 +224,6 @@ SetModel.prototype.guardedUpdate = async function ({ params, body, user }) {
|
|||
*/
|
||||
if (!this.rbac.writeSome(user)) return this.setResponse(403, 'insufficientAccessLevel')
|
||||
|
||||
/*
|
||||
* Check JWT
|
||||
*/
|
||||
if (user.iss && user.status < 1) return this.setResponse(403, 'accountStatusLacking')
|
||||
|
||||
/*
|
||||
* Attempt to read record from database
|
||||
*/
|
||||
|
@ -326,11 +311,6 @@ SetModel.prototype.guardedDelete = async function ({ params, user }) {
|
|||
*/
|
||||
if (!this.rbac.writeSome(user)) return this.setResponse(403, 'insufficientAccessLevel')
|
||||
|
||||
/*
|
||||
* Check the JWT
|
||||
*/
|
||||
if (user.iss && user.status < 1) return this.setResponse(403, 'accountStatusLacking')
|
||||
|
||||
/*
|
||||
* Attempt to read the record from the database
|
||||
*/
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
import jwt from 'jsonwebtoken'
|
||||
import { log } from '../utils/log.mjs'
|
||||
import { hash, hashPassword, randomString, verifyPassword } from '../utils/crypto.mjs'
|
||||
import { replaceImage, importImage } from '../utils/cloudflare-images.mjs'
|
||||
import { clean, asJson, i18nUrl } from '../utils/index.mjs'
|
||||
import { replaceImage, importImage, removeImage } from '../utils/cloudflare-images.mjs'
|
||||
import { clean, asJson, i18nUrl, writeExportedData } from '../utils/index.mjs'
|
||||
import { decorateModel } from '../utils/model-decorator.mjs'
|
||||
|
||||
/*
|
||||
|
@ -51,6 +51,135 @@ UserModel.prototype.profile = async function ({ params }) {
|
|||
})
|
||||
}
|
||||
|
||||
/*
|
||||
* Loads a user from the database based on the where clause you pass it
|
||||
* In addition prepares it for returning all account data
|
||||
* This is guarded so it enforces access control and validates input
|
||||
*
|
||||
* @param {params} object - The request (URL) parameters
|
||||
* @returns {UserModel} object - The UserModel
|
||||
*/
|
||||
UserModel.prototype.allData = async function ({ params }) {
|
||||
/*
|
||||
* Is id set?
|
||||
*/
|
||||
if (typeof params.id === 'undefined') return this.setResponse(403, 'idMissing')
|
||||
|
||||
/*
|
||||
* Try to find the record in the database
|
||||
* Note that find checks lusername, ehash, and id but we
|
||||
* pass it in the username value as that's what the login
|
||||
* rout does
|
||||
*/
|
||||
await this.read(
|
||||
{ id: Number(params.id) },
|
||||
{ apikeys: true, bookmarks: true, patterns: true, sets: true }
|
||||
)
|
||||
|
||||
/*
|
||||
* If it does not exist, return 404
|
||||
*/
|
||||
if (!this.exists) return this.setResponse(404)
|
||||
|
||||
return this.setResponse200({
|
||||
result: 'success',
|
||||
data: this.asData(),
|
||||
})
|
||||
}
|
||||
|
||||
/*
|
||||
* Exports all account data
|
||||
*
|
||||
* @param {user} object - The user as loaded by the authentication middleware
|
||||
* @returns {UserModel} object - The UserModel
|
||||
*/
|
||||
UserModel.prototype.exportAccount = async function ({ user }) {
|
||||
/*
|
||||
* Read the record from the database
|
||||
*/
|
||||
await this.read({ id: user.uid }, { apikeys: true, bookmarks: true, patterns: true, sets: true })
|
||||
|
||||
/*
|
||||
* If it does not exist, return 404
|
||||
*/
|
||||
if (!this.exists) return this.setResponse(404)
|
||||
|
||||
return this.setResponse200({
|
||||
result: 'success',
|
||||
data: writeExportedData(this.asExport()),
|
||||
})
|
||||
}
|
||||
|
||||
/*
|
||||
* Restricts processing of account data
|
||||
*
|
||||
* @param {user} object - The user as loaded by the authentication middleware
|
||||
* @returns {UserModel} object - The UserModel
|
||||
*/
|
||||
UserModel.prototype.restrictAccount = async function ({ user }) {
|
||||
/*
|
||||
* Read the record from the database
|
||||
*/
|
||||
await this.read({ id: user.uid }, { apikeys: true, bookmarks: true, patterns: true, sets: true })
|
||||
|
||||
/*
|
||||
* If it does not exist, return 404
|
||||
*/
|
||||
if (!this.exists) return this.setResponse(404)
|
||||
|
||||
/*
|
||||
* Update status to block the account
|
||||
*/
|
||||
await this.update({ status: -1 })
|
||||
|
||||
return this.setResponse200({
|
||||
result: 'success',
|
||||
data: {},
|
||||
})
|
||||
}
|
||||
|
||||
/*
|
||||
* Remove account
|
||||
*
|
||||
* @param {user} object - The user as loaded by the authentication middleware
|
||||
* @returns {UserModel} object - The UserModel
|
||||
*/
|
||||
UserModel.prototype.removeAccount = async function ({ user }) {
|
||||
/*
|
||||
* Read the record from the database
|
||||
*/
|
||||
await this.read({ id: user.uid }, { apikeys: true, bookmarks: true, patterns: true, sets: true })
|
||||
|
||||
/*
|
||||
* If it does not exist, return 404
|
||||
*/
|
||||
if (!this.exists) return this.setResponse(404)
|
||||
|
||||
/*
|
||||
* Remove user image
|
||||
*/
|
||||
await removeImage(`user-${this.record.ihash}`)
|
||||
|
||||
/*
|
||||
* Remove account
|
||||
*/
|
||||
try {
|
||||
await this.prisma.pattern.deleteMany({ where: { userId: user.uid } })
|
||||
await this.prisma.set.deleteMany({ where: { userId: user.uid } })
|
||||
await this.prisma.bookmark.deleteMany({ where: { userId: user.uid } })
|
||||
await this.prisma.apikey.deleteMany({ where: { userId: user.uid } })
|
||||
await this.prisma.confirmation.deleteMany({ where: { userId: user.uid } })
|
||||
await this.delete()
|
||||
} catch (err) {
|
||||
log.warn(err, 'Error while removing account')
|
||||
}
|
||||
|
||||
return this.setResponse200({
|
||||
result: 'success',
|
||||
data: {},
|
||||
})
|
||||
}
|
||||
|
||||
/*
|
||||
* Loads a user from the database based on the where clause you pass it
|
||||
* In addition prepares it for returning the account data
|
||||
|
@ -65,11 +194,6 @@ UserModel.prototype.guardedRead = async function (where, { user }) {
|
|||
*/
|
||||
if (!this.rbac.readSome(user)) return this.setResponse(403, 'insufficientAccessLevel')
|
||||
|
||||
/*
|
||||
* Ensure the account is active
|
||||
*/
|
||||
if (user.iss && user.status < 1) return this.setResponse(403, 'accountStatusLacking')
|
||||
|
||||
/*
|
||||
* Read record from database
|
||||
*/
|
||||
|
@ -812,11 +936,6 @@ UserModel.prototype.guardedUpdate = async function ({ body, user }) {
|
|||
*/
|
||||
if (!this.rbac.writeSome(user)) return this.setResponse(403, 'insufficientAccessLevel')
|
||||
|
||||
/*
|
||||
* Make sure the account is in a state where it's allowed to do this
|
||||
*/
|
||||
if (user.iss && user.status < 1) return this.setResponse(403, 'accountStatusLacking')
|
||||
|
||||
/*
|
||||
* Create data to update the record
|
||||
*/
|
||||
|
@ -841,7 +960,11 @@ UserModel.prototype.guardedUpdate = async function ({ body, user }) {
|
|||
*/
|
||||
for (const field of this.jsonFields) {
|
||||
if (typeof body[field] !== 'undefined') {
|
||||
if (typeof body[field] === 'object') data[field] = body[field]
|
||||
if (typeof body[field] === 'object')
|
||||
data[field] = {
|
||||
...this.clear[field],
|
||||
...body[field],
|
||||
}
|
||||
else log.warn(body, `Tried to set JDON field ${field} to a non-object`)
|
||||
}
|
||||
}
|
||||
|
@ -867,15 +990,21 @@ UserModel.prototype.guardedUpdate = async function ({ body, user }) {
|
|||
/*
|
||||
* Image (img)
|
||||
*/
|
||||
if (typeof body.img === 'string')
|
||||
data.img = await replaceImage({
|
||||
if (typeof body.img === 'string') {
|
||||
const imgData = {
|
||||
id: `user-${this.record.ihash}`,
|
||||
metadata: {
|
||||
user: user.uid,
|
||||
ihash: this.record.ihash,
|
||||
},
|
||||
b64: body.img,
|
||||
})
|
||||
}
|
||||
/*
|
||||
* Allow both a base64 encoded binary image or an URL
|
||||
*/
|
||||
if (body.img.slice(0, 4) === 'http') imgData.url = body.img
|
||||
else imgData.b64 = body.img
|
||||
data.img = await replaceImage(imgData)
|
||||
}
|
||||
|
||||
/*
|
||||
* Now update the database record
|
||||
|
@ -1020,11 +1149,6 @@ UserModel.prototype.guardedMfaUpdate = async function ({ body, user, ip }) {
|
|||
*/
|
||||
if (!this.rbac.user(user)) return this.setResponse(403, 'insufficientAccessLevel')
|
||||
|
||||
/*
|
||||
* Ensure account is in the proper state to do this
|
||||
*/
|
||||
if (user.iss && user.status < 1) return this.setResponse(403, 'accountStatusLacking')
|
||||
|
||||
/*
|
||||
* If MFA is active and it is an attempt to active it, return 400
|
||||
*/
|
||||
|
@ -1176,6 +1300,7 @@ UserModel.prototype.asProfile = function () {
|
|||
id: this.record.id,
|
||||
bio: this.clear.bio,
|
||||
img: this.clear.img,
|
||||
ihash: this.record.ihash,
|
||||
patron: this.record.patron,
|
||||
role: this.record.role,
|
||||
username: this.record.username,
|
||||
|
@ -1200,7 +1325,7 @@ UserModel.prototype.asAccount = function () {
|
|||
createdAt: this.record.createdAt,
|
||||
email: this.clear.email,
|
||||
data: this.clear.data,
|
||||
ihash: this.ihash,
|
||||
ihash: this.record.ihash,
|
||||
img: this.clear.img,
|
||||
imperial: this.record.imperial,
|
||||
initial: this.clear.initial,
|
||||
|
@ -1223,6 +1348,46 @@ UserModel.prototype.asAccount = function () {
|
|||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Returns all user data (that is not included in the account data)
|
||||
*
|
||||
* @return {account} object - The account data as a plain object
|
||||
*/
|
||||
UserModel.prototype.asData = function () {
|
||||
/*
|
||||
* Nothing to do here but construct the object to return
|
||||
*/
|
||||
return {
|
||||
apikeys: this.record.apikeys
|
||||
? this.record.apikeys.map((key) => {
|
||||
delete key.secret
|
||||
delete key.aud
|
||||
key.name = this.decrypt(key.name)
|
||||
|
||||
return key
|
||||
})
|
||||
: [],
|
||||
bookmarks: this.record.bookmarks || [],
|
||||
patterns: this.record.patterns || [],
|
||||
sets: this.record.sets || [],
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Returns all user data to be exported
|
||||
*
|
||||
* @return {account} object - The account data as a plain object
|
||||
*/
|
||||
UserModel.prototype.asExport = function () {
|
||||
/*
|
||||
* Get both account data and all data
|
||||
*/
|
||||
return {
|
||||
...this.asAccount(),
|
||||
...this.asData(),
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Returns a list of records as search results
|
||||
* Typically used by admin search
|
||||
|
|
49
sites/backend/src/routes/bookmarks.mjs
Normal file
49
sites/backend/src/routes/bookmarks.mjs
Normal file
|
@ -0,0 +1,49 @@
|
|||
import { BookmarksController } from '../controllers/bookmarks.mjs'
|
||||
|
||||
const Bookmarks = new BookmarksController()
|
||||
const jwt = ['jwt', { session: false }]
|
||||
const bsc = ['basic', { session: false }]
|
||||
|
||||
export function bookmarksRoutes(tools) {
|
||||
const { app, passport } = tools
|
||||
|
||||
// Create a bookmark
|
||||
app.post('/bookmarks/jwt', passport.authenticate(...jwt), (req, res) =>
|
||||
Bookmarks.create(req, res, tools)
|
||||
)
|
||||
app.post('/bookmarks/key', passport.authenticate(...bsc), (req, res) =>
|
||||
Bookmarks.create(req, res, tools)
|
||||
)
|
||||
|
||||
// Read a bookmark
|
||||
app.get('/bookmarks/:id/jwt', passport.authenticate(...jwt), (req, res) =>
|
||||
Bookmarks.read(req, res, tools)
|
||||
)
|
||||
app.get('/bookmarks/:id/key', passport.authenticate(...bsc), (req, res) =>
|
||||
Bookmarks.read(req, res, tools)
|
||||
)
|
||||
|
||||
// Get a list of bookmarks for the user
|
||||
app.get('/bookmarks/jwt', passport.authenticate(...jwt), (req, res) =>
|
||||
Bookmarks.list(req, res, tools)
|
||||
)
|
||||
app.get('/bookmarks/key', passport.authenticate(...bsc), (req, res) =>
|
||||
Bookmarks.list(req, res, tools)
|
||||
)
|
||||
|
||||
// Update a bookmark
|
||||
app.patch('/bookmarks/:id/jwt', passport.authenticate(...jwt), (req, res) =>
|
||||
Bookmarks.update(req, res, tools)
|
||||
)
|
||||
app.patch('/bookmarks/:id/key', passport.authenticate(...bsc), (req, res) =>
|
||||
Bookmarks.update(req, res, tools)
|
||||
)
|
||||
|
||||
// Delete a bookmark
|
||||
app.delete('/bookmarks/:id/jwt', passport.authenticate(...jwt), (req, res) =>
|
||||
Bookmarks.delete(req, res, tools)
|
||||
)
|
||||
app.delete('/bookmarks/:id/key', passport.authenticate(...bsc), (req, res) =>
|
||||
Bookmarks.delete(req, res, tools)
|
||||
)
|
||||
}
|
|
@ -1,6 +1,7 @@
|
|||
import { apikeysRoutes } from './apikeys.mjs'
|
||||
import { usersRoutes } from './users.mjs'
|
||||
import { setsRoutes } from './sets.mjs'
|
||||
import { bookmarksRoutes } from './bookmarks.mjs'
|
||||
import { patternsRoutes } from './patterns.mjs'
|
||||
import { confirmationsRoutes } from './confirmations.mjs'
|
||||
import { curatedSetsRoutes } from './curated-sets.mjs'
|
||||
|
@ -13,6 +14,7 @@ export const routes = {
|
|||
apikeysRoutes,
|
||||
usersRoutes,
|
||||
setsRoutes,
|
||||
bookmarksRoutes,
|
||||
patternsRoutes,
|
||||
confirmationsRoutes,
|
||||
curatedSetsRoutes,
|
||||
|
|
|
@ -55,9 +55,41 @@ export function usersRoutes(tools) {
|
|||
Users.isUsernameAvailable(req, res, tools)
|
||||
)
|
||||
|
||||
// Load full user data
|
||||
app.get('/users/:id/jwt', passport.authenticate(...jwt), (req, res) =>
|
||||
Users.allData(req, res, tools)
|
||||
)
|
||||
app.get('/users/:id/key', passport.authenticate(...bsc), (req, res) =>
|
||||
Users.allData(req, res, tools)
|
||||
)
|
||||
|
||||
// Load a user profile
|
||||
app.get('/users/:id', (req, res) => Users.profile(req, res, tools))
|
||||
|
||||
// Export account data
|
||||
app.get('/account/export/jwt', passport.authenticate(...jwt), (req, res) =>
|
||||
Users.exportAccount(req, res, tools)
|
||||
)
|
||||
app.get('/account/export/key', passport.authenticate(...bsc), (req, res) =>
|
||||
Users.exportAccount(req, res, tools)
|
||||
)
|
||||
|
||||
// Restrict processing of account data
|
||||
app.get('/account/restrict/jwt', passport.authenticate(...jwt), (req, res) =>
|
||||
Users.restrictAccount(req, res, tools)
|
||||
)
|
||||
app.get('/account/restrict/key', passport.authenticate(...bsc), (req, res) =>
|
||||
Users.restrictAccount(req, res, tools)
|
||||
)
|
||||
|
||||
// Remove account
|
||||
app.delete('/account/jwt', passport.authenticate(...jwt), (req, res) =>
|
||||
Users.removeAccount(req, res, tools)
|
||||
)
|
||||
app.delete('/account/key', passport.authenticate(...bsc), (req, res) =>
|
||||
Users.removeAccount(req, res, tools)
|
||||
)
|
||||
|
||||
/*
|
||||
|
||||
// Remove account
|
||||
|
|
|
@ -48,7 +48,7 @@ export async function replaceImage(props, isTest = false) {
|
|||
const form = getFormData(props)
|
||||
// Ignore errors on delete, probably means the image does not exist
|
||||
try {
|
||||
await axios.delete(`${config.api}/${props.id}`)
|
||||
await axios.delete(`${config.api}/${props.id}`, { headers })
|
||||
} catch (err) {
|
||||
// It's fine
|
||||
log.info(`Could not delete image ${props.id}`)
|
||||
|
@ -58,10 +58,9 @@ export async function replaceImage(props, isTest = false) {
|
|||
result = await axios.post(config.api, form, { headers })
|
||||
} catch (err) {
|
||||
console.log('Failed to replace image on cloudflare', err)
|
||||
console.log(err.response.data)
|
||||
}
|
||||
|
||||
return result.data?.result?.id ? result.data.result.id : false
|
||||
return result?.data?.result?.id ? result.data.result.id : false
|
||||
}
|
||||
|
||||
/*
|
||||
|
|
|
@ -1,4 +1,7 @@
|
|||
import { website } from '../config.mjs'
|
||||
import { log } from './log.mjs'
|
||||
import { website, exports } from '../config.mjs'
|
||||
import { randomString } from './crypto.mjs'
|
||||
import fs from 'fs'
|
||||
|
||||
/*
|
||||
* Capitalizes a string
|
||||
|
@ -30,3 +33,18 @@ export const i18nUrl = (lang, path) => {
|
|||
|
||||
return url + path
|
||||
}
|
||||
|
||||
/*
|
||||
* Writes a pojo to disk as JSON under a random name
|
||||
* It is used to export account data
|
||||
*/
|
||||
export const writeExportedData = (data) => {
|
||||
const name = randomString()
|
||||
try {
|
||||
fs.writeFileSync(`${exports.dir}${name}.json`, JSON.stringify(data, null, 2))
|
||||
} catch (err) {
|
||||
log.warn(err, 'Failed to write export file')
|
||||
}
|
||||
|
||||
return exports.url + name + '.json'
|
||||
}
|
||||
|
|
180
sites/backend/tests/bookmark.mjs
Normal file
180
sites/backend/tests/bookmark.mjs
Normal file
|
@ -0,0 +1,180 @@
|
|||
export const bookmarkTests = async (chai, config, expect, store) => {
|
||||
const data = {
|
||||
jwt: {
|
||||
type: 'doc',
|
||||
title: 'This is the title',
|
||||
url: '/docs/foo/bar',
|
||||
},
|
||||
key: {
|
||||
type: 'set',
|
||||
title: 'This is the set',
|
||||
url: '/sets/12',
|
||||
},
|
||||
}
|
||||
store.bookmark = {
|
||||
jwt: {},
|
||||
key: {},
|
||||
}
|
||||
store.altbookmark = {
|
||||
jwt: {},
|
||||
key: {},
|
||||
}
|
||||
|
||||
for (const auth of ['jwt', 'key']) {
|
||||
describe(`${store.icon('bookmark', auth)} Bookmark tests (${auth})`, () => {
|
||||
it(`${store.icon('bookmark', auth)} Should create a new bookmark (${auth})`, (done) => {
|
||||
chai
|
||||
.request(config.api)
|
||||
.post(`/bookmarks/${auth}`)
|
||||
.set(
|
||||
'Authorization',
|
||||
auth === 'jwt'
|
||||
? 'Bearer ' + store.account.token
|
||||
: 'Basic ' +
|
||||
new Buffer(`${store.account.apikey.key}:${store.account.apikey.secret}`).toString(
|
||||
'base64'
|
||||
)
|
||||
)
|
||||
.send(data[auth])
|
||||
.end((err, res) => {
|
||||
expect(err === null).to.equal(true)
|
||||
expect(res.status).to.equal(201)
|
||||
expect(res.body.result).to.equal(`created`)
|
||||
for (const [key, val] of Object.entries(data[auth])) {
|
||||
expect(res.body.bookmark[key]).to.equal(val)
|
||||
}
|
||||
store.bookmark[auth] = res.body.bookmark
|
||||
done()
|
||||
})
|
||||
}).timeout(5000)
|
||||
|
||||
for (const field of ['title', 'url']) {
|
||||
it(`${store.icon('bookmark', auth)} Should update the ${field} field (${auth})`, (done) => {
|
||||
const data = {}
|
||||
const val = store.bookmark[auth][field] + '_updated'
|
||||
data[field] = val
|
||||
chai
|
||||
.request(config.api)
|
||||
.patch(`/bookmarks/${store.bookmark[auth].id}/${auth}`)
|
||||
.set(
|
||||
'Authorization',
|
||||
auth === 'jwt'
|
||||
? 'Bearer ' + store.account.token
|
||||
: 'Basic ' +
|
||||
new Buffer(
|
||||
`${store.account.apikey.key}:${store.account.apikey.secret}`
|
||||
).toString('base64')
|
||||
)
|
||||
.send(data)
|
||||
.end((err, res) => {
|
||||
expect(err === null).to.equal(true)
|
||||
expect(res.status).to.equal(200)
|
||||
expect(res.body.result).to.equal(`success`)
|
||||
expect(res.body.bookmark[field]).to.equal(val)
|
||||
done()
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
it(`${store.icon('bookmark', auth)} Should read a bookmark (${auth})`, (done) => {
|
||||
chai
|
||||
.request(config.api)
|
||||
.get(`/bookmarks/${store.bookmark[auth].id}/${auth}`)
|
||||
.set(
|
||||
'Authorization',
|
||||
auth === 'jwt'
|
||||
? 'Bearer ' + store.account.token
|
||||
: 'Basic ' +
|
||||
new Buffer(`${store.account.apikey.key}:${store.account.apikey.secret}`).toString(
|
||||
'base64'
|
||||
)
|
||||
)
|
||||
.end((err, res) => {
|
||||
expect(err === null).to.equal(true)
|
||||
expect(res.status).to.equal(200)
|
||||
expect(res.body.result).to.equal(`success`)
|
||||
expect(typeof res.body.bookmark).to.equal('object')
|
||||
done()
|
||||
})
|
||||
})
|
||||
|
||||
it(`${store.icon(
|
||||
'bookmark',
|
||||
auth
|
||||
)} Should not allow reading another user's bookmark (${auth})`, (done) => {
|
||||
chai
|
||||
.request(config.api)
|
||||
.get(`/bookmarks/${store.bookmark[auth].id}/${auth}`)
|
||||
.set(
|
||||
'Authorization',
|
||||
auth === 'jwt'
|
||||
? 'Bearer ' + store.altaccount.token
|
||||
: 'Basic ' +
|
||||
new Buffer(
|
||||
`${store.altaccount.apikey.key}:${store.altaccount.apikey.secret}`
|
||||
).toString('base64')
|
||||
)
|
||||
.end((err, res) => {
|
||||
expect(err === null).to.equal(true)
|
||||
expect(res.status).to.equal(403)
|
||||
expect(res.body.result).to.equal(`error`)
|
||||
expect(res.body.error).to.equal(`insufficientAccessLevel`)
|
||||
done()
|
||||
})
|
||||
})
|
||||
|
||||
it(`${store.icon(
|
||||
'bookmark',
|
||||
auth
|
||||
)} Should not allow updating another user's bookmark (${auth})`, (done) => {
|
||||
chai
|
||||
.request(config.api)
|
||||
.patch(`/bookmarks/${store.bookmark[auth].id}/${auth}`)
|
||||
.set(
|
||||
'Authorization',
|
||||
auth === 'jwt'
|
||||
? 'Bearer ' + store.altaccount.token
|
||||
: 'Basic ' +
|
||||
new Buffer(
|
||||
`${store.altaccount.apikey.key}:${store.altaccount.apikey.secret}`
|
||||
).toString('base64')
|
||||
)
|
||||
.send({
|
||||
title: 'I have been taken over',
|
||||
})
|
||||
.end((err, res) => {
|
||||
expect(err === null).to.equal(true)
|
||||
expect(res.status).to.equal(403)
|
||||
expect(res.body.result).to.equal(`error`)
|
||||
expect(res.body.error).to.equal(`insufficientAccessLevel`)
|
||||
done()
|
||||
})
|
||||
})
|
||||
|
||||
it(`${store.icon(
|
||||
'bookmark',
|
||||
auth
|
||||
)} Should not allow removing another user's bookmark (${auth})`, (done) => {
|
||||
chai
|
||||
.request(config.api)
|
||||
.delete(`/bookmarks/${store.bookmark[auth].id}/${auth}`)
|
||||
.set(
|
||||
'Authorization',
|
||||
auth === 'jwt'
|
||||
? 'Bearer ' + store.altaccount.token
|
||||
: 'Basic ' +
|
||||
new Buffer(
|
||||
`${store.altaccount.apikey.key}:${store.altaccount.apikey.secret}`
|
||||
).toString('base64')
|
||||
)
|
||||
.end((err, res) => {
|
||||
expect(err === null).to.equal(true)
|
||||
expect(res.status).to.equal(403)
|
||||
expect(res.body.result).to.equal(`error`)
|
||||
expect(res.body.error).to.equal(`insufficientAccessLevel`)
|
||||
done()
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
|
@ -3,6 +3,7 @@ import { mfaTests } from './mfa.mjs'
|
|||
import { accountTests } from './account.mjs'
|
||||
import { apikeyTests } from './apikey.mjs'
|
||||
import { setTests } from './set.mjs'
|
||||
import { bookmarkTests } from './bookmark.mjs'
|
||||
import { curatedSetTests } from './curated-set.mjs'
|
||||
import { patternTests } from './pattern.mjs'
|
||||
import { subscriberTests } from './subscriber.mjs'
|
||||
|
@ -15,6 +16,7 @@ const runTests = async (...params) => {
|
|||
await apikeyTests(...params)
|
||||
await accountTests(...params)
|
||||
await setTests(...params)
|
||||
await bookmarkTests(...params)
|
||||
await curatedSetTests(...params)
|
||||
await patternTests(...params)
|
||||
await subscriberTests(...params)
|
||||
|
|
|
@ -44,6 +44,7 @@ export const setup = async () => {
|
|||
subscriber: '📬 ',
|
||||
flow: '🪁 ',
|
||||
issue: '🚩 ',
|
||||
bookmark: '🔖 ',
|
||||
},
|
||||
randomString,
|
||||
}
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
// Context
|
||||
import { LoadingContext } from 'shared/context/loading-context.mjs'
|
||||
// Hooks
|
||||
import { useAccount } from 'shared/hooks/use-account.mjs'
|
||||
import { useBackend } from 'shared/hooks/use-backend.mjs'
|
||||
import { useToast } from 'shared/hooks/use-toast.mjs'
|
||||
import { useState, useContext } from 'react'
|
||||
|
@ -48,8 +47,7 @@ export const SuggestLanguageForm = () => {
|
|||
const { startLoading, stopLoading } = useContext(LoadingContext)
|
||||
|
||||
// Hooks
|
||||
const { token } = useAccount()
|
||||
const backend = useBackend(token)
|
||||
const backend = useBackend()
|
||||
const toast = useToast()
|
||||
|
||||
// State
|
||||
|
|
|
@ -4,7 +4,6 @@ import translators from 'site/prebuild/translators.json'
|
|||
// Context
|
||||
import { LoadingContext } from 'shared/context/loading-context.mjs'
|
||||
// Hooks
|
||||
import { useAccount } from 'shared/hooks/use-account.mjs'
|
||||
import { useBackend } from 'shared/hooks/use-backend.mjs'
|
||||
import { useToast } from 'shared/hooks/use-toast.mjs'
|
||||
import { useState, useContext } from 'react'
|
||||
|
@ -28,8 +27,7 @@ export const TranslatorInvite = () => {
|
|||
|
||||
// Hooks
|
||||
const { t } = useTranslation(ns)
|
||||
const { token } = useAccount()
|
||||
const backend = useBackend(token)
|
||||
const backend = useBackend()
|
||||
const toast = useToast()
|
||||
|
||||
// State
|
||||
|
|
|
@ -135,8 +135,8 @@ export const EditCuratedSet = ({ id }) => {
|
|||
const { setModal } = useContext(ModalContext)
|
||||
|
||||
// Hooks
|
||||
const { account, token } = useAccount()
|
||||
const backend = useBackend(token)
|
||||
const { account } = useAccount()
|
||||
const backend = useBackend()
|
||||
const {
|
||||
t,
|
||||
//i18n
|
||||
|
|
|
@ -122,8 +122,8 @@ export const CurateSets = () => {
|
|||
const { startLoading, stopLoading } = useContext(LoadingContext)
|
||||
|
||||
// Hooks
|
||||
const { account, token } = useAccount()
|
||||
const backend = useBackend(token)
|
||||
const { account } = useAccount()
|
||||
const backend = useBackend()
|
||||
const { t, i18n } = useTranslation('sets', 'curate', 'toast', 'account')
|
||||
const { language } = i18n
|
||||
const toast = useToast()
|
||||
|
|
|
@ -60,7 +60,6 @@ export const SlugInput = ({ slug, setSlug, title, slugAvailable }) => {
|
|||
useEffect(() => {
|
||||
if (title !== slug) setSlug(slugify(title))
|
||||
}, [title])
|
||||
console.log(slugAvailable)
|
||||
|
||||
return (
|
||||
<input
|
||||
|
|
84
sites/org/pages/account/[platform].mjs
Normal file
84
sites/org/pages/account/[platform].mjs
Normal file
|
@ -0,0 +1,84 @@
|
|||
// Dependencies
|
||||
import dynamic from 'next/dynamic'
|
||||
import { serverSideTranslations } from 'next-i18next/serverSideTranslations'
|
||||
import { nsMerge, capitalize } from 'shared/utils.mjs'
|
||||
import { freeSewingConfig } from 'shared/config/freesewing.config.mjs'
|
||||
import { siteConfig } from 'site/site.config.mjs'
|
||||
// Components
|
||||
import { PageWrapper, ns as pageNs } from 'shared/components/wrappers/page.mjs'
|
||||
import { ns as authNs } from 'shared/components/wrappers/auth/index.mjs'
|
||||
import { ns as platformNs } from 'shared/components/account/platform.mjs'
|
||||
|
||||
// Translation namespaces used on this page
|
||||
const ns = nsMerge(platformNs, authNs, pageNs)
|
||||
|
||||
/*
|
||||
* Some things should never generated as SSR
|
||||
* So for these, we run a dynamic import and disable SSR rendering
|
||||
*/
|
||||
const DynamicAuthWrapper = dynamic(
|
||||
() => import('shared/components/wrappers/auth/index.mjs').then((mod) => mod.AuthWrapper),
|
||||
{ ssr: false }
|
||||
)
|
||||
|
||||
const DynamicPlatform = dynamic(
|
||||
() => import('shared/components/account/platform.mjs').then((mod) => mod.PlatformSettings),
|
||||
{ ssr: false }
|
||||
)
|
||||
|
||||
/*
|
||||
* Each page MUST be wrapped in the PageWrapper component.
|
||||
* You also MUST spread props.page into this wrapper component
|
||||
* when path and locale come from static props (as here)
|
||||
* or set them manually.
|
||||
*/
|
||||
const AccountPage = ({ page, platform }) => (
|
||||
<PageWrapper {...page} title={capitalize(platform)}>
|
||||
<DynamicAuthWrapper>
|
||||
<DynamicPlatform platform={platform} />
|
||||
</DynamicAuthWrapper>
|
||||
</PageWrapper>
|
||||
)
|
||||
|
||||
export default AccountPage
|
||||
|
||||
export async function getStaticProps({ locale, params }) {
|
||||
return {
|
||||
props: {
|
||||
...(await serverSideTranslations(locale, ns)),
|
||||
platform: params.platform,
|
||||
page: {
|
||||
locale,
|
||||
path: ['account', params.platform],
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* getStaticPaths() is used to specify for which routes (think URLs)
|
||||
* this page should be used to generate the result.
|
||||
*
|
||||
* On this page, it is returning a truncated list of routes (think URLs) for all
|
||||
* the mdx blog (markdown) content.
|
||||
* That list comes from prebuild/blog-paths.mjs, which is built in the prebuild step
|
||||
* and contains paths, titles, imageUrls, and intro for all blog posts.
|
||||
*
|
||||
* the fallback: 'blocking' property means that
|
||||
* any pages that haven't been pre-generated
|
||||
* will generate and cache the first time someone visits them
|
||||
*
|
||||
* To learn more, see: https://nextjs.org/docs/basic-features/data-fetching
|
||||
*/
|
||||
export const getStaticPaths = async () => {
|
||||
const paths = []
|
||||
for (const platform of Object.keys(freeSewingConfig.account.fields.identities).filter(
|
||||
(key) => key !== 'github'
|
||||
)) {
|
||||
for (const locale of siteConfig.languages) {
|
||||
paths.push({ params: { platform }, locale })
|
||||
}
|
||||
}
|
||||
|
||||
return { paths, fallback: false }
|
||||
}
|
|
@ -1,13 +1,16 @@
|
|||
// Dependencies
|
||||
import dynamic from 'next/dynamic'
|
||||
import { serverSideTranslations } from 'next-i18next/serverSideTranslations'
|
||||
import { nsMerge } from 'shared/utils.mjs'
|
||||
// Hooks
|
||||
import { useTranslation } from 'next-i18next'
|
||||
// Components
|
||||
import { PageWrapper, ns as pageNs } from 'shared/components/wrappers/page.mjs'
|
||||
import { ns as authNs } from 'shared/components/wrappers/auth/index.mjs'
|
||||
import { ns as bioNs } from 'shared/components/account/bio.mjs'
|
||||
|
||||
// Translation namespaces used on this page
|
||||
const namespaces = [...new Set([...bioNs, ...authNs, ...pageNs])]
|
||||
const ns = nsMerge(bioNs, authNs, pageNs)
|
||||
|
||||
/*
|
||||
* Some things should never generated as SSR
|
||||
|
@ -29,20 +32,24 @@ const DynamicBio = dynamic(
|
|||
* when path and locale come from static props (as here)
|
||||
* or set them manually.
|
||||
*/
|
||||
const AccountBioPage = ({ page }) => (
|
||||
<PageWrapper {...page}>
|
||||
<DynamicAuthWrapper>
|
||||
<DynamicBio title />
|
||||
</DynamicAuthWrapper>
|
||||
</PageWrapper>
|
||||
)
|
||||
const AccountBioPage = ({ page }) => {
|
||||
const { t } = useTranslation(ns)
|
||||
|
||||
return (
|
||||
<PageWrapper {...page} title={t('bio')}>
|
||||
<DynamicAuthWrapper>
|
||||
<DynamicBio title />
|
||||
</DynamicAuthWrapper>
|
||||
</PageWrapper>
|
||||
)
|
||||
}
|
||||
|
||||
export default AccountBioPage
|
||||
|
||||
export async function getStaticProps({ locale }) {
|
||||
return {
|
||||
props: {
|
||||
...(await serverSideTranslations(locale, namespaces)),
|
||||
...(await serverSideTranslations(locale, ns)),
|
||||
page: {
|
||||
locale,
|
||||
path: ['account', 'bio'],
|
||||
|
|
|
@ -1,13 +1,16 @@
|
|||
// Dependencies
|
||||
import dynamic from 'next/dynamic'
|
||||
import { serverSideTranslations } from 'next-i18next/serverSideTranslations'
|
||||
import { nsMerge } from 'shared/utils.mjs'
|
||||
// Hooks
|
||||
import { useTranslation } from 'next-i18next'
|
||||
// Components
|
||||
import { PageWrapper, ns as pageNs } from 'shared/components/wrappers/page.mjs'
|
||||
import { ns as authNs } from 'shared/components/wrappers/auth/index.mjs'
|
||||
import { ns as compareNs } from 'shared/components/account/compare.mjs'
|
||||
|
||||
// Translation namespaces used on this page
|
||||
const namespaces = [...new Set([...compareNs, ...authNs, ...pageNs])]
|
||||
const ns = nsMerge(compareNs, authNs, pageNs)
|
||||
|
||||
/*
|
||||
* Some things should never generated as SSR
|
||||
|
@ -29,20 +32,24 @@ const DynamicCompare = dynamic(
|
|||
* when path and locale come from static props (as here)
|
||||
* or set them manually.
|
||||
*/
|
||||
const AccountComparePage = ({ page }) => (
|
||||
<PageWrapper {...page}>
|
||||
<DynamicAuthWrapper>
|
||||
<DynamicCompare title />
|
||||
</DynamicAuthWrapper>
|
||||
</PageWrapper>
|
||||
)
|
||||
const AccountComparePage = ({ page }) => {
|
||||
const { t } = useTranslation(ns)
|
||||
|
||||
return (
|
||||
<PageWrapper {...page} title={t('compare')}>
|
||||
<DynamicAuthWrapper>
|
||||
<DynamicCompare title />
|
||||
</DynamicAuthWrapper>
|
||||
</PageWrapper>
|
||||
)
|
||||
}
|
||||
|
||||
export default AccountComparePage
|
||||
|
||||
export async function getStaticProps({ locale }) {
|
||||
return {
|
||||
props: {
|
||||
...(await serverSideTranslations(locale, namespaces)),
|
||||
...(await serverSideTranslations(locale, ns)),
|
||||
page: {
|
||||
locale,
|
||||
path: ['account', 'compare'],
|
||||
|
|
|
@ -1,13 +1,16 @@
|
|||
// Dependencies
|
||||
import dynamic from 'next/dynamic'
|
||||
import { serverSideTranslations } from 'next-i18next/serverSideTranslations'
|
||||
import { nsMerge } from 'shared/utils.mjs'
|
||||
// Hooks
|
||||
import { useTranslation } from 'next-i18next'
|
||||
// Components
|
||||
import { PageWrapper, ns as pageNs } from 'shared/components/wrappers/page.mjs'
|
||||
import { ns as authNs } from 'shared/components/wrappers/auth/index.mjs'
|
||||
import { ns as controlNs } from 'shared/components/account/control.mjs'
|
||||
|
||||
// Translation namespaces used on this page
|
||||
const namespaces = [...new Set([...controlNs, ...authNs, ...pageNs])]
|
||||
const ns = nsMerge(controlNs, authNs, pageNs)
|
||||
|
||||
/*
|
||||
* Some things should never generated as SSR
|
||||
|
@ -29,20 +32,24 @@ const DynamicControl = dynamic(
|
|||
* when path and locale come from static props (as here)
|
||||
* or set them manually.
|
||||
*/
|
||||
const AccountPage = ({ page }) => (
|
||||
<PageWrapper {...page}>
|
||||
<DynamicAuthWrapper>
|
||||
<DynamicControl title />
|
||||
</DynamicAuthWrapper>
|
||||
</PageWrapper>
|
||||
)
|
||||
const AccountPage = ({ page }) => {
|
||||
const { t } = useTranslation(ns)
|
||||
|
||||
return (
|
||||
<PageWrapper {...page} title={t('control')}>
|
||||
<DynamicAuthWrapper>
|
||||
<DynamicControl title />
|
||||
</DynamicAuthWrapper>
|
||||
</PageWrapper>
|
||||
)
|
||||
}
|
||||
|
||||
export default AccountPage
|
||||
|
||||
export async function getStaticProps({ locale }) {
|
||||
return {
|
||||
props: {
|
||||
...(await serverSideTranslations(locale, namespaces)),
|
||||
...(await serverSideTranslations(locale, ns)),
|
||||
page: {
|
||||
locale,
|
||||
path: ['account', 'control'],
|
||||
|
|
|
@ -1,13 +1,16 @@
|
|||
// Dependencies
|
||||
import dynamic from 'next/dynamic'
|
||||
import { serverSideTranslations } from 'next-i18next/serverSideTranslations'
|
||||
import { nsMerge } from 'shared/utils.mjs'
|
||||
// Hooks
|
||||
import { useTranslation } from 'next-i18next'
|
||||
// Components
|
||||
import { PageWrapper, ns as pageNs } from 'shared/components/wrappers/page.mjs'
|
||||
import { ns as authNs } from 'shared/components/wrappers/auth/index.mjs'
|
||||
import { ns as emailNs } from 'shared/components/account/email.mjs'
|
||||
|
||||
// Translation namespaces used on this page
|
||||
const namespaces = [...new Set([...emailNs, ...authNs, ...pageNs])]
|
||||
const ns = nsMerge(emailNs, authNs, pageNs)
|
||||
|
||||
/*
|
||||
* Some things should never generated as SSR
|
||||
|
@ -29,20 +32,24 @@ const DynamicEmail = dynamic(
|
|||
* when path and locale come from static props (as here)
|
||||
* or set them manually.
|
||||
*/
|
||||
const AccountEmailPage = ({ page }) => (
|
||||
<PageWrapper {...page}>
|
||||
<DynamicAuthWrapper>
|
||||
<DynamicEmail title />
|
||||
</DynamicAuthWrapper>
|
||||
</PageWrapper>
|
||||
)
|
||||
const AccountEmailPage = ({ page }) => {
|
||||
const { t } = useTranslation(ns)
|
||||
|
||||
return (
|
||||
<PageWrapper {...page} title={t('email')}>
|
||||
<DynamicAuthWrapper>
|
||||
<DynamicEmail title />
|
||||
</DynamicAuthWrapper>
|
||||
</PageWrapper>
|
||||
)
|
||||
}
|
||||
|
||||
export default AccountEmailPage
|
||||
|
||||
export async function getStaticProps({ locale }) {
|
||||
return {
|
||||
props: {
|
||||
...(await serverSideTranslations(locale, namespaces)),
|
||||
...(await serverSideTranslations(locale, ns)),
|
||||
page: {
|
||||
locale,
|
||||
path: ['account', 'email'],
|
||||
|
|
59
sites/org/pages/account/export.mjs
Normal file
59
sites/org/pages/account/export.mjs
Normal file
|
@ -0,0 +1,59 @@
|
|||
// Dependencies
|
||||
import dynamic from 'next/dynamic'
|
||||
import { serverSideTranslations } from 'next-i18next/serverSideTranslations'
|
||||
import { nsMerge } from 'shared/utils.mjs'
|
||||
// Hooks
|
||||
import { useTranslation } from 'next-i18next'
|
||||
// Components
|
||||
import { PageWrapper, ns as pageNs } from 'shared/components/wrappers/page.mjs'
|
||||
import { ns as authNs } from 'shared/components/wrappers/auth/index.mjs'
|
||||
import { ns as reloadNs } from 'shared/components/account/reload.mjs'
|
||||
|
||||
// Translation namespaces used on this page
|
||||
const ns = nsMerge(reloadNs, authNs, pageNs)
|
||||
|
||||
/*
|
||||
* Some things should never generated as SSR
|
||||
* So for these, we run a dynamic import and disable SSR rendering
|
||||
*/
|
||||
const DynamicAuthWrapper = dynamic(
|
||||
() => import('shared/components/wrappers/auth/index.mjs').then((mod) => mod.AuthWrapper),
|
||||
{ ssr: false }
|
||||
)
|
||||
|
||||
const DynamicExport = dynamic(
|
||||
() => import('shared/components/account/export.mjs').then((mod) => mod.ExportAccount),
|
||||
{ ssr: false }
|
||||
)
|
||||
|
||||
/*
|
||||
* Each page MUST be wrapped in the PageWrapper component.
|
||||
* You also MUST spread props.page into this wrapper component
|
||||
* when path and locale come from static props (as here)
|
||||
* or set them manually.
|
||||
*/
|
||||
const AccountExportPage = ({ page }) => {
|
||||
const { t } = useTranslation(ns)
|
||||
|
||||
return (
|
||||
<PageWrapper {...page} title={t('export')}>
|
||||
<DynamicAuthWrapper>
|
||||
<DynamicExport title />
|
||||
</DynamicAuthWrapper>
|
||||
</PageWrapper>
|
||||
)
|
||||
}
|
||||
|
||||
export default AccountExportPage
|
||||
|
||||
export async function getStaticProps({ locale }) {
|
||||
return {
|
||||
props: {
|
||||
...(await serverSideTranslations(locale, ns)),
|
||||
page: {
|
||||
locale,
|
||||
path: ['account', 'export'],
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
|
@ -1,13 +1,14 @@
|
|||
// Dependencies
|
||||
import dynamic from 'next/dynamic'
|
||||
import { serverSideTranslations } from 'next-i18next/serverSideTranslations'
|
||||
import { nsMerge } from 'shared/utils.mjs'
|
||||
// Components
|
||||
import { PageWrapper, ns as pageNs } from 'shared/components/wrappers/page.mjs'
|
||||
import { ns as authNs } from 'shared/components/wrappers/auth/index.mjs'
|
||||
import { ns as githubNs } from 'shared/components/account/github.mjs'
|
||||
|
||||
// Translation namespaces used on this page
|
||||
const namespaces = [...new Set([...githubNs, ...authNs, ...pageNs])]
|
||||
const ns = nsMerge(githubNs, authNs, pageNs)
|
||||
|
||||
/*
|
||||
* Some things should never generated as SSR
|
||||
|
@ -30,7 +31,7 @@ const DynamicGithub = dynamic(
|
|||
* or set them manually.
|
||||
*/
|
||||
const AccountPage = ({ page }) => (
|
||||
<PageWrapper {...page}>
|
||||
<PageWrapper {...page} title="GitHub">
|
||||
<DynamicAuthWrapper>
|
||||
<DynamicGithub title />
|
||||
</DynamicAuthWrapper>
|
||||
|
@ -42,7 +43,7 @@ export default AccountPage
|
|||
export async function getStaticProps({ locale }) {
|
||||
return {
|
||||
props: {
|
||||
...(await serverSideTranslations(locale, namespaces)),
|
||||
...(await serverSideTranslations(locale, ns)),
|
||||
page: {
|
||||
locale,
|
||||
path: ['account', 'github'],
|
||||
|
|
|
@ -1,13 +1,16 @@
|
|||
// Dependencies
|
||||
import dynamic from 'next/dynamic'
|
||||
import { serverSideTranslations } from 'next-i18next/serverSideTranslations'
|
||||
import { nsMerge } from 'shared/utils.mjs'
|
||||
// Hooks
|
||||
import { useTranslation } from 'next-i18next'
|
||||
// Components
|
||||
import { PageWrapper, ns as pageNs } from 'shared/components/wrappers/page.mjs'
|
||||
import { ns as authNs } from 'shared/components/wrappers/auth/index.mjs'
|
||||
import { ns as imgNs } from 'shared/components/account/img.mjs'
|
||||
|
||||
// Translation namespaces used on this page
|
||||
const namespaces = [...new Set([...imgNs, ...authNs, ...pageNs])]
|
||||
const ns = nsMerge(imgNs, authNs, pageNs)
|
||||
|
||||
/*
|
||||
* Some things should never generated as SSR
|
||||
|
@ -29,20 +32,24 @@ const DynamicImg = dynamic(
|
|||
* when path and locale come from static props (as here)
|
||||
* or set them manually.
|
||||
*/
|
||||
const AccountPage = ({ page }) => (
|
||||
<PageWrapper {...page}>
|
||||
<DynamicAuthWrapper>
|
||||
<DynamicImg title />
|
||||
</DynamicAuthWrapper>
|
||||
</PageWrapper>
|
||||
)
|
||||
const AccountPage = ({ page }) => {
|
||||
const { t } = useTranslation(ns)
|
||||
|
||||
return (
|
||||
<PageWrapper {...page} t={t('img')}>
|
||||
<DynamicAuthWrapper>
|
||||
<DynamicImg title />
|
||||
</DynamicAuthWrapper>
|
||||
</PageWrapper>
|
||||
)
|
||||
}
|
||||
|
||||
export default AccountPage
|
||||
|
||||
export async function getStaticProps({ locale }) {
|
||||
return {
|
||||
props: {
|
||||
...(await serverSideTranslations(locale, namespaces)),
|
||||
...(await serverSideTranslations(locale, ns)),
|
||||
page: {
|
||||
locale,
|
||||
path: ['account', 'img'],
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
// Dependencies
|
||||
import dynamic from 'next/dynamic'
|
||||
import { serverSideTranslations } from 'next-i18next/serverSideTranslations'
|
||||
import { nsMerge } from 'shared/utils.mjs'
|
||||
// Hooks
|
||||
import { useTranslation } from 'next-i18next'
|
||||
// Components
|
||||
|
@ -8,7 +9,7 @@ import { PageWrapper, ns as pageNs } from 'shared/components/wrappers/page.mjs'
|
|||
import { ns as authNs } from 'shared/components/wrappers/auth/index.mjs'
|
||||
|
||||
// Translation namespaces used on this page
|
||||
const ns = [...new Set(['account', ...pageNs, ...authNs])]
|
||||
const ns = nsMerge('account', 'status', pageNs, authNs)
|
||||
|
||||
/*
|
||||
* Some things should never generated as SSR
|
||||
|
|
|
@ -1,13 +1,16 @@
|
|||
// Dependencies
|
||||
import dynamic from 'next/dynamic'
|
||||
import { serverSideTranslations } from 'next-i18next/serverSideTranslations'
|
||||
import { nsMerge } from 'shared/utils.mjs'
|
||||
// Hooks
|
||||
import { useTranslation } from 'next-i18next'
|
||||
// Components
|
||||
import { PageWrapper, ns as pageNs } from 'shared/components/wrappers/page.mjs'
|
||||
import { ns as authNs } from 'shared/components/wrappers/auth/index.mjs'
|
||||
import { ns as languageNs } from 'shared/components/account/language.mjs'
|
||||
|
||||
// Translation namespaces used on this page
|
||||
const namespaces = [...new Set([...languageNs, ...authNs, ...pageNs])]
|
||||
const ns = nsMerge(languageNs, authNs, pageNs)
|
||||
|
||||
/*
|
||||
* Some things should never generated as SSR
|
||||
|
@ -29,20 +32,24 @@ const DynamicLanguage = dynamic(
|
|||
* when path and locale come from static props (as here)
|
||||
* or set them manually.
|
||||
*/
|
||||
const AccountLanguagePage = ({ page }) => (
|
||||
<PageWrapper {...page}>
|
||||
<DynamicAuthWrapper>
|
||||
<DynamicLanguage title />
|
||||
</DynamicAuthWrapper>
|
||||
</PageWrapper>
|
||||
)
|
||||
const AccountLanguagePage = ({ page }) => {
|
||||
const { t } = useTranslation(ns)
|
||||
|
||||
return (
|
||||
<PageWrapper {...page} title={t('account:language')}>
|
||||
<DynamicAuthWrapper>
|
||||
<DynamicLanguage title />
|
||||
</DynamicAuthWrapper>
|
||||
</PageWrapper>
|
||||
)
|
||||
}
|
||||
|
||||
export default AccountLanguagePage
|
||||
|
||||
export async function getStaticProps({ locale }) {
|
||||
return {
|
||||
props: {
|
||||
...(await serverSideTranslations(locale, namespaces)),
|
||||
...(await serverSideTranslations(locale, ns)),
|
||||
page: {
|
||||
locale,
|
||||
path: ['account', 'language'],
|
||||
|
|
|
@ -1,13 +1,16 @@
|
|||
// Dependencies
|
||||
import dynamic from 'next/dynamic'
|
||||
import { serverSideTranslations } from 'next-i18next/serverSideTranslations'
|
||||
import { nsMerge } from 'shared/utils.mjs'
|
||||
// Hooks
|
||||
import { useTranslation } from 'next-i18next'
|
||||
// Components
|
||||
import { PageWrapper, ns as pageNs } from 'shared/components/wrappers/page.mjs'
|
||||
import { ns as authNs } from 'shared/components/wrappers/auth/index.mjs'
|
||||
import { ns as mfaNs } from 'shared/components/account/mfa.mjs'
|
||||
|
||||
// Translation namespaces used on this page
|
||||
const namespaces = [...new Set([...mfaNs, ...authNs, ...pageNs])]
|
||||
const ns = nsMerge(mfaNs, authNs, pageNs)
|
||||
|
||||
/*
|
||||
* Some things should never generated as SSR
|
||||
|
@ -29,20 +32,24 @@ const DynamicMfa = dynamic(
|
|||
* when path and locale come from static props (as here)
|
||||
* or set them manually.
|
||||
*/
|
||||
const AccountMfaPage = ({ page }) => (
|
||||
<PageWrapper {...page}>
|
||||
<DynamicAuthWrapper>
|
||||
<DynamicMfa title />
|
||||
</DynamicAuthWrapper>
|
||||
</PageWrapper>
|
||||
)
|
||||
const AccountMfaPage = ({ page }) => {
|
||||
const { t } = useTranslation(ns)
|
||||
|
||||
return (
|
||||
<PageWrapper {...page} title={t('mfa')}>
|
||||
<DynamicAuthWrapper>
|
||||
<DynamicMfa title />
|
||||
</DynamicAuthWrapper>
|
||||
</PageWrapper>
|
||||
)
|
||||
}
|
||||
|
||||
export default AccountMfaPage
|
||||
|
||||
export async function getStaticProps({ locale }) {
|
||||
return {
|
||||
props: {
|
||||
...(await serverSideTranslations(locale, namespaces)),
|
||||
...(await serverSideTranslations(locale, ns)),
|
||||
page: {
|
||||
locale,
|
||||
path: ['account', 'mfa'],
|
||||
|
|
|
@ -1,13 +1,16 @@
|
|||
// Dependencies
|
||||
import dynamic from 'next/dynamic'
|
||||
import { serverSideTranslations } from 'next-i18next/serverSideTranslations'
|
||||
import { nsMerge } from 'shared/utils.mjs'
|
||||
// Hooks
|
||||
import { useTranslation } from 'next-i18next'
|
||||
// Components
|
||||
import { PageWrapper, ns as pageNs } from 'shared/components/wrappers/page.mjs'
|
||||
import { ns as authNs } from 'shared/components/wrappers/auth/index.mjs'
|
||||
import { ns as newsletterNs } from 'shared/components/account/newsletter.mjs'
|
||||
|
||||
// Translation namespaces used on this page
|
||||
const namespaces = [...new Set([...newsletterNs, ...authNs, ...pageNs])]
|
||||
const ns = nsMerge(newsletterNs, authNs, pageNs)
|
||||
|
||||
/*
|
||||
* Some things should never generated as SSR
|
||||
|
@ -29,20 +32,24 @@ const DynamicNewsletter = dynamic(
|
|||
* when path and locale come from static props (as here)
|
||||
* or set them manually.
|
||||
*/
|
||||
const AccountNewsletterPage = ({ page }) => (
|
||||
<PageWrapper {...page}>
|
||||
<DynamicAuthWrapper>
|
||||
<DynamicNewsletter title />
|
||||
</DynamicAuthWrapper>
|
||||
</PageWrapper>
|
||||
)
|
||||
const AccountNewsletterPage = ({ page }) => {
|
||||
const { t } = useTranslation(ns)
|
||||
|
||||
return (
|
||||
<PageWrapper {...page} title={t('newsletter')}>
|
||||
<DynamicAuthWrapper>
|
||||
<DynamicNewsletter title />
|
||||
</DynamicAuthWrapper>
|
||||
</PageWrapper>
|
||||
)
|
||||
}
|
||||
|
||||
export default AccountNewsletterPage
|
||||
|
||||
export async function getStaticProps({ locale }) {
|
||||
return {
|
||||
props: {
|
||||
...(await serverSideTranslations(locale, namespaces)),
|
||||
...(await serverSideTranslations(locale, ns)),
|
||||
page: {
|
||||
locale,
|
||||
path: ['account', 'newsletter'],
|
||||
|
|
|
@ -1,13 +1,16 @@
|
|||
// Dependencies
|
||||
import dynamic from 'next/dynamic'
|
||||
import { serverSideTranslations } from 'next-i18next/serverSideTranslations'
|
||||
import { nsMerge } from 'shared/utils.mjs'
|
||||
// Hooks
|
||||
import { useTranslation } from 'next-i18next'
|
||||
// Components
|
||||
import { PageWrapper, ns as pageNs } from 'shared/components/wrappers/page.mjs'
|
||||
import { ns as authNs } from 'shared/components/wrappers/auth/index.mjs'
|
||||
import { ns as passwordNs } from 'shared/components/account/password.mjs'
|
||||
|
||||
// Translation namespaces used on this page
|
||||
const namespaces = [...new Set([...passwordNs, ...authNs, ...pageNs])]
|
||||
const ns = nsMerge(passwordNs, authNs, pageNs)
|
||||
|
||||
/*
|
||||
* Some things should never generated as SSR
|
||||
|
@ -29,20 +32,24 @@ const DynamicPassword = dynamic(
|
|||
* when path and locale come from static props (as here)
|
||||
* or set them manually.
|
||||
*/
|
||||
const AccountPasswordPage = ({ page }) => (
|
||||
<PageWrapper {...page}>
|
||||
<DynamicAuthWrapper>
|
||||
<DynamicPassword title />
|
||||
</DynamicAuthWrapper>
|
||||
</PageWrapper>
|
||||
)
|
||||
const AccountPasswordPage = ({ page }) => {
|
||||
const { t } = useTranslation(ns)
|
||||
|
||||
return (
|
||||
<PageWrapper {...page} title={t('password')}>
|
||||
<DynamicAuthWrapper>
|
||||
<DynamicPassword title />
|
||||
</DynamicAuthWrapper>
|
||||
</PageWrapper>
|
||||
)
|
||||
}
|
||||
|
||||
export default AccountPasswordPage
|
||||
|
||||
export async function getStaticProps({ locale }) {
|
||||
return {
|
||||
props: {
|
||||
...(await serverSideTranslations(locale, namespaces)),
|
||||
...(await serverSideTranslations(locale, ns)),
|
||||
page: {
|
||||
locale,
|
||||
path: ['account', 'password'],
|
||||
|
|
|
@ -1,13 +1,16 @@
|
|||
// Dependencies
|
||||
import dynamic from 'next/dynamic'
|
||||
import { serverSideTranslations } from 'next-i18next/serverSideTranslations'
|
||||
import { nsMerge } from 'shared/utils.mjs'
|
||||
// Hooks
|
||||
import { useTranslation } from 'next-i18next'
|
||||
// Components
|
||||
import { PageWrapper, ns as pageNs } from 'shared/components/wrappers/page.mjs'
|
||||
import { ns as authNs } from 'shared/components/wrappers/auth/index.mjs'
|
||||
import { ns as reloadNs } from 'shared/components/account/reload.mjs'
|
||||
|
||||
// Translation namespaces used on this page
|
||||
const namespaces = [...new Set([...reloadNs, ...authNs, ...pageNs])]
|
||||
const ns = nsMerge(reloadNs, authNs, pageNs)
|
||||
|
||||
/*
|
||||
* Some things should never generated as SSR
|
||||
|
@ -29,20 +32,24 @@ const DynamicReload = dynamic(
|
|||
* when path and locale come from static props (as here)
|
||||
* or set them manually.
|
||||
*/
|
||||
const AccountReloadPage = ({ page }) => (
|
||||
<PageWrapper {...page}>
|
||||
<DynamicAuthWrapper>
|
||||
<DynamicReload title />
|
||||
</DynamicAuthWrapper>
|
||||
</PageWrapper>
|
||||
)
|
||||
const AccountReloadPage = ({ page }) => {
|
||||
const { t } = useTranslation(ns)
|
||||
|
||||
return (
|
||||
<PageWrapper {...page} title={t('reload')}>
|
||||
<DynamicAuthWrapper>
|
||||
<DynamicReload title />
|
||||
</DynamicAuthWrapper>
|
||||
</PageWrapper>
|
||||
)
|
||||
}
|
||||
|
||||
export default AccountReloadPage
|
||||
|
||||
export async function getStaticProps({ locale }) {
|
||||
return {
|
||||
props: {
|
||||
...(await serverSideTranslations(locale, namespaces)),
|
||||
...(await serverSideTranslations(locale, ns)),
|
||||
page: {
|
||||
locale,
|
||||
path: ['account', 'reload'],
|
||||
|
|
59
sites/org/pages/account/remove.mjs
Normal file
59
sites/org/pages/account/remove.mjs
Normal file
|
@ -0,0 +1,59 @@
|
|||
// Dependencies
|
||||
import dynamic from 'next/dynamic'
|
||||
import { serverSideTranslations } from 'next-i18next/serverSideTranslations'
|
||||
import { nsMerge } from 'shared/utils.mjs'
|
||||
// Hooks
|
||||
import { useTranslation } from 'next-i18next'
|
||||
// Components
|
||||
import { PageWrapper, ns as pageNs } from 'shared/components/wrappers/page.mjs'
|
||||
import { ns as authNs } from 'shared/components/wrappers/auth/index.mjs'
|
||||
import { ns as reloadNs } from 'shared/components/account/reload.mjs'
|
||||
|
||||
// Translation namespaces used on this page
|
||||
const ns = nsMerge(reloadNs, authNs, pageNs)
|
||||
|
||||
/*
|
||||
* Some things should never generated as SSR
|
||||
* So for these, we run a dynamic import and disable SSR rendering
|
||||
*/
|
||||
const DynamicAuthWrapper = dynamic(
|
||||
() => import('shared/components/wrappers/auth/index.mjs').then((mod) => mod.AuthWrapper),
|
||||
{ ssr: false }
|
||||
)
|
||||
|
||||
const DynamicRemove = dynamic(
|
||||
() => import('shared/components/account/remove.mjs').then((mod) => mod.RemoveAccount),
|
||||
{ ssr: false }
|
||||
)
|
||||
|
||||
/*
|
||||
* Each page MUST be wrapped in the PageWrapper component.
|
||||
* You also MUST spread props.page into this wrapper component
|
||||
* when path and locale come from static props (as here)
|
||||
* or set them manually.
|
||||
*/
|
||||
const AccountRemovePage = ({ page }) => {
|
||||
const { t } = useTranslation(ns)
|
||||
|
||||
return (
|
||||
<PageWrapper {...page} title={t('remove')}>
|
||||
<DynamicAuthWrapper>
|
||||
<DynamicRemove />
|
||||
</DynamicAuthWrapper>
|
||||
</PageWrapper>
|
||||
)
|
||||
}
|
||||
|
||||
export default AccountRemovePage
|
||||
|
||||
export async function getStaticProps({ locale }) {
|
||||
return {
|
||||
props: {
|
||||
...(await serverSideTranslations(locale, ns)),
|
||||
page: {
|
||||
locale,
|
||||
path: ['account', 'remove'],
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
59
sites/org/pages/account/restrict.mjs
Normal file
59
sites/org/pages/account/restrict.mjs
Normal file
|
@ -0,0 +1,59 @@
|
|||
// Dependencies
|
||||
import dynamic from 'next/dynamic'
|
||||
import { serverSideTranslations } from 'next-i18next/serverSideTranslations'
|
||||
import { nsMerge } from 'shared/utils.mjs'
|
||||
// Hooks
|
||||
import { useTranslation } from 'next-i18next'
|
||||
// Components
|
||||
import { PageWrapper, ns as pageNs } from 'shared/components/wrappers/page.mjs'
|
||||
import { ns as authNs } from 'shared/components/wrappers/auth/index.mjs'
|
||||
import { ns as reloadNs } from 'shared/components/account/reload.mjs'
|
||||
|
||||
// Translation namespaces used on this page
|
||||
const ns = nsMerge(reloadNs, authNs, pageNs)
|
||||
|
||||
/*
|
||||
* Some things should never generated as SSR
|
||||
* So for these, we run a dynamic import and disable SSR rendering
|
||||
*/
|
||||
const DynamicAuthWrapper = dynamic(
|
||||
() => import('shared/components/wrappers/auth/index.mjs').then((mod) => mod.AuthWrapper),
|
||||
{ ssr: false }
|
||||
)
|
||||
|
||||
const DynamicRestrict = dynamic(
|
||||
() => import('shared/components/account/restrict.mjs').then((mod) => mod.RestrictAccount),
|
||||
{ ssr: false }
|
||||
)
|
||||
|
||||
/*
|
||||
* Each page MUST be wrapped in the PageWrapper component.
|
||||
* You also MUST spread props.page into this wrapper component
|
||||
* when path and locale come from static props (as here)
|
||||
* or set them manually.
|
||||
*/
|
||||
const AccountRestrictPage = ({ page }) => {
|
||||
const { t } = useTranslation(ns)
|
||||
|
||||
return (
|
||||
<PageWrapper {...page} title={t('restrict')}>
|
||||
<DynamicAuthWrapper>
|
||||
<DynamicRestrict />
|
||||
</DynamicAuthWrapper>
|
||||
</PageWrapper>
|
||||
)
|
||||
}
|
||||
|
||||
export default AccountRestrictPage
|
||||
|
||||
export async function getStaticProps({ locale }) {
|
||||
return {
|
||||
props: {
|
||||
...(await serverSideTranslations(locale, ns)),
|
||||
page: {
|
||||
locale,
|
||||
path: ['account', 'restrict'],
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
|
@ -1,13 +1,16 @@
|
|||
// Dependencies
|
||||
import dynamic from 'next/dynamic'
|
||||
import { serverSideTranslations } from 'next-i18next/serverSideTranslations'
|
||||
import { nsMerge } from 'shared/utils.mjs'
|
||||
// Hooks
|
||||
import { useTranslation } from 'next-i18next'
|
||||
// Components
|
||||
import { PageWrapper, ns as pageNs } from 'shared/components/wrappers/page.mjs'
|
||||
import { ns as authNs } from 'shared/components/wrappers/auth/index.mjs'
|
||||
import { ns as unitsNs } from 'shared/components/account/imperial.mjs'
|
||||
|
||||
// Translation namespaces used on this page
|
||||
const namespaces = [...new Set([...unitsNs, ...authNs, ...pageNs])]
|
||||
const namespaces = nsMerge(unitsNs, authNs, pageNs)
|
||||
|
||||
/*
|
||||
* Some things should never generated as SSR
|
||||
|
@ -29,13 +32,17 @@ const DynamicImperial = dynamic(
|
|||
* when path and locale come from static props (as here)
|
||||
* or set them manually.
|
||||
*/
|
||||
const AccountUnitsPage = ({ page }) => (
|
||||
<PageWrapper {...page}>
|
||||
<DynamicAuthWrapper>
|
||||
<DynamicImperial title />
|
||||
</DynamicAuthWrapper>
|
||||
</PageWrapper>
|
||||
)
|
||||
const AccountUnitsPage = ({ page }) => {
|
||||
const { t } = useTranslation(namespaces)
|
||||
|
||||
return (
|
||||
<PageWrapper {...page} title={t('account:units')}>
|
||||
<DynamicAuthWrapper>
|
||||
<DynamicImperial title />
|
||||
</DynamicAuthWrapper>
|
||||
</PageWrapper>
|
||||
)
|
||||
}
|
||||
|
||||
export default AccountUnitsPage
|
||||
|
||||
|
|
|
@ -1,13 +1,16 @@
|
|||
// Dependencies
|
||||
import dynamic from 'next/dynamic'
|
||||
import { serverSideTranslations } from 'next-i18next/serverSideTranslations'
|
||||
import { nsMerge } from 'shared/utils.mjs'
|
||||
// Hooks
|
||||
import { useTranslation } from 'next-i18next'
|
||||
// Components
|
||||
import { PageWrapper, ns as pageNs } from 'shared/components/wrappers/page.mjs'
|
||||
import { ns as authNs } from 'shared/components/wrappers/auth/index.mjs'
|
||||
import { ns as usernameNs } from 'shared/components/account/username.mjs'
|
||||
|
||||
// Translation namespaces used on this page
|
||||
const namespaces = [...new Set([...usernameNs, ...authNs, ...pageNs])]
|
||||
const ns = nsMerge(usernameNs, authNs, pageNs)
|
||||
|
||||
/*
|
||||
* Some things should never generated as SSR
|
||||
|
@ -29,20 +32,24 @@ const DynamicUsername = dynamic(
|
|||
* when path and locale come from static props (as here)
|
||||
* or set them manually.
|
||||
*/
|
||||
const AccountPage = ({ page }) => (
|
||||
<PageWrapper {...page}>
|
||||
<DynamicAuthWrapper>
|
||||
<DynamicUsername title />
|
||||
</DynamicAuthWrapper>
|
||||
</PageWrapper>
|
||||
)
|
||||
const AccountPage = ({ page }) => {
|
||||
const { t } = useTranslation(ns)
|
||||
|
||||
return (
|
||||
<PageWrapper {...page} title={t('username')}>
|
||||
<DynamicAuthWrapper>
|
||||
<DynamicUsername title />
|
||||
</DynamicAuthWrapper>
|
||||
</PageWrapper>
|
||||
)
|
||||
}
|
||||
|
||||
export default AccountPage
|
||||
|
||||
export async function getStaticProps({ locale }) {
|
||||
return {
|
||||
props: {
|
||||
...(await serverSideTranslations(locale, namespaces)),
|
||||
...(await serverSideTranslations(locale, ns)),
|
||||
page: {
|
||||
locale,
|
||||
path: ['account', 'username'],
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
// Hooks
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useAccount } from 'shared/hooks/use-account.mjs'
|
||||
import { useBackend } from 'shared/hooks/use-backend.mjs'
|
||||
import { useRouter } from 'next/router'
|
||||
import { useTranslation } from 'next-i18next'
|
||||
|
@ -26,8 +25,7 @@ const ActiveSignUpPage = () => {
|
|||
path: ['confirm', 'emailchange', confirmationId],
|
||||
}
|
||||
|
||||
const { token } = useAccount()
|
||||
const backend = useBackend(token)
|
||||
const backend = useBackend()
|
||||
const { t } = useTranslation(ns)
|
||||
|
||||
const [id, setId] = useState(false)
|
||||
|
|
|
@ -52,8 +52,8 @@ const ConfirmSignUpPage = () => {
|
|||
path: ['confirm', 'emailchange', confirmationId],
|
||||
}
|
||||
|
||||
const { token, setAccount, setToken } = useAccount()
|
||||
const backend = useBackend(token)
|
||||
const { setAccount, setToken } = useAccount()
|
||||
const backend = useBackend()
|
||||
const { t } = useTranslation(ns)
|
||||
|
||||
const [id, setId] = useState(false)
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
// Hooks
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useAccount } from 'shared/hooks/use-account.mjs'
|
||||
import { useBackend } from 'shared/hooks/use-backend.mjs'
|
||||
import { useDesign } from 'shared/hooks/use-design.mjs'
|
||||
// Dependencies
|
||||
|
@ -20,8 +19,7 @@ const EditPatternPage = ({ page, id }) => {
|
|||
const [pattern, setPattern] = useState(false)
|
||||
|
||||
// Hooks
|
||||
const { token } = useAccount()
|
||||
const backend = useBackend(token)
|
||||
const backend = useBackend()
|
||||
const Design = useDesign(pattern?.design)
|
||||
|
||||
// Effect
|
||||
|
|
|
@ -289,13 +289,12 @@ export const NewApikey = ({ standalone = false }) => {
|
|||
const { startLoading, stopLoading } = useContext(LoadingContext)
|
||||
|
||||
// Hooks
|
||||
const { account, token } = useAccount()
|
||||
const backend = useBackend(token)
|
||||
const { account } = useAccount()
|
||||
const backend = useBackend()
|
||||
const { t } = useTranslation(ns)
|
||||
const toast = useToast()
|
||||
|
||||
// State
|
||||
const [keys, setKeys] = useState([])
|
||||
const [generate, setGenerate] = useState(false)
|
||||
const [added, setAdded] = useState(0)
|
||||
|
||||
|
@ -308,6 +307,7 @@ export const NewApikey = ({ standalone = false }) => {
|
|||
{...{
|
||||
t,
|
||||
account,
|
||||
generate,
|
||||
setGenerate,
|
||||
backend,
|
||||
toast,
|
||||
|
@ -327,8 +327,8 @@ export const Apikeys = () => {
|
|||
const { startLoading, stopLoading, loading } = useContext(LoadingContext)
|
||||
|
||||
// Hooks
|
||||
const { account, token } = useAccount()
|
||||
const backend = useBackend(token)
|
||||
const { account } = useAccount()
|
||||
const backend = useBackend()
|
||||
const { t } = useTranslation(ns)
|
||||
const toast = useToast()
|
||||
const { CollapseButton, closeCollapseButton } = useCollapseButton()
|
||||
|
|
|
@ -1,12 +1,10 @@
|
|||
// Dependencies
|
||||
import { useState, useContext } from 'react'
|
||||
import { useState } from 'react'
|
||||
import { useTranslation } from 'next-i18next'
|
||||
// Hooks
|
||||
import { useAccount } from 'shared/hooks/use-account.mjs'
|
||||
import { useBackend } from 'shared/hooks/use-backend.mjs'
|
||||
import { useToast } from 'shared/hooks/use-toast.mjs'
|
||||
// Context
|
||||
import { LoadingContext } from 'shared/context/loading-context.mjs'
|
||||
import { useLoadingStatus } from 'shared/hooks/use-loading-status.mjs'
|
||||
// Components
|
||||
import Markdown from 'react-markdown'
|
||||
import { Icons, welcomeSteps, BackToAccountButton } from './shared.mjs'
|
||||
|
@ -14,7 +12,7 @@ import { Popout } from 'shared/components/popout/index.mjs'
|
|||
import { SaveSettingsButton } from 'shared/components/buttons/save-settings-button.mjs'
|
||||
import { ContinueButton } from 'shared/components/buttons/continue-button.mjs'
|
||||
|
||||
export const ns = ['account', 'toast']
|
||||
export const ns = ['account', 'status']
|
||||
|
||||
export const Tab = ({ id, activeTab, setActiveTab, t }) => (
|
||||
<button
|
||||
|
@ -27,14 +25,11 @@ export const Tab = ({ id, activeTab, setActiveTab, t }) => (
|
|||
)
|
||||
|
||||
export const BioSettings = ({ title = false, welcome = false }) => {
|
||||
// Context
|
||||
const { loading, startLoading, stopLoading } = useContext(LoadingContext)
|
||||
|
||||
// Hooks
|
||||
const { account, setAccount, token } = useAccount()
|
||||
const backend = useBackend(token)
|
||||
const { account, setAccount } = useAccount()
|
||||
const backend = useBackend()
|
||||
const { t } = useTranslation(ns)
|
||||
const toast = useToast()
|
||||
const { setLoadingStatus, LoadingStatus } = useLoadingStatus()
|
||||
|
||||
// State
|
||||
const [bio, setBio] = useState(account.bio)
|
||||
|
@ -42,13 +37,12 @@ export const BioSettings = ({ title = false, welcome = false }) => {
|
|||
|
||||
// Helper method to save bio
|
||||
const save = async () => {
|
||||
startLoading()
|
||||
setLoadingStatus([true, 'processingUpdate'])
|
||||
const result = await backend.updateAccount({ bio })
|
||||
if (result.success) {
|
||||
setAccount(result.data.account)
|
||||
toast.for.settingsSaved()
|
||||
} else toast.for.backendError()
|
||||
stopLoading()
|
||||
setLoadingStatus([true, 'settingsSaved', true, true])
|
||||
} else setLoadingStatus([true, 'backendError', true, true])
|
||||
}
|
||||
|
||||
// Next step in the onboarding
|
||||
|
@ -62,6 +56,7 @@ export const BioSettings = ({ title = false, welcome = false }) => {
|
|||
|
||||
return (
|
||||
<div className="max-w-xl xl:pl-4">
|
||||
<LoadingStatus />
|
||||
{title ? <h1 className="text-4xl">{t('bioTitle')}</h1> : null}
|
||||
<div className="tabs w-full">
|
||||
<Tab id="edit" {...tabProps} />
|
||||
|
@ -83,7 +78,7 @@ export const BioSettings = ({ title = false, welcome = false }) => {
|
|||
)}
|
||||
</div>
|
||||
<SaveSettingsButton btnProps={{ onClick: save }} welcome={welcome} />
|
||||
{!welcome && <BackToAccountButton loading={loading} />}
|
||||
{!welcome && <BackToAccountButton />}
|
||||
<Popout tip compact>
|
||||
{t('mdSupport')}
|
||||
</Popout>
|
||||
|
|
|
@ -1,26 +1,21 @@
|
|||
// Dependencies
|
||||
import { useState, useContext } from 'react'
|
||||
import { useState } from 'react'
|
||||
import { useTranslation } from 'next-i18next'
|
||||
// Hooks
|
||||
import { useAccount } from 'shared/hooks/use-account.mjs'
|
||||
import { useBackend } from 'shared/hooks/use-backend.mjs'
|
||||
import { useToast } from 'shared/hooks/use-toast.mjs'
|
||||
// Context
|
||||
import { LoadingContext } from 'shared/context/loading-context.mjs'
|
||||
import { useLoadingStatus } from 'shared/hooks/use-loading-status.mjs'
|
||||
// Components
|
||||
import { Choice, Icons, welcomeSteps, BackToAccountButton } from './shared.mjs'
|
||||
import { ContinueButton } from 'shared/components/buttons/continue-button.mjs'
|
||||
|
||||
export const ns = ['account', 'toast']
|
||||
export const ns = ['account', 'status']
|
||||
|
||||
export const CompareSettings = ({ title = false, welcome = false }) => {
|
||||
// Context
|
||||
const { loading, startLoading, stopLoading } = useContext(LoadingContext)
|
||||
|
||||
// Hooks
|
||||
const { account, setAccount, token } = useAccount()
|
||||
const backend = useBackend(token)
|
||||
const toast = useToast()
|
||||
const { account, setAccount } = useAccount()
|
||||
const backend = useBackend()
|
||||
const { setLoadingStatus, LoadingStatus } = useLoadingStatus()
|
||||
const { t } = useTranslation(ns)
|
||||
|
||||
// State
|
||||
|
@ -29,16 +24,15 @@ export const CompareSettings = ({ title = false, welcome = false }) => {
|
|||
// Helper method to update the account
|
||||
const update = async (val) => {
|
||||
if (val !== selection) {
|
||||
startLoading()
|
||||
setLoadingStatus([true, 'processingUpdate'])
|
||||
const result = await backend.updateAccount({
|
||||
compare: val === 'yes' ? true : false,
|
||||
})
|
||||
if (result.success) {
|
||||
setLoadingStatus([true, 'settingsSaved', true, true])
|
||||
setAccount(result.data.account)
|
||||
setSelection(val)
|
||||
toast.for.settingsSaved()
|
||||
} else toast.for.backendError()
|
||||
stopLoading()
|
||||
} else setLoadingStatus([true, 'backendError', true, true])
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -50,6 +44,7 @@ export const CompareSettings = ({ title = false, welcome = false }) => {
|
|||
|
||||
return (
|
||||
<div className="max-w-xl">
|
||||
<LoadingStatus />
|
||||
{title ? <h2 className="text-4xl">{t('compareTitle')}</h2> : null}
|
||||
{['yes', 'no'].map((val) => (
|
||||
<Choice val={val} t={t} update={update} current={selection} bool key={val}>
|
||||
|
@ -85,7 +80,7 @@ export const CompareSettings = ({ title = false, welcome = false }) => {
|
|||
) : null}
|
||||
</>
|
||||
) : (
|
||||
<BackToAccountButton loading={loading} />
|
||||
<BackToAccountButton />
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
|
|
|
@ -1,12 +1,11 @@
|
|||
// Dependencies
|
||||
import { useState, useContext } from 'react'
|
||||
import { useState } from 'react'
|
||||
import { useTranslation } from 'next-i18next'
|
||||
import { nsMerge } from 'shared/utils.mjs'
|
||||
// Hooks
|
||||
import { useAccount } from 'shared/hooks/use-account.mjs'
|
||||
import { useBackend } from 'shared/hooks/use-backend.mjs'
|
||||
import { useToast } from 'shared/hooks/use-toast.mjs'
|
||||
// Context
|
||||
import { LoadingContext } from 'shared/context/loading-context.mjs'
|
||||
import { useLoadingStatus } from 'shared/hooks/use-loading-status.mjs'
|
||||
// Components
|
||||
import Link from 'next/link'
|
||||
import { Popout } from 'shared/components/popout/index.mjs'
|
||||
|
@ -14,7 +13,7 @@ import { BackToAccountButton } from './shared.mjs'
|
|||
import { SaveSettingsButton } from 'shared/components/buttons/save-settings-button.mjs'
|
||||
import { GdprAccountDetails, ns as gdprNs } from 'shared/components/gdpr/details.mjs'
|
||||
|
||||
export const ns = [...gdprNs, 'account', 'toast']
|
||||
export const ns = nsMerge(gdprNs, 'account', 'toast')
|
||||
|
||||
const Checkbox = ({ value, setter, label, children = null }) => (
|
||||
<div
|
||||
|
@ -37,13 +36,10 @@ const Checkbox = ({ value, setter, label, children = null }) => (
|
|||
)
|
||||
|
||||
export const ConsentSettings = ({ title = false }) => {
|
||||
// Context
|
||||
const { loading, startLoading, stopLoading } = useContext(LoadingContext)
|
||||
|
||||
// Hooks
|
||||
const { account, token, setAccount, setToken } = useAccount()
|
||||
const backend = useBackend(token)
|
||||
const toast = useToast()
|
||||
const { account, setAccount, setToken } = useAccount()
|
||||
const backend = useBackend()
|
||||
const { setLoadingStatus, LoadingStatus } = useLoadingStatus()
|
||||
const { t } = useTranslation(ns)
|
||||
|
||||
// State
|
||||
|
@ -56,27 +52,28 @@ export const ConsentSettings = ({ title = false }) => {
|
|||
if (consent1) newConsent = 1
|
||||
if (consent1 && consent2) newConsent = 2
|
||||
if (newConsent !== account.consent) {
|
||||
startLoading()
|
||||
setLoadingStatus([true, 'processingUpdate'])
|
||||
const result = await backend.updateAccount({ consent: newConsent })
|
||||
if (result.data?.result === 'success') toast.for.settingsSaved()
|
||||
else toast.for.backendError()
|
||||
stopLoading()
|
||||
if (result.data?.result === 'success') {
|
||||
setLoadingStatus([true, 'settingsSaved', true, true])
|
||||
setAccount(result.data.account)
|
||||
} else setLoadingStatus([true, 'backendError', true, true])
|
||||
}
|
||||
}
|
||||
|
||||
// Helper method to remove the account
|
||||
const removeAccount = async () => {
|
||||
startLoading()
|
||||
setLoadingStatus([true, 'processingUpdate'])
|
||||
const result = await backend.removeAccount()
|
||||
if (result === true) toast.for.settingsSaved()
|
||||
else toast.for.backendError()
|
||||
if (result === true) setLoadingStatus([true, 'settingsSaved', true, true])
|
||||
else setLoadingStatus([true, 'backendError', true, true])
|
||||
setToken(null)
|
||||
setAccount({ username: false })
|
||||
stopLoading()
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="max-w-xl xl:pl-4">
|
||||
<LoadingStatus />
|
||||
{title ? <h2 className="text-4xl">{t('privacyMatters')}</h2> : null}
|
||||
<p>{t('compliant')}</p>
|
||||
<p>{t('consentWhyAnswer')}</p>
|
||||
|
@ -108,7 +105,7 @@ export const ConsentSettings = ({ title = false }) => {
|
|||
}}
|
||||
/>
|
||||
)}
|
||||
<BackToAccountButton loading={loading} />
|
||||
<BackToAccountButton />
|
||||
<p className="text-center opacity-50 mt-12">
|
||||
<Link href="/docs/various/privacy" className="hover:text-secondary underline">
|
||||
FreeSewing Privacy Notice
|
||||
|
|
|
@ -1,27 +1,22 @@
|
|||
// Dependencies
|
||||
import { useState, useContext } from 'react'
|
||||
import { useState } from 'react'
|
||||
import { useTranslation } from 'next-i18next'
|
||||
// Hooks
|
||||
import { useAccount } from 'shared/hooks/use-account.mjs'
|
||||
import { useBackend } from 'shared/hooks/use-backend.mjs'
|
||||
import { useToast } from 'shared/hooks/use-toast.mjs'
|
||||
// Context
|
||||
import { LoadingContext } from 'shared/context/loading-context.mjs'
|
||||
import { useLoadingStatus } from 'shared/hooks/use-loading-status.mjs'
|
||||
// Components
|
||||
import { BackToAccountButton, Choice, Icons, welcomeSteps } from './shared.mjs'
|
||||
import { ContinueButton } from 'shared/components/buttons/continue-button.mjs'
|
||||
|
||||
export const ns = ['account', 'toast']
|
||||
export const ns = ['account', 'status']
|
||||
|
||||
/** state handlers for any input that changes the control setting */
|
||||
export const useControlState = () => {
|
||||
// Context
|
||||
const { startLoading, stopLoading } = useContext(LoadingContext)
|
||||
|
||||
// Hooks
|
||||
const { account, setAccount, token } = useAccount()
|
||||
const backend = useBackend(token)
|
||||
const toast = useToast()
|
||||
const backend = useBackend()
|
||||
const { setLoadingStatus, LoadingStatus } = useLoadingStatus()
|
||||
|
||||
// State
|
||||
const [selection, setSelection] = useState(account.control)
|
||||
|
@ -30,14 +25,13 @@ export const useControlState = () => {
|
|||
const update = async (control) => {
|
||||
if (control !== selection) {
|
||||
if (token) {
|
||||
startLoading()
|
||||
setLoadingStatus([true, 'processingUpdate'])
|
||||
const result = await backend.updateAccount({ control })
|
||||
if (result.success) {
|
||||
setSelection(control)
|
||||
toast.for.settingsSaved()
|
||||
setAccount(result.data.account)
|
||||
} else toast.for.backendError()
|
||||
stopLoading()
|
||||
setLoadingStatus([true, 'settingsSaved', true, true])
|
||||
} else setLoadingStatus([true, 'backendError', true, true])
|
||||
}
|
||||
//fallback for guest users
|
||||
else {
|
||||
|
@ -47,13 +41,13 @@ export const useControlState = () => {
|
|||
}
|
||||
}
|
||||
|
||||
return { selection, update }
|
||||
return { selection, update, LoadingStatus }
|
||||
}
|
||||
|
||||
export const ControlSettings = ({ title = false, welcome = false, noBack = false }) => {
|
||||
const { t } = useTranslation(ns)
|
||||
|
||||
const { selection, update } = useControlState()
|
||||
const { selection, update, LoadingStatus } = useControlState()
|
||||
|
||||
// Helper to get the link to the next onboarding step
|
||||
const nextHref = welcome
|
||||
|
@ -64,6 +58,7 @@ export const ControlSettings = ({ title = false, welcome = false, noBack = false
|
|||
|
||||
return (
|
||||
<div className="max-w-xl">
|
||||
<LoadingStatus />
|
||||
{title ? <h1 className="text-4xl">{t('controlTitle')}</h1> : null}
|
||||
{[1, 2, 3, 4, 5].map((val) => (
|
||||
<Choice val={val} t={t} update={update} current={selection} key={val}>
|
||||
|
|
|
@ -1,29 +1,24 @@
|
|||
// Dependencies
|
||||
import { useState, useContext } from 'react'
|
||||
import { useState } from 'react'
|
||||
import { useTranslation } from 'next-i18next'
|
||||
// Hooks
|
||||
import { useAccount } from 'shared/hooks/use-account.mjs'
|
||||
import { useBackend } from 'shared/hooks/use-backend.mjs'
|
||||
import { useToast } from 'shared/hooks/use-toast.mjs'
|
||||
// Context
|
||||
import { LoadingContext } from 'shared/context/loading-context.mjs'
|
||||
import { useLoadingStatus } from 'shared/hooks/use-loading-status.mjs'
|
||||
// Verification methods
|
||||
import { validateEmail, validateTld } from 'shared/utils.mjs'
|
||||
// Components
|
||||
import { BackToAccountButton } from './shared.mjs'
|
||||
import { Popout } from 'shared/components/popout/index.mjs'
|
||||
|
||||
export const ns = ['account', 'toast']
|
||||
export const ns = ['account', 'status']
|
||||
|
||||
export const EmailSettings = ({ title = false }) => {
|
||||
// Context
|
||||
const { loading, startLoading, stopLoading } = useContext(LoadingContext)
|
||||
|
||||
// Hooks
|
||||
const { account, setAccount, token } = useAccount()
|
||||
const backend = useBackend(token)
|
||||
const { account, setAccount } = useAccount()
|
||||
const backend = useBackend()
|
||||
const { t } = useTranslation(ns)
|
||||
const toast = useToast()
|
||||
const { setLoadingStatus, LoadingStatus } = useLoadingStatus()
|
||||
|
||||
// State
|
||||
const [email, setEmail] = useState(account.email)
|
||||
|
@ -31,14 +26,13 @@ export const EmailSettings = ({ title = false }) => {
|
|||
|
||||
// Helper method to update account
|
||||
const save = async () => {
|
||||
startLoading()
|
||||
setLoadingStatus([true, 'processingUpdate'])
|
||||
const result = await backend.updateAccount({ email })
|
||||
if (result.success) {
|
||||
setAccount(result.data.account)
|
||||
toast.for.settingsSaved()
|
||||
} else toast.for.backendError()
|
||||
setLoadingStatus([true, 'settingsSaved', true, true])
|
||||
} else setLoadingStatus([true, 'backendError', true, true])
|
||||
setChanged(true)
|
||||
stopLoading()
|
||||
}
|
||||
|
||||
// Is email valid?
|
||||
|
@ -46,6 +40,7 @@ export const EmailSettings = ({ title = false }) => {
|
|||
|
||||
return (
|
||||
<div className="max-w-xl">
|
||||
<LoadingStatus />
|
||||
{title ? <h2 className="text-4xl">{t('emailTitle')}</h2> : null}
|
||||
{changed ? (
|
||||
<Popout note>
|
||||
|
@ -71,7 +66,7 @@ export const EmailSettings = ({ title = false }) => {
|
|||
</button>
|
||||
</>
|
||||
)}
|
||||
<BackToAccountButton loading={loading} />
|
||||
<BackToAccountButton />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -48,11 +48,18 @@ developer: Developer
|
|||
|
||||
reload: Reload account
|
||||
export: Export your data
|
||||
exportMsg: Click below to export your personal data
|
||||
exportNote: The EU's General Data Protection Regulation (GDPR) ensures your so-called right to data portability — the right to obtain and reuse your personal data for your own purposes, or across different services.
|
||||
exportDownload: "Your data was exported and is available for download at the following location:"
|
||||
review: Review your consent
|
||||
restrict: Restrict processing of your data
|
||||
disable: Disable your account
|
||||
remove: Remove your account
|
||||
|
||||
proceedWithCaution: Proceed with caution
|
||||
restrictWarning: While no data will be removed, this will disable your account. Furthermore, you can not undo this on your own, but will have to contact support when you want to restore access to your account.
|
||||
noWayBack: There is no way back from this.
|
||||
|
||||
mdSupport: You can use markdown here
|
||||
or: or
|
||||
continue: Continue
|
||||
|
@ -204,3 +211,12 @@ newBasic: The basics
|
|||
newAdvanced: Go further
|
||||
|
||||
generateANewThing: "Generate a new { thing }"
|
||||
|
||||
website: Website
|
||||
linkedIdentities: Linked Identities
|
||||
|
||||
websiteTitle: Do you have a website or other URL you'd like to add?
|
||||
platformTitle: Who are you on { platform }?
|
||||
platformWhy: We do not use this data in any way. This is only here so FreeSewing users can connect the dots across platforms.
|
||||
|
||||
security: Security
|
||||
|
|
50
sites/shared/components/account/export.mjs
Normal file
50
sites/shared/components/account/export.mjs
Normal file
|
@ -0,0 +1,50 @@
|
|||
// Dependencies
|
||||
import { useTranslation } from 'next-i18next'
|
||||
// Hooks
|
||||
import { useState } from 'react'
|
||||
import { useBackend } from 'shared/hooks/use-backend.mjs'
|
||||
import { useLoadingStatus } from 'shared/hooks/use-loading-status.mjs'
|
||||
// Components
|
||||
import { BackToAccountButton } from './shared.mjs'
|
||||
import { Popout } from 'shared/components/popout/index.mjs'
|
||||
import { WebLink } from 'shared/components/web-link.mjs'
|
||||
|
||||
export const ns = ['account', 'status']
|
||||
|
||||
export const ExportAccount = () => {
|
||||
// Hooks
|
||||
const backend = useBackend()
|
||||
const { t } = useTranslation(ns)
|
||||
const { setLoadingStatus, LoadingStatus } = useLoadingStatus()
|
||||
|
||||
const [link, setLink] = useState()
|
||||
|
||||
// Helper method to export account
|
||||
const exportData = async () => {
|
||||
setLoadingStatus([true, 'processingUpdate'])
|
||||
const result = await backend.exportAccount()
|
||||
if (result.success) {
|
||||
setLink(result.data.data)
|
||||
setLoadingStatus([true, 'nailedIt', true, true])
|
||||
} else setLoadingStatus([true, 'backendError', true, false])
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="max-w-xl">
|
||||
<LoadingStatus />
|
||||
{link ? (
|
||||
<Popout link>
|
||||
<h5>{t('exportDownload')}</h5>
|
||||
<p className="text-lg">
|
||||
<WebLink href={link} txt={link} />
|
||||
</p>
|
||||
</Popout>
|
||||
) : null}
|
||||
<p>{t('exportMsg')}</p>
|
||||
<button className="btn btn-primary capitalize w-full my-2" onClick={exportData}>
|
||||
{t('export')}
|
||||
</button>
|
||||
<BackToAccountButton />
|
||||
</div>
|
||||
)
|
||||
}
|
|
@ -6,8 +6,8 @@ import { useBackend } from 'shared/hooks/use-backend.mjs'
|
|||
|
||||
export const ForceAccountCheck = ({ trigger = null }) => {
|
||||
// Hooks
|
||||
const { account, setAccount, token, signOut } = useAccount()
|
||||
const backend = useBackend(token)
|
||||
const { account, setAccount, signOut } = useAccount()
|
||||
const backend = useBackend()
|
||||
|
||||
// State
|
||||
const [lastCheck, setLastCheck] = useState(Date.now())
|
||||
|
|
|
@ -1,28 +1,23 @@
|
|||
// Dependencies
|
||||
import { useState, useContext } from 'react'
|
||||
import { useState } from 'react'
|
||||
import { useTranslation } from 'next-i18next'
|
||||
// Hooks
|
||||
import { useAccount } from 'shared/hooks/use-account.mjs'
|
||||
import { useBackend } from 'shared/hooks/use-backend.mjs'
|
||||
import { useToast } from 'shared/hooks/use-toast.mjs'
|
||||
// Context
|
||||
import { LoadingContext } from 'shared/context/loading-context.mjs'
|
||||
import { useLoadingStatus } from 'shared/hooks/use-loading-status.mjs'
|
||||
// Components
|
||||
import { BackToAccountButton } from './shared.mjs'
|
||||
import { SaveSettingsButton } from 'shared/components/buttons/save-settings-button.mjs'
|
||||
import { Popout } from 'shared/components/popout/index.mjs'
|
||||
|
||||
export const ns = ['account', 'toast']
|
||||
|
||||
export const GithubSettings = ({ title = false, welcome = false }) => {
|
||||
// Context
|
||||
const { loading, startLoading, stopLoading } = useContext(LoadingContext)
|
||||
export const ns = ['account', 'status']
|
||||
|
||||
export const GithubSettings = () => {
|
||||
// Hooks
|
||||
const { account, setAccount, token } = useAccount()
|
||||
const backend = useBackend(token)
|
||||
const { account, setAccount } = useAccount()
|
||||
const backend = useBackend()
|
||||
const { t } = useTranslation(ns)
|
||||
const toast = useToast()
|
||||
const { setLoadingStatus, LoadingStatus } = useLoadingStatus()
|
||||
|
||||
// State
|
||||
const [githubUsername, setGithubUsername] = useState(account.data.githubUsername || '')
|
||||
|
@ -30,18 +25,18 @@ export const GithubSettings = ({ title = false, welcome = false }) => {
|
|||
|
||||
// Helper method to save changes
|
||||
const save = async () => {
|
||||
startLoading()
|
||||
setLoadingStatus([true, 'processingUpdate'])
|
||||
const result = await backend.updateAccount({ data: { githubUsername, githubEmail } })
|
||||
if (result.success) {
|
||||
setAccount(result.data.account)
|
||||
toast.for.settingsSaved()
|
||||
} else toast.for.backendError()
|
||||
stopLoading()
|
||||
setLoadingStatus([true, 'settingsSaved', true, true])
|
||||
} else setLoadingStatus([true, 'backendError', true, false])
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="max-w-xl">
|
||||
{title ? <h2 className="text-4xl">{t('githubTitle')}</h2> : null}
|
||||
<LoadingStatus />
|
||||
<h2 className="text-4xl">{t('githubTitle')}</h2>
|
||||
<label className="font-bold">{t('username')}</label>
|
||||
<div className="flex flex-row items-center mb-4">
|
||||
<input
|
||||
|
@ -63,7 +58,7 @@ export const GithubSettings = ({ title = false, welcome = false }) => {
|
|||
/>
|
||||
</div>
|
||||
<SaveSettingsButton btnProps={{ onClick: save }} />
|
||||
{!welcome && <BackToAccountButton loading={loading} />}
|
||||
<BackToAccountButton />
|
||||
<Popout note>
|
||||
<p className="text-sm font-bold">{t('githubWhy1')}</p>
|
||||
<p className="text-sm">{t('githubWhy2')}</p>
|
||||
|
|
|
@ -1,28 +1,27 @@
|
|||
// Dependencies
|
||||
import { useState, useContext, useCallback } from 'react'
|
||||
import { useState, useCallback } from 'react'
|
||||
import { useTranslation } from 'next-i18next'
|
||||
import { useDropzone } from 'react-dropzone'
|
||||
// Context
|
||||
import { LoadingContext } from 'shared/context/loading-context.mjs'
|
||||
import { cloudflareImageUrl } from 'shared/utils.mjs'
|
||||
// Hooks
|
||||
import { useAccount } from 'shared/hooks/use-account.mjs'
|
||||
import { useBackend } from 'shared/hooks/use-backend.mjs'
|
||||
import { useToast } from 'shared/hooks/use-toast.mjs'
|
||||
import { useLoadingStatus } from 'shared/hooks/use-loading-status.mjs'
|
||||
// Components
|
||||
import { Icons, welcomeSteps, BackToAccountButton } from './shared.mjs'
|
||||
import { ContinueButton } from 'shared/components/buttons/continue-button.mjs'
|
||||
import { SaveSettingsButton } from 'shared/components/buttons/save-settings-button.mjs'
|
||||
|
||||
export const ns = ['account', 'toast']
|
||||
export const ns = ['account', 'status']
|
||||
|
||||
export const ImgSettings = ({ title = false, welcome = false }) => {
|
||||
const { loading, startLoading, stopLoading } = useContext(LoadingContext)
|
||||
const { account, setAccount, token } = useAccount()
|
||||
const backend = useBackend(token)
|
||||
const toast = useToast()
|
||||
const { account, setAccount } = useAccount()
|
||||
const backend = useBackend()
|
||||
const { setLoadingStatus, LoadingStatus } = useLoadingStatus()
|
||||
const { t } = useTranslation(ns)
|
||||
|
||||
const [img, setImg] = useState(false)
|
||||
const [url, setUrl] = useState('')
|
||||
|
||||
const onDrop = useCallback((acceptedFiles) => {
|
||||
const reader = new FileReader()
|
||||
|
@ -35,23 +34,27 @@ export const ImgSettings = ({ title = false, welcome = false }) => {
|
|||
const { getRootProps, getInputProps } = useDropzone({ onDrop })
|
||||
|
||||
const save = async () => {
|
||||
startLoading()
|
||||
const result = await backend.updateAccount({ img })
|
||||
setLoadingStatus([true, 'processingUpdate'])
|
||||
const result = await backend.updateAccount({ img: url ? url : img })
|
||||
if (result.success) {
|
||||
setAccount(result.data.account)
|
||||
toast.for.settingsSaved()
|
||||
} else toast.for.backendError()
|
||||
stopLoading()
|
||||
setLoadingStatus([true, 'settingsSaved', true, true])
|
||||
} else setLoadingStatus([true, 'backendError', true, false])
|
||||
}
|
||||
|
||||
const nextHref = '/docs/guide'
|
||||
|
||||
return (
|
||||
<div className="max-w-xl">
|
||||
<LoadingStatus />
|
||||
{title ? <h2 className="text-4xl">{t('imgTitle')}</h2> : null}
|
||||
<div>
|
||||
{!welcome || img !== false ? (
|
||||
<img alt="img" src={img || account.img} className="shadow mb-4" />
|
||||
<img
|
||||
alt="img"
|
||||
src={img || cloudflareImageUrl({ id: `user-${account.ihash}`, variant: 'public' })}
|
||||
className="shadow mb-4"
|
||||
/>
|
||||
) : null}
|
||||
<div
|
||||
{...getRootProps()}
|
||||
|
@ -67,6 +70,16 @@ export const ImgSettings = ({ title = false, welcome = false }) => {
|
|||
{t('imgSelectImage')}
|
||||
</button>
|
||||
</div>
|
||||
<p className="hidden lg:block p-0 my-2 text-center">{t('or')}</p>
|
||||
<div className="flex flex-row items-center">
|
||||
<input
|
||||
type="url"
|
||||
className="input input-secondary w-full input-bordered"
|
||||
placeholder="Paste an image URL here"
|
||||
value={url}
|
||||
onChange={(evt) => setUrl(evt.target.value)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{welcome ? (
|
||||
|
@ -96,7 +109,7 @@ export const ImgSettings = ({ title = false, welcome = false }) => {
|
|||
) : (
|
||||
<>
|
||||
<SaveSettingsButton btnProps={{ onClick: save }} />
|
||||
<BackToAccountButton loading={loading} />
|
||||
<BackToAccountButton />
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
|
|
@ -1,26 +1,21 @@
|
|||
// Dependencies
|
||||
import { useState, useContext } from 'react'
|
||||
import { useState } from 'react'
|
||||
import { useTranslation } from 'next-i18next'
|
||||
// Hooks
|
||||
import { useAccount } from 'shared/hooks/use-account.mjs'
|
||||
import { useBackend } from 'shared/hooks/use-backend.mjs'
|
||||
import { useToast } from 'shared/hooks/use-toast.mjs'
|
||||
// Context
|
||||
import { LoadingContext } from 'shared/context/loading-context.mjs'
|
||||
import { useLoadingStatus } from 'shared/hooks/use-loading-status.mjs'
|
||||
// Components
|
||||
import { Choice, Icons, welcomeSteps, BackToAccountButton } from './shared.mjs'
|
||||
import { ContinueButton } from 'shared/components/buttons/continue-button.mjs'
|
||||
|
||||
export const ns = ['account', 'toast']
|
||||
export const ns = ['account', 'status']
|
||||
|
||||
export const ImperialSettings = ({ title = false, welcome = false }) => {
|
||||
// Context
|
||||
const { loading, startLoading, stopLoading } = useContext(LoadingContext)
|
||||
|
||||
// Hooks
|
||||
const { account, setAccount, token } = useAccount()
|
||||
const backend = useBackend(token)
|
||||
const toast = useToast()
|
||||
const { account, setAccount } = useAccount()
|
||||
const { setLoadingStatus, LoadingStatus } = useLoadingStatus()
|
||||
const backend = useBackend()
|
||||
const { t } = useTranslation(ns)
|
||||
|
||||
// State
|
||||
|
@ -29,14 +24,13 @@ export const ImperialSettings = ({ title = false, welcome = false }) => {
|
|||
// Helper method to update account
|
||||
const update = async (val) => {
|
||||
if (val !== selection) {
|
||||
startLoading()
|
||||
setLoadingStatus([true, 'processingUpdate'])
|
||||
const result = await backend.updateAccount({ imperial: val === 'imperial' ? true : false })
|
||||
if (result.success) {
|
||||
setAccount(result.data.account)
|
||||
setSelection(val)
|
||||
toast.for.settingsSaved()
|
||||
} else toast.for.backendError()
|
||||
stopLoading()
|
||||
setLoadingStatus([true, 'settingsSaved', true, true])
|
||||
} else setLoadingStatus([true, 'backendError', true, true])
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -48,6 +42,7 @@ export const ImperialSettings = ({ title = false, welcome = false }) => {
|
|||
|
||||
return (
|
||||
<div className="max-w-xl">
|
||||
<LoadingStatus />
|
||||
{title ? <h1 className="text-4xl">{t('unitsTitle')}</h1> : <h1></h1>}
|
||||
{['metric', 'imperial'].map((val) => (
|
||||
<Choice
|
||||
|
@ -87,7 +82,7 @@ export const ImperialSettings = ({ title = false, welcome = false }) => {
|
|||
) : null}
|
||||
</>
|
||||
) : (
|
||||
<BackToAccountButton loading={loading} />
|
||||
<BackToAccountButton />
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
|
|
|
@ -1,27 +1,22 @@
|
|||
// Dependencies
|
||||
import { useState, useContext } from 'react'
|
||||
import { useState } from 'react'
|
||||
import { useTranslation } from 'next-i18next'
|
||||
// Hooks
|
||||
import { useAccount } from 'shared/hooks/use-account.mjs'
|
||||
import { useBackend } from 'shared/hooks/use-backend.mjs'
|
||||
import { useToast } from 'shared/hooks/use-toast.mjs'
|
||||
// Context
|
||||
import { LoadingContext } from 'shared/context/loading-context.mjs'
|
||||
import { useLoadingStatus, ns as statusNs } from 'shared/hooks/use-loading-status.mjs'
|
||||
// Components
|
||||
import { BackToAccountButton, Choice } from './shared.mjs'
|
||||
// Config
|
||||
import { siteConfig as conf } from 'site/site.config.mjs'
|
||||
|
||||
export const ns = ['account', 'locales', 'toast']
|
||||
export const ns = ['account', 'locales', statusNs]
|
||||
|
||||
export const LanguageSettings = ({ title = false }) => {
|
||||
// Context
|
||||
const { loading, startLoading, stopLoading } = useContext(LoadingContext)
|
||||
|
||||
// Hooks
|
||||
const { account, setAccount, token } = useAccount()
|
||||
const backend = useBackend(token)
|
||||
const toast = useToast()
|
||||
const { account, setAccount } = useAccount()
|
||||
const { setLoadingStatus, LoadingStatus } = useLoadingStatus()
|
||||
const backend = useBackend()
|
||||
const { t } = useTranslation(ns)
|
||||
|
||||
// State
|
||||
|
@ -30,26 +25,26 @@ export const LanguageSettings = ({ title = false }) => {
|
|||
// Helper method to update the account
|
||||
const update = async (lang) => {
|
||||
if (lang !== language) {
|
||||
startLoading()
|
||||
setLoadingStatus([true, 'processingUpdate'])
|
||||
setLanguage(lang)
|
||||
const result = await backend.updateAccount({ language: lang })
|
||||
if (result.success) {
|
||||
setAccount(result.data.account)
|
||||
toast.for.settingsSaved()
|
||||
} else toast.for.backendError()
|
||||
stopLoading()
|
||||
setLoadingStatus([true, 'settingsSaved', true, true])
|
||||
} else setLoadingStatus([true, 'backendError', true, true])
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="max-w-xl">
|
||||
<LoadingStatus />
|
||||
{title ? <h2 className="text-4xl">{t('languageTitle')}</h2> : null}
|
||||
{conf.languages.map((val) => (
|
||||
<Choice val={val} t={t} update={update} current={language} key={val}>
|
||||
<span className="block text-lg leading-5">{t(`locales:${val}`)}</span>
|
||||
</Choice>
|
||||
))}
|
||||
<BackToAccountButton loading={loading} />
|
||||
<BackToAccountButton />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -1,93 +1,260 @@
|
|||
import { useState, useEffect } from 'react'
|
||||
import { useAccount } from 'shared/hooks/use-account.mjs'
|
||||
import { useBackend } from 'shared/hooks/use-backend.mjs'
|
||||
import { useTranslation } from 'next-i18next'
|
||||
import Link from 'next/link'
|
||||
import { PageLink } from 'shared/components/page-link.mjs'
|
||||
import { freeSewingConfig as conf } from 'shared/config/freesewing.config.mjs'
|
||||
import { Fingerprint } from 'shared/components/fingerprint.mjs'
|
||||
import {
|
||||
DesignIcon,
|
||||
MeasieIcon,
|
||||
SignoutIcon,
|
||||
UserIcon,
|
||||
UnitsIcon,
|
||||
I18nIcon,
|
||||
ShowcaseIcon,
|
||||
ChatIcon,
|
||||
EmailIcon,
|
||||
KeyIcon,
|
||||
BookmarkIcon,
|
||||
CompareIcon,
|
||||
PrivacyIcon,
|
||||
ControlIcon,
|
||||
LockIcon,
|
||||
NewsletterIcon,
|
||||
ShieldIcon,
|
||||
FingerprintIcon,
|
||||
GitHubIcon,
|
||||
InstagramIcon,
|
||||
MastodonIcon,
|
||||
TwitterIcon,
|
||||
TwitchIcon,
|
||||
TikTokIcon,
|
||||
LinkIcon,
|
||||
TrashIcon,
|
||||
RedditIcon,
|
||||
ExportIcon,
|
||||
CloseIcon,
|
||||
ReloadIcon,
|
||||
OkIcon,
|
||||
NoIcon,
|
||||
} from 'shared/components/icons.mjs'
|
||||
import { cloudflareImageUrl, capitalize } from 'shared/utils.mjs'
|
||||
import { ControlScore } from 'shared/components/control/score.mjs'
|
||||
|
||||
export const ns = ['account']
|
||||
export const ns = ['account', 'i18n']
|
||||
|
||||
const Li = ({ children }) => <li className="inline">{children}</li>
|
||||
const Spacer = () => <li className="inline px-1 opacity-60"> | </li>
|
||||
|
||||
const LinkList = ({ items, t, control, first = false }) => {
|
||||
const output = []
|
||||
if (first)
|
||||
output.push(
|
||||
<li key="first" className="inline pr-2">
|
||||
<b>{first}:</b>
|
||||
</li>
|
||||
)
|
||||
for (const [item, cscore] of Object.entries(items)) {
|
||||
if (cscore <= control)
|
||||
output.push(
|
||||
<Li key={`${item}-li`}>
|
||||
<PageLink href={`/account/${item}`} txt={t(item)} className="capitalize" />
|
||||
</Li>,
|
||||
<Spacer key={`${item}-spacer`} />
|
||||
)
|
||||
}
|
||||
|
||||
return output.length > 1 ? <ul className="mt-4">{output.slice(0, -1)}</ul> : null
|
||||
const itemIcons = {
|
||||
bookmarks: <BookmarkIcon />,
|
||||
sets: <MeasieIcon />,
|
||||
patterns: <DesignIcon />,
|
||||
apikeys: <KeyIcon />,
|
||||
username: <UserIcon />,
|
||||
email: <EmailIcon />,
|
||||
bio: <ChatIcon />,
|
||||
img: <ShowcaseIcon />,
|
||||
language: <I18nIcon />,
|
||||
units: <UnitsIcon />,
|
||||
compare: <CompareIcon />,
|
||||
consent: <PrivacyIcon />,
|
||||
control: <ControlIcon />,
|
||||
mfa: <ShieldIcon />,
|
||||
newsletter: <NewsletterIcon />,
|
||||
password: <LockIcon />,
|
||||
github: <GitHubIcon />,
|
||||
instagram: <InstagramIcon />,
|
||||
mastodon: <MastodonIcon />,
|
||||
twitter: <TwitterIcon />,
|
||||
twitch: <TwitchIcon />,
|
||||
tiktok: <TikTokIcon />,
|
||||
website: <LinkIcon />,
|
||||
reddit: <RedditIcon />,
|
||||
}
|
||||
|
||||
const actions = {
|
||||
reload: 4,
|
||||
export: 3,
|
||||
restrict: 4,
|
||||
disable: 4,
|
||||
remove: 2,
|
||||
}
|
||||
const itemClasses = 'flex flex-row items-center justify-between bg-opacity-10 p-2 px-4 rounded mb-1'
|
||||
|
||||
const AccountLink = ({ href, title, children, color = 'secondary' }) => (
|
||||
<Link
|
||||
className={`${itemClasses} bg-${color} hover:bg-opacity-100 hover:text-neutral-content`}
|
||||
href={href}
|
||||
title={title}
|
||||
>
|
||||
{children}
|
||||
</Link>
|
||||
)
|
||||
|
||||
const YesNo = ({ check }) =>
|
||||
check ? (
|
||||
<OkIcon className="text-success w-6 h-6" stroke={4} />
|
||||
) : (
|
||||
<NoIcon className="text-error w-6 h-6" stroke={3} />
|
||||
)
|
||||
|
||||
export const AccountLinks = () => {
|
||||
const { account, signOut } = useAccount()
|
||||
const { t } = useTranslation(ns)
|
||||
const backend = useBackend()
|
||||
|
||||
const lprops = { t, control: account.control }
|
||||
const [bookmarks, setBookmarks] = useState([])
|
||||
const [sets, setSets] = useState([])
|
||||
const [patterns, setPatterns] = useState([])
|
||||
const [apikeys, setApikeys] = useState([])
|
||||
|
||||
useEffect(() => {
|
||||
const getUserData = async () => {
|
||||
const result = await backend.getUserData(account.id)
|
||||
if (result.success) {
|
||||
setApikeys(result.data.data.apikeys)
|
||||
setBookmarks(result.data.data.bookmarks)
|
||||
setPatterns(result.data.data.patterns)
|
||||
setSets(result.data.data.sets)
|
||||
}
|
||||
}
|
||||
getUserData()
|
||||
}, [account.id])
|
||||
|
||||
const btnClasses = 'btn capitalize flex flex-row justify-between'
|
||||
|
||||
const itemPreviews = {
|
||||
apikeys: apikeys?.length || 0,
|
||||
bookmarks: bookmarks?.length || 0,
|
||||
sets: sets?.length || 0,
|
||||
patterns: patterns?.length || 0,
|
||||
username: account.username,
|
||||
email: account.email,
|
||||
bio: <span>{account.bio.slice(0, 15)}…</span>,
|
||||
img: (
|
||||
<img
|
||||
src={cloudflareImageUrl({ type: 'sq100', id: `user-${account.ihash}` })}
|
||||
className="w-8 h-8 aspect-square rounded-full shadow"
|
||||
/>
|
||||
),
|
||||
language: t(`i18n:${account.language}`),
|
||||
units: t(account.imperial ? 'imperialUnits' : 'metricUnits'),
|
||||
newsletter: <YesNo check={account.newsletter} />,
|
||||
compare: <YesNo check={account.compare} />,
|
||||
consent: <YesNo check={account.consent} />,
|
||||
control: <ControlScore control={account.control} />,
|
||||
github: account.data.githubUsername || account.data.githubEmail || <NoIcon />,
|
||||
password:
|
||||
account.passwordType === 'v3' ? (
|
||||
<OkIcon className="text-success w-6 h-6" stroke={4} />
|
||||
) : (
|
||||
<NoIcon />
|
||||
),
|
||||
mfa: <YesNo check={false} />,
|
||||
}
|
||||
|
||||
for (const social of Object.keys(conf.account.fields.identities).filter((i) => i !== 'github'))
|
||||
itemPreviews[social] = account.data[social] || (
|
||||
<NoIcon className="text-base-content w-6 h-6" stroke={2} />
|
||||
)
|
||||
|
||||
return (
|
||||
<div className="w-full max-w-md">
|
||||
<Link className="btn btn-primary mb-2 w-full capitalize" href="/create">
|
||||
{t('newPattern')}
|
||||
</Link>
|
||||
<div className="flex flex-row gap-2">
|
||||
<Link className="btn btn-secondary grow capitalize" href="/account/sets">
|
||||
{t('newSet')}
|
||||
<div className="w-full max-w-7xl">
|
||||
<div className="grid grid-cols-1 xl:grid-cols-2 gap-4 mb-8">
|
||||
<div className="">
|
||||
<h4 className="my-2">{t('data')}</h4>
|
||||
{Object.keys(conf.account.fields.data).map((item) => (
|
||||
<AccountLink href={`/account/${item}`} title={t(item)} key={item}>
|
||||
<div className="flex flex-row items-center gap-3 font-medium">
|
||||
{itemIcons[item]}
|
||||
{t(`your${capitalize(item)}`)}
|
||||
</div>
|
||||
<div className="">{itemPreviews[item]}</div>
|
||||
</AccountLink>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="">
|
||||
<h4 className="my-2">{t('info')}</h4>
|
||||
{Object.keys(conf.account.fields.info).map((item) => (
|
||||
<AccountLink href={`/account/${item}`} title={t(item)} key={item}>
|
||||
<div className="flex flex-row items-center gap-3 font-medium">
|
||||
{itemIcons[item]}
|
||||
{t(item)}
|
||||
</div>
|
||||
<div className="">{itemPreviews[item]}</div>
|
||||
</AccountLink>
|
||||
))}
|
||||
<div className={`${itemClasses} bg-neutral`}>
|
||||
<div className="flex flex-row items-center gap-3 font-medium">
|
||||
<FingerprintIcon />
|
||||
<span>{t('userId')}</span>
|
||||
</div>
|
||||
<div className="">{account.id}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="">
|
||||
<h4 className="my-2">{t('settings')}</h4>
|
||||
{Object.keys(conf.account.fields.settings).map((item) => (
|
||||
<AccountLink href={`/account/${item}`} title={t(item)} key={item}>
|
||||
<div className="flex flex-row items-center gap-3 font-medium">
|
||||
{itemIcons[item]}
|
||||
{t(item)}
|
||||
</div>
|
||||
<div className="">{itemPreviews[item]}</div>
|
||||
</AccountLink>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="">
|
||||
<h4 className="my-2">{t('linkedIdentities')}</h4>
|
||||
{Object.keys(conf.account.fields.identities).map((item) => (
|
||||
<AccountLink href={`/account/${item}`} title={t(item)} key={item}>
|
||||
<div className="flex flex-row items-center gap-3 font-medium">
|
||||
{itemIcons[item]}
|
||||
{t(item)}
|
||||
</div>
|
||||
<div className="">{itemPreviews[item]}</div>
|
||||
</AccountLink>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="">
|
||||
<h4 className="my-2">{t('security')}</h4>
|
||||
{Object.keys(conf.account.fields.security).map((item) => (
|
||||
<AccountLink href={`/account/${item}`} title={t(item)} key={item}>
|
||||
<div className="flex flex-row items-center gap-3 font-medium">
|
||||
{itemIcons[item]}
|
||||
{t(item)}
|
||||
</div>
|
||||
<div className="">{itemPreviews[item]}</div>
|
||||
</AccountLink>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="">
|
||||
<h4 className="my-2">{t('actions')}</h4>
|
||||
<AccountLink href={`/account/reload`} title={t('reload')}>
|
||||
<ReloadIcon />
|
||||
{t('reload')}
|
||||
</AccountLink>
|
||||
<AccountLink href={`/account/export`} title={t('export')}>
|
||||
<ExportIcon />
|
||||
{t('export')}
|
||||
</AccountLink>
|
||||
<AccountLink href={`/account/restrict`} title={t('restrict')} color="warning">
|
||||
<CloseIcon />
|
||||
{t('restrict')}
|
||||
</AccountLink>
|
||||
<AccountLink href={`/account/remove`} title={t('remove')} color="error">
|
||||
<TrashIcon />
|
||||
{t('remove')}
|
||||
</AccountLink>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-row flex-wrap gap-2 md:gap-4 justify-end">
|
||||
<Link className={`${btnClasses} btn-primary md:w-64 w-full`} href="/profile">
|
||||
<UserIcon />
|
||||
{t('yourProfile')}
|
||||
</Link>
|
||||
<button className="btn btn-warning btnoutline mb-2 capitalize" onClick={() => signOut()}>
|
||||
<button className={`${btnClasses} btn-warning md:w-64 w-full`} onClick={() => signOut()}>
|
||||
<SignoutIcon />
|
||||
{t('signOut')}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<ul className="mt-8">
|
||||
<li className="inline pr-2">
|
||||
<b>Quick links:</b>
|
||||
</li>
|
||||
<Li>
|
||||
<PageLink href="/profile" txt={t('yourProfile')} />{' '}
|
||||
</Li>
|
||||
<Spacer />
|
||||
<Li>
|
||||
<PageLink href="/account/patterns" txt={t('yourPatterns')} />{' '}
|
||||
</Li>
|
||||
<Spacer />
|
||||
<Li>
|
||||
<PageLink href="/account/sets" txt={t('yourSets')} />{' '}
|
||||
</Li>
|
||||
</ul>
|
||||
|
||||
{Object.keys(conf.account.fields).map((section) => (
|
||||
<LinkList
|
||||
key={section}
|
||||
items={conf.account.fields[section]}
|
||||
first={t(section)}
|
||||
{...lprops}
|
||||
/>
|
||||
))}
|
||||
|
||||
<LinkList items={actions} first={t('actions')} {...lprops} />
|
||||
|
||||
<Fingerprint id={account.id} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -1,12 +1,10 @@
|
|||
// Dependencies
|
||||
import { useState, useContext } from 'react'
|
||||
import { useState } from 'react'
|
||||
import { useTranslation } from 'next-i18next'
|
||||
// Hooks
|
||||
import { useAccount } from 'shared/hooks/use-account.mjs'
|
||||
import { useBackend } from 'shared/hooks/use-backend.mjs'
|
||||
import { useToast } from 'shared/hooks/use-toast.mjs'
|
||||
// Context
|
||||
import { LoadingContext } from 'shared/context/loading-context.mjs'
|
||||
import { useLoadingStatus } from 'shared/hooks/use-loading-status.mjs'
|
||||
// Components
|
||||
import { BackToAccountButton } from './shared.mjs'
|
||||
import { Popout } from 'shared/components/popout/index.mjs'
|
||||
|
@ -25,14 +23,11 @@ const CodeInput = ({ code, setCode, t }) => (
|
|||
)
|
||||
|
||||
export const MfaSettings = ({ title = false, welcome = false }) => {
|
||||
// Context
|
||||
const { loading, startLoading, stopLoading } = useContext(LoadingContext)
|
||||
|
||||
// Hooks
|
||||
const { account, setAccount, token } = useAccount()
|
||||
const backend = useBackend(token)
|
||||
const { account, setAccount } = useAccount()
|
||||
const backend = useBackend()
|
||||
const { t } = useTranslation(ns)
|
||||
const toast = useToast()
|
||||
const { setLoadingStatus, LoadingStatus } = useLoadingStatus()
|
||||
|
||||
// State
|
||||
const [enable, setEnable] = useState(false)
|
||||
|
@ -42,15 +37,17 @@ export const MfaSettings = ({ title = false, welcome = false }) => {
|
|||
|
||||
// Helper method to enable MFA
|
||||
const enableMfa = async () => {
|
||||
startLoading()
|
||||
setLoadingStatus([true, 'processingUpdate'])
|
||||
const result = await backend.enableMfa()
|
||||
if (result.success) setEnable(result.data.mfa)
|
||||
stopLoading()
|
||||
if (result.success) {
|
||||
setEnable(result.data.mfa)
|
||||
setLoadingStatus([true, 'settingsSaved', true, true])
|
||||
} else setLoadingStatus([true, 'backendError', true, false])
|
||||
}
|
||||
|
||||
// Helper method to disable MFA
|
||||
const disableMfa = async () => {
|
||||
startLoading()
|
||||
setLoadingStatus([true, 'processingUpdate'])
|
||||
const result = await backend.disableMfa({
|
||||
mfa: false,
|
||||
password,
|
||||
|
@ -59,19 +56,18 @@ export const MfaSettings = ({ title = false, welcome = false }) => {
|
|||
if (result) {
|
||||
if (result.success) {
|
||||
setAccount(result.data.account)
|
||||
toast.warning(<span>{t('mfaDisabled')}</span>)
|
||||
} else toast.for.backendError()
|
||||
setLoadingStatus([true, 'settingsSaved', true, true])
|
||||
} else setLoadingStatus([true, 'backendError', true, false])
|
||||
setDisable(false)
|
||||
setEnable(false)
|
||||
setCode('')
|
||||
setPassword('')
|
||||
}
|
||||
stopLoading()
|
||||
}
|
||||
|
||||
// Helper method to confirm MFA
|
||||
const confirmMfa = async () => {
|
||||
startLoading()
|
||||
setLoadingStatus([true, 'processingUpdate'])
|
||||
const result = await backend.confirmMfa({
|
||||
mfa: true,
|
||||
secret: enable.secret,
|
||||
|
@ -79,11 +75,10 @@ export const MfaSettings = ({ title = false, welcome = false }) => {
|
|||
})
|
||||
if (result.success) {
|
||||
setAccount(result.data.account)
|
||||
toast.success(<span>{t('mfaEnabled')}</span>)
|
||||
} else toast.for.backendError()
|
||||
setLoadingStatus([true, 'settingsSaved', true, true])
|
||||
} else setLoadingStatus([true, 'backendError', true, false])
|
||||
setEnable(false)
|
||||
setCode('')
|
||||
stopLoading()
|
||||
}
|
||||
|
||||
// Figure out what title to use
|
||||
|
@ -92,6 +87,7 @@ export const MfaSettings = ({ title = false, welcome = false }) => {
|
|||
|
||||
return (
|
||||
<div className="max-w-xl">
|
||||
<LoadingStatus />
|
||||
{title ? <h2 className="text-4xl">{titleText}</h2> : null}
|
||||
{enable ? (
|
||||
<>
|
||||
|
@ -156,7 +152,7 @@ export const MfaSettings = ({ title = false, welcome = false }) => {
|
|||
</div>
|
||||
)}
|
||||
</div>
|
||||
{!welcome && <BackToAccountButton loading={loading} />}
|
||||
{!welcome && <BackToAccountButton />}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -1,42 +1,35 @@
|
|||
// Dependencies
|
||||
import { useState, useContext } from 'react'
|
||||
import { useState } from 'react'
|
||||
import { useTranslation } from 'next-i18next'
|
||||
// Hooks
|
||||
import { useAccount } from 'shared/hooks/use-account.mjs'
|
||||
import { useBackend } from 'shared/hooks/use-backend.mjs'
|
||||
import { useToast } from 'shared/hooks/use-toast.mjs'
|
||||
// Context
|
||||
import { LoadingContext } from 'shared/context/loading-context.mjs'
|
||||
import { useLoadingStatus } from 'shared/hooks/use-loading-status.mjs'
|
||||
// Components
|
||||
import { BackToAccountButton, Choice, Icons, welcomeSteps } from './shared.mjs'
|
||||
import { ContinueButton } from 'shared/components/buttons/continue-button.mjs'
|
||||
|
||||
export const ns = ['account', 'toast']
|
||||
export const ns = ['account', 'status']
|
||||
|
||||
export const NewsletterSettings = ({ title = false, welcome = false }) => {
|
||||
// Context
|
||||
const { loading, startLoading, stopLoading } = useContext(LoadingContext)
|
||||
|
||||
// Hooks
|
||||
const { account, setAccount, token } = useAccount()
|
||||
const backend = useBackend(token)
|
||||
const toast = useToast()
|
||||
const { account, setAccount } = useAccount()
|
||||
const backend = useBackend()
|
||||
const { t } = useTranslation(ns)
|
||||
|
||||
const { LoadingStatus, setLoadingStatus } = useLoadingStatus()
|
||||
// State
|
||||
const [selection, setSelection] = useState(account?.newsletter ? 'yes' : 'no')
|
||||
|
||||
// Helper method to update account
|
||||
const update = async (val) => {
|
||||
if (val !== selection) {
|
||||
startLoading()
|
||||
setLoadingStatus([true, 'processingUpdate'])
|
||||
const result = await backend.updateAccount({ newsletter: val === 'yes' ? true : false })
|
||||
if (result.success) {
|
||||
setAccount(result.data.account)
|
||||
setSelection(val)
|
||||
toast.for.settingsSaved()
|
||||
} else toast.for.backendError()
|
||||
stopLoading()
|
||||
setLoadingStatus([true, 'settingsSaved', true, true])
|
||||
} else setLoadingStatus([true, 'backendError', true, true])
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -48,6 +41,7 @@ export const NewsletterSettings = ({ title = false, welcome = false }) => {
|
|||
|
||||
return (
|
||||
<div className="max-w-xl">
|
||||
<LoadingStatus />
|
||||
{title ? <h1 className="text-4xl">{t('newsletterTitle')}</h1> : null}
|
||||
{['yes', 'no'].map((val) => (
|
||||
<Choice val={val} t={t} update={update} current={selection} bool key={val}>
|
||||
|
@ -83,7 +77,7 @@ export const NewsletterSettings = ({ title = false, welcome = false }) => {
|
|||
) : null}
|
||||
</>
|
||||
) : (
|
||||
<BackToAccountButton loading={loading} />
|
||||
<BackToAccountButton />
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
|
|
|
@ -1,12 +1,10 @@
|
|||
// Dependencies
|
||||
import { useState, useContext } from 'react'
|
||||
import { useState } from 'react'
|
||||
import { useTranslation } from 'next-i18next'
|
||||
// Hooks
|
||||
import { useAccount } from 'shared/hooks/use-account.mjs'
|
||||
import { useBackend } from 'shared/hooks/use-backend.mjs'
|
||||
import { useToast } from 'shared/hooks/use-toast.mjs'
|
||||
// Context
|
||||
import { LoadingContext } from 'shared/context/loading-context.mjs'
|
||||
import { useLoadingStatus } from 'shared/hooks/use-loading-status.mjs'
|
||||
// Components
|
||||
import Link from 'next/link'
|
||||
import { BackToAccountButton } from './shared.mjs'
|
||||
|
@ -14,17 +12,14 @@ import { SaveSettingsButton } from 'shared/components/buttons/save-settings-butt
|
|||
import { Popout } from 'shared/components/popout/index.mjs'
|
||||
import { RightIcon } from 'shared/components/icons.mjs'
|
||||
|
||||
export const ns = ['account', 'toast']
|
||||
export const ns = ['account', 'status']
|
||||
|
||||
export const PasswordSettings = ({ title = false, welcome = false }) => {
|
||||
// Context
|
||||
const { loading, startLoading, stopLoading } = useContext(LoadingContext)
|
||||
|
||||
// Hooks
|
||||
const { account, setAccount, token } = useAccount()
|
||||
const backend = useBackend(token)
|
||||
const { account, setAccount } = useAccount()
|
||||
const backend = useBackend()
|
||||
const { t } = useTranslation(ns)
|
||||
const toast = useToast()
|
||||
const { setLoadingStatus, LoadingStatus } = useLoadingStatus()
|
||||
|
||||
// State
|
||||
const [password, setPassword] = useState('')
|
||||
|
@ -32,17 +27,17 @@ export const PasswordSettings = ({ title = false, welcome = false }) => {
|
|||
|
||||
// Helper method to save password to account
|
||||
const save = async () => {
|
||||
startLoading()
|
||||
setLoadingStatus([true, 'processingUpdate'])
|
||||
const result = await backend.updateAccount({ password })
|
||||
if (result.success) {
|
||||
setAccount(result.data.account)
|
||||
toast.for.settingsSaved()
|
||||
} else toast.for.backendError()
|
||||
stopLoading()
|
||||
setLoadingStatus([true, 'settingsSaved', true, true])
|
||||
} else setLoadingStatus([true, 'backendError', true, false])
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="max-w-xl">
|
||||
<LoadingStatus />
|
||||
{title ? <h2 className="text-4xl">{t('passwordTitle')}</h2> : null}
|
||||
<div className="flex flex-row items-center mt-4 gap-2">
|
||||
<input
|
||||
|
@ -62,7 +57,7 @@ export const PasswordSettings = ({ title = false, welcome = false }) => {
|
|||
</button>
|
||||
</div>
|
||||
<SaveSettingsButton btnProps={{ onClick: save, disabled: password.length < 4 }} />
|
||||
{!welcome && <BackToAccountButton loading={loading} />}
|
||||
{!welcome && <BackToAccountButton />}
|
||||
{!account.mfaEnabled && (
|
||||
<Popout tip>
|
||||
<h5>{t('mfaTipTitle')}</h5>
|
||||
|
|
|
@ -37,8 +37,8 @@ export const ns = ['account', 'patterns', 'toast']
|
|||
export const StandAloneNewSet = () => {
|
||||
const { t } = useTranslation(['account'])
|
||||
const toast = useToast()
|
||||
const { account, token } = useAccount()
|
||||
const backend = useBackend(token)
|
||||
const { account } = useAccount()
|
||||
const backend = useBackend()
|
||||
|
||||
return (
|
||||
<div className="max-w-xl">
|
||||
|
@ -571,8 +571,8 @@ const Pattern = ({ pattern, t, account, backend, refresh }) => {
|
|||
// Component for the account/patterns page
|
||||
export const Patterns = ({ standAlone = false }) => {
|
||||
// Hooks
|
||||
const { account, token } = useAccount()
|
||||
const backend = useBackend(token)
|
||||
const { account } = useAccount()
|
||||
const backend = useBackend()
|
||||
const { t } = useTranslation(ns)
|
||||
|
||||
// State
|
||||
|
|
61
sites/shared/components/account/platform.mjs
Normal file
61
sites/shared/components/account/platform.mjs
Normal file
|
@ -0,0 +1,61 @@
|
|||
// Dependencies
|
||||
import { useState } from 'react'
|
||||
import { useTranslation } from 'next-i18next'
|
||||
// Hooks
|
||||
import { useAccount } from 'shared/hooks/use-account.mjs'
|
||||
import { useBackend } from 'shared/hooks/use-backend.mjs'
|
||||
import { useLoadingStatus } from 'shared/hooks/use-loading-status.mjs'
|
||||
// Components
|
||||
import { BackToAccountButton } from './shared.mjs'
|
||||
import { SaveSettingsButton } from 'shared/components/buttons/save-settings-button.mjs'
|
||||
import { Popout } from 'shared/components/popout/index.mjs'
|
||||
|
||||
export const ns = ['account', 'status']
|
||||
|
||||
export const PlatformSettings = ({ platform }) => {
|
||||
// Hooks
|
||||
const { account, setAccount } = useAccount()
|
||||
const backend = useBackend()
|
||||
const { t } = useTranslation(ns)
|
||||
const { setLoadingStatus, LoadingStatus } = useLoadingStatus()
|
||||
|
||||
// State
|
||||
const [platformId, setPlatformId] = useState(account.data[platform] || '')
|
||||
|
||||
// Helper method to save changes
|
||||
const save = async () => {
|
||||
setLoadingStatus([true, 'processingUpdate'])
|
||||
const data = { data: {} }
|
||||
data.data[platform] = platformId
|
||||
const result = await backend.updateAccount(data)
|
||||
if (result.success) {
|
||||
setAccount(result.data.account)
|
||||
setLoadingStatus([true, 'settingsSaved', true, true])
|
||||
} else setLoadingStatus([true, 'backendError', true, false])
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="max-w-xl">
|
||||
<LoadingStatus />
|
||||
<h2 className="text-4xl">
|
||||
{t(platform === 'website' ? 'account:websiteTitle' : 'account:platformTitle', {
|
||||
platform: platform,
|
||||
})}
|
||||
</h2>
|
||||
<div className="flex flex-row items-center mb-4">
|
||||
<input
|
||||
value={platformId}
|
||||
onChange={(evt) => setPlatformId(evt.target.value)}
|
||||
className="input w-full input-bordered flex flex-row"
|
||||
type="text"
|
||||
placeholder={account[platform]}
|
||||
/>
|
||||
</div>
|
||||
<SaveSettingsButton btnProps={{ onClick: save }} />
|
||||
<BackToAccountButton />
|
||||
<Popout note>
|
||||
<p className="text-sm font-bold">{t('platformWhy')}</p>
|
||||
</Popout>
|
||||
</div>
|
||||
)
|
||||
}
|
|
@ -1,46 +1,40 @@
|
|||
// Dependencies
|
||||
import { useTranslation } from 'next-i18next'
|
||||
// Hooks
|
||||
import { useContext } from 'react'
|
||||
import { useAccount } from 'shared/hooks/use-account.mjs'
|
||||
import { useBackend } from 'shared/hooks/use-backend.mjs'
|
||||
import { useToast } from 'shared/hooks/use-toast.mjs'
|
||||
// Context
|
||||
import { LoadingContext } from 'shared/context/loading-context.mjs'
|
||||
import { useLoadingStatus } from 'shared/hooks/use-loading-status.mjs'
|
||||
// Components
|
||||
import { BackToAccountButton } from './shared.mjs'
|
||||
|
||||
export const ns = ['account', 'toast']
|
||||
export const ns = ['account', 'status']
|
||||
|
||||
export const ReloadAccount = ({ title = false }) => {
|
||||
// Context
|
||||
const { loading, startLoading, stopLoading } = useContext(LoadingContext)
|
||||
|
||||
// Hooks
|
||||
const { setAccount, token } = useAccount()
|
||||
const backend = useBackend(token)
|
||||
const { setAccount } = useAccount()
|
||||
const backend = useBackend()
|
||||
const { t } = useTranslation(ns)
|
||||
const toast = useToast()
|
||||
const { setLoadingStatus, LoadingStatus } = useLoadingStatus()
|
||||
|
||||
// Helper method to reload account
|
||||
const reload = async () => {
|
||||
startLoading()
|
||||
setLoadingStatus([true, 'processingUpdate'])
|
||||
const result = await backend.reloadAccount()
|
||||
if (result.success) {
|
||||
setAccount(result.data.account)
|
||||
toast.success(<span>{t('nailedIt')}</span>)
|
||||
} else toast.for.backendError()
|
||||
stopLoading()
|
||||
setLoadingStatus([true, 'nailedIt', true, true])
|
||||
} else setLoadingStatus([true, 'backendError', true, false])
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="max-w-xl">
|
||||
<LoadingStatus />
|
||||
{title ? <h2>{t('reloadMsg1')}</h2> : null}
|
||||
<p>{t('reloadMsg2')}</p>
|
||||
<button className="btn btn-primary capitalize w-full my-2" onClick={reload}>
|
||||
{t('reload')}
|
||||
</button>
|
||||
<BackToAccountButton loading={loading} />
|
||||
<BackToAccountButton />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
42
sites/shared/components/account/remove.mjs
Normal file
42
sites/shared/components/account/remove.mjs
Normal file
|
@ -0,0 +1,42 @@
|
|||
// Dependencies
|
||||
import { useTranslation } from 'next-i18next'
|
||||
// Hooks
|
||||
import { useAccount } from 'shared/hooks/use-account.mjs'
|
||||
import { useBackend } from 'shared/hooks/use-backend.mjs'
|
||||
import { useLoadingStatus } from 'shared/hooks/use-loading-status.mjs'
|
||||
// Components
|
||||
import { BackToAccountButton } from './shared.mjs'
|
||||
import { Popout } from 'shared/components/popout/index.mjs'
|
||||
|
||||
export const ns = ['account', 'status']
|
||||
|
||||
export const RemoveAccount = () => {
|
||||
// Hooks
|
||||
const { signOut } = useAccount()
|
||||
const backend = useBackend()
|
||||
const { t } = useTranslation(ns)
|
||||
const { setLoadingStatus, LoadingStatus } = useLoadingStatus()
|
||||
|
||||
// Helper method to export account
|
||||
const removeAccount = async () => {
|
||||
setLoadingStatus([true, 'processingUpdate'])
|
||||
const result = await backend.removeAccount()
|
||||
if (result.success) {
|
||||
setLoadingStatus([true, 'nailedIt', true, true])
|
||||
signOut()
|
||||
} else setLoadingStatus([true, 'backendError', true, false])
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="max-w-xl">
|
||||
<LoadingStatus />
|
||||
<Popout warning>
|
||||
<h3>{t('noWayBack')}</h3>
|
||||
<button className="btn btn-error capitalize w-full my-2" onClick={removeAccount}>
|
||||
{t('remove')}
|
||||
</button>
|
||||
</Popout>
|
||||
<BackToAccountButton />
|
||||
</div>
|
||||
)
|
||||
}
|
43
sites/shared/components/account/restrict.mjs
Normal file
43
sites/shared/components/account/restrict.mjs
Normal file
|
@ -0,0 +1,43 @@
|
|||
// Dependencies
|
||||
import { useTranslation } from 'next-i18next'
|
||||
// Hooks
|
||||
import { useAccount } from 'shared/hooks/use-account.mjs'
|
||||
import { useBackend } from 'shared/hooks/use-backend.mjs'
|
||||
import { useLoadingStatus } from 'shared/hooks/use-loading-status.mjs'
|
||||
// Components
|
||||
import { BackToAccountButton } from './shared.mjs'
|
||||
import { Popout } from 'shared/components/popout/index.mjs'
|
||||
|
||||
export const ns = ['account', 'status']
|
||||
|
||||
export const RestrictAccount = () => {
|
||||
// Hooks
|
||||
const { signOut } = useAccount()
|
||||
const backend = useBackend()
|
||||
const { t } = useTranslation(ns)
|
||||
const { setLoadingStatus, LoadingStatus } = useLoadingStatus()
|
||||
|
||||
// Helper method to export account
|
||||
const restrictAccount = async () => {
|
||||
setLoadingStatus([true, 'processingUpdate'])
|
||||
const result = await backend.restrictAccount()
|
||||
if (result.success) {
|
||||
setLoadingStatus([true, 'nailedIt', true, true])
|
||||
signOut()
|
||||
} else setLoadingStatus([true, 'backendError', true, false])
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="max-w-xl">
|
||||
<LoadingStatus />
|
||||
<Popout warning>
|
||||
<h5>{t('proceedWithCaution')}</h5>
|
||||
<p className="text-lg">{t('restrictWarning')}</p>
|
||||
<button className="btn btn-error capitalize w-full my-2" onClick={restrictAccount}>
|
||||
{t('restrict')}
|
||||
</button>
|
||||
</Popout>
|
||||
<BackToAccountButton />
|
||||
</div>
|
||||
)
|
||||
}
|
|
@ -39,8 +39,8 @@ export const ns = ['account', 'patterns', 'toast']
|
|||
export const StandAloneNewSet = () => {
|
||||
const { t } = useTranslation(['account'])
|
||||
const toast = useToast()
|
||||
const { account, token } = useAccount()
|
||||
const backend = useBackend(token)
|
||||
const { account } = useAccount()
|
||||
const backend = useBackend()
|
||||
|
||||
return (
|
||||
<div className="max-w-xl">
|
||||
|
@ -587,31 +587,11 @@ const MeasurementsSet = ({ mset, t, account, backend, refresh }) => {
|
|||
)
|
||||
}
|
||||
|
||||
// Component for the 'new/apikey' page
|
||||
//export const NewApikey = ({ app, standAlone = false }) => {
|
||||
// const { account, token } = useAccount()
|
||||
// const backend = useBackend(token)
|
||||
// const { t } = useTranslation(ns)
|
||||
// const toast = useToast()
|
||||
//
|
||||
// const [keys, setKeys] = useState([])
|
||||
// const [generate, setGenerate] = useState(false)
|
||||
// const [added, setAdded] = useState(0)
|
||||
//
|
||||
// const oneAdded = () => setAdded(added + 1)
|
||||
//
|
||||
// return (
|
||||
// <div className="max-w-xl xl:pl-4">
|
||||
// <NewKey {...{ app, t, account, setGenerate, backend, toast, oneAdded, standAlone }} />
|
||||
// </div>
|
||||
// )
|
||||
//}
|
||||
|
||||
// Component for the account/sets page
|
||||
export const Sets = ({ title = true }) => {
|
||||
// Hooks
|
||||
const { account, token } = useAccount()
|
||||
const backend = useBackend(token)
|
||||
const { account } = useAccount()
|
||||
const backend = useBackend()
|
||||
const { t } = useTranslation(ns)
|
||||
const toast = useToast()
|
||||
const { CollapseButton, closeCollapseButton } = useCollapseButton()
|
||||
|
|
|
@ -1,14 +1,11 @@
|
|||
// Dependencies
|
||||
import { useState, useContext } from 'react'
|
||||
import { useState } from 'react'
|
||||
import { useTranslation } from 'next-i18next'
|
||||
// Hooks
|
||||
import { useAccount } from 'shared/hooks/use-account.mjs'
|
||||
import { useBackend } from 'shared/hooks/use-backend.mjs'
|
||||
import { useToast } from 'shared/hooks/use-toast.mjs'
|
||||
// Context
|
||||
import { LoadingContext } from 'shared/context/loading-context.mjs'
|
||||
import { useLoadingStatus } from 'shared/hooks/use-loading-status.mjs'
|
||||
// Components
|
||||
import { Spinner } from 'shared/components/spinner.mjs'
|
||||
import { Icons, welcomeSteps, BackToAccountButton } from './shared.mjs'
|
||||
import { OkIcon, NoIcon } from 'shared/components/icons.mjs'
|
||||
import { ContinueButton } from 'shared/components/buttons/continue-button.mjs'
|
||||
|
@ -16,13 +13,10 @@ import { ContinueButton } from 'shared/components/buttons/continue-button.mjs'
|
|||
export const ns = ['account', 'toast']
|
||||
|
||||
export const UsernameSettings = ({ title = false, welcome = false }) => {
|
||||
// Context
|
||||
const { loading, startLoading, stopLoading } = useContext(LoadingContext)
|
||||
|
||||
// Hooks
|
||||
const { account, setAccount, token } = useAccount()
|
||||
const backend = useBackend(token)
|
||||
const toast = useToast()
|
||||
const { account, setAccount } = useAccount()
|
||||
const backend = useBackend()
|
||||
const { setLoadingStatus, LoadingStatus } = useLoadingStatus()
|
||||
const { t } = useTranslation(ns)
|
||||
const [username, setUsername] = useState(account.username)
|
||||
const [available, setAvailable] = useState(true)
|
||||
|
@ -38,13 +32,12 @@ export const UsernameSettings = ({ title = false, welcome = false }) => {
|
|||
}
|
||||
|
||||
const save = async () => {
|
||||
startLoading()
|
||||
setLoadingStatus([true, 'processingUpdate'])
|
||||
const result = await backend.updateAccount({ username })
|
||||
if (result.success) {
|
||||
setAccount(result.data.account)
|
||||
toast.for.settingsSaved()
|
||||
} else toast.for.backendError()
|
||||
stopLoading()
|
||||
setLoadingStatus([true, 'settingsSaved', true, true])
|
||||
} else setLoadingStatus([true, 'backendError', true, true])
|
||||
}
|
||||
|
||||
const nextHref =
|
||||
|
@ -53,18 +46,12 @@ export const UsernameSettings = ({ title = false, welcome = false }) => {
|
|||
: '/docs/guide'
|
||||
|
||||
let btnClasses = 'btn mt-4 capitalize '
|
||||
if (welcome) {
|
||||
btnClasses += 'w-64 '
|
||||
if (loading) btnClasses += 'btn-accent '
|
||||
else btnClasses += 'btn-secondary '
|
||||
} else {
|
||||
btnClasses += 'w-full '
|
||||
if (loading) btnClasses += 'btn-accent '
|
||||
else btnClasses += 'btn-primary '
|
||||
}
|
||||
if (welcome) btnClasses += 'w-64 btn-secondary'
|
||||
else btnClasses += 'w-full btn-primary'
|
||||
|
||||
return (
|
||||
<div className="max-w-xl">
|
||||
<LoadingStatus />
|
||||
{title ? <h1 className="text-4xl">{t('usernameTitle')}</h1> : null}
|
||||
<div className="flex flex-row items-center">
|
||||
<input
|
||||
|
@ -84,16 +71,7 @@ export const UsernameSettings = ({ title = false, welcome = false }) => {
|
|||
</div>
|
||||
<button className={btnClasses} disabled={!available} onClick={save}>
|
||||
<span className="flex flex-row items-center gap-2">
|
||||
{loading ? (
|
||||
<>
|
||||
<Spinner />
|
||||
<span>{t('processing')}</span>
|
||||
</>
|
||||
) : available ? (
|
||||
t('save')
|
||||
) : (
|
||||
t('usernameNotAvailable')
|
||||
)}
|
||||
{available ? t('save') : t('usernameNotAvailable')}
|
||||
</span>
|
||||
</button>
|
||||
|
||||
|
@ -119,7 +97,7 @@ export const UsernameSettings = ({ title = false, welcome = false }) => {
|
|||
) : null}
|
||||
</>
|
||||
) : (
|
||||
<BackToAccountButton loading={loading} />
|
||||
<BackToAccountButton />
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
|
|
12
sites/shared/components/control/score.mjs
Normal file
12
sites/shared/components/control/score.mjs
Normal file
|
@ -0,0 +1,12 @@
|
|||
import { BulletIcon } from 'shared/components/icons.mjs'
|
||||
|
||||
const scores = [1, 2, 3, 4, 5]
|
||||
|
||||
export const ControlScore = ({ control, color = 'base-content' }) =>
|
||||
control ? (
|
||||
<div className={`flex flex-row items-center text-${color}`}>
|
||||
{scores.map((score) => (
|
||||
<BulletIcon fill={control >= score ? true : false} className="w-6 h-6 -ml-1" key={score} />
|
||||
))}
|
||||
</div>
|
||||
) : null
|
|
@ -36,6 +36,12 @@ export const BioIcon = (props) => (
|
|||
</IconWrapper>
|
||||
)
|
||||
|
||||
export const BookmarkIcon = (props) => (
|
||||
<IconWrapper {...props}>
|
||||
<path d="M17.593 3.322c1.1.128 1.907 1.077 1.907 2.185V21L12 17.25 4.5 21V5.507c0-1.108.806-2.057 1.907-2.185a48.507 48.507 0 0111.186 0z" />
|
||||
</IconWrapper>
|
||||
)
|
||||
|
||||
export const BoxIcon = (props) => (
|
||||
<IconWrapper {...props}>
|
||||
<path d="M20 7l-8-4-8 4m16 0l-8 4m8-4v10l-8 4m0-10L4 7m8 4v10M4 7v10l8 4" />
|
||||
|
@ -327,6 +333,18 @@ export const LinkIcon = (props) => (
|
|||
</IconWrapper>
|
||||
)
|
||||
|
||||
export const LockIcon = (props) => (
|
||||
<IconWrapper {...props}>
|
||||
<path d="M16.5 10.5V6.75a4.5 4.5 0 10-9 0v3.75m-.75 11.25h10.5a2.25 2.25 0 002.25-2.25v-6.75a2.25 2.25 0 00-2.25-2.25H6.75a2.25 2.25 0 00-2.25 2.25v6.75a2.25 2.25 0 002.25 2.25z" />
|
||||
</IconWrapper>
|
||||
)
|
||||
|
||||
export const MastodonIcon = (props) => (
|
||||
<IconWrapper {...props} fill stroke={0}>
|
||||
<path d="m 11.217423,0.1875 c -2.8267978,0.0231106 -5.545964,0.32921539 -7.1306105,1.056962 0,0 -3.14282962,1.4058166 -3.14282962,6.2023445 0,1.0983506 -0.021349,2.4116171 0.013437,3.8043315 0.11412502,4.690743 0.85993502,9.313695 5.19692442,10.461603 1.9996899,0.529281 3.7166529,0.640169 5.0993757,0.564166 2.507534,-0.139021 3.915187,-0.894849 3.915187,-0.894849 l -0.08272,-1.819364 c 0,0 -1.79194,0.564966 -3.804377,0.496111 -1.9938518,-0.06838 -4.0987697,-0.214969 -4.4212502,-2.662908 -0.029782,-0.215025 -0.044673,-0.445024 -0.044673,-0.686494 0,0 1.9573364,0.47844 4.4378282,0.592088 1.516743,0.06957 2.939062,-0.08886 4.383732,-0.261231 2.770451,-0.330816 5.182722,-2.037815 5.485905,-3.597546 0.477704,-2.456993 0.438356,-5.9959075 0.438356,-5.9959075 0,-4.7965279 -3.142655,-6.2023445 -3.142655,-6.2023445 C 16.83453,0.51671539 14.113674,0.21061063 11.286876,0.1875 Z M 8.0182292,3.9352913 c 1.177465,0 2.0690118,0.4525587 2.6585778,1.3578046 l 0.573249,0.9608111 0.573247,-0.9608111 c 0.589448,-0.9052459 1.480995,-1.3578046 2.65858,-1.3578046 1.017594,0 1.837518,0.3577205 2.463657,1.0555661 0.606959,0.6978459 0.909169,1.6411822 0.909169,2.8281631 V 13.626816 H 15.553691 V 7.9896839 c 0,-1.1882914 -0.49996,-1.7914432 -1.500043,-1.7914432 -1.10575,0 -1.659889,0.715401 -1.659889,2.1301529 V 11.413948 H 10.106352 V 8.3283936 c 0,-1.4147519 -0.5543138,-2.1301529 -1.6600628,-2.1301529 -1.000084,0 -1.5000426,0.6031518 -1.5000426,1.7914432 V 13.626816 H 4.6452275 V 7.8190205 c 0,-1.1869809 0.3022656,-2.1303172 0.9093441,-2.8281631 C 6.1805914,4.2930118 7.0005147,3.9352913 8.0182292,3.9352913 Z" />
|
||||
</IconWrapper>
|
||||
)
|
||||
|
||||
export const MarginIcon = (props) => (
|
||||
<IconWrapper {...props}>
|
||||
<path d="m 2.4889452,14.488945 h 7.0221096 v 7.02211 H 2.4889452 Z M 14.488945,2.4889452 h 7.02211 v 7.0221096 h -7.02211 z m -11.9999998,0 H 9.5110548 V 9.5110548 H 2.4889452 Z M 14.488945,14.488945 h 7.02211 v 7.02211 h -7.02211 z" />
|
||||
|
@ -441,12 +459,24 @@ export const PrintIcon = (props) => (
|
|||
</IconWrapper>
|
||||
)
|
||||
|
||||
export const PrivacyIcon = (props) => (
|
||||
<IconWrapper {...props}>
|
||||
<path d="M3.98 8.223A10.477 10.477 0 001.934 12C3.226 16.338 7.244 19.5 12 19.5c.993 0 1.953-.138 2.863-.395M6.228 6.228A10.45 10.45 0 0112 4.5c4.756 0 8.773 3.162 10.065 7.498a10.523 10.523 0 01-4.293 5.774M6.228 6.228L3 3m3.228 3.228l3.65 3.65m7.894 7.894L21 21m-3.228-3.228l-3.65-3.65m0 0a3 3 0 10-4.243-4.243m4.242 4.242L9.88 9.88" />
|
||||
</IconWrapper>
|
||||
)
|
||||
|
||||
export const RedditIcon = (props) => (
|
||||
<IconWrapper {...props} stroke={0} fill>
|
||||
<path d="M 11.710829,0.00384705 C 5.0683862,0.16990815 -0.16221405,5.6505729 0.00384705,12.293016 0.16990814,18.686369 5.3178021,23.833614 11.628124,24.082706 18.270567,24.248767 23.833939,19.018167 24,12.375723 V 11.710829 C 23.833939,5.0683862 18.353273,-0.16221404 11.710829,0.00384705 Z m 5.187788,5.10021625 c 0.15698,0.00649 0.313636,0.048326 0.458939,0.1313569 0.581214,0.3321223 0.912687,1.0793971 0.580565,1.660611 C 17.605998,7.4772452 16.858724,7.808718 16.27751,7.4765965 15.862357,7.3105352 15.614238,6.8947339 15.614238,6.3965506 L 13.038995,5.8159854 12.208689,9.55236 c 1.826672,0.08303 3.48858,0.664893 4.651007,1.495199 0.664245,-0.664245 1.826673,-0.664245 2.490917,0 0.332122,0.332121 0.49786,0.747274 0.49786,1.245457 0.249091,0.747275 -0.249092,1.327193 -0.830306,1.576284 v 0.49948 c 0,2.740009 -3.155161,4.897506 -7.057597,4.897506 -3.9024357,0 -7.0575963,-2.157497 -7.0575963,-4.897506 V 13.8693 C 3.9896377,13.454147 3.6578398,12.458754 3.989962,11.545418 c 0.2490916,-0.664245 0.9120387,-1.08037 1.5762832,-0.99734 0.4981831,0 0.9133359,0.167358 1.2454581,0.499481 C 8.2232228,10.134222 9.8848065,9.55236 11.545418,9.55236 l 0.913011,-4.1515273 c 0,-0.083031 0.08271,-0.1654124 0.08271,-0.1654125 0.08303,-0.08303 0.166711,-0.084328 0.249741,-0.084328 l 2.906069,0.664893 C 15.946037,5.3800751 16.427678,5.084603 16.898617,5.1040633 Z M 9.3026198,12.293016 c -0.6642443,0 -1.2454583,0.581214 -1.2454583,1.245458 0,0.664245 0.498183,1.245459 1.2454583,1.245459 0.6642442,0 1.2454582,-0.581214 1.2454582,-1.245459 0,-0.664244 -0.581214,-1.245458 -1.2454582,-1.245458 z m 5.4813132,0 c -0.664245,0 -1.245459,0.581214 -1.245459,1.245458 0,0.664245 0.581214,1.245459 1.245459,1.245459 0.664245,0 1.245458,-0.581214 1.245458,-1.245459 0,-0.664244 -0.581213,-1.245458 -1.245458,-1.245458 z m -5.3872557,3.943952 c -0.072653,0 -0.135249,0.04021 -0.1767645,0.123249 -0.1660605,0.16606 -0.1660605,0.332121 0,0.415152 0.8303052,0.830306 2.4905922,0.914633 2.9887762,0.914633 0.498183,0 2.077061,-0.08433 2.990396,-0.914633 -0.08303,-0.08303 -0.084,-0.249092 -0.167034,-0.415152 -0.166061,-0.166062 -0.332121,-0.166062 -0.415152,0 -0.498183,0.581213 -1.660611,0.747598 -2.490917,0.747598 -0.830305,0 -1.992733,-0.166385 -2.4909165,-0.747598 -0.08303,-0.08303 -0.1657365,-0.123249 -0.2383882,-0.123249 z" />
|
||||
</IconWrapper>
|
||||
)
|
||||
|
||||
export const ReloadIcon = (props) => (
|
||||
<IconWrapper {...props}>
|
||||
<path d="M16.023 9.348h4.992v-.001M2.985 19.644v-4.992m0 0h4.992m-4.993 0l3.181 3.183a8.25 8.25 0 0013.803-3.7M4.031 9.865a8.25 8.25 0 0113.803-3.7l3.181 3.182m0-4.991v4.99" />
|
||||
</IconWrapper>
|
||||
)
|
||||
|
||||
export const RightIcon = (props) => (
|
||||
<IconWrapper {...props}>
|
||||
<path d="M9 5l7 7-7 7" />
|
||||
|
@ -493,6 +523,12 @@ export const SettingsIcon = (props) => (
|
|||
</IconWrapper>
|
||||
)
|
||||
|
||||
export const ShieldIcon = (props) => (
|
||||
<IconWrapper {...props}>
|
||||
<path d="M9 12.75L11.25 15 15 9.75m-3-7.036A11.959 11.959 0 013.598 6 11.99 11.99 0 003 9.749c0 5.592 3.824 10.29 9 11.623 5.176-1.332 9-6.03 9-11.622 0-1.31-.21-2.571-.598-3.751h-.152c-3.196 0-6.1-1.248-8.25-3.285z" />
|
||||
</IconWrapper>
|
||||
)
|
||||
|
||||
export const ShowcaseIcon = (props) => (
|
||||
<IconWrapper {...props}>
|
||||
<path d="M3 9a2 2 0 012-2h.93a2 2 0 001.664-.89l.812-1.22A2 2 0 0110.07 4h3.86a2 2 0 011.664.89l.812 1.22A2 2 0 0018.07 7H19a2 2 0 012 2v9a2 2 0 01-2 2H5a2 2 0 01-2-2V9z" />
|
||||
|
@ -500,6 +536,18 @@ export const ShowcaseIcon = (props) => (
|
|||
</IconWrapper>
|
||||
)
|
||||
|
||||
export const SigninIcon = (props) => (
|
||||
<IconWrapper {...props}>
|
||||
<path d="M15.75 9V5.25A2.25 2.25 0 0013.5 3h-6a2.25 2.25 0 00-2.25 2.25v13.5A2.25 2.25 0 007.5 21h6a2.25 2.25 0 002.25-2.25V15M12 9l-3 3m0 0l3 3m-3-3h12.75" />
|
||||
</IconWrapper>
|
||||
)
|
||||
|
||||
export const SignoutIcon = (props) => (
|
||||
<IconWrapper {...props}>
|
||||
<path d="M15.75 9V5.25A2.25 2.25 0 0013.5 3h-6a2.25 2.25 0 00-2.25 2.25v13.5A2.25 2.25 0 007.5 21h6a2.25 2.25 0 002.25-2.25V15m3 0l3-3m0 0l-3-3m3 3H9" />
|
||||
</IconWrapper>
|
||||
)
|
||||
|
||||
export const StarIcon = (props) => (
|
||||
<IconWrapper {...props}>
|
||||
<path d="M11.48 3.499a.562.562 0 011.04 0l2.125 5.111a.563.563 0 00.475.345l5.518.442c.499.04.701.663.321.988l-4.204 3.602a.563.563 0 00-.182.557l1.285 5.385a.562.562 0 01-.84.61l-4.725-2.885a.563.563 0 00-.586 0L6.982 20.54a.562.562 0 01-.84-.61l1.285-5.386a.562.562 0 00-.182-.557l-4.204-3.602a.563.563 0 01.321-.988l5.518-.442a.563.563 0 00.475-.345L11.48 3.5z" />
|
||||
|
@ -512,6 +560,12 @@ export const ThemeIcon = (props) => (
|
|||
</IconWrapper>
|
||||
)
|
||||
|
||||
export const TikTokIcon = (props) => (
|
||||
<IconWrapper {...props}>
|
||||
<path d="M 21.070629,5.6224629 A 5.7508474,5.7508474 0 0 1 16.547219,0.52913011 V 0 H 12.41376 v 16.404252 a 3.474745,3.474745 0 0 1 -6.2403831,2.091334 l -0.0024,-0.0012 0.0024,0.0012 A 3.4735455,3.4735455 0 0 1 9.9924767,13.084289 V 8.8848362 A 7.5938063,7.5938063 0 0 0 3.5205237,21.713559 7.5950059,7.5950059 0 0 0 16.547219,16.405452 V 8.0233494 a 9.8171151,9.8171151 0 0 0 5.72685,1.8309665 V 5.7472464 A 5.7964413,5.7964413 0 0 1 21.070637,5.6225887 Z" />
|
||||
</IconWrapper>
|
||||
)
|
||||
|
||||
export const TipIcon = (props) => (
|
||||
<IconWrapper {...props}>
|
||||
<path d="M9.663 17h4.673M12 3v1m6.364 1.636l-.707.707M21 12h-1M4 12H3m3.343-5.657l-.707-.707m2.828 9.9a5 5 0 117.072 0l-.548.547A3.374 3.374 0 0014 18.469V19a2 2 0 11-4 0v-.531c0-.895-.356-1.754-.988-2.386l-.548-.547z" />
|
||||
|
@ -538,6 +592,12 @@ export const TrophyIcon = (props) => (
|
|||
</IconWrapper>
|
||||
)
|
||||
|
||||
export const TwitchIcon = (props) => (
|
||||
<IconWrapper {...props} stroke={0} fill>
|
||||
<path d="M2.149 0l-1.612 4.119v16.836h5.731v3.045h3.224l3.045-3.045h4.657l6.269-6.269v-14.686h-21.314zm19.164 13.612l-3.582 3.582h-5.731l-3.045 3.045v-3.045h-4.836v-15.045h17.194v11.463zm-3.582-7.343v6.262h-2.149v-6.262h2.149zm-5.731 0v6.262h-2.149v-6.262h2.149z" />
|
||||
</IconWrapper>
|
||||
)
|
||||
|
||||
export const TwitterIcon = (props) => (
|
||||
<IconWrapper {...props} stroke={0} fill>
|
||||
<path d="M23.954 4.569c-.885.389-1.83.654-2.825.775 1.014-.611 1.794-1.574 2.163-2.723-.951.555-2.005.959-3.127 1.184-.896-.959-2.173-1.559-3.591-1.559-2.717 0-4.92 2.203-4.92 4.917 0 .39.045.765.127 1.124C7.691 8.094 4.066 6.13 1.64 3.161c-.427.722-.666 1.561-.666 2.475 0 1.71.87 3.213 2.188 4.096-.807-.026-1.566-.248-2.228-.616v.061c0 2.385 1.693 4.374 3.946 4.827-.413.111-.849.171-1.296.171-.314 0-.615-.03-.916-.086.631 1.953 2.445 3.377 4.604 3.417-1.68 1.319-3.809 2.105-6.102 2.105-.39 0-.779-.023-1.17-.067 2.189 1.394 4.768 2.209 7.557 2.209 9.054 0 13.999-7.496 13.999-13.986 0-.209 0-.42-.015-.63.961-.689 1.8-1.56 2.46-2.548l-.047-.02z" />
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
import Link from 'next/link'
|
||||
|
||||
export const PageLink = ({ href, txt, className = '' }) => (
|
||||
export const PageLink = ({ href, txt, className = '', children = null }) => (
|
||||
<Link
|
||||
href={href}
|
||||
className={`underline decoration-2 hover:decoration-4 ${className}`}
|
||||
title={txt}
|
||||
title={txt ? txt : ''}
|
||||
>
|
||||
{txt}
|
||||
{children ? children : txt}
|
||||
</Link>
|
||||
)
|
||||
|
|
|
@ -39,8 +39,8 @@ export const ManagePattern = ({ id = false }) => {
|
|||
const [error, setError] = useState(false)
|
||||
|
||||
// Hooks
|
||||
const { account, token } = useAccount()
|
||||
const backend = useBackend(token)
|
||||
const { account } = useAccount()
|
||||
const backend = useBackend()
|
||||
const { t, i18n } = useTranslation(ns)
|
||||
const { language } = i18n
|
||||
const toast = useToast()
|
||||
|
|
|
@ -6,7 +6,6 @@ import { capitalize } from 'shared/utils.mjs'
|
|||
// Hooks
|
||||
import { useState, useEffect } from 'react'
|
||||
import { useTranslation } from 'next-i18next'
|
||||
import { useAccount } from 'shared/hooks/use-account.mjs'
|
||||
import { useBackend } from 'shared/hooks/use-backend.mjs'
|
||||
// Components
|
||||
import { SetCandidate, ns as setNs } from 'shared/components/sets/set-candidate.mjs'
|
||||
|
@ -18,8 +17,7 @@ export const ns = setNs
|
|||
|
||||
export const CuratedSetPicker = ({ design, language, href, clickHandler }) => {
|
||||
// Hooks
|
||||
const { token } = useAccount()
|
||||
const backend = useBackend(token)
|
||||
const backend = useBackend()
|
||||
const { t, i18n } = useTranslation('sets')
|
||||
|
||||
// State
|
||||
|
@ -119,8 +117,7 @@ export const CuratedSetPicker = ({ design, language, href, clickHandler }) => {
|
|||
|
||||
export const UserSetPicker = ({ design, t, href, clickHandler }) => {
|
||||
// Hooks
|
||||
const { token } = useAccount()
|
||||
const backend = useBackend(token)
|
||||
const backend = useBackend()
|
||||
|
||||
// State
|
||||
const [sets, setSets] = useState({})
|
||||
|
|
|
@ -1,12 +1,10 @@
|
|||
// Hooks
|
||||
import { useState, useEffect, useContext } from 'react'
|
||||
import { useState, useEffect } from 'react'
|
||||
import { useAccount } from 'shared/hooks/use-account.mjs'
|
||||
import { useTranslation } from 'next-i18next'
|
||||
import { useBackend } from 'shared/hooks/use-backend.mjs'
|
||||
import { useToast } from 'shared/hooks/use-toast.mjs'
|
||||
import { useRouter } from 'next/router'
|
||||
// Context
|
||||
import { LoadingContext } from 'shared/context/loading-context.mjs'
|
||||
import { useLoadingStatus } from 'shared/hooks/use-loading-status.mjs'
|
||||
// Components
|
||||
import Link from 'next/link'
|
||||
import { EmailIcon, KeyIcon, RightIcon, WarningIcon } from 'shared/components/icons.mjs'
|
||||
|
@ -52,12 +50,11 @@ export const ButtonText = ({ children }) => (
|
|||
)
|
||||
|
||||
export const SignIn = () => {
|
||||
const { startLoading, stopLoading } = useContext(LoadingContext)
|
||||
const { setAccount, setToken, seenUser, setSeenUser } = useAccount()
|
||||
const { t } = useTranslation(['signin', 'signup', 'toast'])
|
||||
const { t } = useTranslation(['signin', 'signup', 'status'])
|
||||
const backend = useBackend()
|
||||
const toast = useToast()
|
||||
const router = useRouter()
|
||||
const { setLoadingStatus, LoadingStatus } = useLoadingStatus()
|
||||
|
||||
const [username, setUsername] = useState('')
|
||||
const [password, setPassword] = useState('')
|
||||
|
@ -86,35 +83,33 @@ export const SignIn = () => {
|
|||
|
||||
const signinHandler = async (evt) => {
|
||||
evt.preventDefault()
|
||||
startLoading()
|
||||
setLoadingStatus([true, 'processingUpdate'])
|
||||
const result = magicLink
|
||||
? await backend.signIn({ username, password: false })
|
||||
: await backend.signIn({ username, password })
|
||||
// Sign-in succeeded
|
||||
if (result.success) {
|
||||
let msg
|
||||
if (magicLink) {
|
||||
setLoadingStatus([true, t('singup:emailSent'), true, true])
|
||||
setMagicLinkSent(true)
|
||||
msg = t('signup:emailSent')
|
||||
} else {
|
||||
setAccount(result.data.account)
|
||||
setToken(result.data.token)
|
||||
setSeenUser(result.data.account.username)
|
||||
msg = t('signin:welcomeBackName', { name: result.data.account.username })
|
||||
stopLoading()
|
||||
setLoadingStatus([
|
||||
true,
|
||||
t('signin:welcomeBackName', { name: result.data.account.username }),
|
||||
true,
|
||||
true,
|
||||
])
|
||||
router.push('/account')
|
||||
}
|
||||
return toast.success(<b>{msg}</b>)
|
||||
}
|
||||
// Sign-in failed
|
||||
if (result.status === 401) {
|
||||
let msg
|
||||
if (result.data.error === 'signInFailed') {
|
||||
msg = magicLink ? t('notFound') : t('signInFailed')
|
||||
}
|
||||
if (result.response?.response?.status === 401) {
|
||||
const msg = magicLink ? t('notFound') : t('signInFailed')
|
||||
setSignInFailed(msg)
|
||||
|
||||
return toast.warning(<b>{msg}</b>)
|
||||
setLoadingStatus([true, msg, true, false])
|
||||
}
|
||||
// Bad request
|
||||
if (result.status === 400) {
|
||||
|
@ -122,9 +117,8 @@ export const SignIn = () => {
|
|||
if (result.data.error === 'usernameMissing') msg = t('usernameMissing')
|
||||
else if (result.data.error === 'passwordMissing') msg = t('passwordMissing')
|
||||
setSignInFailed(msg)
|
||||
return toast.warning(<b>{msg}</b>)
|
||||
setLoadingStatus([true, msg, true, false])
|
||||
}
|
||||
stopLoading()
|
||||
}
|
||||
|
||||
const btnClasses = `btn capitalize w-full mt-4 ${
|
||||
|
@ -141,6 +135,7 @@ export const SignIn = () => {
|
|||
if (magicLinkSent)
|
||||
return (
|
||||
<>
|
||||
<LoadingStatus />
|
||||
<h1 className="text-inherit text-3xl lg:text-5xl mb-4 pb-0 text-center">
|
||||
{t('signup:emailSent')}
|
||||
</h1>
|
||||
|
@ -161,6 +156,7 @@ export const SignIn = () => {
|
|||
|
||||
return (
|
||||
<>
|
||||
<LoadingStatus />
|
||||
<h1 className="text-inherit text-3xl lg:text-5xl mb-4 pb-0 text-center">
|
||||
{seenBefore ? t('signin:welcomeBackName', { name: seenUser }) : t('signin:welcome')}
|
||||
</h1>
|
||||
|
|
|
@ -4,7 +4,6 @@ import { capitalize, shortDate } from 'shared/utils.mjs'
|
|||
import { useState, useContext } from 'react'
|
||||
import { useTranslation } from 'next-i18next'
|
||||
import { useRouter } from 'next/router'
|
||||
import { useAccount } from 'shared/hooks/use-account.mjs'
|
||||
import { useBackend } from 'shared/hooks/use-backend.mjs'
|
||||
import { useToast } from 'shared/hooks/use-toast.mjs'
|
||||
// Context
|
||||
|
@ -222,8 +221,7 @@ const SaveExistingPattern = ({
|
|||
export const SaveView = ({ design, settings, from = false }) => {
|
||||
// Hooks
|
||||
const { t } = useTranslation(ns)
|
||||
const { token } = useAccount()
|
||||
const backend = useBackend(token)
|
||||
const backend = useBackend()
|
||||
const router = useRouter()
|
||||
const toast = useToast()
|
||||
// Context
|
||||
|
|
|
@ -11,29 +11,40 @@ export const freeSewingConfig = {
|
|||
account: {
|
||||
fields: {
|
||||
data: {
|
||||
bookmarks: 1,
|
||||
sets: 1,
|
||||
patterns: 1,
|
||||
apikeys: 4,
|
||||
},
|
||||
info: {
|
||||
bio: 1,
|
||||
email: 3,
|
||||
github: 3,
|
||||
img: 2,
|
||||
units: 2,
|
||||
language: 2,
|
||||
username: 2,
|
||||
bio: 1,
|
||||
img: 2,
|
||||
email: 3,
|
||||
},
|
||||
settings: {
|
||||
compare: 3,
|
||||
consent: 2,
|
||||
control: 1,
|
||||
mfa: 4,
|
||||
language: 2,
|
||||
units: 2,
|
||||
newsletter: 2,
|
||||
password: 2,
|
||||
compare: 3,
|
||||
control: 1,
|
||||
consent: 2,
|
||||
},
|
||||
developer: {
|
||||
security: {
|
||||
password: 2,
|
||||
mfa: 4,
|
||||
apikeys: 4,
|
||||
},
|
||||
identities: {
|
||||
github: 3,
|
||||
instagram: 3,
|
||||
mastodon: 3,
|
||||
reddit: 3,
|
||||
twitter: 3,
|
||||
twitch: 3,
|
||||
tiktok: 3,
|
||||
website: 3,
|
||||
},
|
||||
},
|
||||
sets: {
|
||||
name: 1,
|
||||
|
|
|
@ -6,6 +6,9 @@
|
|||
file in it's scans.
|
||||
-->
|
||||
|
||||
<!-- Loading status -->
|
||||
<div className="fixed top-0 md:top-28 md:max-w-2xl md:px-4 md:mx-auto"></div>
|
||||
|
||||
<!-- Classes for the Popout component -->
|
||||
<p class="border-accent bg-accent text-accent" />
|
||||
<p class="border-secondary bg-secondary text-secondary" />
|
||||
|
|
|
@ -152,23 +152,49 @@ Backend.prototype.confirmMfa = async function (data) {
|
|||
* Disable MFA
|
||||
*/
|
||||
Backend.prototype.disableMfa = async function (data) {
|
||||
return responseHandler(
|
||||
await await api.post(`/account/mfa/jwt`, { ...data, mfa: false }, this.auth)
|
||||
)
|
||||
return responseHandler(await api.post(`/account/mfa/jwt`, { ...data, mfa: false }, this.auth))
|
||||
}
|
||||
|
||||
/*
|
||||
* Reload account
|
||||
*/
|
||||
Backend.prototype.reloadAccount = async function () {
|
||||
return responseHandler(await await api.get(`/whoami/jwt`, this.auth))
|
||||
return responseHandler(await api.get(`/whoami/jwt`, this.auth))
|
||||
}
|
||||
|
||||
/*
|
||||
* Export account data
|
||||
*/
|
||||
Backend.prototype.exportAccount = async function () {
|
||||
return responseHandler(await api.get(`/account/export/jwt`, this.auth))
|
||||
}
|
||||
|
||||
/*
|
||||
* Restrict processing of account data
|
||||
*/
|
||||
Backend.prototype.restrictAccount = async function () {
|
||||
return responseHandler(await api.get(`/account/restrict/jwt`, this.auth))
|
||||
}
|
||||
|
||||
/*
|
||||
* Remove account
|
||||
*/
|
||||
Backend.prototype.restrictAccount = async function () {
|
||||
return responseHandler(await api.delete(`/account/jwt`, this.auth))
|
||||
}
|
||||
|
||||
/*
|
||||
* Load all user data
|
||||
*/
|
||||
Backend.prototype.getUserData = async function (uid) {
|
||||
return responseHandler(await api.get(`/users/${uid}/jwt`, this.auth))
|
||||
}
|
||||
|
||||
/*
|
||||
* Load user profile
|
||||
*/
|
||||
Backend.prototype.getProfile = async function (uid) {
|
||||
return responseHandler(await await api.get(`/users/${uid}`))
|
||||
return responseHandler(await api.get(`/users/${uid}`))
|
||||
}
|
||||
|
||||
/*
|
||||
|
|
|
@ -1,20 +1,80 @@
|
|||
import { useState } from 'react'
|
||||
import { useState, useEffect } from 'react'
|
||||
import { Spinner } from 'shared/components/spinner.mjs'
|
||||
import { OkIcon, WarningIcon } from 'shared/components/icons.mjs'
|
||||
import { useTranslation } from 'next-i18next'
|
||||
|
||||
const LoadingStatus = ({ loadingStatus }) =>
|
||||
loadingStatus[0] ? (
|
||||
<div className="fixed top-28 left-0 w-full z-30">
|
||||
export const ns = ['status']
|
||||
|
||||
/*
|
||||
* Timeout in seconds before the loading status dissapears
|
||||
*/
|
||||
const timeout = 2
|
||||
|
||||
const LoadingStatus = ({ loadingStatus }) => {
|
||||
const { t } = useTranslation(ns)
|
||||
|
||||
const [fade, setFade] = useState('opacity-100')
|
||||
const [timer, setTimer] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
if (loadingStatus[2]) {
|
||||
if (timer) clearTimeout(timer)
|
||||
setTimer(
|
||||
window.setTimeout(() => {
|
||||
setFade('opacity-0')
|
||||
}, timeout * 1000 - 350)
|
||||
)
|
||||
}
|
||||
}, [loadingStatus[2]])
|
||||
|
||||
if (!loadingStatus[0]) return null
|
||||
|
||||
let color = 'secondary'
|
||||
let icon = <Spinner />
|
||||
if (loadingStatus[2]) {
|
||||
color = loadingStatus[3] ? 'success' : 'error'
|
||||
icon = loadingStatus[3] ? (
|
||||
<OkIcon stroke={4} className="w-8 h-8" />
|
||||
) : (
|
||||
<WarningIcon className="w-8 h-8" stroke={2} />
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="fixed top-0 md:top-28 left-0 w-full z-30 md:px-4 md:mx-auto">
|
||||
<div
|
||||
className={`w-full max-w-lg m-auto bg-secondary flex flex-row gap-4 p-4 px-4
|
||||
rounded-lg shadow text-secondary-content text-medium bg-opacity-90`}
|
||||
className={`w-full md:max-w-2xl m-auto bg-${color} flex flex-row gap-4 p-4 px-4 ${fade}
|
||||
transition-opacity delay-[${timeout * 1000 - 400}ms] duration-300
|
||||
md:rounded-lg shadow text-secondary-content text-lg lg:text-xl font-medium md:bg-opacity-90`}
|
||||
>
|
||||
<Spinner /> {loadingStatus[1]}
|
||||
{icon}
|
||||
{t(loadingStatus[1])}
|
||||
</div>
|
||||
</div>
|
||||
) : null
|
||||
)
|
||||
}
|
||||
|
||||
export const useLoadingStatus = () => {
|
||||
/*
|
||||
* LoadingStatus should hold an array with 1 to 4 elements:
|
||||
* 0 => Show loading status or not (true or false)
|
||||
* 1 => Message to show
|
||||
* 2 => Set this to true to make the loadingStatus dissapear after 2 seconds
|
||||
* 3 => Set this to true to show success, false to show error (only when 2 is true)
|
||||
*/
|
||||
const [loadingStatus, setLoadingStatus] = useState([false])
|
||||
const [timer, setTimer] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
if (loadingStatus[2]) {
|
||||
if (timer) clearTimeout(timer)
|
||||
setTimer(
|
||||
window.setTimeout(() => {
|
||||
setLoadingStatus([false])
|
||||
}, timeout * 1000)
|
||||
)
|
||||
}
|
||||
}, [loadingStatus[2]])
|
||||
|
||||
return {
|
||||
setLoadingStatus,
|
||||
|
|
|
@ -1,100 +0,0 @@
|
|||
import { useState, useMemo, useCallback } from 'react'
|
||||
import set from 'lodash.set'
|
||||
import unset from 'lodash.unset'
|
||||
import cloneDeep from 'lodash.clonedeep'
|
||||
import { useLocalStorage } from './useLocalStorage'
|
||||
import { defaultGist as baseGist } from 'shared/components/workbench/gist.mjs'
|
||||
|
||||
// Generates a default design gist to start from
|
||||
export const defaultGist = (design, locale = 'en') => {
|
||||
const gist = {
|
||||
design,
|
||||
...baseGist,
|
||||
_state: { view: 'draft' },
|
||||
}
|
||||
if (locale) gist.locale = locale
|
||||
|
||||
return gist
|
||||
}
|
||||
|
||||
// generate the gist state and its handlers
|
||||
export function useGist(design, locale) {
|
||||
// memoize the initial gist for this design so that it doesn't change between renders and cause an infinite loop
|
||||
const initialGist = useMemo(() => defaultGist(design, locale), [design, locale])
|
||||
|
||||
// get the localstorage state and setter
|
||||
const [gist, _setGist, gistReady] = useLocalStorage(`${design}_gist`, initialGist)
|
||||
const [gistHistory, setGistHistory] = useState([])
|
||||
const [gistFuture, setGistFuture] = useState([])
|
||||
|
||||
const setGist = useCallback(
|
||||
(newGist, addToHistory = true) => {
|
||||
let oldGist
|
||||
_setGist((gistState) => {
|
||||
// have to clone it or nested objects will be referenced instead of copied, which defeats the purpose
|
||||
if (addToHistory) oldGist = cloneDeep(gistState)
|
||||
|
||||
return typeof newGist === 'function' ? newGist(cloneDeep(gistState)) : newGist
|
||||
})
|
||||
|
||||
if (addToHistory) {
|
||||
setGistHistory((history) => {
|
||||
return [...history, oldGist]
|
||||
})
|
||||
setGistFuture([])
|
||||
}
|
||||
},
|
||||
[_setGist, setGistFuture, setGistHistory]
|
||||
)
|
||||
|
||||
/** update a single gist value */
|
||||
const updateGist = useCallback(
|
||||
(path, value, addToHistory = true) => {
|
||||
setGist((gistState) => {
|
||||
const newGist = { ...gistState }
|
||||
set(newGist, path, value)
|
||||
return newGist
|
||||
}, addToHistory)
|
||||
},
|
||||
[setGist]
|
||||
)
|
||||
|
||||
/** unset a single gist value */
|
||||
const unsetGist = useCallback(
|
||||
(path, addToHistory = true) => {
|
||||
setGist((gistState) => {
|
||||
const newGist = { ...gistState }
|
||||
unset(newGist, path)
|
||||
return newGist
|
||||
}, addToHistory)
|
||||
},
|
||||
[setGist]
|
||||
)
|
||||
|
||||
const undoGist = useCallback(() => {
|
||||
_setGist((gistState) => {
|
||||
let prevGist
|
||||
setGistHistory((history) => {
|
||||
const newHistory = [...history]
|
||||
prevGist = newHistory.pop() || defaultGist(design, locale)
|
||||
return newHistory
|
||||
})
|
||||
setGistFuture((future) => [gistState, ...future])
|
||||
|
||||
return { ...prevGist }
|
||||
})
|
||||
}, [_setGist, setGistFuture, setGistHistory])
|
||||
|
||||
const redoGist = useCallback(() => {
|
||||
const newHistory = [...gistHistory, gist]
|
||||
const newFuture = [...gistFuture]
|
||||
const newGist = newFuture.shift()
|
||||
setGistHistory(newHistory)
|
||||
setGistFuture(newFuture)
|
||||
_setGist(newGist)
|
||||
}, [_setGist, setGistFuture, setGistHistory])
|
||||
|
||||
const resetGist = useCallback(() => setGist(defaultGist(design, locale)), [setGist])
|
||||
|
||||
return { gist, setGist, unsetGist, gistReady, updateGist, undoGist, redoGist, resetGist }
|
||||
}
|
|
@ -1,38 +0,0 @@
|
|||
import { useState, useEffect } from 'react'
|
||||
|
||||
const prefix = 'fs_'
|
||||
|
||||
// See: https://usehooks.com/useLocalStorage/
|
||||
export function useLocalStorage(key, initialValue) {
|
||||
// use this to track whether it's mounted. useful for doing other effects outside this hook
|
||||
// and for making sure we don't write the initial value over the current value
|
||||
const [ready, setReady] = useState(false)
|
||||
|
||||
// State to store our value
|
||||
const [storedValue, setValue] = useState(initialValue)
|
||||
|
||||
// set to localstorage every time the storedValue changes
|
||||
// we do it this way instead of a callback because
|
||||
// getting the current state inside `useCallback` didn't seem to be working
|
||||
useEffect(() => {
|
||||
if (ready) {
|
||||
window.localStorage.setItem(prefix + key, JSON.stringify(storedValue))
|
||||
}
|
||||
}, [storedValue, key, ready])
|
||||
|
||||
// read from local storage on mount
|
||||
useEffect(() => {
|
||||
try {
|
||||
// Get from local storage by key
|
||||
const item = window.localStorage.getItem(prefix + key)
|
||||
// Parse stored json or if none return initialValue
|
||||
const valToSet = item ? JSON.parse(item) : initialValue
|
||||
setValue(valToSet)
|
||||
setReady(true)
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
}
|
||||
}, [setReady, setValue, key, initialValue])
|
||||
|
||||
return [storedValue, setValue, ready]
|
||||
}
|
|
@ -1,3 +1,5 @@
|
|||
updatingSettings: Updating settings
|
||||
settingsSaved: Settings saved
|
||||
backendError: Backend returned an error
|
||||
copiedToClipboard: Copied to clipboard
|
||||
processingUpdate: Processing update
|
|
@ -39,7 +39,7 @@ export const extendSiteNav = async (siteNav, lang) => {
|
|||
h: 1,
|
||||
t: t('sections:new'),
|
||||
apikey: {
|
||||
c: conf.account.fields.developer.apikeys,
|
||||
c: conf.account.fields.security.apikeys,
|
||||
s: 'new/apikey',
|
||||
t: t('newApikey'),
|
||||
o: 30,
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue