cleanup: wip 6
This commit is contained in:
parent
ea5be502b3
commit
590ed9ce73
|
|
@ -9,10 +9,9 @@ services:
|
||||||
# Update 'VARIANT' to pick a version of PHP version: 8, 8.1, 8.0, 7, 7.4
|
# Update 'VARIANT' to pick a version of PHP version: 8, 8.1, 8.0, 7, 7.4
|
||||||
# Append -bullseye or -buster to pin to an OS version.
|
# Append -bullseye or -buster to pin to an OS version.
|
||||||
# Use -bullseye variants on local arm64/Apple Silicon.
|
# Use -bullseye variants on local arm64/Apple Silicon.
|
||||||
VARIANT: "8-bullseye"
|
VARIANT: 8-bullseye
|
||||||
# Optional Node.js version
|
# Optional Node.js version
|
||||||
NODE_VERSION: "lts/*"
|
NODE_VERSION: 'lts/*'
|
||||||
|
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
- ..:/workspace:cached
|
- ..:/workspace:cached
|
||||||
|
|
|
||||||
|
|
@ -1,9 +0,0 @@
|
||||||
public/
|
|
||||||
vendor/
|
|
||||||
coverage/
|
|
||||||
plugins/
|
|
||||||
node_modules/
|
|
||||||
*.d.ts
|
|
||||||
resources/assets/tests/__mocks__/
|
|
||||||
resources/assets/tests/ts-shims/
|
|
||||||
resources/assets/tests/*.ts
|
|
||||||
27
.gitpod.yml
27
.gitpod.yml
|
|
@ -13,28 +13,15 @@ tasks:
|
||||||
php artisan serve --host=0.0.0.0
|
php artisan serve --host=0.0.0.0
|
||||||
- command: gp ports await 8080 && gp preview $(gp url 8000)
|
- command: gp ports await 8080 && gp preview $(gp url 8000)
|
||||||
|
|
||||||
github:
|
|
||||||
prebuilds:
|
|
||||||
# enable for the master/default branch (defaults to true)
|
|
||||||
master: true
|
|
||||||
# enable for all branches in this repo (defaults to false)
|
|
||||||
branches: false
|
|
||||||
# enable for pull requests coming from this repo (defaults to true)
|
|
||||||
pullRequests: true
|
|
||||||
# add a check to pull requests (defaults to true)
|
|
||||||
addCheck: true
|
|
||||||
# add a "Review in Gitpod" button as a comment to pull requests (defaults to false)
|
|
||||||
addComment: false
|
|
||||||
|
|
||||||
vscode:
|
vscode:
|
||||||
extensions:
|
extensions:
|
||||||
- 'editorconfig.editorconfig'
|
- editorconfig.editorconfig
|
||||||
- 'eamodio.gitlens'
|
- eamodio.gitlens
|
||||||
- 'bmewburn.vscode-intelephense-client'
|
- bmewburn.vscode-intelephense-client
|
||||||
- 'esbenp.prettier-vscode'
|
- esbenp.prettier-vscode
|
||||||
- 'jpoissonnier.vscode-styled-components'
|
- jpoissonnier.vscode-styled-components
|
||||||
- 'mblode.twig-language-2'
|
- mblode.twig-language-2
|
||||||
- 'felixfbecker.php-debug'
|
- felixfbecker.php-debug
|
||||||
|
|
||||||
ports:
|
ports:
|
||||||
- port: 8080
|
- port: 8080
|
||||||
|
|
|
||||||
72
.vscode/launch.json
vendored
72
.vscode/launch.json
vendored
|
|
@ -1,38 +1,38 @@
|
||||||
{
|
{
|
||||||
"version": "0.2.0",
|
"version": "0.2.0",
|
||||||
"configurations": [
|
"configurations": [
|
||||||
{
|
{
|
||||||
"type": "node",
|
"type": "node",
|
||||||
"request": "launch",
|
"request": "launch",
|
||||||
"name": "Launch Jest Tests",
|
"name": "Launch Jest Tests",
|
||||||
"program": "${workspaceFolder}/node_modules/.bin/jest",
|
"program": "${workspaceFolder}/node_modules/.bin/jest",
|
||||||
"args": ["${file}"],
|
"args": ["${file}"],
|
||||||
"internalConsoleOptions": "openOnSessionStart",
|
"internalConsoleOptions": "openOnSessionStart",
|
||||||
"skipFiles": [
|
"skipFiles": [
|
||||||
"<node_internals>/**"
|
"<node_internals>/**"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "php",
|
"type": "php",
|
||||||
"request": "launch",
|
"request": "launch",
|
||||||
"name": "Launch with XDebug",
|
"name": "Launch with XDebug",
|
||||||
"ignore": [
|
"ignore": [
|
||||||
"**/vendor/**/*.php"
|
"**/vendor/**/*.php"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "firefox",
|
"type": "firefox",
|
||||||
"request": "launch",
|
"request": "launch",
|
||||||
"reAttach": true,
|
"reAttach": true,
|
||||||
"name": "Launch with Firefox Debugger",
|
"name": "Launch with Firefox Debugger",
|
||||||
"url": "http://localhost/",
|
"url": "http://localhost/",
|
||||||
"webRoot": "${workspaceFolder}",
|
"webRoot": "${workspaceFolder}",
|
||||||
"pathMappings": [
|
"pathMappings": [
|
||||||
{
|
{
|
||||||
"url": "webpack:///",
|
"url": "webpack:///",
|
||||||
"path": "${workspaceFolder}/"
|
"path": "${workspaceFolder}/"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
11
eslint.config.js
Normal file
11
eslint.config.js
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
import {configBuilder} from '@mochaa/eslintrc';
|
||||||
|
|
||||||
|
export default configBuilder({
|
||||||
|
ignores: [
|
||||||
|
'public/',
|
||||||
|
'vendor/',
|
||||||
|
'vendor/',
|
||||||
|
'plugins/',
|
||||||
|
'storage/',
|
||||||
|
],
|
||||||
|
});
|
||||||
132
package.json
132
package.json
|
|
@ -1,15 +1,16 @@
|
||||||
{
|
{
|
||||||
"name": "blessing-skin-server",
|
"name": "blessing-skin-server",
|
||||||
|
"type": "module",
|
||||||
"version": "6.0.2",
|
"version": "6.0.2",
|
||||||
"private": true,
|
"private": true,
|
||||||
|
"packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e",
|
||||||
"description": "A web application brings your custom skins back in offline Minecraft servers.",
|
"description": "A web application brings your custom skins back in offline Minecraft servers.",
|
||||||
|
"author": "printempw",
|
||||||
|
"license": "MIT",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://github.com/bs-community/blessing-skin-server"
|
"url": "https://github.com/bs-community/blessing-skin-server"
|
||||||
},
|
},
|
||||||
"license": "MIT",
|
|
||||||
"author": "printempw",
|
|
||||||
"type": "module",
|
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "vite build",
|
"build": "vite build",
|
||||||
"build:urls": "ts-node tools/generateUrls.ts",
|
"build:urls": "ts-node tools/generateUrls.ts",
|
||||||
|
|
@ -22,109 +23,64 @@
|
||||||
"iOS >= 12.5",
|
"iOS >= 12.5",
|
||||||
"Chrome >= 87"
|
"Chrome >= 87"
|
||||||
],
|
],
|
||||||
"eslintConfig": {
|
|
||||||
"env": {
|
|
||||||
"es2024": true
|
|
||||||
},
|
|
||||||
"parserOptions": {
|
|
||||||
"ecmaVersion": "latest",
|
|
||||||
"sourceType": "module"
|
|
||||||
},
|
|
||||||
"extends": [
|
|
||||||
"xo",
|
|
||||||
"xo-react",
|
|
||||||
"plugin:react/jsx-runtime",
|
|
||||||
"./node_modules/xo/config/plugins.cjs"
|
|
||||||
],
|
|
||||||
"rules": {
|
|
||||||
"import/extensions": "off",
|
|
||||||
"import/no-named-as-default": "off",
|
|
||||||
"n/file-extension-in-import": "off",
|
|
||||||
"unicorn/filename-case": "off",
|
|
||||||
"n/prefer-global/process": "off"
|
|
||||||
},
|
|
||||||
"overrides": [
|
|
||||||
{
|
|
||||||
"files": [
|
|
||||||
"*.ts",
|
|
||||||
"*.tsx"
|
|
||||||
],
|
|
||||||
"extends": [
|
|
||||||
"xo-typescript"
|
|
||||||
],
|
|
||||||
"rules": {
|
|
||||||
"@typescript-eslint/ban-types": "off",
|
|
||||||
"@typescript-eslint/consistent-type-definitions": "warn",
|
|
||||||
"@typescript-eslint/naming-convention": "warn"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"ignorePatterns": [
|
|
||||||
"dist",
|
|
||||||
"public"
|
|
||||||
],
|
|
||||||
"root": true
|
|
||||||
},
|
|
||||||
"resolutions": {
|
|
||||||
"kleur": "^4.1.3"
|
|
||||||
},
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@emotion/react": "^11.11.4",
|
"@emotion/react": "^11.14.0",
|
||||||
"@emotion/styled": "^11.0.0",
|
"@emotion/styled": "^11.14.0",
|
||||||
"@fortawesome/fontawesome-free": "^6.3.0",
|
"@fortawesome/fontawesome-free": "^6.7.2",
|
||||||
"@tweenjs/tween.js": "^23.1.1",
|
"@tweenjs/tween.js": "^25.0.0",
|
||||||
"admin-lte": "next",
|
"admin-lte": "4.0.0-beta3",
|
||||||
"bootstrap": "^5.3.3",
|
"bootstrap": "^5.3.3",
|
||||||
"clsx": "^2.1.0",
|
"clsx": "^2.1.1",
|
||||||
"echarts": "^5.5.0",
|
"downshift": "^9.0.8",
|
||||||
"immer": "^10.0.3",
|
"echarts": "^5.6.0",
|
||||||
|
"immer": "^10.1.1",
|
||||||
"jquery": "^3.6.0",
|
"jquery": "^3.6.0",
|
||||||
"lodash-es": "^4.0.8",
|
"lodash-es": "^4.0.8",
|
||||||
"nanoid": "^5.0.6",
|
"nanoid": "^5.0.9",
|
||||||
"prompts": "^2.4.0",
|
"prompts": "^2.4.0",
|
||||||
"react": "^18.2.0",
|
"react": "^18.0.0",
|
||||||
"react-autosuggest": "^10.0.2",
|
"react-dom": "^18.0.0",
|
||||||
"react-dom": "^18.2.0",
|
|
||||||
"react-draggable": "^4.4.2",
|
"react-draggable": "^4.4.2",
|
||||||
"react-loading-skeleton": "^3.4.0",
|
"react-loading-skeleton": "^3.5.0",
|
||||||
"react-use": "^17.5.0",
|
"react-use": "^17.6.0",
|
||||||
"reaptcha": "^1.7.2",
|
"reaptcha": "^1.7.2",
|
||||||
"rxjs": "^7.8.1",
|
"rxjs": "^7.8.1",
|
||||||
"skinview-utils": "^0.7.1",
|
"skinview-utils": "^0.7.1",
|
||||||
"skinview3d": "^3.0.0-alpha.1",
|
"skinview3d": "^3.1.0",
|
||||||
"spectre.css": "npm:@angular-package/spectre.css",
|
"spectre.css": "github:angular-package/spectre.css",
|
||||||
"use-immer": "^0.9.0"
|
"use-immer": "^0.11.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@testing-library/jest-dom": "^6.4.2",
|
"@eslint-react/eslint-plugin": "^1.23.2",
|
||||||
"@testing-library/react": "^14.2.1",
|
"@mochaa/eslintrc": "^0.1.12",
|
||||||
"@tsconfig/vite-react": "^3.0.0",
|
"@testing-library/jest-dom": "^6.6.3",
|
||||||
|
"@testing-library/react": "^16.2.0",
|
||||||
|
"@tsconfig/vite-react": "^3.4.0",
|
||||||
"@types/bootstrap": "^5.2.10",
|
"@types/bootstrap": "^5.2.10",
|
||||||
"@types/jquery": "^3.5.13",
|
"@types/jquery": "^3.5.32",
|
||||||
"@types/js-yaml": "^4.0.9",
|
"@types/js-yaml": "^4.0.9",
|
||||||
"@types/lodash-es": "^4.0.6",
|
"@types/lodash-es": "^4.0.6",
|
||||||
"@types/prompts": "^2.0.9",
|
"@types/prompts": "^2.0.9",
|
||||||
"@types/react": "^18.2.62",
|
"@types/react": "^18",
|
||||||
"@types/react-autosuggest": "^10.1.11",
|
"@types/react-dom": "^18",
|
||||||
"@types/react-dom": "^18.2.19",
|
|
||||||
"@types/tween.js": "^18.5.0",
|
"@types/tween.js": "^18.5.0",
|
||||||
"@vitejs/plugin-react-swc": "^3.6.0",
|
"@vitejs/plugin-react-swc": "^3.7.2",
|
||||||
"autoprefixer": "^10.4.18",
|
"autoprefixer": "^10.4.20",
|
||||||
"browserslist": "^4.23.0",
|
"browserslist": "^4.24.4",
|
||||||
"browserslist-to-esbuild": "^2.1.1",
|
"browserslist-to-esbuild": "^2.1.1",
|
||||||
"eslint-config-xo-react": "^0.27.0",
|
"eslint": "^9.18.0",
|
||||||
"eslint-plugin-react": "^7.34.0",
|
"eslint-plugin-react-hooks": "^5.1.0",
|
||||||
"eslint-plugin-react-hooks": "^4.6.0",
|
"eslint-plugin-react-refresh": "^0.4.18",
|
||||||
"js-yaml": "^4.1.0",
|
"js-yaml": "^4.1.0",
|
||||||
"laravel-vite-plugin": "^1.0.2",
|
"laravel-vite-plugin": "^1.1.1",
|
||||||
"postcss": "^8.4.35",
|
"postcss": "^8.5.1",
|
||||||
"sass": "^1.71.1",
|
"sass": "^1.83.4",
|
||||||
"typescript": "^5.3.3",
|
"typescript": "^5.7.3",
|
||||||
"vite": "^5.1.5",
|
"vite": "^6.0.7",
|
||||||
"vite-plugin-top-level-await": "^1.4.1",
|
"vitest": "^3.0.2"
|
||||||
"vite-plugin-wasm": "^3.3.0",
|
},
|
||||||
"vitest": "^1.3.1",
|
"resolutions": {
|
||||||
"xo": "^0.57.0"
|
"kleur": "^4.1.3"
|
||||||
},
|
},
|
||||||
"postcss": {
|
"postcss": {
|
||||||
"plugins": {
|
"plugins": {
|
||||||
|
|
|
||||||
1
public/.gitignore
vendored
1
public/.gitignore
vendored
|
|
@ -1,2 +1,3 @@
|
||||||
app/
|
app/
|
||||||
build/
|
build/
|
||||||
|
hot
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,3 @@
|
||||||
|
|
||||||
type AlertType = 'success' | 'info' | 'warning' | 'danger';
|
type AlertType = 'success' | 'info' | 'warning' | 'danger';
|
||||||
|
|
||||||
const icons = new Map<AlertType, string>([
|
const icons = new Map<AlertType, string>([
|
||||||
|
|
@ -13,16 +12,17 @@ type Properties = {
|
||||||
readonly children?: React.ReactNode;
|
readonly children?: React.ReactNode;
|
||||||
};
|
};
|
||||||
|
|
||||||
const Alert: React.FC<Properties> = properties => {
|
const Alert: React.FC<Properties> = ({type, children}) => {
|
||||||
const {type} = properties;
|
|
||||||
const icon = icons.get(type);
|
const icon = icons.get(type);
|
||||||
|
|
||||||
return properties.children ? (
|
return children === ''
|
||||||
<div className={`alert alert-${type}`}>
|
? null
|
||||||
<i className={`icon fas fa-${icon}`}/>
|
: (
|
||||||
{properties.children}
|
<div className={`alert alert-${type}`}>
|
||||||
</div>
|
<i className={`icon fas fa-${icon}`}/>
|
||||||
) : null;
|
{children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Alert;
|
export default Alert;
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,10 @@
|
||||||
|
|
||||||
type Properties = {
|
type Properties = {
|
||||||
readonly title?: string;
|
readonly title?: string;
|
||||||
readonly onClick: React.MouseEventHandler<HTMLAnchorElement>;
|
readonly onClick: React.MouseEventHandler<HTMLAnchorElement>;
|
||||||
};
|
};
|
||||||
|
|
||||||
const ButtonEdit: React.FC<Properties> = properties => (
|
const ButtonEdit: React.FC<Properties> = ({title, onClick}) => (
|
||||||
<a href='#' title={properties.title} className='ml-2' onClick={properties.onClick}>
|
<a href='#' title={title} className='ml-2' onClick={onClick}>
|
||||||
<i className='fas fa-edit'/>
|
<i className='fas fa-edit'/>
|
||||||
</a>
|
</a>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,10 @@
|
||||||
/** @jsxImportSource @emotion/react */
|
|
||||||
import React from 'react';
|
|
||||||
import Reaptcha from 'reaptcha';
|
|
||||||
import {emit, on} from '@/scripts/event';
|
import {emit, on} from '@/scripts/event';
|
||||||
import {t} from '@/scripts/i18n';
|
import {t} from '@/scripts/i18n';
|
||||||
import * as cssUtils from '@/styles/utils';
|
import * as cssUtils from '@/styles/utils';
|
||||||
|
import React from 'react';
|
||||||
|
import Reaptcha from 'reaptcha';
|
||||||
|
|
||||||
const eventId = Symbol();
|
const eventId = Symbol('EventId');
|
||||||
|
|
||||||
type State = {
|
type State = {
|
||||||
value: string;
|
value: string;
|
||||||
|
|
@ -16,20 +15,22 @@ type State = {
|
||||||
|
|
||||||
class Captcha extends React.Component<Record<string, unknown>, State> {
|
class Captcha extends React.Component<Record<string, unknown>, State> {
|
||||||
state: State;
|
state: State;
|
||||||
ref: React.MutableRefObject<Reaptcha | undefined>;
|
// eslint-disable-next-line ts/no-restricted-types
|
||||||
|
ref: React.RefObject<Reaptcha | null>;
|
||||||
|
|
||||||
constructor(properties: Record<string, unknown>) {
|
constructor(properties: Record<string, unknown>) {
|
||||||
super(properties);
|
super(properties);
|
||||||
this.state = {
|
this.state = {
|
||||||
value: '',
|
value: '',
|
||||||
time: Date.now(),
|
time: Date.now(),
|
||||||
sitekey: blessing.extra.recaptcha,
|
sitekey: blessing.extra.recaptcha as string,
|
||||||
invisible: blessing.extra.invisible,
|
invisible: blessing.extra.invisible as boolean,
|
||||||
};
|
};
|
||||||
this.ref = React.createRef();
|
this.ref = React.createRef<Reaptcha>();
|
||||||
}
|
}
|
||||||
|
|
||||||
execute = async () => {
|
// eslint-disable-next-line react/no-unused-class-component-members
|
||||||
|
async execute() {
|
||||||
const recaptcha = this.ref.current;
|
const recaptcha = this.ref.current;
|
||||||
if (recaptcha && this.state.invisible) {
|
if (recaptcha && this.state.invisible) {
|
||||||
return new Promise<string>(resolve => {
|
return new Promise<string>(resolve => {
|
||||||
|
|
@ -37,21 +38,22 @@ class Captcha extends React.Component<Record<string, unknown>, State> {
|
||||||
resolve(value);
|
resolve(value);
|
||||||
off();
|
off();
|
||||||
});
|
});
|
||||||
recaptcha.execute();
|
void recaptcha.execute();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.state.value;
|
return this.state.value;
|
||||||
};
|
}
|
||||||
|
|
||||||
reset = () => {
|
// eslint-disable-next-line react/no-unused-class-component-members
|
||||||
|
reset() {
|
||||||
const recaptcha = this.ref.current;
|
const recaptcha = this.ref.current;
|
||||||
if (recaptcha) {
|
if (recaptcha) {
|
||||||
recaptcha.reset();
|
void recaptcha.reset();
|
||||||
} else {
|
} else {
|
||||||
this.setState({time: Date.now()});
|
this.setState({time: Date.now()});
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
handleValueChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
handleValueChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
this.setState({value: event.target.value});
|
this.setState({value: event.target.value});
|
||||||
|
|
@ -67,37 +69,39 @@ class Captcha extends React.Component<Record<string, unknown>, State> {
|
||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
return this.state.sitekey ? (
|
return this.state.sitekey
|
||||||
<div className='mb-2'>
|
? (
|
||||||
<Reaptcha
|
<div className='mb-2'>
|
||||||
ref={this.ref}
|
<Reaptcha
|
||||||
sitekey={this.state.sitekey}
|
ref={this.ref}
|
||||||
size={this.state.invisible ? 'invisible' : 'normal'}
|
sitekey={this.state.sitekey}
|
||||||
onVerify={this.handleVerify}
|
size={this.state.invisible ? 'invisible' : 'normal'}
|
||||||
/>
|
onVerify={this.handleVerify}
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div className='d-flex'>
|
|
||||||
<div className='form-group mb-3 mr-2'>
|
|
||||||
<input
|
|
||||||
required
|
|
||||||
type='text'
|
|
||||||
className='form-control'
|
|
||||||
placeholder={t('auth.captcha')}
|
|
||||||
value={this.state.value}
|
|
||||||
onChange={this.handleValueChange}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<img
|
)
|
||||||
src={`${blessing.base_url}/auth/captcha?v=${this.state.time}`}
|
: (
|
||||||
alt={t('auth.captcha')}
|
<div className='d-flex'>
|
||||||
css={cssUtils.pointerCursor}
|
<div className='form-group mb-3 mr-2'>
|
||||||
height={34}
|
<input
|
||||||
title={t('auth.change-captcha')}
|
required
|
||||||
onClick={this.handleRefresh}
|
type='text'
|
||||||
/>
|
className='form-control'
|
||||||
</div>
|
placeholder={t('auth.captcha')}
|
||||||
);
|
value={this.state.value}
|
||||||
|
onChange={this.handleValueChange}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<img
|
||||||
|
src={`${blessing.base_url}/auth/captcha?v=${this.state.time}`}
|
||||||
|
alt={t('auth.captcha')}
|
||||||
|
css={cssUtils.pointerCursor}
|
||||||
|
height={34}
|
||||||
|
title={t('auth.change-captcha')}
|
||||||
|
onClick={this.handleRefresh}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import {useState} from 'react';
|
|
||||||
import * as fetch from '@/scripts/net';
|
import * as fetch from '@/scripts/net';
|
||||||
|
import {useState} from 'react';
|
||||||
|
|
||||||
type Properties = {
|
type Properties = {
|
||||||
readonly initMode: boolean;
|
readonly initMode: boolean;
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,9 @@
|
||||||
/** @jsxImportSource @emotion/react */
|
|
||||||
|
|
||||||
import {useState, useEffect} from 'react';
|
|
||||||
import Autosuggest from 'react-autosuggest';
|
|
||||||
import {css} from '@emotion/react';
|
|
||||||
import {emit} from '@/scripts/event';
|
import {emit} from '@/scripts/event';
|
||||||
import {pointerCursor} from '@/styles/utils';
|
import {pointerCursor} from '@/styles/utils';
|
||||||
|
import {css} from '@emotion/react';
|
||||||
|
import clsx from 'clsx';
|
||||||
|
import {useCombobox} from 'downshift';
|
||||||
|
import {useEffect, useState} from 'react';
|
||||||
|
|
||||||
const styles = css`
|
const styles = css`
|
||||||
.dropdown-menu li {
|
.dropdown-menu li {
|
||||||
|
|
@ -14,75 +13,53 @@ const styles = css`
|
||||||
|
|
||||||
const domainNames = new Set(['qq.com', '163.com', 'gmail.com', 'hotmail.com']);
|
const domainNames = new Set(['qq.com', '163.com', 'gmail.com', 'hotmail.com']);
|
||||||
|
|
||||||
type Properties = Omit<Autosuggest.InputProps<string>, 'onChange'> & {
|
type Properties = Omit<React.InputHTMLAttributes<HTMLInputElement>, 'onChange'> & {
|
||||||
onChange(value: string): void;
|
onChange: (value: string) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
const EmailSuggestion: React.FC<Properties> = properties => {
|
const EmailSuggestion: React.FC<Properties> = props => {
|
||||||
const [suggestions, setSuggestions] = useState<string[]>([]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
emit('emailDomainsSuggestion', domainNames);
|
emit('emailDomainsSuggestion', domainNames);
|
||||||
}, []);
|
}, []);
|
||||||
|
const [inputItems, setInputItems] = useState<string[]>([]);
|
||||||
|
|
||||||
const handleSuggestionsFetchRequested: Autosuggest.SuggestionsFetchRequested
|
const {
|
||||||
= ({value}) => {
|
isOpen,
|
||||||
const segments = value.split('@');
|
getLabelProps,
|
||||||
setSuggestions([...domainNames].map(name => `${segments[0]}@${name}`));
|
getMenuProps,
|
||||||
};
|
getInputProps,
|
||||||
|
highlightedIndex,
|
||||||
|
getItemProps,
|
||||||
|
} = useCombobox({
|
||||||
|
items: inputItems,
|
||||||
|
onInputValueChange({inputValue: value}) {
|
||||||
|
setInputItems([...domainNames].map(name => `${value.split('@')[0]}@${name}`));
|
||||||
|
if (value.length === 0 || value.includes('@')) {
|
||||||
|
setInputItems([]);
|
||||||
|
}
|
||||||
|
|
||||||
const handleSuggestionsClearRequested = () => {
|
const {onChange} = props;
|
||||||
setSuggestions([]);
|
onChange(value);
|
||||||
};
|
},
|
||||||
|
});
|
||||||
|
|
||||||
const shouldRenderSuggestions = (value: string) => {
|
return (
|
||||||
const isSelecting = [...domainNames].some(name =>
|
<div>
|
||||||
value.endsWith(`@${name}`),
|
<div className='input-group'>
|
||||||
);
|
<input className='form-control' {...{...props, onChange: undefined}} {...getInputProps()}/>
|
||||||
|
<div className='input-group-text' {...getLabelProps()}>
|
||||||
return isSelecting || (value.length > 0 && !value.includes('@'));
|
|
||||||
};
|
|
||||||
|
|
||||||
const getSuggestionValue = (value: string) => value;
|
|
||||||
|
|
||||||
const renderSuggestion = (suggestion: string) => suggestion;
|
|
||||||
|
|
||||||
const handleChange = (_: React.FormEvent, event: Autosuggest.ChangeEvent) => {
|
|
||||||
properties.onChange(event.newValue);
|
|
||||||
};
|
|
||||||
|
|
||||||
const renderInputComponent = (
|
|
||||||
properties_: Omit<Autosuggest.InputProps<string>, 'onChange'>,
|
|
||||||
) => (
|
|
||||||
<div className='input-group'>
|
|
||||||
<input className='form-control' {...properties_}/>
|
|
||||||
<div className='input-group-append'>
|
|
||||||
<div className='input-group-text'>
|
|
||||||
<i className='fas fa-envelope'/>
|
<i className='fas fa-envelope'/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div className='mb-3 dropdown' css={styles}>
|
||||||
);
|
<ul className={clsx('dropdown-menu', isOpen && inputItems.length > 0 && 'show')} {...getMenuProps()}>
|
||||||
|
{isOpen && inputItems.length > 0 && inputItems.map((item, index) => (
|
||||||
return (
|
<li key={`${item}`} className={clsx('dropdown-item', {active: index === highlightedIndex})} {...getItemProps({item, index})}>
|
||||||
<div css={styles}>
|
{item}
|
||||||
<Autosuggest
|
</li>
|
||||||
suggestions={suggestions}
|
))}
|
||||||
getSuggestionValue={getSuggestionValue}
|
</ul>
|
||||||
renderSuggestion={renderSuggestion}
|
</div>
|
||||||
shouldRenderSuggestions={shouldRenderSuggestions}
|
|
||||||
inputProps={({...properties, onChange: handleChange})}
|
|
||||||
renderInputComponent={renderInputComponent}
|
|
||||||
theme={{
|
|
||||||
container: 'mb-3',
|
|
||||||
suggestion: 'dropdown-item',
|
|
||||||
suggestionsContainer: 'dropdown',
|
|
||||||
suggestionsList: `dropdown-menu ${suggestions.length > 0 ? 'show' : ''}`,
|
|
||||||
suggestionHighlighted: 'active',
|
|
||||||
}}
|
|
||||||
onSuggestionsFetchRequested={handleSuggestionsFetchRequested}
|
|
||||||
onSuggestionsClearRequested={handleSuggestionsClearRequested}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
/** @jsxImportSource @emotion/react */
|
|
||||||
import {useRef} from 'react';
|
|
||||||
import {css} from '@emotion/react';
|
|
||||||
import {t} from '@/scripts/i18n';
|
import {t} from '@/scripts/i18n';
|
||||||
|
import {css} from '@emotion/react';
|
||||||
|
import {useRef} from 'react';
|
||||||
|
|
||||||
const hideRawBrowseButton = css`
|
const hideRawBrowseButton = css`
|
||||||
::after {
|
::after {
|
||||||
|
|
@ -12,7 +11,7 @@ const hideRawBrowseButton = css`
|
||||||
type Properties = {
|
type Properties = {
|
||||||
file: File | undefined;
|
file: File | undefined;
|
||||||
accept?: string;
|
accept?: string;
|
||||||
onChange(event: React.ChangeEvent<HTMLInputElement>): void;
|
onChange: (event: React.ChangeEvent<HTMLInputElement>) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
const FileInput: React.FC<Properties> = properties => {
|
const FileInput: React.FC<Properties> = properties => {
|
||||||
|
|
|
||||||
|
|
@ -1,16 +1,16 @@
|
||||||
import {useState, useEffect, useRef} from 'react';
|
import {Modal as BootstrapModal} from 'bootstrap';
|
||||||
import $ from 'jquery';
|
import clsx from 'clsx';
|
||||||
import 'bootstrap';
|
import {useEffect, useRef, useState} from 'react';
|
||||||
import {t} from '../scripts/i18n';
|
import {t} from '../scripts/i18n';
|
||||||
import ModalHeader, {type Props as HeaderProperties} from './ModalHeader';
|
|
||||||
import ModalBody, {type Props as BodyProperties} from './ModalBody';
|
import ModalBody, {type Props as BodyProperties} from './ModalBody';
|
||||||
import ModalFooter, {type Props as FooterProperties} from './ModalFooter';
|
import ModalFooter, {type Props as FooterProperties} from './ModalFooter';
|
||||||
|
import ModalHeader, {type Props as HeaderProperties} from './ModalHeader';
|
||||||
|
|
||||||
type BasicOptions = {
|
type BasicOptions = {
|
||||||
readonly mode?: 'alert' | 'confirm' | 'prompt';
|
readonly mode?: 'alert' | 'confirm' | 'prompt';
|
||||||
readonly show?: boolean;
|
readonly show?: boolean;
|
||||||
readonly input?: string;
|
readonly input?: string;
|
||||||
validator?(value: any): string | boolean | undefined;
|
validator?: (value: any) => string | boolean | undefined;
|
||||||
readonly type?: string;
|
readonly type?: string;
|
||||||
readonly showHeader?: boolean;
|
readonly showHeader?: boolean;
|
||||||
readonly center?: boolean;
|
readonly center?: boolean;
|
||||||
|
|
@ -23,9 +23,9 @@ type Properties = {
|
||||||
readonly id?: string;
|
readonly id?: string;
|
||||||
readonly children?: React.ReactNode;
|
readonly children?: React.ReactNode;
|
||||||
readonly footer?: React.ReactNode;
|
readonly footer?: React.ReactNode;
|
||||||
onConfirm?(payload: {value: string}): void;
|
onConfirm?: (payload: {value: string}) => void;
|
||||||
onDismiss?(): void;
|
onDismiss?: () => void;
|
||||||
onClose?(): void;
|
onClose?: () => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type ModalResult = {
|
export type ModalResult = {
|
||||||
|
|
@ -49,14 +49,36 @@ const Modal: React.FC<ModalOptions & Properties> = properties => {
|
||||||
cancelButtonText = t('general.cancel'),
|
cancelButtonText = t('general.cancel'),
|
||||||
cancelButtonType = 'secondary',
|
cancelButtonType = 'secondary',
|
||||||
flexFooter = false,
|
flexFooter = false,
|
||||||
|
footer,
|
||||||
|
show,
|
||||||
|
onClose,
|
||||||
|
onDismiss,
|
||||||
|
id,
|
||||||
|
validator,
|
||||||
|
onConfirm,
|
||||||
|
children,
|
||||||
|
choices,
|
||||||
|
dangerousHTML: html,
|
||||||
} = properties;
|
} = properties;
|
||||||
|
|
||||||
const [value, setValue] = useState(input);
|
const [value, setValue] = useState(input);
|
||||||
const [valid, setValid] = useState(true);
|
const [valid, setValid] = useState(true);
|
||||||
const [validatorMessage, setValidatorMessage] = useState('');
|
const [validatorMessage, setValidatorMessage] = useState('');
|
||||||
const reference = useRef<HTMLDivElement>(null);
|
const reference = useRef<HTMLDivElement>(null);
|
||||||
|
const [modal, setModal] = useState<BootstrapModal>();
|
||||||
|
|
||||||
const {show, onClose} = properties;
|
useEffect(() => {
|
||||||
|
if (!reference.current) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const _modal = new BootstrapModal(reference.current);
|
||||||
|
setModal(_modal);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
_modal.dispose();
|
||||||
|
};
|
||||||
|
}, [reference]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!show) {
|
if (!show) {
|
||||||
|
|
@ -64,23 +86,26 @@ const Modal: React.FC<ModalOptions & Properties> = properties => {
|
||||||
}
|
}
|
||||||
|
|
||||||
const onHidden = () => {
|
const onHidden = () => {
|
||||||
onClose ? onClose() : void 0;
|
onClose?.();
|
||||||
};
|
};
|
||||||
|
|
||||||
const element = $(reference.current!);
|
const element = reference.current;
|
||||||
element.on('hidden.bs.modal', onHidden);
|
if (!element) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
element.addEventListener('hidden.bs.modal', onHidden);
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
element.off('hidden.bs.modal', onHidden);
|
element.removeEventListener('hidden.bs.modal', onHidden);
|
||||||
};
|
};
|
||||||
}, [show, onClose]);
|
}, [reference, show, onClose]);
|
||||||
|
|
||||||
const handleInputChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
const handleInputChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
setValue(event.target.value);
|
setValue(event.target.value);
|
||||||
};
|
};
|
||||||
|
|
||||||
const confirm = () => {
|
const confirm = () => {
|
||||||
const {validator} = properties;
|
|
||||||
if (typeof validator === 'function') {
|
if (typeof validator === 'function') {
|
||||||
const result = validator(value);
|
const result = validator(value);
|
||||||
if (typeof result === 'string') {
|
if (typeof result === 'string') {
|
||||||
|
|
@ -90,49 +115,54 @@ const Modal: React.FC<ModalOptions & Properties> = properties => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
properties.onConfirm?.({value});
|
onConfirm?.({value});
|
||||||
$(reference.current!).modal('hide');
|
modal?.hide();
|
||||||
|
|
||||||
// The "hidden.bs.modal" event can't be trigged automatically when testing.
|
// The "hidden.bs.modal" event can't be trigged automatically when testing.
|
||||||
|
|
||||||
if (process.env.NODE_ENV === 'test') {
|
if (import.meta.env.NODE_ENV === 'test') {
|
||||||
$(reference.current!).trigger('hidden.bs.modal');
|
$(reference.current!).trigger('hidden.bs.modal');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const dismiss = () => {
|
const dismiss = () => {
|
||||||
properties.onDismiss?.();
|
onDismiss?.();
|
||||||
$(reference.current!).modal('hide');
|
modal?.hide();
|
||||||
|
|
||||||
if (process.env.NODE_ENV === 'test') {
|
if (import.meta.env.NODE_ENV === 'test') {
|
||||||
$(reference.current!).trigger('hidden.bs.modal');
|
$(reference.current!).trigger('hidden.bs.modal');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (show) {
|
if (show && modal) {
|
||||||
setTimeout(() => $(reference.current!).modal('show'), 50);
|
const timeout = setTimeout(() => {
|
||||||
|
modal.show();
|
||||||
|
}, 50);
|
||||||
|
return () => {
|
||||||
|
clearTimeout(timeout);
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}, [show]);
|
}, [show, modal]);
|
||||||
|
|
||||||
if (!show) {
|
if (!show) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div ref={reference} id={properties.id} className='modal fade' role='dialog'>
|
<div ref={reference} id={id} className='modal fade' role='dialog'>
|
||||||
<div
|
<div
|
||||||
className={`modal-dialog ${center ? 'modal-dialog-centered' : ''}`}
|
className={clsx('modal-dialog', center && 'modal-dialog-centered')}
|
||||||
role='document'
|
role='document'
|
||||||
>
|
>
|
||||||
<div className={`modal-content bg-${type}`}>
|
<div className={`modal-content bg-${type}`}>
|
||||||
<ModalHeader show={showHeader} title={title} onDismiss={dismiss}/>
|
<ModalHeader show={showHeader} title={title} onDismiss={dismiss}/>
|
||||||
<ModalBody
|
<ModalBody
|
||||||
text={text}
|
text={text}
|
||||||
dangerousHTML={properties.dangerousHTML}
|
dangerousHTML={html}
|
||||||
showInput={mode === 'prompt'}
|
showInput={mode === 'prompt'}
|
||||||
value={value}
|
value={value}
|
||||||
choices={properties.choices}
|
choices={choices}
|
||||||
inputType={inputType}
|
inputType={inputType}
|
||||||
inputMode={inputMode}
|
inputMode={inputMode}
|
||||||
placeholder={placeholder}
|
placeholder={placeholder}
|
||||||
|
|
@ -140,7 +170,7 @@ const Modal: React.FC<ModalOptions & Properties> = properties => {
|
||||||
validatorMessage={validatorMessage}
|
validatorMessage={validatorMessage}
|
||||||
onChange={handleInputChange}
|
onChange={handleInputChange}
|
||||||
>
|
>
|
||||||
{properties.children}
|
{children}
|
||||||
</ModalBody>
|
</ModalBody>
|
||||||
<ModalFooter
|
<ModalFooter
|
||||||
showCancelButton={mode !== 'alert'}
|
showCancelButton={mode !== 'alert'}
|
||||||
|
|
@ -152,7 +182,7 @@ const Modal: React.FC<ModalOptions & Properties> = properties => {
|
||||||
onConfirm={confirm}
|
onConfirm={confirm}
|
||||||
onDismiss={dismiss}
|
onDismiss={dismiss}
|
||||||
>
|
>
|
||||||
{properties.footer}
|
{footer}
|
||||||
</ModalFooter>
|
</ModalFooter>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -2,8 +2,9 @@
|
||||||
import ModalContent, {type Props as ContentProperties} from './ModalContent';
|
import ModalContent, {type Props as ContentProperties} from './ModalContent';
|
||||||
import ModalInput, {
|
import ModalInput, {
|
||||||
type
|
type
|
||||||
Props as InputProperties, type
|
|
||||||
InternalProps as InputInteralProperties,
|
InternalProps as InputInteralProperties,
|
||||||
|
type
|
||||||
|
Props as InputProperties,
|
||||||
} from './ModalInput';
|
} from './ModalInput';
|
||||||
|
|
||||||
type InternalProperties = {
|
type InternalProperties = {
|
||||||
|
|
|
||||||
|
|
@ -13,9 +13,8 @@ const ModalContent: React.FC<Props> = properties => {
|
||||||
if (properties.text) {
|
if (properties.text) {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{properties.text.split(/\r?\n/).map((line, i) => (
|
{properties.text.split(/\r?\n/).map((line, i) =>
|
||||||
<p key={i}>{line}</p>
|
<p key={i}>{line}</p>)}
|
||||||
))}
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -10,8 +10,8 @@ export type Props = {
|
||||||
|
|
||||||
type InternalProperties = {
|
type InternalProperties = {
|
||||||
readonly showCancelButton: boolean;
|
readonly showCancelButton: boolean;
|
||||||
onConfirm?(): void;
|
onConfirm?: () => void;
|
||||||
onDismiss?(): void;
|
onDismiss?: () => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
const ModalFooter: React.FC<InternalProperties & Props> = properties => {
|
const ModalFooter: React.FC<InternalProperties & Props> = properties => {
|
||||||
|
|
@ -22,29 +22,29 @@ const ModalFooter: React.FC<InternalProperties & Props> = properties => {
|
||||||
|
|
||||||
const footerClass = classes.join(' ');
|
const footerClass = classes.join(' ');
|
||||||
|
|
||||||
return properties.children ? (
|
return properties.children
|
||||||
<div className={footerClass}>{properties.children}</div>
|
? <div className={footerClass}>{properties.children}</div>
|
||||||
) : (
|
: (
|
||||||
<div className={footerClass}>
|
<div className={footerClass}>
|
||||||
{properties.showCancelButton && (
|
{properties.showCancelButton && (
|
||||||
|
<button
|
||||||
|
type='button'
|
||||||
|
className={`btn btn-${properties.cancelButtonType}`}
|
||||||
|
data-dismiss='modal'
|
||||||
|
onClick={properties.onDismiss}
|
||||||
|
>
|
||||||
|
{properties.cancelButtonText}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
<button
|
<button
|
||||||
type='button'
|
type='button'
|
||||||
className={`btn btn-${properties.cancelButtonType}`}
|
className={`btn btn-${properties.okButtonType}`}
|
||||||
data-dismiss='modal'
|
onClick={properties.onConfirm}
|
||||||
onClick={properties.onDismiss}
|
|
||||||
>
|
>
|
||||||
{properties.cancelButtonText}
|
{properties.okButtonText}
|
||||||
</button>
|
</button>
|
||||||
)}
|
</div>
|
||||||
<button
|
);
|
||||||
type='button'
|
|
||||||
className={`btn btn-${properties.okButtonType}`}
|
|
||||||
onClick={properties.onConfirm}
|
|
||||||
>
|
|
||||||
{properties.okButtonText}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default ModalFooter;
|
export default ModalFooter;
|
||||||
|
|
|
||||||
|
|
@ -4,24 +4,24 @@ export type Props = {
|
||||||
};
|
};
|
||||||
|
|
||||||
type InternalProperties = {
|
type InternalProperties = {
|
||||||
onDismiss?(): void;
|
onDismiss?: () => void;
|
||||||
readonly show?: boolean;
|
readonly show?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
const ModalHeader: React.FC<Props & InternalProperties> = properties =>
|
const ModalHeader: React.FC<Props & InternalProperties> = ({show, title, onDismiss}) =>
|
||||||
properties.show ? (
|
show
|
||||||
<div className='modal-header'>
|
? (
|
||||||
<h5 className='modal-title'>{properties.title}</h5>
|
<div className='modal-header'>
|
||||||
<button
|
<h5 className='modal-title'>{title}</h5>
|
||||||
type='button'
|
<button
|
||||||
className='close'
|
type='button'
|
||||||
data-dismiss='modal'
|
className='btn-close'
|
||||||
aria-label='Close'
|
data-bs-dismiss='modal'
|
||||||
onClick={properties.onDismiss}
|
aria-label='Close'
|
||||||
>
|
onClick={onDismiss}
|
||||||
<span aria-hidden>×</span>
|
/>
|
||||||
</button>
|
</div>
|
||||||
</div>
|
)
|
||||||
) : null;
|
: null;
|
||||||
|
|
||||||
export default ModalHeader;
|
export default ModalHeader;
|
||||||
|
|
|
||||||
|
|
@ -15,36 +15,38 @@ export type InternalProps = {
|
||||||
|
|
||||||
const ModalInput: React.FC<InternalProps & Props> = properties => (
|
const ModalInput: React.FC<InternalProps & Props> = properties => (
|
||||||
<>
|
<>
|
||||||
{properties.inputType === 'radios' && properties.choices ? (
|
{properties.inputType === 'radios' && properties.choices
|
||||||
<>
|
? (
|
||||||
{properties.choices.map(choice => (
|
<>
|
||||||
<div key={choice.value}>
|
{properties.choices.map(choice => (
|
||||||
<input
|
<div key={choice.value}>
|
||||||
type='radio'
|
<input
|
||||||
name='modal-radios'
|
type='radio'
|
||||||
id={`modal-radio-${choice.value}`}
|
name='modal-radios'
|
||||||
value={choice.value}
|
id={`modal-radio-${choice.value}`}
|
||||||
checked={choice.value === properties.value}
|
value={choice.value}
|
||||||
onChange={properties.onChange}
|
checked={choice.value === properties.value}
|
||||||
/>
|
onChange={properties.onChange}
|
||||||
<label htmlFor={`modal-radio-${choice.value}`} className='ml-1'>
|
/>
|
||||||
{choice.text}
|
<label htmlFor={`modal-radio-${choice.value}`} className='ml-1'>
|
||||||
</label>
|
{choice.text}
|
||||||
</div>
|
</label>
|
||||||
))}
|
</div>
|
||||||
</>
|
))}
|
||||||
) : (
|
</>
|
||||||
<div className='form-group'>
|
)
|
||||||
<input
|
: (
|
||||||
value={properties.value}
|
<div className='form-group'>
|
||||||
type={properties.inputType}
|
<input
|
||||||
inputMode={properties.inputMode}
|
value={properties.value}
|
||||||
className='form-control'
|
type={properties.inputType}
|
||||||
placeholder={properties.placeholder}
|
inputMode={properties.inputMode}
|
||||||
onChange={properties.onChange}
|
className='form-control'
|
||||||
/>
|
placeholder={properties.placeholder}
|
||||||
</div>
|
onChange={properties.onChange}
|
||||||
)}
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
{properties.invalid && (
|
{properties.invalid && (
|
||||||
<div className='alert alert-danger'>
|
<div className='alert alert-danger'>
|
||||||
<i className='icon far fa-times-circle'/>
|
<i className='icon far fa-times-circle'/>
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,11 @@
|
||||||
|
|
||||||
import PaginationItem from './PaginationItem';
|
|
||||||
import {t} from '@/scripts/i18n';
|
import {t} from '@/scripts/i18n';
|
||||||
|
import PaginationItem from './PaginationItem';
|
||||||
|
|
||||||
type Properties = {
|
type Properties = {
|
||||||
readonly page: number;
|
readonly page: number;
|
||||||
readonly totalPages: number;
|
readonly totalPages: number;
|
||||||
onChange(page: number): void | Promise<void>;
|
onChange: (page: number) => void | Promise<void>;
|
||||||
};
|
};
|
||||||
|
|
||||||
const labels = {
|
const labels = {
|
||||||
|
|
@ -32,8 +32,8 @@ const Pagination: React.FC<Properties> = properties => {
|
||||||
{t('vendor.datatable.prev')}
|
{t('vendor.datatable.prev')}
|
||||||
</span>
|
</span>
|
||||||
</PaginationItem>
|
</PaginationItem>
|
||||||
{totalPages < 8 ? (
|
{totalPages < 8
|
||||||
Array.from({length: totalPages}).map((_, i) => (
|
? Array.from({length: totalPages}).map((_, i) => (
|
||||||
<PaginationItem
|
<PaginationItem
|
||||||
key={i}
|
key={i}
|
||||||
className='d-none d-sm-block'
|
className='d-none d-sm-block'
|
||||||
|
|
@ -43,33 +43,10 @@ const Pagination: React.FC<Properties> = properties => {
|
||||||
{i + 1}
|
{i + 1}
|
||||||
</PaginationItem>
|
</PaginationItem>
|
||||||
))
|
))
|
||||||
) : (
|
: (
|
||||||
<>
|
<>
|
||||||
{page < 4 ? (
|
{page < 4
|
||||||
[1, 2, 3, 4].map(n => (
|
? [1, 2, 3, 4].map(n => (
|
||||||
<PaginationItem
|
|
||||||
key={n}
|
|
||||||
className='d-none d-sm-block'
|
|
||||||
active={page === n}
|
|
||||||
onClick={async () => onChange(n)}
|
|
||||||
>
|
|
||||||
{n}
|
|
||||||
</PaginationItem>
|
|
||||||
))
|
|
||||||
) : (
|
|
||||||
<PaginationItem
|
|
||||||
className='d-none d-sm-block'
|
|
||||||
onClick={async () => onChange(1)}
|
|
||||||
>
|
|
||||||
1
|
|
||||||
</PaginationItem>
|
|
||||||
)}
|
|
||||||
<PaginationItem disabled className='d-none d-sm-block'>
|
|
||||||
...
|
|
||||||
</PaginationItem>
|
|
||||||
{page > 3 && page < totalPages - 2 && (
|
|
||||||
<>
|
|
||||||
{[page - 1, page, page + 1].map(n => (
|
|
||||||
<PaginationItem
|
<PaginationItem
|
||||||
key={n}
|
key={n}
|
||||||
className='d-none d-sm-block'
|
className='d-none d-sm-block'
|
||||||
|
|
@ -78,15 +55,37 @@ const Pagination: React.FC<Properties> = properties => {
|
||||||
>
|
>
|
||||||
{n}
|
{n}
|
||||||
</PaginationItem>
|
</PaginationItem>
|
||||||
))}
|
))
|
||||||
<PaginationItem disabled className='d-none d-sm-block'>
|
: (
|
||||||
...
|
<PaginationItem
|
||||||
</PaginationItem>
|
className='d-none d-sm-block'
|
||||||
</>
|
onClick={async () => onChange(1)}
|
||||||
)}
|
>
|
||||||
{totalPages - page < 3 ? (
|
1
|
||||||
[totalPages - 3, totalPages - 2, totalPages - 1, totalPages].map(
|
</PaginationItem>
|
||||||
n => (
|
)}
|
||||||
|
<PaginationItem disabled className='d-none d-sm-block'>
|
||||||
|
...
|
||||||
|
</PaginationItem>
|
||||||
|
{page > 3 && page < totalPages - 2 && (
|
||||||
|
<>
|
||||||
|
{[page - 1, page, page + 1].map(n => (
|
||||||
|
<PaginationItem
|
||||||
|
key={n}
|
||||||
|
className='d-none d-sm-block'
|
||||||
|
active={page === n}
|
||||||
|
onClick={async () => onChange(n)}
|
||||||
|
>
|
||||||
|
{n}
|
||||||
|
</PaginationItem>
|
||||||
|
))}
|
||||||
|
<PaginationItem disabled className='d-none d-sm-block'>
|
||||||
|
...
|
||||||
|
</PaginationItem>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{totalPages - page < 3
|
||||||
|
? [totalPages - 3, totalPages - 2, totalPages - 1, totalPages].map(n => (
|
||||||
<PaginationItem
|
<PaginationItem
|
||||||
key={n}
|
key={n}
|
||||||
className='d-none d-sm-block'
|
className='d-none d-sm-block'
|
||||||
|
|
@ -95,18 +94,17 @@ const Pagination: React.FC<Properties> = properties => {
|
||||||
>
|
>
|
||||||
{n}
|
{n}
|
||||||
</PaginationItem>
|
</PaginationItem>
|
||||||
),
|
))
|
||||||
)
|
: (
|
||||||
) : (
|
<PaginationItem
|
||||||
<PaginationItem
|
className='d-none d-sm-block'
|
||||||
className='d-none d-sm-block'
|
onClick={async () => onChange(totalPages)}
|
||||||
onClick={async () => onChange(totalPages)}
|
>
|
||||||
>
|
{totalPages}
|
||||||
{totalPages}
|
</PaginationItem>
|
||||||
</PaginationItem>
|
)}
|
||||||
)}
|
</>
|
||||||
</>
|
)}
|
||||||
)}
|
|
||||||
<PaginationItem
|
<PaginationItem
|
||||||
title={t('vendor.datatable.next')}
|
title={t('vendor.datatable.next')}
|
||||||
disabled={page === totalPages}
|
disabled={page === totalPages}
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ type Properties = {
|
||||||
readonly active?: boolean;
|
readonly active?: boolean;
|
||||||
readonly title?: string;
|
readonly title?: string;
|
||||||
readonly className?: string;
|
readonly className?: string;
|
||||||
onClick?(): void;
|
onClick?: () => void;
|
||||||
readonly children?: React.ReactNode;
|
readonly children?: React.ReactNode;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,12 @@
|
||||||
/** @jsxImportSource @emotion/react */
|
|
||||||
|
|
||||||
import {useState, useEffect} from 'react';
|
|
||||||
import {css} from '@emotion/react';
|
import {css} from '@emotion/react';
|
||||||
|
import {useEffect, useState} from 'react';
|
||||||
|
|
||||||
export type ToastType = 'success' | 'info' | 'warning' | 'error';
|
export type ToastType = 'success' | 'info' | 'warning' | 'error';
|
||||||
|
|
||||||
type Properties = {
|
type Properties = {
|
||||||
readonly type: ToastType;
|
readonly type: ToastType;
|
||||||
readonly distance: number;
|
readonly distance: number;
|
||||||
onClose(): void | Promise<void>;
|
onClose: () => void | Promise<void>;
|
||||||
readonly children: React.ReactNode;
|
readonly children: React.ReactNode;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,11 @@
|
||||||
/** @jsxImportSource @emotion/react */
|
import {t} from '@/scripts/i18n';
|
||||||
|
import * as breakpoints from '@/styles/breakpoints';
|
||||||
import {useState, useEffect, useRef} from 'react';
|
import * as cssUtils from '@/styles/utils';
|
||||||
import {useMeasure} from 'react-use';
|
|
||||||
import {css} from '@emotion/react';
|
import {css} from '@emotion/react';
|
||||||
import styled from '@emotion/styled';
|
import styled from '@emotion/styled';
|
||||||
|
import {useEffect, useRef, useState} from 'react';
|
||||||
|
import {useMeasure} from 'react-use';
|
||||||
import * as skinview3d from 'skinview3d';
|
import * as skinview3d from 'skinview3d';
|
||||||
import SkinSteve from '../../../misc/textures/steve.png';
|
|
||||||
import bg1 from '../../../misc/backgrounds/1.webp';
|
import bg1 from '../../../misc/backgrounds/1.webp';
|
||||||
import bg2 from '../../../misc/backgrounds/2.webp';
|
import bg2 from '../../../misc/backgrounds/2.webp';
|
||||||
import bg3 from '../../../misc/backgrounds/3.webp';
|
import bg3 from '../../../misc/backgrounds/3.webp';
|
||||||
|
|
@ -13,9 +13,7 @@ import bg4 from '../../../misc/backgrounds/4.webp';
|
||||||
import bg5 from '../../../misc/backgrounds/5.webp';
|
import bg5 from '../../../misc/backgrounds/5.webp';
|
||||||
import bg6 from '../../../misc/backgrounds/6.webp';
|
import bg6 from '../../../misc/backgrounds/6.webp';
|
||||||
import bg7 from '../../../misc/backgrounds/7.webp';
|
import bg7 from '../../../misc/backgrounds/7.webp';
|
||||||
import * as breakpoints from '@/styles/breakpoints';
|
import SkinSteve from '../../../misc/textures/steve.png';
|
||||||
import * as cssUtils from '@/styles/utils';
|
|
||||||
import {t} from '@/scripts/i18n';
|
|
||||||
|
|
||||||
const backgrounds = [bg1, bg2, bg3, bg4, bg5, bg6, bg7];
|
const backgrounds = [bg1, bg2, bg3, bg4, bg5, bg6, bg7];
|
||||||
export const PICTURES_COUNT = backgrounds.length;
|
export const PICTURES_COUNT = backgrounds.length;
|
||||||
|
|
@ -227,9 +225,8 @@ const Viewer: React.FC<Properties> = properties => {
|
||||||
<div className='d-flex justify-content-between'>
|
<div className='d-flex justify-content-between'>
|
||||||
<h3 className='card-title'>
|
<h3 className='card-title'>
|
||||||
<span>{t('general.texturePreview')}</span>
|
<span>{t('general.texturePreview')}</span>
|
||||||
{properties.showIndicator && (
|
{properties.showIndicator
|
||||||
<span className='badge bg-olive ml-1'>{indicator}</span>
|
&& <span className='badge bg-olive ml-1'>{indicator}</span>}
|
||||||
)}
|
|
||||||
</h3>
|
</h3>
|
||||||
<div>
|
<div>
|
||||||
<ActionButton
|
<ActionButton
|
||||||
|
|
@ -238,14 +235,14 @@ const Viewer: React.FC<Properties> = properties => {
|
||||||
data-placement='bottom'
|
data-placement='bottom'
|
||||||
title={t('general.switchCapeElytra')}
|
title={t('general.switchCapeElytra')}
|
||||||
onClick={toggleBackEquippment}
|
onClick={toggleBackEquippment}
|
||||||
/>
|
/>
|
||||||
<ActionButton
|
<ActionButton
|
||||||
className='fas fa-person-running'
|
className='fas fa-person-running'
|
||||||
data-toggle='tooltip'
|
data-toggle='tooltip'
|
||||||
data-placement='bottom'
|
data-placement='bottom'
|
||||||
title={t('general.switchAnimation')}
|
title={t('general.switchAnimation')}
|
||||||
onClick={toggleAnimation}
|
onClick={toggleAnimation}
|
||||||
/>
|
/>
|
||||||
<ActionButton
|
<ActionButton
|
||||||
className={`fas fa-${paused ? 'play' : 'pause'}`}
|
className={`fas fa-${paused ? 'play' : 'pause'}`}
|
||||||
data-toggle='tooltip'
|
data-toggle='tooltip'
|
||||||
|
|
@ -256,14 +253,14 @@ const Viewer: React.FC<Properties> = properties => {
|
||||||
: t('general.pauseAnimation')
|
: t('general.pauseAnimation')
|
||||||
}
|
}
|
||||||
onClick={togglePause}
|
onClick={togglePause}
|
||||||
/>
|
/>
|
||||||
<ActionButton
|
<ActionButton
|
||||||
className='fas fa-rotate-right'
|
className='fas fa-rotate-right'
|
||||||
data-toggle='tooltip'
|
data-toggle='tooltip'
|
||||||
data-placement='bottom'
|
data-placement='bottom'
|
||||||
title={t('general.rotation')}
|
title={t('general.rotation')}
|
||||||
onClick={toggleRotate}
|
onClick={toggleRotate}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -1,26 +1,23 @@
|
||||||
|
import $ from 'jquery';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import ReactDOM from 'react-dom';
|
import ReactDOM from 'react-dom';
|
||||||
import {createRoot} from 'react-dom/client';
|
import {createRoot} from 'react-dom/client';
|
||||||
import $ from 'jquery';
|
|
||||||
// eslint-disable-next-line import/no-unassigned-import
|
|
||||||
import './scripts/app';
|
|
||||||
import routes from './scripts/route';
|
import routes from './scripts/route';
|
||||||
|
|
||||||
|
import './scripts/app';
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||||
Object.assign(window, {React, ReactDOM, $});
|
Object.assign(window, {React, ReactDOM, $});
|
||||||
|
|
||||||
const route = routes.find(route =>
|
const route = routes.find(route =>
|
||||||
new RegExp(`^${route.path}$`, 'i').test(blessing.route),
|
new RegExp(`^${route.path}$`, 'i').test(blessing.route));
|
||||||
);
|
|
||||||
if (route) {
|
if (route) {
|
||||||
if (route.module) {
|
if (route.module) {
|
||||||
void Promise.all(route.module.map(async m => m()));
|
void Promise.all(route.module.map(async m => m()));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (route.react) {
|
if (route.react) {
|
||||||
const Component = React.lazy(
|
const Component = React.lazy(route.react as () => Promise<{default: React.ComponentType}>);
|
||||||
route.react as () => Promise<{default: React.ComponentType}>,
|
|
||||||
);
|
|
||||||
|
|
||||||
const container = typeof route.el === 'string'
|
const container = typeof route.el === 'string'
|
||||||
? document.querySelector(route.el)
|
? document.querySelector(route.el)
|
||||||
|
|
@ -28,11 +25,11 @@ if (route) {
|
||||||
|
|
||||||
const root = createRoot(container!);
|
const root = createRoot(container!);
|
||||||
root.render(
|
root.render(
|
||||||
<React.StrictMode>
|
<React.StrictMode>
|
||||||
<React.Suspense fallback={route.frame?.() ?? ''}>
|
<React.Suspense fallback={route.frame?.() ?? ''}>
|
||||||
<Component/>
|
<Component/>
|
||||||
</React.Suspense>
|
</React.Suspense>
|
||||||
</React.StrictMode>,
|
</React.StrictMode>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
/* eslint-disable import/no-unassigned-import */
|
import {Tooltip} from 'bootstrap';
|
||||||
|
import '@popperjs/core';
|
||||||
import 'admin-lte';
|
import 'admin-lte';
|
||||||
import './extra';
|
import './extra';
|
||||||
import './i18n';
|
import './i18n';
|
||||||
|
|
@ -10,5 +11,5 @@ import './logout';
|
||||||
import './darkMode';
|
import './darkMode';
|
||||||
|
|
||||||
window.addEventListener('load', () => {
|
window.addEventListener('load', () => {
|
||||||
$('[data-toggle="tooltip"]').tooltip();
|
[...document.querySelectorAll('[data-toggle="tooltip"]')].map(el => new Tooltip(el));
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import ReactDOM from 'react-dom';
|
|
||||||
import DarkModeButton from '@/components/DarkModeButton';
|
import DarkModeButton from '@/components/DarkModeButton';
|
||||||
|
import ReactDOM from 'react-dom';
|
||||||
|
|
||||||
const element = document.querySelector('#toggle-dark-mode');
|
const element = document.querySelector('#toggle-dark-mode');
|
||||||
if (element) {
|
if (element) {
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import ReactDOM from 'react-dom';
|
|
||||||
import EmailVerification from '@/views/widgets/EmailVerification';
|
import EmailVerification from '@/views/widgets/EmailVerification';
|
||||||
|
import ReactDOM from 'react-dom';
|
||||||
|
|
||||||
const container = document.querySelector('#email-verification');
|
const container = document.querySelector('#email-verification');
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
|
const bus = new Map<string | symbol, Set<(...args: any[]) => void>>();
|
||||||
|
|
||||||
const bus = new Map<string | symbol, Set<CallableFunction>>();
|
export function on(event: string | symbol, listener: (...args: any[]) => void) {
|
||||||
|
|
||||||
export function on(event: string | symbol, listener: CallableFunction) {
|
|
||||||
if (!bus.has(event)) {
|
if (!bus.has(event)) {
|
||||||
bus.set(event, new Set());
|
bus.set(event, new Set());
|
||||||
}
|
}
|
||||||
|
|
@ -15,7 +14,9 @@ export function on(event: string | symbol, listener: CallableFunction) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function emit(event: string | symbol, payload?: unknown) {
|
export function emit(event: string | symbol, payload?: unknown) {
|
||||||
bus.get(event)?.forEach(listener => listener(payload));
|
bus.get(event)?.forEach(listener => {
|
||||||
|
listener(payload);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
blessing.event = {on, emit};
|
blessing.event = {on, emit};
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import {useState, useEffect} from 'react';
|
import {useEffect, useState} from 'react';
|
||||||
|
|
||||||
export default function useBlessingExtra<T>(key: string, defaultValue?: T): T {
|
export default function useBlessingExtra<T>(key: string, defaultValue?: T): T {
|
||||||
const [value, setValue] = useState<T>(defaultValue!);
|
const [value, setValue] = useState<T>(defaultValue!);
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import {useState, useEffect} from 'react';
|
import {useEffect, useState} from 'react';
|
||||||
|
|
||||||
export default function useIsLargeScreen() {
|
export default function useIsLargeScreen() {
|
||||||
const [isLarge, setIsLarge] = useState(false);
|
const [isLarge, setIsLarge] = useState(false);
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import {useState, useEffect} from 'react';
|
import {useEffect, useState} from 'react';
|
||||||
import * as fetch from '../net';
|
import * as fetch from '../net';
|
||||||
import {type Texture, TextureType} from '../types';
|
import {type Texture, TextureType} from '../types';
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import {useState, useEffect, useRef} from 'react';
|
|
||||||
import TWEEN from '@tweenjs/tween.js';
|
import TWEEN from '@tweenjs/tween.js';
|
||||||
|
import {useEffect, useRef, useState} from 'react';
|
||||||
|
|
||||||
export default function useTween<T = any>(initialValue: T) {
|
export default function useTween<T = any>(initialValue: T) {
|
||||||
const [value, setValue] = useState<T>(initialValue);
|
const [value, setValue] = useState<T>(initialValue);
|
||||||
|
|
|
||||||
|
|
@ -2,14 +2,17 @@ type I18nTable = {
|
||||||
[key: string]: string | I18nTable | undefined;
|
[key: string]: string | I18nTable | undefined;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function t(key: string, parameters: Record<string, string> = Object.create(null)): string {
|
export function t(
|
||||||
|
key: string,
|
||||||
|
parameters: Record<string, string> = Object.create(null) as Record<string, string>,
|
||||||
|
): string {
|
||||||
const segments = key.split('.');
|
const segments = key.split('.');
|
||||||
let temporary = blessing.i18n as I18nTable | undefined;
|
let temporary = blessing.i18n as I18nTable | undefined;
|
||||||
let result = '';
|
let result = '';
|
||||||
|
|
||||||
for (const segment of segments) {
|
for (const segment of segments) {
|
||||||
const middle = temporary?.[segment];
|
const middle = temporary?.[segment];
|
||||||
if (!middle) {
|
if (middle === undefined) {
|
||||||
return key;
|
return key;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -21,7 +24,7 @@ export function t(key: string, parameters: Record<string, string> = Object.creat
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const slot of Object.keys(parameters)) {
|
for (const slot of Object.keys(parameters)) {
|
||||||
(result = result.replace(`:${slot}`, parameters[slot] ?? `%{${slot}}`));
|
result = result.replace(`:${slot}`, parameters[slot] ?? `%{${slot}}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
declare let __webpack_public_path__: string;
|
declare let __webpack_public_path__: string;
|
||||||
declare const __blessing_public_path__: string;
|
declare const __blessing_public_path__: string;
|
||||||
|
|
||||||
if (process.env.NODE_ENV === 'development') {
|
if (import.meta.env.NODE_ENV === 'development') {
|
||||||
__webpack_public_path__ = __blessing_public_path__;
|
__webpack_public_path__ = __blessing_public_path__;
|
||||||
} else {
|
} else {
|
||||||
const link = document.querySelector<HTMLLinkElement>('link#cdn-host');
|
const link = document.querySelector<HTMLLinkElement>('link#cdn-host');
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import {post} from './net';
|
|
||||||
import {t} from './i18n';
|
import {t} from './i18n';
|
||||||
|
import {post} from './net';
|
||||||
import {showModal} from './notify';
|
import {showModal} from './notify';
|
||||||
import urls from './urls';
|
import urls from './urls';
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,17 +1,18 @@
|
||||||
import ReactDOM from 'react-dom';
|
import {createRoot} from 'react-dom/client';
|
||||||
import Modal, {type ModalOptions, type ModalResult} from '../components/Modal';
|
import Modal, {type ModalOptions, type ModalResult} from '../components/Modal';
|
||||||
|
|
||||||
export async function showModal(options: ModalOptions = {}): Promise<ModalResult> {
|
export async function showModal(options: ModalOptions = {}): Promise<ModalResult> {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const container = document.createElement('div');
|
const container = document.createElement('div');
|
||||||
document.body.append(container);
|
document.body.append(container);
|
||||||
|
const root = createRoot(container);
|
||||||
|
|
||||||
const handleClose = () => {
|
const handleClose = () => {
|
||||||
ReactDOM.unmountComponentAtNode(container);
|
root.unmount();
|
||||||
container.remove();
|
container.remove();
|
||||||
};
|
};
|
||||||
|
|
||||||
ReactDOM.render(
|
root.render((
|
||||||
<Modal
|
<Modal
|
||||||
{...options}
|
{...options}
|
||||||
show
|
show
|
||||||
|
|
@ -19,8 +20,7 @@ export async function showModal(options: ModalOptions = {}): Promise<ModalResult
|
||||||
onConfirm={resolve}
|
onConfirm={resolve}
|
||||||
onDismiss={reject}
|
onDismiss={reject}
|
||||||
onClose={handleClose}
|
onClose={handleClose}
|
||||||
/>,
|
/>
|
||||||
container,
|
));
|
||||||
);
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import {emit} from './event';
|
import {emit} from './event';
|
||||||
import {showModal} from './notify';
|
|
||||||
import {t} from './i18n';
|
import {t} from './i18n';
|
||||||
|
import {showModal} from './notify';
|
||||||
|
|
||||||
export type ResponseBody<T = undefined> = {
|
export type ResponseBody<T = undefined> = {
|
||||||
code: number;
|
code: number;
|
||||||
|
|
@ -26,9 +26,7 @@ export const init: RequestInit = {
|
||||||
};
|
};
|
||||||
|
|
||||||
function retrieveToken() {
|
function retrieveToken() {
|
||||||
const csrfField = document.querySelector<HTMLMetaElement>(
|
const csrfField = document.querySelector<HTMLMetaElement>('meta[name="csrf-token"]');
|
||||||
'meta[name="csrf-token"]',
|
|
||||||
);
|
|
||||||
|
|
||||||
return csrfField?.content || '';
|
return csrfField?.content || '';
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import ReactDOM from 'react-dom';
|
|
||||||
import NotificationsList from '@/views/widgets/NotificationsList';
|
import NotificationsList from '@/views/widgets/NotificationsList';
|
||||||
|
import {createRoot} from 'react-dom/client';
|
||||||
|
|
||||||
const container = document.querySelector('[data-notifications]');
|
const container = document.querySelector('[data-notifications]');
|
||||||
if (container) {
|
if (container) {
|
||||||
ReactDOM.render(<NotificationsList/>, container);
|
createRoot(container).render(<NotificationsList/>);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@ import {Toast} from './toast';
|
||||||
|
|
||||||
export const toast = new Toast();
|
export const toast = new Toast();
|
||||||
|
|
||||||
if (process.env.NODE_ENV === 'test') {
|
if (import.meta.env.NODE_ENV === 'test') {
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
toast.clear();
|
toast.clear();
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -9,8 +9,8 @@ function checkPixel(
|
||||||
|
|
||||||
return (
|
return (
|
||||||
imageData.data[0] === 0
|
imageData.data[0] === 0
|
||||||
&& imageData.data[1] === 0
|
&& imageData.data[1] === 0
|
||||||
&& imageData.data[2] === 0
|
&& imageData.data[2] === 0
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,14 @@
|
||||||
import React, {useState, useEffect} from 'react';
|
|
||||||
import ReactDOM from 'react-dom';
|
|
||||||
import {nanoid} from 'nanoid';
|
import {nanoid} from 'nanoid';
|
||||||
|
import React, {useEffect, useState} from 'react';
|
||||||
|
import {createRoot, type Root} from 'react-dom/client';
|
||||||
import ToastBox, {type ToastType} from '../components/Toast';
|
import ToastBox, {type ToastType} from '../components/Toast';
|
||||||
import * as emitter from './event';
|
import * as emitter from './event';
|
||||||
|
|
||||||
type QueueElement = {id: string; type: ToastType; message: string};
|
type QueueElement = {id: string; type: ToastType; message: string};
|
||||||
type ToastQueue = QueueElement[];
|
type ToastQueue = QueueElement[];
|
||||||
|
|
||||||
const TOAST_EVENT = Symbol('toast');
|
const ToastEvent = Symbol('toast');
|
||||||
const CLEAR_EVENT = Symbol('clear');
|
const ClearEvent = Symbol('clear');
|
||||||
|
|
||||||
export function ToastContainer() {
|
export function ToastContainer() {
|
||||||
const [queue, setQueue] = useState<ToastQueue>([]);
|
const [queue, setQueue] = useState<ToastQueue>([]);
|
||||||
|
|
@ -18,17 +18,19 @@ export function ToastContainer() {
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const off1 = emitter.on(TOAST_EVENT, (toast: QueueElement) => {
|
const off1 = emitter.on(ToastEvent, (toast: QueueElement) => {
|
||||||
setQueue(queue => {
|
setQueue(queue => {
|
||||||
queue.push(toast);
|
queue.push(toast);
|
||||||
return [...queue];
|
return [...queue];
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Effect dependency is empty
|
||||||
|
// eslint-disable-next-line react-web-api/no-leaked-timeout
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
handleClose(toast.id);
|
handleClose(toast.id);
|
||||||
}, 3100);
|
}, 3100);
|
||||||
});
|
});
|
||||||
const off2 = emitter.on(CLEAR_EVENT, () => {
|
const off2 = emitter.on(ClearEvent, () => {
|
||||||
setQueue([]);
|
setQueue([]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -44,7 +46,7 @@ export function ToastContainer() {
|
||||||
<ToastBox
|
<ToastBox
|
||||||
key={element.id}
|
key={element.id}
|
||||||
type={element.type}
|
type={element.type}
|
||||||
distance={50 + i * 70}
|
distance={50 + (i * 70)}
|
||||||
onClose={() => {
|
onClose={() => {
|
||||||
handleClose(element.id);
|
handleClose(element.id);
|
||||||
}}
|
}}
|
||||||
|
|
@ -58,40 +60,42 @@ export function ToastContainer() {
|
||||||
|
|
||||||
export class Toast {
|
export class Toast {
|
||||||
private readonly container: HTMLDivElement;
|
private readonly container: HTMLDivElement;
|
||||||
|
private readonly root: Root;
|
||||||
|
|
||||||
constructor(render?: (element: JSX.Element) => void) {
|
constructor(render?: (element: React.JSX.Element) => void) {
|
||||||
this.container = document.createElement('div');
|
this.container = document.createElement('div');
|
||||||
document.body.append(this.container);
|
document.body.append(this.container);
|
||||||
|
this.root = createRoot(this.container);
|
||||||
|
|
||||||
if (render) {
|
if (render) {
|
||||||
render(<ToastContainer/>);
|
render(<ToastContainer/>);
|
||||||
} else {
|
} else {
|
||||||
ReactDOM.render(<ToastContainer/>, this.container);
|
this.root.render(<ToastContainer/>);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
success(message: string) {
|
success(message: string) {
|
||||||
emitter.emit(TOAST_EVENT, {id: nanoid(4), type: 'success', message});
|
emitter.emit(ToastEvent, {id: nanoid(4), type: 'success', message});
|
||||||
}
|
}
|
||||||
|
|
||||||
info(message: string) {
|
info(message: string) {
|
||||||
emitter.emit(TOAST_EVENT, {id: nanoid(4), type: 'info', message});
|
emitter.emit(ToastEvent, {id: nanoid(4), type: 'info', message});
|
||||||
}
|
}
|
||||||
|
|
||||||
warning(message: string) {
|
warning(message: string) {
|
||||||
emitter.emit(TOAST_EVENT, {id: nanoid(4), type: 'warning', message});
|
emitter.emit(ToastEvent, {id: nanoid(4), type: 'warning', message});
|
||||||
}
|
}
|
||||||
|
|
||||||
error(message: string) {
|
error(message: string) {
|
||||||
emitter.emit(TOAST_EVENT, {id: nanoid(4), type: 'error', message});
|
emitter.emit(ToastEvent, {id: nanoid(4), type: 'error', message});
|
||||||
}
|
}
|
||||||
|
|
||||||
clear() {
|
clear() {
|
||||||
emitter.emit(CLEAR_EVENT);
|
emitter.emit(ClearEvent);
|
||||||
}
|
}
|
||||||
|
|
||||||
dispose() {
|
dispose() {
|
||||||
ReactDOM.unmountComponentAtNode(this.container);
|
this.root.unmount();
|
||||||
this.container.remove();
|
this.container.remove();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
56
resources/assets/src/shims.d.ts
vendored
56
resources/assets/src/shims.d.ts
vendored
|
|
@ -1,35 +1,33 @@
|
||||||
import JQuery from 'jquery'
|
import type {ModalOptions, ModalResult} from './components/Modal';
|
||||||
import { ModalOptions, ModalResult } from './components/Modal'
|
import type {Toast} from './scripts/toast';
|
||||||
import { Toast } from './scripts/toast'
|
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
// eslint-disable-next-line no-redeclare
|
let blessing: {
|
||||||
let blessing: {
|
base_url: string;
|
||||||
base_url: string
|
debug: boolean;
|
||||||
debug: boolean
|
env: string;
|
||||||
env: string
|
locale: string;
|
||||||
locale: string
|
site_name: string;
|
||||||
site_name: string
|
version: string;
|
||||||
version: string
|
route: string;
|
||||||
route: string
|
extra: Record<string, unknown>;
|
||||||
extra: Record<string, any>
|
i18n: Record<string, unknown>;
|
||||||
i18n: object
|
|
||||||
|
|
||||||
fetch: {
|
fetch: {
|
||||||
get(url: string, params?: object): Promise<object>
|
get: (url: string, params?: Record<string, unknown>) => Promise<Record<string, unknown>>;
|
||||||
post(url: string, data?: object): Promise<object>
|
post: (url: string, data?: Record<string, unknown>) => Promise<Record<string, unknown>>;
|
||||||
put(url: string, data?: object): Promise<object>
|
put: (url: string, data?: Record<string, unknown>) => Promise<Record<string, unknown>>;
|
||||||
del(url: string, data?: object): Promise<object>
|
del: (url: string, data?: Record<string, unknown>) => Promise<Record<string, unknown>>;
|
||||||
}
|
};
|
||||||
|
|
||||||
event: {
|
event: {
|
||||||
on(eventName: string, listener: Function): void
|
on: (eventName: string, listener: (...args: any[]) => void) => void;
|
||||||
emit(eventName: string, payload: object): void
|
emit: (eventName: string, payload: Record<string, unknown>) => void;
|
||||||
}
|
};
|
||||||
|
|
||||||
notify: {
|
notify: {
|
||||||
showModal(options?: ModalOptions): Promise<ModalResult>
|
showModal: (options?: ModalOptions) => Promise<ModalResult>;
|
||||||
toast: Toast
|
toast: Toast;
|
||||||
}
|
};
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,10 @@
|
||||||
/* eslint-disable object-curly-newline */
|
|
||||||
import {fromEvent, merge, of, partition} from 'rxjs';
|
import {
|
||||||
|
fromEvent,
|
||||||
|
merge,
|
||||||
|
of,
|
||||||
|
partition,
|
||||||
|
} from 'rxjs';
|
||||||
import {filter, map, pairwise} from 'rxjs/operators';
|
import {filter, map, pairwise} from 'rxjs/operators';
|
||||||
|
|
||||||
export function registerNavbarPicker(
|
export function registerNavbarPicker(
|
||||||
|
|
@ -9,9 +14,7 @@ export function registerNavbarPicker(
|
||||||
): void {
|
): void {
|
||||||
const color$ = fromEvent(picker, 'click').pipe(
|
const color$ = fromEvent(picker, 'click').pipe(
|
||||||
map(event => event.target as HTMLElement),
|
map(event => event.target as HTMLElement),
|
||||||
filter(
|
filter((element): element is HTMLInputElement => element.tagName === 'INPUT'),
|
||||||
(element): element is HTMLInputElement => element.tagName === 'INPUT',
|
|
||||||
),
|
|
||||||
map(element => element.value),
|
map(element => element.value),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -22,8 +25,7 @@ export function registerNavbarPicker(
|
||||||
});
|
});
|
||||||
|
|
||||||
const [light$, dark$] = partition(color$, color =>
|
const [light$, dark$] = partition(color$, color =>
|
||||||
['light', 'warning', 'white', 'orange', 'lime'].includes(color),
|
['light', 'warning', 'white', 'orange', 'lime'].includes(color));
|
||||||
);
|
|
||||||
light$.subscribe(() => {
|
light$.subscribe(() => {
|
||||||
// DO NOT use `classList.replace`.
|
// DO NOT use `classList.replace`.
|
||||||
navbar.classList.remove('navbar-dark');
|
navbar.classList.remove('navbar-dark');
|
||||||
|
|
@ -53,9 +55,7 @@ export function registerSidebarPicker(
|
||||||
fromEvent(light, 'click'),
|
fromEvent(light, 'click'),
|
||||||
).pipe(
|
).pipe(
|
||||||
map(event => event.target as HTMLElement),
|
map(event => event.target as HTMLElement),
|
||||||
filter(
|
filter((element): element is HTMLInputElement => element.tagName === 'INPUT'),
|
||||||
(element): element is HTMLInputElement => element.tagName === 'INPUT',
|
|
||||||
),
|
|
||||||
map(element => element.value),
|
map(element => element.value),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -67,12 +67,8 @@ export function registerSidebarPicker(
|
||||||
}
|
}
|
||||||
|
|
||||||
const sidebar = document.querySelector<HTMLElement>('.main-sidebar');
|
const sidebar = document.querySelector<HTMLElement>('.main-sidebar');
|
||||||
const darkPicker = document.querySelector<HTMLDivElement>(
|
const darkPicker = document.querySelector<HTMLDivElement>('#sidebar-dark-picker');
|
||||||
'#sidebar-dark-picker',
|
const lightPicker = document.querySelector<HTMLDivElement>('#sidebar-light-picker');
|
||||||
);
|
|
||||||
const lightPicker = document.querySelector<HTMLDivElement>(
|
|
||||||
'#sidebar-light-picker',
|
|
||||||
);
|
|
||||||
|
|
||||||
if (sidebar && darkPicker && lightPicker) {
|
if (sidebar && darkPicker && lightPicker) {
|
||||||
registerSidebarPicker(
|
registerSidebarPicker(
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,3 @@
|
||||||
import * as echarts from 'echarts/core';
|
|
||||||
import {SVGRenderer} from 'echarts/renderers';
|
|
||||||
import {LineChart} from 'echarts/charts';
|
import {LineChart} from 'echarts/charts';
|
||||||
import {
|
import {
|
||||||
DataZoomComponent,
|
DataZoomComponent,
|
||||||
|
|
@ -7,6 +5,8 @@ import {
|
||||||
TitleComponent,
|
TitleComponent,
|
||||||
TooltipComponent,
|
TooltipComponent,
|
||||||
} from 'echarts/components';
|
} from 'echarts/components';
|
||||||
|
import * as echarts from 'echarts/core';
|
||||||
|
import {SVGRenderer} from 'echarts/renderers';
|
||||||
import {get} from '../../scripts/net';
|
import {get} from '../../scripts/net';
|
||||||
|
|
||||||
type ChartData = {
|
type ChartData = {
|
||||||
|
|
@ -31,12 +31,8 @@ echarts.use([
|
||||||
]);
|
]);
|
||||||
|
|
||||||
async function main() {
|
async function main() {
|
||||||
const elementUsersRegistration = document.querySelector<HTMLDivElement>(
|
const elementUsersRegistration = document.querySelector<HTMLDivElement>('#chart-users-registration');
|
||||||
'#chart-users-registration',
|
const elementTexturesUpload = document.querySelector<HTMLDivElement>('#chart-textures-upload');
|
||||||
);
|
|
||||||
const elementTexturesUpload = document.querySelector<HTMLDivElement>(
|
|
||||||
'#chart-textures-upload',
|
|
||||||
);
|
|
||||||
if (!elementUsersRegistration || !elementTexturesUpload) {
|
if (!elementUsersRegistration || !elementTexturesUpload) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,16 +1,16 @@
|
||||||
|
|
||||||
import clsx from 'clsx';
|
import type {Player} from '@/scripts/types';
|
||||||
import {Box} from './styles';
|
|
||||||
import {t} from '@/scripts/i18n';
|
import {t} from '@/scripts/i18n';
|
||||||
import {showModal} from '@/scripts/notify';
|
import {showModal} from '@/scripts/notify';
|
||||||
import type {Player} from '@/scripts/types';
|
import clsx from 'clsx';
|
||||||
|
import {Box} from './styles';
|
||||||
|
|
||||||
type Properties = {
|
type Properties = {
|
||||||
readonly player: Player;
|
readonly player: Player;
|
||||||
onUpdateName(): void;
|
onUpdateName: () => void;
|
||||||
onUpdateOwner(): void;
|
onUpdateOwner: () => void;
|
||||||
onUpdateTexture(): void;
|
onUpdateTexture: () => void;
|
||||||
onDelete(): void;
|
onDelete: () => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
const Card: React.FC<Properties> = properties => {
|
const Card: React.FC<Properties> = properties => {
|
||||||
|
|
@ -31,7 +31,8 @@ const Card: React.FC<Properties> = properties => {
|
||||||
{player.tid_skin > 0 && (
|
{player.tid_skin > 0 && (
|
||||||
<a
|
<a
|
||||||
href={`${blessing.base_url}/skinlib/show/${player.tid_skin}`}
|
href={`${blessing.base_url}/skinlib/show/${player.tid_skin}`}
|
||||||
target='_blank' rel='noreferrer'
|
target='_blank'
|
||||||
|
rel='noreferrer'
|
||||||
>
|
>
|
||||||
<picture>
|
<picture>
|
||||||
<source srcSet={skinPreview} type='image/webp'/>
|
<source srcSet={skinPreview} type='image/webp'/>
|
||||||
|
|
@ -48,7 +49,8 @@ const Card: React.FC<Properties> = properties => {
|
||||||
{player.tid_cape > 0 && (
|
{player.tid_cape > 0 && (
|
||||||
<a
|
<a
|
||||||
href={`${blessing.base_url}/skinlib/show/${player.tid_cape}`}
|
href={`${blessing.base_url}/skinlib/show/${player.tid_cape}`}
|
||||||
target='_blank' rel='noreferrer'
|
target='_blank'
|
||||||
|
rel='noreferrer'
|
||||||
>
|
>
|
||||||
<picture>
|
<picture>
|
||||||
<source srcSet={capePreview} type='image/webp'/>
|
<source srcSet={capePreview} type='image/webp'/>
|
||||||
|
|
@ -143,9 +145,14 @@ const Card: React.FC<Properties> = properties => {
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<div>
|
<div>
|
||||||
<span className='mr-2'>PID: {player.pid}</span>
|
<span className='mr-2'>
|
||||||
|
PID:
|
||||||
|
{player.pid}
|
||||||
|
</span>
|
||||||
<span>
|
<span>
|
||||||
{t('general.player.owner')}: {player.uid}
|
{t('general.player.owner')}
|
||||||
|
:
|
||||||
|
{player.uid}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import styled from '@emotion/styled';
|
import styled from '@emotion/styled';
|
||||||
import Skeleton from 'react-loading-skeleton';
|
|
||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
|
import Skeleton from 'react-loading-skeleton';
|
||||||
import {Box} from './styles';
|
import {Box} from './styles';
|
||||||
|
|
||||||
const isDarkMode = document.body.classList.contains('dark-mode');
|
const isDarkMode = document.body.classList.contains('dark-mode');
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,12 @@
|
||||||
import {useState} from 'react';
|
import Modal from '@/components/Modal';
|
||||||
import {t} from '@/scripts/i18n';
|
import {t} from '@/scripts/i18n';
|
||||||
import {TextureType} from '@/scripts/types';
|
import {TextureType} from '@/scripts/types';
|
||||||
import Modal from '@/components/Modal';
|
import {useState} from 'react';
|
||||||
|
|
||||||
type Properties = {
|
type Properties = {
|
||||||
readonly open: boolean;
|
readonly open: boolean;
|
||||||
onSubmit(type: 'skin' | 'cape', tid: number): void;
|
onSubmit: (type: 'skin' | 'cape', tid: number) => void;
|
||||||
onClose(): void;
|
onClose: () => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
const ModalUpdateTexture: React.FC<Properties> = properties => {
|
const ModalUpdateTexture: React.FC<Properties> = properties => {
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,14 @@
|
||||||
|
|
||||||
import {t} from '@/scripts/i18n';
|
|
||||||
import type {Player} from '@/scripts/types';
|
import type {Player} from '@/scripts/types';
|
||||||
import ButtonEdit from '@/components/ButtonEdit';
|
import ButtonEdit from '@/components/ButtonEdit';
|
||||||
|
import {t} from '@/scripts/i18n';
|
||||||
|
|
||||||
type Properties = {
|
type Properties = {
|
||||||
readonly player: Player;
|
readonly player: Player;
|
||||||
onUpdateName(): void;
|
onUpdateName: () => void;
|
||||||
onUpdateOwner(): void;
|
onUpdateOwner: () => void;
|
||||||
onUpdateTexture(): void;
|
onUpdateTexture: () => void;
|
||||||
onDelete(): void;
|
onDelete: () => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
const Row: React.FC<Properties> = properties => {
|
const Row: React.FC<Properties> = properties => {
|
||||||
|
|
@ -40,7 +40,8 @@ const Row: React.FC<Properties> = properties => {
|
||||||
<a
|
<a
|
||||||
href={`${blessing.base_url}/skinlib/show/${player.tid_skin}`}
|
href={`${blessing.base_url}/skinlib/show/${player.tid_skin}`}
|
||||||
target='_blank'
|
target='_blank'
|
||||||
className='mr-1' rel='noreferrer'
|
className='mr-1'
|
||||||
|
rel='noreferrer'
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
src={`${blessing.base_url}/preview/${player.tid_skin}`}
|
src={`${blessing.base_url}/preview/${player.tid_skin}`}
|
||||||
|
|
@ -52,7 +53,8 @@ const Row: React.FC<Properties> = properties => {
|
||||||
{player.tid_cape > 0 && (
|
{player.tid_cape > 0 && (
|
||||||
<a
|
<a
|
||||||
href={`${blessing.base_url}/skinlib/show/${player.tid_cape}`}
|
href={`${blessing.base_url}/skinlib/show/${player.tid_cape}`}
|
||||||
target='_blank' rel='noreferrer'
|
target='_blank'
|
||||||
|
rel='noreferrer'
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
src={`${blessing.base_url}/preview/${player.tid_cape}`}
|
src={`${blessing.base_url}/preview/${player.tid_cape}`}
|
||||||
|
|
|
||||||
|
|
@ -1,18 +1,18 @@
|
||||||
import {useState, useEffect, useLayoutEffect} from 'react';
|
import type {Paginator, Player} from '@/scripts/types';
|
||||||
|
import Pagination from '@/components/Pagination';
|
||||||
|
import useIsLargeScreen from '@/scripts/hooks/useIsLargeScreen';
|
||||||
|
import {t} from '@/scripts/i18n';
|
||||||
|
import * as fetch from '@/scripts/net';
|
||||||
|
import {showModal, toast} from '@/scripts/notify';
|
||||||
|
import urls from '@/scripts/urls';
|
||||||
|
import {useEffect, useLayoutEffect, useState} from 'react';
|
||||||
import {useImmer} from 'use-immer';
|
import {useImmer} from 'use-immer';
|
||||||
import Header from '../UsersManagement/Header';
|
import Header from '../UsersManagement/Header';
|
||||||
import Card from './Card';
|
import Card from './Card';
|
||||||
import LoadingCard from './LoadingCard';
|
import LoadingCard from './LoadingCard';
|
||||||
import Row from './Row';
|
|
||||||
import LoadingRow from './LoadingRow';
|
import LoadingRow from './LoadingRow';
|
||||||
import ModalUpdateTexture from './ModalUpdateTexture';
|
import ModalUpdateTexture from './ModalUpdateTexture';
|
||||||
import useIsLargeScreen from '@/scripts/hooks/useIsLargeScreen';
|
import Row from './Row';
|
||||||
import {t} from '@/scripts/i18n';
|
|
||||||
import * as fetch from '@/scripts/net';
|
|
||||||
import type {Player, Paginator} from '@/scripts/types';
|
|
||||||
import {toast, showModal} from '@/scripts/notify';
|
|
||||||
import urls from '@/scripts/urls';
|
|
||||||
import Pagination from '@/components/Pagination';
|
|
||||||
|
|
||||||
function PlayersManagement() {
|
function PlayersManagement() {
|
||||||
const [players, setPlayers] = useImmer<Player[]>([]);
|
const [players, setPlayers] = useImmer<Player[]>([]);
|
||||||
|
|
@ -152,9 +152,7 @@ function PlayersManagement() {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const {code, message} = await fetch.del<fetch.ResponseBody>(
|
const {code, message} = await fetch.del<fetch.ResponseBody>(urls.admin.players.delete(player.pid));
|
||||||
urls.admin.players.delete(player.pid),
|
|
||||||
);
|
|
||||||
if (code === 0) {
|
if (code === 0) {
|
||||||
setPlayers(players => players.filter(({pid}) => pid !== player.pid));
|
setPlayers(players => players.filter(({pid}) => pid !== player.pid));
|
||||||
toast.success(message);
|
toast.success(message);
|
||||||
|
|
@ -208,26 +206,47 @@ function PlayersManagement() {
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
</Header>
|
</Header>
|
||||||
{players.length === 0 && !isLoading ? (
|
{players.length === 0 && !isLoading
|
||||||
<div className='card-body text-center'>{t('general.noResult')}</div>
|
? <div className='card-body text-center'>{t('general.noResult')}</div>
|
||||||
) : (isTableMode ? (
|
: isTableMode
|
||||||
<div className='card-body table-responsive p-0'>
|
? (
|
||||||
<table className={`table ${isLoading ? '' : 'table-striped'}`}>
|
<div className='card-body table-responsive p-0'>
|
||||||
<thead>
|
<table className={`table ${isLoading ? '' : 'table-striped'}`}>
|
||||||
<tr>
|
<thead>
|
||||||
<th>PID</th>
|
<tr>
|
||||||
<th>{t('general.player.player-name')}</th>
|
<th>PID</th>
|
||||||
<th>{t('general.player.owner')}</th>
|
<th>{t('general.player.player-name')}</th>
|
||||||
<th>{t('general.player.previews')}</th>
|
<th>{t('general.player.owner')}</th>
|
||||||
<th>{t('general.player.last-modified')}</th>
|
<th>{t('general.player.previews')}</th>
|
||||||
<th>{t('admin.operationsTitle')}</th>
|
<th>{t('general.player.last-modified')}</th>
|
||||||
</tr>
|
<th>{t('admin.operationsTitle')}</th>
|
||||||
</thead>
|
</tr>
|
||||||
<tbody>
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{isLoading
|
||||||
|
? Array.from({length: 10}).fill(null).map((_, i) => <LoadingRow key={i}/>)
|
||||||
|
: players.map((player, i) => (
|
||||||
|
<Row
|
||||||
|
key={player.pid}
|
||||||
|
player={player}
|
||||||
|
onUpdateName={async () => handleUpdateName(player, i)}
|
||||||
|
onUpdateOwner={async () => handleUpdateOwner(player, i)}
|
||||||
|
onUpdateTexture={() => {
|
||||||
|
setTextureUpdating(i);
|
||||||
|
}}
|
||||||
|
onDelete={async () => handleDelete(player)}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
: (
|
||||||
|
<div className='card-body d-flex flex-wrap'>
|
||||||
{isLoading
|
{isLoading
|
||||||
? Array.from({length: 10}).fill(null).map((_, i) => <LoadingRow key={i}/>)
|
? Array.from({length: 10}).fill(null).map((_, i) => <LoadingCard key={i}/>)
|
||||||
: players.map((player, i) => (
|
: players.map((player, i) => (
|
||||||
<Row
|
<Card
|
||||||
key={player.pid}
|
key={player.pid}
|
||||||
player={player}
|
player={player}
|
||||||
onUpdateName={async () => handleUpdateName(player, i)}
|
onUpdateName={async () => handleUpdateName(player, i)}
|
||||||
|
|
@ -238,27 +257,8 @@ function PlayersManagement() {
|
||||||
onDelete={async () => handleDelete(player)}
|
onDelete={async () => handleDelete(player)}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</tbody>
|
</div>
|
||||||
</table>
|
)}
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div className='card-body d-flex flex-wrap'>
|
|
||||||
{isLoading
|
|
||||||
? Array.from({length: 10}).fill(null).map((_, i) => <LoadingCard key={i}/>)
|
|
||||||
: players.map((player, i) => (
|
|
||||||
<Card
|
|
||||||
key={player.pid}
|
|
||||||
player={player}
|
|
||||||
onUpdateName={async () => handleUpdateName(player, i)}
|
|
||||||
onUpdateOwner={async () => handleUpdateOwner(player, i)}
|
|
||||||
onUpdateTexture={() => {
|
|
||||||
setTextureUpdating(i);
|
|
||||||
}}
|
|
||||||
onDelete={async () => handleDelete(player)}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
<div className='card-footer'>
|
<div className='card-footer'>
|
||||||
<div className='float-right'>
|
<div className='float-right'>
|
||||||
<Pagination page={page} totalPages={totalPages} onChange={setPage}/>
|
<Pagination page={page} totalPages={totalPages} onChange={setPage}/>
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import styled from '@emotion/styled';
|
|
||||||
import * as breakpoints from '@/styles/breakpoints';
|
import * as breakpoints from '@/styles/breakpoints';
|
||||||
|
import styled from '@emotion/styled';
|
||||||
|
|
||||||
export const Box = styled.div`
|
export const Box = styled.div`
|
||||||
width: 48%;
|
width: 48%;
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
|
|
||||||
import styled from '@emotion/styled';
|
|
||||||
import clsx from 'clsx';
|
|
||||||
import type {Plugin} from './types';
|
import type {Plugin} from './types';
|
||||||
import {t} from '@/scripts/i18n';
|
import {t} from '@/scripts/i18n';
|
||||||
|
import styled from '@emotion/styled';
|
||||||
|
import clsx from 'clsx';
|
||||||
|
|
||||||
const Box = styled.div`
|
const Box = styled.div`
|
||||||
cursor: default;
|
cursor: default;
|
||||||
|
|
@ -41,9 +41,9 @@ const Description = styled.div`
|
||||||
|
|
||||||
type Properties = {
|
type Properties = {
|
||||||
readonly plugin: Plugin;
|
readonly plugin: Plugin;
|
||||||
onEnable(plugin: Plugin): void;
|
onEnable: (plugin: Plugin) => void;
|
||||||
onDisable(plugin: Plugin): void;
|
onDisable: (plugin: Plugin) => void;
|
||||||
onDelete(plugin: Plugin): void;
|
onDelete: (plugin: Plugin) => void;
|
||||||
readonly baseUrl: string;
|
readonly baseUrl: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -89,7 +89,8 @@ const InfoBox: React.FC<Properties> = properties => {
|
||||||
{plugin.title}
|
{plugin.title}
|
||||||
</strong>
|
</strong>
|
||||||
<span className='d-none d-sm-inline-block text-gray'>
|
<span className='d-none d-sm-inline-block text-gray'>
|
||||||
v{plugin.version}
|
v
|
||||||
|
{plugin.version}
|
||||||
</span>
|
</span>
|
||||||
</Header>
|
</Header>
|
||||||
<div>
|
<div>
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,12 @@
|
||||||
import {useState, useEffect} from 'react';
|
|
||||||
import {useImmer} from 'use-immer';
|
|
||||||
import InfoBox from './InfoBox';
|
|
||||||
import type {Plugin} from './types';
|
import type {Plugin} from './types';
|
||||||
import {t} from '@/scripts/i18n';
|
|
||||||
import * as fetch from '@/scripts/net';
|
|
||||||
import {toast, showModal} from '@/scripts/notify';
|
|
||||||
import FileInput from '@/components/FileInput';
|
import FileInput from '@/components/FileInput';
|
||||||
import Loading from '@/components/Loading';
|
import Loading from '@/components/Loading';
|
||||||
|
import {t} from '@/scripts/i18n';
|
||||||
|
import * as fetch from '@/scripts/net';
|
||||||
|
import {showModal, toast} from '@/scripts/notify';
|
||||||
|
import {useEffect, useState} from 'react';
|
||||||
|
import {useImmer} from 'use-immer';
|
||||||
|
import InfoBox from './InfoBox';
|
||||||
|
|
||||||
function PluginsManagement() {
|
function PluginsManagement() {
|
||||||
const [isLoading, setIsLoading] = useState(true);
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
|
|
@ -33,9 +33,9 @@ function PluginsManagement() {
|
||||||
message,
|
message,
|
||||||
data: {reason} = {reason: []},
|
data: {reason} = {reason: []},
|
||||||
} = await fetch.post<
|
} = await fetch.post<
|
||||||
fetch.ResponseBody<{
|
fetch.ResponseBody<{
|
||||||
reason: string[];
|
reason: string[];
|
||||||
}>
|
}>
|
||||||
>('/admin/plugins/manage', {
|
>('/admin/plugins/manage', {
|
||||||
action: 'enable',
|
action: 'enable',
|
||||||
name: plugin.name,
|
name: plugin.name,
|
||||||
|
|
@ -52,9 +52,8 @@ function PluginsManagement() {
|
||||||
<div>
|
<div>
|
||||||
<p>{message}</p>
|
<p>{message}</p>
|
||||||
<ul>
|
<ul>
|
||||||
{reason.map((t, i) => (
|
{reason.map((t, i) =>
|
||||||
<li key={i}>{t}</li>
|
<li key={i}>{t}</li>)}
|
||||||
))}
|
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
),
|
),
|
||||||
|
|
@ -166,27 +165,25 @@ function PluginsManagement() {
|
||||||
return (
|
return (
|
||||||
<div className='row'>
|
<div className='row'>
|
||||||
<div className='col-lg-8'>
|
<div className='col-lg-8'>
|
||||||
{isLoading ? (
|
{isLoading
|
||||||
<Loading/>
|
? <Loading/>
|
||||||
) : (plugins.length === 0 ? (
|
: plugins.length === 0
|
||||||
t('general.noResult')
|
? t('general.noResult')
|
||||||
) : (
|
: chunks.map((chunk, i) => (
|
||||||
chunks.map((chunk, i) => (
|
<div key={`${chunk[0].name}&${chunk[1]?.name}`} className='row'>
|
||||||
<div key={`${chunk[0].name}&${chunk[1]?.name}`} className='row'>
|
{(chunk as Plugin[]).map((plugin, index) => (
|
||||||
{(chunk as Plugin[]).map((plugin, index) => (
|
<div key={plugin.name} className='col-md-6'>
|
||||||
<div key={plugin.name} className='col-md-6'>
|
<InfoBox
|
||||||
<InfoBox
|
plugin={plugin}
|
||||||
plugin={plugin}
|
baseUrl={blessing.base_url}
|
||||||
baseUrl={blessing.base_url}
|
onEnable={async plugin => handleEnable(plugin, i * 2 + index)}
|
||||||
onEnable={async plugin => handleEnable(plugin, i * 2 + index)}
|
onDisable={async plugin => handleDisable(plugin, i * 2 + index)}
|
||||||
onDisable={async plugin => handleDisable(plugin, i * 2 + index)}
|
onDelete={handleDelete}
|
||||||
onDelete={handleDelete}
|
/>
|
||||||
/>
|
</div>
|
||||||
</div>
|
))}
|
||||||
))}
|
</div>
|
||||||
</div>
|
))}
|
||||||
))
|
|
||||||
))}
|
|
||||||
</div>
|
</div>
|
||||||
<div className='col-lg-4'>
|
<div className='col-lg-4'>
|
||||||
<div className='card card-primary card-outline'>
|
<div className='card card-primary card-outline'>
|
||||||
|
|
|
||||||
|
|
@ -5,8 +5,8 @@ import {t} from '@/scripts/i18n';
|
||||||
type Properties = {
|
type Properties = {
|
||||||
readonly plugin: Plugin;
|
readonly plugin: Plugin;
|
||||||
readonly isInstalling: boolean;
|
readonly isInstalling: boolean;
|
||||||
onInstall(): void;
|
onInstall: () => void;
|
||||||
onUpdate(): void;
|
onUpdate: () => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
const Row: React.FC<Properties> = properties => {
|
const Row: React.FC<Properties> = properties => {
|
||||||
|
|
@ -27,63 +27,71 @@ const Row: React.FC<Properties> = properties => {
|
||||||
<td>{plugin.author}</td>
|
<td>{plugin.author}</td>
|
||||||
<td>{plugin.version}</td>
|
<td>{plugin.version}</td>
|
||||||
<td style={{width: '100px'}}>
|
<td style={{width: '100px'}}>
|
||||||
{allDeps.length === 0 ? (
|
{allDeps.length === 0
|
||||||
<i>{t('admin.noDependencies')}</i>
|
? <i>{t('admin.noDependencies')}</i>
|
||||||
) : (
|
: (
|
||||||
<div className='d-flex flex-column'>
|
<div className='d-flex flex-column'>
|
||||||
{allDeps.map(([name, constraint]) => {
|
{allDeps.map(([name, constraint]) => {
|
||||||
const classes = [
|
const classes = [
|
||||||
'mb-1',
|
'mb-1',
|
||||||
'badge',
|
'badge',
|
||||||
`bg-${unsatisfied.includes(name) ? 'red' : 'green'}`,
|
`bg-${unsatisfied.includes(name) ? 'red' : 'green'}`,
|
||||||
];
|
];
|
||||||
return (
|
return (
|
||||||
<span key={name} className={classes.join(' ')}>
|
<span key={name} className={classes.join(' ')}>
|
||||||
{name}: {constraint}
|
{name}
|
||||||
</span>
|
:
|
||||||
);
|
{constraint}
|
||||||
})}
|
</span>
|
||||||
</div>
|
);
|
||||||
)}
|
})}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</td>
|
</td>
|
||||||
<td style={{width: '12%'}}>
|
<td style={{width: '12%'}}>
|
||||||
{plugin.can_update ? (
|
{plugin.can_update
|
||||||
<button
|
? (
|
||||||
className='btn btn-success'
|
<button
|
||||||
disabled={isInstalling}
|
className='btn btn-success'
|
||||||
onClick={properties.onUpdate}
|
disabled={isInstalling}
|
||||||
>
|
onClick={properties.onUpdate}
|
||||||
{isInstalling ? (
|
>
|
||||||
<>
|
{isInstalling
|
||||||
<i className='fas fa-spinner fa-spin mr-1'/>
|
? (
|
||||||
{t('admin.pluginUpdating')}
|
<>
|
||||||
</>
|
<i className='fas fa-spinner fa-spin mr-1'/>
|
||||||
) : (
|
{t('admin.pluginUpdating')}
|
||||||
<>
|
</>
|
||||||
<i className='fas fa-sync-alt mr-1'/>
|
)
|
||||||
{t('admin.updatePlugin')}
|
: (
|
||||||
</>
|
<>
|
||||||
)}
|
<i className='fas fa-sync-alt mr-1'/>
|
||||||
</button>
|
{t('admin.updatePlugin')}
|
||||||
) : (
|
</>
|
||||||
<button
|
)}
|
||||||
className='btn btn-default'
|
</button>
|
||||||
disabled={properties.isInstalling || Boolean(plugin.installed)}
|
)
|
||||||
onClick={properties.onInstall}
|
: (
|
||||||
>
|
<button
|
||||||
{isInstalling ? (
|
className='btn btn-default'
|
||||||
<>
|
disabled={properties.isInstalling || Boolean(plugin.installed)}
|
||||||
<i className='fas fa-spinner fa-spin mr-1'/>
|
onClick={properties.onInstall}
|
||||||
{t('admin.pluginInstalling')}
|
>
|
||||||
</>
|
{isInstalling
|
||||||
) : (
|
? (
|
||||||
<>
|
<>
|
||||||
<i className='fas fa-download mr-1'/>
|
<i className='fas fa-spinner fa-spin mr-1'/>
|
||||||
{t('admin.installPlugin')}
|
{t('admin.pluginInstalling')}
|
||||||
</>
|
</>
|
||||||
)}
|
)
|
||||||
</button>
|
: (
|
||||||
)}
|
<>
|
||||||
|
<i className='fas fa-download mr-1'/>
|
||||||
|
{t('admin.installPlugin')}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,13 @@
|
||||||
import {useState, useEffect, useMemo} from 'react';
|
|
||||||
import {enableMapSet} from 'immer';
|
|
||||||
import {useImmer} from 'use-immer';
|
|
||||||
import type {Plugin} from './types';
|
import type {Plugin} from './types';
|
||||||
import Row from './Row';
|
|
||||||
import {t} from '@/scripts/i18n';
|
|
||||||
import * as fetch from '@/scripts/net';
|
|
||||||
import {toast, showModal} from '@/scripts/notify';
|
|
||||||
import Loading from '@/components/Loading';
|
import Loading from '@/components/Loading';
|
||||||
import Pagination from '@/components/Pagination';
|
import Pagination from '@/components/Pagination';
|
||||||
|
import {t} from '@/scripts/i18n';
|
||||||
|
import * as fetch from '@/scripts/net';
|
||||||
|
import {showModal, toast} from '@/scripts/notify';
|
||||||
|
import {enableMapSet} from 'immer';
|
||||||
|
import {useEffect, useMemo, useState} from 'react';
|
||||||
|
import {useImmer} from 'use-immer';
|
||||||
|
import Row from './Row';
|
||||||
|
|
||||||
enableMapSet();
|
enableMapSet();
|
||||||
|
|
||||||
|
|
@ -21,10 +21,8 @@ export default function PluginsMarket() {
|
||||||
|
|
||||||
const searchedPlugins = useMemo(
|
const searchedPlugins = useMemo(
|
||||||
() =>
|
() =>
|
||||||
plugins.filter(
|
plugins.filter(plugin =>
|
||||||
plugin =>
|
plugin.name.includes(search) || plugin.title.includes(search)),
|
||||||
plugin.name.includes(search) || plugin.title.includes(search),
|
|
||||||
),
|
|
||||||
[plugins, search],
|
[plugins, search],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -45,9 +43,7 @@ export default function PluginsMarket() {
|
||||||
setSearch(search);
|
setSearch(search);
|
||||||
setPage(1);
|
setPage(1);
|
||||||
|
|
||||||
const searchedPlugins = plugins.filter(
|
const searchedPlugins = plugins.filter(plugin => plugin.name.includes(search) || plugin.title.includes(search));
|
||||||
plugin => plugin.name.includes(search) || plugin.title.includes(search),
|
|
||||||
);
|
|
||||||
setTotalPages(Math.ceil(searchedPlugins.length / 10));
|
setTotalPages(Math.ceil(searchedPlugins.length / 10));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -79,9 +75,8 @@ export default function PluginsMarket() {
|
||||||
<div>
|
<div>
|
||||||
<p>{message}</p>
|
<p>{message}</p>
|
||||||
<ul>
|
<ul>
|
||||||
{data.reason.map((t, i) => (
|
{data.reason.map((t, i) =>
|
||||||
<li key={i}>{t}</li>
|
<li key={i}>{t}</li>)}
|
||||||
))}
|
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
),
|
),
|
||||||
|
|
@ -122,39 +117,41 @@ export default function PluginsMarket() {
|
||||||
onChange={handleSearchChange}
|
onChange={handleSearchChange}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{isLoading ? (
|
{isLoading
|
||||||
<div className='card-body'>
|
? (
|
||||||
<Loading/>
|
<div className='card-body'>
|
||||||
</div>
|
<Loading/>
|
||||||
) : (searchedPlugins.length === 0 ? (
|
</div>
|
||||||
<div className='card-body text-center'>{t('general.noResult')}</div>
|
)
|
||||||
) : (
|
: searchedPlugins.length === 0
|
||||||
<div className='card-body table-responsive p-0'>
|
? <div className='card-body text-center'>{t('general.noResult')}</div>
|
||||||
<table className='table table-striped'>
|
: (
|
||||||
<thead>
|
<div className='card-body table-responsive p-0'>
|
||||||
<tr>
|
<table className='table table-striped'>
|
||||||
<th>{t('admin.pluginTitle')}</th>
|
<thead>
|
||||||
<th>{t('admin.pluginDescription')}</th>
|
<tr>
|
||||||
<th>{t('admin.pluginAuthor')}</th>
|
<th>{t('admin.pluginTitle')}</th>
|
||||||
<th>{t('admin.pluginVersion')}</th>
|
<th>{t('admin.pluginDescription')}</th>
|
||||||
<th>{t('admin.pluginDependencies')}</th>
|
<th>{t('admin.pluginAuthor')}</th>
|
||||||
<th>{t('admin.operationsTitle')}</th>
|
<th>{t('admin.pluginVersion')}</th>
|
||||||
</tr>
|
<th>{t('admin.pluginDependencies')}</th>
|
||||||
</thead>
|
<th>{t('admin.operationsTitle')}</th>
|
||||||
<tbody>
|
</tr>
|
||||||
{pagedPlugins.map((plugin, i) => (
|
</thead>
|
||||||
<Row
|
<tbody>
|
||||||
key={plugin.name}
|
{pagedPlugins.map((plugin, i) => (
|
||||||
plugin={plugin}
|
<Row
|
||||||
isInstalling={installings.has(plugin.name)}
|
key={plugin.name}
|
||||||
onInstall={async () => handleInstall(plugin, ((page - 1) * 10) + i)}
|
plugin={plugin}
|
||||||
onUpdate={async () => handleUpdate(plugin, ((page - 1) * 10) + i)}
|
isInstalling={installings.has(plugin.name)}
|
||||||
/>
|
onInstall={async () => handleInstall(plugin, ((page - 1) * 10) + i)}
|
||||||
))}
|
onUpdate={async () => handleUpdate(plugin, ((page - 1) * 10) + i)}
|
||||||
</tbody>
|
/>
|
||||||
</table>
|
))}
|
||||||
</div>
|
</tbody>
|
||||||
))}
|
</table>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
<div className='card-footer'>
|
<div className='card-footer'>
|
||||||
<div className='float-right'>
|
<div className='float-right'>
|
||||||
<Pagination page={page} totalPages={totalPages} onChange={setPage}/>
|
<Pagination page={page} totalPages={totalPages} onChange={setPage}/>
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
|
|
||||||
|
import type {Texture} from '@/scripts/types';
|
||||||
|
import {t} from '@/scripts/i18n';
|
||||||
import styled from '@emotion/styled';
|
import styled from '@emotion/styled';
|
||||||
import {type Report, Status} from './types';
|
import {type Report, Status} from './types';
|
||||||
import {t} from '@/scripts/i18n';
|
|
||||||
import type {Texture} from '@/scripts/types';
|
|
||||||
|
|
||||||
const Card = styled.div`
|
const Card = styled.div`
|
||||||
width: 240px;
|
width: 240px;
|
||||||
|
|
@ -31,10 +31,10 @@ const Card = styled.div`
|
||||||
|
|
||||||
type Properties = {
|
type Properties = {
|
||||||
readonly report: Report;
|
readonly report: Report;
|
||||||
onClick(texture: Texture | undefined): void;
|
onClick: (texture: Texture | undefined) => void;
|
||||||
onBan(): void;
|
onBan: () => void;
|
||||||
onDelete(): void;
|
onDelete: () => void;
|
||||||
onReject(): void;
|
onReject: () => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
const ImageBox: React.FC<Properties> = properties => {
|
const ImageBox: React.FC<Properties> = properties => {
|
||||||
|
|
@ -54,7 +54,10 @@ const ImageBox: React.FC<Properties> = properties => {
|
||||||
{': '}
|
{': '}
|
||||||
</b>
|
</b>
|
||||||
<span className='mr-1'>{report.texture_uploader?.nickname}</span>
|
<span className='mr-1'>{report.texture_uploader?.nickname}</span>
|
||||||
(UID: {report.uploader})
|
(UID:
|
||||||
|
{' '}
|
||||||
|
{report.uploader}
|
||||||
|
)
|
||||||
</div>
|
</div>
|
||||||
<div className='card-body'>
|
<div className='card-body'>
|
||||||
<picture>
|
<picture>
|
||||||
|
|
@ -70,14 +73,15 @@ const ImageBox: React.FC<Properties> = properties => {
|
||||||
<div className='card-footer'>
|
<div className='card-footer'>
|
||||||
<div className='d-flex justify-content-between'>
|
<div className='d-flex justify-content-between'>
|
||||||
<div>
|
<div>
|
||||||
{report.status === Status.Pending ? (
|
{report.status === Status.Pending
|
||||||
<span className='badge bg-warning'>{t('report.status.0')}</span>
|
? <span className='badge bg-warning'>{t('report.status.0')}</span>
|
||||||
) : (report.status === Status.Resolved ? (
|
: report.status === Status.Resolved
|
||||||
<span className='badge bg-success'>{t('report.status.1')}</span>
|
? <span className='badge bg-success'>{t('report.status.1')}</span>
|
||||||
) : (
|
: <span className='badge bg-danger'>{t('report.status.2')}</span>}
|
||||||
<span className='badge bg-danger'>{t('report.status.2')}</span>
|
<span className='badge bg-info ml-1'>
|
||||||
))}
|
TID:
|
||||||
<span className='badge bg-info ml-1'>TID: {report.tid}</span>
|
{report.tid}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className='dropdown'>
|
<div className='dropdown'>
|
||||||
<a
|
<a
|
||||||
|
|
@ -92,7 +96,8 @@ const ImageBox: React.FC<Properties> = properties => {
|
||||||
<a
|
<a
|
||||||
href={`${blessing.base_url}/skinlib/show/${report.tid}`}
|
href={`${blessing.base_url}/skinlib/show/${report.tid}`}
|
||||||
className='dropdown-item'
|
className='dropdown-item'
|
||||||
target='_blank' rel='noreferrer'
|
target='_blank'
|
||||||
|
rel='noreferrer'
|
||||||
>
|
>
|
||||||
<i className='fas fa-share-square mr-2'/>
|
<i className='fas fa-share-square mr-2'/>
|
||||||
{t('user.viewInSkinlib')}
|
{t('user.viewInSkinlib')}
|
||||||
|
|
@ -122,7 +127,10 @@ const ImageBox: React.FC<Properties> = properties => {
|
||||||
{': '}
|
{': '}
|
||||||
</b>
|
</b>
|
||||||
<span className='mr-1'>{report.informer?.nickname}</span>
|
<span className='mr-1'>{report.informer?.nickname}</span>
|
||||||
(UID: {report.reporter})
|
(UID:
|
||||||
|
{' '}
|
||||||
|
{report.reporter}
|
||||||
|
)
|
||||||
</div>
|
</div>
|
||||||
<details>
|
<details>
|
||||||
<summary className='text-truncate'>
|
<summary className='text-truncate'>
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,14 @@
|
||||||
import React, {useState, useEffect} from 'react';
|
|
||||||
import {useImmer} from 'use-immer';
|
|
||||||
import type {Report, Status} from './types';
|
import type {Report, Status} from './types';
|
||||||
import ImageBox from './ImageBox';
|
|
||||||
import {t} from '@/scripts/i18n';
|
|
||||||
import * as fetch from '@/scripts/net';
|
|
||||||
import {type Paginator, type Texture, TextureType} from '@/scripts/types';
|
|
||||||
import {toast, showModal} from '@/scripts/notify';
|
|
||||||
import Loading from '@/components/Loading';
|
import Loading from '@/components/Loading';
|
||||||
import Pagination from '@/components/Pagination';
|
import Pagination from '@/components/Pagination';
|
||||||
import ViewerSkeleton from '@/components/ViewerSkeleton';
|
import ViewerSkeleton from '@/components/ViewerSkeleton';
|
||||||
|
import {t} from '@/scripts/i18n';
|
||||||
|
import * as fetch from '@/scripts/net';
|
||||||
|
import {showModal, toast} from '@/scripts/notify';
|
||||||
|
import {type Paginator, type Texture, TextureType} from '@/scripts/types';
|
||||||
|
import React, {useEffect, useState} from 'react';
|
||||||
|
import {useImmer} from 'use-immer';
|
||||||
|
import ImageBox from './ImageBox';
|
||||||
|
|
||||||
const Previewer = React.lazy(async () => import('@/components/Viewer'));
|
const Previewer = React.lazy(async () => import('@/components/Viewer'));
|
||||||
|
|
||||||
|
|
@ -104,26 +104,28 @@ function ReportsManagement() {
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
{isLoading ? (
|
{isLoading
|
||||||
<div className='card-body'>
|
? (
|
||||||
<Loading/>
|
<div className='card-body'>
|
||||||
</div>
|
<Loading/>
|
||||||
) : (reports.length === 0 ? (
|
</div>
|
||||||
<div className='card-body text-center'>{t('general.noResult')}</div>
|
)
|
||||||
) : (
|
: reports.length === 0
|
||||||
<div className='card-body d-flex flex-wrap'>
|
? <div className='card-body text-center'>{t('general.noResult')}</div>
|
||||||
{reports.map((report, i) => (
|
: (
|
||||||
<ImageBox
|
<div className='card-body d-flex flex-wrap'>
|
||||||
key={report.id}
|
{reports.map((report, i) => (
|
||||||
report={report}
|
<ImageBox
|
||||||
onClick={setViewingTexture}
|
key={report.id}
|
||||||
onBan={async () => handleProceedReport(report, i, 'ban')}
|
report={report}
|
||||||
onDelete={async () => handleDelete(report, i)}
|
onClick={setViewingTexture}
|
||||||
onReject={async () => handleProceedReport(report, i, 'reject')}
|
onBan={async () => handleProceedReport(report, i, 'ban')}
|
||||||
/>
|
onDelete={async () => handleDelete(report, i)}
|
||||||
))}
|
onReject={async () => handleProceedReport(report, i, 'reject')}
|
||||||
</div>
|
/>
|
||||||
))}
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
<div className='card-footer'>
|
<div className='card-footer'>
|
||||||
<div className='float-right'>
|
<div className='float-right'>
|
||||||
<Pagination
|
<Pagination
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import styled from '@emotion/styled';
|
|
||||||
import type {Line} from './types';
|
import type {Line} from './types';
|
||||||
import {t} from '@/scripts/i18n';
|
import {t} from '@/scripts/i18n';
|
||||||
|
import styled from '@emotion/styled';
|
||||||
|
|
||||||
const Group = styled.td`
|
const Group = styled.td`
|
||||||
width: 15%;
|
width: 15%;
|
||||||
|
|
@ -14,8 +14,8 @@ const Operations = styled.td`
|
||||||
|
|
||||||
type Properties = {
|
type Properties = {
|
||||||
readonly line: Line;
|
readonly line: Line;
|
||||||
onEdit(line: Line): void;
|
onEdit: (line: Line) => void;
|
||||||
onRemove(line: Line): void;
|
onRemove: (line: Line) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
const Row: React.FC<Properties> = properties => {
|
const Row: React.FC<Properties> = properties => {
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,13 @@
|
||||||
import React, {useState, useEffect} from 'react';
|
import type {Paginator} from '@/scripts/types';
|
||||||
import {useImmer} from 'use-immer';
|
|
||||||
import type {Line} from './types';
|
import type {Line} from './types';
|
||||||
import Row from './Row';
|
import Loading from '@/components/Loading';
|
||||||
|
import Pagination from '@/components/Pagination';
|
||||||
import {t} from '@/scripts/i18n';
|
import {t} from '@/scripts/i18n';
|
||||||
import * as fetch from '@/scripts/net';
|
import * as fetch from '@/scripts/net';
|
||||||
import {showModal, toast} from '@/scripts/notify';
|
import {showModal, toast} from '@/scripts/notify';
|
||||||
import type {Paginator} from '@/scripts/types';
|
import React, {useEffect, useState} from 'react';
|
||||||
import Loading from '@/components/Loading';
|
import {useImmer} from 'use-immer';
|
||||||
import Pagination from '@/components/Pagination';
|
import Row from './Row';
|
||||||
|
|
||||||
function Translations() {
|
function Translations() {
|
||||||
const [lines, setLines] = useImmer<Line[]>([]);
|
const [lines, setLines] = useImmer<Line[]>([]);
|
||||||
|
|
@ -85,28 +85,30 @@ function Translations() {
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{isLoading ? (
|
{isLoading
|
||||||
<tr>
|
? (
|
||||||
<td className='text-center' colSpan={4}>
|
<tr>
|
||||||
<Loading/>
|
<td className='text-center' colSpan={4}>
|
||||||
</td>
|
<Loading/>
|
||||||
</tr>
|
</td>
|
||||||
) : (lines.length === 0 ? (
|
</tr>
|
||||||
<tr>
|
)
|
||||||
<td className='text-center' colSpan={4}>
|
: lines.length === 0
|
||||||
{t('general.noResult')}
|
? (
|
||||||
</td>
|
<tr>
|
||||||
</tr>
|
<td className='text-center' colSpan={4}>
|
||||||
) : (
|
{t('general.noResult')}
|
||||||
lines.map((line, i) => (
|
</td>
|
||||||
<Row
|
</tr>
|
||||||
key={line.id}
|
)
|
||||||
line={line}
|
: lines.map((line, i) => (
|
||||||
onEdit={async line => handleEdit(line, i)}
|
<Row
|
||||||
onRemove={handleRemove}
|
key={line.id}
|
||||||
/>
|
line={line}
|
||||||
))
|
onEdit={async line => handleEdit(line, i)}
|
||||||
))}
|
onRemove={handleRemove}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -1,15 +1,13 @@
|
||||||
|
import {t} from '../../scripts/i18n';
|
||||||
import {post, type ResponseBody} from '../../scripts/net';
|
import {post, type ResponseBody} from '../../scripts/net';
|
||||||
import {showModal} from '../../scripts/notify';
|
import {showModal} from '../../scripts/notify';
|
||||||
import {t} from '../../scripts/i18n';
|
|
||||||
|
|
||||||
export default async function handler(event: MouseEvent) {
|
export default async function handler(event: MouseEvent) {
|
||||||
const button = event.target as HTMLButtonElement;
|
const button = event.target as HTMLButtonElement;
|
||||||
button.disabled = true;
|
button.disabled = true;
|
||||||
|
|
||||||
const text = button.textContent;
|
const text = button.textContent;
|
||||||
button.innerHTML = `<i class="fas fa-spinner fa-spin"></i> ${t(
|
button.innerHTML = `<i class="fas fa-spinner fa-spin"></i> ${t('admin.downloading')}`;
|
||||||
'admin.downloading',
|
|
||||||
)}`;
|
|
||||||
|
|
||||||
const {code, message}: ResponseBody = await post('/admin/update/download');
|
const {code, message}: ResponseBody = await post('/admin/update/download');
|
||||||
button.textContent = text;
|
button.textContent = text;
|
||||||
|
|
|
||||||
|
|
@ -1,25 +1,25 @@
|
||||||
|
|
||||||
|
import type {User} from '@/scripts/types';
|
||||||
|
import {t} from '@/scripts/i18n';
|
||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
import {Box, Icon, InfoTable} from './styles';
|
import {Box, Icon, InfoTable} from './styles';
|
||||||
import {
|
import {
|
||||||
|
canModifyPermission,
|
||||||
|
canModifyUser,
|
||||||
humanizePermission,
|
humanizePermission,
|
||||||
verificationStatusText,
|
verificationStatusText,
|
||||||
canModifyUser,
|
|
||||||
canModifyPermission,
|
|
||||||
} from './utils';
|
} from './utils';
|
||||||
import {t} from '@/scripts/i18n';
|
|
||||||
import type {User} from '@/scripts/types';
|
|
||||||
|
|
||||||
type Properties = {
|
type Properties = {
|
||||||
readonly user: User;
|
readonly user: User;
|
||||||
readonly currentUser: User;
|
readonly currentUser: User;
|
||||||
onEmailChange(): void;
|
onEmailChange: () => void;
|
||||||
onNicknameChange(): void;
|
onNicknameChange: () => void;
|
||||||
onScoreChange(): void;
|
onScoreChange: () => void;
|
||||||
onPermissionChange(): void;
|
onPermissionChange: () => void;
|
||||||
onVerificationToggle(): void;
|
onVerificationToggle: () => void;
|
||||||
onPasswordChange(): void;
|
onPasswordChange: () => void;
|
||||||
onDelete(): void;
|
onDelete: () => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
const Card: React.FC<Properties> = properties => {
|
const Card: React.FC<Properties> = properties => {
|
||||||
|
|
@ -124,7 +124,10 @@ const Card: React.FC<Properties> = properties => {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<div>UID: {user.uid}</div>
|
<div>
|
||||||
|
UID:
|
||||||
|
{user.uid}
|
||||||
|
</div>
|
||||||
<div>
|
<div>
|
||||||
{t('general.user.email')}
|
{t('general.user.email')}
|
||||||
{': '}
|
{': '}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
|
import {Breakpoint, lessThan} from '@/styles/breakpoints';
|
||||||
import styled from '@emotion/styled';
|
import styled from '@emotion/styled';
|
||||||
import {lessThan, Breakpoint} from '@/styles/breakpoints';
|
|
||||||
|
|
||||||
const Header = styled.div`
|
const Header = styled.div`
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
import styled from '@emotion/styled';
|
|
||||||
import Skeleton from 'react-loading-skeleton';
|
|
||||||
import clsx from 'clsx';
|
|
||||||
import {Box, Icon, InfoTable} from './styles';
|
|
||||||
import {t} from '@/scripts/i18n';
|
import {t} from '@/scripts/i18n';
|
||||||
|
import styled from '@emotion/styled';
|
||||||
|
import clsx from 'clsx';
|
||||||
|
import Skeleton from 'react-loading-skeleton';
|
||||||
|
import {Box, Icon, InfoTable} from './styles';
|
||||||
|
|
||||||
const ShrinkedSkeleton = styled(Skeleton)<{width?: string}>`
|
const ShrinkedSkeleton = styled(Skeleton)<{width?: string}>`
|
||||||
width: ${properties => properties.width};
|
width: ${properties => properties.width};
|
||||||
|
|
|
||||||
|
|
@ -1,24 +1,24 @@
|
||||||
|
|
||||||
import {
|
|
||||||
humanizePermission,
|
|
||||||
verificationStatusText,
|
|
||||||
canModifyUser,
|
|
||||||
canModifyPermission,
|
|
||||||
} from './utils';
|
|
||||||
import {t} from '@/scripts/i18n';
|
|
||||||
import type {User} from '@/scripts/types';
|
import type {User} from '@/scripts/types';
|
||||||
import ButtonEdit from '@/components/ButtonEdit';
|
import ButtonEdit from '@/components/ButtonEdit';
|
||||||
|
import {t} from '@/scripts/i18n';
|
||||||
|
import {
|
||||||
|
canModifyPermission,
|
||||||
|
canModifyUser,
|
||||||
|
humanizePermission,
|
||||||
|
verificationStatusText,
|
||||||
|
} from './utils';
|
||||||
|
|
||||||
type Properties = {
|
type Properties = {
|
||||||
readonly user: User;
|
readonly user: User;
|
||||||
readonly currentUser: User;
|
readonly currentUser: User;
|
||||||
onEmailChange(): void;
|
onEmailChange: () => void;
|
||||||
onNicknameChange(): void;
|
onNicknameChange: () => void;
|
||||||
onScoreChange(): void;
|
onScoreChange: () => void;
|
||||||
onPermissionChange(): void;
|
onPermissionChange: () => void;
|
||||||
onVerificationToggle(): void;
|
onVerificationToggle: () => void;
|
||||||
onPasswordChange(): void;
|
onPasswordChange: () => void;
|
||||||
onDelete(): void;
|
onDelete: () => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
const Row: React.FC<Properties> = properties => {
|
const Row: React.FC<Properties> = properties => {
|
||||||
|
|
@ -82,11 +82,9 @@ const Row: React.FC<Properties> = properties => {
|
||||||
title={t('admin.toggleVerification')}
|
title={t('admin.toggleVerification')}
|
||||||
onClick={properties.onVerificationToggle}
|
onClick={properties.onVerificationToggle}
|
||||||
>
|
>
|
||||||
{user.verified ? (
|
{user.verified
|
||||||
<i className='fas fa-toggle-on'/>
|
? <i className='fas fa-toggle-on'/>
|
||||||
) : (
|
: <i className='fas fa-toggle-off'/>}
|
||||||
<i className='fas fa-toggle-off'/>
|
|
||||||
)}
|
|
||||||
</a>
|
</a>
|
||||||
)}
|
)}
|
||||||
</td>
|
</td>
|
||||||
|
|
|
||||||
|
|
@ -1,19 +1,19 @@
|
||||||
import {useState, useEffect, useLayoutEffect} from 'react';
|
import type {Props as ModalInputProperties} from '@/components/ModalInput';
|
||||||
import {useImmer} from 'use-immer';
|
import Pagination from '@/components/Pagination';
|
||||||
import Header from './Header';
|
|
||||||
import Card from './Card';
|
|
||||||
import LoadingCard from './LoadingCard';
|
|
||||||
import Row from './Row';
|
|
||||||
import LoadingRow from './LoadingRow';
|
|
||||||
import useBlessingExtra from '@/scripts/hooks/useBlessingExtra';
|
import useBlessingExtra from '@/scripts/hooks/useBlessingExtra';
|
||||||
import useIsLargeScreen from '@/scripts/hooks/useIsLargeScreen';
|
import useIsLargeScreen from '@/scripts/hooks/useIsLargeScreen';
|
||||||
import {t} from '@/scripts/i18n';
|
import {t} from '@/scripts/i18n';
|
||||||
import * as fetch from '@/scripts/net';
|
import * as fetch from '@/scripts/net';
|
||||||
import {type User, UserPermission, type Paginator} from '@/scripts/types';
|
import {showModal, toast} from '@/scripts/notify';
|
||||||
import {toast, showModal} from '@/scripts/notify';
|
import {type Paginator, type User, UserPermission} from '@/scripts/types';
|
||||||
import urls from '@/scripts/urls';
|
import urls from '@/scripts/urls';
|
||||||
import type {Props as ModalInputProperties} from '@/components/ModalInput';
|
import {useEffect, useLayoutEffect, useState} from 'react';
|
||||||
import Pagination from '@/components/Pagination';
|
import {useImmer} from 'use-immer';
|
||||||
|
import Card from './Card';
|
||||||
|
import Header from './Header';
|
||||||
|
import LoadingCard from './LoadingCard';
|
||||||
|
import LoadingRow from './LoadingRow';
|
||||||
|
import Row from './Row';
|
||||||
|
|
||||||
function UsersManagement() {
|
function UsersManagement() {
|
||||||
const [users, setUsers] = useImmer<User[]>([]);
|
const [users, setUsers] = useImmer<User[]>([]);
|
||||||
|
|
@ -196,9 +196,7 @@ function UsersManagement() {
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleVerificationToggle = async (user: User, index: number) => {
|
const handleVerificationToggle = async (user: User, index: number) => {
|
||||||
const {code, message} = await fetch.put<fetch.ResponseBody>(
|
const {code, message} = await fetch.put<fetch.ResponseBody>(urls.admin.users.verification(user.uid));
|
||||||
urls.admin.users.verification(user.uid),
|
|
||||||
);
|
|
||||||
if (code === 0) {
|
if (code === 0) {
|
||||||
toast.success(message);
|
toast.success(message);
|
||||||
setUsers(users => {
|
setUsers(users => {
|
||||||
|
|
@ -298,28 +296,52 @@ function UsersManagement() {
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
</Header>
|
</Header>
|
||||||
{users.length === 0 && !isLoading ? (
|
{users.length === 0 && !isLoading
|
||||||
<div className='card-body text-center'>{t('general.noResult')}</div>
|
? <div className='card-body text-center'>{t('general.noResult')}</div>
|
||||||
) : (isTableMode ? (
|
: isTableMode
|
||||||
<div className='card-body table-responsive p-0'>
|
? (
|
||||||
<table className={`table ${isLoading ? '' : 'table-striped'}`}>
|
<div className='card-body table-responsive p-0'>
|
||||||
<thead>
|
<table className={`table ${isLoading ? '' : 'table-striped'}`}>
|
||||||
<tr>
|
<thead>
|
||||||
<th>UID</th>
|
<tr>
|
||||||
<th>{t('general.user.email')}</th>
|
<th>UID</th>
|
||||||
<th>{t('general.user.nickname')}</th>
|
<th>{t('general.user.email')}</th>
|
||||||
<th>{t('general.user.score')}</th>
|
<th>{t('general.user.nickname')}</th>
|
||||||
<th>{t('admin.permission')}</th>
|
<th>{t('general.user.score')}</th>
|
||||||
<th>{t('admin.verification')}</th>
|
<th>{t('admin.permission')}</th>
|
||||||
<th>{t('general.user.register-at')}</th>
|
<th>{t('admin.verification')}</th>
|
||||||
<th>{t('admin.operationsTitle')}</th>
|
<th>{t('general.user.register-at')}</th>
|
||||||
</tr>
|
<th>{t('admin.operationsTitle')}</th>
|
||||||
</thead>
|
</tr>
|
||||||
<tbody>
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{isLoading
|
||||||
|
? Array.from({length: 10}).fill(null).map((_, i) => <LoadingRow key={i}/>)
|
||||||
|
: users.map((user, i) => (
|
||||||
|
<Row
|
||||||
|
key={user.uid}
|
||||||
|
user={user}
|
||||||
|
currentUser={currentUser}
|
||||||
|
onEmailChange={async () => handleEmailChange(user, i)}
|
||||||
|
onNicknameChange={async () => handleNicknameChange(user, i)}
|
||||||
|
onScoreChange={async () => handleScoreChange(user, i)}
|
||||||
|
onPermissionChange={async () => handlePermissionChange(user, i)}
|
||||||
|
onVerificationToggle={async () =>
|
||||||
|
handleVerificationToggle(user, i)}
|
||||||
|
onPasswordChange={async () => handlePasswordChange(user)}
|
||||||
|
onDelete={async () => handleDelete(user)}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
: (
|
||||||
|
<div className='card-body d-flex flex-wrap'>
|
||||||
{isLoading
|
{isLoading
|
||||||
? Array.from({length: 10}).fill(null).map((_, i) => <LoadingRow key={i}/>)
|
? Array.from({length: 10}).fill(null).map((_, i) => <LoadingCard key={i}/>)
|
||||||
: users.map((user, i) => (
|
: users.map((user, i) => (
|
||||||
<Row
|
<Card
|
||||||
key={user.uid}
|
key={user.uid}
|
||||||
user={user}
|
user={user}
|
||||||
currentUser={currentUser}
|
currentUser={currentUser}
|
||||||
|
|
@ -327,35 +349,13 @@ function UsersManagement() {
|
||||||
onNicknameChange={async () => handleNicknameChange(user, i)}
|
onNicknameChange={async () => handleNicknameChange(user, i)}
|
||||||
onScoreChange={async () => handleScoreChange(user, i)}
|
onScoreChange={async () => handleScoreChange(user, i)}
|
||||||
onPermissionChange={async () => handlePermissionChange(user, i)}
|
onPermissionChange={async () => handlePermissionChange(user, i)}
|
||||||
onVerificationToggle={async () =>
|
onVerificationToggle={async () => handleVerificationToggle(user, i)}
|
||||||
handleVerificationToggle(user, i)}
|
|
||||||
onPasswordChange={async () => handlePasswordChange(user)}
|
onPasswordChange={async () => handlePasswordChange(user)}
|
||||||
onDelete={async () => handleDelete(user)}
|
onDelete={async () => handleDelete(user)}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</tbody>
|
</div>
|
||||||
</table>
|
)}
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div className='card-body d-flex flex-wrap'>
|
|
||||||
{isLoading
|
|
||||||
? Array.from({length: 10}).fill(null).map((_, i) => <LoadingCard key={i}/>)
|
|
||||||
: users.map((user, i) => (
|
|
||||||
<Card
|
|
||||||
key={user.uid}
|
|
||||||
user={user}
|
|
||||||
currentUser={currentUser}
|
|
||||||
onEmailChange={async () => handleEmailChange(user, i)}
|
|
||||||
onNicknameChange={async () => handleNicknameChange(user, i)}
|
|
||||||
onScoreChange={async () => handleScoreChange(user, i)}
|
|
||||||
onPermissionChange={async () => handlePermissionChange(user, i)}
|
|
||||||
onVerificationToggle={async () => handleVerificationToggle(user, i)}
|
|
||||||
onPasswordChange={async () => handlePasswordChange(user)}
|
|
||||||
onDelete={async () => handleDelete(user)}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
<div className='card-footer'>
|
<div className='card-footer'>
|
||||||
<div className='float-right'>
|
<div className='float-right'>
|
||||||
<Pagination page={page} totalPages={totalPages} onChange={setPage}/>
|
<Pagination page={page} totalPages={totalPages} onChange={setPage}/>
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import styled from '@emotion/styled';
|
|
||||||
import * as breakpoints from '@/styles/breakpoints';
|
import * as breakpoints from '@/styles/breakpoints';
|
||||||
|
import styled from '@emotion/styled';
|
||||||
|
|
||||||
export const Box = styled.div`
|
export const Box = styled.div`
|
||||||
width: 48%;
|
width: 48%;
|
||||||
|
|
@ -14,7 +14,7 @@ export const Icon = styled.div<{py?: boolean}>`
|
||||||
width: 70px;
|
width: 70px;
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
padding-top: ${properties => (properties.py ? '22px' : '0')};
|
padding-top: ${properties => properties.py ? '22px' : '0'};
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const InfoTable = styled.div`
|
export const InfoTable = styled.div`
|
||||||
|
|
|
||||||
|
|
@ -1,18 +1,18 @@
|
||||||
import {useState, useRef} from 'react';
|
import Alert from '@/components/Alert';
|
||||||
|
import Captcha from '@/components/Captcha';
|
||||||
|
import EmailSuggestion from '@/components/EmailSuggestion';
|
||||||
import useEmitMounted from '@/scripts/hooks/useEmitMounted';
|
import useEmitMounted from '@/scripts/hooks/useEmitMounted';
|
||||||
import {t} from '@/scripts/i18n';
|
import {t} from '@/scripts/i18n';
|
||||||
import * as fetch from '@/scripts/net';
|
import * as fetch from '@/scripts/net';
|
||||||
import urls from '@/scripts/urls';
|
import urls from '@/scripts/urls';
|
||||||
import Alert from '@/components/Alert';
|
import {useRef, useState} from 'react';
|
||||||
import Captcha from '@/components/Captcha';
|
|
||||||
import EmailSuggestion from '@/components/EmailSuggestion';
|
|
||||||
|
|
||||||
export default function Forgot() {
|
export default function Forgot() {
|
||||||
const [email, setEmail] = useState('');
|
const [email, setEmail] = useState('');
|
||||||
const [isSending, setIsSending] = useState(false);
|
const [isSending, setIsSending] = useState(false);
|
||||||
const [successMessage, setSuccessMessage] = useState('');
|
const [successMessage, setSuccessMessage] = useState('');
|
||||||
const [warningMessage, setWarningMessage] = useState('');
|
const [warningMessage, setWarningMessage] = useState('');
|
||||||
const reference = useRef<Captcha | null>(null);
|
const reference = useRef<Captcha>(null);
|
||||||
|
|
||||||
useEmitMounted();
|
useEmitMounted();
|
||||||
|
|
||||||
|
|
@ -57,14 +57,14 @@ export default function Forgot() {
|
||||||
{t('auth.forgot.login-link')}
|
{t('auth.forgot.login-link')}
|
||||||
</a>
|
</a>
|
||||||
<button className='btn btn-primary' type='submit' disabled={isSending}>
|
<button className='btn btn-primary' type='submit' disabled={isSending}>
|
||||||
{isSending ? (
|
{isSending
|
||||||
<>
|
? (
|
||||||
<i className='fas fa-spinner fa-spin mr-1'/>
|
<>
|
||||||
{t('auth.sending')}
|
<i className='fas fa-spinner fa-spin mr-1'/>
|
||||||
</>
|
{t('auth.sending')}
|
||||||
) : (
|
</>
|
||||||
t('auth.send')
|
)
|
||||||
)}
|
: t('auth.send')}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,13 @@
|
||||||
import {useState, useRef, useEffect} from 'react';
|
import Alert from '@/components/Alert';
|
||||||
|
import Captcha from '@/components/Captcha';
|
||||||
|
import EmailSuggestion from '@/components/EmailSuggestion';
|
||||||
import useBlessingExtra from '@/scripts/hooks/useBlessingExtra';
|
import useBlessingExtra from '@/scripts/hooks/useBlessingExtra';
|
||||||
import useEmitMounted from '@/scripts/hooks/useEmitMounted';
|
import useEmitMounted from '@/scripts/hooks/useEmitMounted';
|
||||||
import {t} from '@/scripts/i18n';
|
import {t} from '@/scripts/i18n';
|
||||||
import * as fetch from '@/scripts/net';
|
import * as fetch from '@/scripts/net';
|
||||||
import {showModal} from '@/scripts/notify';
|
import {showModal} from '@/scripts/notify';
|
||||||
import urls from '@/scripts/urls';
|
import urls from '@/scripts/urls';
|
||||||
import Alert from '@/components/Alert';
|
import {useEffect, useRef, useState} from 'react';
|
||||||
import Captcha from '@/components/Captcha';
|
|
||||||
import EmailSuggestion from '@/components/EmailSuggestion';
|
|
||||||
|
|
||||||
type SuccessfulResponse = {
|
type SuccessfulResponse = {
|
||||||
code: 0;
|
code: 0;
|
||||||
|
|
@ -21,9 +21,7 @@ type FailedResponse = {
|
||||||
};
|
};
|
||||||
type Response = SuccessfulResponse | FailedResponse;
|
type Response = SuccessfulResponse | FailedResponse;
|
||||||
|
|
||||||
function isSuccessfulResponse(
|
function isSuccessfulResponse(response: Response): response is SuccessfulResponse {
|
||||||
response: Response,
|
|
||||||
): response is SuccessfulResponse {
|
|
||||||
return response.code === 0;
|
return response.code === 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -34,7 +32,7 @@ export default function Login() {
|
||||||
const [hasTooManyFails, setHasTooManyFails] = useState(false);
|
const [hasTooManyFails, setHasTooManyFails] = useState(false);
|
||||||
const [isPending, setIsPending] = useState(false);
|
const [isPending, setIsPending] = useState(false);
|
||||||
const [warningMessage, setWarningMessage] = useState('');
|
const [warningMessage, setWarningMessage] = useState('');
|
||||||
const reference = useRef<Captcha | null>(null);
|
const reference = useRef<Captcha>(null);
|
||||||
const recaptcha = useBlessingExtra<string>('recaptcha');
|
const recaptcha = useBlessingExtra<string>('recaptcha');
|
||||||
const invisibleRecaptcha = useBlessingExtra<boolean>('invisible');
|
const invisibleRecaptcha = useBlessingExtra<boolean>('invisible');
|
||||||
|
|
||||||
|
|
@ -111,10 +109,8 @@ export default function Login() {
|
||||||
value={password}
|
value={password}
|
||||||
onChange={handlePasswordChange}
|
onChange={handlePasswordChange}
|
||||||
/>
|
/>
|
||||||
<div className='input-group-append'>
|
<div className='input-group-text'>
|
||||||
<div className='input-group-text'>
|
<i className='fas fa-lock'/>
|
||||||
<i className='fas fa-lock'/>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -126,7 +122,7 @@ export default function Login() {
|
||||||
<label>
|
<label>
|
||||||
<input
|
<input
|
||||||
type='checkbox'
|
type='checkbox'
|
||||||
className='mr-1'
|
className='me-1'
|
||||||
checked={remember}
|
checked={remember}
|
||||||
onChange={handleRememberChange}
|
onChange={handleRememberChange}
|
||||||
/>
|
/>
|
||||||
|
|
@ -140,14 +136,14 @@ export default function Login() {
|
||||||
type='submit'
|
type='submit'
|
||||||
disabled={isPending}
|
disabled={isPending}
|
||||||
>
|
>
|
||||||
{isPending ? (
|
{isPending
|
||||||
<>
|
? (
|
||||||
<i className='fas fa-spinner fa-spin mr-1'/>
|
<>
|
||||||
{t('auth.loggingIn')}
|
<i className='fas fa-spinner fa-spin mr-1'/>
|
||||||
</>
|
{t('auth.loggingIn')}
|
||||||
) : (
|
</>
|
||||||
t('auth.login')
|
)
|
||||||
)}
|
: t('auth.login')}
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,13 @@
|
||||||
import {useState, useRef} from 'react';
|
import Alert from '@/components/Alert';
|
||||||
|
import Captcha from '@/components/Captcha';
|
||||||
|
import EmailSuggestion from '@/components/EmailSuggestion';
|
||||||
import useBlessingExtra from '@/scripts/hooks/useBlessingExtra';
|
import useBlessingExtra from '@/scripts/hooks/useBlessingExtra';
|
||||||
import useEmitMounted from '@/scripts/hooks/useEmitMounted';
|
import useEmitMounted from '@/scripts/hooks/useEmitMounted';
|
||||||
import {t} from '@/scripts/i18n';
|
import {t} from '@/scripts/i18n';
|
||||||
import * as fetch from '@/scripts/net';
|
import * as fetch from '@/scripts/net';
|
||||||
import {toast} from '@/scripts/notify';
|
import {toast} from '@/scripts/notify';
|
||||||
import urls from '@/scripts/urls';
|
import urls from '@/scripts/urls';
|
||||||
import Alert from '@/components/Alert';
|
import {useRef, useState} from 'react';
|
||||||
import Captcha from '@/components/Captcha';
|
|
||||||
import EmailSuggestion from '@/components/EmailSuggestion';
|
|
||||||
|
|
||||||
export default function Registration() {
|
export default function Registration() {
|
||||||
const [email, setEmail] = useState('');
|
const [email, setEmail] = useState('');
|
||||||
|
|
@ -18,8 +18,8 @@ export default function Registration() {
|
||||||
const [isPending, setIsPending] = useState(false);
|
const [isPending, setIsPending] = useState(false);
|
||||||
const [warningMessage, setWarningMessage] = useState('');
|
const [warningMessage, setWarningMessage] = useState('');
|
||||||
const requirePlayer = useBlessingExtra<boolean>('player');
|
const requirePlayer = useBlessingExtra<boolean>('player');
|
||||||
const confirmationReference = useRef<HTMLInputElement | undefined>(null);
|
const confirmationReference = useRef<HTMLInputElement>(null);
|
||||||
const captchaReference = useRef<Captcha | undefined>(null);
|
const captchaReference = useRef<Captcha>(null);
|
||||||
|
|
||||||
useEmitMounted();
|
useEmitMounted();
|
||||||
|
|
||||||
|
|
@ -27,9 +27,7 @@ export default function Registration() {
|
||||||
setPassword(event.target.value);
|
setPassword(event.target.value);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleConfirmationChange = (
|
const handleConfirmationChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
event: React.ChangeEvent<HTMLInputElement>,
|
|
||||||
) => {
|
|
||||||
setConfirmation(event.target.value);
|
setConfirmation(event.target.value);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -37,9 +35,7 @@ export default function Registration() {
|
||||||
setNickName(event.target.value);
|
setNickName(event.target.value);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handlePlayerNameChange = (
|
const handlePlayerNameChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
event: React.ChangeEvent<HTMLInputElement>,
|
|
||||||
) => {
|
|
||||||
setPlayerName(event.target.value);
|
setPlayerName(event.target.value);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -57,8 +53,11 @@ export default function Registration() {
|
||||||
const {code, message} = await fetch.post<fetch.ResponseBody>(
|
const {code, message} = await fetch.post<fetch.ResponseBody>(
|
||||||
urls.auth.register(),
|
urls.auth.register(),
|
||||||
{
|
{
|
||||||
email, password, captcha: await captchaReference.current!.execute(),
|
email,
|
||||||
...(requirePlayer ? {player_name: playerName} : {nickname: nickName}),
|
password,
|
||||||
|
captcha: await captchaReference.current!.execute(),
|
||||||
|
// eslint-disable-next-line ts/naming-convention
|
||||||
|
...requirePlayer ? {player_name: playerName} : {nickname: nickName},
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
if (code === 0) {
|
if (code === 0) {
|
||||||
|
|
@ -121,39 +120,41 @@ export default function Registration() {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{requirePlayer ? (
|
{requirePlayer
|
||||||
<div className='input-group mb-3' title={t('auth.player-name-intro')}>
|
? (
|
||||||
<input
|
<div className='input-group mb-3' title={t('auth.player-name-intro')}>
|
||||||
required
|
<input
|
||||||
type='text'
|
required
|
||||||
className='form-control'
|
type='text'
|
||||||
placeholder={t('auth.player-name')}
|
className='form-control'
|
||||||
value={playerName}
|
placeholder={t('auth.player-name')}
|
||||||
onChange={handlePlayerNameChange}
|
value={playerName}
|
||||||
/>
|
onChange={handlePlayerNameChange}
|
||||||
<div className='input-group-append'>
|
/>
|
||||||
<div className='input-group-text'>
|
<div className='input-group-append'>
|
||||||
<i className='fas fa-gamepad'/>
|
<div className='input-group-text'>
|
||||||
|
<i className='fas fa-gamepad'/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
)
|
||||||
) : (
|
: (
|
||||||
<div className='input-group mb-3' title={t('auth.nickname-intro')}>
|
<div className='input-group mb-3' title={t('auth.nickname-intro')}>
|
||||||
<input
|
<input
|
||||||
required
|
required
|
||||||
type='text'
|
type='text'
|
||||||
className='form-control'
|
className='form-control'
|
||||||
placeholder={t('auth.nickname')}
|
placeholder={t('auth.nickname')}
|
||||||
value={nickName}
|
value={nickName}
|
||||||
onChange={handleNickNameChange}
|
onChange={handleNickNameChange}
|
||||||
/>
|
/>
|
||||||
<div className='input-group-append'>
|
<div className='input-group-append'>
|
||||||
<div className='input-group-text'>
|
<div className='input-group-text'>
|
||||||
<i className='fas fa-gamepad'/>
|
<i className='fas fa-gamepad'/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
)}
|
||||||
)}
|
|
||||||
<Captcha ref={captchaReference}/>
|
<Captcha ref={captchaReference}/>
|
||||||
|
|
||||||
<Alert type='warning'>{warningMessage}</Alert>
|
<Alert type='warning'>{warningMessage}</Alert>
|
||||||
|
|
@ -161,14 +162,14 @@ export default function Registration() {
|
||||||
<div className='d-flex justify-content-between align-items-center mb-3'>
|
<div className='d-flex justify-content-between align-items-center mb-3'>
|
||||||
<a href={`${blessing.base_url}/auth/login`}>{t('auth.login-link')}</a>
|
<a href={`${blessing.base_url}/auth/login`}>{t('auth.login-link')}</a>
|
||||||
<button className='btn btn-primary' type='submit' disabled={isPending}>
|
<button className='btn btn-primary' type='submit' disabled={isPending}>
|
||||||
{isPending ? (
|
{isPending
|
||||||
<>
|
? (
|
||||||
<i className='fas fa-spinner fa-spin mr-1'/>
|
<>
|
||||||
{t('auth.registering')}
|
<i className='fas fa-spinner fa-spin mr-1'/>
|
||||||
</>
|
{t('auth.registering')}
|
||||||
) : (
|
</>
|
||||||
t('auth.register')
|
)
|
||||||
)}
|
: t('auth.register')}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,10 @@
|
||||||
import {useState} from 'react';
|
import Alert from '@/components/Alert';
|
||||||
import useEmitMounted from '@/scripts/hooks/useEmitMounted';
|
import useEmitMounted from '@/scripts/hooks/useEmitMounted';
|
||||||
import {t} from '@/scripts/i18n';
|
import {t} from '@/scripts/i18n';
|
||||||
import * as fetch from '@/scripts/net';
|
import * as fetch from '@/scripts/net';
|
||||||
import {toast} from '@/scripts/notify';
|
import {toast} from '@/scripts/notify';
|
||||||
import urls from '@/scripts/urls';
|
import urls from '@/scripts/urls';
|
||||||
import Alert from '@/components/Alert';
|
import {useState} from 'react';
|
||||||
|
|
||||||
export default function Reset() {
|
export default function Reset() {
|
||||||
const [password, setPassword] = useState('');
|
const [password, setPassword] = useState('');
|
||||||
|
|
@ -18,9 +18,7 @@ export default function Reset() {
|
||||||
setPassword(event.target.value);
|
setPassword(event.target.value);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleConfirmationChange = (
|
const handleConfirmationChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
event: React.ChangeEvent<HTMLInputElement>,
|
|
||||||
) => {
|
|
||||||
setConfirmation(event.target.value);
|
setConfirmation(event.target.value);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -94,14 +92,14 @@ export default function Reset() {
|
||||||
type='submit'
|
type='submit'
|
||||||
disabled={isPending}
|
disabled={isPending}
|
||||||
>
|
>
|
||||||
{isPending ? (
|
{isPending
|
||||||
<>
|
? (
|
||||||
<i className='fas fa-spinner fa-spin mr-1'/>
|
<>
|
||||||
{t('auth.resetting')}
|
<i className='fas fa-spinner fa-spin mr-1'/>
|
||||||
</>
|
{t('auth.resetting')}
|
||||||
) : (
|
</>
|
||||||
t('auth.reset')
|
)
|
||||||
)}
|
: t('auth.reset')}
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,10 @@
|
||||||
|
import type {Texture} from '@/scripts/types';
|
||||||
import {t} from '@/scripts/i18n';
|
import {t} from '@/scripts/i18n';
|
||||||
import * as fetch from '@/scripts/net';
|
import * as fetch from '@/scripts/net';
|
||||||
import {showModal, toast} from '@/scripts/notify';
|
import {showModal, toast} from '@/scripts/notify';
|
||||||
import type {Texture} from '@/scripts/types';
|
|
||||||
import urls from '@/scripts/urls';
|
import urls from '@/scripts/urls';
|
||||||
|
|
||||||
export default async function addClosetItem(
|
export default async function addClosetItem(texture: Pick<Texture, 'tid' | 'name'>): Promise<boolean> {
|
||||||
texture: Pick<Texture, 'tid' | 'name'>,
|
|
||||||
): Promise<boolean> {
|
|
||||||
let name: string;
|
let name: string;
|
||||||
try {
|
try {
|
||||||
const {value} = await showModal({
|
const {value} = await showModal({
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,5 @@
|
||||||
import React, {useState, useEffect} from 'react';
|
import ButtonEdit from '@/components/ButtonEdit';
|
||||||
import {createPortal} from 'react-dom';
|
import ViewerSkeleton from '@/components/ViewerSkeleton';
|
||||||
import Skeleton from 'react-loading-skeleton';
|
|
||||||
import addClosetItem from './addClosetItem';
|
|
||||||
import useBlessingExtra from '@/scripts/hooks/useBlessingExtra';
|
import useBlessingExtra from '@/scripts/hooks/useBlessingExtra';
|
||||||
import useEmitMounted from '@/scripts/hooks/useEmitMounted';
|
import useEmitMounted from '@/scripts/hooks/useEmitMounted';
|
||||||
import useMount from '@/scripts/hooks/useMount';
|
import useMount from '@/scripts/hooks/useMount';
|
||||||
|
|
@ -10,11 +8,13 @@ import * as fetch from '@/scripts/net';
|
||||||
import {showModal, toast} from '@/scripts/notify';
|
import {showModal, toast} from '@/scripts/notify';
|
||||||
import {type Texture, TextureType} from '@/scripts/types';
|
import {type Texture, TextureType} from '@/scripts/types';
|
||||||
import urls from '@/scripts/urls';
|
import urls from '@/scripts/urls';
|
||||||
import ButtonEdit from '@/components/ButtonEdit';
|
|
||||||
import ViewerSkeleton from '@/components/ViewerSkeleton';
|
|
||||||
import ModalApply from '@/views/user/Closet/ModalApply';
|
import ModalApply from '@/views/user/Closet/ModalApply';
|
||||||
import removeClosetItem from '@/views/user/Closet/removeClosetItem';
|
import removeClosetItem from '@/views/user/Closet/removeClosetItem';
|
||||||
import setAsAvatar from '@/views/user/Closet/setAsAvatar';
|
import setAsAvatar from '@/views/user/Closet/setAsAvatar';
|
||||||
|
import React, {useEffect, useState} from 'react';
|
||||||
|
import {createPortal} from 'react-dom';
|
||||||
|
import Skeleton from 'react-loading-skeleton';
|
||||||
|
import addClosetItem from './addClosetItem';
|
||||||
|
|
||||||
export type Badge = {
|
export type Badge = {
|
||||||
color: string;
|
color: string;
|
||||||
|
|
@ -191,9 +191,7 @@ export default function Show() {
|
||||||
type ErrorResp = {code: 1; message: string};
|
type ErrorResp = {code: 1; message: string};
|
||||||
type DuplicatedResp = {code: 2; message: string; data: {tid: number}};
|
type DuplicatedResp = {code: 2; message: string; data: {tid: number}};
|
||||||
|
|
||||||
const resp = await fetch.put<OkResp | ErrorResp | DuplicatedResp>(
|
const resp = await fetch.put<OkResp | ErrorResp | DuplicatedResp>(urls.texture.privacy(texture.tid));
|
||||||
urls.texture.privacy(texture.tid),
|
|
||||||
);
|
|
||||||
const {code, message} = resp;
|
const {code, message} = resp;
|
||||||
if (code === 0) {
|
if (code === 0) {
|
||||||
toast.success(message);
|
toast.success(message);
|
||||||
|
|
@ -224,9 +222,7 @@ export default function Show() {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const {code, message} = await fetch.del<fetch.ResponseBody>(
|
const {code, message} = await fetch.del<fetch.ResponseBody>(urls.texture.delete(texture.tid));
|
||||||
urls.texture.delete(texture.tid),
|
|
||||||
);
|
|
||||||
if (code === 0) {
|
if (code === 0) {
|
||||||
toast.success(message);
|
toast.success(message);
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
|
|
@ -274,80 +270,84 @@ export default function Show() {
|
||||||
isAlex={texture.type === TextureType.Alex}
|
isAlex={texture.type === TextureType.Alex}
|
||||||
initPositionZ={60}
|
initPositionZ={60}
|
||||||
>
|
>
|
||||||
{currentUid === 0 ? (
|
{currentUid === 0
|
||||||
<button
|
? (
|
||||||
disabled
|
<button
|
||||||
type='button'
|
disabled
|
||||||
className='btn btn-outline-secondary'
|
type='button'
|
||||||
title={t('skinlib.show.anonymous')}
|
className='btn btn-outline-secondary'
|
||||||
>
|
title={t('skinlib.show.anonymous')}
|
||||||
{t('skinlib.addToCloset')}
|
|
||||||
</button>
|
|
||||||
) : (
|
|
||||||
<div className='d-flex justify-content-between align-items-center'>
|
|
||||||
<div>
|
|
||||||
{liked && (
|
|
||||||
<button
|
|
||||||
type='button'
|
|
||||||
className='btn btn-outline-success mr-2'
|
|
||||||
onClick={handleOpenModalApply}
|
|
||||||
>
|
|
||||||
{t('skinlib.apply')}
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
{liked ? (
|
|
||||||
<button
|
|
||||||
type='button'
|
|
||||||
className='btn btn-outline-primary mr-2'
|
|
||||||
onClick={handleRemoveItemClick}
|
|
||||||
>
|
|
||||||
{t('skinlib.removeFromCloset')}
|
|
||||||
</button>
|
|
||||||
) : (
|
|
||||||
<button
|
|
||||||
type='button'
|
|
||||||
className='btn btn-outline-primary mr-2'
|
|
||||||
onClick={handleAddItemClick}
|
|
||||||
>
|
|
||||||
{t('skinlib.addToCloset')}
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
{texture.type !== TextureType.Cape && (
|
|
||||||
<button
|
|
||||||
type='button'
|
|
||||||
className='btn btn-outline-info mr-2'
|
|
||||||
onClick={handleSetAsAvatar}
|
|
||||||
>
|
|
||||||
{t('user.setAsAvatar')}
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
{canBeDownloaded && (
|
|
||||||
<a
|
|
||||||
role='button'
|
|
||||||
className='btn btn-outline-info mr-2'
|
|
||||||
href={`${blessing.base_url}/raw/${texture.tid}`}
|
|
||||||
download={`${texture.name}.png`}
|
|
||||||
>
|
|
||||||
{t('skinlib.show.download')}
|
|
||||||
</a>
|
|
||||||
)}
|
|
||||||
<button
|
|
||||||
type='button'
|
|
||||||
className='btn btn-outline-info mr-2'
|
|
||||||
onClick={handleReport}
|
|
||||||
>
|
|
||||||
{t('skinlib.report.title')}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
className={liked ? 'text-red' : 'text-gray'}
|
|
||||||
title={t('skinlib.show.likes')}
|
|
||||||
>
|
>
|
||||||
<i className='fas fa-heart mr-1'/>
|
{t('skinlib.addToCloset')}
|
||||||
<span>{texture.likes}</span>
|
</button>
|
||||||
|
)
|
||||||
|
: (
|
||||||
|
<div className='d-flex justify-content-between align-items-center'>
|
||||||
|
<div>
|
||||||
|
{liked && (
|
||||||
|
<button
|
||||||
|
type='button'
|
||||||
|
className='btn btn-outline-success mr-2'
|
||||||
|
onClick={handleOpenModalApply}
|
||||||
|
>
|
||||||
|
{t('skinlib.apply')}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
{liked
|
||||||
|
? (
|
||||||
|
<button
|
||||||
|
type='button'
|
||||||
|
className='btn btn-outline-primary mr-2'
|
||||||
|
onClick={handleRemoveItemClick}
|
||||||
|
>
|
||||||
|
{t('skinlib.removeFromCloset')}
|
||||||
|
</button>
|
||||||
|
)
|
||||||
|
: (
|
||||||
|
<button
|
||||||
|
type='button'
|
||||||
|
className='btn btn-outline-primary mr-2'
|
||||||
|
onClick={handleAddItemClick}
|
||||||
|
>
|
||||||
|
{t('skinlib.addToCloset')}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
{texture.type !== TextureType.Cape && (
|
||||||
|
<button
|
||||||
|
type='button'
|
||||||
|
className='btn btn-outline-info mr-2'
|
||||||
|
onClick={handleSetAsAvatar}
|
||||||
|
>
|
||||||
|
{t('user.setAsAvatar')}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
{canBeDownloaded && (
|
||||||
|
<a
|
||||||
|
role='button'
|
||||||
|
className='btn btn-outline-info mr-2'
|
||||||
|
href={`${blessing.base_url}/raw/${texture.tid}`}
|
||||||
|
download={`${texture.name}.png`}
|
||||||
|
>
|
||||||
|
{t('skinlib.show.download')}
|
||||||
|
</a>
|
||||||
|
)}
|
||||||
|
<button
|
||||||
|
type='button'
|
||||||
|
className='btn btn-outline-info mr-2'
|
||||||
|
onClick={handleReport}
|
||||||
|
>
|
||||||
|
{t('skinlib.report.title')}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className={liked ? 'text-red' : 'text-gray'}
|
||||||
|
title={t('skinlib.show.likes')}
|
||||||
|
>
|
||||||
|
<i className='fas fa-heart mr-1'/>
|
||||||
|
<span>{texture.likes}</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
)}
|
||||||
)}
|
|
||||||
</Previewer>
|
</Previewer>
|
||||||
</React.Suspense>,
|
</React.Suspense>,
|
||||||
container,
|
container,
|
||||||
|
|
@ -360,49 +360,53 @@ export default function Show() {
|
||||||
<div className='container'>
|
<div className='container'>
|
||||||
<div className='row mt-2 mb-4'>
|
<div className='row mt-2 mb-4'>
|
||||||
<div className='col-4'>{t('skinlib.show.name')}</div>
|
<div className='col-4'>{t('skinlib.show.name')}</div>
|
||||||
{isLoading ? (
|
{isLoading
|
||||||
<div className='col-8'>
|
? (
|
||||||
<Skeleton/>
|
<div className='col-8'>
|
||||||
</div>
|
<Skeleton/>
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<div className='col-7 text-truncate' title={texture.name}>
|
|
||||||
{texture.name}
|
|
||||||
</div>
|
</div>
|
||||||
{canEdit && (
|
)
|
||||||
<div className='col-1'>
|
: (
|
||||||
<ButtonEdit
|
<>
|
||||||
title={t('skinlib.show.edit')}
|
<div className='col-7 text-truncate' title={texture.name}>
|
||||||
onClick={handleEditName}
|
{texture.name}
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
{canEdit && (
|
||||||
</>
|
<div className='col-1'>
|
||||||
)}
|
<ButtonEdit
|
||||||
|
title={t('skinlib.show.edit')}
|
||||||
|
onClick={handleEditName}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className='row my-4'>
|
<div className='row my-4'>
|
||||||
<div className='col-4'>{t('skinlib.show.model')}</div>
|
<div className='col-4'>{t('skinlib.show.model')}</div>
|
||||||
{isLoading ? (
|
{isLoading
|
||||||
<div className='col-8'>
|
? (
|
||||||
<Skeleton/>
|
<div className='col-8'>
|
||||||
</div>
|
<Skeleton/>
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<div className='col-7'>
|
|
||||||
{texture.type === TextureType.Cape
|
|
||||||
? t('general.cape')
|
|
||||||
: texture.type}
|
|
||||||
</div>
|
</div>
|
||||||
{canEdit && (
|
)
|
||||||
<div className='col-1'>
|
: (
|
||||||
<ButtonEdit
|
<>
|
||||||
title={t('skinlib.show.edit')}
|
<div className='col-7'>
|
||||||
onClick={handleSwitchType}
|
{texture.type === TextureType.Cape
|
||||||
/>
|
? t('general.cape')
|
||||||
|
: texture.type}
|
||||||
</div>
|
</div>
|
||||||
)}
|
{canEdit && (
|
||||||
</>
|
<div className='col-1'>
|
||||||
)}
|
<ButtonEdit
|
||||||
|
title={t('skinlib.show.edit')}
|
||||||
|
onClick={handleSwitchType}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className='row my-4'>
|
<div className='row my-4'>
|
||||||
<div className='col-4'>Hash</div>
|
<div className='col-4'>Hash</div>
|
||||||
|
|
@ -416,35 +420,41 @@ export default function Show() {
|
||||||
<div className='row my-4'>
|
<div className='row my-4'>
|
||||||
<div className='col-4'>{t('skinlib.show.size')}</div>
|
<div className='col-4'>{t('skinlib.show.size')}</div>
|
||||||
<div className='col-8'>
|
<div className='col-8'>
|
||||||
{isLoading ? <Skeleton/> : <span>{texture.size} KB</span>}
|
{isLoading
|
||||||
|
? <Skeleton/>
|
||||||
|
: <span>
|
||||||
|
{texture.size}
|
||||||
|
{' '}
|
||||||
|
KB
|
||||||
|
</span>}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className='row my-4'>
|
<div className='row my-4'>
|
||||||
<div className='col-4'>{t('skinlib.show.uploader')}</div>
|
<div className='col-4'>{t('skinlib.show.uploader')}</div>
|
||||||
<div className='col-8 text-truncate'>
|
<div className='col-8 text-truncate'>
|
||||||
{isLoading ? (
|
{isLoading
|
||||||
<Skeleton/>
|
? <Skeleton/>
|
||||||
) : (isUploaderExists ? (
|
: isUploaderExists
|
||||||
<>
|
? (
|
||||||
<div>
|
<>
|
||||||
<a href={linkToUploader} target='_blank' rel='noreferrer'>
|
<div>
|
||||||
{nickname}
|
<a href={linkToUploader} target='_blank' rel='noreferrer'>
|
||||||
</a>
|
{nickname}
|
||||||
</div>
|
</a>
|
||||||
<div>
|
</div>
|
||||||
{badges.map(badge => (
|
<div>
|
||||||
<span
|
{badges.map(badge => (
|
||||||
key={badge.text}
|
<span
|
||||||
className={`badge bg-${badge.color} mr-2`}
|
key={badge.text}
|
||||||
>
|
className={`badge bg-${badge.color} mr-2`}
|
||||||
{badge.text}
|
>
|
||||||
</span>
|
{badge.text}
|
||||||
))}
|
</span>
|
||||||
</div>
|
))}
|
||||||
</>
|
</div>
|
||||||
) : (
|
</>
|
||||||
nickname
|
)
|
||||||
))}
|
: nickname}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className='row mt-4 mb-2'>
|
<div className='row mt-4 mb-2'>
|
||||||
|
|
|
||||||
|
|
@ -5,8 +5,8 @@ type Properties = {
|
||||||
};
|
};
|
||||||
|
|
||||||
type Attributes = React.DetailedHTMLProps<
|
type Attributes = React.DetailedHTMLProps<
|
||||||
React.ButtonHTMLAttributes<HTMLButtonElement>,
|
React.ButtonHTMLAttributes<HTMLButtonElement>,
|
||||||
HTMLButtonElement
|
HTMLButtonElement
|
||||||
>;
|
>;
|
||||||
|
|
||||||
const Button: React.FC<Properties & Attributes> = properties => {
|
const Button: React.FC<Properties & Attributes> = properties => {
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,13 @@
|
||||||
|
|
||||||
import Button from './Button';
|
|
||||||
import type {Filter} from './types';
|
import type {Filter} from './types';
|
||||||
import {humanizeType} from './utils';
|
|
||||||
import {TextureType} from '@/scripts/types';
|
|
||||||
import {t} from '@/scripts/i18n';
|
import {t} from '@/scripts/i18n';
|
||||||
|
import {TextureType} from '@/scripts/types';
|
||||||
|
import Button from './Button';
|
||||||
|
import {humanizeType} from './utils';
|
||||||
|
|
||||||
type Properties = {
|
type Properties = {
|
||||||
readonly filter: Filter;
|
readonly filter: Filter;
|
||||||
onChange(filter: Filter): void;
|
onChange: (filter: Filter) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
const FilterSelector: React.FC<Properties> = properties => {
|
const FilterSelector: React.FC<Properties> = properties => {
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,9 @@
|
||||||
|
|
||||||
import styled from '@emotion/styled';
|
|
||||||
import type {LibraryItem} from './types';
|
import type {LibraryItem} from './types';
|
||||||
import {humanizeType} from './utils';
|
|
||||||
import {t} from '@/scripts/i18n';
|
import {t} from '@/scripts/i18n';
|
||||||
import * as cssUtils from '@/styles/utils';
|
import * as cssUtils from '@/styles/utils';
|
||||||
|
import styled from '@emotion/styled';
|
||||||
|
import {humanizeType} from './utils';
|
||||||
|
|
||||||
const Card = styled.div`
|
const Card = styled.div`
|
||||||
width: 245px;
|
width: 245px;
|
||||||
|
|
@ -42,9 +42,9 @@ const ButtonLike = styled.a<ButtonLikeProperties>`
|
||||||
${cssUtils.pointerCursor}
|
${cssUtils.pointerCursor}
|
||||||
|
|
||||||
i, span {
|
i, span {
|
||||||
color: ${properties => (properties.liked ? '#dc3545' : '#6c757d')};
|
color: ${properties => properties.liked ? '#dc3545' : '#6c757d'};
|
||||||
&:hover {
|
&:hover {
|
||||||
color: ${properties => (properties.liked ? '#dc3545' : '#343a40')};
|
color: ${properties => properties.liked ? '#dc3545' : '#343a40'};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
@ -52,9 +52,9 @@ const ButtonLike = styled.a<ButtonLikeProperties>`
|
||||||
type Properties = {
|
type Properties = {
|
||||||
readonly item: LibraryItem;
|
readonly item: LibraryItem;
|
||||||
readonly liked: boolean;
|
readonly liked: boolean;
|
||||||
onAdd(texture: LibraryItem): Promise<void>;
|
onAdd: (texture: LibraryItem) => Promise<void>;
|
||||||
onRemove(texture: LibraryItem): Promise<void>;
|
onRemove: (texture: LibraryItem) => Promise<void>;
|
||||||
onUploaderClick(uploader: number): void;
|
onUploaderClick: (uploader: number) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
const Item: React.FC<Properties> = properties => {
|
const Item: React.FC<Properties> = properties => {
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,6 @@
|
||||||
import {useState, useEffect} from 'react';
|
|
||||||
import addClosetItem from '../Show/addClosetItem';
|
|
||||||
import FilterSelector from './FilterSelector';
|
|
||||||
import Button from './Button';
|
|
||||||
import Item from './Item';
|
|
||||||
import type {Filter, LibraryItem} from './types';
|
import type {Filter, LibraryItem} from './types';
|
||||||
|
import Loading from '@/components/Loading';
|
||||||
|
import Pagination from '@/components/Pagination';
|
||||||
import useBlessingExtra from '@/scripts/hooks/useBlessingExtra';
|
import useBlessingExtra from '@/scripts/hooks/useBlessingExtra';
|
||||||
import useEmitMounted from '@/scripts/hooks/useEmitMounted';
|
import useEmitMounted from '@/scripts/hooks/useEmitMounted';
|
||||||
import {t} from '@/scripts/i18n';
|
import {t} from '@/scripts/i18n';
|
||||||
|
|
@ -11,9 +8,12 @@ import * as fetch from '@/scripts/net';
|
||||||
import {toast} from '@/scripts/notify';
|
import {toast} from '@/scripts/notify';
|
||||||
import {type Paginator, TextureType} from '@/scripts/types';
|
import {type Paginator, TextureType} from '@/scripts/types';
|
||||||
import urls from '@/scripts/urls';
|
import urls from '@/scripts/urls';
|
||||||
import Loading from '@/components/Loading';
|
|
||||||
import Pagination from '@/components/Pagination';
|
|
||||||
import removeClosetItem from '@/views/user/Closet/removeClosetItem';
|
import removeClosetItem from '@/views/user/Closet/removeClosetItem';
|
||||||
|
import {useEffect, useState} from 'react';
|
||||||
|
import addClosetItem from '../Show/addClosetItem';
|
||||||
|
import Button from './Button';
|
||||||
|
import FilterSelector from './FilterSelector';
|
||||||
|
import Item from './Item';
|
||||||
|
|
||||||
function SkinLibrary() {
|
function SkinLibrary() {
|
||||||
const [isLoading, setIsLoading] = useState(true);
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
|
|
@ -35,16 +35,14 @@ function SkinLibrary() {
|
||||||
const search = new URLSearchParams(query);
|
const search = new URLSearchParams(query);
|
||||||
|
|
||||||
const filter = search.get('filter') ?? '';
|
const filter = search.get('filter') ?? '';
|
||||||
setFilter(
|
setFilter([
|
||||||
[
|
'skin',
|
||||||
'skin',
|
TextureType.Steve,
|
||||||
TextureType.Steve,
|
TextureType.Alex,
|
||||||
TextureType.Alex,
|
TextureType.Cape,
|
||||||
TextureType.Cape,
|
].includes(filter)
|
||||||
].includes(filter)
|
? (filter as Filter)
|
||||||
? (filter as Filter)
|
: 'skin');
|
||||||
: 'skin',
|
|
||||||
);
|
|
||||||
|
|
||||||
const keyword = decodeURIComponent(search.get('keyword') ?? '');
|
const keyword = decodeURIComponent(search.get('keyword') ?? '');
|
||||||
setName(keyword);
|
setName(keyword);
|
||||||
|
|
@ -187,17 +185,19 @@ function SkinLibrary() {
|
||||||
<div className='container-fluid d-flex justify-content-between'>
|
<div className='container-fluid d-flex justify-content-between'>
|
||||||
<h1>{t('general.skinlib')}</h1>
|
<h1>{t('general.skinlib')}</h1>
|
||||||
<span>
|
<span>
|
||||||
{uploader ? (
|
{uploader
|
||||||
<>
|
? (
|
||||||
<i className='fas fa-user mr-1'/>
|
<>
|
||||||
{t('skinlib.filter.uploader', {uid: uploader.toString()})}
|
<i className='fas fa-user mr-1'/>
|
||||||
</>
|
{t('skinlib.filter.uploader', {uid: uploader.toString()})}
|
||||||
) : (
|
</>
|
||||||
<>
|
)
|
||||||
<i className='fas fa-user-friends mr-1'/>
|
: (
|
||||||
{t('skinlib.filter.allUsers')}
|
<>
|
||||||
</>
|
<i className='fas fa-user-friends mr-1'/>
|
||||||
)}
|
{t('skinlib.filter.allUsers')}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -263,22 +263,22 @@ function SkinLibrary() {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{items.length > 0 ? (
|
{items.length > 0
|
||||||
<div className='d-flex flex-wrap'>
|
? (
|
||||||
{items.map((item, i) => (
|
<div className='d-flex flex-wrap'>
|
||||||
<Item
|
{items.map((item, i) => (
|
||||||
key={item.tid}
|
<Item
|
||||||
item={item}
|
key={item.tid}
|
||||||
liked={closet.includes(item.tid)}
|
item={item}
|
||||||
onAdd={async item => handleAddToCloset(item, i)}
|
liked={closet.includes(item.tid)}
|
||||||
onRemove={async item => handleRemoveFromCloset(item, i)}
|
onAdd={async item => handleAddToCloset(item, i)}
|
||||||
onUploaderClick={handleUploaderClick}
|
onRemove={async item => handleRemoveFromCloset(item, i)}
|
||||||
/>
|
onUploaderClick={handleUploaderClick}
|
||||||
))}
|
/>
|
||||||
</div>
|
))}
|
||||||
) : (
|
</div>
|
||||||
<p className='text-center m-5'>{t('general.noResult')}</p>
|
)
|
||||||
)}
|
: <p className='text-center m-5'>{t('general.noResult')}</p>}
|
||||||
</div>
|
</div>
|
||||||
<div className='card-footer'>
|
<div className='card-footer'>
|
||||||
<div className='d-flex justify-content-center'>
|
<div className='d-flex justify-content-center'>
|
||||||
|
|
|
||||||
|
|
@ -1,16 +1,16 @@
|
||||||
import React, {useState} from 'react';
|
import FileInput from '@/components/FileInput';
|
||||||
import ReactDOM from 'react-dom';
|
import ViewerSkeleton from '@/components/ViewerSkeleton';
|
||||||
import {t} from '@/scripts/i18n';
|
|
||||||
import useBlessingExtra from '@/scripts/hooks/useBlessingExtra';
|
import useBlessingExtra from '@/scripts/hooks/useBlessingExtra';
|
||||||
import useEmitMounted from '@/scripts/hooks/useEmitMounted';
|
import useEmitMounted from '@/scripts/hooks/useEmitMounted';
|
||||||
import useMount from '@/scripts/hooks/useMount';
|
import useMount from '@/scripts/hooks/useMount';
|
||||||
|
import {t} from '@/scripts/i18n';
|
||||||
import * as fetch from '@/scripts/net';
|
import * as fetch from '@/scripts/net';
|
||||||
import {showModal, toast} from '@/scripts/notify';
|
import {showModal, toast} from '@/scripts/notify';
|
||||||
import {isAlex} from '@/scripts/textureUtils';
|
import {isAlex} from '@/scripts/textureUtils';
|
||||||
import {TextureType} from '@/scripts/types';
|
import {TextureType} from '@/scripts/types';
|
||||||
import urls from '@/scripts/urls';
|
import urls from '@/scripts/urls';
|
||||||
import FileInput from '@/components/FileInput';
|
import React, {useState} from 'react';
|
||||||
import ViewerSkeleton from '@/components/ViewerSkeleton';
|
import ReactDOM from 'react-dom';
|
||||||
|
|
||||||
const Previewer = React.lazy(async () => import('@/components/Viewer'));
|
const Previewer = React.lazy(async () => import('@/components/Viewer'));
|
||||||
|
|
||||||
|
|
@ -46,9 +46,7 @@ function Upload() {
|
||||||
setIsPrivate(event.target.checked);
|
setIsPrivate(event.target.checked);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleFileChange = async (
|
const handleFileChange = async (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
event: React.ChangeEvent<HTMLInputElement>,
|
|
||||||
) => {
|
|
||||||
const files = event.target.files!;
|
const files = event.target.files!;
|
||||||
const [file] = files;
|
const [file] = files;
|
||||||
if (file) {
|
if (file) {
|
||||||
|
|
@ -60,7 +58,7 @@ function Upload() {
|
||||||
const texture = URL.createObjectURL(file);
|
const texture = URL.createObjectURL(file);
|
||||||
setTexture(texture);
|
setTexture(texture);
|
||||||
if (type !== TextureType.Cape) {
|
if (type !== TextureType.Cape) {
|
||||||
setType((await isAlex(texture)) ? TextureType.Alex : TextureType.Steve);
|
setType(await isAlex(texture) ? TextureType.Alex : TextureType.Steve);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
@ -208,14 +206,14 @@ function Upload() {
|
||||||
disabled={isUploading}
|
disabled={isUploading}
|
||||||
onClick={handleUpload}
|
onClick={handleUpload}
|
||||||
>
|
>
|
||||||
{isUploading ? (
|
{isUploading
|
||||||
<>
|
? (
|
||||||
<i className='fas fa-spinner fa-spin mr-1'/>
|
<>
|
||||||
<span>{t('skinlib.uploading')}</span>
|
<i className='fas fa-spinner fa-spin mr-1'/>
|
||||||
</>
|
<span>{t('skinlib.uploading')}</span>
|
||||||
) : (
|
</>
|
||||||
t('skinlib.upload.button')
|
)
|
||||||
)}
|
: t('skinlib.upload.button')}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
{file && (
|
{file && (
|
||||||
|
|
@ -231,9 +229,8 @@ function Upload() {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{isPrivate && (
|
{isPrivate
|
||||||
<div className='callout callout-info mt-3'>{privacyNotice}</div>
|
&& <div className='callout callout-info mt-3'>{privacyNotice}</div>}
|
||||||
)}
|
|
||||||
{!isPrivate && award > 0 && (
|
{!isPrivate && award > 0 && (
|
||||||
<div className='callout callout-success mt-3'>
|
<div className='callout callout-success mt-3'>
|
||||||
{t('skinlib.upload.award', {score: award.toString()})}
|
{t('skinlib.upload.award', {score: award.toString()})}
|
||||||
|
|
@ -242,16 +239,16 @@ function Upload() {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{container
|
{container
|
||||||
&& ReactDOM.createPortal(
|
&& ReactDOM.createPortal(
|
||||||
<React.Suspense fallback={<ViewerSkeleton/>}>
|
<React.Suspense fallback={<ViewerSkeleton/>}>
|
||||||
<Previewer
|
<Previewer
|
||||||
skin={type === TextureType.Cape ? undefined : texture}
|
skin={type === TextureType.Cape ? undefined : texture}
|
||||||
cape={type === TextureType.Cape ? texture : undefined}
|
cape={type === TextureType.Cape ? texture : undefined}
|
||||||
isAlex={type === TextureType.Alex}
|
isAlex={type === TextureType.Alex}
|
||||||
/>
|
/>
|
||||||
</React.Suspense>,
|
</React.Suspense>,
|
||||||
container,
|
container,
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,15 +1,15 @@
|
||||||
|
|
||||||
|
import type {ClosetItem as ClosetItemType} from '@/scripts/types';
|
||||||
|
import {t} from '@/scripts/i18n';
|
||||||
import setAsAvatar from './setAsAvatar';
|
import setAsAvatar from './setAsAvatar';
|
||||||
import {Card, DropdownButton} from './styles';
|
import {Card, DropdownButton} from './styles';
|
||||||
import {t} from '@/scripts/i18n';
|
|
||||||
import type {ClosetItem as ClosetItemType} from '@/scripts/types';
|
|
||||||
|
|
||||||
type Properties = {
|
type Properties = {
|
||||||
readonly item: ClosetItemType;
|
readonly item: ClosetItemType;
|
||||||
readonly selected: boolean;
|
readonly selected: boolean;
|
||||||
onClick(item: ClosetItemType): void;
|
onClick: (item: ClosetItemType) => void;
|
||||||
onRename(): void;
|
onRename: () => void;
|
||||||
onRemove(): void;
|
onRemove: () => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
const ClosetItem: React.FC<Properties> = properties => {
|
const ClosetItem: React.FC<Properties> = properties => {
|
||||||
|
|
@ -58,7 +58,8 @@ const ClosetItem: React.FC<Properties> = properties => {
|
||||||
<a
|
<a
|
||||||
href={`${blessing.base_url}/skinlib/show/${item.tid}`}
|
href={`${blessing.base_url}/skinlib/show/${item.tid}`}
|
||||||
className='dropdown-item'
|
className='dropdown-item'
|
||||||
target='_blank' rel='noreferrer'
|
target='_blank'
|
||||||
|
rel='noreferrer'
|
||||||
>
|
>
|
||||||
{t('user.viewInSkinlib')}
|
{t('user.viewInSkinlib')}
|
||||||
</a>
|
</a>
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,12 @@
|
||||||
import {useState, useEffect} from 'react';
|
import type {Player} from '@/scripts/types';
|
||||||
import $ from 'jquery';
|
import Loading from '@/components/Loading';
|
||||||
|
import Modal from '@/components/Modal';
|
||||||
import {t} from '@/scripts/i18n';
|
import {t} from '@/scripts/i18n';
|
||||||
import * as fetch from '@/scripts/net';
|
import * as fetch from '@/scripts/net';
|
||||||
import {toast} from '@/scripts/notify';
|
import {toast} from '@/scripts/notify';
|
||||||
import type {Player} from '@/scripts/types';
|
|
||||||
import urls from '@/scripts/urls';
|
import urls from '@/scripts/urls';
|
||||||
import Loading from '@/components/Loading';
|
import $ from 'jquery';
|
||||||
import Modal from '@/components/Modal';
|
import {useEffect, useState} from 'react';
|
||||||
|
|
||||||
const baseUrl = blessing.base_url;
|
const baseUrl = blessing.base_url;
|
||||||
|
|
||||||
|
|
@ -15,7 +15,7 @@ type Properties = {
|
||||||
readonly canAdd: boolean;
|
readonly canAdd: boolean;
|
||||||
readonly skin?: number;
|
readonly skin?: number;
|
||||||
readonly cape?: number;
|
readonly cape?: number;
|
||||||
onClose(): void;
|
onClose: () => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
const ModalApply: React.FC<Properties> = properties => {
|
const ModalApply: React.FC<Properties> = properties => {
|
||||||
|
|
@ -67,46 +67,46 @@ const ModalApply: React.FC<Properties> = properties => {
|
||||||
footer={<></>}
|
footer={<></>}
|
||||||
onClose={properties.onClose}
|
onClose={properties.onClose}
|
||||||
>
|
>
|
||||||
{isLoading ? (
|
{isLoading
|
||||||
<Loading/>
|
? <Loading/>
|
||||||
) : (players.length === 0 ? (
|
: players.length === 0
|
||||||
<p>{t('user.closet.use-as.empty')}</p>
|
? <p>{t('user.closet.use-as.empty')}</p>
|
||||||
) : (
|
: (
|
||||||
<>
|
<>
|
||||||
<div className='form-group'>
|
<div className='form-group'>
|
||||||
<input
|
<input
|
||||||
type='text'
|
type='text'
|
||||||
className='form-control'
|
className='form-control'
|
||||||
placeholder={t('user.typeToSearch')}
|
placeholder={t('user.typeToSearch')}
|
||||||
onChange={handleSearch}
|
onChange={handleSearch}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{players
|
{players
|
||||||
.filter(player => player.name.includes(search))
|
.filter(player => player.name.includes(search))
|
||||||
.map(player => (
|
.map(player => (
|
||||||
<button
|
<button
|
||||||
key={player.pid}
|
key={player.pid}
|
||||||
className='btn btn-block btn-outline-info text-left'
|
className='btn btn-block btn-outline-info text-left'
|
||||||
title={player.name}
|
title={player.name}
|
||||||
onClick={async () => handleSelect(player)}
|
onClick={async () => handleSelect(player)}
|
||||||
>
|
>
|
||||||
<picture>
|
<picture>
|
||||||
<source
|
<source
|
||||||
srcSet={`${baseUrl}/avatar/${player.tid_skin}?3d&size=45`}
|
srcSet={`${baseUrl}/avatar/${player.tid_skin}?3d&size=45`}
|
||||||
type='image/webp'
|
type='image/webp'
|
||||||
/>
|
/>
|
||||||
<img
|
<img
|
||||||
src={`${baseUrl}/avatar/${player.tid_skin}?3d&png&size=45`}
|
src={`${baseUrl}/avatar/${player.tid_skin}?3d&png&size=45`}
|
||||||
alt={player.name}
|
alt={player.name}
|
||||||
width={45}
|
width={45}
|
||||||
height={45}
|
height={45}
|
||||||
/>
|
/>
|
||||||
</picture>
|
</picture>
|
||||||
<span className='ml-1'>{player.name}</span>
|
<span className='ml-1'>{player.name}</span>
|
||||||
</button>
|
</button>
|
||||||
))}
|
))}
|
||||||
</>
|
</>
|
||||||
))}
|
)}
|
||||||
</Modal>
|
</Modal>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
|
import ViewerSkeleton from '@/components/ViewerSkeleton';
|
||||||
|
import useMount from '@/scripts/hooks/useMount';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import ReactDOM from 'react-dom';
|
import ReactDOM from 'react-dom';
|
||||||
import useMount from '@/scripts/hooks/useMount';
|
|
||||||
import ViewerSkeleton from '@/components/ViewerSkeleton';
|
|
||||||
|
|
||||||
const Viewer = React.lazy(async () => import('@/components/Viewer'));
|
const Viewer = React.lazy(async () => import('@/components/Viewer'));
|
||||||
|
|
||||||
|
|
@ -20,14 +20,14 @@ const Previewer: React.FC<Properties> = properties => {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
container
|
container
|
||||||
&& ReactDOM.createPortal(
|
&& ReactDOM.createPortal(
|
||||||
<React.Suspense fallback={<ViewerSkeleton/>}>
|
<React.Suspense fallback={<ViewerSkeleton/>}>
|
||||||
<Viewer showIndicator skin={skin} cape={cape} isAlex={properties.isAlex}>
|
<Viewer showIndicator skin={skin} cape={cape} isAlex={properties.isAlex}>
|
||||||
{properties.children}
|
{properties.children}
|
||||||
</Viewer>
|
</Viewer>
|
||||||
</React.Suspense>,
|
</React.Suspense>,
|
||||||
container,
|
container,
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,22 +1,22 @@
|
||||||
import {useState, useEffect, useRef} from 'react';
|
import Pagination from '@/components/Pagination';
|
||||||
import debounce from 'lodash-es/debounce';
|
|
||||||
import ClosetItem from './ClosetItem';
|
|
||||||
import LoadingClosetItem from './LoadingClosetItem';
|
|
||||||
import Previewer from './Previewer';
|
|
||||||
import ModalApply from './ModalApply';
|
|
||||||
import removeClosetItem from './removeClosetItem';
|
|
||||||
import useEmitMounted from '@/scripts/hooks/useEmitMounted';
|
import useEmitMounted from '@/scripts/hooks/useEmitMounted';
|
||||||
import {t} from '@/scripts/i18n';
|
import {t} from '@/scripts/i18n';
|
||||||
import * as fetch from '@/scripts/net';
|
import * as fetch from '@/scripts/net';
|
||||||
import {showModal, toast} from '@/scripts/notify';
|
import {showModal, toast} from '@/scripts/notify';
|
||||||
import {
|
import {
|
||||||
type ClosetItem as Item,
|
type ClosetItem as Item,
|
||||||
type Texture,
|
|
||||||
type Paginator,
|
type Paginator,
|
||||||
|
type Texture,
|
||||||
TextureType,
|
TextureType,
|
||||||
} from '@/scripts/types';
|
} from '@/scripts/types';
|
||||||
import urls from '@/scripts/urls';
|
import urls from '@/scripts/urls';
|
||||||
import Pagination from '@/components/Pagination';
|
import debounce from 'lodash-es/debounce';
|
||||||
|
import {useEffect, useRef, useState} from 'react';
|
||||||
|
import ClosetItem from './ClosetItem';
|
||||||
|
import LoadingClosetItem from './LoadingClosetItem';
|
||||||
|
import ModalApply from './ModalApply';
|
||||||
|
import Previewer from './Previewer';
|
||||||
|
import removeClosetItem from './removeClosetItem';
|
||||||
|
|
||||||
type Category = 'skin' | 'cape';
|
type Category = 'skin' | 'cape';
|
||||||
|
|
||||||
|
|
@ -39,10 +39,10 @@ function Closet() {
|
||||||
const [page, setPage] = useState(1);
|
const [page, setPage] = useState(1);
|
||||||
const [totalPages, setTotalPages] = useState(1);
|
const [totalPages, setTotalPages] = useState(1);
|
||||||
const [items, setItems] = useState<Item[]>([]);
|
const [items, setItems] = useState<Item[]>([]);
|
||||||
const [skin, setSkin] = useState<Texture | null>(null);
|
const [skin, setSkin] = useState<Texture | undefined>(null);
|
||||||
const [cape, setCape] = useState<Texture | null>(null);
|
const [cape, setCape] = useState<Texture | undefined>(null);
|
||||||
const [showModalApply, setShowModalApply] = useState(false);
|
const [showModalApply, setShowModalApply] = useState(false);
|
||||||
const containerReference = useRef<HTMLDivElement | null>(null);
|
const containerReference = useRef<HTMLDivElement | undefined>(null);
|
||||||
const perPageReference = useRef(6);
|
const perPageReference = useRef(6);
|
||||||
|
|
||||||
useEmitMounted();
|
useEmitMounted();
|
||||||
|
|
@ -64,7 +64,10 @@ function Closet() {
|
||||||
const {data, last_page: lastPage} = await fetch.get<Paginator<Item>>(
|
const {data, last_page: lastPage} = await fetch.get<Paginator<Item>>(
|
||||||
urls.user.closet.list(),
|
urls.user.closet.list(),
|
||||||
{
|
{
|
||||||
category, q: query, page: page.toString(), perPage: perPageReference.current.toString(),
|
category,
|
||||||
|
q: query,
|
||||||
|
page: page.toString(),
|
||||||
|
perPage: perPageReference.current.toString(),
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -223,40 +226,43 @@ function Closet() {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className='card-body'>
|
<div className='card-body'>
|
||||||
{isLoading ? (
|
{isLoading
|
||||||
<div className='d-flex flex-wrap'>
|
? (
|
||||||
{new Array(perPageReference.current).fill(null).map((_, i) => (
|
<div className='d-flex flex-wrap'>
|
||||||
<LoadingClosetItem key={i}/>
|
{new Array(perPageReference.current).fill(null).map((_, i) =>
|
||||||
))}
|
<LoadingClosetItem key={i}/>)}
|
||||||
</div>
|
</div>
|
||||||
) : (items.length === 0 ? (
|
)
|
||||||
<div className='text-center p-3'>
|
: items.length === 0
|
||||||
{search ? (
|
? (
|
||||||
t('general.noResult')
|
<div className='text-center p-3'>
|
||||||
) : (
|
{search
|
||||||
<span
|
? t('general.noResult')
|
||||||
dangerouslySetInnerHTML={{
|
: (
|
||||||
__html: t('user.emptyClosetMsg', {
|
<span
|
||||||
url: `${blessing.base_url}/skinlib?filter=${category}`,
|
dangerouslySetInnerHTML={{
|
||||||
}),
|
__html: t('user.emptyClosetMsg', {
|
||||||
}}
|
url: `${blessing.base_url}/skinlib?filter=${category}`,
|
||||||
/>
|
}),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
: (
|
||||||
|
<div className='d-flex flex-wrap'>
|
||||||
|
{items.map((item, i) => (
|
||||||
|
<ClosetItem
|
||||||
|
key={item.tid}
|
||||||
|
item={item}
|
||||||
|
selected={isSelected(item)}
|
||||||
|
onClick={handleSelect}
|
||||||
|
onRename={async () => renameItem(item, i)}
|
||||||
|
onRemove={async () => removeItem(item)}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div className='d-flex flex-wrap'>
|
|
||||||
{items.map((item, i) => (
|
|
||||||
<ClosetItem
|
|
||||||
key={item.tid}
|
|
||||||
item={item}
|
|
||||||
selected={isSelected(item)}
|
|
||||||
onClick={handleSelect}
|
|
||||||
onRename={async () => renameItem(item, i)}
|
|
||||||
onRemove={async () => removeItem(item)}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
</div>
|
||||||
<div className='card-footer'>
|
<div className='card-footer'>
|
||||||
<div className='float-right'>
|
<div className='float-right'>
|
||||||
|
|
|
||||||
|
|
@ -13,9 +13,7 @@ export default async function removeClosetItem(tid: number): Promise<boolean> {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
const {code, message} = await fetch.del<fetch.ResponseBody>(
|
const {code, message} = await fetch.del<fetch.ResponseBody>(urls.user.closet.remove(tid));
|
||||||
urls.user.closet.remove(tid),
|
|
||||||
);
|
|
||||||
if (code === 0) {
|
if (code === 0) {
|
||||||
toast.success(message);
|
toast.success(message);
|
||||||
} else {
|
} else {
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,12 @@ const InfoBox: React.FC<Properties> = properties => {
|
||||||
<div className='info-box-content'>
|
<div className='info-box-content'>
|
||||||
<span className='info-box-text'>{properties.name}</span>
|
<span className='info-box-text'>{properties.name}</span>
|
||||||
<span className='info-box-number'>
|
<span className='info-box-number'>
|
||||||
<b>{properties.used}</b> / {total} {properties.unit}
|
<b>{properties.used}</b>
|
||||||
|
{' '}
|
||||||
|
/
|
||||||
|
{total}
|
||||||
|
{' '}
|
||||||
|
{properties.unit}
|
||||||
</span>
|
</span>
|
||||||
<div className='progress'>
|
<div className='progress'>
|
||||||
<div className='progress-bar' style={{width: `${percentage}%`}}/>
|
<div className='progress-bar' style={{width: `${percentage}%`}}/>
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
|
import {t} from '@/scripts/i18n';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import * as scoreUtils from './scoreUtils';
|
import * as scoreUtils from './scoreUtils';
|
||||||
import {t} from '@/scripts/i18n';
|
|
||||||
|
|
||||||
type Properties = {
|
type Properties = {
|
||||||
readonly isLoading: boolean;
|
readonly isLoading: boolean;
|
||||||
|
|
@ -27,7 +27,9 @@ const SignButton: React.FC<Properties> = properties => {
|
||||||
disabled={!canSign || properties.isLoading}
|
disabled={!canSign || properties.isLoading}
|
||||||
onClick={properties.onClick}
|
onClick={properties.onClick}
|
||||||
>
|
>
|
||||||
<i className='far fa-calendar-check' aria-hidden='true'/>
|
<i className='far fa-calendar-check' aria-hidden='true'/>
|
||||||
|
{' '}
|
||||||
|
|
||||||
{canSign ? t('user.sign') : remainingTimeText}
|
{canSign ? t('user.sign') : remainingTimeText}
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -1,15 +1,15 @@
|
||||||
import {useState, useEffect, useCallback} from 'react';
|
|
||||||
import styled from '@emotion/styled';
|
|
||||||
import InfoBox from './InfoBox';
|
|
||||||
import SignButton from './SignButton';
|
|
||||||
import * as scoreUtils from './scoreUtils';
|
|
||||||
import useEmitMounted from '@/scripts/hooks/useEmitMounted';
|
import useEmitMounted from '@/scripts/hooks/useEmitMounted';
|
||||||
|
import useTween from '@/scripts/hooks/useTween';
|
||||||
import {t} from '@/scripts/i18n';
|
import {t} from '@/scripts/i18n';
|
||||||
import * as fetch from '@/scripts/net';
|
import * as fetch from '@/scripts/net';
|
||||||
import {toast} from '@/scripts/notify';
|
import {toast} from '@/scripts/notify';
|
||||||
import useTween from '@/scripts/hooks/useTween';
|
|
||||||
import urls from '@/scripts/urls';
|
import urls from '@/scripts/urls';
|
||||||
import * as breakpoints from '@/styles/breakpoints';
|
import * as breakpoints from '@/styles/breakpoints';
|
||||||
|
import styled from '@emotion/styled';
|
||||||
|
import {useCallback, useEffect, useState} from 'react';
|
||||||
|
import InfoBox from './InfoBox';
|
||||||
|
import * as scoreUtils from './scoreUtils';
|
||||||
|
import SignButton from './SignButton';
|
||||||
|
|
||||||
type ScoreInfo = {
|
type ScoreInfo = {
|
||||||
signAfterZero: boolean;
|
signAfterZero: boolean;
|
||||||
|
|
@ -77,7 +77,7 @@ export default function Dashboard() {
|
||||||
const handleSign = useCallback(async () => {
|
const handleSign = useCallback(async () => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
const {code, message, data} = await fetch.post<
|
const {code, message, data} = await fetch.post<
|
||||||
fetch.ResponseBody<SignReturn>
|
fetch.ResponseBody<SignReturn>
|
||||||
>(urls.user.sign());
|
>(urls.user.sign());
|
||||||
|
|
||||||
if (code === 0) {
|
if (code === 0) {
|
||||||
|
|
@ -116,25 +116,27 @@ export default function Dashboard() {
|
||||||
unused={score / playersRate}
|
unused={score / playersRate}
|
||||||
unit=''
|
unit=''
|
||||||
/>
|
/>
|
||||||
{storage > 1024 ? (
|
{storage > 1024
|
||||||
<InfoBox
|
? (
|
||||||
color='maroon'
|
<InfoBox
|
||||||
icon='hdd'
|
color='maroon'
|
||||||
name={t('user.used.storage')}
|
icon='hdd'
|
||||||
used={Math.trunc(storage / 1024)}
|
name={t('user.used.storage')}
|
||||||
unused={Math.trunc(score / storageRate / 1024)}
|
used={Math.trunc(storage / 1024)}
|
||||||
unit='MB'
|
unused={Math.trunc(score / storageRate / 1024)}
|
||||||
/>
|
unit='MB'
|
||||||
) : (
|
/>
|
||||||
<InfoBox
|
)
|
||||||
color='maroon'
|
: (
|
||||||
icon='hdd'
|
<InfoBox
|
||||||
name={t('user.used.storage')}
|
color='maroon'
|
||||||
used={storage}
|
icon='hdd'
|
||||||
unused={score / storageRate}
|
name={t('user.used.storage')}
|
||||||
unit='KB'
|
used={storage}
|
||||||
/>
|
unused={score / storageRate}
|
||||||
)}
|
unit='KB'
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className='col-md-4 text-center'>
|
<div className='col-md-4 text-center'>
|
||||||
<ScoreTitle>{t('user.cur-score')}</ScoreTitle>
|
<ScoreTitle>{t('user.cur-score')}</ScoreTitle>
|
||||||
|
|
|
||||||
|
|
@ -24,11 +24,11 @@ export function remainingTimeText(remainingTime: number): string {
|
||||||
const time = remainingTime / ONE_MINUTE;
|
const time = remainingTime / ONE_MINUTE;
|
||||||
return time < 60
|
return time < 60
|
||||||
? t('user.signRemainingTime', {
|
? t('user.signRemainingTime', {
|
||||||
time: (Math.trunc(time)).toString(),
|
time: Math.trunc(time).toString(),
|
||||||
unit: t('user.timeUnitMin'),
|
unit: t('user.timeUnitMin'),
|
||||||
})
|
})
|
||||||
: t('user.signRemainingTime', {
|
: t('user.signRemainingTime', {
|
||||||
time: (Math.trunc(time / 60)).toString(),
|
time: Math.trunc(time / 60).toString(),
|
||||||
unit: t('user.timeUnitHour'),
|
unit: t('user.timeUnitHour'),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,11 @@
|
||||||
import {useState} from 'react';
|
|
||||||
import {t} from '@/scripts/i18n';
|
|
||||||
import Modal from '@/components/Modal';
|
import Modal from '@/components/Modal';
|
||||||
|
import {t} from '@/scripts/i18n';
|
||||||
|
import {useState} from 'react';
|
||||||
|
|
||||||
type Properties = {
|
type Properties = {
|
||||||
readonly show: boolean;
|
readonly show: boolean;
|
||||||
onCreate(name: string, redirect: string): Promise<void>;
|
onCreate: (name: string, redirect: string) => Promise<void>;
|
||||||
onClose(): void;
|
onClose: () => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
const ModalCreate: React.FC<Properties> = properties => {
|
const ModalCreate: React.FC<Properties> = properties => {
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
|
|
||||||
import type {App} from './types';
|
import type {App} from './types';
|
||||||
import {t} from '@/scripts/i18n';
|
|
||||||
import ButtonEdit from '@/components/ButtonEdit';
|
import ButtonEdit from '@/components/ButtonEdit';
|
||||||
|
import {t} from '@/scripts/i18n';
|
||||||
|
|
||||||
type Properties = {
|
type Properties = {
|
||||||
readonly app: App;
|
readonly app: App;
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,11 @@
|
||||||
import React, {useState, useEffect} from 'react';
|
|
||||||
import Row from './Row';
|
|
||||||
import ModalCreate from './ModalCreate';
|
|
||||||
import type {App} from './types';
|
import type {App} from './types';
|
||||||
|
import Loading from '@/components/Loading';
|
||||||
import {t} from '@/scripts/i18n';
|
import {t} from '@/scripts/i18n';
|
||||||
import * as fetch from '@/scripts/net';
|
import * as fetch from '@/scripts/net';
|
||||||
import {showModal, toast} from '@/scripts/notify';
|
import {showModal, toast} from '@/scripts/notify';
|
||||||
import Loading from '@/components/Loading';
|
import React, {useEffect, useState} from 'react';
|
||||||
|
import ModalCreate from './ModalCreate';
|
||||||
|
import Row from './Row';
|
||||||
|
|
||||||
type Exception = {
|
type Exception = {
|
||||||
message: string;
|
message: string;
|
||||||
|
|
@ -133,14 +133,15 @@ function OAuth() {
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{apps.length === 0 ? (
|
{apps.length === 0
|
||||||
<tr>
|
? (
|
||||||
<td className='text-center' colSpan={5}>
|
<tr>
|
||||||
{isLoading ? <Loading/> : t('general.noResult')}
|
<td className='text-center' colSpan={5}>
|
||||||
</td>
|
{isLoading ? <Loading/> : t('general.noResult')}
|
||||||
</tr>
|
</td>
|
||||||
) : (
|
</tr>
|
||||||
apps.map((app, i) => (
|
)
|
||||||
|
: apps.map((app, i) => (
|
||||||
<Row
|
<Row
|
||||||
key={app.id}
|
key={app.id}
|
||||||
app={app}
|
app={app}
|
||||||
|
|
@ -148,8 +149,7 @@ function OAuth() {
|
||||||
onEditRedirect={async () => editRedirect(app, i)}
|
onEditRedirect={async () => editRedirect(app, i)}
|
||||||
onDelete={async () => handleDelete(app)}
|
onDelete={async () => handleDelete(app)}
|
||||||
/>
|
/>
|
||||||
))
|
))}
|
||||||
)}
|
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,10 @@
|
||||||
import {useState} from 'react';
|
import type {Player} from '@/scripts/types';
|
||||||
|
import Modal from '@/components/Modal';
|
||||||
import {t} from '@/scripts/i18n';
|
import {t} from '@/scripts/i18n';
|
||||||
import * as fetch from '@/scripts/net';
|
import * as fetch from '@/scripts/net';
|
||||||
import {toast} from '@/scripts/notify';
|
import {toast} from '@/scripts/notify';
|
||||||
import type {Player} from '@/scripts/types';
|
|
||||||
import urls from '@/scripts/urls';
|
import urls from '@/scripts/urls';
|
||||||
import Modal from '@/components/Modal';
|
import {useState} from 'react';
|
||||||
|
|
||||||
type Extra = {
|
type Extra = {
|
||||||
score: number;
|
score: number;
|
||||||
|
|
@ -15,8 +15,8 @@ type Extra = {
|
||||||
|
|
||||||
type Properties = {
|
type Properties = {
|
||||||
readonly show: boolean;
|
readonly show: boolean;
|
||||||
onAdd(player: Player): void;
|
onAdd: (player: Player) => void;
|
||||||
onClose(): void;
|
onClose: () => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
const ModalAddPlayer: React.FC<Properties> = properties => {
|
const ModalAddPlayer: React.FC<Properties> = properties => {
|
||||||
|
|
@ -81,7 +81,9 @@ const ModalAddPlayer: React.FC<Properties> = properties => {
|
||||||
>
|
>
|
||||||
<i className={`icon fas fa-${isScoreEnough ? 'check' : 'times'}`}/>
|
<i className={`icon fas fa-${isScoreEnough ? 'check' : 'times'}`}/>
|
||||||
<span className='ml-1'>
|
<span className='ml-1'>
|
||||||
{t('user.cur-score')} {score}
|
{t('user.cur-score')}
|
||||||
|
{' '}
|
||||||
|
{score}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,11 @@
|
||||||
import {useState} from 'react';
|
|
||||||
import {t} from '@/scripts/i18n';
|
|
||||||
import Modal from '@/components/Modal';
|
import Modal from '@/components/Modal';
|
||||||
|
import {t} from '@/scripts/i18n';
|
||||||
|
import {useState} from 'react';
|
||||||
|
|
||||||
type Properties = {
|
type Properties = {
|
||||||
readonly show: boolean;
|
readonly show: boolean;
|
||||||
onSubmit(skin: boolean, cape: boolean): Promise<void>;
|
onSubmit: (skin: boolean, cape: boolean) => Promise<void>;
|
||||||
onClose(): void;
|
onClose: () => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
const ModalReset: React.FC<Properties> = properties => {
|
const ModalReset: React.FC<Properties> = properties => {
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,9 @@
|
||||||
|
import ViewerSkeleton from '@/components/ViewerSkeleton';
|
||||||
|
import useMount from '@/scripts/hooks/useMount';
|
||||||
|
import {t} from '@/scripts/i18n';
|
||||||
import React, {useState} from 'react';
|
import React, {useState} from 'react';
|
||||||
import ReactDOM from 'react-dom';
|
import ReactDOM from 'react-dom';
|
||||||
import Viewer2d from './Viewer2d';
|
import Viewer2d from './Viewer2d';
|
||||||
import useMount from '@/scripts/hooks/useMount';
|
|
||||||
import {t} from '@/scripts/i18n';
|
|
||||||
import ViewerSkeleton from '@/components/ViewerSkeleton';
|
|
||||||
|
|
||||||
const Viewer3d = React.lazy(async () => import('@/components/Viewer'));
|
const Viewer3d = React.lazy(async () => import('@/components/Viewer'));
|
||||||
|
|
||||||
|
|
@ -32,20 +32,22 @@ const Previewer: React.FC<Properties> = properties => {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
container
|
container
|
||||||
&& ReactDOM.createPortal(
|
&& ReactDOM.createPortal(
|
||||||
is3d ? (
|
is3d
|
||||||
<React.Suspense fallback={<ViewerSkeleton/>}>
|
? (
|
||||||
<Viewer3d skin={skin} cape={cape} isAlex={isAlex}>
|
<React.Suspense fallback={<ViewerSkeleton/>}>
|
||||||
{switcher}
|
<Viewer3d skin={skin} cape={cape} isAlex={isAlex}>
|
||||||
</Viewer3d>
|
{switcher}
|
||||||
</React.Suspense>
|
</Viewer3d>
|
||||||
) : (
|
</React.Suspense>
|
||||||
<Viewer2d skin={skin} cape={cape}>
|
)
|
||||||
{switcher}
|
: (
|
||||||
</Viewer2d>
|
<Viewer2d skin={skin} cape={cape}>
|
||||||
),
|
{switcher}
|
||||||
container,
|
</Viewer2d>
|
||||||
)
|
),
|
||||||
|
container,
|
||||||
|
)
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,17 +1,16 @@
|
||||||
/** @jsxImportSource @emotion/react */
|
|
||||||
import {css} from '@emotion/react';
|
|
||||||
import {t} from '@/scripts/i18n';
|
|
||||||
import type {Player} from '@/scripts/types';
|
import type {Player} from '@/scripts/types';
|
||||||
import ButtonEdit from '@/components/ButtonEdit';
|
import ButtonEdit from '@/components/ButtonEdit';
|
||||||
|
import {t} from '@/scripts/i18n';
|
||||||
import * as cssUtils from '@/styles/utils';
|
import * as cssUtils from '@/styles/utils';
|
||||||
|
import {css} from '@emotion/react';
|
||||||
|
|
||||||
type Properties = {
|
type Properties = {
|
||||||
player: Player;
|
player: Player;
|
||||||
selected: boolean;
|
selected: boolean;
|
||||||
onClick: React.MouseEventHandler;
|
onClick: React.MouseEventHandler;
|
||||||
onEditName(player: Player): Promise<void>;
|
onEditName: (player: Player) => Promise<void>;
|
||||||
onReset(): void;
|
onReset: () => void;
|
||||||
onDelete(player: Player): Promise<void>;
|
onDelete: (player: Player) => Promise<void>;
|
||||||
};
|
};
|
||||||
|
|
||||||
const Row: React.FC<Properties> = properties => {
|
const Row: React.FC<Properties> = properties => {
|
||||||
|
|
@ -27,7 +26,7 @@ const Row: React.FC<Properties> = properties => {
|
||||||
|
|
||||||
const selected
|
const selected
|
||||||
= properties.selected
|
= properties.selected
|
||||||
&& css`
|
&& css`
|
||||||
background: #efefef;
|
background: #efefef;
|
||||||
.dark-mode & {
|
.dark-mode & {
|
||||||
background: var(--dark);
|
background: var(--dark);
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
|
|
||||||
import styled from '@emotion/styled';
|
|
||||||
import {t} from '@/scripts/i18n';
|
import {t} from '@/scripts/i18n';
|
||||||
|
import styled from '@emotion/styled';
|
||||||
|
|
||||||
const TexturePreview = styled.div`
|
const TexturePreview = styled.div`
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|
@ -27,19 +27,15 @@ const Viewer2d: React.FC<Properties> = properties => (
|
||||||
<div className='card-body'>
|
<div className='card-body'>
|
||||||
<TexturePreview className='mb-5'>
|
<TexturePreview className='mb-5'>
|
||||||
<span>{t('general.skin')}</span>
|
<span>{t('general.skin')}</span>
|
||||||
{properties.skin ? (
|
{properties.skin
|
||||||
<img src={properties.skin} alt={t('general.skin')}/>
|
? <img src={properties.skin} alt={t('general.skin')}/>
|
||||||
) : (
|
: <span>{t('user.player.texture-empty')}</span>}
|
||||||
<span>{t('user.player.texture-empty')}</span>
|
|
||||||
)}
|
|
||||||
</TexturePreview>
|
</TexturePreview>
|
||||||
<TexturePreview className='mt-5'>
|
<TexturePreview className='mt-5'>
|
||||||
<span>{t('general.cape')}</span>
|
<span>{t('general.cape')}</span>
|
||||||
{properties.cape ? (
|
{properties.cape
|
||||||
<img src={properties.cape} alt={t('general.cape')}/>
|
? <img src={properties.cape} alt={t('general.cape')}/>
|
||||||
) : (
|
: <span>{t('user.player.texture-empty')}</span>}
|
||||||
<span>{t('user.player.texture-empty')}</span>
|
|
||||||
)}
|
|
||||||
</TexturePreview>
|
</TexturePreview>
|
||||||
</div>
|
</div>
|
||||||
<div className='card-footer'>{properties.children}</div>
|
<div className='card-footer'>{properties.children}</div>
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,3 @@
|
||||||
import {useState, useEffect} from 'react';
|
|
||||||
import Row from './Row';
|
|
||||||
import LoadingRow from './LoadingRow';
|
|
||||||
import Previewer from './Previewer';
|
|
||||||
import ModalAddPlayer from './ModalAddPlayer';
|
|
||||||
import ModalReset from './ModalReset';
|
|
||||||
import useBlessingExtra from '@/scripts/hooks/useBlessingExtra';
|
import useBlessingExtra from '@/scripts/hooks/useBlessingExtra';
|
||||||
import useEmitMounted from '@/scripts/hooks/useEmitMounted';
|
import useEmitMounted from '@/scripts/hooks/useEmitMounted';
|
||||||
import useTexture from '@/scripts/hooks/useTexture';
|
import useTexture from '@/scripts/hooks/useTexture';
|
||||||
|
|
@ -12,6 +6,12 @@ import * as fetch from '@/scripts/net';
|
||||||
import {showModal, toast} from '@/scripts/notify';
|
import {showModal, toast} from '@/scripts/notify';
|
||||||
import {type Player, TextureType} from '@/scripts/types';
|
import {type Player, TextureType} from '@/scripts/types';
|
||||||
import urls from '@/scripts/urls';
|
import urls from '@/scripts/urls';
|
||||||
|
import {useEffect, useState} from 'react';
|
||||||
|
import LoadingRow from './LoadingRow';
|
||||||
|
import ModalAddPlayer from './ModalAddPlayer';
|
||||||
|
import ModalReset from './ModalReset';
|
||||||
|
import Previewer from './Previewer';
|
||||||
|
import Row from './Row';
|
||||||
|
|
||||||
function Players() {
|
function Players() {
|
||||||
const [players, setPlayers] = useState<Player[]>([]);
|
const [players, setPlayers] = useState<Player[]>([]);
|
||||||
|
|
@ -104,9 +104,7 @@ function Players() {
|
||||||
search.append('cape', 'true');
|
search.append('cape', 'true');
|
||||||
}
|
}
|
||||||
|
|
||||||
const {code, message} = await fetch.del<fetch.ResponseBody>(
|
const {code, message} = await fetch.del<fetch.ResponseBody>(`${urls.user.player.clear(selected)}?${search.toString()}`);
|
||||||
`${urls.user.player.clear(selected)}?${search.toString()}`,
|
|
||||||
);
|
|
||||||
if (code === 0) {
|
if (code === 0) {
|
||||||
toast.success(message);
|
toast.success(message);
|
||||||
if (skin) {
|
if (skin) {
|
||||||
|
|
@ -147,9 +145,7 @@ function Players() {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const {code, message} = await fetch.del<fetch.ResponseBody>(
|
const {code, message} = await fetch.del<fetch.ResponseBody>(urls.user.player.delete(player.pid));
|
||||||
urls.user.player.delete(player.pid),
|
|
||||||
);
|
|
||||||
if (code === 0) {
|
if (code === 0) {
|
||||||
toast.success(message);
|
toast.success(message);
|
||||||
const {pid} = player;
|
const {pid} = player;
|
||||||
|
|
@ -196,33 +192,33 @@ function Players() {
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{isLoading ? (
|
{isLoading
|
||||||
new Array(playersCount)
|
? new Array(playersCount)
|
||||||
.fill(null)
|
.fill(null)
|
||||||
.map((_, i) => <LoadingRow key={i}/>)
|
.map((_, i) => <LoadingRow key={i}/>)
|
||||||
) : (players.length === 0 ? (
|
: players.length === 0
|
||||||
<tr>
|
? (
|
||||||
<td className='text-center' colSpan={3}>
|
<tr>
|
||||||
{t('general.noResult')}
|
<td className='text-center' colSpan={3}>
|
||||||
</td>
|
{t('general.noResult')}
|
||||||
</tr>
|
</td>
|
||||||
) : (
|
</tr>
|
||||||
players
|
)
|
||||||
.filter(({name}) => name.includes(search))
|
: players
|
||||||
.map((player, i) => (
|
.filter(({name}) => name.includes(search))
|
||||||
<Row
|
.map((player, i) => (
|
||||||
key={player.pid}
|
<Row
|
||||||
player={player}
|
key={player.pid}
|
||||||
selected={selected === player.pid}
|
player={player}
|
||||||
onClick={() => {
|
selected={selected === player.pid}
|
||||||
selectPlayer(player);
|
onClick={() => {
|
||||||
}}
|
selectPlayer(player);
|
||||||
onEditName={async () => editName(player, i)}
|
}}
|
||||||
onReset={openModalReset}
|
onEditName={async () => editName(player, i)}
|
||||||
onDelete={deletePlayer}
|
onReset={openModalReset}
|
||||||
/>
|
onDelete={deletePlayer}
|
||||||
))
|
/>
|
||||||
))}
|
))}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
import resetAvatar from './resetAvatar';
|
|
||||||
import passwordFormHandler from './password';
|
|
||||||
import nicknameFormHandler from './nickname';
|
|
||||||
import emailFormHandler from './email';
|
|
||||||
import deleteAccountFormHandler from './deleteAccount';
|
import deleteAccountFormHandler from './deleteAccount';
|
||||||
|
import emailFormHandler from './email';
|
||||||
|
import nicknameFormHandler from './nickname';
|
||||||
|
import passwordFormHandler from './password';
|
||||||
|
import resetAvatar from './resetAvatar';
|
||||||
|
|
||||||
const buttonResetAvatar = document.querySelector('#reset-avatar');
|
const buttonResetAvatar = document.querySelector('#reset-avatar');
|
||||||
buttonResetAvatar?.addEventListener('click', resetAvatar);
|
buttonResetAvatar?.addEventListener('click', resetAvatar);
|
||||||
|
|
@ -16,7 +16,5 @@ nicknameForm?.addEventListener('submit', nicknameFormHandler);
|
||||||
const emailForm = document.querySelector<HTMLFormElement>('#change-email');
|
const emailForm = document.querySelector<HTMLFormElement>('#change-email');
|
||||||
emailForm?.addEventListener('submit', emailFormHandler);
|
emailForm?.addEventListener('submit', emailFormHandler);
|
||||||
|
|
||||||
const deleteAccountForm = document.querySelector<HTMLFormElement>(
|
const deleteAccountForm = document.querySelector<HTMLFormElement>('#modal-delete-account');
|
||||||
'#modal-delete-account',
|
|
||||||
);
|
|
||||||
deleteAccountForm?.addEventListener('submit', deleteAccountFormHandler);
|
deleteAccountForm?.addEventListener('submit', deleteAccountFormHandler);
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import {post, type ResponseBody} from '@/scripts/net';
|
|
||||||
import {t} from '@/scripts/i18n';
|
import {t} from '@/scripts/i18n';
|
||||||
|
import {post, type ResponseBody} from '@/scripts/net';
|
||||||
import {showModal, toast} from '@/scripts/notify';
|
import {showModal, toast} from '@/scripts/notify';
|
||||||
|
|
||||||
export default async function handler(event: Event) {
|
export default async function handler(event: Event) {
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import {showModal, toast} from '@/scripts/notify';
|
|
||||||
import {t} from '@/scripts/i18n';
|
import {t} from '@/scripts/i18n';
|
||||||
import {post, type ResponseBody} from '@/scripts/net';
|
import {post, type ResponseBody} from '@/scripts/net';
|
||||||
|
import {showModal, toast} from '@/scripts/notify';
|
||||||
|
|
||||||
export default async function resetAvatar() {
|
export default async function resetAvatar() {
|
||||||
try {
|
try {
|
||||||
|
|
|
||||||
|
|
@ -1,16 +1,14 @@
|
||||||
import React, {useState} from 'react';
|
|
||||||
import {t} from '@/scripts/i18n';
|
import {t} from '@/scripts/i18n';
|
||||||
import * as fetch from '@/scripts/net';
|
import * as fetch from '@/scripts/net';
|
||||||
import {toast} from '@/scripts/notify';
|
import {toast} from '@/scripts/notify';
|
||||||
|
import React, {useState} from 'react';
|
||||||
|
|
||||||
function EmailVerification() {
|
function EmailVerification() {
|
||||||
const [isSending, setIsSending] = useState(false);
|
const [isSending, setIsSending] = useState(false);
|
||||||
|
|
||||||
const send = async () => {
|
const send = async () => {
|
||||||
setIsSending(true);
|
setIsSending(true);
|
||||||
const {code, message} = await fetch.post<fetch.ResponseBody>(
|
const {code, message} = await fetch.post<fetch.ResponseBody>('/user/email-verification');
|
||||||
'/user/email-verification',
|
|
||||||
);
|
|
||||||
if (code === 0) {
|
if (code === 0) {
|
||||||
toast.success(message);
|
toast.success(message);
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -23,20 +21,24 @@ function EmailVerification() {
|
||||||
return (
|
return (
|
||||||
<div className='callout callout-info'>
|
<div className='callout callout-info'>
|
||||||
<h4>
|
<h4>
|
||||||
<i className='fas fa-envelope'/> {t('user.verification.title')}
|
<i className='fas fa-envelope'/>
|
||||||
|
{' '}
|
||||||
|
{t('user.verification.title')}
|
||||||
</h4>
|
</h4>
|
||||||
<p>
|
<p>
|
||||||
{t('user.verification.message')}
|
{t('user.verification.message')}
|
||||||
{isSending ? (
|
{isSending
|
||||||
<>
|
? (
|
||||||
<i className='fas fa-spin fa-spinner mr-1'/>
|
<>
|
||||||
{t('user.verification.sending')}
|
<i className='fas fa-spin fa-spinner mr-1'/>
|
||||||
</>
|
{t('user.verification.sending')}
|
||||||
) : (
|
</>
|
||||||
<a className='link-info' href='#' onClick={send}>
|
)
|
||||||
{t('user.verification.resend')}
|
: (
|
||||||
</a>
|
<a className='link-info' href='#' onClick={send}>
|
||||||
)}
|
{t('user.verification.resend')}
|
||||||
|
</a>
|
||||||
|
)}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import React, {useState, useEffect} from 'react';
|
|
||||||
import * as fetch from '@/scripts/net';
|
import * as fetch from '@/scripts/net';
|
||||||
import {showModal} from '@/scripts/notify';
|
import {showModal} from '@/scripts/notify';
|
||||||
|
import React, {useEffect, useState} from 'react';
|
||||||
|
|
||||||
export type Notification = {
|
export type Notification = {
|
||||||
id: string;
|
id: string;
|
||||||
|
|
@ -12,9 +12,7 @@ function NotificationsList() {
|
||||||
const [noUnreadText, setNoUnreadText] = useState('');
|
const [noUnreadText, setNoUnreadText] = useState('');
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const dataset = document.querySelector<HTMLLIElement>(
|
const dataset = document.querySelector<HTMLLIElement>('[data-notifications]')?.dataset;
|
||||||
'[data-notifications]',
|
|
||||||
)?.dataset;
|
|
||||||
if (dataset) {
|
if (dataset) {
|
||||||
const notifications: Notification[] = JSON.parse(dataset.notifications!);
|
const notifications: Notification[] = JSON.parse(dataset.notifications!);
|
||||||
setNotifications(notifications);
|
setNotifications(notifications);
|
||||||
|
|
@ -41,8 +39,7 @@ function NotificationsList() {
|
||||||
),
|
),
|
||||||
});
|
});
|
||||||
setNotifications(notifications =>
|
setNotifications(notifications =>
|
||||||
notifications.filter(notification => notification.id !== id),
|
notifications.filter(notification => notification.id !== id));
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const hasUnread = notifications.length > 0;
|
const hasUnread = notifications.length > 0;
|
||||||
|
|
@ -58,8 +55,8 @@ function NotificationsList() {
|
||||||
)}
|
)}
|
||||||
</a>
|
</a>
|
||||||
<div className='dropdown-menu dropdown-menu-lg dropdown-menu-right'>
|
<div className='dropdown-menu dropdown-menu-lg dropdown-menu-right'>
|
||||||
{hasUnread ? (
|
{hasUnread
|
||||||
notifications.map(notification => (
|
? notifications.map(notification => (
|
||||||
<>
|
<>
|
||||||
<a
|
<a
|
||||||
key={notification.id}
|
key={notification.id}
|
||||||
|
|
@ -73,9 +70,7 @@ function NotificationsList() {
|
||||||
<div className='dropdown-divider'/>
|
<div className='dropdown-divider'/>
|
||||||
</>
|
</>
|
||||||
))
|
))
|
||||||
) : (
|
: <p className='text-center text-muted pt-2 pb-2'>{noUnreadText}</p>}
|
||||||
<p className='text-center text-muted pt-2 pb-2'>{noUnreadText}</p>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
2
resources/assets/src/vite-env.d.ts
vendored
Normal file
2
resources/assets/src/vite-env.d.ts
vendored
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
/* eslint-disable ts/triple-slash-reference */
|
||||||
|
/// <reference types="vite/client" />
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user