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