From 2b196a95a81d4d4dfba8ac42921a466686ee0d5c Mon Sep 17 00:00:00 2001 From: Zephyr Lykos Date: Fri, 23 Feb 2024 16:58:44 +0800 Subject: [PATCH 1/9] cleanup: wip part 1 --- .editorconfig | 9 +- .eslintrc.yml | 27 - .husky/pre-commit | 4 - .postcssrc.json | 5 + package.json | 189 +- postcss.config.js | 5 - resources/assets/src/components/Alert.tsx | 43 +- .../assets/src/components/ButtonEdit.tsx | 22 +- resources/assets/src/components/Captcha.tsx | 177 +- .../assets/src/components/DarkModeButton.tsx | 40 +- .../assets/src/components/EmailSuggestion.tsx | 138 +- resources/assets/src/components/FileInput.tsx | 84 +- resources/assets/src/components/Loading.tsx | 16 +- resources/assets/src/components/Modal.tsx | 283 +- resources/assets/src/components/ModalBody.tsx | 45 +- .../assets/src/components/ModalContent.tsx | 49 +- .../assets/src/components/ModalFooter.tsx | 90 +- .../assets/src/components/ModalHeader.tsx | 48 +- .../assets/src/components/ModalInput.tsx | 108 +- .../assets/src/components/Pagination.tsx | 234 +- .../assets/src/components/PaginationItem.tsx | 71 +- resources/assets/src/components/Toast.tsx | 107 +- resources/assets/src/components/Viewer.tsx | 522 +- .../assets/src/components/ViewerSkeleton.tsx | 28 +- resources/assets/src/index.tsx | 69 +- resources/assets/src/scripts/app.ts | 24 +- resources/assets/src/scripts/cli.tsx | 193 +- .../assets/src/scripts/cli/AptCommand.ts | 34 +- .../assets/src/scripts/cli/ClosetCommand.ts | 82 +- .../assets/src/scripts/cli/DnfCommand.ts | 34 +- .../assets/src/scripts/cli/PacmanCommand.ts | 56 +- resources/assets/src/scripts/cli/RmCommand.ts | 74 +- resources/assets/src/scripts/cli/Spinner.ts | 40 +- .../assets/src/scripts/cli/configureStdio.ts | 52 +- .../assets/src/scripts/cli/pluginManager.ts | 64 +- resources/assets/src/scripts/cli/readline.ts | 10 +- resources/assets/src/scripts/darkMode.tsx | 13 +- .../assets/src/scripts/emailVerification.tsx | 9 +- resources/assets/src/scripts/event.ts | 25 +- resources/assets/src/scripts/extra.ts | 16 +- resources/assets/src/scripts/homePage.ts | 36 +- .../src/scripts/hooks/useBlessingExtra.ts | 12 +- .../src/scripts/hooks/useEmitMounted.ts | 10 +- .../src/scripts/hooks/useIsLargeScreen.ts | 16 +- .../assets/src/scripts/hooks/useMount.ts | 28 +- .../assets/src/scripts/hooks/useTexture.ts | 39 +- .../assets/src/scripts/hooks/useTween.ts | 32 +- resources/assets/src/scripts/i18n.ts | 59 +- resources/assets/src/scripts/init.ts | 14 +- resources/assets/src/scripts/logout.ts | 32 +- resources/assets/src/scripts/modal.tsx | 45 +- resources/assets/src/scripts/net.ts | 247 +- resources/assets/src/scripts/notification.tsx | 9 +- resources/assets/src/scripts/notify.ts | 16 +- resources/assets/src/scripts/route.tsx | 232 +- resources/assets/src/scripts/textureUtils.ts | 76 +- resources/assets/src/scripts/toast.tsx | 150 +- resources/assets/src/scripts/types.ts | 92 +- resources/assets/src/scripts/urls.ts | 134 +- resources/assets/src/shims.d.ts | 2 +- resources/assets/src/styles/breakpoints.ts | 16 +- resources/assets/src/styles/utils.ts | 6 +- .../assets/src/views/admin/Customization.ts | 122 +- resources/assets/src/views/admin/Dashboard.ts | 210 +- .../views/admin/PlayersManagement/Card.tsx | 310 +- .../admin/PlayersManagement/LoadingCard.tsx | 64 +- .../admin/PlayersManagement/LoadingRow.tsx | 22 +- .../PlayersManagement/ModalUpdateTexture.tsx | 152 +- .../src/views/admin/PlayersManagement/Row.tsx | 154 +- .../views/admin/PlayersManagement/index.tsx | 510 +- .../views/admin/PlayersManagement/styles.ts | 6 +- .../views/admin/PluginsManagement/InfoBox.tsx | 178 +- .../views/admin/PluginsManagement/index.tsx | 457 +- .../views/admin/PluginsManagement/types.ts | 18 +- .../src/views/admin/PluginsMarket/Row.tsx | 174 +- .../src/views/admin/PluginsMarket/index.tsx | 305 +- .../src/views/admin/PluginsMarket/types.ts | 24 +- .../admin/ReportsManagement/ImageBox.tsx | 238 +- .../views/admin/ReportsManagement/index.tsx | 276 +- .../views/admin/ReportsManagement/types.ts | 30 +- .../src/views/admin/Translations/Row.tsx | 72 +- .../src/views/admin/Translations/index.tsx | 223 +- .../src/views/admin/Translations/types.ts | 10 +- resources/assets/src/views/admin/Update.ts | 36 +- .../src/views/admin/UsersManagement/Card.tsx | 314 +- .../views/admin/UsersManagement/Header.tsx | 8 +- .../admin/UsersManagement/LoadingCard.tsx | 112 +- .../admin/UsersManagement/LoadingRow.tsx | 22 +- .../src/views/admin/UsersManagement/Row.tsx | 216 +- .../src/views/admin/UsersManagement/index.tsx | 691 +- .../src/views/admin/UsersManagement/styles.ts | 14 +- .../src/views/admin/UsersManagement/utils.ts | 37 +- resources/assets/src/views/auth/Forgot.tsx | 129 +- resources/assets/src/views/auth/Login.tsx | 268 +- .../assets/src/views/auth/Registration.tsx | 329 +- resources/assets/src/views/auth/Reset.tsx | 198 +- .../src/views/skinlib/Show/addClosetItem.ts | 66 +- .../assets/src/views/skinlib/Show/index.tsx | 935 +- .../src/views/skinlib/SkinLibrary/Button.tsx | 49 +- .../skinlib/SkinLibrary/FilterSelector.tsx | 129 +- .../src/views/skinlib/SkinLibrary/Item.tsx | 176 +- .../src/views/skinlib/SkinLibrary/index.tsx | 542 +- .../src/views/skinlib/SkinLibrary/types.ts | 20 +- .../src/views/skinlib/SkinLibrary/utils.ts | 27 +- resources/assets/src/views/skinlib/Upload.tsx | 479 +- .../src/views/user/Closet/ClosetItem.tsx | 140 +- .../views/user/Closet/LoadingClosetItem.tsx | 40 +- .../src/views/user/Closet/ModalApply.tsx | 209 +- .../src/views/user/Closet/Previewer.tsx | 53 +- .../assets/src/views/user/Closet/index.tsx | 540 +- .../src/views/user/Closet/removeClosetItem.ts | 42 +- .../src/views/user/Closet/setAsAvatar.ts | 51 +- .../assets/src/views/user/Closet/styles.ts | 6 +- .../src/views/user/Dashboard/InfoBox.tsx | 60 +- .../src/views/user/Dashboard/SignButton.tsx | 64 +- .../assets/src/views/user/Dashboard/index.tsx | 268 +- .../src/views/user/Dashboard/scoreUtils.ts | 48 +- .../src/views/user/OAuth/ModalCreate.tsx | 114 +- resources/assets/src/views/user/OAuth/Row.tsx | 78 +- .../assets/src/views/user/OAuth/index.tsx | 297 +- .../assets/src/views/user/OAuth/types.ts | 10 +- .../src/views/user/Players/LoadingRow.tsx | 24 +- .../src/views/user/Players/ModalAddPlayer.tsx | 162 +- .../src/views/user/Players/ModalReset.tsx | 108 +- .../src/views/user/Players/Previewer.tsx | 82 +- .../assets/src/views/user/Players/Row.tsx | 88 +- .../src/views/user/Players/Viewer2d.tsx | 73 +- .../assets/src/views/user/Players/index.tsx | 458 +- .../src/views/user/profile/deleteAccount.ts | 26 +- .../assets/src/views/user/profile/email.ts | 34 +- .../assets/src/views/user/profile/index.ts | 32 +- .../assets/src/views/user/profile/nickname.ts | 34 +- .../assets/src/views/user/profile/password.ts | 48 +- .../src/views/user/profile/resetAvatar.ts | 33 +- .../src/views/widgets/EmailVerification.tsx | 79 +- .../src/views/widgets/NotificationsList.tsx | 148 +- resources/assets/tests/.eslintrc.yml | 5 - .../assets/tests/components/Captcha.test.tsx | 23 +- .../tests/components/DarkModeButton.test.tsx | 4 +- .../tests/components/EmailSuggestion.test.tsx | 1 + .../tests/components/FileInput.test.tsx | 4 +- .../assets/tests/components/Modal.test.tsx | 42 +- .../tests/components/Pagination.test.tsx | 18 +- .../assets/tests/components/Viewer.test.tsx | 2 +- .../tests/scripts/cli/AptCommand.test.ts | 4 +- .../tests/scripts/cli/ClosetCommand.test.ts | 2 +- .../tests/scripts/cli/DnfCommand.test.ts | 4 +- .../tests/scripts/cli/PacmanCommand.test.ts | 4 +- .../tests/scripts/cli/RmCommand.test.ts | 2 +- .../assets/tests/scripts/cli/Spinner.test.ts | 2 +- resources/assets/tests/scripts/event.test.ts | 8 +- resources/assets/tests/scripts/logout.test.ts | 4 +- resources/assets/tests/scripts/net.test.ts | 30 +- resources/assets/tests/scripts/toast.test.ts | 2 +- resources/assets/tests/setup.ts | 11 +- resources/assets/tests/tsconfig.json | 4 + resources/assets/tests/types.d.ts | 34 - .../tests/views/admin/Customization.test.ts | 1 + .../views/admin/PlayersManagement.test.tsx | 4 +- .../views/admin/PluginsManagement.test.tsx | 4 +- .../tests/views/admin/PluginsMarket.test.tsx | 4 +- .../views/admin/ReportsManagement.test.tsx | 8 +- .../tests/views/admin/Translations.test.tsx | 4 +- .../assets/tests/views/admin/Update.test.ts | 5 +- .../views/admin/UsersManagement.test.tsx | 6 +- .../assets/tests/views/auth/Forgot.test.tsx | 4 +- .../assets/tests/views/auth/Login.test.tsx | 4 +- .../tests/views/auth/Registration.test.tsx | 6 +- .../assets/tests/views/auth/Reset.test.tsx | 6 +- .../assets/tests/views/skinlib/Show.test.tsx | 14 +- .../tests/views/skinlib/SkinLibrary.test.tsx | 4 +- .../tests/views/skinlib/Upload.test.tsx | 8 +- .../assets/tests/views/user/Closet.test.tsx | 6 +- .../tests/views/user/Dashboard.test.tsx | 6 +- .../assets/tests/views/user/OAuth.test.tsx | 4 +- .../assets/tests/views/user/Players.test.tsx | 6 +- .../views/user/profile/deleteAccount.test.ts | 5 +- .../tests/views/user/profile/email.test.ts | 5 +- .../tests/views/user/profile/nickname.test.ts | 5 +- .../tests/views/user/profile/password.test.ts | 5 +- .../views/user/profile/resetAvatar.test.ts | 5 +- .../views/widgets/EmailVerification.test.tsx | 4 +- .../views/widgets/NotificationsList.test.tsx | 6 +- resources/assets/tests/webpack.d.ts | 15 - tools/HtmlWebpackEnhancementPlugin.ts | 120 +- tools/generateUrls.ts | 266 +- tsconfig.build.json | 9 - tsconfig.dev.json | 6 - tsconfig.eslint.json | 4 - tsconfig.json | 27 +- vite.config.ts | 37 + webpack.config.ts | 312 +- yarn.lock | 9194 +++++------------ 193 files changed, 11518 insertions(+), 15064 deletions(-) delete mode 100644 .eslintrc.yml delete mode 100755 .husky/pre-commit create mode 100644 .postcssrc.json delete mode 100644 postcss.config.js delete mode 100644 resources/assets/tests/.eslintrc.yml delete mode 100644 resources/assets/tests/types.d.ts delete mode 100644 resources/assets/tests/webpack.d.ts delete mode 100644 tsconfig.build.json delete mode 100644 tsconfig.dev.json delete mode 100644 tsconfig.eslint.json create mode 100644 vite.config.ts diff --git a/.editorconfig b/.editorconfig index 76332f27..93ec9757 100644 --- a/.editorconfig +++ b/.editorconfig @@ -4,10 +4,11 @@ root = true [*] charset = utf-8 end_of_line = lf -indent_size = 2 -indent_style = space +indent_size = 4 +indent_style = tab insert_final_newline = true trim_trailing_whitespace = true -[*.{php,md,ps1,Dockerfile}] -indent_size = 4 +[*.{json,yaml,yml}] +indent_style = space +indent_size = 2 diff --git a/.eslintrc.yml b/.eslintrc.yml deleted file mode 100644 index 3a8c2fa4..00000000 --- a/.eslintrc.yml +++ /dev/null @@ -1,27 +0,0 @@ -root: true -parser: '@typescript-eslint/parser' -parserOptions: - project: tsconfig.eslint.json -plugins: - - '@typescript-eslint/eslint-plugin' -extends: - - eslint:recommended - - plugin:@typescript-eslint/recommended - - plugin:@typescript-eslint/recommended-requiring-type-checking - - plugin:react-hooks/recommended -rules: - prefer-const: error - '@typescript-eslint/no-unsafe-assignment': off - '@typescript-eslint/no-unsafe-member-access': off - '@typescript-eslint/no-unsafe-return': off - '@typescript-eslint/no-unused-vars': off - '@typescript-eslint/explicit-module-boundary-types': off - '@typescript-eslint/no-explicit-any': off - '@typescript-eslint/ban-ts-comment': off - '@typescript-eslint/no-non-null-assertion': off - '@typescript-eslint/no-floating-promises': off - '@typescript-eslint/no-misused-promises': - - off - - checksVoidReturn: false - '@typescript-eslint/unbound-method': off - '@typescript-eslint/restrict-template-expressions': off diff --git a/.husky/pre-commit b/.husky/pre-commit deleted file mode 100755 index f3a67960..00000000 --- a/.husky/pre-commit +++ /dev/null @@ -1,4 +0,0 @@ -#!/bin/sh -. "$(dirname "$0")/_/husky.sh" - -yarn pretty-quick --staged diff --git a/.postcssrc.json b/.postcssrc.json new file mode 100644 index 00000000..529e0a2b --- /dev/null +++ b/.postcssrc.json @@ -0,0 +1,5 @@ +{ + "plugins": { + "autoprefixer": {} + } +} diff --git a/package.json b/package.json index a401c2dd..3f6fc912 100644 --- a/package.json +++ b/package.json @@ -8,15 +8,16 @@ }, "author": "printempw", "license": "MIT", + "type": "module", "private": true, "scripts": { "dev": "webpack serve", "build": "webpack --env production --progress", - "lint": "eslint --ext=ts -f=beauty .", + "lint": "eslint .", "fmt": "prettier --write resources/assets tools webpack.config.ts", "fmt:check": "prettier --check resources/assets tools webpack.config.ts", "type:check": "tsc -p . --noEmit && tsc -p ./resources/assets/tests --noEmit", - "test": "jest", + "test": "node --experimental-vm-modules node_modules/.bin/jest", "build:urls": "ts-node tools/generateUrls.ts", "prepare": "husky install" }, @@ -24,89 +25,71 @@ "@emotion/react": "^11.0.0", "@emotion/styled": "^11.0.0", "@fortawesome/fontawesome-free": "^6.3.0", - "@hot-loader/react-dom": "^17.0.0", - "@tweenjs/tween.js": "^18.5.0", + "@tweenjs/tween.js": "^23.1.1", "admin-lte": "^3.2.0", "blessing-skin-shell": "^0.3.4", - "bootstrap": "^4.6.1", - "cac": "6.6.1", + "bootstrap": "^5.3.3", + "cac": "6.7.14", "cli-spinners": "^2.5.0", - "clsx": "^1.1.1", - "echarts": "^5.1.2", + "clsx": "^2.1.0", + "echarts": "^5.5.0", "events": "^3.2.0", - "immer": "^7.0.4", + "immer": "^10.0.3", "jquery": "^3.6.0", - "lodash.debounce": "^4.0.8", - "nanoid": "^3.1.9", + "lodash-es": "^4.0.8", + "nanoid": "^5.0.6", "prompts": "^2.4.0", - "react": "^17.0.1", + "react": "^18.2.0", "react-autosuggest": "^10.0.2", - "react-dom": "^17.0.1", + "react-dom": "^18.2.0", "react-draggable": "^4.4.2", - "react-hot-loader": "^4.12.21", - "react-loading-skeleton": "^2.1.1", - "react-use": "^17.4.0", + "react-loading-skeleton": "^3.4.0", + "react-use": "^17.5.0", "reaptcha": "^1.7.2", - "rxjs": "^6.5.5", - "skinview-utils": "^0.5.5", + "rxjs": "^7.8.1", + "skinview-utils": "^0.7.1", "skinview3d": "^3.0.0-alpha.1", "spectre.css": "^0.5.8", - "use-immer": "^0.4.2", - "xterm": "^4.6.0", - "xterm-addon-fit": "^0.4.0" + "use-immer": "^0.9.0", + "xterm": "^5.3.0", + "xterm-addon-fit": "^0.8.0" }, "devDependencies": { - "@gplane/tsconfig": "^4.2.0", - "@testing-library/jest-dom": "^5.11.10", - "@testing-library/react": "^11.2.6", - "@types/bootstrap": "^4.3.3", - "@types/css-minimizer-webpack-plugin": "^1.1.0", - "@types/jest": "^26.0.23", + "@testing-library/jest-dom": "^6.4.2", + "@testing-library/react": "^14.2.1", + "@tsconfig/vite-react": "^3.0.0", + "@types/bootstrap": "^5.2.10", "@types/jquery": "^3.5.13", - "@types/js-yaml": "^3.12.4", - "@types/lodash.debounce": "^4.0.6", - "@types/mini-css-extract-plugin": "^1.2.1", + "@types/js-yaml": "^4.0.9", + "@types/lodash-es": "^4.0.6", "@types/prompts": "^2.0.9", - "@types/react": "^16.9.35", - "@types/react-autosuggest": "^9.3.14", - "@types/react-dom": "^16.9.8", + "@types/react": "^18.2.57", + "@types/react-autosuggest": "^10.1.11", + "@types/react-dom": "^18.2.19", "@types/tween.js": "^18.5.0", - "@types/webpack-dev-server": "^3.11.0", - "@typescript-eslint/eslint-plugin": "^3.6.0", - "@typescript-eslint/parser": "^3.6.0", - "autoprefixer": "^10.2.6", - "css-loader": "^5.2.6", - "css-minimizer-webpack-plugin": "^3.0.1", - "eslint": "^7.4.0", - "eslint-formatter-beauty": "^3.0.0", - "eslint-plugin-react-hooks": "^4.3.0", - "html-webpack-plugin": "^5.3.1", - "husky": "^7.0.4", - "jest": "^27.0.4", - "jest-extended": "^0.11.5", - "js-yaml": "^3.13.1", - "mini-css-extract-plugin": "^1.6.0", - "postcss": "^8.3.0", - "postcss-loader": "^5.3.0", - "prettier": "^2.3.0", - "pretty-quick": "^3.1.3", - "style-loader": "^2.0.0", - "ts-jest": "^27.0.2", - "ts-loader": "^9.2.2", - "ts-node": "^10.0.0", - "typescript": "^4.3.2", - "webpack": "^5.38.1", - "webpack-cli": "^4.7.0", - "webpack-dev-server": "^3.11.2" + "@vitejs/plugin-react-swc": "^3.6.0", + "autoprefixer": "^10.4.17", + "browserslist": "^4.23.0", + "browserslist-to-esbuild": "^2.1.1", + "eslint-config-xo-react": "^0.27.0", + "eslint-plugin-react": "^7.33.2", + "eslint-plugin-react-hooks": "^4.6.0", + "husky": "^9.0.11", + "js-yaml": "^4.1.0", + "postcss": "^8.4.35", + "typescript": "^5.3.3", + "vite": "^5.1.4", + "vite-plugin-wasm": "^3.3.0", + "vitest": "^1.3.1", + "xo": "^0.57.0" }, "resolutions": { "kleur": "^4.1.3" }, "browserslist": [ - "> 1%", - "not dead", - "not ie 11", - "Chrome > 52" + "Firefox ESR", + "iOS >= 12.5", + "Chrome >= 87" ], "prettier": { "printWidth": 80, @@ -115,48 +98,46 @@ "trailingComma": "all", "tabWidth": 2 }, - "jest": { - "preset": "ts-jest", - "resetMocks": true, - "testEnvironment": "jsdom", - "moduleFileExtensions": [ - "js", - "ts", - "tsx", - "json", - "node" - ], - "moduleNameMapper": { - "\\.css$": "/resources/assets/tests/__mocks__/style.ts", - "\\.(png|webp)$": "/resources/assets/tests/__mocks__/file.ts", - "^@/(.*)$": "/resources/assets/src/$1" + "eslintConfig": { + "env": { + "es2024": true }, - "setupFilesAfterEnv": [ - "/resources/assets/tests/setup.ts" + "parserOptions": { + "ecmaVersion": "latest", + "sourceType": "module" + }, + "extends": [ + "xo", + "xo-react", + "./node_modules/xo/config/plugins.cjs" ], - "coveragePathIgnorePatterns": [ - "/node_modules/", - "/resources/assets/src/styles", - "/resources/assets/src/scripts/extra.ts", - "/resources/assets/src/scripts/urls.ts", - "/resources/assets/tests/setup", - "/resources/assets/tests/utils", - "/resources/assets/tests/scripts/cli/stdio" - ], - "testMatch": [ - "/resources/assets/tests/**/*.test.ts", - "/resources/assets/tests/**/*.test.tsx" - ], - "testPathIgnorePatterns": [ - "/node_modules/", - "/resources/assets/tests/(views|components)/.*\\.ts$" - ], - "maxWorkers": "50%", - "globals": { - "ts-jest": { - "tsconfig": "/resources/assets/tests/tsconfig.json", - "isolatedModules": true + "rules": { + "import/extensions": "off", + "import/no-named-as-default": "off", + "unicorn/filename-case": "off", + "n/file-extension-in-import": "off", + "n/prefer-global/process": "off" + }, + "overrides": [ + { + "files": [ + "*.ts", + "*.tsx" + ], + "extends": [ + "xo-typescript" + ], + "rules": { + "@typescript-eslint/consistent-type-definitions": "warn", + "@typescript-eslint/naming-convention": "warn", + "@typescript-eslint/ban-types": "off" + } } - } + ], + "ignorePatterns": [ + "dist", + "public" + ], + "root": true } } diff --git a/postcss.config.js b/postcss.config.js deleted file mode 100644 index f5815656..00000000 --- a/postcss.config.js +++ /dev/null @@ -1,5 +0,0 @@ -module.exports = { - plugins: [ - require('autoprefixer'), - ], -} diff --git a/resources/assets/src/components/Alert.tsx b/resources/assets/src/components/Alert.tsx index 722b16f2..9df55115 100644 --- a/resources/assets/src/components/Alert.tsx +++ b/resources/assets/src/components/Alert.tsx @@ -1,28 +1,29 @@ -import React from 'react' +import React from 'react'; -type AlertType = 'success' | 'info' | 'warning' | 'danger' +type AlertType = 'success' | 'info' | 'warning' | 'danger'; const icons = new Map([ - ['success', 'check'], - ['info', 'info'], - ['warning', 'exclamation-triangle'], - ['danger', 'times-circle'], -]) + ['success', 'check'], + ['info', 'info'], + ['warning', 'exclamation-triangle'], + ['danger', 'times-circle'], +]); -interface Props { - type: AlertType -} +type Properties = { + readonly type: AlertType; + readonly children?: React.ReactNode; +}; -const Alert: React.FC = (props) => { - const { type } = props - const icon = icons.get(type) +const Alert: React.FC = properties => { + const {type} = properties; + const icon = icons.get(type); - return props.children ? ( -
- - {props.children} -
- ) : null -} + return properties.children ? ( +
+ + {properties.children} +
+ ) : null; +}; -export default Alert +export default Alert; diff --git a/resources/assets/src/components/ButtonEdit.tsx b/resources/assets/src/components/ButtonEdit.tsx index 83c055aa..1df5169d 100644 --- a/resources/assets/src/components/ButtonEdit.tsx +++ b/resources/assets/src/components/ButtonEdit.tsx @@ -1,14 +1,14 @@ -import React from 'react' +import React from 'react'; -interface Props { - title?: string - onClick: React.MouseEventHandler -} +type Properties = { + readonly title?: string; + readonly onClick: React.MouseEventHandler; +}; -const ButtonEdit: React.FC = (props) => ( - - - -) +const ButtonEdit: React.FC = properties => ( + + + +); -export default ButtonEdit +export default ButtonEdit; diff --git a/resources/assets/src/components/Captcha.tsx b/resources/assets/src/components/Captcha.tsx index 33c710e5..4d3f2015 100644 --- a/resources/assets/src/components/Captcha.tsx +++ b/resources/assets/src/components/Captcha.tsx @@ -1,103 +1,104 @@ /** @jsxImportSource @emotion/react */ -import * as 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 * as React from 'react'; +import Reaptcha from 'reaptcha'; +import {emit, on} from '@/scripts/event'; +import {t} from '@/scripts/i18n'; +import * as cssUtils from '@/styles/utils'; -const eventId = Symbol() +const eventId = Symbol(); type State = { - value: string - time: number - sitekey: string - invisible: boolean -} + value: string; + time: number; + sitekey: string; + invisible: boolean; +}; class Captcha extends React.Component, State> { - state: State - ref: React.MutableRefObject + state: State; + ref: React.MutableRefObject; - constructor(props: Record) { - super(props) - this.state = { - value: '', - time: Date.now(), - sitekey: blessing.extra.recaptcha, - invisible: blessing.extra.invisible, - } - this.ref = React.createRef() - } + constructor(properties: Record) { + super(properties); + this.state = { + value: '', + time: Date.now(), + sitekey: blessing.extra.recaptcha, + invisible: blessing.extra.invisible, + }; + this.ref = React.createRef(); + } - execute = async () => { - const recaptcha = this.ref.current - if (recaptcha && this.state.invisible) { - return new Promise((resolve) => { - const off = on(eventId, (value: string) => { - resolve(value) - off() - }) - recaptcha.execute() - }) - } - return this.state.value - } + execute = async () => { + const recaptcha = this.ref.current; + if (recaptcha && this.state.invisible) { + return new Promise(resolve => { + const off = on(eventId, (value: string) => { + resolve(value); + off(); + }); + recaptcha.execute(); + }); + } - reset = () => { - const recaptcha = this.ref.current - if (recaptcha) { - recaptcha.reset() - } else { - this.setState({ time: Date.now() }) - } - } + return this.state.value; + }; - handleValueChange = (event: React.ChangeEvent) => { - this.setState({ value: event.target.value }) - } + reset = () => { + const recaptcha = this.ref.current; + if (recaptcha) { + recaptcha.reset(); + } else { + this.setState({time: Date.now()}); + } + }; - handleVerify = (value: string) => { - emit(eventId, value) - this.setState({ value }) - } + handleValueChange = (event: React.ChangeEvent) => { + this.setState({value: event.target.value}); + }; - handleRefresh = () => { - this.setState({ time: Date.now() }) - } + handleVerify = (value: string) => { + emit(eventId, value); + this.setState({value}); + }; - render() { - return this.state.sitekey ? ( -
- -
- ) : ( -
-
- -
- {t('auth.captcha')} -
- ) - } + handleRefresh = () => { + this.setState({time: Date.now()}); + }; + + render() { + return this.state.sitekey ? ( +
+ +
+ ) : ( +
+
+ +
+ {t('auth.captcha')} +
+ ); + } } -export default Captcha +export default Captcha; diff --git a/resources/assets/src/components/DarkModeButton.tsx b/resources/assets/src/components/DarkModeButton.tsx index 6ba04d2f..942c4bc0 100644 --- a/resources/assets/src/components/DarkModeButton.tsx +++ b/resources/assets/src/components/DarkModeButton.tsx @@ -1,27 +1,27 @@ -import React, { useState } from 'react' -import * as fetch from '@/scripts/net' +import React, {useState} from 'react'; +import * as fetch from '@/scripts/net'; -interface Props { - initMode: boolean -} +type Properties = { + readonly initMode: boolean; +}; -const DarkModeButton: React.FC = ({ initMode }) => { - const [darkMode, setDarkMode] = useState(initMode) +const DarkModeButton: React.FC = ({initMode}) => { + const [darkMode, setDarkMode] = useState(initMode); - const icon = darkMode ? 'moon' : 'sun' + const icon = darkMode ? 'moon' : 'sun'; - const handleClick = async () => { - setDarkMode((value) => !value) + const handleClick = async () => { + setDarkMode(value => !value); - await fetch.put('/user/dark-mode') - document.body.classList.toggle('dark-mode') - } + await fetch.put('/user/dark-mode'); + document.body.classList.toggle('dark-mode'); + }; - return ( - - - - ) -} + return ( + + + + ); +}; -export default DarkModeButton +export default DarkModeButton; diff --git a/resources/assets/src/components/EmailSuggestion.tsx b/resources/assets/src/components/EmailSuggestion.tsx index 9ecfe080..e75d2f97 100644 --- a/resources/assets/src/components/EmailSuggestion.tsx +++ b/resources/assets/src/components/EmailSuggestion.tsx @@ -1,89 +1,89 @@ /** @jsxImportSource @emotion/react */ -import React, { 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 React, {useState, useEffect} from 'react'; +import Autosuggest from 'react-autosuggest'; +import {css} from '@emotion/react'; +import {emit} from '@/scripts/event'; +import {pointerCursor} from '@/styles/utils'; const styles = css` .dropdown-menu li { ${pointerCursor} } -` +`; -const domainNames = new Set(['qq.com', '163.com', 'gmail.com', 'hotmail.com']) +const domainNames = new Set(['qq.com', '163.com', 'gmail.com', 'hotmail.com']); -type Props = Omit, 'onChange'> & { - onChange(value: string): void -} +type Properties = Omit, 'onChange'> & { + onChange(value: string): void; +}; -const EmailSuggestion: React.FC = (props) => { - const [suggestions, setSuggestions] = useState([]) +const EmailSuggestion: React.FC = properties => { + const [suggestions, setSuggestions] = useState([]); - useEffect(() => { - emit('emailDomainsSuggestion', domainNames) - }, []) + useEffect(() => { + emit('emailDomainsSuggestion', domainNames); + }, []); - const handleSuggestionsFetchRequested: Autosuggest.SuggestionsFetchRequested = - ({ value }) => { - const segments = value.split('@') - setSuggestions([...domainNames].map((name) => `${segments[0]}@${name}`)) - } + const handleSuggestionsFetchRequested: Autosuggest.SuggestionsFetchRequested + = ({value}) => { + const segments = value.split('@'); + setSuggestions([...domainNames].map(name => `${segments[0]}@${name}`)); + }; - const handleSuggestionsClearRequested = () => { - setSuggestions([]) - } + const handleSuggestionsClearRequested = () => { + setSuggestions([]); + }; - const shouldRenderSuggestions = (value: string) => { - const isSelecting = [...domainNames].some((name) => - value.endsWith(`@${name}`), - ) + const shouldRenderSuggestions = (value: string) => { + const isSelecting = [...domainNames].some(name => + value.endsWith(`@${name}`), + ); - return isSelecting || (value.length > 0 && !value.includes('@')) - } + return isSelecting || (value.length > 0 && !value.includes('@')); + }; - const getSuggestionValue = (value: string) => value + const getSuggestionValue = (value: string) => value; - const renderSuggestion = (suggestion: string) => suggestion + const renderSuggestion = (suggestion: string) => suggestion; - const handleChange = (_: React.FormEvent, event: Autosuggest.ChangeEvent) => { - props.onChange(event.newValue) - } + const handleChange = (_: React.FormEvent, event: Autosuggest.ChangeEvent) => { + properties.onChange(event.newValue); + }; - const renderInputComponent = ( - props: Omit, 'onChange'>, - ) => ( -
- -
-
- -
-
-
- ) + const renderInputComponent = ( + properties_: Omit, 'onChange'>, + ) => ( +
+ +
+
+ +
+
+
+ ); - return ( -
- -
- ) -} + return ( +
+ 0 ? 'show' : ''}`, + suggestionHighlighted: 'active', + }} + onSuggestionsFetchRequested={handleSuggestionsFetchRequested} + onSuggestionsClearRequested={handleSuggestionsClearRequested} + /> +
+ ); +}; -export default EmailSuggestion +export default EmailSuggestion; diff --git a/resources/assets/src/components/FileInput.tsx b/resources/assets/src/components/FileInput.tsx index cf6f2164..29f4f8f6 100644 --- a/resources/assets/src/components/FileInput.tsx +++ b/resources/assets/src/components/FileInput.tsx @@ -1,53 +1,53 @@ /** @jsxImportSource @emotion/react */ -import { useRef } from 'react' -import { css } from '@emotion/react' -import { t } from '@/scripts/i18n' +import {useRef} from 'react'; +import {css} from '@emotion/react'; +import {t} from '@/scripts/i18n'; const hideRawBrowseButton = css` ::after { display: none; } -` +`; -interface Props { - file: File | null - accept?: string - onChange(event: React.ChangeEvent): void -} +type Properties = { + file: File | undefined; + accept?: string; + onChange(event: React.ChangeEvent): void; +}; -const FileInput: React.FC = (props) => { - const ref = useRef(null) +const FileInput: React.FC = properties => { + const reference = useRef(null); - const handleClick = () => { - ref.current!.click() - } + const handleClick = () => { + reference.current!.click(); + }; - return ( -
- -
-
- - -
-
- -
-
-
- ) -} + return ( +
+ +
+
+ + +
+
+ +
+
+
+ ); +}; -export default FileInput +export default FileInput; diff --git a/resources/assets/src/components/Loading.tsx b/resources/assets/src/components/Loading.tsx index 980ba246..ad647978 100644 --- a/resources/assets/src/components/Loading.tsx +++ b/resources/assets/src/components/Loading.tsx @@ -1,9 +1,9 @@ -import React from 'react' +function Loading() { + return ( +
+ +
+ ); +} -const Loading = () => ( -
- -
-) - -export default Loading +export default Loading; diff --git a/resources/assets/src/components/Modal.tsx b/resources/assets/src/components/Modal.tsx index e2c8246c..3cbeccad 100644 --- a/resources/assets/src/components/Modal.tsx +++ b/resources/assets/src/components/Modal.tsx @@ -1,165 +1,164 @@ -import React, { useState, useEffect, useRef } from 'react' -import $ from 'jquery' -import 'bootstrap' -import { t } from '../scripts/i18n' -import ModalHeader from './ModalHeader' -import ModalBody from './ModalBody' -import ModalFooter from './ModalFooter' -import type { Props as HeaderProps } from './ModalHeader' -import type { Props as BodyProps } from './ModalBody' -import type { Props as FooterProps } from './ModalFooter' +import React, {useState, useEffect, useRef} from 'react'; +import $ from 'jquery'; +import 'bootstrap'; +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'; type BasicOptions = { - mode?: 'alert' | 'confirm' | 'prompt' - show?: boolean - input?: string - validator?(value: any): string | boolean | undefined - type?: string - showHeader?: boolean - center?: boolean - children?: React.ReactNode -} + readonly mode?: 'alert' | 'confirm' | 'prompt'; + readonly show?: boolean; + readonly input?: string; + validator?(value: any): string | boolean | undefined; + readonly type?: string; + readonly showHeader?: boolean; + readonly center?: boolean; + children?: React.ReactNode; +}; -export type ModalOptions = BasicOptions & HeaderProps & BodyProps & FooterProps +export type ModalOptions = BasicOptions & HeaderProperties & BodyProperties & FooterProperties; -type Props = { - id?: string - children?: React.ReactNode - footer?: React.ReactNode - onConfirm?(payload: { value: string }): void - onDismiss?(): void - onClose?(): void -} +type Properties = { + readonly id?: string; + readonly children?: React.ReactNode; + readonly footer?: React.ReactNode; + onConfirm?(payload: {value: string}): void; + onDismiss?(): void; + onClose?(): void; +}; export type ModalResult = { - value: string -} + value: string; +}; -const Modal: React.FC = (props) => { - const { - mode = 'confirm', - title = t('general.tip'), - text = '', - input = '', - placeholder = '', - inputType = 'text', - inputMode, - type = 'default', - showHeader = true, - center = false, - okButtonText = t('general.confirm'), - okButtonType = 'primary', - cancelButtonText = t('general.cancel'), - cancelButtonType = 'secondary', - flexFooter = false, - } = props +const Modal: React.FC = properties => { + const { + mode = 'confirm', + title = t('general.tip'), + text = '', + input = '', + placeholder = '', + inputType = 'text', + inputMode, + type = 'default', + showHeader = true, + center = false, + okButtonText = t('general.confirm'), + okButtonType = 'primary', + cancelButtonText = t('general.cancel'), + cancelButtonType = 'secondary', + flexFooter = false, + } = properties; - const [value, setValue] = useState(input) - const [valid, setValid] = useState(true) - const [validatorMessage, setValidatorMessage] = useState('') - const ref = useRef(null) + const [value, setValue] = useState(input); + const [valid, setValid] = useState(true); + const [validatorMessage, setValidatorMessage] = useState(''); + const reference = useRef(null); - const { show } = props + const {show, onClose} = properties; - useEffect(() => { - if (!show) { - return - } + useEffect(() => { + if (!show) { + return; + } - const onHidden = () => props.onClose?.() + const onHidden = () => { + onClose ? onClose() : void 0; + }; - const el = $(ref.current!) - el.on('hidden.bs.modal', onHidden) + const element = $(reference.current!); + element.on('hidden.bs.modal', onHidden); - return () => { - el.off('hidden.bs.modal', onHidden) - } - }, [show, props.onClose]) + return () => { + element.off('hidden.bs.modal', onHidden); + }; + }, [show, onClose]); - const handleInputChange = (event: React.ChangeEvent) => { - setValue(event.target.value) - } + const handleInputChange = (event: React.ChangeEvent) => { + setValue(event.target.value); + }; - const confirm = () => { - const { validator } = props - if (typeof validator === 'function') { - const result = validator(value) - if (typeof result === 'string') { - setValidatorMessage(result) - setValid(false) - return - } - } + const confirm = () => { + const {validator} = properties; + if (typeof validator === 'function') { + const result = validator(value); + if (typeof result === 'string') { + setValidatorMessage(result); + setValid(false); + return; + } + } - props.onConfirm?.({ value }) - $(ref.current!).modal('hide') + properties.onConfirm?.({value}); + $(reference.current!).modal('hide'); - // The "hidden.bs.modal" event can't be trigged automatically when testing. - /* istanbul ignore next */ - if (process.env.NODE_ENV === 'test') { - $(ref.current!).trigger('hidden.bs.modal') - } - } + // The "hidden.bs.modal" event can't be trigged automatically when testing. + /* istanbul ignore next */ + if (process.env.NODE_ENV === 'test') { + $(reference.current!).trigger('hidden.bs.modal'); + } + }; - const dismiss = () => { - props.onDismiss?.() - $(ref.current!).modal('hide') + const dismiss = () => { + properties.onDismiss?.(); + $(reference.current!).modal('hide'); - /* istanbul ignore next */ - if (process.env.NODE_ENV === 'test') { - $(ref.current!).trigger('hidden.bs.modal') - } - } + /* istanbul ignore next */ + if (process.env.NODE_ENV === 'test') { + $(reference.current!).trigger('hidden.bs.modal'); + } + }; - useEffect(() => { - if (show) { - setTimeout(() => $(ref.current!).modal('show'), 50) - } - }, [show]) + useEffect(() => { + if (show) { + setTimeout(() => $(reference.current!).modal('show'), 50); + } + }, [show]); - if (!show) { - return null - } + if (!show) { + return null; + } - return ( - - ) -} + return ( + + ); +}; -export default Modal +export default Modal; diff --git a/resources/assets/src/components/ModalBody.tsx b/resources/assets/src/components/ModalBody.tsx index 9b38b366..5fff86cc 100644 --- a/resources/assets/src/components/ModalBody.tsx +++ b/resources/assets/src/components/ModalBody.tsx @@ -1,29 +1,24 @@ -import React from 'react' -import ModalContent from './ModalContent' -import ModalInput from './ModalInput' -import type { Props as ContentProps } from './ModalContent' -import type { - Props as InputProps, - InternalProps as InputInteralProps, -} from './ModalInput' +import React from 'react'; +import ModalContent, {type Props as ContentProperties} from './ModalContent'; +import ModalInput, { + type + Props as InputProperties, type + InternalProps as InputInteralProperties, +} from './ModalInput'; -interface InternalProps { - showInput: boolean -} +type InternalProperties = { + readonly showInput: boolean; +}; -export type Props = ContentProps & InputProps +export type Props = ContentProperties & InputProperties; -const ModalBody: React.FC = ( - props, -) => { - return ( -
- - {props.children} - - {props.showInput && } -
- ) -} +const ModalBody: React.FC = properties => ( +
+ + {properties.children} + + {properties.showInput && } +
+); -export default ModalBody +export default ModalBody; diff --git a/resources/assets/src/components/ModalContent.tsx b/resources/assets/src/components/ModalContent.tsx index 2d03d188..71f13b91 100644 --- a/resources/assets/src/components/ModalContent.tsx +++ b/resources/assets/src/components/ModalContent.tsx @@ -1,26 +1,31 @@ -import React from 'react' +import React from 'react'; -export interface Props { - text?: string - dangerousHTML?: string -} +export type Props = { + readonly text?: string; + readonly dangerousHTML?: string; + readonly children?: React.ReactNode; +}; -const ModalContent: React.FC = (props) => { - if (props.children) { - return <>{props.children} - } else if (props.text) { - return ( - <> - {props.text.split(/\r?\n/).map((line, i) => ( -

{line}

- ))} - - ) - } else if (props.dangerousHTML) { - return
- } +const ModalContent: React.FC = properties => { + if (properties.children) { + return <>{properties.children}; + } - return <> -} + if (properties.text) { + return ( + <> + {properties.text.split(/\r?\n/).map((line, i) => ( +

{line}

+ ))} + + ); + } -export default ModalContent + if (properties.dangerousHTML) { + return
; + } + + return <>; +}; + +export default ModalContent; diff --git a/resources/assets/src/components/ModalFooter.tsx b/resources/assets/src/components/ModalFooter.tsx index 63084f90..cb746cbc 100644 --- a/resources/assets/src/components/ModalFooter.tsx +++ b/resources/assets/src/components/ModalFooter.tsx @@ -1,49 +1,51 @@ -import React from 'react' +import React from 'react'; -export interface Props { - flexFooter?: boolean - okButtonText?: string - okButtonType?: string - cancelButtonText?: string - cancelButtonType?: string -} +export type Props = { + readonly flexFooter?: boolean; + readonly okButtonText?: string; + readonly okButtonType?: string; + readonly cancelButtonText?: string; + readonly cancelButtonType?: string; + readonly children?: React.ReactNode; +}; -interface InternalProps { - showCancelButton: boolean - onConfirm?(): void - onDismiss?(): void -} +type InternalProperties = { + readonly showCancelButton: boolean; + onConfirm?(): void; + onDismiss?(): void; +}; -const ModalFooter: React.FC = (props) => { - const classes = ['modal-footer'] - if (props.flexFooter) { - classes.push('d-flex', 'justify-content-between') - } - const footerClass = classes.join(' ') +const ModalFooter: React.FC = properties => { + const classes = ['modal-footer']; + if (properties.flexFooter) { + classes.push('d-flex', 'justify-content-between'); + } - return props.children ? ( -
{props.children}
- ) : ( -
- {props.showCancelButton && ( - - )} - -
- ) -} + const footerClass = classes.join(' '); -export default ModalFooter + return properties.children ? ( +
{properties.children}
+ ) : ( +
+ {properties.showCancelButton && ( + + )} + +
+ ); +}; + +export default ModalFooter; diff --git a/resources/assets/src/components/ModalHeader.tsx b/resources/assets/src/components/ModalHeader.tsx index 761daf07..e2f267f7 100644 --- a/resources/assets/src/components/ModalHeader.tsx +++ b/resources/assets/src/components/ModalHeader.tsx @@ -1,28 +1,28 @@ -import React from 'react' +import React from 'react'; -export interface Props { - title?: string -} +export type Props = { + readonly title?: string; +}; -interface InternalProps { - onDismiss?(): void - show?: boolean -} +type InternalProperties = { + onDismiss?(): void; + readonly show?: boolean; +}; -const ModalHeader: React.FC = (props) => - props.show ? ( -
-
{props.title}
- -
- ) : null +const ModalHeader: React.FC = properties => + properties.show ? ( +
+
{properties.title}
+ +
+ ) : null; -export default ModalHeader +export default ModalHeader; diff --git a/resources/assets/src/components/ModalInput.tsx b/resources/assets/src/components/ModalInput.tsx index d155f8c0..786d19b6 100644 --- a/resources/assets/src/components/ModalInput.tsx +++ b/resources/assets/src/components/ModalInput.tsx @@ -1,58 +1,58 @@ -import React, { HTMLAttributes } from 'react' +import React, {type HTMLAttributes} from 'react'; -export interface Props { - inputType?: string - inputMode?: HTMLAttributes['inputMode'] - choices?: { text: string; value: string }[] - placeholder?: string -} +export type Props = { + readonly inputType?: string; + readonly inputMode?: HTMLAttributes['inputMode']; + readonly choices?: Array<{text: string; value: string}>; + readonly placeholder?: string; +}; -export interface InternalProps { - value?: string - invalid?: boolean - validatorMessage?: string - onChange?: React.ChangeEventHandler -} +export type InternalProps = { + readonly value?: string; + readonly invalid?: boolean; + readonly validatorMessage?: string; + readonly onChange?: React.ChangeEventHandler; +}; -const ModalInput: React.FC = (props) => ( - <> - {props.inputType === 'radios' && props.choices ? ( - <> - {props.choices.map((choice) => ( -
- - -
- ))} - - ) : ( -
- -
- )} - {props.invalid && ( -
- - {props.validatorMessage} -
- )} - -) +const ModalInput: React.FC = properties => ( + <> + {properties.inputType === 'radios' && properties.choices ? ( + <> + {properties.choices.map(choice => ( +
+ + +
+ ))} + + ) : ( +
+ +
+ )} + {properties.invalid && ( +
+ + {properties.validatorMessage} +
+ )} + +); -export default ModalInput +export default ModalInput; diff --git a/resources/assets/src/components/Pagination.tsx b/resources/assets/src/components/Pagination.tsx index 2888177c..bbe482b6 100644 --- a/resources/assets/src/components/Pagination.tsx +++ b/resources/assets/src/components/Pagination.tsx @@ -1,124 +1,124 @@ -import React from 'react' -import { t } from '@/scripts/i18n' -import PaginationItem from './PaginationItem' +import React from 'react'; +import PaginationItem from './PaginationItem'; +import {t} from '@/scripts/i18n'; -interface Props { - page: number - totalPages: number - onChange(page: number): void | Promise -} +type Properties = { + readonly page: number; + readonly totalPages: number; + onChange(page: number): void | Promise; +}; const labels = { - prev: '‹', - next: '›', -} + prev: '‹', + next: '›', +}; -const Pagination: React.FC = (props) => { - const { page, totalPages, onChange } = props +const Pagination: React.FC = properties => { + const {page, totalPages, onChange} = properties; - if (totalPages < 1) { - return null - } + if (totalPages < 1) { + return null; + } - return ( -
    - onChange(page - 1)} - > - {labels.prev} - - {t('vendor.datatable.prev')} - - - {totalPages < 8 ? ( - Array.from({ length: totalPages }).map((_, i) => ( - onChange(i + 1)} - > - {i + 1} - - )) - ) : ( - <> - {page < 4 ? ( - [1, 2, 3, 4].map((n) => ( - onChange(n)} - > - {n} - - )) - ) : ( - onChange(1)} - > - 1 - - )} - - ... - - {page > 3 && page < totalPages - 2 && ( - <> - {[page - 1, page, page + 1].map((n) => ( - onChange(n)} - > - {n} - - ))} - - ... - - - )} - {totalPages - page < 3 ? ( - [totalPages - 3, totalPages - 2, totalPages - 1, totalPages].map( - (n) => ( - onChange(n)} - > - {n} - - ), - ) - ) : ( - onChange(totalPages)} - > - {totalPages} - - )} - - )} - onChange(page + 1)} - > - - {t('vendor.datatable.next')} - - {labels.next} - -
- ) -} + return ( +
    + onChange(page - 1)} + > + {labels.prev} + + {t('vendor.datatable.prev')} + + + {totalPages < 8 ? ( + Array.from({length: totalPages}).map((_, i) => ( + onChange(i + 1)} + > + {i + 1} + + )) + ) : ( + <> + {page < 4 ? ( + [1, 2, 3, 4].map(n => ( + onChange(n)} + > + {n} + + )) + ) : ( + onChange(1)} + > + 1 + + )} + + ... + + {page > 3 && page < totalPages - 2 && ( + <> + {[page - 1, page, page + 1].map(n => ( + onChange(n)} + > + {n} + + ))} + + ... + + + )} + {totalPages - page < 3 ? ( + [totalPages - 3, totalPages - 2, totalPages - 1, totalPages].map( + n => ( + onChange(n)} + > + {n} + + ), + ) + ) : ( + onChange(totalPages)} + > + {totalPages} + + )} + + )} + onChange(page + 1)} + > + + {t('vendor.datatable.next')} + + {labels.next} + +
+ ); +}; -export default Pagination +export default Pagination; diff --git a/resources/assets/src/components/PaginationItem.tsx b/resources/assets/src/components/PaginationItem.tsx index fe75ec1c..f914d4f0 100644 --- a/resources/assets/src/components/PaginationItem.tsx +++ b/resources/assets/src/components/PaginationItem.tsx @@ -1,39 +1,42 @@ -import React from 'react' +import React from 'react'; -interface Props { - disabled?: boolean - active?: boolean - title?: string - className?: string - onClick?(): void -} +type Properties = { + readonly disabled?: boolean; + readonly active?: boolean; + readonly title?: string; + readonly className?: string; + onClick?(): void; + readonly children?: React.ReactNode; +}; -const PaginationItem: React.FC = (props) => { - const classes = ['page-item'] - if (props.active) { - classes.push('active') - } - if (props.disabled) { - classes.push('disabled') - } - if (props.className) { - classes.push(props.className) - } +const PaginationItem: React.FC = properties => { + const classes = ['page-item']; + if (properties.active) { + classes.push('active'); + } - const handleClick = (event: React.MouseEvent) => { - event.preventDefault() - if (!props.disabled && props.onClick) { - props.onClick() - } - } + if (properties.disabled) { + classes.push('disabled'); + } - return ( -
  • - - {props.children} - -
  • - ) -} + if (properties.className) { + classes.push(properties.className); + } -export default PaginationItem + const handleClick = (event: React.MouseEvent) => { + event.preventDefault(); + if (!properties.disabled && properties.onClick) { + properties.onClick(); + } + }; + + return ( +
  • + + {properties.children} + +
  • + ); +}; + +export default PaginationItem; diff --git a/resources/assets/src/components/Toast.tsx b/resources/assets/src/components/Toast.tsx index a20a9649..8aafae48 100644 --- a/resources/assets/src/components/Toast.tsx +++ b/resources/assets/src/components/Toast.tsx @@ -1,21 +1,22 @@ /** @jsxImportSource @emotion/react */ -import React, { useState, useEffect } from 'react' -import { css } from '@emotion/react' +import React, {useState, useEffect} from 'react'; +import {css} from '@emotion/react'; -export type ToastType = 'success' | 'info' | 'warning' | 'error' +export type ToastType = 'success' | 'info' | 'warning' | 'error'; -interface Props { - type: ToastType - distance: number - onClose(): void | Promise -} +type Properties = { + readonly type: ToastType; + readonly distance: number; + onClose(): void | Promise; + readonly children: React.ReactNode; +}; const icons = new Map([ - ['success', 'check'], - ['info', 'info'], - ['warning', 'exclamation-triangle'], - ['error', 'times-circle'], -]) + ['success', 'check'], + ['info', 'info'], + ['warning', 'exclamation-triangle'], + ['error', 'times-circle'], +]); const wrapper = css` position: fixed; @@ -24,52 +25,54 @@ const wrapper = css` z-index: 1050; transition-property: top; transition-duration: 0.3s; -` +`; const shadow = css` box-shadow: 0 0.25rem 0.75rem rgba(0, 0, 0, 0.1); -` +`; -const Toast: React.FC = (props) => { - const [show, setShow] = useState(false) +const Toast: React.FC = properties => { + const [show, setShow] = useState(false); - useEffect(() => { - const timer = setTimeout(() => setShow(true), 100) + useEffect(() => { + const timer = setTimeout(() => { + setShow(true); + }, 100); - return () => { - clearTimeout(timer) - } - }, [props.onClose]) + return () => { + clearTimeout(timer); + }; + }, [properties.onClose]); - const type = props.type === 'error' ? 'danger' : props.type + const type = properties.type === 'error' ? 'danger' : properties.type; - const classes = [ - `alert alert-${type}`, - 'd-flex justify-content-between', - 'fade', - ] - if (show) { - classes.push('show') - } + const classes = [ + `alert alert-${type}`, + 'd-flex justify-content-between', + 'fade', + ]; + if (show) { + classes.push('show'); + } - const role = type === 'success' || type === 'info' ? 'status' : 'alert' + const role = type === 'success' || type === 'info' ? 'status' : 'alert'; - return ( -
    -
    - - - - {props.children} - -
    -
    - ) -} + return ( +
    +
    + + + + {properties.children} + +
    +
    + ); +}; -export default Toast +export default Toast; diff --git a/resources/assets/src/components/Viewer.tsx b/resources/assets/src/components/Viewer.tsx index 923fe480..65ebbcd7 100644 --- a/resources/assets/src/components/Viewer.tsx +++ b/resources/assets/src/components/Viewer.tsx @@ -1,38 +1,39 @@ /** @jsxImportSource @emotion/react */ -import React, { useState, useEffect, useRef } from 'react' -import { useMeasure } from 'react-use' -import { css } from '@emotion/react' -import styled from '@emotion/styled' -import * as skinview3d from 'skinview3d' -import { t } from '@/scripts/i18n' -import * as cssUtils from '@/styles/utils' -import * as breakpoints from '@/styles/breakpoints' -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' -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 React, {useState, useEffect, useRef} from 'react'; +import {useMeasure} from 'react-use'; +import {css} from '@emotion/react'; +import styled from '@emotion/styled'; +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'; +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'; -const backgrounds = [bg1, bg2, bg3, bg4, bg5, bg6, bg7] -export const PICTURES_COUNT = backgrounds.length +const backgrounds = [bg1, bg2, bg3, bg4, bg5, bg6, bg7]; +export const PICTURES_COUNT = backgrounds.length; -interface Props { - skin?: string - cape?: string - isAlex: boolean - showIndicator?: boolean - initPositionZ?: number -} +type Properties = { + readonly skin?: string; + readonly cape?: string; + readonly children?: React.ReactNode; + readonly isAlex: boolean; + readonly showIndicator?: boolean; + readonly initPositionZ?: number; +}; const animationFactories = [ - () => new skinview3d.WalkingAnimation(), - () => new skinview3d.RunningAnimation(), - () => new skinview3d.FlyingAnimation(), - () => new skinview3d.IdleAnimation(), -] + () => new skinview3d.WalkingAnimation(), + () => new skinview3d.RunningAnimation(), + () => new skinview3d.FlyingAnimation(), + () => new skinview3d.IdleAnimation(), +]; const ActionButton = styled.i` display: inline; @@ -41,7 +42,7 @@ const ActionButton = styled.i` color: #555; cursor: pointer; } -` +`; const cssViewer = css` flex: 1 1 auto; @@ -56,251 +57,256 @@ const cssViewer = css` display: flex; justify-content: center; } -` +`; -const Viewer: React.FC = (props) => { - const { initPositionZ = 70 } = props +const Viewer: React.FC = properties => { + const {initPositionZ = 70} = properties; - const viewRef: React.MutableRefObject = useRef(null!) - const containerRef = useRef(null) + const viewReference: React.MutableRefObject = useRef(null!); + const containerReference = useRef(null); - const [paused, setPaused] = useState(false) - const [animation, setAnimation] = useState(0) - const [bgPicture, setBgPicture] = useState(-1) + const [paused, setPaused] = useState(false); + const [animation, setAnimation] = useState(0); + const [bgPicture, setBgPicture] = useState(-1); - const indicator = (() => { - const { skin, cape } = props - if (skin && cape) { - return `${t('general.skin')} & ${t('general.cape')}` - } else if (skin) { - return t('general.skin') - } else if (cape) { - return t('general.cape') - } - return '' - })() + const indicator = (() => { + const {skin, cape} = properties; + if (skin && cape) { + return `${t('general.skin')} & ${t('general.cape')}`; + } - useEffect(() => { - const container = containerRef.current! - const viewer = new skinview3d.SkinViewer({ - canvas: container, - width: container.clientWidth, - height: container.clientHeight, - skin: props.skin || SkinSteve, - cape: props.cape || undefined, - model: props.isAlex ? 'slim' : 'default', - zoom: initPositionZ / 100, - }) - viewer.autoRotate = true + if (skin) { + return t('general.skin'); + } - if (document.body.classList.contains('dark-mode')) { - viewer.background = '#6c757d' - } + if (cape) { + return t('general.cape'); + } - viewRef.current = viewer + return ''; + })(); - return () => { - viewer.dispose() - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []) + useEffect(() => { + const container = containerReference.current!; + const viewer = new skinview3d.SkinViewer({ + canvas: container, + width: container.clientWidth, + height: container.clientHeight, + skin: properties.skin || SkinSteve, + cape: properties.cape || undefined, + model: properties.isAlex ? 'slim' : 'default', + zoom: initPositionZ / 100, + }); + viewer.autoRotate = true; - const [containerWrapperRef, containerMeasure] = useMeasure() - useEffect(() => { - viewRef.current.setSize(containerMeasure.width, containerMeasure.height) - }) + if (document.body.classList.contains('dark-mode')) { + viewer.background = '#6c757d'; + } - useEffect(() => { - const viewer = viewRef.current - viewer.loadSkin(props.skin || SkinSteve, { - model: props.isAlex ? 'slim' : 'default', - }) - }, [props.skin, props.isAlex]) + viewReference.current = viewer; - useEffect(() => { - const viewer = viewRef.current - if (props.cape) { - viewer.loadCape(props.cape) - } else { - viewer.resetCape() - } - }, [props.cape]) + return () => { + viewer.dispose(); + }; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); - useEffect(() => { - const viewer = viewRef.current - const factory = animationFactories[animation] - if (factory === undefined) { - viewer.animation = null - } else { - const newAnimation = factory() - newAnimation.paused = paused // Perseve `paused` state - viewer.animation = newAnimation - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [animation]) + const [containerWrapperReference, containerMeasure] = useMeasure(); + useEffect(() => { + viewReference.current.setSize(containerMeasure.width, containerMeasure.height); + }); - useEffect(() => { - const currentAnimation = viewRef.current.animation - if (currentAnimation !== null) { - currentAnimation.paused = paused - } - }, [paused]) + useEffect(() => { + const viewer = viewReference.current; + viewer.loadSkin(properties.skin || SkinSteve, { + model: properties.isAlex ? 'slim' : 'default', + }); + }, [properties.skin, properties.isAlex]); - useEffect(() => { - const viewer = viewRef.current - const backgroundUrl = backgrounds[bgPicture] - if (backgroundUrl === undefined) { - viewer.background = null - } else { - viewer.loadBackground(backgroundUrl) - } - }, [bgPicture]) + useEffect(() => { + const viewer = viewReference.current; + if (properties.cape) { + viewer.loadCape(properties.cape); + } else { + viewer.resetCape(); + } + }, [properties.cape]); - const togglePause = () => { - setPaused((paused) => { - if (paused) { - return false - } else { - viewRef.current.autoRotate = false - return true - } - }) - } + useEffect(() => { + const viewer = viewReference.current; + const factory = animationFactories[animation]; + if (factory === undefined) { + viewer.animation = null; + } else { + const newAnimation = factory(); + newAnimation.paused = paused; // Perseve `paused` state + viewer.animation = newAnimation; + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [animation]); - const toggleAnimation = () => { - setAnimation((index) => (index + 1) % animationFactories.length) - setPaused(false) - } + useEffect(() => { + const currentAnimation = viewReference.current.animation; + if (currentAnimation !== null) { + currentAnimation.paused = paused; + } + }, [paused]); - const toggleRotate = () => { - const viewer = viewRef.current - viewer.autoRotate = !viewer.autoRotate - } + useEffect(() => { + const viewer = viewReference.current; + const backgroundUrl = backgrounds[bgPicture]; + if (backgroundUrl === undefined) { + viewer.background = null; + } else { + viewer.loadBackground(backgroundUrl); + } + }, [bgPicture]); - const toggleBackEquippment = () => { - const player = viewRef.current.playerObject - if (player.backEquipment === 'cape') { - player.backEquipment = 'elytra' - } else { - player.backEquipment = 'cape' - } - } + const togglePause = () => { + setPaused(paused => { + if (paused) { + return false; + } - const setWhite = () => { - viewRef.current.background = '#fff' - } - const setGray = () => { - viewRef.current.background = '#6c757d' - } - const setBlack = () => { - viewRef.current.background = '#000' - } - const setPrevPicture = () => { - setBgPicture((index) => { - if (bgPicture <= 0) { - return PICTURES_COUNT - 1 - } else { - return index - 1 - } - }) - } - const setNextPicture = () => { - setBgPicture((index) => { - if (bgPicture >= PICTURES_COUNT - 1) { - return 0 - } else { - return index + 1 - } - }) - } + viewReference.current.autoRotate = false; + return true; + }); + }; - return ( -
    -
    -
    -

    - {t('general.texturePreview')} - {props.showIndicator && ( - {indicator} - )} -

    -
    - - - - -
    -
    -
    -
    - -
    -
    -
    -
    -
    -
    -
    - -
    -
    - -
    -
    - {props.children} -
    -
    - ) -} + const toggleAnimation = () => { + setAnimation(index => (index + 1) % animationFactories.length); + setPaused(false); + }; -export default Viewer + const toggleRotate = () => { + const viewer = viewReference.current; + viewer.autoRotate = !viewer.autoRotate; + }; + + const toggleBackEquippment = () => { + const player = viewReference.current.playerObject; + player.backEquipment = player.backEquipment === 'cape' ? 'elytra' : 'cape'; + }; + + const setWhite = () => { + viewReference.current.background = '#fff'; + }; + + const setGray = () => { + viewReference.current.background = '#6c757d'; + }; + + const setBlack = () => { + viewReference.current.background = '#000'; + }; + + const setPreviousPicture = () => { + setBgPicture(index => { + if (bgPicture <= 0) { + return PICTURES_COUNT - 1; + } + + return index - 1; + }); + }; + + const setNextPicture = () => { + setBgPicture(index => { + if (bgPicture >= PICTURES_COUNT - 1) { + return 0; + } + + return index + 1; + }); + }; + + return ( +
    +
    +
    +

    + {t('general.texturePreview')} + {properties.showIndicator && ( + {indicator} + )} +

    +
    + + + + +
    +
    +
    +
    + +
    +
    +
    +
    +
    +
    +
    + +
    +
    + +
    +
    + {properties.children} +
    +
    + ); +}; + +export default Viewer; diff --git a/resources/assets/src/components/ViewerSkeleton.tsx b/resources/assets/src/components/ViewerSkeleton.tsx index 3dfd1e67..9c94ebfc 100644 --- a/resources/assets/src/components/ViewerSkeleton.tsx +++ b/resources/assets/src/components/ViewerSkeleton.tsx @@ -1,17 +1,17 @@ -import React from 'react' -import { t } from '@/scripts/i18n' +import React from 'react'; +import {t} from '@/scripts/i18n'; const ViewerSkeleton: React.FC = () => ( -
    -
    -
    -

    - {t('general.texturePreview')} -

    -
    -
    -
    -
    -) +
    +
    +
    +

    + {t('general.texturePreview')} +

    +
    +
    +
    +
    +); -export default ViewerSkeleton +export default ViewerSkeleton; diff --git a/resources/assets/src/index.tsx b/resources/assets/src/index.tsx index 370fc0ba..95554b15 100644 --- a/resources/assets/src/index.tsx +++ b/resources/assets/src/index.tsx @@ -1,37 +1,44 @@ -import * as React from 'react' -import * as ReactDOM from 'react-dom' -import $ from 'jquery' -import './scripts/app' -import routes from './scripts/route' +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'; -Object.assign(window, { React, ReactDOM, $ }) +// eslint-disable-next-line @typescript-eslint/naming-convention +Object.assign(window, {React, ReactDOM, $}); -const entry = document.querySelector('[href="#launch-cli"]') +const entry = document.querySelector('[href="#launch-cli"]'); entry?.addEventListener('click', async () => { - const { launch } = await import('./scripts/cli') - launch() -}) + const {launch} = await import('./scripts/cli'); + launch(); +}); -const route = routes.find((route) => - new RegExp(`^${route.path}$`, 'i').test(blessing.route), -) +const route = routes.find(route => + new RegExp(`^${route.path}$`, 'i').test(blessing.route), +); if (route) { - if (route.module) { - Promise.all(route.module.map((m) => m())) - } - if (route.react) { - const Component = React.lazy( - route.react as () => Promise<{ default: React.ComponentType }>, - ) - const Root = () => ( - - - - - - ) - const c = - typeof route.el === 'string' ? document.querySelector(route.el) : route.el - ReactDOM.render(, c) - } + 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 container = typeof route.el === 'string' + ? document.querySelector(route.el) + : null; + + const root = createRoot(container!); + root.render( + + + + + , + ); + } } diff --git a/resources/assets/src/scripts/app.ts b/resources/assets/src/scripts/app.ts index 8476510e..a3c44248 100644 --- a/resources/assets/src/scripts/app.ts +++ b/resources/assets/src/scripts/app.ts @@ -1,14 +1,14 @@ -import './init' // must be first -import 'admin-lte' -import './extra' -import './i18n' -import './net' -import './event' -import './notification' -import './emailVerification' -import './logout' -import './darkMode' +/* eslint-disable import/no-unassigned-import */ +import 'admin-lte'; +import './extra'; +import './i18n'; +import './net'; +import './event'; +import './notification'; +import './emailVerification'; +import './logout'; +import './darkMode'; window.addEventListener('load', () => { - $('[data-toggle="tooltip"]').tooltip() -}) + $('[data-toggle="tooltip"]').tooltip(); +}); diff --git a/resources/assets/src/scripts/cli.tsx b/resources/assets/src/scripts/cli.tsx index 23f06054..4777a755 100644 --- a/resources/assets/src/scripts/cli.tsx +++ b/resources/assets/src/scripts/cli.tsx @@ -1,20 +1,20 @@ -import React, { useEffect, useRef } from 'react' -import ReactDOM from 'react-dom' -import styled from '@emotion/styled' -import { Terminal } from 'xterm' -import { FitAddon } from 'xterm-addon-fit' -import { Shell, Stdio } from 'blessing-skin-shell' -import 'xterm/css/xterm.css' -import Draggable from 'react-draggable' -import * as event from './event' -import AptCommand from './cli/AptCommand' -import ClosetCommand from './cli/ClosetCommand' -import DnfCommand from './cli/DnfCommand' -import PacmanCommand from './cli/PacmanCommand' -import RmCommand from './cli/RmCommand' -import * as breakpoints from '@/styles/breakpoints' +import React, {useEffect, useRef} from 'react'; +import ReactDOM from 'react-dom'; +import styled from '@emotion/styled'; +import {Terminal} from 'xterm'; +import {FitAddon} from 'xterm-addon-fit'; +import {Shell, type Stdio} from 'blessing-skin-shell'; +import 'xterm/css/xterm.css'; +import Draggable from 'react-draggable'; +import * as event from './event'; +import AptCommand from './cli/AptCommand'; +import ClosetCommand from './cli/ClosetCommand'; +import DnfCommand from './cli/DnfCommand'; +import PacmanCommand from './cli/PacmanCommand'; +import RmCommand from './cli/RmCommand'; +import * as breakpoints from '@/styles/breakpoints'; -let launched = false +let launched = false; const TerminalContainer = styled.div` z-index: 1040; @@ -43,98 +43,99 @@ const TerminalContainer = styled.div` width: 98vw; height: 35vh; } -` +`; -const TerminalWindow: React.FC<{ onClose(): void }> = (props) => { - const mount = useRef(null) +const TerminalWindow: React.FC<{onClose(): void}> = properties => { + const mount = useRef(null); - useEffect(() => { - const el = mount.current - if (!el) { - return - } + useEffect(() => { + const element = mount.current; + if (!element) { + return; + } - const terminal = new Terminal() - const fitAddon = new FitAddon() - terminal.loadAddon(fitAddon) - terminal.setOption( - 'fontFamily', - 'Monaco, Consolas, "Roboto Mono", "Noto Sans", "Droid Sans Mono"', - ) - terminal.open(el) - fitAddon.fit() + const terminal = new Terminal(); + const fitAddon = new FitAddon(); + terminal.loadAddon(fitAddon); + terminal.options.fontFamily = 'Monaco, Consolas, "Roboto Mono", "Noto Sans", "Droid Sans Mono"'; + terminal.open(element); + fitAddon.fit(); - const programs = new Map void>() - programs.set('apt', AptCommand) - programs.set('closet', ClosetCommand) - programs.set('dnf', DnfCommand) - programs.set('pacman', PacmanCommand) - programs.set('rm', RmCommand) - event.emit('registerCLIPrograms', programs) + const programs = new Map void>(); + programs.set('apt', AptCommand); + programs.set('closet', ClosetCommand); + programs.set('dnf', DnfCommand); + programs.set('pacman', PacmanCommand); + programs.set('rm', RmCommand); + event.emit('registerCLIPrograms', programs); - const shell = new Shell(terminal) - programs.forEach((program, name) => { - shell.addExternal(name, program) - }) + const shell = new Shell(terminal); + for (const [name, program] of programs.entries()) { + shell.addExternal(name, program); + } - const originalLogger = console.log - console.log = (data: string, ...args: any[]) => { - const stack = new Error().stack - if (stack?.includes('outputHelp')) { - terminal.writeln(data.replace(/\n/g, '\r\n')) - } else { - originalLogger(data, ...args) - } - } + const originalLogger = console.log; + console.log = (data: string, ...arguments_: any[]) => { + const {stack} = new Error(); + if (stack?.includes('outputHelp')) { + terminal.writeln(data.replaceAll('\n', '\r\n')); + } else { + // eslint-disable-next-line @typescript-eslint/no-unsafe-argument + originalLogger(data, ...arguments_); + } + }; - const unbindData = terminal.onData((e) => shell.input(e)) - const unbindKey = terminal.onKey((e) => - event.emit('terminalKeyPress', e.key), - ) - launched = true + const unbindData = terminal.onData(e => { + shell.input(e); + }); + const unbindKey = terminal.onKey(e => { + event.emit('terminalKeyPress', e.key); + }, + ); + launched = true; - return () => { - unbindData.dispose() - unbindKey.dispose() - shell.free() - fitAddon.dispose() - terminal.dispose() - console.log = originalLogger - launched = false - } - }, []) + return () => { + unbindData.dispose(); + unbindKey.dispose(); + shell.free(); + fitAddon.dispose(); + terminal.dispose(); + console.log = originalLogger; + launched = false; + }; + }, []); - return ( - - -
    -
    -

    - Blessing Skin Shell -

    - -
    -
    -
    -
    -
    - ) -} + return ( + + +
    +
    +

    + Blessing Skin Shell +

    + +
    +
    +
    + + + ); +}; export function launch() { - if (launched) { - return - } + if (launched) { + return; + } - const container = document.createElement('div') - document.body.appendChild(container) + const container = document.createElement('div'); + document.body.append(container); - const handleClose = () => { - ReactDOM.unmountComponentAtNode(container) - container.remove() - } + const handleClose = () => { + ReactDOM.unmountComponentAtNode(container); + container.remove(); + }; - ReactDOM.render(, container) + ReactDOM.render(, container); } diff --git a/resources/assets/src/scripts/cli/AptCommand.ts b/resources/assets/src/scripts/cli/AptCommand.ts index 360030ca..2e60b468 100644 --- a/resources/assets/src/scripts/cli/AptCommand.ts +++ b/resources/assets/src/scripts/cli/AptCommand.ts @@ -1,23 +1,23 @@ -import type { Stdio } from 'blessing-skin-shell' -import cac from 'cac' -import { install, remove } from './pluginManager' +import type {Stdio} from 'blessing-skin-shell'; +import cac from 'cac'; +import {install, remove} from './pluginManager'; -export default async function apt(stdio: Stdio, args: string[]) { - const program = cac('apt') - program.help() +export default async function apt(stdio: Stdio, arguments_: string[]) { + const program = cac('apt'); + program.help(); - program - .command('install ', 'install a new plugin') - .action((plugin: string) => install(plugin, stdio)) + program + .command('install ', 'install a new plugin') + .action(async (plugin: string) => install(plugin, stdio)); - program - .command('upgrade ', 'upgrade an existed plugin') - .action((plugin: string) => install(plugin, stdio)) + program + .command('upgrade ', 'upgrade an existed plugin') + .action(async (plugin: string) => install(plugin, stdio)); - program - .command('remove ', 'remove a plugin') - .action((plugin: string) => remove(plugin, stdio)) + program + .command('remove ', 'remove a plugin') + .action(async (plugin: string) => remove(plugin, stdio)); - program.parse(['', ''].concat(args), { run: false }) - await program.runMatchedCommand() + program.parse(['', ''].concat(arguments_), {run: false}); + await program.runMatchedCommand(); } diff --git a/resources/assets/src/scripts/cli/ClosetCommand.ts b/resources/assets/src/scripts/cli/ClosetCommand.ts index 0d5ebe2d..5d08fa36 100644 --- a/resources/assets/src/scripts/cli/ClosetCommand.ts +++ b/resources/assets/src/scripts/cli/ClosetCommand.ts @@ -1,46 +1,46 @@ -import type { Stdio } from 'blessing-skin-shell' -import cac from 'cac' -import * as fetch from '../net' -import type { User, Texture } from '../types' +import type {Stdio} from 'blessing-skin-shell'; +import cac from 'cac'; +import * as fetch from '../net'; +import type {User, Texture} from '../types'; -type Response = fetch.ResponseBody<{ user: User; texture: Texture }> +type Response = fetch.ResponseBody<{user: User; texture: Texture}>; -export default async function closet(stdio: Stdio, args: string[]) { - const program = cac('closet') - program.help() +export default async function closet(stdio: Stdio, arguments_: string[]) { + const program = cac('closet'); + program.help(); - program - .command('add ', "add texture to someone's closet") - .action(async (uid: string, tid: string) => { - const { code, data } = await fetch.post( - `/admin/closet/${uid}`, - { tid }, - ) - if (code === 0) { - const { texture, user } = data - stdio.println( - `Texture "${texture.name}" was added to user ${user.nickname}'s closet.`, - ) - } else { - stdio.println('Error occurred.') - } - }) - program - .command('remove ', "remove texture from someone's closet") - .action(async (uid: string, tid: string) => { - const { code, data } = await fetch.del(`/admin/closet/${uid}`, { - tid, - }) - if (code === 0) { - const { texture, user } = data - stdio.println( - `Texture "${texture.name}" was removed from user ${user.nickname}'s closet.`, - ) - } else { - stdio.println('Error occurred.') - } - }) + program + .command('add ', 'add texture to someone\'s closet') + .action(async (uid: string, tid: string) => { + const {code, data} = await fetch.post( + `/admin/closet/${uid}`, + {tid}, + ); + if (code === 0) { + const {texture, user} = data; + stdio.println( + `Texture "${texture.name}" was added to user ${user.nickname}'s closet.`, + ); + } else { + stdio.println('Error occurred.'); + } + }); + program + .command('remove ', 'remove texture from someone\'s closet') + .action(async (uid: string, tid: string) => { + const {code, data} = await fetch.del(`/admin/closet/${uid}`, { + tid, + }); + if (code === 0) { + const {texture, user} = data; + stdio.println( + `Texture "${texture.name}" was removed from user ${user.nickname}'s closet.`, + ); + } else { + stdio.println('Error occurred.'); + } + }); - program.parse(['', ''].concat(args), { run: false }) - await program.runMatchedCommand() + program.parse(['', ''].concat(arguments_), {run: false}); + await program.runMatchedCommand(); } diff --git a/resources/assets/src/scripts/cli/DnfCommand.ts b/resources/assets/src/scripts/cli/DnfCommand.ts index 8fb7feb7..b735c694 100644 --- a/resources/assets/src/scripts/cli/DnfCommand.ts +++ b/resources/assets/src/scripts/cli/DnfCommand.ts @@ -1,23 +1,23 @@ -import type { Stdio } from 'blessing-skin-shell' -import cac from 'cac' -import { install, remove } from './pluginManager' +import type {Stdio} from 'blessing-skin-shell'; +import cac from 'cac'; +import {install, remove} from './pluginManager'; -export default async function dnf(stdio: Stdio, args: string[]) { - const program = cac('dnf') - program.help() +export default async function dnf(stdio: Stdio, arguments_: string[]) { + const program = cac('dnf'); + program.help(); - program - .command('install ', 'install a new plugin') - .action((plugin: string) => install(plugin, stdio)) + program + .command('install ', 'install a new plugin') + .action(async (plugin: string) => install(plugin, stdio)); - program - .command('upgrade ', 'upgrade an existed plugin') - .action((plugin: string) => install(plugin, stdio)) + program + .command('upgrade ', 'upgrade an existed plugin') + .action(async (plugin: string) => install(plugin, stdio)); - program - .command('remove ', 'remove a plugin') - .action((plugin: string) => remove(plugin, stdio)) + program + .command('remove ', 'remove a plugin') + .action(async (plugin: string) => remove(plugin, stdio)); - program.parse(['', ''].concat(args), { run: false }) - await program.runMatchedCommand() + program.parse(['', ''].concat(arguments_), {run: false}); + await program.runMatchedCommand(); } diff --git a/resources/assets/src/scripts/cli/PacmanCommand.ts b/resources/assets/src/scripts/cli/PacmanCommand.ts index e5b73e9b..e21fdf0c 100644 --- a/resources/assets/src/scripts/cli/PacmanCommand.ts +++ b/resources/assets/src/scripts/cli/PacmanCommand.ts @@ -1,31 +1,31 @@ -import type { Stdio } from 'blessing-skin-shell' -import cac from 'cac' -import { install, remove } from './pluginManager' +import type {Stdio} from 'blessing-skin-shell'; +import cac from 'cac'; +import {install, remove} from './pluginManager'; type Options = { - sync?: string - remove?: string -} - -export default async function pacman(stdio: Stdio, args: string[]) { - if (args.length === 0) { - stdio.println('error: no operation specified (use -h for help)') - return - } - - const program = cac('pacman') - program.help() - - program.option('-S, --sync ', 'install or upgrade a plugin') - program.option('-R, --remove ', 'remove a plugin') - - const { options } = program.parse(['', ''].concat(args), { run: false }) - - const opts: Options = options - /* istanbul ignore else */ - if (opts.sync) { - await install(opts.sync, stdio) - } else if (opts.remove) { - await remove(opts.remove, stdio) - } + sync?: string; + remove?: string; +}; + +export default async function pacman(stdio: Stdio, arguments_: string[]) { + if (arguments_.length === 0) { + stdio.println('error: no operation specified (use -h for help)'); + return; + } + + const program = cac('pacman'); + program.help(); + + program.option('-S, --sync ', 'install or upgrade a plugin'); + program.option('-R, --remove ', 'remove a plugin'); + + const {options} = program.parse(['', ''].concat(arguments_), {run: false}); + + const options_: Options = options; + /* istanbul ignore else */ + if (options_.sync) { + await install(options_.sync, stdio); + } else if (options_.remove) { + await remove(options_.remove, stdio); + } } diff --git a/resources/assets/src/scripts/cli/RmCommand.ts b/resources/assets/src/scripts/cli/RmCommand.ts index 75176359..b28384a4 100644 --- a/resources/assets/src/scripts/cli/RmCommand.ts +++ b/resources/assets/src/scripts/cli/RmCommand.ts @@ -1,40 +1,40 @@ -import type { Stdio } from 'blessing-skin-shell' -import cac from 'cac' -import * as fetch from '../net' +import type {Stdio} from 'blessing-skin-shell'; +import cac from 'cac'; +import * as fetch from '../net'; type Options = { - force?: boolean - recursive?: boolean - help?: boolean -} - -export default async function rm(stdio: Stdio, args: string[]) { - const program = cac('rm') - program.help() - - program - .command('') - .option( - '-f, --force', - 'ignore nonexistent files and arguments, never prompt', - ) - .option( - '-r, --recursive', - 'remove directories and their contents recursively', - ) - .option('--no-preserve-root', "do not treat '/' specially") - - const opts: Options = program.parse(['', ''].concat(args), { - run: false, - }).options - const path = program.args[0] - - if (!path && !opts.help) { - stdio.println('rm: missing operand') - stdio.println("Try 'rm --help' for more information.") - } - - if (opts.force && opts.recursive && path?.startsWith('/')) { - await fetch.post('/admin/resource?clear-cache') - } + force?: boolean; + recursive?: boolean; + help?: boolean; +}; + +export default async function rm(stdio: Stdio, arguments_: string[]) { + const program = cac('rm'); + program.help(); + + program + .command('') + .option( + '-f, --force', + 'ignore nonexistent files and arguments, never prompt', + ) + .option( + '-r, --recursive', + 'remove directories and their contents recursively', + ) + .option('--no-preserve-root', 'do not treat \'/\' specially'); + + const {options} = program.parse(['', ''].concat(arguments_), { + run: false, + }); + const path = program.args[0]; + + if (!path && !options.help) { + stdio.println('rm: missing operand'); + stdio.println('Try \'rm --help\' for more information.'); + } + + if (options.force && options.recursive && path?.startsWith('/')) { + await fetch.post('/admin/resource?clear-cache'); + } } diff --git a/resources/assets/src/scripts/cli/Spinner.ts b/resources/assets/src/scripts/cli/Spinner.ts index 34c8b635..dbe01b3d 100644 --- a/resources/assets/src/scripts/cli/Spinner.ts +++ b/resources/assets/src/scripts/cli/Spinner.ts @@ -1,28 +1,28 @@ -import type { Stdio } from 'blessing-skin-shell' -import spinners from 'cli-spinners/spinners.json' +import type {Stdio} from 'blessing-skin-shell'; +import spinners from 'cli-spinners/spinners.json'; -const { dots } = spinners +const {dots} = spinners; export class Spinner { - private timerId = 0 - private index = 0 + private timerId = 0; + private index = 0; - constructor(private stdio: Stdio) {} + constructor(private readonly stdio: Stdio) {} - start(message = '') { - this.timerId = window.setInterval(() => { - this.index += 1 - this.index %= dots.frames.length + start(message = '') { + this.timerId = window.setInterval(() => { + this.index += 1; + this.index %= dots.frames.length; - this.stdio.reset() - this.stdio.print(`${dots.frames[this.index]} ${message}`) - }, dots.interval) - } + this.stdio.reset(); + this.stdio.print(`${dots.frames[this.index]} ${message}`); + }, dots.interval); + } - stop(message = '') { - clearInterval(this.timerId) - this.stdio.reset() - this.stdio.println(message) - this.stdio.print('\u001B[?25h') - } + stop(message = '') { + clearInterval(this.timerId); + this.stdio.reset(); + this.stdio.println(message); + this.stdio.print('\u001B[?25h'); + } } diff --git a/resources/assets/src/scripts/cli/configureStdio.ts b/resources/assets/src/scripts/cli/configureStdio.ts index 9f6d6ff1..bbf43756 100644 --- a/resources/assets/src/scripts/cli/configureStdio.ts +++ b/resources/assets/src/scripts/cli/configureStdio.ts @@ -1,35 +1,35 @@ -import type { Stdio } from 'blessing-skin-shell' -import * as event from '../event' +import type {Stdio} from 'blessing-skin-shell'; +import * as event from '../event'; /* istanbul ignore next */ export function hackStdin() { - if (process.env.NODE_ENV === 'test') { - return process.stdin - } + if (process.env.NODE_ENV === 'test') { + return process.stdin; + } - // @ts-ignore - return { - on(eventName: string, handler: (str: string, key: string) => void) { - if (eventName === 'keypress') { - this._off = event.on('terminalKeyPress', (key: string) => { - handler(key, key) - }) - } - }, - isTTY: true, - setRawMode() {}, - removeListener() { - this._off() - }, - } as NodeJS.ReadStream & { _off(): void } + // @ts-expect-error + return { + on(eventName: string, handler: (string_: string, key: string) => void) { + if (eventName === 'keypress') { + this._off = event.on('terminalKeyPress', (key: string) => { + handler(key, key); + }); + } + }, + isTTY: true, + setRawMode() {}, + removeListener() { + this._off(); + }, + } as NodeJS.ReadStream & {_off(): void}; } /* istanbul ignore next */ export function hackStdout(stdio: Stdio) { - return { - write(msg: string) { - stdio.print(msg.replace(/\n/g, '\r\n')) - return true - }, - } as NodeJS.WriteStream + return { + write(message: string) { + stdio.print(message.replaceAll('\n', '\r\n')); + return true; + }, + } as NodeJS.WriteStream; } diff --git a/resources/assets/src/scripts/cli/pluginManager.ts b/resources/assets/src/scripts/cli/pluginManager.ts index 68f60706..e191d955 100644 --- a/resources/assets/src/scripts/cli/pluginManager.ts +++ b/resources/assets/src/scripts/cli/pluginManager.ts @@ -1,43 +1,43 @@ -import type { Stdio } from 'blessing-skin-shell' -import prompts from 'prompts' -import * as fetch from '../net' -import { hackStdout, hackStdin } from './configureStdio' -import { Spinner } from './Spinner' +import type {Stdio} from 'blessing-skin-shell'; +import prompts from 'prompts'; +import * as fetch from '../net'; +import {hackStdout, hackStdin} from './configureStdio'; +import {Spinner} from './Spinner'; export async function install(plugin: string, stdio: Stdio) { - const spinner = new Spinner(stdio) - spinner.start('Installing plugin...') + const spinner = new Spinner(stdio); + spinner.start('Installing plugin...'); - const { message, data } = await fetch.post< - fetch.ResponseBody<{ reason?: string[] } | undefined> - >('/admin/plugins/market/download', { name: plugin }) + const {message, data} = await fetch.post< + fetch.ResponseBody<{reason?: string[]} | undefined> + >('/admin/plugins/market/download', {name: plugin}); - spinner.stop(` ${message}`) - const reasons = data?.reason - if (reasons) { - stdio.println(reasons.map((reason) => `- ${reason}`).join('\r\n')) - } + spinner.stop(` ${message}`); + const reasons = data?.reason; + if (reasons) { + stdio.println(reasons.map(reason => `- ${reason}`).join('\r\n')); + } } export async function remove(plugin: string, stdio: Stdio) { - const { confirm }: { confirm: boolean } = await prompts({ - name: 'confirm', - type: 'confirm', - message: `Are you sure to remove plugin "${plugin}"?`, - stdin: hackStdin(), - stdout: hackStdout(stdio), - }) + const {confirm}: {confirm: boolean} = await prompts({ + name: 'confirm', + type: 'confirm', + message: `Are you sure to remove plugin "${plugin}"?`, + stdin: hackStdin(), + stdout: hackStdout(stdio), + }); - if (!confirm) { - return - } + if (!confirm) { + return; + } - const spinner = new Spinner(stdio) - spinner.start('Uninstalling plugin...') + const spinner = new Spinner(stdio); + spinner.start('Uninstalling plugin...'); - const { message } = await fetch.post( - '/admin/plugins/manage', - { action: 'delete', name: plugin }, - ) - spinner.stop(` ${message}`) + const {message} = await fetch.post( + '/admin/plugins/manage', + {action: 'delete', name: plugin}, + ); + spinner.stop(` ${message}`); } diff --git a/resources/assets/src/scripts/cli/readline.ts b/resources/assets/src/scripts/cli/readline.ts index c8dbfa05..c06941ea 100644 --- a/resources/assets/src/scripts/cli/readline.ts +++ b/resources/assets/src/scripts/cli/readline.ts @@ -1,9 +1,9 @@ export function emitKeypressEvents() {} export function createInterface() { - return { - pause() {}, - resume() {}, - close() {}, - } + return { + pause() {}, + resume() {}, + close() {}, + }; } diff --git a/resources/assets/src/scripts/darkMode.tsx b/resources/assets/src/scripts/darkMode.tsx index adafe9dd..9ba8e23b 100644 --- a/resources/assets/src/scripts/darkMode.tsx +++ b/resources/assets/src/scripts/darkMode.tsx @@ -1,9 +1,8 @@ -import * as React from 'react' -import * as ReactDOM from 'react-dom' -import DarkModeButton from '@/components/DarkModeButton' +import ReactDOM from 'react-dom'; +import DarkModeButton from '@/components/DarkModeButton'; -const el = document.querySelector('#toggle-dark-mode') -if (el) { - const initMode = document.body.classList.contains('dark-mode') - ReactDOM.render(, el) +const element = document.querySelector('#toggle-dark-mode'); +if (element) { + const initMode = document.body.classList.contains('dark-mode'); + ReactDOM.render(, element); } diff --git a/resources/assets/src/scripts/emailVerification.tsx b/resources/assets/src/scripts/emailVerification.tsx index 2389fa59..99796c87 100644 --- a/resources/assets/src/scripts/emailVerification.tsx +++ b/resources/assets/src/scripts/emailVerification.tsx @@ -1,9 +1,8 @@ -import React from 'react' -import ReactDOM from 'react-dom' -import EmailVerification from '@/views/widgets/EmailVerification' +import ReactDOM from 'react-dom'; +import EmailVerification from '@/views/widgets/EmailVerification'; -const container = document.querySelector('#email-verification') +const container = document.querySelector('#email-verification'); if (blessing.extra.unverified && container) { - ReactDOM.render(, container) + ReactDOM.render(, container); } diff --git a/resources/assets/src/scripts/event.ts b/resources/assets/src/scripts/event.ts index afc5b20c..fc66ffce 100644 --- a/resources/assets/src/scripts/event.ts +++ b/resources/assets/src/scripts/event.ts @@ -1,20 +1,21 @@ -/* eslint-disable @typescript-eslint/no-unnecessary-condition */ -const bus = new Map>() + +const bus = new Map>(); export function on(event: string | symbol, listener: CallableFunction) { - if (!bus.has(event)) { - bus.set(event, new Set()) - } - const listeners = bus.get(event)! - listeners.add(listener) + if (!bus.has(event)) { + bus.set(event, new Set()); + } - return () => { - listeners.delete(listener) - } + const listeners = bus.get(event)!; + listeners.add(listener); + + return () => { + listeners.delete(listener); + }; } export function emit(event: string | symbol, payload?: unknown) { - bus.get(event)?.forEach((listener) => listener(payload)) + bus.get(event)?.forEach(listener => listener(payload)); } -blessing.event = { on, emit } +blessing.event = {on, emit}; diff --git a/resources/assets/src/scripts/extra.ts b/resources/assets/src/scripts/extra.ts index 8a185eeb..7c01b028 100644 --- a/resources/assets/src/scripts/extra.ts +++ b/resources/assets/src/scripts/extra.ts @@ -1,11 +1,11 @@ export function getExtraData(): Record { - const jsonElement = document.querySelector('#blessing-extra') - /* istanbul ignore next */ - if (jsonElement) { - return JSON.parse(jsonElement.textContent ?? '{}') - } else { - return {} - } + const jsonElement = document.querySelector('#blessing-extra'); + /* istanbul ignore next */ + if (jsonElement) { + return JSON.parse(jsonElement.textContent ?? '{}'); + } + + return {}; } -blessing.extra = getExtraData() +blessing.extra = getExtraData(); diff --git a/resources/assets/src/scripts/homePage.ts b/resources/assets/src/scripts/homePage.ts index b8dad290..a55bc4fd 100644 --- a/resources/assets/src/scripts/homePage.ts +++ b/resources/assets/src/scripts/homePage.ts @@ -1,25 +1,25 @@ -import { getExtraData } from './extra' +import {getExtraData} from './extra'; export function scrollHander() { - const header = document.querySelector('.navbar') - /* istanbul ignore else */ - if (header) { - window.addEventListener('scroll', () => { - if (window.scrollY >= (window.innerHeight * 2) / 3) { - header.classList.remove('transparent') - } else { - header.classList.add('transparent') - } - }) - } + const header = document.querySelector('.navbar'); + /* istanbul ignore else */ + if (header) { + window.addEventListener('scroll', () => { + if (window.scrollY >= (window.innerHeight * 2) / 3) { + header.classList.remove('transparent'); + } else { + header.classList.add('transparent'); + } + }); + } } /* istanbul ignore next */ if (process.env.NODE_ENV !== 'test') { - const { transparent_navbar } = getExtraData() as { - transparent_navbar: boolean - } - if (transparent_navbar) { - window.addEventListener('load', scrollHander) - } + const {transparent_navbar} = getExtraData() as { + transparent_navbar: boolean; + }; + if (transparent_navbar) { + window.addEventListener('load', scrollHander); + } } diff --git a/resources/assets/src/scripts/hooks/useBlessingExtra.ts b/resources/assets/src/scripts/hooks/useBlessingExtra.ts index aca79165..6ceedfaa 100644 --- a/resources/assets/src/scripts/hooks/useBlessingExtra.ts +++ b/resources/assets/src/scripts/hooks/useBlessingExtra.ts @@ -1,11 +1,11 @@ -import { useState, useEffect } from 'react' +import {useState, useEffect} from 'react'; export default function useBlessingExtra(key: string, defaultValue?: T): T { - const [value, setValue] = useState(defaultValue!) + const [value, setValue] = useState(defaultValue!); - useEffect(() => { - setValue(blessing.extra[key] as T) - }, [key]) + useEffect(() => { + setValue(blessing.extra[key] as T); + }, [key]); - return value + return value; } diff --git a/resources/assets/src/scripts/hooks/useEmitMounted.ts b/resources/assets/src/scripts/hooks/useEmitMounted.ts index 5c75f9f5..0430d248 100644 --- a/resources/assets/src/scripts/hooks/useEmitMounted.ts +++ b/resources/assets/src/scripts/hooks/useEmitMounted.ts @@ -1,8 +1,8 @@ -import { useEffect } from 'react' -import { emit } from '../event' +import {useEffect} from 'react'; +import {emit} from '../event'; export default function useEmitMounted() { - useEffect(() => { - emit('mounted') - }, []) + useEffect(() => { + emit('mounted'); + }, []); } diff --git a/resources/assets/src/scripts/hooks/useIsLargeScreen.ts b/resources/assets/src/scripts/hooks/useIsLargeScreen.ts index b7019916..b1bb750e 100644 --- a/resources/assets/src/scripts/hooks/useIsLargeScreen.ts +++ b/resources/assets/src/scripts/hooks/useIsLargeScreen.ts @@ -1,13 +1,13 @@ -import { useState, useEffect } from 'react' +import {useState, useEffect} from 'react'; export default function useIsLargeScreen() { - const [isLarge, setIsLarge] = useState(false) + const [isLarge, setIsLarge] = useState(false); - useEffect(() => { - if (window.innerWidth >= 992) { - setIsLarge(true) - } - }, []) + useEffect(() => { + if (window.innerWidth >= 992) { + setIsLarge(true); + } + }, []); - return isLarge + return isLarge; } diff --git a/resources/assets/src/scripts/hooks/useMount.ts b/resources/assets/src/scripts/hooks/useMount.ts index 0577fbd4..89ec3e68 100644 --- a/resources/assets/src/scripts/hooks/useMount.ts +++ b/resources/assets/src/scripts/hooks/useMount.ts @@ -1,20 +1,20 @@ -import { useEffect, useRef } from 'react' +import {useEffect, useRef} from 'react'; -export default function useMount(selector: string): HTMLElement | null { - const container = useRef(null) +export default function useMount(selector: string): HTMLElement | undefined { + const container = useRef(null); - useEffect(() => { - const mount = document.querySelector(selector)! - const div = document.createElement('div') - container.current = div + useEffect(() => { + const mount = document.querySelector(selector)!; + const div = document.createElement('div'); + container.current = div; - mount.appendChild(div) + mount.append(div); - return () => { - mount.removeChild(div) - container.current = null - } - }, [selector]) + return () => { + div.remove(); + container.current = null; + }; + }, [selector]); - return container.current + return container.current; } diff --git a/resources/assets/src/scripts/hooks/useTexture.ts b/resources/assets/src/scripts/hooks/useTexture.ts index 74843997..d85a1c54 100644 --- a/resources/assets/src/scripts/hooks/useTexture.ts +++ b/resources/assets/src/scripts/hooks/useTexture.ts @@ -1,26 +1,27 @@ -import { useState, useEffect } from 'react' -import * as fetch from '../net' -import { Texture, TextureType } from '../types' +import {useState, useEffect} from 'react'; +import * as fetch from '../net'; +import {type Texture, TextureType} from '../types'; export default function useTexture() { - const [tid, setTid] = useState(0) - const [url, setUrl] = useState('') - const [type, setType] = useState(TextureType.Steve) + const [tid, setTid] = useState(0); + const [url, setUrl] = useState(''); + const [type, setType] = useState(TextureType.Steve); - useEffect(() => { - if (tid <= 0) { - setUrl('') - return - } + useEffect(() => { + if (tid <= 0) { + setUrl(''); + return; + } - const getTexture = async () => { - const { hash, type } = await fetch.get(`/skinlib/info/${tid}`) + const getTexture = async () => { + const {hash, type} = await fetch.get(`/skinlib/info/${tid}`); - setUrl(`${blessing.base_url}/textures/${hash}`) - setType(type) - } - getTexture() - }, [tid]) + setUrl(`${blessing.base_url}/textures/${hash}`); + setType(type); + }; - return [{ url, type }, setTid] as const + getTexture(); + }, [tid]); + + return [{url, type}, setTid] as const; } diff --git a/resources/assets/src/scripts/hooks/useTween.ts b/resources/assets/src/scripts/hooks/useTween.ts index 60cab3c6..d6f85c6d 100644 --- a/resources/assets/src/scripts/hooks/useTween.ts +++ b/resources/assets/src/scripts/hooks/useTween.ts @@ -1,22 +1,22 @@ -import { useState, useEffect, useRef } from 'react' -import TWEEN from '@tweenjs/tween.js' +import {useState, useEffect, useRef} from 'react'; +import TWEEN from '@tweenjs/tween.js'; export default function useTween(initialValue: T) { - const [value, setValue] = useState(initialValue) - const ref = useRef(value) - const [dest, setDest] = useState(initialValue) + const [value, setValue] = useState(initialValue); + const reference = useRef(value); + const [destination, setDestination] = useState(initialValue); - useEffect(() => { - function animate() { - requestAnimationFrame(animate) - TWEEN.update() - setValue(ref.current) - } + useEffect(() => { + function animate() { + requestAnimationFrame(animate); + TWEEN.update(); + setValue(reference.current); + } - const tween = new TWEEN.Tween(ref) - tween.to({ current: dest }, 1000).start() - animate() - }, [dest]) + const tween = new TWEEN.Tween(reference); + tween.to({current: destination}, 1000).start(); + animate(); + }, [destination]); - return [value, setDest] as const + return [value, setDestination] as const; } diff --git a/resources/assets/src/scripts/i18n.ts b/resources/assets/src/scripts/i18n.ts index 17b36f40..3da3f363 100644 --- a/resources/assets/src/scripts/i18n.ts +++ b/resources/assets/src/scripts/i18n.ts @@ -1,31 +1,32 @@ -interface I18nTable { - [key: string]: string | I18nTable | undefined +type I18nTable = { + [key: string]: string | I18nTable | undefined; +}; + +export function t(key: string, parameters: Record = Object.create(null)): string { + const segments = key.split('.'); + let temporary = blessing.i18n as I18nTable | undefined; + let result = ''; + + for (const segment of segments) { + /* istanbul ignore next */ + const middle = temporary?.[segment]; + if (!middle) { + return key; + } + + if (typeof middle === 'string') { + result = middle; + } else { + temporary = middle; + } + } + + for (const slot of Object.keys(parameters)) { + (result = result.replace(`:${slot}`, parameters[slot] ?? `%{${slot}}`)); + } + + return result; } -export function t(key: string, parameters = Object.create(null)): string { - const segments = key.split('.') - let temp = blessing.i18n as I18nTable | undefined - let result = '' - - for (const segment of segments) { - /* istanbul ignore next */ - const middle = temp?.[segment] - if (!middle) { - return key - } - if (typeof middle === 'string') { - result = middle - } else { - temp = middle - } - } - - Object.keys(parameters).forEach( - (slot) => (result = result.replace(`:${slot}`, parameters[slot])), - ) - - return result -} - -Object.assign(window, { trans: t }) -Object.assign(blessing, { t }) +Object.assign(window, {trans: t}); +Object.assign(blessing, {t}); diff --git a/resources/assets/src/scripts/init.ts b/resources/assets/src/scripts/init.ts index 715a7b7d..4b905714 100644 --- a/resources/assets/src/scripts/init.ts +++ b/resources/assets/src/scripts/init.ts @@ -1,12 +1,12 @@ -declare let __webpack_public_path__: string -declare const __blessing_public_path__: string +declare let __webpack_public_path__: string; +declare const __blessing_public_path__: string; if (process.env.NODE_ENV === 'development') { - __webpack_public_path__ = __blessing_public_path__ + __webpack_public_path__ = __blessing_public_path__; } else { - const link = document.querySelector('link#cdn-host') - const base = link?.href ?? blessing.base_url - __webpack_public_path__ = `${base}/app/` + const link = document.querySelector('link#cdn-host'); + const base = link?.href ?? blessing.base_url; + __webpack_public_path__ = `${base}/app/`; } -export {} +export {}; diff --git a/resources/assets/src/scripts/logout.ts b/resources/assets/src/scripts/logout.ts index 74cd4ca8..d8169cb6 100644 --- a/resources/assets/src/scripts/logout.ts +++ b/resources/assets/src/scripts/logout.ts @@ -1,22 +1,22 @@ -import { post } from './net' -import { t } from './i18n' -import { showModal } from './notify' -import urls from './urls' +import {post} from './net'; +import {t} from './i18n'; +import {showModal} from './notify'; +import urls from './urls'; export async function logout() { - try { - await showModal({ - text: t('general.confirmLogout'), - center: true, - }) - } catch { - return - } + try { + await showModal({ + text: t('general.confirmLogout'), + center: true, + }); + } catch { + return; + } - await post(urls.auth.logout()) - window.location.href = blessing.base_url + await post(urls.auth.logout()); + window.location.href = blessing.base_url; } -const button = document.querySelector('#logout-button') +const button = document.querySelector('#logout-button'); /* istanbul ignore next */ -button?.addEventListener('click', logout) +button?.addEventListener('click', logout); diff --git a/resources/assets/src/scripts/modal.tsx b/resources/assets/src/scripts/modal.tsx index 6d0747dc..64a12041 100644 --- a/resources/assets/src/scripts/modal.tsx +++ b/resources/assets/src/scripts/modal.tsx @@ -1,27 +1,26 @@ -import React from 'react' -import ReactDOM from 'react-dom' -import Modal, { ModalOptions, ModalResult } from '../components/Modal' +import ReactDOM from 'react-dom'; +import Modal, {type ModalOptions, type ModalResult} from '../components/Modal'; -export function showModal(options: ModalOptions = {}): Promise { - return new Promise((resolve, reject) => { - const container = document.createElement('div') - document.body.appendChild(container) +export async function showModal(options: ModalOptions = {}): Promise { + return new Promise((resolve, reject) => { + const container = document.createElement('div'); + document.body.append(container); - const handleClose = () => { - ReactDOM.unmountComponentAtNode(container) - document.body.removeChild(container) - } + const handleClose = () => { + ReactDOM.unmountComponentAtNode(container); + container.remove(); + }; - ReactDOM.render( - , - container, - ) - }) + ReactDOM.render( + , + container, + ); + }); } diff --git a/resources/assets/src/scripts/net.ts b/resources/assets/src/scripts/net.ts index ede006d1..10315c64 100644 --- a/resources/assets/src/scripts/net.ts +++ b/resources/assets/src/scripts/net.ts @@ -1,159 +1,164 @@ -import { emit } from './event' -import { showModal } from './notify' -import { t } from './i18n' +import {emit} from './event'; +import {showModal} from './notify'; +import {t} from './i18n'; -export interface ResponseBody { - code: number - message: string - data: T extends null ? never : T -} +export type ResponseBody = { + code: number; + message: string; + data: T extends undefined ? never : T; +}; class HTTPError extends Error { - response: Response + response: Response; - constructor(message: string, response: Response) { - super(message) - this.response = response - } + constructor(message: string, response: Response) { + super(message); + this.response = response; + } } -const empty = Object.create(null) +const empty: Record = Object.create(null); export const init: RequestInit = { - credentials: 'same-origin', - headers: new Headers({ - Accept: 'application/json', - }), -} + credentials: 'same-origin', + headers: new Headers({ + Accept: 'application/json', + }), +}; function retrieveToken() { - const csrfField = document.querySelector( - 'meta[name="csrf-token"]', - ) - // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition - return csrfField?.content || '' + const csrfField = document.querySelector( + 'meta[name="csrf-token"]', + ); + + return csrfField?.content || ''; } export async function walkFetch(request: Request): Promise { - request.headers.set('X-CSRF-TOKEN', retrieveToken()) + request.headers.set('X-CSRF-TOKEN', retrieveToken()); - try { - const response = await fetch(request) - const cloned = response.clone() - const body = - response.headers.get('Content-Type') === 'application/json' - ? await response.json() - : await response.text() - if (response.ok) { - return body - } - let message: string = body.message + try { + const response = await fetch(request); + const cloned = response.clone(); + const body + = response.headers.get('Content-Type') === 'application/json' + ? await response.json() + : await response.text(); + if (response.ok) { + return body; + } - if (response.status === 422) { - // Process validation errors from Laravel. - const { - errors, - }: { - message: string - errors: { [field: string]: string[] } - } = body - return { - code: 1, - message: Object.keys(errors).map((field) => errors[field]![0])[0], - } - } else if (response.status === 419) { - return showModal({ - mode: 'alert', - text: t('general.csrf'), - }) - } else if (response.status === 403 || response.status === 400) { - return showModal({ - mode: 'alert', - text: message, - type: 'warning', - }) - } + let {message} = body; - if (body.exception && Array.isArray(body.trace)) { - const trace = (body.trace as Array<{ file: string; line: number }>) - .map((t, i) => `[${i + 1}] ${t.file}#L${t.line}`) - .join('
    ') - message = `${message}
    ${trace}
    ` - } + if (response.status === 422) { + // Process validation errors from Laravel. + const { + errors, + }: { + message: string; + errors: Record; + } = body; + return { + code: 1, + message: Object.keys(errors).map(field => errors[field][0])[0], + }; + } - throw new HTTPError(message || body, cloned) - } catch (error: any) { - emit('fetchError', error) - await showModal({ - mode: 'alert', - title: t('general.fatalError'), - dangerousHTML: error.message, - type: 'danger', - okButtonType: 'outline-light', - }) + if (response.status === 419) { + return await showModal({ + mode: 'alert', + text: t('general.csrf'), + }); + } - return { code: -1, message: t('general.fatalError') } - } + if (response.status === 403 || response.status === 400) { + return await showModal({ + mode: 'alert', + text: message, + type: 'warning', + }); + } + + if (body.exception && Array.isArray(body.trace)) { + const trace = (body.trace as Array<{file: string; line: number}>) + .map((t, i) => `[${i + 1}] ${t.file}#L${t.line}`) + .join('
    '); + message = `${message}
    ${trace}
    `; + } + + throw new HTTPError(message || String(body), cloned); + } catch (error: any) { + emit('fetchError', error); + await showModal({ + mode: 'alert', + title: t('general.fatalError'), + dangerousHTML: error.message, + type: 'danger', + okButtonType: 'outline-light', + }); + + return {code: -1, message: t('general.fatalError')}; + } } -export function get(url: string, params = empty): Promise { - emit('beforeFetch', { - method: 'GET', - url, - data: params, - }) +export async function get(url: string, parameters: Record | URLSearchParams = empty): Promise { + emit('beforeFetch', { + method: 'GET', + url, + data: parameters, + }); - const qs = new URLSearchParams(params).toString() + const qs = new URLSearchParams(parameters).toString(); - return walkFetch(new Request(`${blessing.base_url}${url}?${qs}`, init)) + return walkFetch(new Request(`${blessing.base_url}${url}?${qs}`, init)); } -function nonGet( - method: string, - url: string, - data?: FormData | Record, +async function nonGet( + method: string, + url: string, + data?: FormData | Record, ): Promise { - emit('beforeFetch', { - method: method.toUpperCase(), - url, - data, - }) + emit('beforeFetch', { + method: method.toUpperCase(), + url, + data, + }); - const request = new Request(`${blessing.base_url}${url}`, { - body: data instanceof FormData ? data : JSON.stringify(data), - method: method.toUpperCase(), - ...init, - }) - if (!(data instanceof FormData)) { - request.headers.set('Content-Type', 'application/json') - } + const request = new Request(`${blessing.base_url}${url}`, { + body: data instanceof FormData ? data : JSON.stringify(data), + method: method.toUpperCase(), + ...init, + }); + if (!(data instanceof FormData)) { + request.headers.set('Content-Type', 'application/json'); + } - return walkFetch(request) + return walkFetch(request); } -export function post( - url: string, - data?: FormData | Record, +export async function post( + url: string, + data?: FormData | Record, ): Promise { - return nonGet('POST', url, data) + return nonGet('POST', url, data); } -export function put( - url: string, - data?: FormData | Record, +export async function put( + url: string, + data?: FormData | Record, ): Promise { - return nonGet('PUT', url, data) + return nonGet('PUT', url, data); } -export function del( - url: string, - data?: FormData | Record, +export async function del( + url: string, + data?: FormData | Record, ): Promise { - return nonGet('DELETE', url, data) + return nonGet('DELETE', url, data); } blessing.fetch = { - get, - post, - put, - del, -} + get, + post, + put, + del, +}; diff --git a/resources/assets/src/scripts/notification.tsx b/resources/assets/src/scripts/notification.tsx index e807c4e1..13b83c58 100644 --- a/resources/assets/src/scripts/notification.tsx +++ b/resources/assets/src/scripts/notification.tsx @@ -1,8 +1,7 @@ -import React from 'react' -import ReactDOM from 'react-dom' -import NotificationsList from '@/views/widgets/NotificationsList' +import ReactDOM from 'react-dom'; +import NotificationsList from '@/views/widgets/NotificationsList'; -const container = document.querySelector('[data-notifications]') +const container = document.querySelector('[data-notifications]'); if (container) { - ReactDOM.render(, container) + ReactDOM.render(, container); } diff --git a/resources/assets/src/scripts/notify.ts b/resources/assets/src/scripts/notify.ts index d7f90f57..90510023 100644 --- a/resources/assets/src/scripts/notify.ts +++ b/resources/assets/src/scripts/notify.ts @@ -1,15 +1,15 @@ -import { showModal } from './modal' -import { Toast } from './toast' +import {showModal} from './modal'; +import {Toast} from './toast'; -export const toast = new Toast() +export const toast = new Toast(); /* istanbul ignore next */ if (process.env.NODE_ENV === 'test') { - afterEach(() => { - toast.clear() - }) + afterEach(() => { + toast.clear(); + }); } -Object.assign(blessing, { notify: { showModal, toast } }) +Object.assign(blessing, {notify: {showModal, toast}}); -export { showModal } from './modal' +export {showModal} from './modal'; diff --git a/resources/assets/src/scripts/route.tsx b/resources/assets/src/scripts/route.tsx index 19d3d958..e40f133c 100644 --- a/resources/assets/src/scripts/route.tsx +++ b/resources/assets/src/scripts/route.tsx @@ -1,118 +1,116 @@ -import React from 'react' - export default [ - { - path: 'user', - react: () => import('../views/user/Dashboard'), - el: '#usage-box', - frame: () => ( -
    -
     
    -
    -
     
    -
    - ), - }, - { - path: 'user/closet', - react: () => import('../views/user/Closet'), - el: '#closet-list', - }, - { - path: 'user/player', - react: () => import('../views/user/Players'), - el: '#players-list', - frame: () => ( -
    -
     
    -
    -
    - ), - }, - { - path: 'user/profile', - module: [() => import('../views/user/profile/index')], - }, - { - path: 'user/oauth/manage', - react: () => import('../views/user/OAuth'), - el: '.content > .container-fluid', - }, - { - path: 'admin', - module: [() => import('../views/admin/Dashboard')], - }, - { - path: 'admin/users', - react: () => import('../views/admin/UsersManagement'), - el: '.content > .container-fluid', - }, - { - path: 'admin/players', - react: () => import('../views/admin/PlayersManagement'), - el: '.content > .container-fluid', - }, - { - path: 'admin/reports', - react: () => import('../views/admin/ReportsManagement'), - el: '.content > .container-fluid', - }, - { - path: 'admin/customize', - module: [() => import('../views/admin/Customization')], - }, - { - path: 'admin/i18n', - react: () => import('../views/admin/Translations'), - el: '#table', - }, - { - path: 'admin/plugins/manage', - react: () => import('../views/admin/PluginsManagement'), - el: '.content > .container-fluid', - }, - { - path: 'admin/plugins/market', - react: () => import('../views/admin/PluginsMarket'), - el: '.content > .container-fluid', - }, - { - path: 'admin/update', - module: [() => import('../views/admin/Update')], - }, - { - path: 'auth/login', - react: () => import('../views/auth/Login'), - el: 'main', - }, - { - path: 'auth/register', - react: () => import('../views/auth/Registration'), - el: 'main', - }, - { - path: 'auth/forgot', - react: () => import('../views/auth/Forgot'), - el: 'main', - }, - { - path: 'auth/reset/(\\d+)', - react: () => import('../views/auth/Reset'), - el: 'main', - }, - { - path: 'skinlib', - react: () => import('../views/skinlib/SkinLibrary'), - el: '.content-wrapper', - }, - { - path: 'skinlib/show/(\\d+)', - react: () => import('../views/skinlib/Show'), - el: '#side', - }, - { - path: 'skinlib/upload', - react: () => import('../views/skinlib/Upload'), - el: '#file-input', - }, -] + { + path: 'user', + react: async () => import('../views/user/Dashboard'), + el: '#usage-box', + frame: () => ( +
    +
     
    +
    +
     
    +
    + ), + }, + { + path: 'user/closet', + react: async () => import('../views/user/Closet'), + el: '#closet-list', + }, + { + path: 'user/player', + react: async () => import('../views/user/Players'), + el: '#players-list', + frame: () => ( +
    +
     
    +
    +
    + ), + }, + { + path: 'user/profile', + module: [async () => import('../views/user/profile/index')], + }, + { + path: 'user/oauth/manage', + react: async () => import('../views/user/OAuth'), + el: '.content > .container-fluid', + }, + { + path: 'admin', + module: [async () => import('../views/admin/Dashboard')], + }, + { + path: 'admin/users', + react: async () => import('../views/admin/UsersManagement'), + el: '.content > .container-fluid', + }, + { + path: 'admin/players', + react: async () => import('../views/admin/PlayersManagement'), + el: '.content > .container-fluid', + }, + { + path: 'admin/reports', + react: async () => import('../views/admin/ReportsManagement'), + el: '.content > .container-fluid', + }, + { + path: 'admin/customize', + module: [async () => import('../views/admin/Customization')], + }, + { + path: 'admin/i18n', + react: async () => import('../views/admin/Translations'), + el: '#table', + }, + { + path: 'admin/plugins/manage', + react: async () => import('../views/admin/PluginsManagement'), + el: '.content > .container-fluid', + }, + { + path: 'admin/plugins/market', + react: async () => import('../views/admin/PluginsMarket'), + el: '.content > .container-fluid', + }, + { + path: 'admin/update', + module: [async () => import('../views/admin/Update')], + }, + { + path: 'auth/login', + react: async () => import('../views/auth/Login'), + el: 'main', + }, + { + path: 'auth/register', + react: async () => import('../views/auth/Registration'), + el: 'main', + }, + { + path: 'auth/forgot', + react: async () => import('../views/auth/Forgot'), + el: 'main', + }, + { + path: 'auth/reset/(\\d+)', + react: async () => import('../views/auth/Reset'), + el: 'main', + }, + { + path: 'skinlib', + react: async () => import('../views/skinlib/SkinLibrary'), + el: '.content-wrapper', + }, + { + path: 'skinlib/show/(\\d+)', + react: async () => import('../views/skinlib/Show'), + el: '#side', + }, + { + path: 'skinlib/upload', + react: async () => import('../views/skinlib/Upload'), + el: '#file-input', + }, +]; diff --git a/resources/assets/src/scripts/textureUtils.ts b/resources/assets/src/scripts/textureUtils.ts index baaf3ea6..15c403a5 100644 --- a/resources/assets/src/scripts/textureUtils.ts +++ b/resources/assets/src/scripts/textureUtils.ts @@ -1,51 +1,51 @@ -import { loadSkinToCanvas } from 'skinview-utils' +import {loadSkinToCanvas} from 'skinview-utils'; /* istanbul ignore next */ function checkPixel( - context: CanvasRenderingContext2D, - x: number, - y: number, + context: CanvasRenderingContext2D, + x: number, + y: number, ): boolean { - const imageData = context.getImageData(x, y, 1, 1) + const imageData = context.getImageData(x, y, 1, 1); - return ( - imageData.data[0] === 0 && - imageData.data[1] === 0 && - imageData.data[2] === 0 - ) + return ( + imageData.data[0] === 0 + && imageData.data[1] === 0 + && imageData.data[2] === 0 + ); } /* istanbul ignore next */ -export function isAlex(texture: string): Promise { - return new Promise((resolve) => { - const image = new Image() - image.src = texture - image.onload = () => { - if (image.width !== image.height) { - resolve(false) - return - } +export async function isAlex(texture: string): Promise { + return new Promise(resolve => { + const image = new Image(); + image.src = texture; + image.addEventListener('load', () => { + if (image.width !== image.height) { + resolve(false); + return; + } - const canvas = document.createElement('canvas') - loadSkinToCanvas(canvas, image) + const canvas = document.createElement('canvas'); + loadSkinToCanvas(canvas, image); - const ratio = canvas.width / 64 - const context = canvas.getContext('2d') - if (!context) { - resolve(false) - return - } + const ratio = canvas.width / 64; + const context = canvas.getContext('2d'); + if (!context) { + resolve(false); + return; + } - for (let x = 46 * ratio; x < 48 * ratio; x += 1) { - for (let y = 52 * ratio; y < 64 * ratio; y += 1) { - if (!checkPixel(context, x, y)) { - resolve(false) - return - } - } - } + for (let x = 46 * ratio; x < 48 * ratio; x += 1) { + for (let y = 52 * ratio; y < 64 * ratio; y += 1) { + if (!checkPixel(context, x, y)) { + resolve(false); + return; + } + } + } - resolve(true) - } - }) + resolve(true); + }); + }); } diff --git a/resources/assets/src/scripts/toast.tsx b/resources/assets/src/scripts/toast.tsx index 2826807c..71ae55e6 100644 --- a/resources/assets/src/scripts/toast.tsx +++ b/resources/assets/src/scripts/toast.tsx @@ -1,93 +1,97 @@ -import React, { useState, useEffect } from 'react' -import ReactDOM from 'react-dom' -import { nanoid } from 'nanoid' -import * as emitter from './event' -import ToastBox, { ToastType } from '../components/Toast' +import React, {useState, useEffect} from 'react'; +import ReactDOM from 'react-dom'; +import {nanoid} from 'nanoid'; +import ToastBox, {type ToastType} from '../components/Toast'; +import * as emitter from './event'; -type QueueElement = { id: string; type: ToastType; message: string } -type ToastQueue = QueueElement[] +type QueueElement = {id: string; type: ToastType; message: string}; +type ToastQueue = QueueElement[]; -const TOAST_EVENT = Symbol('toast') -const CLEAR_EVENT = Symbol('clear') +const TOAST_EVENT = Symbol('toast'); +const CLEAR_EVENT = Symbol('clear'); export const ToastContainer: React.FC = () => { - const [queue, setQueue] = useState([]) + const [queue, setQueue] = useState([]); - const handleClose = (id: string) => { - setQueue((queue) => queue.filter((el) => el.id !== id)) - } + const handleClose = (id: string) => { + setQueue(queue => queue.filter(element => element.id !== id)); + }; - useEffect(() => { - const off1 = emitter.on(TOAST_EVENT, (toast: QueueElement) => { - setQueue((queue) => { - queue.push(toast) - return queue.slice() - }) + useEffect(() => { + const off1 = emitter.on(TOAST_EVENT, (toast: QueueElement) => { + setQueue(queue => { + queue.push(toast); + return [...queue]; + }); - setTimeout(() => { - handleClose(toast.id) - }, 3100) - }) - const off2 = emitter.on(CLEAR_EVENT, () => setQueue([])) + setTimeout(() => { + handleClose(toast.id); + }, 3100); + }); + const off2 = emitter.on(CLEAR_EVENT, () => { + setQueue([]); + }); - return () => { - off1() - off2() - } - }, []) + return () => { + off1(); + off2(); + }; + }, []); - return ( - <> - {queue.map((el, i) => ( - handleClose(el.id)} - > - {el.message} - - ))} - - ) -} + return ( + <> + {queue.map((element, i) => ( + { + handleClose(element.id); + }} + > + {element.message} + + ))} + + ); +}; export class Toast { - private container: HTMLDivElement + private readonly container: HTMLDivElement; - constructor(render?: (element: JSX.Element) => void) { - this.container = document.createElement('div') - document.body.appendChild(this.container) + constructor(render?: (element: JSX.Element) => void) { + this.container = document.createElement('div'); + document.body.append(this.container); - if (render) { - render() - } else { - ReactDOM.render(, this.container) - } - } + if (render) { + render(); + } else { + ReactDOM.render(, this.container); + } + } - success(message: string) { - emitter.emit(TOAST_EVENT, { id: nanoid(4), type: 'success', message }) - } + success(message: string) { + emitter.emit(TOAST_EVENT, {id: nanoid(4), type: 'success', message}); + } - info(message: string) { - emitter.emit(TOAST_EVENT, { id: nanoid(4), type: 'info', message }) - } + info(message: string) { + emitter.emit(TOAST_EVENT, {id: nanoid(4), type: 'info', message}); + } - warning(message: string) { - emitter.emit(TOAST_EVENT, { id: nanoid(4), type: 'warning', message }) - } + warning(message: string) { + emitter.emit(TOAST_EVENT, {id: nanoid(4), type: 'warning', message}); + } - error(message: string) { - emitter.emit(TOAST_EVENT, { id: nanoid(4), type: 'error', message }) - } + error(message: string) { + emitter.emit(TOAST_EVENT, {id: nanoid(4), type: 'error', message}); + } - clear() { - emitter.emit(CLEAR_EVENT) - } + clear() { + emitter.emit(CLEAR_EVENT); + } - dispose() { - ReactDOM.unmountComponentAtNode(this.container) - this.container.remove() - } + dispose() { + ReactDOM.unmountComponentAtNode(this.container); + this.container.remove(); + } } diff --git a/resources/assets/src/scripts/types.ts b/resources/assets/src/scripts/types.ts index 9ab3706f..5d0d7cef 100644 --- a/resources/assets/src/scripts/types.ts +++ b/resources/assets/src/scripts/types.ts @@ -1,61 +1,61 @@ export type User = { - uid: number - email: string - nickname: string - locale: string | null - score: number - avatar: number - permission: UserPermission - ip: string - is_dark_mode: boolean - last_sign_at: string - register_at: string - verified: boolean -} + uid: number; + email: string; + nickname: string; + locale: string | undefined; + score: number; + avatar: number; + permission: UserPermission; + ip: string; + is_dark_mode: boolean; + last_sign_at: string; + register_at: string; + verified: boolean; +}; export const enum UserPermission { - Banned = -1, - Normal = 0, - Admin = 1, - SuperAdmin = 2, + Banned = -1, + Normal = 0, + Admin = 1, + SuperAdmin = 2, } export type Player = { - pid: number - name: string - uid: number - tid_skin: number - tid_cape: number - last_modified: string -} + pid: number; + name: string; + uid: number; + tid_skin: number; + tid_cape: number; + last_modified: string; +}; export type Texture = { - tid: number - name: string - type: TextureType - hash: string - size: number - uploader: number - public: boolean - upload_at: string - likes: number -} + tid: number; + name: string; + type: TextureType; + hash: string; + size: number; + uploader: number; + public: boolean; + upload_at: string; + likes: number; +}; export const enum TextureType { - Steve = 'steve', - Alex = 'alex', - Cape = 'cape', + Steve = 'steve', + Alex = 'alex', + Cape = 'cape', } export type ClosetItem = Texture & { - pivot: { user_uid: number; texture_tid: number; item_name: string } -} + pivot: {user_uid: number; texture_tid: number; item_name: string}; +}; export type Paginator = { - data: T[] - current_page: number - last_page: number - from: number - to: number - total: number -} + data: T[]; + current_page: number; + last_page: number; + from: number; + to: number; + total: number; +}; diff --git a/resources/assets/src/scripts/urls.ts b/resources/assets/src/scripts/urls.ts index 9bae27d6..1b474997 100644 --- a/resources/assets/src/scripts/urls.ts +++ b/resources/assets/src/scripts/urls.ts @@ -1,68 +1,68 @@ export default { - admin: { - players: { - delete: (player: number) => `/admin/players/${player}`, - list: () => '/admin/players/list' as const, - name: (player: number) => `/admin/players/${player}/name`, - owner: (player: number) => `/admin/players/${player}/owner`, - texture: (player: number) => `/admin/players/${player}/textures`, - }, - users: { - delete: (user: number) => `/admin/users/${user}`, - email: (user: number) => `/admin/users/${user}/email`, - list: () => '/admin/users/list' as const, - nickname: (user: number) => `/admin/users/${user}/nickname`, - password: (user: number) => `/admin/users/${user}/password`, - permission: (user: number) => `/admin/users/${user}/permission`, - score: (user: number) => `/admin/users/${user}/score`, - verification: (user: number) => `/admin/users/${user}/verification`, - }, - }, - auth: { - bind: () => '/auth/bind' as const, - forgot: () => '/auth/forgot' as const, - login: () => '/auth/login' as const, - logout: () => '/auth/logout' as const, - register: () => '/auth/register' as const, - reset: (uid: number) => `/auth/reset/${uid}`, - verify: (uid: number) => `/auth/verify/${uid}`, - }, - skinlib: { - home: () => '/skinlib' as const, - info: (texture: number) => `/skinlib/info/${texture}`, - list: () => '/skinlib/list' as const, - show: (tid: number) => `/skinlib/show/${tid}`, - }, - texture: { - delete: (texture: number) => `/texture/${texture}`, - info: (texture: number) => `/texture/${texture}`, - name: (texture: number) => `/texture/${texture}/name`, - privacy: (texture: number) => `/texture/${texture}/privacy`, - type: (texture: number) => `/texture/${texture}/type`, - upload: () => '/texture' as const, - }, - user: { - closet: { - add: () => '/user/closet' as const, - ids: () => '/user/closet/ids' as const, - list: () => '/user/closet/list' as const, - page: () => '/user/closet' as const, - remove: (tid: number) => `/user/closet/${tid}`, - rename: (tid: number) => `/user/closet/${tid}`, - }, - home: () => '/user' as const, - notification: (id: number) => `/user/notifications/${id}`, - player: { - add: () => '/user/player' as const, - clear: (player: number) => `/user/player/${player}/textures`, - delete: (player: number) => `/user/player/${player}`, - list: () => '/user/player/list' as const, - page: () => '/user/player' as const, - rename: (player: number) => `/user/player/${player}/name`, - set: (player: number) => `/user/player/${player}/textures`, - }, - profile: { avatar: () => '/user/profile/avatar' as const }, - score: () => '/user/score-info' as const, - sign: () => '/user/sign' as const, - }, -} + admin: { + players: { + delete: (player: number) => `/admin/players/${player}`, + list: () => '/admin/players/list' as const, + name: (player: number) => `/admin/players/${player}/name`, + owner: (player: number) => `/admin/players/${player}/owner`, + texture: (player: number) => `/admin/players/${player}/textures`, + }, + users: { + delete: (user: number) => `/admin/users/${user}`, + email: (user: number) => `/admin/users/${user}/email`, + list: () => '/admin/users/list' as const, + nickname: (user: number) => `/admin/users/${user}/nickname`, + password: (user: number) => `/admin/users/${user}/password`, + permission: (user: number) => `/admin/users/${user}/permission`, + score: (user: number) => `/admin/users/${user}/score`, + verification: (user: number) => `/admin/users/${user}/verification`, + }, + }, + auth: { + bind: () => '/auth/bind' as const, + forgot: () => '/auth/forgot' as const, + login: () => '/auth/login' as const, + logout: () => '/auth/logout' as const, + register: () => '/auth/register' as const, + reset: (uid: number) => `/auth/reset/${uid}`, + verify: (uid: number) => `/auth/verify/${uid}`, + }, + skinlib: { + home: () => '/skinlib' as const, + info: (texture: number) => `/skinlib/info/${texture}`, + list: () => '/skinlib/list' as const, + show: (tid: number) => `/skinlib/show/${tid}`, + }, + texture: { + delete: (texture: number) => `/texture/${texture}`, + info: (texture: number) => `/texture/${texture}`, + name: (texture: number) => `/texture/${texture}/name`, + privacy: (texture: number) => `/texture/${texture}/privacy`, + type: (texture: number) => `/texture/${texture}/type`, + upload: () => '/texture' as const, + }, + user: { + closet: { + add: () => '/user/closet' as const, + ids: () => '/user/closet/ids' as const, + list: () => '/user/closet/list' as const, + page: () => '/user/closet' as const, + remove: (tid: number) => `/user/closet/${tid}`, + rename: (tid: number) => `/user/closet/${tid}`, + }, + home: () => '/user' as const, + notification: (id: number) => `/user/notifications/${id}`, + player: { + add: () => '/user/player' as const, + clear: (player: number) => `/user/player/${player}/textures`, + delete: (player: number) => `/user/player/${player}`, + list: () => '/user/player/list' as const, + page: () => '/user/player' as const, + rename: (player: number) => `/user/player/${player}/name`, + set: (player: number) => `/user/player/${player}/textures`, + }, + profile: {avatar: () => '/user/profile/avatar' as const}, + score: () => '/user/score-info' as const, + sign: () => '/user/sign' as const, + }, +}; diff --git a/resources/assets/src/shims.d.ts b/resources/assets/src/shims.d.ts index afc17317..ce41198a 100644 --- a/resources/assets/src/shims.d.ts +++ b/resources/assets/src/shims.d.ts @@ -12,7 +12,7 @@ declare global { site_name: string version: string route: string - extra: any + extra: Record i18n: object fetch: { diff --git a/resources/assets/src/styles/breakpoints.ts b/resources/assets/src/styles/breakpoints.ts index 8334820c..966be311 100644 --- a/resources/assets/src/styles/breakpoints.ts +++ b/resources/assets/src/styles/breakpoints.ts @@ -1,19 +1,19 @@ export const enum Breakpoint { - xs = 0, - sm = 576, - md = 768, - lg = 992, - xl = 1200, + xs = 0, + sm = 576, + md = 768, + lg = 992, + xl = 1200, } export function lessThan(breakpoint: Breakpoint): string { - return `@media (max-width: ${breakpoint}px)` + return `@media (max-width: ${breakpoint}px)`; } export function between(down: Breakpoint, up: Breakpoint): string { - return `@media (min-width: ${down}px) and (max-width: ${up}px)` + return `@media (min-width: ${down}px) and (max-width: ${up}px)`; } export function greaterThan(breakpoint: Breakpoint): string { - return `@media (min-width: ${breakpoint}px)` + return `@media (min-width: ${breakpoint}px)`; } diff --git a/resources/assets/src/styles/utils.ts b/resources/assets/src/styles/utils.ts index c050957f..8aa92cbc 100644 --- a/resources/assets/src/styles/utils.ts +++ b/resources/assets/src/styles/utils.ts @@ -1,11 +1,11 @@ -import { css } from '@emotion/react' +import {css} from '@emotion/react'; export const pointerCursor = css` cursor: pointer; -` +`; export const center = css` display: flex; justify-content: center; align-items: center; -` +`; diff --git a/resources/assets/src/views/admin/Customization.ts b/resources/assets/src/views/admin/Customization.ts index cf39329e..4e0c6bf5 100644 --- a/resources/assets/src/views/admin/Customization.ts +++ b/resources/assets/src/views/admin/Customization.ts @@ -1,83 +1,83 @@ /* eslint-disable object-curly-newline */ -import { fromEvent, merge, of, partition } from 'rxjs' -import { filter, map, pairwise } from 'rxjs/operators' +import {fromEvent, merge, of, partition} from 'rxjs'; +import {filter, map, pairwise} from 'rxjs/operators'; export function registerNavbarPicker( - navbar: HTMLElement, - picker: HTMLDivElement, - init: string, + navbar: HTMLElement, + picker: HTMLDivElement, + init: string, ): void { - const color$ = fromEvent(picker, 'click').pipe( - map((event) => event.target as HTMLElement), - filter( - (element): element is HTMLInputElement => element.tagName === 'INPUT', - ), - map((element) => element.value), - ) + const color$ = fromEvent(picker, 'click').pipe( + map(event => event.target as HTMLElement), + filter( + (element): element is HTMLInputElement => element.tagName === 'INPUT', + ), + map(element => element.value), + ); - merge(of(init), color$) - .pipe(pairwise()) - .subscribe(([previous, current]) => { - navbar.classList.replace(`navbar-${previous}`, `navbar-${current}`) - }) + merge(of(init), color$) + .pipe(pairwise()) + .subscribe(([previous, current]) => { + navbar.classList.replace(`navbar-${previous}`, `navbar-${current}`); + }); - const [light$, dark$] = partition(color$, (color) => - ['light', 'warning', 'white', 'orange', 'lime'].includes(color), - ) - light$.subscribe(() => { - // DO NOT use `classList.replace`. - navbar.classList.remove('navbar-dark') - navbar.classList.add('navbar-light') - }) - dark$.subscribe(() => { - // DO NOT use `classList.replace`. - navbar.classList.remove('navbar-light') - navbar.classList.add('navbar-dark') - }) + const [light$, dark$] = partition(color$, color => + ['light', 'warning', 'white', 'orange', 'lime'].includes(color), + ); + light$.subscribe(() => { + // DO NOT use `classList.replace`. + navbar.classList.remove('navbar-dark'); + navbar.classList.add('navbar-light'); + }); + dark$.subscribe(() => { + // DO NOT use `classList.replace`. + navbar.classList.remove('navbar-light'); + navbar.classList.add('navbar-dark'); + }); } -const navbar = document.querySelector('.wrapper > nav') -const picker = document.querySelector('#navbar-color-picker') +const navbar = document.querySelector('.wrapper > nav'); +const picker = document.querySelector('#navbar-color-picker'); /* istanbul ignore next */ if (navbar && picker) { - registerNavbarPicker(navbar, picker, blessing.extra.navbar || 'white') + registerNavbarPicker(navbar, picker, blessing.extra.navbar as string || 'white'); } export function registerSidebarPicker( - sidebar: HTMLElement, - { dark, light }: { dark: HTMLDivElement; light: HTMLDivElement }, - init: string, + sidebar: HTMLElement, + {dark, light}: {dark: HTMLDivElement; light: HTMLDivElement}, + init: string, ): void { - const color$ = merge( - fromEvent(dark, 'click'), - fromEvent(light, 'click'), - ).pipe( - map((event) => event.target as HTMLElement), - filter( - (element): element is HTMLInputElement => element.tagName === 'INPUT', - ), - map((element) => element.value), - ) + const color$ = merge( + fromEvent(dark, 'click'), + fromEvent(light, 'click'), + ).pipe( + map(event => event.target as HTMLElement), + filter( + (element): element is HTMLInputElement => element.tagName === 'INPUT', + ), + map(element => element.value), + ); - merge(of(init), color$) - .pipe(pairwise()) - .subscribe(([previous, current]) => { - sidebar.classList.replace(`sidebar-${previous}`, `sidebar-${current}`) - }) + merge(of(init), color$) + .pipe(pairwise()) + .subscribe(([previous, current]) => { + sidebar.classList.replace(`sidebar-${previous}`, `sidebar-${current}`); + }); } -const sidebar = document.querySelector('.main-sidebar') +const sidebar = document.querySelector('.main-sidebar'); const darkPicker = document.querySelector( - '#sidebar-dark-picker', -) + '#sidebar-dark-picker', +); const lightPicker = document.querySelector( - '#sidebar-light-picker', -) + '#sidebar-light-picker', +); /* istanbul ignore next */ if (sidebar && darkPicker && lightPicker) { - registerSidebarPicker( - sidebar, - { dark: darkPicker, light: lightPicker }, - blessing.extra.sidebar || 'dark-primary', - ) + registerSidebarPicker( + sidebar, + {dark: darkPicker, light: lightPicker}, + blessing.extra.sidebar as string || 'dark-primary', + ); } diff --git a/resources/assets/src/views/admin/Dashboard.ts b/resources/assets/src/views/admin/Dashboard.ts index ae75c110..0272ce07 100644 --- a/resources/assets/src/views/admin/Dashboard.ts +++ b/resources/assets/src/views/admin/Dashboard.ts @@ -1,120 +1,120 @@ -import * as echarts from 'echarts/core' -import { SVGRenderer } from 'echarts/renderers' -import { LineChart } from 'echarts/charts' +import * as echarts from 'echarts/core'; +import {SVGRenderer} from 'echarts/renderers'; +import {LineChart} from 'echarts/charts'; import { - DataZoomComponent, - GridComponent, - TitleComponent, - TooltipComponent, -} from 'echarts/components' -import { get } from '../../scripts/net' + DataZoomComponent, + GridComponent, + TitleComponent, + TooltipComponent, +} from 'echarts/components'; +import {get} from '../../scripts/net'; -interface ChartData { - labels: string[] - xAxis: string[] - data: number[][] -} +type ChartData = { + labels: string[]; + xAxis: string[]; + data: number[][]; +}; -interface SingleChartData { - label: string - xAxis: string[] - data: number[] -} +type SingleChartData = { + label: string; + xAxis: string[]; + data: number[]; +}; echarts.use([ - SVGRenderer, - LineChart, - DataZoomComponent, - GridComponent, - TitleComponent, - TooltipComponent, -]) + SVGRenderer, + LineChart, + DataZoomComponent, + GridComponent, + TitleComponent, + TooltipComponent, +]); async function main() { - const elUsersRegistration = document.querySelector( - '#chart-users-registration', - ) - const elTexturesUpload = document.querySelector( - '#chart-textures-upload', - ) - if (!elUsersRegistration || !elTexturesUpload) { - return - } + const elementUsersRegistration = document.querySelector( + '#chart-users-registration', + ); + const elementTexturesUpload = document.querySelector( + '#chart-textures-upload', + ); + if (!elementUsersRegistration || !elementTexturesUpload) { + return; + } - const isDarkMode = document.body.classList.contains('dark-mode') - const textColor = isDarkMode ? '#fff' : '#000' + const isDarkMode = document.body.classList.contains('dark-mode'); + const textColor = isDarkMode ? '#fff' : '#000'; - const chartData: ChartData = await get('/admin/chart') - createLineChart( - elUsersRegistration, - isDarkMode ? '#3498db' : '#17a2b8', - textColor, - { - label: chartData.labels[0]!, - xAxis: chartData.xAxis, - data: chartData.data[0]!, - }, - ) - createLineChart(elTexturesUpload, '#6f42c1', textColor, { - label: chartData.labels[1]!, - xAxis: chartData.xAxis, - data: chartData.data[1]!, - }) + const chartData: ChartData = await get('/admin/chart'); + createLineChart( + elementUsersRegistration, + isDarkMode ? '#3498db' : '#17a2b8', + textColor, + { + label: chartData.labels[0], + xAxis: chartData.xAxis, + data: chartData.data[0], + }, + ); + createLineChart(elementTexturesUpload, '#6f42c1', textColor, { + label: chartData.labels[1], + xAxis: chartData.xAxis, + data: chartData.data[1], + }); } function createLineChart( - el: HTMLDivElement, - color: string, - textColor: string, - data: SingleChartData, + element: HTMLDivElement, + color: string, + textColor: string, + data: SingleChartData, ) { - const chart = echarts.init(el) - chart.setOption({ - title: { - text: data.label, - textStyle: { - color: textColor, - }, - }, - textStyle: { - color: textColor, - }, - tooltip: { - trigger: 'axis', - }, - dataZoom: [ - { type: 'inside', start: 75 }, - { type: 'slider', start: 75 }, - ], - xAxis: [ - { - type: 'category', - boundaryGap: false, - data: data.xAxis, - }, - ], - yAxis: [ - { - type: 'value', - minInterval: 1, - boundaryGap: false, - }, - ], - series: [ - { - name: data.label, - type: 'line', - itemStyle: { - color, - }, - areaStyle: { - color, - }, - data: data.data, - smooth: true, - }, - ], - }) + const chart = echarts.init(element); + chart.setOption({ + title: { + text: data.label, + textStyle: { + color: textColor, + }, + }, + textStyle: { + color: textColor, + }, + tooltip: { + trigger: 'axis', + }, + dataZoom: [ + {type: 'inside', start: 75}, + {type: 'slider', start: 75}, + ], + xAxis: [ + { + type: 'category', + boundaryGap: false, + data: data.xAxis, + }, + ], + yAxis: [ + { + type: 'value', + minInterval: 1, + boundaryGap: false, + }, + ], + series: [ + { + name: data.label, + type: 'line', + itemStyle: { + color, + }, + areaStyle: { + color, + }, + data: data.data, + smooth: true, + }, + ], + }); } -main() +main(); diff --git a/resources/assets/src/views/admin/PlayersManagement/Card.tsx b/resources/assets/src/views/admin/PlayersManagement/Card.tsx index 66079f4d..1bb05793 100644 --- a/resources/assets/src/views/admin/PlayersManagement/Card.tsx +++ b/resources/assets/src/views/admin/PlayersManagement/Card.tsx @@ -1,163 +1,163 @@ -import React from 'react' -import { t } from '@/scripts/i18n' -import { showModal } from '@/scripts/notify' -import type { Player } from '@/scripts/types' -import { Box } from './styles' -import clsx from 'clsx' +import React from 'react'; +import clsx from 'clsx'; +import {Box} from './styles'; +import {t} from '@/scripts/i18n'; +import {showModal} from '@/scripts/notify'; +import type {Player} from '@/scripts/types'; -interface Props { - player: Player - onUpdateName(): void - onUpdateOwner(): void - onUpdateTexture(): void - onDelete(): void -} +type Properties = { + readonly player: Player; + onUpdateName(): void; + onUpdateOwner(): void; + onUpdateTexture(): void; + onDelete(): void; +}; -const Card: React.FC = (props) => { - const { player } = props +const Card: React.FC = properties => { + const {player} = properties; - const handlePreviewTextures = () => { - const skinPreview = `${blessing.base_url}/preview/${player.tid_skin}` - const skinPreviewPNG = `${skinPreview}?png` - const capePreview = `${blessing.base_url}/preview/${player.tid_cape}` - const capePreviewPNG = `${capePreview}?png` + const handlePreviewTextures = () => { + const skinPreview = `${blessing.base_url}/preview/${player.tid_skin}`; + const skinPreviewPNG = `${skinPreview}?png`; + const capePreview = `${blessing.base_url}/preview/${player.tid_cape}`; + const capePreviewPNG = `${capePreview}?png`; - showModal({ - mode: 'alert', - title: t('general.player.previews'), - children: ( -
    -
    - {player.tid_skin > 0 && ( - - - - {`${player.name} - - - )} -
    -
    - {player.tid_cape > 0 && ( - - - - {`${player.name} - - - )} -
    -
    - ), - }) - } + showModal({ + mode: 'alert', + title: t('general.player.previews'), + children: ( +
    +
    + {player.tid_skin > 0 && ( + + + + {`${player.name} + + + )} +
    +
    + {player.tid_cape > 0 && ( + + + + {`${player.name} + + + )} +
    +
    + ), + }); + }; - const isDarkMode = document.body.classList.contains('dark-mode') + const isDarkMode = document.body.classList.contains('dark-mode'); - const avatar = `${blessing.base_url}/avatar/player/${player.name}` - const avatarPNG = `${avatar}?png` + const avatar = `${blessing.base_url}/avatar/player/${player.name}`; + const avatarPNG = `${avatar}?png`; - return ( - -
    - - - - -
    -
    - -
    -
    - PID: {player.pid} - - {t('general.player.owner')}: {player.uid} - -
    -
    - - {`${t('general.player.last-modified')}: `} - {player.last_modified} - -
    -
    -
    -
    - ) -} + return ( + +
    + + + + +
    +
    +
    +
    + {player.name} +
    +
    + +
    +
    + PID: {player.pid} + + {t('general.player.owner')}: {player.uid} + +
    +
    + + {`${t('general.player.last-modified')}: `} + {player.last_modified} + +
    +
    +
    + + ); +}; -export default Card +export default Card; diff --git a/resources/assets/src/views/admin/PlayersManagement/LoadingCard.tsx b/resources/assets/src/views/admin/PlayersManagement/LoadingCard.tsx index b83ad5b3..43ad9e01 100644 --- a/resources/assets/src/views/admin/PlayersManagement/LoadingCard.tsx +++ b/resources/assets/src/views/admin/PlayersManagement/LoadingCard.tsx @@ -1,37 +1,37 @@ -import React from 'react' -import styled from '@emotion/styled' -import Skeleton from 'react-loading-skeleton' -import { Box } from './styles' -import clsx from 'clsx' +import React from 'react'; +import styled from '@emotion/styled'; +import Skeleton from 'react-loading-skeleton'; +import clsx from 'clsx'; +import {Box} from './styles'; -const isDarkMode = document.body.classList.contains('dark-mode') +const isDarkMode = document.body.classList.contains('dark-mode'); -const ShrinkedSkeleton = styled(Skeleton)<{ width?: string }>` - width: ${(props) => props.width}; -` +const ShrinkedSkeleton = styled(Skeleton)<{width?: string}>` + width: ${properties => properties.width}; +`; const LoadingCard: React.FC = () => ( - -
    - -
    -
    -
    -
    - -
    -
    -
    -
    -
    - -
    -
    - -
    -
    -
    -
    -) + +
    + +
    +
    +
    +
    + +
    +
    +
    +
    +
    + +
    +
    + +
    +
    +
    + +); -export default LoadingCard +export default LoadingCard; diff --git a/resources/assets/src/views/admin/PlayersManagement/LoadingRow.tsx b/resources/assets/src/views/admin/PlayersManagement/LoadingRow.tsx index b7043a28..af7c1a62 100644 --- a/resources/assets/src/views/admin/PlayersManagement/LoadingRow.tsx +++ b/resources/assets/src/views/admin/PlayersManagement/LoadingRow.tsx @@ -1,17 +1,17 @@ -import React from 'react' -import styled from '@emotion/styled' -import Skeleton from 'react-loading-skeleton' +import React from 'react'; +import styled from '@emotion/styled'; +import Skeleton from 'react-loading-skeleton'; const ThickSkeleton = styled(Skeleton)` line-height: 2; -` +`; const LoadingRow: React.FC = () => ( - - - - - -) + + + + + +); -export default LoadingRow +export default LoadingRow; diff --git a/resources/assets/src/views/admin/PlayersManagement/ModalUpdateTexture.tsx b/resources/assets/src/views/admin/PlayersManagement/ModalUpdateTexture.tsx index e70a4499..2927d008 100644 --- a/resources/assets/src/views/admin/PlayersManagement/ModalUpdateTexture.tsx +++ b/resources/assets/src/views/admin/PlayersManagement/ModalUpdateTexture.tsx @@ -1,84 +1,84 @@ -import React, { useState } from 'react' -import { t } from '@/scripts/i18n' -import { TextureType } from '@/scripts/types' -import Modal from '@/components/Modal' +import React, {useState} from 'react'; +import {t} from '@/scripts/i18n'; +import {TextureType} from '@/scripts/types'; +import Modal from '@/components/Modal'; -interface Props { - open: boolean - onSubmit(type: 'skin' | 'cape', tid: number): void - onClose(): void -} +type Properties = { + readonly open: boolean; + onSubmit(type: 'skin' | 'cape', tid: number): void; + onClose(): void; +}; -const ModalUpdateTexture: React.FC = (props) => { - const [type, setType] = useState<'skin' | 'cape'>('skin') - const [tid, setTid] = useState('') +const ModalUpdateTexture: React.FC = properties => { + const [type, setType] = useState<'skin' | 'cape'>('skin'); + const [tid, setTid] = useState(''); - const handleTypeChange = (event: React.ChangeEvent) => { - setType(event.target.value as 'skin' | 'cape') - } + const handleTypeChange = (event: React.ChangeEvent) => { + setType(event.target.value as 'skin' | 'cape'); + }; - const handleTidChange = (event: React.ChangeEvent) => { - setTid(event.target.value) - } + const handleTidChange = (event: React.ChangeEvent) => { + setTid(event.target.value); + }; - const handleConfirm = () => { - props.onSubmit(type, Number.parseInt(tid)) - setType('skin') - setTid('') - } + const handleConfirm = () => { + properties.onSubmit(type, Number.parseInt(tid)); + setType('skin'); + setTid(''); + }; - const handleClose = () => { - setType('skin') - setTid('') - props.onClose() - } + const handleClose = () => { + setType('skin'); + setTid(''); + properties.onClose(); + }; - return ( - -
    - -
    - - -
    -
    -
    - - -
    -
    - ) -} + return ( + +
    + +
    + + +
    +
    +
    + + +
    +
    + ); +}; -export default ModalUpdateTexture +export default ModalUpdateTexture; diff --git a/resources/assets/src/views/admin/PlayersManagement/Row.tsx b/resources/assets/src/views/admin/PlayersManagement/Row.tsx index d24b65db..d811ff96 100644 --- a/resources/assets/src/views/admin/PlayersManagement/Row.tsx +++ b/resources/assets/src/views/admin/PlayersManagement/Row.tsx @@ -1,81 +1,81 @@ -import React from 'react' -import { t } from '@/scripts/i18n' -import type { Player } from '@/scripts/types' -import ButtonEdit from '@/components/ButtonEdit' +import React from 'react'; +import {t} from '@/scripts/i18n'; +import type {Player} from '@/scripts/types'; +import ButtonEdit from '@/components/ButtonEdit'; -interface Props { - player: Player - onUpdateName(): void - onUpdateOwner(): void - onUpdateTexture(): void - onDelete(): void -} +type Properties = { + readonly player: Player; + onUpdateName(): void; + onUpdateOwner(): void; + onUpdateTexture(): void; + onDelete(): void; +}; -const Row: React.FC = (props) => { - const { player } = props +const Row: React.FC = properties => { + const {player} = properties; - return ( - - {player.pid} - - {player.name} - - - - - - {player.uid} - - - - - - {player.tid_skin > 0 && ( - - {`${player.name} - - )} - {player.tid_cape > 0 && ( - - {`${player.name} - - )} - - {player.last_modified} - - - - - - ) -} + return ( + + {player.pid} + + {player.name} + + + + + + {player.uid} + + + + + + {player.tid_skin > 0 && ( + + {`${player.name} + + )} + {player.tid_cape > 0 && ( + + {`${player.name} + + )} + + {player.last_modified} + + + + + + ); +}; -export default Row +export default Row; diff --git a/resources/assets/src/views/admin/PlayersManagement/index.tsx b/resources/assets/src/views/admin/PlayersManagement/index.tsx index 69b21c86..e4609b14 100644 --- a/resources/assets/src/views/admin/PlayersManagement/index.tsx +++ b/resources/assets/src/views/admin/PlayersManagement/index.tsx @@ -1,271 +1,277 @@ -import React, { useState, useEffect, useLayoutEffect } from 'react' -import { hot } from 'react-hot-loader/root' -import { useImmer } from 'use-immer' -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 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 React, {useState, useEffect, useLayoutEffect} from 'react'; +import {hot} from 'react-hot-loader/root'; +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'; const PlayersManagement: React.FC = () => { - const [players, setPlayers] = useImmer([]) - const [page, setPage] = useState(1) - const [totalPages, setTotalPages] = useState(1) - const [isLoading, setIsLoading] = useState(false) - const isLargeScreen = useIsLargeScreen() - const [isTableMode, setIsTableMode] = useState(false) - const [query, setQuery] = useState('') - const [textureUpdating, setTextureUpdating] = useState(-1) + const [players, setPlayers] = useImmer([]); + const [page, setPage] = useState(1); + const [totalPages, setTotalPages] = useState(1); + const [isLoading, setIsLoading] = useState(false); + const isLargeScreen = useIsLargeScreen(); + const [isTableMode, setIsTableMode] = useState(false); + const [query, setQuery] = useState(''); + const [textureUpdating, setTextureUpdating] = useState(-1); - useLayoutEffect(() => { - if (isLargeScreen) { - setIsTableMode(true) - } - }, [isLargeScreen]) + useLayoutEffect(() => { + if (isLargeScreen) { + setIsTableMode(true); + } + }, [isLargeScreen]); - const getPlayers = async () => { - setIsLoading(true) - const { data, last_page }: Paginator = await fetch.get( - urls.admin.players.list(), - { - q: query, - page, - }, - ) - setTotalPages(last_page) - setPlayers(() => data) - setIsLoading(false) - } + const getPlayers = async () => { + setIsLoading(true); + const {data, last_page}: Paginator = await fetch.get( + urls.admin.players.list(), + { + q: query, + page: page.toString(), + }, + ); + setTotalPages(last_page); + setPlayers(() => data); + setIsLoading(false); + }; - useEffect(() => { - getPlayers() - }, [page]) + useEffect(() => { + getPlayers(); + }, [page]); - const handleModeChange = (event: React.ChangeEvent) => { - setIsTableMode(event.target.value === 'table') - } + const handleModeChange = (event: React.ChangeEvent) => { + setIsTableMode(event.target.value === 'table'); + }; - const handleQueryChange = (event: React.ChangeEvent) => { - setQuery(event.target.value) - } + const handleQueryChange = (event: React.ChangeEvent) => { + setQuery(event.target.value); + }; - const handleSubmitQuery = (event: React.FormEvent) => { - event.preventDefault() - getPlayers() - } + const handleSubmitQuery = (event: React.FormEvent) => { + event.preventDefault(); + getPlayers(); + }; - const handleUpdateName = async (player: Player, index: number) => { - let name: string - try { - const { value } = await showModal({ - mode: 'prompt', - text: t('admin.changePlayerNameNotice'), - input: player.name, - validator: (value: string) => { - if (!value) { - return t('admin.emptyPlayerName') - } - }, - }) - name = value - } catch { - return - } + const handleUpdateName = async (player: Player, index: number) => { + let name: string; + try { + const {value} = await showModal({ + mode: 'prompt', + text: t('admin.changePlayerNameNotice'), + input: player.name, + validator(value: string) { + if (!value) { + return t('admin.emptyPlayerName'); + } + }, + }); + name = value; + } catch { + return; + } - const { code, message } = await fetch.put( - urls.admin.players.name(player.pid), - { player_name: name }, - ) - if (code === 0) { - toast.success(message) - setPlayers((players) => { - players[index]!.name = name - }) - } else { - toast.error(message) - } - } + const {code, message} = await fetch.put( + urls.admin.players.name(player.pid), + {player_name: name}, + ); + if (code === 0) { + toast.success(message); + setPlayers(players => { + players[index].name = name; + }); + } else { + toast.error(message); + } + }; - const handleUpdateOwner = async (player: Player, index: number) => { - let uid: number - try { - const { value } = await showModal({ - mode: 'prompt', - text: t('admin.changePlayerOwner'), - input: player.uid.toString(), - inputMode: 'numeric', - }) - uid = Number.parseInt(value) - } catch { - return - } + const handleUpdateOwner = async (player: Player, index: number) => { + let uid: number; + try { + const {value} = await showModal({ + mode: 'prompt', + text: t('admin.changePlayerOwner'), + input: player.uid.toString(), + inputMode: 'numeric', + }); + uid = Number.parseInt(value); + } catch { + return; + } - const { code, message } = await fetch.put( - urls.admin.players.owner(player.pid), - { uid }, - ) - if (code === 0) { - toast.success(message) - setPlayers((players) => { - players[index]!.uid = uid - }) - } else { - toast.error(message) - } - } + const {code, message} = await fetch.put( + urls.admin.players.owner(player.pid), + {uid}, + ); + if (code === 0) { + toast.success(message); + setPlayers(players => { + players[index].uid = uid; + }); + } else { + toast.error(message); + } + }; - const handleCloseModalUpdateTexture = () => setTextureUpdating(-1) + const handleCloseModalUpdateTexture = () => { + setTextureUpdating(-1); + }; - const handleUpdateTexture = async (type: 'skin' | 'cape', tid: number) => { - const { code, message } = await fetch.put( - urls.admin.players.texture(players[textureUpdating]!.pid), - { type, tid }, - ) + const handleUpdateTexture = async (type: 'skin' | 'cape', tid: number) => { + const {code, message} = await fetch.put( + urls.admin.players.texture(players[textureUpdating].pid), + {type, tid}, + ); - if (code === 0) { - toast.success(message) - setPlayers((players) => { - const field = `tid_${type}` as const - players[textureUpdating]![field] = tid - }) - } else { - toast.error(message) - } - } + if (code === 0) { + toast.success(message); + setPlayers(players => { + const field = `tid_${type}` as const; + players[textureUpdating][field] = tid; + }); + } else { + toast.error(message); + } + }; - const handleDelete = async (player: Player) => { - try { - await showModal({ - text: t('admin.deletePlayerNotice'), - okButtonType: 'danger', - }) - } catch { - return - } + const handleDelete = async (player: Player) => { + try { + await showModal({ + text: t('admin.deletePlayerNotice'), + okButtonType: 'danger', + }); + } catch { + return; + } - const { code, message } = await fetch.del( - urls.admin.players.delete(player.pid), - ) - if (code === 0) { - setPlayers((players) => players.filter(({ pid }) => pid !== player.pid)) - toast.success(message) - } else { - toast.error(message) - } - } + const {code, message} = await fetch.del( + urls.admin.players.delete(player.pid), + ); + if (code === 0) { + setPlayers(players => players.filter(({pid}) => pid !== player.pid)); + toast.success(message); + } else { + toast.error(message); + } + }; - return ( -
    -
    -
    - -
    - -
    -
    -
    - - -
    -
    - {players.length === 0 && !isLoading ? ( -
    {t('general.noResult')}
    - ) : isTableMode ? ( -
    - - - - - - - - - - - - - {isLoading - ? new Array(10).fill(null).map((_, i) => ) - : players.map((player, i) => ( - handleUpdateName(player, i)} - onUpdateOwner={() => handleUpdateOwner(player, i)} - onUpdateTexture={() => setTextureUpdating(i)} - onDelete={() => handleDelete(player)} - /> - ))} - -
    PID{t('general.player.player-name')}{t('general.player.owner')}{t('general.player.previews')}{t('general.player.last-modified')}{t('admin.operationsTitle')}
    -
    - ) : ( -
    - {isLoading - ? new Array(10).fill(null).map((_, i) => ) - : players.map((player, i) => ( - handleUpdateName(player, i)} - onUpdateOwner={() => handleUpdateOwner(player, i)} - onUpdateTexture={() => setTextureUpdating(i)} - onDelete={() => handleDelete(player)} - /> - ))} -
    - )} -
    -
    - -
    -
    - -1} - onSubmit={handleUpdateTexture} - onClose={handleCloseModalUpdateTexture} - /> -
    - ) -} + return ( +
    +
    +
    + +
    + +
    +
    +
    + + +
    +
    + {players.length === 0 && !isLoading ? ( +
    {t('general.noResult')}
    + ) : (isTableMode ? ( +
    + + + + + + + + + + + + + {isLoading + ? Array.from({length: 10}).fill(null).map((_, i) => ) + : players.map((player, i) => ( + handleUpdateName(player, i)} + onUpdateOwner={async () => handleUpdateOwner(player, i)} + onUpdateTexture={() => { + setTextureUpdating(i); + }} + onDelete={async () => handleDelete(player)} + /> + ))} + +
    PID{t('general.player.player-name')}{t('general.player.owner')}{t('general.player.previews')}{t('general.player.last-modified')}{t('admin.operationsTitle')}
    +
    + ) : ( +
    + {isLoading + ? Array.from({length: 10}).fill(null).map((_, i) => ) + : players.map((player, i) => ( + handleUpdateName(player, i)} + onUpdateOwner={async () => handleUpdateOwner(player, i)} + onUpdateTexture={() => { + setTextureUpdating(i); + }} + onDelete={async () => handleDelete(player)} + /> + ))} +
    + ))} +
    +
    + +
    +
    + -1} + onSubmit={handleUpdateTexture} + onClose={handleCloseModalUpdateTexture} + /> +
    + ); +}; -export default hot(PlayersManagement) +export default hot(PlayersManagement); diff --git a/resources/assets/src/views/admin/PlayersManagement/styles.ts b/resources/assets/src/views/admin/PlayersManagement/styles.ts index 20e33a5f..61392d27 100644 --- a/resources/assets/src/views/admin/PlayersManagement/styles.ts +++ b/resources/assets/src/views/admin/PlayersManagement/styles.ts @@ -1,5 +1,5 @@ -import styled from '@emotion/styled' -import * as breakpoints from '@/styles/breakpoints' +import styled from '@emotion/styled'; +import * as breakpoints from '@/styles/breakpoints'; export const Box = styled.div` width: 48%; @@ -8,4 +8,4 @@ export const Box = styled.div` ${breakpoints.lessThan(breakpoints.Breakpoint.lg)} { width: 98%; } -` +`; diff --git a/resources/assets/src/views/admin/PluginsManagement/InfoBox.tsx b/resources/assets/src/views/admin/PluginsManagement/InfoBox.tsx index 99bda687..3cb8d926 100644 --- a/resources/assets/src/views/admin/PluginsManagement/InfoBox.tsx +++ b/resources/assets/src/views/admin/PluginsManagement/InfoBox.tsx @@ -1,8 +1,8 @@ -import React from 'react' -import styled from '@emotion/styled' -import { t } from '@/scripts/i18n' -import type { Plugin } from './types' -import clsx from 'clsx' +import React from 'react'; +import styled from '@emotion/styled'; +import clsx from 'clsx'; +import type {Plugin} from './types'; +import {t} from '@/scripts/i18n'; const Box = styled.div` cursor: default; @@ -15,7 +15,7 @@ const Box = styled.div` .info-box-content { max-width: calc(100% - 70px); } -` +`; const ActionButton = styled.a` transition-property: color; transition-duration: 0.3s; @@ -29,99 +29,101 @@ const ActionButton = styled.a` &:not(:last-child) { margin-right: 9px; } -` +`; const Header = styled.div` max-width: calc(100% - 40px); display: flex; align-items: center; -` +`; const Description = styled.div` font-size: 14px; -` +`; -interface Props { - plugin: Plugin - onEnable(plugin: Plugin): void - onDisable(plugin: Plugin): void - onDelete(plugin: Plugin): void - baseUrl: string -} +type Properties = { + readonly plugin: Plugin; + onEnable(plugin: Plugin): void; + onDisable(plugin: Plugin): void; + onDelete(plugin: Plugin): void; + readonly baseUrl: string; +}; -const InfoBox: React.FC = (props) => { - const { plugin } = props +const InfoBox: React.FC = properties => { + const {plugin} = properties; - const handleChange = (event: React.ChangeEvent) => { - event.preventDefault() + const handleChange = (event: React.ChangeEvent) => { + event.preventDefault(); - if (event.target.checked) { - props.onEnable(plugin) - } else { - props.onDisable(plugin) - } - } + if (event.target.checked) { + properties.onEnable(plugin); + } else { + properties.onDisable(plugin); + } + }; - const handleDelete = () => props.onDelete(plugin) + const handleDelete = () => { + properties.onDelete(plugin); + }; - const isDarkMode = document.body.classList.contains('dark-mode') + const isDarkMode = document.body.classList.contains('dark-mode'); - return ( - - - - -
    -
    -
    - - - {plugin.title} - - - v{plugin.version} - -
    -
    - {plugin.readme && ( - - - - )} - {plugin.enabled && plugin.config && ( - - - - )} - - - -
    -
    - - {plugin.description} - -
    -
    - ) -} + return ( + + + + +
    +
    +
    + + + {plugin.title} + + + v{plugin.version} + +
    +
    + {plugin.readme && ( + + + + )} + {plugin.enabled && plugin.config && ( + + + + )} + + + +
    +
    + + {plugin.description} + +
    +
    + ); +}; -export default InfoBox +export default InfoBox; diff --git a/resources/assets/src/views/admin/PluginsManagement/index.tsx b/resources/assets/src/views/admin/PluginsManagement/index.tsx index e5dd4d88..0a364c7e 100644 --- a/resources/assets/src/views/admin/PluginsManagement/index.tsx +++ b/resources/assets/src/views/admin/PluginsManagement/index.tsx @@ -1,247 +1,248 @@ -import React, { useState, useEffect } from 'react' -import { hot } from 'react-hot-loader/root' -import { useImmer } from 'use-immer' -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 InfoBox from './InfoBox' -import type { Plugin } from './types' +import React, {useState, useEffect} from 'react'; +import {hot} from 'react-hot-loader/root'; +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'; const PluginsManagement: React.FC = () => { - const [isLoading, setIsLoading] = useState(true) - const [plugins, setPlugins] = useImmer([]) - const [file, setFile] = useState(null) - const [isUploading, setIsUploading] = useState(false) - const [url, setUrl] = useState('') - const [isDownloading, setIsDownloading] = useState(false) + const [isLoading, setIsLoading] = useState(true); + const [plugins, setPlugins] = useImmer([]); + const [file, setFile] = useState(null); + const [isUploading, setIsUploading] = useState(false); + const [url, setUrl] = useState(''); + const [isDownloading, setIsDownloading] = useState(false); - useEffect(() => { - const getPlugins = async () => { - setIsLoading(true) - const plugins = await fetch.get('/admin/plugins/data') - setPlugins(() => plugins) - setIsLoading(false) - } - getPlugins() - }, []) + useEffect(() => { + const getPlugins = async () => { + setIsLoading(true); + const plugins = await fetch.get('/admin/plugins/data'); + setPlugins(() => plugins); + setIsLoading(false); + }; - const handleEnable = async (plugin: Plugin, i: number) => { - const { - code, - message, - data: { reason } = { reason: [] }, - } = await fetch.post< - fetch.ResponseBody<{ - reason: string[] - }> - >('/admin/plugins/manage', { - action: 'enable', - name: plugin.name, - }) - if (code === 0) { - toast.success(message) - setPlugins((plugins) => { - plugins[i]!.enabled = true - }) - } else { - showModal({ - mode: 'alert', - children: ( -
    -

    {message}

    -
      - {reason.map((t, i) => ( -
    • {t}
    • - ))} -
    -
    - ), - }) - } - } + getPlugins(); + }, []); - const handleDisable = async (plugin: Plugin, i: number) => { - const { code, message } = await fetch.post( - '/admin/plugins/manage', - { - action: 'disable', - name: plugin.name, - }, - ) - if (code === 0) { - toast.success(message) - setPlugins((plugins) => { - plugins[i]!.enabled = false - }) - } else { - toast.error(message) - } - } + const handleEnable = async (plugin: Plugin, i: number) => { + const { + code, + message, + data: {reason} = {reason: []}, + } = await fetch.post< + fetch.ResponseBody<{ + reason: string[]; + }> + >('/admin/plugins/manage', { + action: 'enable', + name: plugin.name, + }); + if (code === 0) { + toast.success(message); + setPlugins(plugins => { + plugins[i].enabled = true; + }); + } else { + showModal({ + mode: 'alert', + children: ( +
    +

    {message}

    +
      + {reason.map((t, i) => ( +
    • {t}
    • + ))} +
    +
    + ), + }); + } + }; - const handleDelete = async (plugin: Plugin) => { - try { - await showModal({ - title: plugin.title, - text: t('admin.confirmDeletion'), - okButtonType: 'danger', - }) - } catch { - return - } + const handleDisable = async (plugin: Plugin, i: number) => { + const {code, message} = await fetch.post( + '/admin/plugins/manage', + { + action: 'disable', + name: plugin.name, + }, + ); + if (code === 0) { + toast.success(message); + setPlugins(plugins => { + plugins[i].enabled = false; + }); + } else { + toast.error(message); + } + }; - const { code, message } = await fetch.post( - '/admin/plugins/manage', - { - action: 'delete', - name: plugin.name, - }, - ) - if (code === 0) { - const { name } = plugin - setPlugins((plugins) => plugins.filter((plugin) => plugin.name !== name)) - toast.success(message) - } else { - toast.error(message) - } - } + const handleDelete = async (plugin: Plugin) => { + try { + await showModal({ + title: plugin.title, + text: t('admin.confirmDeletion'), + okButtonType: 'danger', + }); + } catch { + return; + } - const handleFileChange = (event: React.ChangeEvent) => { - setFile(event.target.files![0]!) - } + const {code, message} = await fetch.post( + '/admin/plugins/manage', + { + action: 'delete', + name: plugin.name, + }, + ); + if (code === 0) { + const {name} = plugin; + setPlugins(plugins => plugins.filter(plugin => plugin.name !== name)); + toast.success(message); + } else { + toast.error(message); + } + }; - const handleUrlChange = (event: React.ChangeEvent) => { - setUrl(event.target.value) - } + const handleFileChange = (event: React.ChangeEvent) => { + setFile(event.target.files![0]); + }; - const handleUpload = async () => { - if (!file) { - return - } + const handleUrlChange = (event: React.ChangeEvent) => { + setUrl(event.target.value); + }; - setIsUploading(true) - const formData = new FormData() - formData.append('file', file, file.name) - const { code, message } = await fetch.post( - '/admin/plugins/upload', - formData, - ) + const handleUpload = async () => { + if (!file) { + return; + } - setIsUploading(false) - if (code === 0) { - toast.success(message) - setFile(null) + setIsUploading(true); + const formData = new FormData(); + formData.append('file', file, file.name); + const {code, message} = await fetch.post( + '/admin/plugins/upload', + formData, + ); - const plugins = await fetch.get('/admin/plugins/data') - setPlugins(() => plugins) - } else { - toast.error(message) - } - } + setIsUploading(false); + if (code === 0) { + toast.success(message); + setFile(null); - const handleSubmitUrl = async () => { - setIsDownloading(true) - const { code, message } = await fetch.post( - '/admin/plugins/wget', - { url }, - ) + const plugins = await fetch.get('/admin/plugins/data'); + setPlugins(() => plugins); + } else { + toast.error(message); + } + }; - setIsDownloading(false) - if (code === 0) { - toast.success(message) - setUrl('') + const handleSubmitUrl = async () => { + setIsDownloading(true); + const {code, message} = await fetch.post( + '/admin/plugins/wget', + {url}, + ); - const plugins = await fetch.get('/admin/plugins/data') - setPlugins(() => plugins) - } else { - toast.error(message) - } - } + setIsDownloading(false); + if (code === 0) { + toast.success(message); + setUrl(''); - const chunks = Array(Math.ceil(plugins.length / 2)) - .fill(null) - .map((_, i) => plugins.slice(i * 2, (i + 1) * 2) as [Plugin, Plugin?]) + const plugins = await fetch.get('/admin/plugins/data'); + setPlugins(() => plugins); + } else { + toast.error(message); + } + }; - return ( -
    -
    - {isLoading ? ( - - ) : plugins.length === 0 ? ( - t('general.noResult') - ) : ( - chunks.map((chunk, i) => ( -
    - {(chunk as Plugin[]).map((plugin, j) => ( -
    - handleEnable(plugin, i * 2 + j)} - onDisable={(plugin) => handleDisable(plugin, i * 2 + j)} - onDelete={handleDelete} - baseUrl={blessing.base_url} - /> -
    - ))} -
    - )) - )} -
    -
    -
    -
    -

    {t('admin.uploadArchive')}

    -
    -
    -

    {t('admin.uploadArchiveNotice')}

    - -
    -
    - -
    -
    -
    -
    -

    {t('admin.downloadRemote')}

    -
    -
    -

    {t('admin.downloadRemoteNotice')}

    -
    - - -
    -
    -
    - -
    -
    -
    -
    - ) -} + const chunks = Array.from({length: Math.ceil(plugins.length / 2)}) + .fill(null) + .map((_, i) => plugins.slice(i * 2, (i + 1) * 2) as [Plugin, Plugin?]); -export default hot(PluginsManagement) + return ( +
    +
    + {isLoading ? ( + + ) : (plugins.length === 0 ? ( + t('general.noResult') + ) : ( + chunks.map((chunk, i) => ( +
    + {(chunk as Plugin[]).map((plugin, index) => ( +
    + handleEnable(plugin, i * 2 + index)} + onDisable={async plugin => handleDisable(plugin, i * 2 + index)} + onDelete={handleDelete} + /> +
    + ))} +
    + )) + ))} +
    +
    +
    +
    +

    {t('admin.uploadArchive')}

    +
    +
    +

    {t('admin.uploadArchiveNotice')}

    + +
    +
    + +
    +
    +
    +
    +

    {t('admin.downloadRemote')}

    +
    +
    +

    {t('admin.downloadRemoteNotice')}

    +
    + + +
    +
    +
    + +
    +
    +
    +
    + ); +}; + +export default hot(PluginsManagement); diff --git a/resources/assets/src/views/admin/PluginsManagement/types.ts b/resources/assets/src/views/admin/PluginsManagement/types.ts index 3ebf9d04..6e74e23b 100644 --- a/resources/assets/src/views/admin/PluginsManagement/types.ts +++ b/resources/assets/src/views/admin/PluginsManagement/types.ts @@ -1,10 +1,10 @@ export type Plugin = { - name: string - title: string - description: string - version: string - enabled: boolean - config: boolean - readme: boolean - icon: { fa: string; faType: 'fas' | 'fab'; bg: string } -} + name: string; + title: string; + description: string; + version: string; + enabled: boolean; + config: boolean; + readme: boolean; + icon: {fa: string; faType: 'fas' | 'fab'; bg: string}; +}; diff --git a/resources/assets/src/views/admin/PluginsMarket/Row.tsx b/resources/assets/src/views/admin/PluginsMarket/Row.tsx index 3cc1a575..c59caddb 100644 --- a/resources/assets/src/views/admin/PluginsMarket/Row.tsx +++ b/resources/assets/src/views/admin/PluginsMarket/Row.tsx @@ -1,92 +1,92 @@ -import React from 'react' -import { t } from '@/scripts/i18n' -import type { Plugin } from './types' +import React from 'react'; +import type {Plugin} from './types'; +import {t} from '@/scripts/i18n'; -interface Props { - plugin: Plugin - isInstalling: boolean - onInstall(): void - onUpdate(): void -} +type Properties = { + readonly plugin: Plugin; + readonly isInstalling: boolean; + onInstall(): void; + onUpdate(): void; +}; -const Row: React.FC = (props) => { - const { plugin, isInstalling } = props +const Row: React.FC = properties => { + const {plugin, isInstalling} = properties; - const allDeps = Object.entries(plugin.dependencies.all) - const unsatisfied = Object.keys(plugin.dependencies.unsatisfied) + const allDeps = Object.entries(plugin.dependencies.all); + const unsatisfied = Object.keys(plugin.dependencies.unsatisfied); - return ( - - -
    - {plugin.title} -
    -
    {plugin.name}
    - - {plugin.description} - {plugin.author} - {plugin.version} - - {allDeps.length === 0 ? ( - {t('admin.noDependencies')} - ) : ( -
    - {allDeps.map(([name, constraint]) => { - const classes = [ - 'mb-1', - 'badge', - `bg-${unsatisfied.includes(name) ? 'red' : 'green'}`, - ] - return ( - - {name}: {constraint} - - ) - })} -
    - )} - - - {plugin.can_update ? ( - - ) : ( - - )} - - - ) -} + return ( + + +
    + {plugin.title} +
    +
    {plugin.name}
    + + {plugin.description} + {plugin.author} + {plugin.version} + + {allDeps.length === 0 ? ( + {t('admin.noDependencies')} + ) : ( +
    + {allDeps.map(([name, constraint]) => { + const classes = [ + 'mb-1', + 'badge', + `bg-${unsatisfied.includes(name) ? 'red' : 'green'}`, + ]; + return ( + + {name}: {constraint} + + ); + })} +
    + )} + + + {plugin.can_update ? ( + + ) : ( + + )} + + + ); +}; -export default Row +export default Row; diff --git a/resources/assets/src/views/admin/PluginsMarket/index.tsx b/resources/assets/src/views/admin/PluginsMarket/index.tsx index ad7ce04e..9a47429c 100644 --- a/resources/assets/src/views/admin/PluginsMarket/index.tsx +++ b/resources/assets/src/views/admin/PluginsMarket/index.tsx @@ -1,167 +1,168 @@ -import React, { useState, useEffect, useMemo } from 'react' -import { hot } from 'react-hot-loader/root' -import { enableMapSet } from 'immer' -import { useImmer } from 'use-immer' -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 type { Plugin } from './types' -import Row from './Row' +import React, {useState, useEffect, useMemo} from 'react'; +import {hot} from 'react-hot-loader/root'; +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'; -enableMapSet() +enableMapSet(); const PluginsMarket: React.FC = () => { - const [plugins, setPlugins] = useImmer([]) - const [isLoading, setIsLoading] = useState(true) - const [search, setSearch] = useState('') - const [page, setPage] = useState(1) - const [totalPages, setTotalPages] = useState(1) - const [installings, setInstallings] = useImmer>(() => new Set()) + const [plugins, setPlugins] = useImmer([]); + const [isLoading, setIsLoading] = useState(true); + const [search, setSearch] = useState(''); + const [page, setPage] = useState(1); + const [totalPages, setTotalPages] = useState(1); + const [installings, setInstallings] = useImmer>(() => new Set()); - const searchedPlugins = useMemo( - () => - plugins.filter( - (plugin) => - plugin.name.includes(search) || plugin.title.includes(search), - ), - [plugins, search], - ) + const searchedPlugins = useMemo( + () => + plugins.filter( + plugin => + plugin.name.includes(search) || plugin.title.includes(search), + ), + [plugins, search], + ); - useEffect(() => { - const getPlugins = async () => { - setIsLoading(true) - const plugins = await fetch.get('/admin/plugins/market/list') - setPlugins(() => plugins) - setTotalPages(Math.ceil(plugins.length / 10)) - setIsLoading(false) - } - getPlugins() - }, []) + useEffect(() => { + const getPlugins = async () => { + setIsLoading(true); + const plugins = await fetch.get('/admin/plugins/market/list'); + setPlugins(() => plugins); + setTotalPages(Math.ceil(plugins.length / 10)); + setIsLoading(false); + }; - const handleSearchChange = (event: React.ChangeEvent) => { - const search = event.target.value - setSearch(search) - setPage(1) + getPlugins(); + }, []); - const searchedPlugins = plugins.filter( - (plugin) => plugin.name.includes(search) || plugin.title.includes(search), - ) - setTotalPages(Math.ceil(searchedPlugins.length / 10)) - } + const handleSearchChange = (event: React.ChangeEvent) => { + const search = event.target.value; + setSearch(search); + setPage(1); - const handleInstall = async (plugin: Plugin, index: number) => { - setInstallings((installings) => { - installings.add(plugin.name) - }) + const searchedPlugins = plugins.filter( + plugin => plugin.name.includes(search) || plugin.title.includes(search), + ); + setTotalPages(Math.ceil(searchedPlugins.length / 10)); + }; - const { - code, - message, - data = { reason: [] }, - } = await fetch.post>( - '/admin/plugins/market/download', - { - name: plugin.name, - }, - ) - if (code === 0) { - toast.success(message) - setPlugins((plugins) => { - plugins[index]!.can_update = false - plugins[index]!.installed = plugins[index]!.version - }) - } else { - showModal({ - mode: 'alert', - children: ( -
    -

    {message}

    -
      - {data.reason.map((t, i) => ( -
    • {t}
    • - ))} -
    -
    - ), - }) - } + const handleInstall = async (plugin: Plugin, index: number) => { + setInstallings(installings => { + installings.add(plugin.name); + }); - setInstallings((installings) => { - installings.delete(plugin.name) - }) - } + const { + code, + message, + data = {reason: []}, + } = await fetch.post>( + '/admin/plugins/market/download', + { + name: plugin.name, + }, + ); + if (code === 0) { + toast.success(message); + setPlugins(plugins => { + plugins[index].can_update = false; + plugins[index].installed = plugins[index].version; + }); + } else { + showModal({ + mode: 'alert', + children: ( +
    +

    {message}

    +
      + {data.reason.map((t, i) => ( +
    • {t}
    • + ))} +
    +
    + ), + }); + } - const handleUpdate = async (plugin: Plugin, index: number) => { - try { - await showModal({ - text: t('admin.confirmUpdate', { - plugin: plugin.title, - old: plugin.installed, - new: plugin.version, - }), - }) - } catch { - return - } + setInstallings(installings => { + installings.delete(plugin.name); + }); + }; - handleInstall(plugin, index) - } + const handleUpdate = async (plugin: Plugin, index: number) => { + try { + await showModal({ + text: t('admin.confirmUpdate', { + plugin: plugin.title, + old: plugin.installed.toString(), + new: plugin.version, + }), + }); + } catch { + return; + } - const pagedPlugins = searchedPlugins.slice((page - 1) * 10, page * 10) + handleInstall(plugin, index); + }; - return ( -
    -
    - -
    - {isLoading ? ( -
    - -
    - ) : searchedPlugins.length === 0 ? ( -
    {t('general.noResult')}
    - ) : ( -
    - - - - - - - - - - - - - {pagedPlugins.map((plugin, i) => ( - handleInstall(plugin, (page - 1) * 10 + i)} - onUpdate={() => handleUpdate(plugin, (page - 1) * 10 + i)} - /> - ))} - -
    {t('admin.pluginTitle')}{t('admin.pluginDescription')}{t('admin.pluginAuthor')}{t('admin.pluginVersion')}{t('admin.pluginDependencies')}{t('admin.operationsTitle')}
    -
    - )} -
    -
    - -
    -
    -
    - ) -} + const pagedPlugins = searchedPlugins.slice((page - 1) * 10, page * 10); -export default hot(PluginsMarket) + return ( +
    +
    + +
    + {isLoading ? ( +
    + +
    + ) : (searchedPlugins.length === 0 ? ( +
    {t('general.noResult')}
    + ) : ( +
    + + + + + + + + + + + + + {pagedPlugins.map((plugin, i) => ( + handleInstall(plugin, (page - 1) * 10 + i)} + onUpdate={async () => handleUpdate(plugin, (page - 1) * 10 + i)} + /> + ))} + +
    {t('admin.pluginTitle')}{t('admin.pluginDescription')}{t('admin.pluginAuthor')}{t('admin.pluginVersion')}{t('admin.pluginDependencies')}{t('admin.operationsTitle')}
    +
    + ))} +
    +
    + +
    +
    +
    + ); +}; + +export default hot(PluginsMarket); diff --git a/resources/assets/src/views/admin/PluginsMarket/types.ts b/resources/assets/src/views/admin/PluginsMarket/types.ts index c97ef8a9..cbd97989 100644 --- a/resources/assets/src/views/admin/PluginsMarket/types.ts +++ b/resources/assets/src/views/admin/PluginsMarket/types.ts @@ -1,13 +1,13 @@ export type Plugin = { - name: string - version: string - title: string - description: string - author: string - installed: string | false - can_update?: boolean - dependencies: { - all: Record - unsatisfied: Record - } -} + name: string; + version: string; + title: string; + description: string; + author: string; + installed: string | false; + can_update?: boolean; + dependencies: { + all: Record; + unsatisfied: Record; + }; +}; diff --git a/resources/assets/src/views/admin/ReportsManagement/ImageBox.tsx b/resources/assets/src/views/admin/ReportsManagement/ImageBox.tsx index 94b73637..01f018e9 100644 --- a/resources/assets/src/views/admin/ReportsManagement/ImageBox.tsx +++ b/resources/assets/src/views/admin/ReportsManagement/ImageBox.tsx @@ -1,8 +1,8 @@ -import React from 'react' -import styled from '@emotion/styled' -import { t } from '@/scripts/i18n' -import type { Texture } from '@/scripts/types' -import { Report, Status } from './types' +import React from 'react'; +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; @@ -27,121 +27,123 @@ const Card = styled.div` margin: 2.5px 0; } } -` +`; -interface Props { - report: Report - onClick(texture: Texture | null): void - onBan(): void - onDelete(): void - onReject(): void -} +type Properties = { + readonly report: Report; + onClick(texture: Texture | undefined): void; + onBan(): void; + onDelete(): void; + onReject(): void; +}; -const ImageBox: React.FC = (props) => { - const { report } = props - const preview = `${blessing.base_url}/preview/${report.tid}?height=150` - const previewPNG = `${preview}&png` +const ImageBox: React.FC = properties => { + const {report} = properties; + const preview = `${blessing.base_url}/preview/${report.tid}?height=150`; + const previewPNG = `${preview}&png`; - const handleImageClick = () => props.onClick(report.texture) + const handleImageClick = () => { + properties.onClick(report.texture); + }; - return ( - -
    - - {t('skinlib.show.uploader')} - {': '} - - {report.texture_uploader?.nickname} - (UID: {report.uploader}) -
    -
    - - - {report.tid.toString()} - -
    -
    -
    -
    - {report.status === Status.Pending ? ( - {t('report.status.0')} - ) : report.status === Status.Resolved ? ( - {t('report.status.1')} - ) : ( - {t('report.status.2')} - )} - TID: {report.tid} -
    - -
    -
    - - {t('report.reporter')} - {': '} - - {report.informer?.nickname} - (UID: {report.reporter}) -
    -
    - - - {t('report.reason')} - {': '} - - {report.reason} - -
    {report.reason}
    -
    - - {t('report.time')} - {': '} - {report.report_at} - -
    -
    -
    -
    - ) -} + return ( + +
    + + {t('skinlib.show.uploader')} + {': '} + + {report.texture_uploader?.nickname} + (UID: {report.uploader}) +
    +
    + + + {report.tid.toString()} + +
    +
    +
    +
    + {report.status === Status.Pending ? ( + {t('report.status.0')} + ) : (report.status === Status.Resolved ? ( + {t('report.status.1')} + ) : ( + {t('report.status.2')} + ))} + TID: {report.tid} +
    + +
    +
    + + {t('report.reporter')} + {': '} + + {report.informer?.nickname} + (UID: {report.reporter}) +
    +
    + + + {t('report.reason')} + {': '} + + {report.reason} + +
    {report.reason}
    +
    + + {t('report.time')} + {': '} + {report.report_at} + +
    +
    +
    +
    + ); +}; -export default ImageBox +export default ImageBox; diff --git a/resources/assets/src/views/admin/ReportsManagement/index.tsx b/resources/assets/src/views/admin/ReportsManagement/index.tsx index 1b9239fa..aa621ee1 100644 --- a/resources/assets/src/views/admin/ReportsManagement/index.tsx +++ b/resources/assets/src/views/admin/ReportsManagement/index.tsx @@ -1,155 +1,155 @@ -import React, { useState, useEffect } from 'react' -import { hot } from 'react-hot-loader/root' -import { useImmer } from 'use-immer' -import { t } from '@/scripts/i18n' -import * as fetch from '@/scripts/net' -import { Paginator, 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 type { Report, Status } from './types' -import ImageBox from './ImageBox' +import React, {useState, useEffect} from 'react'; +import {hot} from 'react-hot-loader/root'; +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'; -const Previewer = React.lazy(() => import('@/components/Viewer')) +const Previewer = React.lazy(async () => import('@/components/Viewer')); const ReportsManagement: React.FC = () => { - const [reports, setReports] = useImmer([]) - const [page, setPage] = useState(1) - const [totalPages, setTotalPages] = useState(1) - const [isLoading, setIsLoading] = useState(true) - const [query, setQuery] = useState('status:0 sort:-report_at') - const [viewingTexture, setViewingTexture] = useState(null) + const [reports, setReports] = useImmer([]); + const [page, setPage] = useState(1); + const [totalPages, setTotalPages] = useState(1); + const [isLoading, setIsLoading] = useState(true); + const [query, setQuery] = useState('status:0 sort:-report_at'); + const [viewingTexture, setViewingTexture] = useState(null); - const getReports = async () => { - setIsLoading(true) - const { data, last_page }: Paginator = await fetch.get( - '/admin/reports/list', - { - q: query, - page, - }, - ) - setTotalPages(last_page) - setReports(() => data) - setIsLoading(false) - } + const getReports = async () => { + setIsLoading(true); + const {data, last_page}: Paginator = await fetch.get( + '/admin/reports/list', + { + q: query, + page: page.toString(), + }, + ); + setTotalPages(last_page); + setReports(() => data); + setIsLoading(false); + }; - useEffect(() => { - getReports() - }, [page]) + useEffect(() => { + getReports(); + }, [page]); - const handleQueryChange = (event: React.ChangeEvent) => { - setQuery(event.target.value) - } + const handleQueryChange = (event: React.ChangeEvent) => { + setQuery(event.target.value); + }; - const handleSubmitQuery = (event: React.FormEvent) => { - event.preventDefault() - getReports() - } + const handleSubmitQuery = (event: React.FormEvent) => { + event.preventDefault(); + getReports(); + }; - const handleProceedReport = async ( - report: Report, - index: number, - action: 'ban' | 'delete' | 'reject', - ) => { - type Ok = { code: 0; message: string; data: { status: Status } } - type Err = { code: 1; message: string } - const resp = await fetch.put(`/admin/reports/${report.id}`, { - action, - }) + const handleProceedReport = async ( + report: Report, + index: number, + action: 'ban' | 'delete' | 'reject', + ) => { + type Ok = {code: 0; message: string; data: {status: Status}}; + type Error_ = {code: 1; message: string}; + const resp = await fetch.put(`/admin/reports/${report.id}`, { + action, + }); if (resp.code === 0) { - toast.success(resp.message) - setReports((reports) => { - reports[index]!.status = resp.data.status - }) + toast.success(resp.message); + setReports(reports => { + reports[index].status = resp.data.status; + }); } else { - toast.error(resp.message) + toast.error(resp.message); } - } + }; - const handleDelete = async (report: Report, index: number) => { - try { - await showModal({ - text: t('skinlib.deleteNotice'), - okButtonType: 'danger', - }) - } catch { - return - } + const handleDelete = async (report: Report, index: number) => { + try { + await showModal({ + text: t('skinlib.deleteNotice'), + okButtonType: 'danger', + }); + } catch { + return; + } - handleProceedReport(report, index, 'delete') - } + handleProceedReport(report, index, 'delete'); + }; - const textureUrl = - viewingTexture && `${blessing.base_url}/textures/${viewingTexture.hash}` + const textureUrl + = viewingTexture && `${blessing.base_url}/textures/${viewingTexture.hash}`; - return ( -
    -
    -
    -
    -
    - -
    - -
    -
    -
    - {isLoading ? ( -
    - -
    - ) : reports.length === 0 ? ( -
    {t('general.noResult')}
    - ) : ( -
    - {reports.map((report, i) => ( - handleProceedReport(report, i, 'ban')} - onDelete={() => handleDelete(report, i)} - onReject={() => handleProceedReport(report, i, 'reject')} - /> - ))} -
    - )} -
    -
    - -
    -
    -
    -
    -
    - }> - - -
    -
    - ) -} + return ( +
    +
    +
    +
    +
    + +
    + +
    +
    +
    + {isLoading ? ( +
    + +
    + ) : (reports.length === 0 ? ( +
    {t('general.noResult')}
    + ) : ( +
    + {reports.map((report, i) => ( + handleProceedReport(report, i, 'ban')} + onDelete={async () => handleDelete(report, i)} + onReject={async () => handleProceedReport(report, i, 'reject')} + /> + ))} +
    + ))} +
    +
    + +
    +
    +
    +
    +
    + }> + + +
    +
    + ); +}; -export default hot(ReportsManagement) +export default hot(ReportsManagement); diff --git a/resources/assets/src/views/admin/ReportsManagement/types.ts b/resources/assets/src/views/admin/ReportsManagement/types.ts index 802ea9fe..35588b81 100644 --- a/resources/assets/src/views/admin/ReportsManagement/types.ts +++ b/resources/assets/src/views/admin/ReportsManagement/types.ts @@ -1,20 +1,20 @@ -import type { Texture, User } from '@/scripts/types' +import type {Texture, User} from '@/scripts/types'; export const enum Status { - Pending = 0, - Resolved = 1, - Rejected = 2, + Pending = 0, + Resolved = 1, + Rejected = 2, } export type Report = { - id: number - tid: number - texture: Texture | null - uploader: number - texture_uploader: User | null - reporter: number - informer: User | null - reason: string - status: Status - report_at: string -} + id: number; + tid: number; + texture: Texture | undefined; + uploader: number; + texture_uploader: User | undefined; + reporter: number; + informer: User | undefined; + reason: string; + status: Status; + report_at: string; +}; diff --git a/resources/assets/src/views/admin/Translations/Row.tsx b/resources/assets/src/views/admin/Translations/Row.tsx index 6b485d50..ea52954e 100644 --- a/resources/assets/src/views/admin/Translations/Row.tsx +++ b/resources/assets/src/views/admin/Translations/Row.tsx @@ -1,47 +1,51 @@ -import styled from '@emotion/styled' -import React from 'react' -import { t } from '@/scripts/i18n' -import type { Line } from './types' +import styled from '@emotion/styled'; +import React from 'react'; +import type {Line} from './types'; +import {t} from '@/scripts/i18n'; const Group = styled.td` width: 15%; -` +`; const Key = styled.td` width: 20%; -` +`; const Operations = styled.td` width: 25%; -` +`; -interface Props { - line: Line - onEdit(line: Line): void - onRemove(line: Line): void -} +type Properties = { + readonly line: Line; + onEdit(line: Line): void; + onRemove(line: Line): void; +}; -const Row: React.FC = (props) => { - const { line, onEdit, onRemove } = props - const text = line.text[blessing.locale] +const Row: React.FC = properties => { + const {line, onEdit, onRemove} = properties; + const text = line.text[blessing.locale]; - const handleEditClick = () => onEdit(line) + const handleEditClick = () => { + onEdit(line); + }; - const handleRemoveClick = () => onRemove(line) + const handleRemoveClick = () => { + onRemove(line); + }; - return ( - - {line.group} - {line.key} - {text || t('admin.i18n.empty')} - - - - - - ) -} + return ( + + {line.group} + {line.key} + {text || t('admin.i18n.empty')} + + + + + + ); +}; -export default Row +export default Row; diff --git a/resources/assets/src/views/admin/Translations/index.tsx b/resources/assets/src/views/admin/Translations/index.tsx index 04699989..054cb786 100644 --- a/resources/assets/src/views/admin/Translations/index.tsx +++ b/resources/assets/src/views/admin/Translations/index.tsx @@ -1,120 +1,121 @@ -import React, { useState, useEffect } from 'react' -import { hot } from 'react-hot-loader/root' -import { useImmer } from 'use-immer' -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 type { Line } from './types' -import Row from './Row' +import React, {useState, useEffect} from 'react'; +import {hot} from 'react-hot-loader/root'; +import {useImmer} from 'use-immer'; +import type {Line} from './types'; +import Row from './Row'; +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'; const Translations: React.FC = () => { - const [lines, setLines] = useImmer([]) - const [isLoading, setIsLoading] = useState(true) - const [page, setPage] = useState(1) - const [totalPages, setTotalPages] = useState(1) + const [lines, setLines] = useImmer([]); + const [isLoading, setIsLoading] = useState(true); + const [page, setPage] = useState(1); + const [totalPages, setTotalPages] = useState(1); - useEffect(() => { - const getLines = async () => { - setIsLoading(true) - const result = await fetch.get>('/admin/i18n/list', { - page, - }) - setLines(() => result.data) - setTotalPages(result.last_page) - setIsLoading(false) - } - getLines() - }, [page]) + useEffect(() => { + const getLines = async () => { + setIsLoading(true); + const result = await fetch.get>('/admin/i18n/list', { + page: page.toString(), + }); + setLines(() => result.data); + setTotalPages(result.last_page); + setIsLoading(false); + }; - const handleEdit = async (line: Line, index: number) => { - let text: string - try { - const { value } = await showModal({ - mode: 'prompt', - text: t('admin.i18n.updating'), - input: line.text[blessing.locale], - }) - text = value - } catch { - return - } + getLines(); + }, [page]); - const { code, message } = await fetch.put( - `/admin/i18n/${line.id}`, - { text }, - ) - if (code === 0) { - toast.success(message) - setLines((lines) => { - lines[index]!.text[blessing.locale] = text - }) - } else { - toast.error(message) - } - } + const handleEdit = async (line: Line, index: number) => { + let text: string; + try { + const {value} = await showModal({ + mode: 'prompt', + text: t('admin.i18n.updating'), + input: line.text[blessing.locale], + }); + text = value; + } catch { + return; + } - const handleRemove = async (line: Line) => { - try { - await showModal({ - text: t('admin.i18n.confirmDelete'), - okButtonType: 'danger', - }) - } catch { - return - } + const {code, message} = await fetch.put( + `/admin/i18n/${line.id}`, + {text}, + ); + if (code === 0) { + toast.success(message); + setLines(lines => { + lines[index].text[blessing.locale] = text; + }); + } else { + toast.error(message); + } + }; - const { message } = await fetch.del(`/admin/i18n/${line.id}`) - toast.success(message) - const { id } = line - setLines((lines) => lines.filter((line) => line.id !== id)) - } + const handleRemove = async (line: Line) => { + try { + await showModal({ + text: t('admin.i18n.confirmDelete'), + okButtonType: 'danger', + }); + } catch { + return; + } - return ( - <> -
    - - - - - - - - - - - {isLoading ? ( - - - - ) : lines.length === 0 ? ( - - - - ) : ( - lines.map((line, i) => ( - handleEdit(line, i)} - onRemove={handleRemove} - /> - )) - )} - -
    {t('admin.i18n.group')}{t('admin.i18n.key')}{t('admin.i18n.text')}{t('admin.operationsTitle')}
    - -
    - {t('general.noResult')} -
    -
    -
    - -
    - - ) -} + const {message} = await fetch.del(`/admin/i18n/${line.id}`); + toast.success(String(message)); + const {id} = line; + setLines(lines => lines.filter(line => line.id !== id)); + }; -export default hot(Translations) + return ( + <> +
    + + + + + + + + + + + {isLoading ? ( + + + + ) : (lines.length === 0 ? ( + + + + ) : ( + lines.map((line, i) => ( + handleEdit(line, i)} + onRemove={handleRemove} + /> + )) + ))} + +
    {t('admin.i18n.group')}{t('admin.i18n.key')}{t('admin.i18n.text')}{t('admin.operationsTitle')}
    + +
    + {t('general.noResult')} +
    +
    +
    + +
    + + ); +}; + +export default hot(Translations); diff --git a/resources/assets/src/views/admin/Translations/types.ts b/resources/assets/src/views/admin/Translations/types.ts index 20a08269..8d5015ee 100644 --- a/resources/assets/src/views/admin/Translations/types.ts +++ b/resources/assets/src/views/admin/Translations/types.ts @@ -1,6 +1,6 @@ export type Line = { - id: number - group: string - key: string - text: Record -} + id: number; + group: string; + key: string; + text: Record; +}; diff --git a/resources/assets/src/views/admin/Update.ts b/resources/assets/src/views/admin/Update.ts index f081e736..c521ae82 100644 --- a/resources/assets/src/views/admin/Update.ts +++ b/resources/assets/src/views/admin/Update.ts @@ -1,24 +1,24 @@ -import { post, ResponseBody } from '../../scripts/net' -import { showModal } from '../../scripts/notify' -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 button = event.target as HTMLButtonElement; + button.disabled = true; - const text = button.textContent - button.innerHTML = ` ${t( - 'admin.downloading', - )}` + const text = button.textContent; + button.innerHTML = ` ${t( + 'admin.downloading', + )}`; - const { code, message }: ResponseBody = await post('/admin/update/download') - button.textContent = text - button.disabled = false - await showModal({ mode: 'alert', text: message }) - if (code === 0) { - location.href = '/' - } + const {code, message}: ResponseBody = await post('/admin/update/download'); + button.textContent = text; + button.disabled = false; + await showModal({mode: 'alert', text: message}); + if (code === 0) { + location.href = '/'; + } } -const button = document.querySelector('#update') -button?.addEventListener('click', handler) +const button = document.querySelector('#update'); +button?.addEventListener('click', handler); diff --git a/resources/assets/src/views/admin/UsersManagement/Card.tsx b/resources/assets/src/views/admin/UsersManagement/Card.tsx index de41fbe4..7c664e32 100644 --- a/resources/assets/src/views/admin/UsersManagement/Card.tsx +++ b/resources/assets/src/views/admin/UsersManagement/Card.tsx @@ -1,164 +1,164 @@ -import React from 'react' -import { t } from '@/scripts/i18n' -import type { User } from '@/scripts/types' -import { Box, Icon, InfoTable } from './styles' +import React from 'react'; +import clsx from 'clsx'; +import {Box, Icon, InfoTable} from './styles'; import { - humanizePermission, - verificationStatusText, - canModifyUser, - canModifyPermission, -} from './utils' -import clsx from 'clsx' + humanizePermission, + verificationStatusText, + canModifyUser, + canModifyPermission, +} from './utils'; +import {t} from '@/scripts/i18n'; +import type {User} from '@/scripts/types'; -interface Props { - user: User - currentUser: User - onEmailChange(): void - onNicknameChange(): void - onScoreChange(): void - onPermissionChange(): void - onVerificationToggle(): void - onPasswordChange(): void - onDelete(): void -} +type Properties = { + readonly user: User; + readonly currentUser: User; + onEmailChange(): void; + onNicknameChange(): void; + onScoreChange(): void; + onPermissionChange(): void; + onVerificationToggle(): void; + onPasswordChange(): void; + onDelete(): void; +}; -const Card: React.FC = (props) => { - const { user, currentUser } = props +const Card: React.FC = properties => { + const {user, currentUser} = properties; - const isDarkMode = document.body.classList.contains('dark-mode') + const isDarkMode = document.body.classList.contains('dark-mode'); - const avatar = `${blessing.base_url}/avatar/user/${user.uid}` - const avatarPNG = `${avatar}?png` - const canModify = canModifyUser(user, currentUser) + const avatar = `${blessing.base_url}/avatar/user/${user.uid}`; + const avatarPNG = `${avatar}?png`; + const canModify = canModifyUser(user, currentUser); - return ( - - - - - - - -
    -
    -
    - {user.nickname} -
    - -
    -
    -
    UID: {user.uid}
    -
    - {t('general.user.email')} - {': '} - {user.email} -
    - -
    - {t('general.user.score')} - {user.score} -
    -
    - {t('admin.permission')} - - {humanizePermission(user.permission)} - -
    -
    - {t('admin.verification')} - - {verificationStatusText(user.verified)} - -
    -
    -
    - - {t('general.user.register-at')} - {': '} - {user.register_at} - -
    -
    -
    -
    - ) -} + return ( + + + + + + + +
    +
    +
    + {user.nickname} +
    +
    + {canModify && ( + +
    +
    UID: {user.uid}
    +
    + {t('general.user.email')} + {': '} + {user.email} +
    + +
    + {t('general.user.score')} + {user.score} +
    +
    + {t('admin.permission')} + + {humanizePermission(user.permission)} + +
    +
    + {t('admin.verification')} + + {verificationStatusText(user.verified)} + +
    +
    +
    + + {t('general.user.register-at')} + {': '} + {user.register_at} + +
    +
    +
    + + ); +}; -export default Card +export default Card; diff --git a/resources/assets/src/views/admin/UsersManagement/Header.tsx b/resources/assets/src/views/admin/UsersManagement/Header.tsx index f3f0c98d..05c73259 100644 --- a/resources/assets/src/views/admin/UsersManagement/Header.tsx +++ b/resources/assets/src/views/admin/UsersManagement/Header.tsx @@ -1,5 +1,5 @@ -import styled from '@emotion/styled' -import { lessThan, Breakpoint } from '@/styles/breakpoints' +import styled from '@emotion/styled'; +import {lessThan, Breakpoint} from '@/styles/breakpoints'; const Header = styled.div` display: flex; @@ -17,6 +17,6 @@ const Header = styled.div` margin: 7px 0 0 0; } } -` +`; -export default Header +export default Header; diff --git a/resources/assets/src/views/admin/UsersManagement/LoadingCard.tsx b/resources/assets/src/views/admin/UsersManagement/LoadingCard.tsx index 10e47590..ddb74748 100644 --- a/resources/assets/src/views/admin/UsersManagement/LoadingCard.tsx +++ b/resources/assets/src/views/admin/UsersManagement/LoadingCard.tsx @@ -1,61 +1,61 @@ -import React from 'react' -import styled from '@emotion/styled' -import Skeleton from 'react-loading-skeleton' -import { t } from '@/scripts/i18n' -import { Box, Icon, InfoTable } from './styles' -import clsx from 'clsx' +import React from 'react'; +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'; -const ShrinkedSkeleton = styled(Skeleton)<{ width?: string }>` - width: ${(props) => props.width}; -` +const ShrinkedSkeleton = styled(Skeleton)<{width?: string}>` + width: ${properties => properties.width}; +`; -const isDarkMode = document.body.classList.contains('dark-mode') +const isDarkMode = document.body.classList.contains('dark-mode'); const LoadingCard: React.FC = () => ( - - - - -
    -
    -
    - -
    -
    -
    -
    -
    - -
    -
    - -
    - -
    - {t('general.user.score')} - - - -
    -
    - {t('admin.permission')} - - - -
    -
    - {t('admin.verification')} - - - -
    -
    -
    - -
    -
    -
    -
    -) + + + + +
    +
    +
    + +
    +
    +
    +
    +
    + +
    +
    + +
    + +
    + {t('general.user.score')} + + + +
    +
    + {t('admin.permission')} + + + +
    +
    + {t('admin.verification')} + + + +
    +
    +
    + +
    +
    +
    + +); -export default LoadingCard +export default LoadingCard; diff --git a/resources/assets/src/views/admin/UsersManagement/LoadingRow.tsx b/resources/assets/src/views/admin/UsersManagement/LoadingRow.tsx index 3687744e..7c9a5de7 100644 --- a/resources/assets/src/views/admin/UsersManagement/LoadingRow.tsx +++ b/resources/assets/src/views/admin/UsersManagement/LoadingRow.tsx @@ -1,17 +1,17 @@ -import React from 'react' -import styled from '@emotion/styled' -import Skeleton from 'react-loading-skeleton' +import React from 'react'; +import styled from '@emotion/styled'; +import Skeleton from 'react-loading-skeleton'; const ThickSkeleton = styled(Skeleton)` line-height: 2; -` +`; const LoadingRow: React.FC = () => ( - - - - - -) + + + + + +); -export default LoadingRow +export default LoadingRow; diff --git a/resources/assets/src/views/admin/UsersManagement/Row.tsx b/resources/assets/src/views/admin/UsersManagement/Row.tsx index 0813f5b4..f8a2cdaf 100644 --- a/resources/assets/src/views/admin/UsersManagement/Row.tsx +++ b/resources/assets/src/views/admin/UsersManagement/Row.tsx @@ -1,114 +1,114 @@ -import React from 'react' -import { t } from '@/scripts/i18n' -import type { User } from '@/scripts/types' -import ButtonEdit from '@/components/ButtonEdit' +import React from 'react'; import { - humanizePermission, - verificationStatusText, - canModifyUser, - canModifyPermission, -} from './utils' + humanizePermission, + verificationStatusText, + canModifyUser, + canModifyPermission, +} from './utils'; +import {t} from '@/scripts/i18n'; +import type {User} from '@/scripts/types'; +import ButtonEdit from '@/components/ButtonEdit'; -interface Props { - user: User - currentUser: User - onEmailChange(): void - onNicknameChange(): void - onScoreChange(): void - onPermissionChange(): void - onVerificationToggle(): void - onPasswordChange(): void - onDelete(): void -} +type Properties = { + readonly user: User; + readonly currentUser: User; + onEmailChange(): void; + onNicknameChange(): void; + onScoreChange(): void; + onPermissionChange(): void; + onVerificationToggle(): void; + onPasswordChange(): void; + onDelete(): void; +}; -const Row: React.FC = (props) => { - const { user, currentUser } = props +const Row: React.FC = properties => { + const {user, currentUser} = properties; - const canModify = canModifyUser(user, currentUser) + const canModify = canModifyUser(user, currentUser); - return ( - - {user.uid} - - {user.email} - {canModify && ( - - - - )} - - - {user.nickname} - {canModify && ( - - - - )} - - - {user.score} - {canModify && ( - - - - )} - - - {humanizePermission(user.permission)} - {canModifyPermission(user, currentUser) && ( - - - - )} - - - {verificationStatusText(user.verified)} - {canModify && ( - - {user.verified ? ( - - ) : ( - - )} - - )} - - {user.register_at} - - - - - - ) -} + return ( + + {user.uid} + + {user.email} + {canModify && ( + + + + )} + + + {user.nickname} + {canModify && ( + + + + )} + + + {user.score} + {canModify && ( + + + + )} + + + {humanizePermission(user.permission)} + {canModifyPermission(user, currentUser) && ( + + + + )} + + + {verificationStatusText(user.verified)} + {canModify && ( + + {user.verified ? ( + + ) : ( + + )} + + )} + + {user.register_at} + + + + + + ); +}; -export default Row +export default Row; diff --git a/resources/assets/src/views/admin/UsersManagement/index.tsx b/resources/assets/src/views/admin/UsersManagement/index.tsx index 9eeb32c7..b8aa8390 100644 --- a/resources/assets/src/views/admin/UsersManagement/index.tsx +++ b/resources/assets/src/views/admin/UsersManagement/index.tsx @@ -1,370 +1,369 @@ -import React, { useState, useEffect, useLayoutEffect } from 'react' -import { hot } from 'react-hot-loader/root' -import { useImmer } from 'use-immer' -import useBlessingExtra from '@/scripts/hooks/useBlessingExtra' -import useIsLargeScreen from '@/scripts/hooks/useIsLargeScreen' -import { t } from '@/scripts/i18n' -import * as fetch from '@/scripts/net' -import { User, UserPermission, Paginator } from '@/scripts/types' -import { toast, showModal } from '@/scripts/notify' -import urls from '@/scripts/urls' -import type { Props as ModalInputProps } from '@/components/ModalInput' -import Pagination from '@/components/Pagination' -import Header from './Header' -import Card from './Card' -import LoadingCard from './LoadingCard' -import Row from './Row' -import LoadingRow from './LoadingRow' +import React, {useState, useEffect, useLayoutEffect} from 'react'; +import {hot} from 'react-hot-loader/root'; +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 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 urls from '@/scripts/urls'; +import type {Props as ModalInputProperties} from '@/components/ModalInput'; +import Pagination from '@/components/Pagination'; const UsersManagement: React.FC = () => { - const [users, setUsers] = useImmer([]) - const [page, setPage] = useState(1) - const [totalPages, setTotalPages] = useState(1) - const [isLoading, setIsLoading] = useState(false) - const isLargeScreen = useIsLargeScreen() - const [isTableMode, setIsTableMode] = useState(false) - const [query, setQuery] = useState('') - const currentUser = useBlessingExtra('currentUser', { - uid: 0, - permission: UserPermission.Admin, - } as User) + const [users, setUsers] = useImmer([]); + const [page, setPage] = useState(1); + const [totalPages, setTotalPages] = useState(1); + const [isLoading, setIsLoading] = useState(false); + const isLargeScreen = useIsLargeScreen(); + const [isTableMode, setIsTableMode] = useState(false); + const [query, setQuery] = useState(''); + const currentUser = useBlessingExtra('currentUser', { + uid: 0, + permission: UserPermission.Admin, + } as User); - useLayoutEffect(() => { - if (isLargeScreen) { - setIsTableMode(true) - } - }, [isLargeScreen]) + useLayoutEffect(() => { + if (isLargeScreen) { + setIsTableMode(true); + } + }, [isLargeScreen]); - const getUsers = async () => { - setIsLoading(true) - const { data, last_page }: Paginator = await fetch.get( - urls.admin.users.list(), - { - q: query, - page, - }, - ) - setUsers(() => data) - setTotalPages(last_page) - setIsLoading(false) - } + const getUsers = async () => { + setIsLoading(true); + const {data, last_page}: Paginator = await fetch.get( + urls.admin.users.list(), + { + q: query, + page: page.toString(), + }, + ); + setUsers(() => data); + setTotalPages(last_page); + setIsLoading(false); + }; - useEffect(() => { - getUsers() - }, [page]) + useEffect(() => { + getUsers(); + }, [page]); - const handleModeChange = (event: React.ChangeEvent) => { - setIsTableMode(event.target.value === 'table') - } + const handleModeChange = (event: React.ChangeEvent) => { + setIsTableMode(event.target.value === 'table'); + }; - const handleQueryChange = (event: React.ChangeEvent) => { - setQuery(event.target.value) - } + const handleQueryChange = (event: React.ChangeEvent) => { + setQuery(event.target.value); + }; - const handleSubmitQuery = (event: React.FormEvent) => { - event.preventDefault() - getUsers() - } + const handleSubmitQuery = (event: React.FormEvent) => { + event.preventDefault(); + getUsers(); + }; - const handleEmailChange = async (user: User, index: number) => { - let email: string - try { - const { value } = await showModal({ - mode: 'prompt', - text: t('admin.newUserEmail'), - input: user.email, - inputMode: 'email', - validator: (value: string) => { - if (!value) { - return t('auth.emptyEmail') - } - }, - }) - email = value - } catch { - return - } + const handleEmailChange = async (user: User, index: number) => { + let email: string; + try { + const {value} = await showModal({ + mode: 'prompt', + text: t('admin.newUserEmail'), + input: user.email, + inputMode: 'email', + validator(value: string) { + if (!value) { + return t('auth.emptyEmail'); + } + }, + }); + email = value; + } catch { + return; + } - const { code, message } = await fetch.put( - urls.admin.users.email(user.uid), - { email }, - ) - if (code === 0) { - toast.success(message) - setUsers((users) => { - users[index]!.email = email - }) - } else { - toast.error(message) - } - } + const {code, message} = await fetch.put( + urls.admin.users.email(user.uid), + {email}, + ); + if (code === 0) { + toast.success(message); + setUsers(users => { + users[index].email = email; + }); + } else { + toast.error(message); + } + }; - const handleNicknameChange = async (user: User, index: number) => { - let nickname: string - try { - const { value } = await showModal({ - mode: 'prompt', - text: t('admin.newUserNickname'), - input: user.nickname, - validator: (value: string) => { - if (!value) { - return t('auth.emptyNickname') - } - }, - }) - nickname = value - } catch { - return - } + const handleNicknameChange = async (user: User, index: number) => { + let nickname: string; + try { + const {value} = await showModal({ + mode: 'prompt', + text: t('admin.newUserNickname'), + input: user.nickname, + validator(value: string) { + if (!value) { + return t('auth.emptyNickname'); + } + }, + }); + nickname = value; + } catch { + return; + } - const { code, message } = await fetch.put( - urls.admin.users.nickname(user.uid), - { nickname }, - ) - if (code === 0) { - toast.success(message) - setUsers((users) => { - users[index]!.nickname = nickname - }) - } else { - toast.error(message) - } - } + const {code, message} = await fetch.put( + urls.admin.users.nickname(user.uid), + {nickname}, + ); + if (code === 0) { + toast.success(message); + setUsers(users => { + users[index].nickname = nickname; + }); + } else { + toast.error(message); + } + }; - const handleScoreChange = async (user: User, index: number) => { - let score: number - try { - const { value } = await showModal({ - mode: 'prompt', - text: t('admin.newScore'), - input: user.score.toString(), - inputMode: 'numeric', - }) - score = Number.parseInt(value) - } catch { - return - } + const handleScoreChange = async (user: User, index: number) => { + let score: number; + try { + const {value} = await showModal({ + mode: 'prompt', + text: t('admin.newScore'), + input: user.score.toString(), + inputMode: 'numeric', + }); + score = Number.parseInt(value); + } catch { + return; + } - const { code, message } = await fetch.put( - urls.admin.users.score(user.uid), - { score }, - ) - if (code === 0) { - toast.success(message) - setUsers((users) => { - users[index]!.score = score - }) - } else { - toast.error(message) - } - } + const {code, message} = await fetch.put( + urls.admin.users.score(user.uid), + {score}, + ); + if (code === 0) { + toast.success(message); + setUsers(users => { + users[index].score = score; + }); + } else { + toast.error(message); + } + }; - const handlePermissionChange = async (user: User, index: number) => { - const permissions: ModalInputProps['choices'] = [ - { text: t('admin.banned'), value: '-1' }, - { text: t('admin.normal'), value: '0' }, - ] - if (currentUser.permission > UserPermission.Admin) { - permissions.push({ text: t('admin.admin'), value: '1' }) - } + const handlePermissionChange = async (user: User, index: number) => { + const permissions: ModalInputProperties['choices'] = [ + {text: t('admin.banned'), value: '-1'}, + {text: t('admin.normal'), value: '0'}, + ]; + if (currentUser.permission > UserPermission.Admin) { + permissions.push({text: t('admin.admin'), value: '1'}); + } - let permission: UserPermission - try { - const { value } = await showModal({ - mode: 'prompt', - text: t('admin.newPermission'), - input: user.permission.toString(), - inputType: 'radios', - choices: permissions, - }) - permission = Number.parseInt(value) - } catch { - return - } + let permission: UserPermission; + try { + const {value} = await showModal({ + mode: 'prompt', + text: t('admin.newPermission'), + input: user.permission.toString(), + inputType: 'radios', + choices: permissions, + }); + permission = Number.parseInt(value); + } catch { + return; + } - const { code, message } = await fetch.put( - urls.admin.users.permission(user.uid), - { permission }, - ) - if (code === 0) { - toast.success(message) - setUsers((users) => { - users[index]!.permission = permission - }) - } else { - toast.error(message) - } - } + const {code, message} = await fetch.put( + urls.admin.users.permission(user.uid), + {permission}, + ); + if (code === 0) { + toast.success(message); + setUsers(users => { + users[index].permission = permission; + }); + } else { + toast.error(message); + } + }; - const handleVerificationToggle = async (user: User, index: number) => { - const { code, message } = await fetch.put( - urls.admin.users.verification(user.uid), - ) - if (code === 0) { - toast.success(message) - setUsers((users) => { - users[index]!.verified = !users[index]!.verified - }) - } else { - toast.error(message) - } - } + const handleVerificationToggle = async (user: User, index: number) => { + const {code, message} = await fetch.put( + urls.admin.users.verification(user.uid), + ); + if (code === 0) { + toast.success(message); + setUsers(users => { + users[index].verified = !users[index].verified; + }); + } else { + toast.error(message); + } + }; - const handlePasswordChange = async (user: User) => { - let password: string - try { - const { value } = await showModal({ - mode: 'prompt', - text: t('admin.newUserPassword'), - inputType: 'password', - placeholder: t('admin.changePassword'), - }) - password = value - } catch { - return - } + const handlePasswordChange = async (user: User) => { + let password: string; + try { + const {value} = await showModal({ + mode: 'prompt', + text: t('admin.newUserPassword'), + inputType: 'password', + placeholder: t('admin.changePassword'), + }); + password = value; + } catch { + return; + } - const { code, message } = await fetch.put( - urls.admin.users.password(user.uid), - { password }, - ) - if (code === 0) { - toast.success(message) - } else { - toast.error(message) - } - } + const {code, message} = await fetch.put( + urls.admin.users.password(user.uid), + {password}, + ); + if (code === 0) { + toast.success(message); + } else { + toast.error(message); + } + }; - const handleDelete = async (user: User) => { - try { - await showModal({ - text: t('admin.deleteUserNotice'), - okButtonType: 'danger', - }) - } catch { - return - } + const handleDelete = async (user: User) => { + try { + await showModal({ + text: t('admin.deleteUserNotice'), + okButtonType: 'danger', + }); + } catch { + return; + } - const { code, message } = await fetch.del(urls.admin.users.delete(user.uid)) - if (code === 0) { - toast.success(message) - setUsers((users) => users.filter(({ uid }) => uid !== user.uid)) - } else { - toast.error(message) - } - } + const {code, message} = await fetch.del(urls.admin.users.delete(user.uid)); + if (code === 0) { + toast.success(String(message)); + setUsers(users => users.filter(({uid}) => uid !== user.uid)); + } else { + toast.error(String(message)); + } + }; - return ( -
    -
    -
    - -
    - -
    -
    -
    - - -
    -
    - {users.length === 0 && !isLoading ? ( -
    {t('general.noResult')}
    - ) : isTableMode ? ( -
    - - - - - - - - - - - - - - - {isLoading - ? new Array(10).fill(null).map((_, i) => ) - : users.map((user, i) => ( - handleEmailChange(user, i)} - onNicknameChange={() => handleNicknameChange(user, i)} - onScoreChange={() => handleScoreChange(user, i)} - onPermissionChange={() => handlePermissionChange(user, i)} - onVerificationToggle={() => - handleVerificationToggle(user, i) - } - onPasswordChange={() => handlePasswordChange(user)} - onDelete={() => handleDelete(user)} - /> - ))} - -
    UID{t('general.user.email')}{t('general.user.nickname')}{t('general.user.score')}{t('admin.permission')}{t('admin.verification')}{t('general.user.register-at')}{t('admin.operationsTitle')}
    -
    - ) : ( -
    - {isLoading - ? new Array(10).fill(null).map((_, i) => ) - : users.map((user, i) => ( - handleEmailChange(user, i)} - onNicknameChange={() => handleNicknameChange(user, i)} - onScoreChange={() => handleScoreChange(user, i)} - onPermissionChange={() => handlePermissionChange(user, i)} - onVerificationToggle={() => handleVerificationToggle(user, i)} - onPasswordChange={() => handlePasswordChange(user)} - onDelete={() => handleDelete(user)} - /> - ))} -
    - )} -
    -
    - -
    -
    -
    - ) -} + return ( +
    +
    +
    + +
    + +
    +
    +
    + + +
    +
    + {users.length === 0 && !isLoading ? ( +
    {t('general.noResult')}
    + ) : (isTableMode ? ( +
    + + + + + + + + + + + + + + + {isLoading + ? Array.from({length: 10}).fill(null).map((_, i) => ) + : users.map((user, i) => ( + 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)} + /> + ))} + +
    UID{t('general.user.email')}{t('general.user.nickname')}{t('general.user.score')}{t('admin.permission')}{t('admin.verification')}{t('general.user.register-at')}{t('admin.operationsTitle')}
    +
    + ) : ( +
    + {isLoading + ? Array.from({length: 10}).fill(null).map((_, i) => ) + : users.map((user, i) => ( + 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)} + /> + ))} +
    + ))} +
    +
    + +
    +
    +
    + ); +}; -export default hot(UsersManagement) +export default hot(UsersManagement); diff --git a/resources/assets/src/views/admin/UsersManagement/styles.ts b/resources/assets/src/views/admin/UsersManagement/styles.ts index 3a1850df..98fe657f 100644 --- a/resources/assets/src/views/admin/UsersManagement/styles.ts +++ b/resources/assets/src/views/admin/UsersManagement/styles.ts @@ -1,5 +1,5 @@ -import styled from '@emotion/styled' -import * as breakpoints from '@/styles/breakpoints' +import styled from '@emotion/styled'; +import * as breakpoints from '@/styles/breakpoints'; export const Box = styled.div` width: 48%; @@ -8,14 +8,14 @@ export const Box = styled.div` ${breakpoints.lessThan(breakpoints.Breakpoint.lg)} { width: 98%; } -` +`; -export const Icon = styled.div<{ py?: boolean }>` +export const Icon = styled.div<{py?: boolean}>` width: 70px; display: flex; justify-content: center; - padding-top: ${(props) => (props.py ? '22px' : '0')}; -` + padding-top: ${properties => (properties.py ? '22px' : '0')}; +`; export const InfoTable = styled.div` > div:not(:last-child) { @@ -26,4 +26,4 @@ export const InfoTable = styled.div` border-right: 1px solid rgba(0, 0, 0, 0.125); } } -` +`; diff --git a/resources/assets/src/views/admin/UsersManagement/utils.ts b/resources/assets/src/views/admin/UsersManagement/utils.ts index 3ef0b83f..14e36308 100644 --- a/resources/assets/src/views/admin/UsersManagement/utils.ts +++ b/resources/assets/src/views/admin/UsersManagement/utils.ts @@ -1,27 +1,34 @@ -import { t } from '@/scripts/i18n' -import { User, UserPermission } from '@/scripts/types' +import {t} from '@/scripts/i18n'; +import {type User, UserPermission} from '@/scripts/types'; export function humanizePermission(permission: UserPermission): string { - switch (permission) { - case UserPermission.Banned: - return t('admin.banned') - case UserPermission.Normal: - return t('admin.normal') - case UserPermission.Admin: - return t('admin.admin') - case UserPermission.SuperAdmin: - return t('admin.superAdmin') - } + switch (permission) { + case UserPermission.Banned: { + return t('admin.banned'); + } + + case UserPermission.Normal: { + return t('admin.normal'); + } + + case UserPermission.Admin: { + return t('admin.admin'); + } + + case UserPermission.SuperAdmin: { + return t('admin.superAdmin'); + } + } } export function verificationStatusText(isVerified: boolean): string { - return isVerified ? t('admin.verified') : t('admin.unverified') + return isVerified ? t('admin.verified') : t('admin.unverified'); } export function canModifyUser(target: User, current: User): boolean { - return target.uid === current.uid || current.permission > target.permission + return target.uid === current.uid || current.permission > target.permission; } export function canModifyPermission(target: User, current: User): boolean { - return current.permission > target.permission + return current.permission > target.permission; } diff --git a/resources/assets/src/views/auth/Forgot.tsx b/resources/assets/src/views/auth/Forgot.tsx index 346b1482..0d46fffe 100644 --- a/resources/assets/src/views/auth/Forgot.tsx +++ b/resources/assets/src/views/auth/Forgot.tsx @@ -1,74 +1,75 @@ -import React, { useState, useRef } from 'react' -import { hot } from 'react-hot-loader/root' -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 React, {useState, useRef} from 'react'; +import {hot} from 'react-hot-loader/root'; +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'; const Forgot: React.FC = () => { - const [email, setEmail] = useState('') - const [isSending, setIsSending] = useState(false) - const [successMessage, setSuccessMessage] = useState('') - const [warningMessage, setWarningMessage] = useState('') - const ref = useRef(null) + const [email, setEmail] = useState(''); + const [isSending, setIsSending] = useState(false); + const [successMessage, setSuccessMessage] = useState(''); + const [warningMessage, setWarningMessage] = useState(''); + const reference = useRef(null); - useEmitMounted() + useEmitMounted(); - const handleSubmit = async (event: React.FormEvent) => { - event.preventDefault() - setWarningMessage('') - setIsSending(true) + const handleSubmit = async (event: React.FormEvent) => { + event.preventDefault(); + setWarningMessage(''); + setIsSending(true); - const captcha = await ref.current!.execute() - const { code, message } = await fetch.post( - urls.auth.forgot(), - { email, captcha }, - ) - if (code === 0) { - setSuccessMessage(message) - } else { - setWarningMessage(message) - ref.current!.reset() - } - setIsSending(false) - } + const captcha = await reference.current!.execute(); + const {code, message} = await fetch.post( + urls.auth.forgot(), + {email, captcha}, + ); + if (code === 0) { + setSuccessMessage(message); + } else { + setWarningMessage(message); + reference.current!.reset(); + } - return ( -
    - + setIsSending(false); + }; - + return ( + + - {successMessage} - {warningMessage} + -
    - - {t('auth.forgot.login-link')} - - -
    - - ) -} + {successMessage} + {warningMessage} -export default hot(Forgot) +
    + + {t('auth.forgot.login-link')} + + +
    + + ); +}; + +export default hot(Forgot); diff --git a/resources/assets/src/views/auth/Login.tsx b/resources/assets/src/views/auth/Login.tsx index bd164025..e4052030 100644 --- a/resources/assets/src/views/auth/Login.tsx +++ b/resources/assets/src/views/auth/Login.tsx @@ -1,157 +1,157 @@ -import React, { useState, useRef, useEffect } from 'react' -import { hot } from 'react-hot-loader/root' -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 React, {useState, useRef, useEffect} from 'react'; +import {hot} from 'react-hot-loader/root'; +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'; type SuccessfulResponse = { - code: 0 - message: string - data: { redirectTo: string } -} + code: 0; + message: string; + data: {redirectTo: string}; +}; type FailedResponse = { - code: number - message: string - data: { login_fails: number } -} -type Response = SuccessfulResponse | FailedResponse + code: number; + message: string; + data: {login_fails: number}; +}; +type Response = SuccessfulResponse | FailedResponse; function isSuccessfulResponse( - response: Response, + response: Response, ): response is SuccessfulResponse { - return response.code === 0 + return response.code === 0; } const Login: React.FC = () => { - const [identification, setIdentification] = useState('') - const [password, setPassword] = useState('') - const [remember, setRemember] = useState(false) - const [hasTooManyFails, setHasTooManyFails] = useState(false) - const [isPending, setIsPending] = useState(false) - const [warningMessage, setWarningMessage] = useState('') - const ref = useRef(null) - const recaptcha = useBlessingExtra('recaptcha') - const invisibleRecaptcha = useBlessingExtra('invisible') + const [identification, setIdentification] = useState(''); + const [password, setPassword] = useState(''); + const [remember, setRemember] = useState(false); + const [hasTooManyFails, setHasTooManyFails] = useState(false); + const [isPending, setIsPending] = useState(false); + const [warningMessage, setWarningMessage] = useState(''); + const reference = useRef(null); + const recaptcha = useBlessingExtra('recaptcha'); + const invisibleRecaptcha = useBlessingExtra('invisible'); - useEmitMounted() + useEmitMounted(); - useEffect(() => { - setHasTooManyFails(blessing.extra.tooManyFails as boolean) - }, []) + useEffect(() => { + setHasTooManyFails(blessing.extra.tooManyFails as boolean); + }, []); - const handlePasswordChange = (event: React.ChangeEvent) => { - setPassword(event.target.value) - } + const handlePasswordChange = (event: React.ChangeEvent) => { + setPassword(event.target.value); + }; - const handleRememberChange = (event: React.ChangeEvent) => { - setRemember(event.target.checked) - } + const handleRememberChange = (event: React.ChangeEvent) => { + setRemember(event.target.checked); + }; - const handleSubmit = async (event: React.FormEvent) => { - event.preventDefault() - setIsPending(true) + const handleSubmit = async (event: React.FormEvent) => { + event.preventDefault(); + setIsPending(true); - const response = await fetch.post(urls.auth.login(), { - identification, - password, - keep: remember, - captcha: hasTooManyFails ? await ref.current!.execute() : undefined, - }) + const response = await fetch.post(urls.auth.login(), { + identification, + password, + keep: remember, + captcha: hasTooManyFails ? await reference.current!.execute() : undefined, + }); - if (isSuccessfulResponse(response)) { - window.location.href = response.data.redirectTo - } else { - setWarningMessage(response.message) - setIsPending(false) - ref.current?.reset() + if (isSuccessfulResponse(response)) { + window.location.href = response.data.redirectTo; + } else { + setWarningMessage(response.message); + setIsPending(false); + reference.current?.reset(); - // only notify user if he/she fails too much at the first time - if (response.data.login_fails > 3 && !hasTooManyFails) { - setHasTooManyFails(true) - if (recaptcha) { - // no need to notify if using invisible recaptcha - if (!invisibleRecaptcha) { - showModal({ - mode: 'alert', - text: t('auth.tooManyFails.recaptcha'), - }) - } - } else { - showModal({ - mode: 'alert', - text: t('auth.tooManyFails.captcha'), - }) - } - } - } - } + // Only notify user if he/she fails too much at the first time + if (response.data.login_fails > 3 && !hasTooManyFails) { + setHasTooManyFails(true); + if (recaptcha) { + // No need to notify if using invisible recaptcha + if (!invisibleRecaptcha) { + showModal({ + mode: 'alert', + text: t('auth.tooManyFails.recaptcha'), + }); + } + } else { + showModal({ + mode: 'alert', + text: t('auth.tooManyFails.captcha'), + }); + } + } + } + }; - return ( -
    - -
    - -
    -
    - -
    -
    -
    + return ( + + +
    + +
    +
    + +
    +
    +
    - {hasTooManyFails && } + {hasTooManyFails && } - {warningMessage} + {warningMessage} -
    - - {t('auth.forgot-link')} -
    +
    + + {t('auth.forgot-link')} +
    - - - ) -} + + + ); +}; -export default hot(Login) +export default hot(Login); diff --git a/resources/assets/src/views/auth/Registration.tsx b/resources/assets/src/views/auth/Registration.tsx index f18c577a..c44f4d47 100644 --- a/resources/assets/src/views/auth/Registration.tsx +++ b/resources/assets/src/views/auth/Registration.tsx @@ -1,178 +1,179 @@ -import React, { useState, useRef } from 'react' -import { hot } from 'react-hot-loader/root' -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 React, {useState, useRef} from 'react'; +import {hot} from 'react-hot-loader/root'; +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'; const Registration: React.FC = () => { - const [email, setEmail] = useState('') - const [password, setPassword] = useState('') - const [confirmation, setConfirmation] = useState('') - const [nickName, setNickName] = useState('') - const [playerName, setPlayerName] = useState('') - const [isPending, setIsPending] = useState(false) - const [warningMessage, setWarningMessage] = useState('') - const requirePlayer = useBlessingExtra('player') - const confirmationRef = useRef(null) - const captchaRef = useRef(null) + const [email, setEmail] = useState(''); + const [password, setPassword] = useState(''); + const [confirmation, setConfirmation] = useState(''); + const [nickName, setNickName] = useState(''); + const [playerName, setPlayerName] = useState(''); + const [isPending, setIsPending] = useState(false); + const [warningMessage, setWarningMessage] = useState(''); + const requirePlayer = useBlessingExtra('player'); + const confirmationReference = useRef(null); + const captchaReference = useRef(null); - useEmitMounted() + useEmitMounted(); - const handlePasswordChange = (event: React.ChangeEvent) => { - setPassword(event.target.value) - } + const handlePasswordChange = (event: React.ChangeEvent) => { + setPassword(event.target.value); + }; - const handleConfirmationChange = ( - event: React.ChangeEvent, - ) => { - setConfirmation(event.target.value) - } + const handleConfirmationChange = ( + event: React.ChangeEvent, + ) => { + setConfirmation(event.target.value); + }; - const handleNickNameChange = (event: React.ChangeEvent) => { - setNickName(event.target.value) - } + const handleNickNameChange = (event: React.ChangeEvent) => { + setNickName(event.target.value); + }; - const handlePlayerNameChange = ( - event: React.ChangeEvent, - ) => { - setPlayerName(event.target.value) - } + const handlePlayerNameChange = ( + event: React.ChangeEvent, + ) => { + setPlayerName(event.target.value); + }; - const handleSubmit = async (event: React.FormEvent) => { - event.preventDefault() - setWarningMessage('') + const handleSubmit = async (event: React.FormEvent) => { + event.preventDefault(); + setWarningMessage(''); - if (password !== confirmation) { - setWarningMessage(t('auth.invalidConfirmPwd')) - confirmationRef.current!.focus() - return - } + if (password !== confirmation) { + setWarningMessage(t('auth.invalidConfirmPwd')); + confirmationReference.current!.focus(); + return; + } - setIsPending(true) - const { code, message } = await fetch.post( - urls.auth.register(), - Object.assign( - { email, password, captcha: await captchaRef.current!.execute() }, - requirePlayer ? { player_name: playerName } : { nickname: nickName }, - ), - ) - if (code === 0) { - toast.success(message) - setTimeout(() => { - window.location.href = `${blessing.base_url}/user` - }, 3000) - } else { - setWarningMessage(message) - captchaRef.current!.reset() - } - setIsPending(false) - } + setIsPending(true); + const {code, message} = await fetch.post( + urls.auth.register(), + { + email, password, captcha: await captchaReference.current!.execute(), + ...(requirePlayer ? {player_name: playerName} : {nickname: nickName}), + }, + ); + if (code === 0) { + toast.success(message); + setTimeout(() => { + window.location.href = `${blessing.base_url}/user`; + }, 3000); + } else { + setWarningMessage(message); + captchaReference.current!.reset(); + } - return ( -
    - -
    - -
    -
    - -
    -
    -
    -
    - -
    -
    - -
    -
    -
    - {requirePlayer ? ( -
    - -
    -
    - -
    -
    -
    - ) : ( -
    - -
    -
    - -
    -
    -
    - )} - + setIsPending(false); + }; - {warningMessage} + return ( + + +
    + +
    +
    + +
    +
    +
    +
    + +
    +
    + +
    +
    +
    + {requirePlayer ? ( +
    + +
    +
    + +
    +
    +
    + ) : ( +
    + +
    +
    + +
    +
    +
    + )} + -
    - {t('auth.login-link')} - -
    - - ) -} + {warningMessage} -export default hot(Registration) +
    + {t('auth.login-link')} + +
    + + ); +}; + +export default hot(Registration); diff --git a/resources/assets/src/views/auth/Reset.tsx b/resources/assets/src/views/auth/Reset.tsx index 62a1b174..c0ab537b 100644 --- a/resources/assets/src/views/auth/Reset.tsx +++ b/resources/assets/src/views/auth/Reset.tsx @@ -1,111 +1,111 @@ -import React, { useState } from 'react' -import { hot } from 'react-hot-loader/root' -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 React, {useState} from 'react'; +import {hot} from 'react-hot-loader/root'; +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'; const Reset: React.FC = () => { - const [password, setPassword] = useState('') - const [confirmation, setConfirmation] = useState('') - const [warningMessage, setWarningMessage] = useState('') - const [isPending, setIsPending] = useState(false) + const [password, setPassword] = useState(''); + const [confirmation, setConfirmation] = useState(''); + const [warningMessage, setWarningMessage] = useState(''); + const [isPending, setIsPending] = useState(false); - useEmitMounted() + useEmitMounted(); - const handlePasswordChange = (event: React.ChangeEvent) => { - setPassword(event.target.value) - } + const handlePasswordChange = (event: React.ChangeEvent) => { + setPassword(event.target.value); + }; - const handleConfirmationChange = ( - event: React.ChangeEvent, - ) => { - setConfirmation(event.target.value) - } + const handleConfirmationChange = ( + event: React.ChangeEvent, + ) => { + setConfirmation(event.target.value); + }; - const handleSubmit = async (event: React.FormEvent) => { - event.preventDefault() + const handleSubmit = async (event: React.FormEvent) => { + event.preventDefault(); - if (password !== confirmation) { - setWarningMessage(t('auth.invalidConfirmPwd')) - return - } + if (password !== confirmation) { + setWarningMessage(t('auth.invalidConfirmPwd')); + return; + } - setIsPending(true) - const { code, message } = await fetch.post( - location.href.replace(blessing.base_url, ''), - { password }, - ) - if (code === 0) { - toast.success(message) - setTimeout(() => { - window.location.href = blessing.base_url + urls.auth.login() - }, 2000) - } else { - setWarningMessage(message) - setIsPending(false) - } - } + setIsPending(true); + const {code, message} = await fetch.post( + location.href.replace(blessing.base_url, ''), + {password}, + ); + if (code === 0) { + toast.success(message); + setTimeout(() => { + window.location.href = blessing.base_url + urls.auth.login(); + }, 2000); + } else { + setWarningMessage(message); + setIsPending(false); + } + }; - return ( -
    -
    - -
    -
    - -
    -
    -
    -
    - -
    -
    - -
    -
    -
    + return ( + +
    + +
    +
    + +
    +
    +
    +
    + +
    +
    + +
    +
    +
    - {warningMessage} + {warningMessage} - - - ) -} + + + ); +}; -export default hot(Reset) +export default hot(Reset); diff --git a/resources/assets/src/views/skinlib/Show/addClosetItem.ts b/resources/assets/src/views/skinlib/Show/addClosetItem.ts index dda5a5b3..5124a7fb 100644 --- a/resources/assets/src/views/skinlib/Show/addClosetItem.ts +++ b/resources/assets/src/views/skinlib/Show/addClosetItem.ts @@ -1,39 +1,39 @@ -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' +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: Pick, ): Promise { - let name: string - try { - const { value } = await showModal({ - mode: 'prompt', - title: t('skinlib.setItemName'), - text: t('skinlib.applyNotice'), - input: texture.name, - validator: (value: string) => { - if (!value) { - return t('skinlib.emptyItemName') - } - }, - }) - name = value - } catch { - return false - } + let name: string; + try { + const {value} = await showModal({ + mode: 'prompt', + title: t('skinlib.setItemName'), + text: t('skinlib.applyNotice'), + input: texture.name, + validator(value: string) { + if (!value) { + return t('skinlib.emptyItemName'); + } + }, + }); + name = value; + } catch { + return false; + } - const { code, message } = await fetch.post( - urls.user.closet.add(), - { tid: texture.tid, name }, - ) - if (code === 0) { - toast.success(message) - } else { - toast.error(message) - } + const {code, message} = await fetch.post( + urls.user.closet.add(), + {tid: texture.tid, name}, + ); + if (code === 0) { + toast.success(message); + } else { + toast.error(message); + } - return code === 0 + return code === 0; } diff --git a/resources/assets/src/views/skinlib/Show/index.tsx b/resources/assets/src/views/skinlib/Show/index.tsx index e71b5ee7..4c7ab2f8 100644 --- a/resources/assets/src/views/skinlib/Show/index.tsx +++ b/resources/assets/src/views/skinlib/Show/index.tsx @@ -1,489 +1,498 @@ -import React, { useState, useEffect } from 'react' -import { createPortal } from 'react-dom' -import { hot } from 'react-hot-loader/root' -import Skeleton from 'react-loading-skeleton' -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 { 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 addClosetItem from './addClosetItem' +import React, {useState, useEffect} from 'react'; +import {createPortal} from 'react-dom'; +import {hot} from 'react-hot-loader/root'; +import Skeleton from 'react-loading-skeleton'; +import addClosetItem from './addClosetItem'; +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 {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'; export type Badge = { - color: string - text: string -} + color: string; + text: string; +}; -const Previewer = React.lazy(() => import('@/components/Viewer')) +const Previewer = React.lazy(async () => import('@/components/Viewer')); const Show: React.FC = () => { - const [texture, setTexture] = useState({} as Texture) - const [isLoading, setIsLoading] = useState(true) - const [showModalApply, setShowModalApply] = useState(false) - const [liked, setLiked] = useState(false) - const nickname = useBlessingExtra('nickname') - const isUploaderExists = useBlessingExtra('uploaderExists') - const currentUid = useBlessingExtra('currentUid', 0) - const isAdmin = useBlessingExtra('admin') - const badges = useBlessingExtra('badges', []) - const canBeDownloaded = useBlessingExtra('download') - const reportScore = useBlessingExtra('report') - const container = useMount('#previewer') + const [texture, setTexture] = useState({} as Texture); + const [isLoading, setIsLoading] = useState(true); + const [showModalApply, setShowModalApply] = useState(false); + const [liked, setLiked] = useState(false); + const nickname = useBlessingExtra('nickname'); + const isUploaderExists = useBlessingExtra('uploaderExists'); + const currentUid = useBlessingExtra('currentUid', 0); + const isAdmin = useBlessingExtra('admin'); + const badges = useBlessingExtra('badges', []); + const canBeDownloaded = useBlessingExtra('download'); + const reportScore = useBlessingExtra('report'); + const container = useMount('#previewer'); - useEmitMounted() + useEmitMounted(); - useEffect(() => { - const fetchInfo = async () => { - const url = location.href - .replace(blessing.base_url, '') - .replace('skinlib/show', 'texture') + useEffect(() => { + const fetchInfo = async () => { + const url = location.href + .replace(blessing.base_url, '') + .replace('skinlib/show', 'texture'); - const texture = await fetch.get(url) - setTexture(texture) - setIsLoading(false) - } - fetchInfo() - }, []) + const texture = await fetch.get(url); + setTexture(texture); + setIsLoading(false); + }; - useEffect(() => { - setLiked(blessing.extra.inCloset as boolean) - }, []) + fetchInfo(); + }, []); - const handleEditName = async () => { - let name: string - try { - const { value } = await showModal({ - mode: 'prompt', - text: t('skinlib.setNewTextureName'), - input: texture.name, - validator: (value: string) => { - if (!value) { - return t('skinlib.emptyNewTextureName') - } - }, - }) - name = value - } catch { - return - } + useEffect(() => { + setLiked(blessing.extra.inCloset as boolean); + }, []); - const { code, message } = await fetch.put( - urls.texture.name(texture.tid), - { name }, - ) + const handleEditName = async () => { + let name: string; + try { + const {value} = await showModal({ + mode: 'prompt', + text: t('skinlib.setNewTextureName'), + input: texture.name, + validator(value: string) { + if (!value) { + return t('skinlib.emptyNewTextureName'); + } + }, + }); + name = value; + } catch { + return; + } + + const {code, message} = await fetch.put( + urls.texture.name(texture.tid), + {name}, + ); + if (code === 0) { + toast.success(message); + setTexture(texture => ({...texture, name})); + } else { + toast.error(message); + } + }; + + const handleSwitchType = async () => { + let type: TextureType; + try { + const {value} = await showModal({ + mode: 'prompt', + text: t('skinlib.setNewTextureModel'), + input: texture.type, + inputType: 'radios', + choices: [ + {text: 'Steve', value: TextureType.Steve}, + {text: 'Alex', value: TextureType.Alex}, + {text: t('general.cape'), value: TextureType.Cape}, + ], + }); + type = value as TextureType; + } catch { + return; + } + + const {code, message} = await fetch.put( + urls.texture.type(texture.tid), + {type}, + ); + if (code === 0) { + toast.success(message); + setTexture(texture => ({...texture, type})); + } else { + toast.error(message); + } + }; + + const handleAddItemClick = async () => { + const ok = await addClosetItem(texture); + if (ok) { + setTexture(texture => ({...texture, likes: texture.likes + 1})); + setLiked(true); + } + }; + + const handleRemoveItemClick = async () => { + const ok = await removeClosetItem(texture.tid); + if (ok) { + setTexture(texture => ({...texture, likes: texture.likes - 1})); + setLiked(false); + } + }; + + const handleSetAsAvatar = async () => setAsAvatar(texture.tid); + + const handleDownloadClick = () => { + const a = document.createElement('a'); + a.href = `${blessing.base_url}/raw/${texture.tid}`; + a.download = `${texture.name}.png`; + a.click(); + }; + + const handleReport = async () => { + const prompt = (() => { + if (reportScore > 0) { + return t('skinlib.report.positive', {score: reportScore.toString()}); + } + + if (reportScore < 0) { + return t('skinlib.report.negative', {score: (-reportScore).toString()}); + } + + return ''; + })(); + + let reason: string; + try { + const {value} = await showModal({ + mode: 'prompt', + title: t('skinlib.report.title'), + text: prompt, + placeholder: t('skinlib.report.reason'), + }); + reason = value; + } catch { + return; + } + + const {code, message} = await fetch.post( + '/skinlib/report', + { + tid: texture.tid, + reason, + }, + ); + if (code === 0) { + toast.success(message); + } else { + toast.error(message); + } + }; + + const handlePrivacyClick = async () => { + try { + await showModal({ + text: texture.public + ? t('skinlib.setPrivateNotice') + : t('skinlib.setPublicNotice'), + }); + } catch { + return; + } + + type Ok = {code: 0; message: string}; + type Error_ = {code: 1; message: string}; + type Duplicated = {code: 2; message: string; data: {tid: number}}; + + const resp = await fetch.put( + urls.texture.privacy(texture.tid), + ); + const {code, message} = resp; if (code === 0) { - toast.success(message) - setTexture((texture) => ({ ...texture, name })) - } else { - toast.error(message) - } - } - - const handleSwitchType = async () => { - let type: TextureType - try { - const { value } = await showModal({ - mode: 'prompt', - text: t('skinlib.setNewTextureModel'), - input: texture.type, - inputType: 'radios', - choices: [ - { text: 'Steve', value: TextureType.Steve }, - { text: 'Alex', value: TextureType.Alex }, - { text: t('general.cape'), value: TextureType.Cape }, - ], - }) - type = value as TextureType - } catch { - return - } - - const { code, message } = await fetch.put( - urls.texture.type(texture.tid), - { type }, - ) - if (code === 0) { - toast.success(message) - setTexture((texture) => ({ ...texture, type })) - } else { - toast.error(message) - } - } - - const handleAddItemClick = async () => { - const ok = await addClosetItem(texture) - if (ok) { - setTexture((texture) => ({ ...texture, likes: texture.likes + 1 })) - setLiked(true) - } - } - - const handleRemoveItemClick = async () => { - const ok = await removeClosetItem(texture.tid) - if (ok) { - setTexture((texture) => ({ ...texture, likes: texture.likes - 1 })) - setLiked(false) - } - } - - const handleSetAsAvatar = () => setAsAvatar(texture.tid) - - const handleDownloadClick = () => { - const a = document.createElement('a') - a.href = `${blessing.base_url}/raw/${texture.tid}` - a.download = `${texture.name}.png` - a.click() - } - - const handleReport = async () => { - const prompt = (() => { - if (reportScore > 0) { - return t('skinlib.report.positive', { score: reportScore }) - } else if (reportScore < 0) { - return t('skinlib.report.negative', { score: -reportScore }) - } - return '' - })() - - let reason: string - try { - const { value } = await showModal({ - mode: 'prompt', - title: t('skinlib.report.title'), - text: prompt, - placeholder: t('skinlib.report.reason'), - }) - reason = value - } catch { - return - } - - const { code, message } = await fetch.post( - '/skinlib/report', - { - tid: texture.tid, - reason, - }, - ) - if (code === 0) { - toast.success(message) - } else { - toast.error(message) - } - } - - const handlePrivacyClick = async () => { - try { - await showModal({ - text: texture.public - ? t('skinlib.setPrivateNotice') - : t('skinlib.setPublicNotice'), - }) - } catch { - return - } - - type Ok = { code: 0; message: string } - type Err = { code: 1; message: string } - type Duplicated = { code: 2; message: string; data: { tid: number } } - - const resp = await fetch.put( - urls.texture.privacy(texture.tid), - ) - const { code, message } = resp - if (code === 0) { - toast.success(message) - setTexture((texture) => ({ ...texture, public: !texture.public })) + toast.success(message); + setTexture(texture => ({...texture, public: !texture.public})); } else if (resp.code === 2) { - try { - await showModal({ - mode: 'confirm', - text: message, - okButtonText: t('user.viewInSkinlib'), - }) - window.location.href = - blessing.base_url + urls.skinlib.show(resp.data.tid) - } catch { - // - } + try { + await showModal({ + mode: 'confirm', + text: message, + okButtonText: t('user.viewInSkinlib'), + }); + window.location.href + = blessing.base_url + urls.skinlib.show(resp.data.tid); + } catch { + // + } } else { - toast.error(message) + toast.error(message); } - } + }; - const handleDeleteTextureClick = async () => { - try { - await showModal({ - text: t('skinlib.deleteNotice'), - okButtonType: 'danger', - }) - } catch { - return - } + const handleDeleteTextureClick = async () => { + try { + await showModal({ + text: t('skinlib.deleteNotice'), + okButtonType: 'danger', + }); + } catch { + return; + } - const { code, message } = await fetch.del( - urls.texture.delete(texture.tid), - ) - if (code === 0) { - toast.success(message) - setTimeout(() => { - window.location.href = `${blessing.base_url}/skinlib` - }, 2000) - } else { - toast.error(message) - } - } + const {code, message} = await fetch.del( + urls.texture.delete(texture.tid), + ); + if (code === 0) { + toast.success(message); + setTimeout(() => { + window.location.href = `${blessing.base_url}/skinlib`; + }, 2000); + } else { + toast.error(message); + } + }; - const handleOpenModalApply = () => setShowModalApply(true) - const handleCloseModalApply = () => setShowModalApply(false) + const handleOpenModalApply = () => { + setShowModalApply(true); + }; - const linkToUploader = (() => { - const search = new URLSearchParams() - search.append( - 'filter', - texture.type === TextureType.Cape ? TextureType.Cape : 'skin', - ) - search.append('uploader', texture.uploader?.toString()) + const handleCloseModalApply = () => { + setShowModalApply(false); + }; - return `${blessing.base_url}/skinlib?${search.toString()}` - })() + const linkToUploader = (() => { + const search = new URLSearchParams(); + search.append( + 'filter', + texture.type === TextureType.Cape ? TextureType.Cape : 'skin', + ); + search.append('uploader', texture.uploader?.toString()); - const canEdit = currentUid === texture.uploader || isAdmin - const textureUrl = texture.hash - ? `${blessing.base_url}/textures/${texture.hash}` - : '' + return `${blessing.base_url}/skinlib?${search.toString()}`; + })(); - return ( - <> - {container && - createPortal( - }> - + {container + && createPortal( + }> + - {currentUid === 0 ? ( - - ) : ( -
    -
    - {liked && ( - - )} - {liked ? ( - - ) : ( - - )} - {texture.type !== TextureType.Cape && ( - - )} - {canBeDownloaded && ( - - )} - -
    -
    - - {texture.likes} -
    -
    - )} -
    -
    , - container, + }} + isAlex={texture.type === TextureType.Alex} + initPositionZ={60} + > + {currentUid === 0 ? ( + + ) : ( +
    +
    + {liked && ( + + )} + {liked ? ( + + ) : ( + + )} + {texture.type !== TextureType.Cape && ( + + )} + {canBeDownloaded && ( + + )} + +
    +
    + + {texture.likes} +
    +
    + )} +
    +
    , + container, )} -
    -
    -

    {t('skinlib.show.detail')}

    -
    -
    -
    -
    -
    {t('skinlib.show.name')}
    - {isLoading ? ( -
    - -
    - ) : ( - <> -
    - {texture.name} -
    - {canEdit && ( -
    - -
    - )} - - )} -
    -
    -
    {t('skinlib.show.model')}
    - {isLoading ? ( -
    - -
    - ) : ( - <> -
    - {texture.type === TextureType.Cape - ? t('general.cape') - : texture.type} -
    - {canEdit && ( -
    - -
    - )} - - )} -
    -
    -
    Hash
    -
    - {isLoading ? : texture.hash} -
    -
    -
    -
    {t('skinlib.show.size')}
    -
    - {isLoading ? : {texture.size} KB} -
    -
    -
    -
    {t('skinlib.show.uploader')}
    -
    - {isLoading ? ( - - ) : isUploaderExists ? ( - <> - -
    - {badges.map((badge) => ( - - {badge.text} - - ))} -
    - - ) : ( - nickname - )} -
    -
    -
    -
    {t('skinlib.show.upload-at')}
    -
    - {isLoading ? : texture.upload_at} -
    -
    -
    -
    -
    - {canEdit && ( -
    -
    -

    {t('admin.operationsTitle')}

    -
    -
    -

    {t('skinlib.show.manage-notice')}

    -
    -
    -
    - - -
    -
    -
    - )} - +
    +

    {t('skinlib.show.detail')}

    +
    +
    +
    +
    +
    {t('skinlib.show.name')}
    + {isLoading ? ( +
    + +
    + ) : ( + <> +
    + {texture.name} +
    + {canEdit && ( +
    + +
    + )} + + )} +
    +
    +
    {t('skinlib.show.model')}
    + {isLoading ? ( +
    + +
    + ) : ( + <> +
    + {texture.type === TextureType.Cape + ? t('general.cape') + : texture.type} +
    + {canEdit && ( +
    + +
    + )} + + )} +
    +
    +
    Hash
    +
    + {isLoading ? : texture.hash} +
    +
    +
    +
    {t('skinlib.show.size')}
    +
    + {isLoading ? : {texture.size} KB} +
    +
    +
    +
    {t('skinlib.show.uploader')}
    +
    + {isLoading ? ( + + ) : (isUploaderExists ? ( + <> + +
    + {badges.map(badge => ( + + {badge.text} + + ))} +
    + + ) : ( + nickname + ))} +
    +
    +
    +
    {t('skinlib.show.upload-at')}
    +
    + {isLoading ? : texture.upload_at} +
    +
    +
    +
    +
    + {canEdit && ( +
    +
    +

    {t('admin.operationsTitle')}

    +
    +
    +

    {t('skinlib.show.manage-notice')}

    +
    +
    +
    + + +
    +
    +
    + )} + - - ) -} + }} + onClose={handleCloseModalApply} + /> + + ); +}; -export default hot(Show) +export default hot(Show); diff --git a/resources/assets/src/views/skinlib/SkinLibrary/Button.tsx b/resources/assets/src/views/skinlib/SkinLibrary/Button.tsx index 623c43d6..0f267bbe 100644 --- a/resources/assets/src/views/skinlib/SkinLibrary/Button.tsx +++ b/resources/assets/src/views/skinlib/SkinLibrary/Button.tsx @@ -1,31 +1,32 @@ -import React from 'react' +import React from 'react'; -interface Props { - active?: boolean - bg?: string -} +type Properties = { + readonly active?: boolean; + readonly bg?: string; +}; type Attributes = React.DetailedHTMLProps< - React.ButtonHTMLAttributes, - HTMLButtonElement -> +React.ButtonHTMLAttributes, +HTMLButtonElement +>; -const Button: React.FC = (props) => { - const classes = [props.className ?? ''] - if (props.bg) { - classes.push('btn', `bg-${props.bg}`) - } - if (props.active) { - classes.push('active') - } +const Button: React.FC = properties => { + const classes = [properties.className ?? '']; + if (properties.bg) { + classes.push('btn', `bg-${properties.bg}`); + } - const rest = { ...props, active: undefined, bg: undefined } + if (properties.active) { + classes.push('active'); + } - return ( - - ) -} + const rest = {...properties, active: undefined, bg: undefined}; -export default Button + return ( + + ); +}; + +export default Button; diff --git a/resources/assets/src/views/skinlib/SkinLibrary/FilterSelector.tsx b/resources/assets/src/views/skinlib/SkinLibrary/FilterSelector.tsx index c215fa9e..628d1608 100644 --- a/resources/assets/src/views/skinlib/SkinLibrary/FilterSelector.tsx +++ b/resources/assets/src/views/skinlib/SkinLibrary/FilterSelector.tsx @@ -1,64 +1,75 @@ -import React from 'react' -import { t } from '@/scripts/i18n' -import { TextureType } from '@/scripts/types' -import Button from './Button' -import type { Filter } from './types' -import { humanizeType } from './utils' +import React from 'react'; +import Button from './Button'; +import type {Filter} from './types'; +import {humanizeType} from './utils'; +import {TextureType} from '@/scripts/types'; +import {t} from '@/scripts/i18n'; -interface Props { - filter: Filter - onChange(filter: Filter): void -} +type Properties = { + readonly filter: Filter; + onChange(filter: Filter): void; +}; -const FilterSelector: React.FC = (props) => { - const { filter, onChange } = props +const FilterSelector: React.FC = properties => { + const {filter, onChange} = properties; - const handleSkinClick = () => onChange('skin') - const handleSteveClick = () => onChange(TextureType.Steve) - const handleAlexClick = () => onChange(TextureType.Alex) - const handleCapeClick = () => onChange(TextureType.Cape) + const handleSkinClick = () => { + onChange('skin'); + }; - return ( - <> - -
    - - - - -
    - - ) -} + const handleSteveClick = () => { + onChange(TextureType.Steve); + }; -export default FilterSelector + const handleAlexClick = () => { + onChange(TextureType.Alex); + }; + + const handleCapeClick = () => { + onChange(TextureType.Cape); + }; + + return ( + <> + +
    + + + + +
    + + ); +}; + +export default FilterSelector; diff --git a/resources/assets/src/views/skinlib/SkinLibrary/Item.tsx b/resources/assets/src/views/skinlib/SkinLibrary/Item.tsx index 86f4f8cc..2026787b 100644 --- a/resources/assets/src/views/skinlib/SkinLibrary/Item.tsx +++ b/resources/assets/src/views/skinlib/SkinLibrary/Item.tsx @@ -1,9 +1,9 @@ -import React from 'react' -import styled from '@emotion/styled' -import { t } from '@/scripts/i18n' -import * as cssUtils from '@/styles/utils' -import type { LibraryItem } from './types' -import { humanizeType } from './utils' +import React from 'react'; +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'; const Card = styled.div` width: 245px; @@ -20,109 +20,109 @@ const Card = styled.div` img { height: 210px; } -` +`; const Icon = styled.i` height: 24px; -` +`; const Badge = styled.span` padding-top: 0.4rem; -` +`; const NickNameBadge = styled(Badge)` ${cssUtils.pointerCursor} max-width: 100px; -` +`; -interface ButtonLikeProps { - liked: boolean -} -const ButtonLike = styled.a` +type ButtonLikeProperties = { + liked: boolean; +}; +const ButtonLike = styled.a` ${cssUtils.pointerCursor} i, span { - color: ${(props) => (props.liked ? '#dc3545' : '#6c757d')}; + color: ${properties => (properties.liked ? '#dc3545' : '#6c757d')}; &:hover { - color: ${(props) => (props.liked ? '#dc3545' : '#343a40')}; + color: ${properties => (properties.liked ? '#dc3545' : '#343a40')}; } } -` +`; -interface Props { - item: LibraryItem - liked: boolean - onAdd(texture: LibraryItem): Promise - onRemove(texture: LibraryItem): Promise - onUploaderClick(uploader: number): void -} +type Properties = { + readonly item: LibraryItem; + readonly liked: boolean; + onAdd(texture: LibraryItem): Promise; + onRemove(texture: LibraryItem): Promise; + onUploaderClick(uploader: number): void; +}; -const Item: React.FC = (props) => { - const { item } = props +const Item: React.FC = properties => { + const {item} = properties; - const link = `${blessing.base_url}/skinlib/show/${item.tid}` - const preview = `${blessing.base_url}/preview/${item.tid}?height=150` - const previewPNG = `${preview}&png` + const link = `${blessing.base_url}/skinlib/show/${item.tid}`; + const preview = `${blessing.base_url}/preview/${item.tid}?height=150`; + const previewPNG = `${preview}&png`; - const handleUploaderClick = (event: React.MouseEvent) => { - event.preventDefault() - props.onUploaderClick(item.uploader) - } + const handleUploaderClick = (event: React.MouseEvent) => { + event.preventDefault(); + properties.onUploaderClick(item.uploader); + }; - const handleHeartClick = (event: React.MouseEvent) => { - event.preventDefault() - props.liked ? props.onRemove(item) : props.onAdd(item) - } + const handleHeartClick = (event: React.MouseEvent) => { + event.preventDefault(); + properties.liked ? properties.onRemove(item) : properties.onAdd(item); + }; - return ( - - - -
    -
    - {item.public || ( - - )} - - {item.name} - -
    -
    -
    - - {humanizeType(item.type)} - - - {item.nickname} - -
    - - - {item.likes} - -
    -
    - - - ) -} + return ( + + + +
    +
    + {item.public || ( + + )} + + {item.name} + +
    +
    +
    + + {humanizeType(item.type)} + + + {item.nickname} + +
    + + + {item.likes} + +
    +
    + + + ); +}; -export default Item +export default Item; diff --git a/resources/assets/src/views/skinlib/SkinLibrary/index.tsx b/resources/assets/src/views/skinlib/SkinLibrary/index.tsx index 4963ae97..0b8c3fbc 100644 --- a/resources/assets/src/views/skinlib/SkinLibrary/index.tsx +++ b/resources/assets/src/views/skinlib/SkinLibrary/index.tsx @@ -1,290 +1,304 @@ -import React, { useState, useEffect } from 'react' -import { hot } from 'react-hot-loader/root' -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 { Paginator, TextureType } from '@/scripts/types' -import urls from '@/scripts/urls' -import Loading from '@/components/Loading' -import Pagination from '@/components/Pagination' -import addClosetItem from '../Show/addClosetItem' -import removeClosetItem from '@/views/user/Closet/removeClosetItem' -import FilterSelector from './FilterSelector' -import Button from './Button' -import Item from './Item' -import type { Filter, LibraryItem } from './types' +import React, {useState, useEffect} from 'react'; +import {hot} from 'react-hot-loader/root'; +import addClosetItem from '../Show/addClosetItem'; +import FilterSelector from './FilterSelector'; +import Button from './Button'; +import Item from './Item'; +import type {Filter, LibraryItem} from './types'; +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 {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'; const SkinLibrary: React.FC = () => { - const [isLoading, setIsLoading] = useState(true) - const [items, setItems] = useState([]) - const [closet, setCloset] = useState([]) - const [filter, setFilter] = useState('skin') - const [name, setName] = useState('') - const [keyword, setKeyword] = useState('') - const [uploader, setUploader] = useState(0) - const [sort, setSort] = useState('time') - const [page, setPage] = useState(1) - const [totalPages, setTotalPages] = useState(1) - const currentUid = useBlessingExtra('currentUid', null) + const [isLoading, setIsLoading] = useState(true); + const [items, setItems] = useState([]); + const [closet, setCloset] = useState([]); + const [filter, setFilter] = useState('skin'); + const [name, setName] = useState(''); + const [keyword, setKeyword] = useState(''); + const [uploader, setUploader] = useState(0); + const [sort, setSort] = useState('time'); + const [page, setPage] = useState(1); + const [totalPages, setTotalPages] = useState(1); + const currentUid = useBlessingExtra('currentUid', null); - useEmitMounted() + useEmitMounted(); - useEffect(() => { - const parseSearch = (query: string) => { - const search = new URLSearchParams(query) + useEffect(() => { + const parseSearch = (query: string) => { + const search = new URLSearchParams(query); - const filter = search.get('filter') ?? '' - setFilter( - [ - 'skin', - TextureType.Steve, - TextureType.Alex, - TextureType.Cape, - ].includes(filter) - ? (filter as Filter) - : 'skin', - ) + const filter = search.get('filter') ?? ''; + setFilter( + [ + 'skin', + TextureType.Steve, + TextureType.Alex, + TextureType.Cape, + ].includes(filter) + ? (filter as Filter) + : 'skin', + ); - const keyword = decodeURIComponent(search.get('keyword') ?? '') - setName(keyword) - setKeyword(keyword) + const keyword = decodeURIComponent(search.get('keyword') ?? ''); + setName(keyword); + setKeyword(keyword); - const uploader = search.get('uploader') ?? '0' - setUploader(Number.parseInt(uploader)) + const uploader = search.get('uploader') ?? '0'; + setUploader(Number.parseInt(uploader)); - setSort(search.get('sort') ?? 'time') + setSort(search.get('sort') ?? 'time'); - setPage(Number.parseInt(search.get('page') ?? '1')) - } + setPage(Number.parseInt(search.get('page') ?? '1')); + }; - parseSearch(location.search) + parseSearch(location.search); - const handler = (event: PopStateEvent) => parseSearch(event.state) - window.addEventListener('popstate', handler) + const handler = (event: PopStateEvent) => { + parseSearch(String(event.state)); + }; - return () => { - window.removeEventListener('popstate', handler) - } - }, []) + window.addEventListener('popstate', handler); - useEffect(() => { - const getItems = async () => { - setIsLoading(true) + return () => { + window.removeEventListener('popstate', handler); + }; + }, []); - const search = new URLSearchParams() - search.append('filter', filter) - if (keyword) { - search.append('keyword', keyword) - } - if (uploader) { - search.append('uploader', uploader.toString()) - } - search.append('sort', sort) - search.append('page', page.toString()) - window.history.pushState(search.toString(), '', `?${search}`) + useEffect(() => { + const getItems = async () => { + setIsLoading(true); - const result = await fetch.get>( - urls.skinlib.list(), - search, - ) - setItems(result.data) - setTotalPages(result.last_page) - setIsLoading(false) - } - getItems() - }, [filter, keyword, uploader, sort, page]) + const search = new URLSearchParams(); + search.append('filter', filter); + if (keyword) { + search.append('keyword', keyword); + } - useEffect(() => { - const getCloset = async () => { - const closet = await fetch.get(urls.user.closet.ids()) - setCloset(closet) - } - if (currentUid) { - getCloset() - } - }, [currentUid]) + if (uploader) { + search.append('uploader', uploader.toString()); + } - const handleFilterChange = (filter: Filter) => { - setFilter(filter) - setPage(1) - } + search.append('sort', sort); + search.append('page', page.toString()); + window.history.pushState(search.toString(), '', `?${search}`); - const handleNameChange = (event: React.ChangeEvent) => { - setName(event.target.value) - } + const result = await fetch.get>( + urls.skinlib.list(), + search, + ); + setItems(result.data); + setTotalPages(result.last_page); + setIsLoading(false); + }; - const handleFormSubmit = (event: React.FormEvent) => { - event.preventDefault() - setKeyword(name) - setPage(1) - } + getItems(); + }, [filter, keyword, uploader, sort, page]); - const handleLikesSortClick = () => setSort('likes') - const handleTimeSortClick = () => setSort('time') - const handleSelfUploadClick = () => { - setUploader(currentUid) - setPage(1) - } - const handleResetClick = () => { - setFilter('skin') - setName('') - setKeyword('') - setSort('time') - setUploader(0) - setPage(1) - } + useEffect(() => { + const getCloset = async () => { + const closet = await fetch.get(urls.user.closet.ids()); + setCloset(closet); + }; - const handleUploaderClick = (uploader: number) => { - setUploader(uploader) - setPage(1) - } + if (currentUid) { + getCloset(); + } + }, [currentUid]); - const handleAddToCloset = async (item: LibraryItem, index: number) => { - if (!currentUid) { - toast.warning(t('skinlib.anonymous')) - return - } + const handleFilterChange = (filter: Filter) => { + setFilter(filter); + setPage(1); + }; - const ok = await addClosetItem(item) - if (ok) { - setCloset((closet) => [...closet, item.tid]) - setItems((items) => { - items[index] = { ...item, likes: item.likes + 1 } - return items.slice() - }) - } - } + const handleNameChange = (event: React.ChangeEvent) => { + setName(event.target.value); + }; - const handleRemoveFromCloset = async (item: LibraryItem, index: number) => { - const ok = await removeClosetItem(item.tid) - if (ok) { - setCloset((closet) => closet.filter((id) => id !== item.tid)) - setItems((items) => { - items[index] = { ...item, likes: item.likes - 1 } - return items.slice() - }) - } - } + const handleFormSubmit = (event: React.FormEvent) => { + event.preventDefault(); + setKeyword(name); + setPage(1); + }; - return ( -
    -
    -
    -

    {t('general.skinlib')}

    - - {uploader ? ( - <> - - {t('skinlib.filter.uploader', { uid: uploader })} - - ) : ( - <> - - {t('skinlib.filter.allUsers')} - - )} - -
    -
    -
    -
    -
    -
    -
    -
    -
    - -
    - -
    - -
    -
    -
    -
    -
    - - - {currentUid !== null && ( - - )} - -
    -
    -
    - {items.length > 0 ? ( -
    - {items.map((item, i) => ( - handleAddToCloset(item, i)} - onRemove={(item) => handleRemoveFromCloset(item, i)} - onUploaderClick={handleUploaderClick} - /> - ))} -
    - ) : ( -

    {t('general.noResult')}

    - )} -
    -
    -
    - -
    -
    - {isLoading && ( -
    - -
    - )} -
    -
    -
    - ) -} + const handleLikesSortClick = () => { + setSort('likes'); + }; -export default hot(SkinLibrary) + const handleTimeSortClick = () => { + setSort('time'); + }; + + const handleSelfUploadClick = () => { + setUploader(currentUid); + setPage(1); + }; + + const handleResetClick = () => { + setFilter('skin'); + setName(''); + setKeyword(''); + setSort('time'); + setUploader(0); + setPage(1); + }; + + const handleUploaderClick = (uploader: number) => { + setUploader(uploader); + setPage(1); + }; + + const handleAddToCloset = async (item: LibraryItem, index: number) => { + if (!currentUid) { + toast.warning(t('skinlib.anonymous')); + return; + } + + const ok = await addClosetItem(item); + if (ok) { + setCloset(closet => [...closet, item.tid]); + setItems(items => { + items[index] = {...item, likes: item.likes + 1}; + return [...items]; + }); + } + }; + + const handleRemoveFromCloset = async (item: LibraryItem, index: number) => { + const ok = await removeClosetItem(item.tid); + if (ok) { + setCloset(closet => closet.filter(id => id !== item.tid)); + setItems(items => { + items[index] = {...item, likes: item.likes - 1}; + return [...items]; + }); + } + }; + + return ( +
    +
    +
    +

    {t('general.skinlib')}

    + + {uploader ? ( + <> + + {t('skinlib.filter.uploader', {uid: uploader.toString()})} + + ) : ( + <> + + {t('skinlib.filter.allUsers')} + + )} + +
    +
    +
    +
    +
    +
    +
    +
    +
    + +
    + +
    + +
    +
    + +
    +
    + + + {currentUid !== null && ( + + )} + +
    +
    +
    + {items.length > 0 ? ( +
    + {items.map((item, i) => ( + handleAddToCloset(item, i)} + onRemove={async item => handleRemoveFromCloset(item, i)} + onUploaderClick={handleUploaderClick} + /> + ))} +
    + ) : ( +

    {t('general.noResult')}

    + )} +
    +
    +
    + +
    +
    + {isLoading && ( +
    + +
    + )} +
    +
    +
    + ); +}; + +export default hot(SkinLibrary); diff --git a/resources/assets/src/views/skinlib/SkinLibrary/types.ts b/resources/assets/src/views/skinlib/SkinLibrary/types.ts index c37e7d0a..5329c8ec 100644 --- a/resources/assets/src/views/skinlib/SkinLibrary/types.ts +++ b/resources/assets/src/views/skinlib/SkinLibrary/types.ts @@ -1,13 +1,13 @@ -import type { TextureType } from '@/scripts/types' +import type {TextureType} from '@/scripts/types'; -export type Filter = 'skin' | TextureType +export type Filter = 'skin' | TextureType; export type LibraryItem = { - tid: number - name: string - type: TextureType - uploader: number - public: boolean - likes: number - nickname: string -} + tid: number; + name: string; + type: TextureType; + uploader: number; + public: boolean; + likes: number; + nickname: string; +}; diff --git a/resources/assets/src/views/skinlib/SkinLibrary/utils.ts b/resources/assets/src/views/skinlib/SkinLibrary/utils.ts index 90891ea8..4cc520e1 100644 --- a/resources/assets/src/views/skinlib/SkinLibrary/utils.ts +++ b/resources/assets/src/views/skinlib/SkinLibrary/utils.ts @@ -1,14 +1,19 @@ -import { t } from '@/scripts/i18n' -import { TextureType } from '@/scripts/types' -import type { Filter } from './types' +import type {Filter} from './types'; +import {t} from '@/scripts/i18n'; +import {TextureType} from '@/scripts/types'; export function humanizeType(type: Filter): string { - switch (type) { - case TextureType.Steve: - return 'Steve' - case TextureType.Alex: - return 'Alex' - default: - return t(`general.${type}`) - } + switch (type) { + case TextureType.Steve: { + return 'Steve'; + } + + case TextureType.Alex: { + return 'Alex'; + } + + default: { + return t(`general.${type}`); + } + } } diff --git a/resources/assets/src/views/skinlib/Upload.tsx b/resources/assets/src/views/skinlib/Upload.tsx index 0f9e3ed3..13912114 100644 --- a/resources/assets/src/views/skinlib/Upload.tsx +++ b/resources/assets/src/views/skinlib/Upload.tsx @@ -1,259 +1,260 @@ -import React, { useState } from 'react' -import ReactDOM from 'react-dom' -import { hot } from 'react-hot-loader/root' -import { t } from '@/scripts/i18n' -import useBlessingExtra from '@/scripts/hooks/useBlessingExtra' -import useEmitMounted from '@/scripts/hooks/useEmitMounted' -import useMount from '@/scripts/hooks/useMount' -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'; +import {hot} from 'react-hot-loader/root'; +import {t} from '@/scripts/i18n'; +import useBlessingExtra from '@/scripts/hooks/useBlessingExtra'; +import useEmitMounted from '@/scripts/hooks/useEmitMounted'; +import useMount from '@/scripts/hooks/useMount'; +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'; -const Previewer = React.lazy(() => import('@/components/Viewer')) +const Previewer = React.lazy(async () => import('@/components/Viewer')); const Upload: React.FC = () => { - const [name, setName] = useState('') - const [type, setType] = useState(TextureType.Steve) - const [isPrivate, setIsPrivate] = useState(false) - const [isUploading, setIsUploading] = useState(false) - const [file, setFile] = useState(null) - const [texture, setTexture] = useState('') - const nameRule = useBlessingExtra('rule') - const contentPolicy = useBlessingExtra('contentPolicy') - const privacyNotice = useBlessingExtra('privacyNotice') - const award = useBlessingExtra('award') - const currentScore = useBlessingExtra('score', 0) - const scorePublic = useBlessingExtra('scorePublic') - const scorePrivate = useBlessingExtra('scorePrivate') - const closetItemCost = useBlessingExtra('closetItemCost') + const [name, setName] = useState(''); + const [type, setType] = useState(TextureType.Steve); + const [isPrivate, setIsPrivate] = useState(false); + const [isUploading, setIsUploading] = useState(false); + const [file, setFile] = useState(null); + const [texture, setTexture] = useState(''); + const nameRule = useBlessingExtra('rule'); + const contentPolicy = useBlessingExtra('contentPolicy'); + const privacyNotice = useBlessingExtra('privacyNotice'); + const award = useBlessingExtra('award'); + const currentScore = useBlessingExtra('score', 0); + const scorePublic = useBlessingExtra('scorePublic'); + const scorePrivate = useBlessingExtra('scorePrivate'); + const closetItemCost = useBlessingExtra('closetItemCost'); - const container = useMount('#previewer') + const container = useMount('#previewer'); - useEmitMounted() + useEmitMounted(); - const handleNameChange = (event: React.ChangeEvent) => { - setName(event.target.value) - } + const handleNameChange = (event: React.ChangeEvent) => { + setName(event.target.value); + }; - const handleTypeChange = (event: React.ChangeEvent) => { - setType(event.target.value as TextureType) - } + const handleTypeChange = (event: React.ChangeEvent) => { + setType(event.target.value as TextureType); + }; - const handlePrivateChange = (event: React.ChangeEvent) => { - setIsPrivate(event.target.checked) - } + const handlePrivateChange = (event: React.ChangeEvent) => { + setIsPrivate(event.target.checked); + }; - const handleFileChange = async ( - event: React.ChangeEvent, - ) => { - const files = event.target.files! - const [file] = files - if (file) { - setFile(file) - if (!name && file.name.endsWith('.png')) { - setName(file.name.slice(0, file.name.length - 4)) - } - const texture = URL.createObjectURL(file) - setTexture(texture) - if (type !== TextureType.Cape) { - setType((await isAlex(texture)) ? TextureType.Alex : TextureType.Steve) - } - } - } + const handleFileChange = async ( + event: React.ChangeEvent, + ) => { + const files = event.target.files!; + const [file] = files; + if (file) { + setFile(file); + if (!name && file.name.endsWith('.png')) { + setName(file.name.slice(0, -4)); + } - const handleUpload = async () => { - if (!file) { - toast.error(t('skinlib.emptyUploadFile')) - return - } + const texture = URL.createObjectURL(file); + setTexture(texture); + if (type !== TextureType.Cape) { + setType((await isAlex(texture)) ? TextureType.Alex : TextureType.Steve); + } + } + }; - if (!name) { - toast.error(t('skinlib.emptyTextureName')) - return - } + const handleUpload = async () => { + if (!file) { + toast.error(t('skinlib.emptyUploadFile')); + return; + } - if (file.type !== 'image/png' && file.type !== 'image/x-png') { - toast.error(t('skinlib.fileExtError')) - return - } + if (!name) { + toast.error(t('skinlib.emptyTextureName')); + return; + } - const formData = new FormData() - formData.append('name', name) - formData.append('type', type) - formData.append('file', file, file.name) - formData.append('public', isPrivate ? '0' : '1') + if (file.type !== 'image/png' && file.type !== 'image/x-png') { + toast.error(t('skinlib.fileExtError')); + return; + } - setIsUploading(true) - const { - code, - message, - data: { tid } = { tid: 0 }, - } = await fetch.post>( - urls.texture.upload(), - formData, - ) - setIsUploading(false) + const formData = new FormData(); + formData.append('name', name); + formData.append('type', type); + formData.append('file', file, file.name); + formData.append('public', isPrivate ? '0' : '1'); - if (code === 0) { - window.location.href = blessing.base_url + urls.skinlib.show(tid) - } else if (code === 2) { - try { - await showModal({ - mode: 'confirm', - text: message, - okButtonText: t('user.viewInSkinlib'), - }) - window.location.href = blessing.base_url + urls.skinlib.show(tid) - } catch { - // - } - } else { - toast.error(message) - } - } + setIsUploading(true); + const { + code, + message, + data: {tid} = {tid: 0}, + } = await fetch.post>( + urls.texture.upload(), + formData, + ); + setIsUploading(false); - const costRatio = isPrivate ? scorePrivate : scorePublic - const size = file?.size ?? 0 - const scoreCost = Math.ceil(size / 1024) * costRatio + closetItemCost + if (code === 0) { + window.location.href = blessing.base_url + urls.skinlib.show(tid); + } else if (code === 2) { + try { + await showModal({ + mode: 'confirm', + text: message, + okButtonText: t('user.viewInSkinlib'), + }); + window.location.href = blessing.base_url + urls.skinlib.show(tid); + } catch { + // + } + } else { + toast.error(message); + } + }; - return ( - <> -
    -
    -
    - - -
    -
    - -
    - - - -
    - + const costRatio = isPrivate ? scorePrivate : scorePublic; + const size = file?.size ?? 0; + const scoreCost = Math.ceil(size / 1024) * costRatio + closetItemCost; - {contentPolicy && ( -
    - )} -
    -
    -
    - - -
    - {file && ( -
    scoreCost ? 'success' : 'danger' - } mt-3`} - > -
    {t('skinlib.upload.cost', { score: scoreCost })}
    -
    - {t('user.cur-score')} - {currentScore} -
    -
    - )} - {isPrivate && ( -
    {privacyNotice}
    - )} - {!isPrivate && award > 0 && ( -
    - {t('skinlib.upload.award', { score: award })} -
    - )} -
    -
    - {container && - ReactDOM.createPortal( - }> - - , - container, + return ( + <> +
    +
    +
    + + +
    +
    + +
    + + + +
    + + + {contentPolicy && ( +
    + )} +
    +
    +
    + + +
    + {file && ( +
    scoreCost ? 'success' : 'danger' + } mt-3`} + > +
    {t('skinlib.upload.cost', {score: scoreCost.toString()})}
    +
    + {t('user.cur-score')} + {currentScore} +
    +
    + )} + {isPrivate && ( +
    {privacyNotice}
    + )} + {!isPrivate && award > 0 && ( +
    + {t('skinlib.upload.award', {score: award.toString()})} +
    + )} +
    +
    + {container + && ReactDOM.createPortal( + }> + + , + container, )} - - ) -} + + ); +}; -export default hot(Upload) +export default hot(Upload); diff --git a/resources/assets/src/views/user/Closet/ClosetItem.tsx b/resources/assets/src/views/user/Closet/ClosetItem.tsx index 831d02c7..5739196e 100644 --- a/resources/assets/src/views/user/Closet/ClosetItem.tsx +++ b/resources/assets/src/views/user/Closet/ClosetItem.tsx @@ -1,76 +1,76 @@ -import React from 'react' -import { t } from '@/scripts/i18n' -import type { ClosetItem as ClosetItemType } from '@/scripts/types' -import setAsAvatar from './setAsAvatar' -import { Card, DropdownButton } from './styles' +import React from 'react'; +import setAsAvatar from './setAsAvatar'; +import {Card, DropdownButton} from './styles'; +import {t} from '@/scripts/i18n'; +import type {ClosetItem as ClosetItemType} from '@/scripts/types'; -interface Props { - item: ClosetItemType - selected: boolean - onClick(item: ClosetItemType): void - onRename(): void - onRemove(): void -} +type Properties = { + readonly item: ClosetItemType; + readonly selected: boolean; + onClick(item: ClosetItemType): void; + onRename(): void; + onRemove(): void; +}; -const ClosetItem: React.FC = (props) => { - const { item } = props - const preview = `${blessing.base_url}/preview/${item.tid}?height=150` - const previewPNG = `${preview}&png` +const ClosetItem: React.FC = properties => { + const {item} = properties; + const preview = `${blessing.base_url}/preview/${item.tid}?height=150`; + const previewPNG = `${preview}&png`; - const handleItemClick = () => { - props.onClick(item) - } + const handleItemClick = () => { + properties.onClick(item); + }; - const handleSetAsAvatar = () => setAsAvatar(item.tid) + const handleSetAsAvatar = async () => setAsAvatar(item.tid); - return ( - -
    - - - {item.pivot.item_name} - -
    - -
    - ) -} + return ( + +
    + + + {item.pivot.item_name} + +
    + +
    + ); +}; -export default ClosetItem +export default ClosetItem; diff --git a/resources/assets/src/views/user/Closet/LoadingClosetItem.tsx b/resources/assets/src/views/user/Closet/LoadingClosetItem.tsx index a1a718a6..5402ba4e 100644 --- a/resources/assets/src/views/user/Closet/LoadingClosetItem.tsx +++ b/resources/assets/src/views/user/Closet/LoadingClosetItem.tsx @@ -1,26 +1,26 @@ -import React from 'react' -import styled from '@emotion/styled' -import Skeleton from 'react-loading-skeleton' -import { Card, DropdownButton } from './styles' +import React from 'react'; +import styled from '@emotion/styled'; +import Skeleton from 'react-loading-skeleton'; +import {Card, DropdownButton} from './styles'; const ItemNameSkeleton = styled(Skeleton)` width: 150px; -` +`; const LoadingClosetItem: React.FC = () => ( - -
    -
    -
    - - - - - - -
    -
    -
    -) + +
    +
    +
    + + + + + + +
    +
    + +); -export default LoadingClosetItem +export default LoadingClosetItem; diff --git a/resources/assets/src/views/user/Closet/ModalApply.tsx b/resources/assets/src/views/user/Closet/ModalApply.tsx index 7cae9885..840caea2 100644 --- a/resources/assets/src/views/user/Closet/ModalApply.tsx +++ b/resources/assets/src/views/user/Closet/ModalApply.tsx @@ -1,113 +1,114 @@ -import React, { useState, useEffect } from 'react' -import $ from 'jquery' -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 React, {useState, useEffect} from 'react'; +import $ from 'jquery'; +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'; -const baseUrl = blessing.base_url +const baseUrl = blessing.base_url; -interface Props { - show: boolean - canAdd: boolean - skin?: number - cape?: number - onClose(): void -} +type Properties = { + readonly show: boolean; + readonly canAdd: boolean; + readonly skin?: number; + readonly cape?: number; + onClose(): void; +}; -const ModalApply: React.FC = (props) => { - const [players, setPlayers] = useState([]) - const [search, setSearch] = useState('') - const [isLoading, setIsLoading] = useState(false) +const ModalApply: React.FC = properties => { + const [players, setPlayers] = useState([]); + const [search, setSearch] = useState(''); + const [isLoading, setIsLoading] = useState(false); - useEffect(() => { - if (!props.show) { - return - } + useEffect(() => { + if (!properties.show) { + return; + } - const getPlayers = async () => { - setIsLoading(true) - const players = await fetch.get(urls.user.player.list()) - setPlayers(players) - setIsLoading(false) - } - getPlayers() - }, [props.show]) + const getPlayers = async () => { + setIsLoading(true); + const players = await fetch.get(urls.user.player.list()); + setPlayers(players); + setIsLoading(false); + }; - const handleSearch = (event: React.ChangeEvent) => { - setSearch(event.target.value) - } + getPlayers(); + }, [properties.show]); - const handleSelect = async (player: Player) => { - const { code, message } = await fetch.put( - urls.user.player.set(player.pid), - { - skin: props.skin, - cape: props.cape, - }, - ) - if (code === 0) { - toast.success(message) - $('#modal-apply').modal('hide') - } else { - toast.error(message) - } - } + const handleSearch = (event: React.ChangeEvent) => { + setSearch(event.target.value); + }; - return ( - } - onClose={props.onClose} - > - {isLoading ? ( - - ) : players.length === 0 ? ( -

    {t('user.closet.use-as.empty')}

    - ) : ( - <> -
    - -
    - {players - .filter((player) => player.name.includes(search)) - .map((player) => ( - - ))} - - )} -
    - ) -} + const handleSelect = async (player: Player) => { + const {code, message} = await fetch.put( + urls.user.player.set(player.pid), + { + skin: properties.skin, + cape: properties.cape, + }, + ); + if (code === 0) { + toast.success(message); + $('#modal-apply').modal('hide'); + } else { + toast.error(message); + } + }; -export default ModalApply + return ( + } + onClose={properties.onClose} + > + {isLoading ? ( + + ) : (players.length === 0 ? ( +

    {t('user.closet.use-as.empty')}

    + ) : ( + <> +
    + +
    + {players + .filter(player => player.name.includes(search)) + .map(player => ( + + ))} + + ))} +
    + ); +}; + +export default ModalApply; diff --git a/resources/assets/src/views/user/Closet/Previewer.tsx b/resources/assets/src/views/user/Closet/Previewer.tsx index 01ff4f4f..08aee849 100644 --- a/resources/assets/src/views/user/Closet/Previewer.tsx +++ b/resources/assets/src/views/user/Closet/Previewer.tsx @@ -1,33 +1,34 @@ -import React from 'react' -import ReactDOM from 'react-dom' -import useMount from '@/scripts/hooks/useMount' -import ViewerSkeleton from '@/components/ViewerSkeleton' +import React from 'react'; +import ReactDOM from 'react-dom'; +import useMount from '@/scripts/hooks/useMount'; +import ViewerSkeleton from '@/components/ViewerSkeleton'; -const Viewer = React.lazy(() => import('@/components/Viewer')) +const Viewer = React.lazy(async () => import('@/components/Viewer')); -interface Props { - skin?: string - cape?: string - isAlex: boolean -} +type Properties = { + skin?: string; + cape?: string; + children: React.ReactNode; + isAlex: boolean; +}; -const Previewer: React.FC = (props) => { - const container = useMount('#previewer') +const Previewer: React.FC = properties => { + const container = useMount('#previewer'); - const skin = props.skin ? `${blessing.base_url}/textures/${props.skin}` : '' - const cape = props.cape ? `${blessing.base_url}/textures/${props.cape}` : '' + const skin = properties.skin ? `${blessing.base_url}/textures/${properties.skin}` : ''; + const cape = properties.cape ? `${blessing.base_url}/textures/${properties.cape}` : ''; - return ( - container && - ReactDOM.createPortal( - }> - - {props.children} - - , - container, + return ( + container + && ReactDOM.createPortal( + }> + + {properties.children} + + , + container, ) - ) -} + ); +}; -export default Previewer +export default Previewer; diff --git a/resources/assets/src/views/user/Closet/index.tsx b/resources/assets/src/views/user/Closet/index.tsx index 45c31551..862084d4 100644 --- a/resources/assets/src/views/user/Closet/index.tsx +++ b/resources/assets/src/views/user/Closet/index.tsx @@ -1,288 +1,298 @@ -import React, { useState, useEffect, useRef } from 'react' -import { hot } from 'react-hot-loader/root' -import debounce from 'lodash.debounce' -import useEmitMounted from '@/scripts/hooks/useEmitMounted' -import { t } from '@/scripts/i18n' -import * as fetch from '@/scripts/net' -import { showModal, toast } from '@/scripts/notify' +import React, {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 useEmitMounted from '@/scripts/hooks/useEmitMounted'; +import {t} from '@/scripts/i18n'; +import * as fetch from '@/scripts/net'; +import {showModal, toast} from '@/scripts/notify'; import { - ClosetItem as Item, - Texture, - Paginator, - TextureType, -} from '@/scripts/types' -import urls from '@/scripts/urls' -import Pagination from '@/components/Pagination' -import ClosetItem from './ClosetItem' -import LoadingClosetItem from './LoadingClosetItem' -import Previewer from './Previewer' -import ModalApply from './ModalApply' -import removeClosetItem from './removeClosetItem' + type ClosetItem as Item, + type Texture, + type Paginator, + TextureType, +} from '@/scripts/types'; +import urls from '@/scripts/urls'; +import Pagination from '@/components/Pagination'; -type Category = 'skin' | 'cape' +type Category = 'skin' | 'cape'; const updater = debounce( - ( - value: React.SetStateAction, - setter: React.Dispatch>, - ) => setter(value), - 350, -) + // eslint-disable-next-line @typescript-eslint/comma-dangle + ( + value: React.SetStateAction, + setter: React.Dispatch>, + ) => { + setter(value); + }, + 350, +) as (value: React.SetStateAction, setter: React.Dispatch>) => void; -const Closet: React.FC = () => { - const [isLoading, setIsLoading] = useState(true) - const [category, setCategory] = useState('skin') - const [search, setSearch] = useState('') - const [query, setQuery] = useState('') - const [page, setPage] = useState(1) - const [totalPages, setTotalPages] = useState(1) - const [items, setItems] = useState([]) - const [skin, setSkin] = useState(null) - const [cape, setCape] = useState(null) - const [showModalApply, setShowModalApply] = useState(false) - const containerRef = useRef(null) - const perPageRef = useRef(6) +function Closet() { + const [isLoading, setIsLoading] = useState(true); + const [category, setCategory] = useState('skin'); + const [search, setSearch] = useState(''); + const [query, setQuery] = useState(''); + const [page, setPage] = useState(1); + const [totalPages, setTotalPages] = useState(1); + const [items, setItems] = useState([]); + const [skin, setSkin] = useState(null); + const [cape, setCape] = useState(null); + const [showModalApply, setShowModalApply] = useState(false); + const containerReference = useRef(null); + const perPageReference = useRef(6); - useEmitMounted() + useEmitMounted(); - useEffect(() => { - const element = containerRef.current - /* istanbul ignore next */ - if (element) { - const { width } = element.getBoundingClientRect() - if (width >= 500) { - perPageRef.current = Math.floor(width / 235) * 2 - } - } - }, []) + useEffect(() => { + const element = containerReference.current; + /* istanbul ignore next */ + if (element) { + const {width} = element.getBoundingClientRect(); + if (width >= 500) { + perPageReference.current = Math.floor(width / 235) * 2; + } + } + }, []); - useEffect(() => { - const getItems = async () => { - setIsLoading(true) - const { data, last_page } = await fetch.get>( - urls.user.closet.list(), - { category, q: query, page, perPage: perPageRef.current }, - ) + useEffect(() => { + const getItems = async () => { + setIsLoading(true); + const {data, last_page: lastPage} = await fetch.get>( + urls.user.closet.list(), + { + category, q: query, page: page.toString(), perPage: perPageReference.current.toString(), + }, + ); - setItems(data) - setTotalPages(last_page) - setIsLoading(false) - } - getItems() - }, [category, query, page]) + setItems(data); + setTotalPages(lastPage); + setIsLoading(false); + }; - const switchCategoryToSkin = () => { - if (category !== 'skin') { - setCategory('skin') - setPage(1) - } - } + void getItems(); + }, [category, query, page]); - const switchCategoryToCape = () => { - if (category !== 'cape') { - setCategory('cape') - setPage(1) - } - } + const switchCategoryToSkin = () => { + if (category !== 'skin') { + setCategory('skin'); + setPage(1); + } + }; - const handleSearch = (event: React.ChangeEvent) => { - const value = event.target.value - setSearch(value) - updater(value, setQuery) - } + const switchCategoryToCape = () => { + if (category !== 'cape') { + setCategory('cape'); + setPage(1); + } + }; - const handlePageChange = (page: number) => setPage(page) + const handleSearch = (event: React.ChangeEvent) => { + const {value} = event.target; + setSearch(value); + updater(value, setQuery); + }; - const isSelected = (item: Item): boolean => { - if (category === 'skin') { - return item.tid === skin?.tid - } else { - return item.tid === cape?.tid - } - } + const handlePageChange = (page: number) => { + setPage(page); + }; - const handleSelect = (item: Item) => { - if (item.type === TextureType.Cape) { - setCape(item) - } else { - setSkin(item) - } - } + const isSelected = (item: Item): boolean => { + if (category === 'skin') { + return item.tid === skin?.tid; + } - const resetSelected = () => { - setSkin(null) - setCape(null) - } + return item.tid === cape?.tid; + }; - const renameItem = async (item: Item, index: number) => { - let name: string - try { - const { value } = await showModal({ - mode: 'prompt', - text: t('user.renameClosetItem'), - input: item.pivot.item_name, - validator: (value: string) => { - if (!value) { - return t('skinlib.emptyNewTextureName') - } - }, - }) - name = value - } catch { - return - } + const handleSelect = (item: Item) => { + if (item.type === TextureType.Cape) { + setCape(item); + } else { + setSkin(item); + } + }; - const { code, message } = await fetch.put( - urls.user.closet.rename(item.tid), - { name }, - ) - if (code === 0) { - toast.success(message) - setItems((items) => { - items[index] = { ...item, pivot: { ...item.pivot, item_name: name } } - return items.slice() - }) - } else { - toast.error(message) - } - } + const resetSelected = () => { + setSkin(null); + setCape(null); + }; - const removeItem = async (item: Item) => { - const { tid } = item - const ok = await removeClosetItem(tid) - if (ok) { - setItems((items) => items.filter((item) => item.tid !== tid)) - } - } + const renameItem = async (item: Item, index: number) => { + let name: string; + try { + const {value} = await showModal({ + mode: 'prompt', + text: t('user.renameClosetItem'), + input: item.pivot.item_name, + validator(value: string) { + if (!value) { + return t('skinlib.emptyNewTextureName'); + } + }, + }); + name = value; + } catch { + return; + } - const applyToPlayer = () => { - if (!skin && !cape) { - toast.info(t('user.emptySelectedTexture')) - return - } - setShowModalApply(true) - } + const {code, message} = await fetch.put( + urls.user.closet.rename(item.tid), + {name}, + ); + if (code === 0) { + toast.success(message); + setItems(items => { + items[index] = {...item, pivot: {...item.pivot, item_name: name}}; + return [...items]; + }); + } else { + toast.error(message); + } + }; - return ( - <> -
    - -
    - {isLoading ? ( -
    - {new Array(perPageRef.current).fill(null).map((_, i) => ( - - ))} -
    - ) : items.length === 0 ? ( -
    - {search ? ( - t('general.noResult') - ) : ( - - )} -
    - ) : ( -
    - {items.map((item, i) => ( - renameItem(item, i)} - onRemove={() => removeItem(item)} - /> - ))} -
    - )} -
    -
    -
    - -
    -
    -
    - -
    - - -
    -
    - setShowModalApply(false)} - /> - - ) + const removeItem = async (item: Item) => { + const {tid} = item; + const ok = await removeClosetItem(tid); + if (ok) { + setItems(items => items.filter(item => item.tid !== tid)); + } + }; + + const applyToPlayer = () => { + if (!skin && !cape) { + toast.info(t('user.emptySelectedTexture')); + return; + } + + setShowModalApply(true); + }; + + return ( + <> +
    + +
    + {isLoading ? ( +
    + {new Array(perPageReference.current).fill(null).map((_, i) => ( + + ))} +
    + ) : (items.length === 0 ? ( +
    + {search ? ( + t('general.noResult') + ) : ( + + )} +
    + ) : ( +
    + {items.map((item, i) => ( + renameItem(item, i)} + onRemove={async () => removeItem(item)} + /> + ))} +
    + ))} +
    +
    +
    + +
    +
    +
    + +
    + + +
    +
    + { + setShowModalApply(false); + }} + /> + + ); } -export default hot(Closet) +export default hot(Closet); diff --git a/resources/assets/src/views/user/Closet/removeClosetItem.ts b/resources/assets/src/views/user/Closet/removeClosetItem.ts index 9f62c254..035d99ca 100644 --- a/resources/assets/src/views/user/Closet/removeClosetItem.ts +++ b/resources/assets/src/views/user/Closet/removeClosetItem.ts @@ -1,26 +1,26 @@ -import { t } from '@/scripts/i18n' -import * as fetch from '@/scripts/net' -import { showModal, toast } from '@/scripts/notify' -import urls from '@/scripts/urls' +import {t} from '@/scripts/i18n'; +import * as fetch from '@/scripts/net'; +import {showModal, toast} from '@/scripts/notify'; +import urls from '@/scripts/urls'; export default async function removeClosetItem(tid: number): Promise { - try { - await showModal({ - text: t('user.removeFromClosetNotice'), - okButtonType: 'danger', - }) - } catch { - return false - } + try { + await showModal({ + text: t('user.removeFromClosetNotice'), + okButtonType: 'danger', + }); + } catch { + return false; + } - const { code, message } = await fetch.del( - urls.user.closet.remove(tid), - ) - if (code === 0) { - toast.success(message) - } else { - toast.error(message) - } + const {code, message} = await fetch.del( + urls.user.closet.remove(tid), + ); + if (code === 0) { + toast.success(message); + } else { + toast.error(message); + } - return code === 0 + return code === 0; } diff --git a/resources/assets/src/views/user/Closet/setAsAvatar.ts b/resources/assets/src/views/user/Closet/setAsAvatar.ts index c75ec617..a5282d87 100644 --- a/resources/assets/src/views/user/Closet/setAsAvatar.ts +++ b/resources/assets/src/views/user/Closet/setAsAvatar.ts @@ -1,30 +1,29 @@ -import { t } from '@/scripts/i18n' -import * as fetch from '@/scripts/net' -import { showModal, toast } from '@/scripts/notify' -import urls from '@/scripts/urls' +import {t} from '@/scripts/i18n'; +import * as fetch from '@/scripts/net'; +import {showModal, toast} from '@/scripts/notify'; +import urls from '@/scripts/urls'; export default async function setAsAvatar(tid: number) { - try { - await showModal({ - title: t('user.setAvatar'), - text: t('user.setAvatarNotice'), - }) - } catch { - return - } + try { + await showModal({ + title: t('user.setAvatar'), + text: t('user.setAvatarNotice'), + }); + } catch { + return; + } - const { code, message } = await fetch.post( - urls.user.profile.avatar(), - { tid }, - ) - if (code === 0) { - toast.success(message) - document - .querySelectorAll('[alt="User Image"]') - .forEach((el) => { - el.src = `${blessing.base_url}/avatar/${tid}` - }) - } else { - toast.error(message) - } + const {code, message} = await fetch.post( + urls.user.profile.avatar(), + {tid}, + ); + if (code === 0) { + toast.success(message); + for (const element of document + .querySelectorAll('[alt="User Image"]')) { + element.src = `${blessing.base_url}/avatar/${tid}`; + } + } else { + toast.error(message); + } } diff --git a/resources/assets/src/views/user/Closet/styles.ts b/resources/assets/src/views/user/Closet/styles.ts index 1deb7913..f381a49c 100644 --- a/resources/assets/src/views/user/Closet/styles.ts +++ b/resources/assets/src/views/user/Closet/styles.ts @@ -1,4 +1,4 @@ -import styled from '@emotion/styled' +import styled from '@emotion/styled'; export const Card = styled.div` width: 235px; @@ -12,11 +12,11 @@ export const Card = styled.div` .card-body { background-color: #eff1f0; } -` +`; export const DropdownButton = styled.span` color: var(--gray); :hover { color: #000; } -` +`; diff --git a/resources/assets/src/views/user/Dashboard/InfoBox.tsx b/resources/assets/src/views/user/Dashboard/InfoBox.tsx index 5f4836f2..e288081c 100644 --- a/resources/assets/src/views/user/Dashboard/InfoBox.tsx +++ b/resources/assets/src/views/user/Dashboard/InfoBox.tsx @@ -1,34 +1,34 @@ -import React from 'react' +import React from 'react'; -interface Props { - name: string - icon: string - color: string - used: number - unused: number - unit: string -} +type Properties = { + readonly name: string; + readonly icon: string; + readonly color: string; + readonly used: number; + readonly unused: number; + readonly unit: string; +}; -const InfoBox: React.FC = (props) => { - const total = ~~(props.used + props.unused) - const percentage = (props.used / total) * 100 +const InfoBox: React.FC = properties => { + const total = Math.trunc(properties.used + properties.unused); + const percentage = (properties.used / total) * 100; - return ( -
    - - - -
    - {props.name} - - {props.used} / {total} {props.unit} - -
    -
    -
    -
    -
    - ) -} + return ( +
    + + + +
    + {properties.name} + + {properties.used} / {total} {properties.unit} + +
    +
    +
    +
    +
    + ); +}; -export default React.memo(InfoBox) +export default React.memo(InfoBox); diff --git a/resources/assets/src/views/user/Dashboard/SignButton.tsx b/resources/assets/src/views/user/Dashboard/SignButton.tsx index 5d32a08b..a279e9b6 100644 --- a/resources/assets/src/views/user/Dashboard/SignButton.tsx +++ b/resources/assets/src/views/user/Dashboard/SignButton.tsx @@ -1,36 +1,36 @@ -import React from 'react' -import { t } from '@/scripts/i18n' -import * as scoreUtils from './scoreUtils' +import React from 'react'; +import * as scoreUtils from './scoreUtils'; +import {t} from '@/scripts/i18n'; -interface Props { - isLoading: boolean - lastSign: Date - canSignAfterZero: boolean - signGap: number - onClick: React.MouseEventHandler -} +type Properties = { + readonly isLoading: boolean; + readonly lastSign: Date; + readonly canSignAfterZero: boolean; + readonly signGap: number; + readonly onClick: React.MouseEventHandler; +}; -const SignButton: React.FC = (props) => { - const { lastSign, signGap, canSignAfterZero } = props - const remainingTime = scoreUtils.remainingTime( - lastSign, - signGap, - canSignAfterZero, - ) - const remainingTimeText = scoreUtils.remainingTimeText(remainingTime) - const canSign = remainingTime <= 0 +const SignButton: React.FC = properties => { + const {lastSign, signGap, canSignAfterZero} = properties; + const remainingTime = scoreUtils.remainingTime( + lastSign, + signGap, + canSignAfterZero, + ); + const remainingTimeText = scoreUtils.remainingTimeText(remainingTime); + const canSign = remainingTime <= 0; - return ( -