diff --git a/.eslintrc b/.eslintrc index 26d7dd6e7..9056a1653 100644 --- a/.eslintrc +++ b/.eslintrc @@ -1,92 +1,87 @@ { - "extends": ["airbnb", "airbnb/rules/react", "eslint:recommended", "prettier"], - "parserOptions": { - "ecmaVersion": "latest" - }, + "$schema": "https://json.schemastore.org/eslintrc.json", + "root": true, "env": { - "browser": true, - "jest": true, + "browser": false, + "es2023": true, "node": true }, - "globals": { - "document": true, - "window": true, - "CONFIG": true - }, + "extends": [ + "plugin:prettier/recommended" + ], + "plugins": [ + "unused-imports", + "@typescript-eslint", + "import", + "prettier" + ], + "parser": "@typescript-eslint/parser", "rules": { - "global-require": 0, - "lines-between-class-members": 0, - "no-continue": 0, - "no-plusplus": 0, - "import/no-dynamic-require": 0, - "camelcase": 0, - "react/jsx-props-no-spreading": 0, - "linebreak-style": 0, - "react/prop-types": 0, - "no-confusing-arrow": 0, - "no-restricting-syntax": 0, - "semi": 0, - "allowForLoopAfterthoughts": 0, - "radix": 0, - "eqeqeq": 0, - "jsx-a11y/aria-role": 0, - "consistent-return": 0, - "no-use-before-define": 0, - "no-param-reassign": 0, - "arrow-parens": 0, - "react/jsx-one-expression-per-line": 0, - "jsx-a11y/no-noninteractive-element-interactions": 0, - "jsx-a11y/click-events-have-key-events": 0, - "react/function-component-definition": 0, - "no-underscore-dangle": 0, - "no-return-assign": 0, - "no-nested-ternary": "off", - "import/prefer-default-export": 0, - "max-len": [ - "error", - 120, - 2, + "no-console": "warn", + "prettier/prettier": "warn", + "no-unused-vars": "off", + "unused-imports/no-unused-vars": "off", + "unused-imports/no-unused-imports": "warn", + "@typescript-eslint/no-unused-vars": [ + "warn", { - "ignoreUrls": true, - "ignoreComments": true, - "ignoreRegExpLiterals": true, - "ignoreStrings": true, - "ignoreTemplateLiterals": true, - "ignorePattern": "^(.*)@typedef(.*)" + "args": "after-used", + "ignoreRestSiblings": false, + "argsIgnorePattern": "^_.*?$" } ], - "no-unused-vars": [ - "error", + "import/order": [ + "warn", { - "varsIgnorePattern": "^TypesDefs(.*)" + "groups": [ + "type", + "builtin", + "object", + "external", + "internal", + "parent", + "sibling", + "index" + ], + "pathGroups": [ + { + "pattern": "~/**", + "group": "external", + "position": "after" + } + ], + "newlines-between": "always" } ], - "object-curly-newline": [ - "error", + "padding-line-between-statements": [ + "warn", { - "ExportDeclaration": { - "minProperties": 4 - } - } - ] - }, - "settings": { - "node": { - "extensions": [".mjs", ".js", ".jsx", ".ts", ".tsx"] - }, - "import/resolver": { - "alias": { - "map": [ - ["@components", "./src/components/"], - ["@features", "./src/features/"], - ["@services", "./src/services/"], - ["@hooks", "./src/hooks/"], - ["@assets", "./src/assets/"], - ["@utils", "./src/utils/"], - ["@store", "./src/store/"] + "blankLine": "always", + "prev": "*", + "next": "return" + }, + { + "blankLine": "always", + "prev": [ + "const", + "let", + "var" ], - "extensions": [".mjs", ".js", ".jsx", ".ts", ".tsx"] + "next": "*" + }, + { + "blankLine": "any", + "prev": [ + "const", + "let", + "var" + ], + "next": [ + "const", + "let", + "var" + ] } - } + ] } -} +} \ No newline at end of file diff --git a/.gitignore b/.gitignore index dbbc19e20..541cf9be3 100644 --- a/.gitignore +++ b/.gitignore @@ -7,6 +7,7 @@ server/src/configs/**/* config/local.json config/areas.json !server/src/configs/.gitkeep +config/user/**/* .env # Masterfile diff --git a/.husky/commit-msg b/.husky/commit-msg index fc28b52c8..418bf3132 100755 --- a/.husky/commit-msg +++ b/.husky/commit-msg @@ -1,4 +1,4 @@ #!/bin/sh -. "$(dirname "$0")/_/husky.sh" +# . "$(dirname "$0")/_/husky.sh" -npx --no -- commitlint --edit "${1}" +# npx --no -- commitlint --edit "${1}" diff --git a/.husky/pre-commit b/.husky/pre-commit index ba85fbeec..88b0afa71 100755 --- a/.husky/pre-commit +++ b/.husky/pre-commit @@ -1,4 +1,4 @@ #!/bin/sh -. "$(dirname "$0")/_/husky.sh" +# . "$(dirname "$0")/_/husky.sh" -npx lint-staged --verbose +# npx lint-staged --verbose diff --git a/.vscode/settings.json b/.vscode/settings.json index 7a7d02ce0..8794e9292 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -15,4 +15,4 @@ }, "editor.formatOnSave": true, "typescript.tsdk": "node_modules/typescript/lib" -} +} \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index da98a0a1c..246afd929 100644 --- a/Dockerfile +++ b/Dockerfile @@ -13,7 +13,6 @@ ENV PATH=$PATH:/home/node/.npm-global/bin WORKDIR /home/node COPY package.json . COPY yarn.lock . -RUN apk add git RUN npm install -g yarn COPY . . RUN yarn install diff --git a/README.md b/README.md index af0245d70..fe62f8802 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ ReactMap_Logo

-# ReactMap +# ReactMap 2.0 [![GitHub Release](https://img.shields.io/github/release/WatWowMap/ReactMap.svg)](https://github.com/WatWowMap/ReactMap/releases/) [![GitHub Contributors](https://img.shields.io/github/contributors/WatWowMap/ReactMap.svg)](https://github.com/WatWowMap/ReactMap/graphs/contributors/) diff --git a/ReactMap.js b/ReactMap.js index f01005a0c..c1411ce56 100644 --- a/ReactMap.js +++ b/ReactMap.js @@ -1,12 +1,9 @@ // @ts-check -/* eslint-disable import/no-extraneous-dependencies */ -const { build } = require('vite') - const { log, TAGS } = require('@rm/logger') const { generate } = require('@rm/masterfile') generate(true).then(() => - build() + require('@rm/client') .then(() => log.info(TAGS.build, 'React Map Compiled')) - .then(() => require('./server/src/index')), + .then(() => require('@rm/server')), ) diff --git a/client/.eslintrc b/client/.eslintrc new file mode 100644 index 000000000..5b8e87276 --- /dev/null +++ b/client/.eslintrc @@ -0,0 +1,51 @@ +{ + "$schema": "https://json.schemastore.org/eslintrc.json", + "env": { + "browser": true, + "es2023": true, + "node": false + }, + "globals": { + "CONFIG": true + }, + "extends": [ + "plugin:react/recommended", + "plugin:react-hooks/recommended", + "plugin:jsx-a11y/recommended", + "../.eslintrc" + ], + "plugins": [ + "react", + "jsx-a11y" + ], + "parser": "@typescript-eslint/parser", + "parserOptions": { + "ecmaFeatures": { + "jsx": true + }, + "ecmaVersion": 15 + }, + "settings": { + "react": { + "version": "detect" + } + }, + "rules": { + "react/prop-types": "off", + "react/jsx-uses-react": "off", + "react/react-in-jsx-scope": "off", + "react-hooks/exhaustive-deps": "off", + "jsx-a11y/click-events-have-key-events": "warn", + "jsx-a11y/interactive-supports-focus": "warn", + "react/self-closing-comp": "warn", + "react/jsx-sort-props": [ + "warn", + { + "callbacksLast": true, + "shorthandFirst": true, + "noSortAlphabetically": false, + "reservedFirst": true + } + ] + } +} \ No newline at end of file diff --git a/client/build.js b/client/build.js new file mode 100644 index 000000000..188b716c7 --- /dev/null +++ b/client/build.js @@ -0,0 +1,14 @@ +// @ts-check + +const { build } = require('vite') + +const viteConfig = require('./vite.config') + +process.env.SKIP_CONFIG = 'true' + +module.exports = build( + viteConfig({ + mode: process.env.NODE_ENV || 'production', + command: 'build', + }), +) diff --git a/index.html b/client/index.html similarity index 96% rename from index.html rename to client/index.html index 1d46b76f7..9695a4783 100644 --- a/index.html +++ b/client/index.html @@ -47,6 +47,6 @@

This app requires JavaScript

rel="stylesheet" href="https://use.fontawesome.com/releases/v6.4.0/css/all.css" /> - + diff --git a/client/package.json b/client/package.json new file mode 100644 index 000000000..f5ab42ef5 --- /dev/null +++ b/client/package.json @@ -0,0 +1,64 @@ +{ + "name": "@rm/client", + "version": "1.0.0", + "private": true, + "main": "./build.js", + "scripts": { + "build": "vite build", + "release": "vite build -- -r", + "watch": "vite" + }, + "dependencies": { + "@apollo/client": "3.11.4", + "@emotion/react": "11.13.0", + "@emotion/styled": "11.13.0", + "@monaco-editor/react": "4.6.0", + "@mui/icons-material": "5.16.7", + "@mui/lab": "5.0.0-alpha.173", + "@mui/material": "5.16.7", + "@rm/config": "*", + "@rm/locales": "*", + "@rm/logger": "*", + "@sentry/react": "^7.65.0", + "@turf/boolean-point-in-polygon": "7.1.0", + "@turf/destination": "7.1.0", + "@turf/helpers": "7.1.0", + "@turtlesocks/react-leaflet.locatecontrol": "^0.1.1", + "dlv": "^1.1.3", + "graphql": "16.9.0", + "i18next": "23.12.3", + "i18next-browser-languagedetector": "8.0.0", + "i18next-fs-backend": "2.3.2", + "i18next-http-backend": "2.5.2", + "leaflet": "1.9.4", + "leaflet.locatecontrol": "0.81.0", + "lodash": "^4.17.21", + "nodes2ts": "3.0.0", + "react": "18.3.1", + "react-dom": "18.3.1", + "react-ga4": "^1.4.1", + "react-i18next": "15.0.1", + "react-leaflet": "4.2.1", + "react-router-dom": "^6.15.0", + "react-virtuoso": "4.10.1", + "suncalc": "^1.9.0", + "supercluster": "^8.0.1", + "uicons.js": "2.0.2", + "zustand": "4.4.6" + }, + "devDependencies": { + "@rm/types": "*", + "@sentry/vite-plugin": "2.10.3", + "@types/leaflet": "1.9.12", + "@types/leaflet.locatecontrol": "0.74.5", + "@types/react": "^18.3.3", + "@types/react-dom": "^18.3.0", + "@types/suncalc": "1.9.2", + "@types/supercluster": "7.1.3", + "@vitejs/plugin-react-swc": "3.7.0", + "monaco-editor": "^0.41.0", + "rollup-plugin-delete": "^2.0.0", + "vite": "5.4.6", + "vite-plugin-checker": "0.7.2" + } +} \ No newline at end of file diff --git a/packages/vite-plugins/lib/customFile.js b/client/plugins/customFile.js similarity index 99% rename from packages/vite-plugins/lib/customFile.js rename to client/plugins/customFile.js index 510144bb8..c8933b74c 100644 --- a/packages/vite-plugins/lib/customFile.js +++ b/client/plugins/customFile.js @@ -9,14 +9,17 @@ const fs = require('fs') const customFilePlugin = (isDevelopment) => { const fileRegex = /\.(jsx?|css)$/ const customPaths = [] + return { name: 'vite-plugin-custom-file-checker', load(id) { if (fileRegex.test(id) && !/node_modules/.test(id)) { const ext = extname(id) const newPath = id.replace(ext, `.custom${ext}`) + if (fs.existsSync(newPath)) { customPaths.push(newPath) + return { code: fs.readFileSync(newPath, 'utf8'), map: null, diff --git a/packages/vite-plugins/lib/locale.js b/client/plugins/locale.js similarity index 73% rename from packages/vite-plugins/lib/locale.js rename to client/plugins/locale.js index 6e7fd86e5..d270d80c5 100644 --- a/packages/vite-plugins/lib/locale.js +++ b/client/plugins/locale.js @@ -1,7 +1,7 @@ // @ts-check const { join } = require('path') -const { create, writeAll } = require('@rm/locales') +const { create } = require('@rm/locales') /** * @param {boolean} isDevelopment @@ -9,17 +9,18 @@ const { create, writeAll } = require('@rm/locales') */ const localePlugin = (isDevelopment) => ({ name: 'vite-plugin-locales', - async buildStart() { - if (!isDevelopment) return - const localeObj = await create() - await writeAll(localeObj, true, __dirname, '../../../public/locales') - }, + // async buildStart() { + // if (!isDevelopment) return + // const localeObj = await create() + // await writeAll(localeObj, true, __dirname, '../public/locales') + // }, async generateBundle() { if (isDevelopment) return const localeObj = await create() Object.entries(localeObj).forEach(([locale, translations]) => { const fileName = join('locales', locale, 'translation.json') + this.emitFile({ type: 'asset', fileName, diff --git a/packages/vite-plugins/lib/muteWarnings.js b/client/plugins/muteWarnings.js similarity index 99% rename from packages/vite-plugins/lib/muteWarnings.js rename to client/plugins/muteWarnings.js index 79036fcc9..63bd500ac 100644 --- a/packages/vite-plugins/lib/muteWarnings.js +++ b/client/plugins/muteWarnings.js @@ -6,6 +6,7 @@ */ const muteWarningsPlugin = (warningsToIgnore) => { const mutedMessages = new Set() + return { name: 'vite-mute-warnings', enforce: 'pre', @@ -21,6 +22,7 @@ const muteWarningsPlugin = (warningsToIgnore) => { if (muted) { mutedMessages.add(muted.join()) + return } } @@ -36,6 +38,7 @@ const muteWarningsPlugin = (warningsToIgnore) => { }), closeBundle() { const diff = warningsToIgnore.filter((x) => !mutedMessages.has(x.join())) + if (diff.length > 0) { this.warn( 'Some of your muted warnings never appeared during the build process:', diff --git a/client/plugins/public.js b/client/plugins/public.js new file mode 100644 index 000000000..d64baa2dc --- /dev/null +++ b/client/plugins/public.js @@ -0,0 +1,72 @@ +// @ts-check +const path = require('path') +const fs = require('fs') + +const { log, TAGS } = require('@rm/logger') + +const configDir = path.join(__dirname, '../../config/user/public') + +/** + * @returns {import('vite').Plugin} + */ +const publicPlugin = () => { + const markerPath = path.join( + __dirname, + '../../node_modules/leaflet/dist/images/marker-icon.png', + ) + let outDir = '' + + return { + name: 'vite-plugin-public', + configResolved(config) { + outDir = config.build.outDir + }, + generateBundle() { + this.emitFile({ + type: 'asset', + fileName: 'images/fallback-marker.png', + source: fs.readFileSync(markerPath), + }) + }, + async closeBundle() { + if (fs.existsSync(configDir)) { + log.info(TAGS.build, 'copying public folder from config') + await fs.promises.cp(configDir, outDir, { + recursive: true, + }) + } + }, + configureServer(server) { + const customPath = process.env.NODE_CONFIG_ENV + ? path.join(configDir, `favicon-${process.env.NODE_CONFIG_ENV}.ico`) + : path.join(configDir, `favicon.ico`) + const favicon = fs.existsSync(customPath) + ? customPath + : path.join(__dirname, '../public/favicon.ico') + + server.middlewares.use((req, res, next) => { + if (req.url === '/favicon.ico') { + res.writeHead(200, { 'Content-Type': 'image/x-icon' }) + res.end(fs.readFileSync(favicon)) + + return + } + if (req.url === '/images/fallback-marker.png') { + res.writeHead(200, { 'Content-Type': 'image/png' }) + res.end(fs.readFileSync(markerPath)) + + return + } + if (req.url.startsWith('/images/u')) { + res.writeHead(200, { 'Content-Type': 'image/png' }) + res.end(fs.readFileSync(path.join(configDir, req.url))) + + return + } + next() + }) + }, + } +} + +module.exports = { publicPlugin } diff --git a/public/favicon/fallback.ico b/client/public/favicon.ico similarity index 100% rename from public/favicon/fallback.ico rename to client/public/favicon.ico diff --git a/public/images/custom/.gitkeep b/client/public/images/custom/.gitkeep similarity index 100% rename from public/images/custom/.gitkeep rename to client/public/images/custom/.gitkeep diff --git a/public/images/perms/backups.png b/client/public/images/perms/backups.png similarity index 100% rename from public/images/perms/backups.png rename to client/public/images/perms/backups.png diff --git a/public/images/perms/devices.png b/client/public/images/perms/devices.png similarity index 100% rename from public/images/perms/devices.png rename to client/public/images/perms/devices.png diff --git a/public/images/perms/dynamax.png b/client/public/images/perms/dynamax.png similarity index 100% rename from public/images/perms/dynamax.png rename to client/public/images/perms/dynamax.png diff --git a/public/images/perms/eventStops.png b/client/public/images/perms/eventStops.png similarity index 100% rename from public/images/perms/eventStops.png rename to client/public/images/perms/eventStops.png diff --git a/public/images/perms/gymBadges.png b/client/public/images/perms/gymBadges.png similarity index 100% rename from public/images/perms/gymBadges.png rename to client/public/images/perms/gymBadges.png diff --git a/public/images/perms/gyms.png b/client/public/images/perms/gyms.png similarity index 100% rename from public/images/perms/gyms.png rename to client/public/images/perms/gyms.png diff --git a/public/images/perms/invasions.png b/client/public/images/perms/invasions.png similarity index 100% rename from public/images/perms/invasions.png rename to client/public/images/perms/invasions.png diff --git a/public/images/perms/iv.png b/client/public/images/perms/iv.png similarity index 100% rename from public/images/perms/iv.png rename to client/public/images/perms/iv.png diff --git a/public/images/perms/lures.png b/client/public/images/perms/lures.png similarity index 100% rename from public/images/perms/lures.png rename to client/public/images/perms/lures.png diff --git a/public/images/perms/map.png b/client/public/images/perms/map.png similarity index 100% rename from public/images/perms/map.png rename to client/public/images/perms/map.png diff --git a/public/images/perms/nestSubmissions.png b/client/public/images/perms/nestSubmissions.png similarity index 100% rename from public/images/perms/nestSubmissions.png rename to client/public/images/perms/nestSubmissions.png diff --git a/public/images/perms/nests.png b/client/public/images/perms/nests.png similarity index 100% rename from public/images/perms/nests.png rename to client/public/images/perms/nests.png diff --git a/public/images/perms/pokemon.png b/client/public/images/perms/pokemon.png similarity index 100% rename from public/images/perms/pokemon.png rename to client/public/images/perms/pokemon.png diff --git a/public/images/perms/pokestops.png b/client/public/images/perms/pokestops.png similarity index 100% rename from public/images/perms/pokestops.png rename to client/public/images/perms/pokestops.png diff --git a/public/images/perms/portals.png b/client/public/images/perms/portals.png similarity index 100% rename from public/images/perms/portals.png rename to client/public/images/perms/portals.png diff --git a/public/images/perms/pvp.png b/client/public/images/perms/pvp.png similarity index 100% rename from public/images/perms/pvp.png rename to client/public/images/perms/pvp.png diff --git a/public/images/perms/quests.png b/client/public/images/perms/quests.png similarity index 100% rename from public/images/perms/quests.png rename to client/public/images/perms/quests.png diff --git a/public/images/perms/raids.png b/client/public/images/perms/raids.png similarity index 100% rename from public/images/perms/raids.png rename to client/public/images/perms/raids.png diff --git a/public/images/perms/routes.png b/client/public/images/perms/routes.png similarity index 100% rename from public/images/perms/routes.png rename to client/public/images/perms/routes.png diff --git a/public/images/perms/s2cells.png b/client/public/images/perms/s2cells.png similarity index 100% rename from public/images/perms/s2cells.png rename to client/public/images/perms/s2cells.png diff --git a/public/images/perms/scanAreas.png b/client/public/images/perms/scanAreas.png similarity index 100% rename from public/images/perms/scanAreas.png rename to client/public/images/perms/scanAreas.png diff --git a/public/images/perms/scanCells.png b/client/public/images/perms/scanCells.png similarity index 100% rename from public/images/perms/scanCells.png rename to client/public/images/perms/scanCells.png diff --git a/public/images/perms/showcaseRankings.png b/client/public/images/perms/showcaseRankings.png similarity index 100% rename from public/images/perms/showcaseRankings.png rename to client/public/images/perms/showcaseRankings.png diff --git a/public/images/perms/spawnpoints.png b/client/public/images/perms/spawnpoints.png similarity index 100% rename from public/images/perms/spawnpoints.png rename to client/public/images/perms/spawnpoints.png diff --git a/public/images/perms/stations.png b/client/public/images/perms/stations.png similarity index 100% rename from public/images/perms/stations.png rename to client/public/images/perms/stations.png diff --git a/public/images/perms/stats.png b/client/public/images/perms/stats.png similarity index 100% rename from public/images/perms/stats.png rename to client/public/images/perms/stats.png diff --git a/public/images/perms/submissionCells.png b/client/public/images/perms/submissionCells.png similarity index 100% rename from public/images/perms/submissionCells.png rename to client/public/images/perms/submissionCells.png diff --git a/public/images/perms/weather.png b/client/public/images/perms/weather.png similarity index 100% rename from public/images/perms/weather.png rename to client/public/images/perms/weather.png diff --git a/public/images/uicons/.gitkeep b/client/public/images/uicons/.gitkeep similarity index 100% rename from public/images/uicons/.gitkeep rename to client/public/images/uicons/.gitkeep diff --git a/public/loading.js b/client/public/loading.js similarity index 99% rename from public/loading.js rename to client/public/loading.js index 1bf089e72..8fe9a74f9 100644 --- a/public/loading.js +++ b/client/public/loading.js @@ -2,8 +2,10 @@ try { const localState = window?.localStorage?.getItem('local-state') + if (localState) { const { state } = JSON.parse(localState) + if (state.darkMode) { document.body.classList.add('dark') } @@ -28,6 +30,7 @@ try { } const locale = window?.localStorage?.getItem('i18nextLng') || 'en' const element = document.getElementById('loading-text') + if (element) { element.innerText = locales[locale.toLowerCase()] || locales.en } diff --git a/public/robots.txt b/client/public/robots.txt similarity index 100% rename from public/robots.txt rename to client/public/robots.txt diff --git a/src/App.jsx b/client/src/App.tsx similarity index 99% rename from src/App.jsx rename to client/src/App.tsx index 799951547..069d6f0d4 100644 --- a/src/App.jsx +++ b/client/src/App.tsx @@ -1,4 +1,3 @@ -// @ts-check import '@assets/css/main.css' import 'leaflet.locatecontrol/dist/L.Control.Locate.css' import 'leaflet/dist/leaflet.css' @@ -8,7 +7,6 @@ import { BrowserRouter } from 'react-router-dom' import CssBaseline from '@mui/material/CssBaseline' import ThemeProvider from '@mui/material/styles/ThemeProvider' import { ApolloProvider } from '@apollo/client' - import { useCustomTheme } from '@assets/theme' import { globalStyles } from '@components/Global' import { apolloClient } from '@services/apollo' @@ -40,7 +38,9 @@ const LOADING_LOCALES = { function SetText() { const locale = localStorage?.getItem('i18nextLng') || 'en' + setLoadingText(LOADING_LOCALES[locale.toLowerCase()] || LOADING_LOCALES.en) + return
} diff --git a/client/src/assets/constants.ts b/client/src/assets/constants.ts new file mode 100644 index 000000000..195fbe2f0 --- /dev/null +++ b/client/src/assets/constants.ts @@ -0,0 +1,65 @@ +// @ts-check + +export const ICON_SIZES = ['sm', 'md', 'lg', 'xl'] as const + +export const XXS_XXL = ['xxs', 'xxl'] as const + +export const NUNDO_HUNDO = ['zeroIv', 'hundoIv'] as const + +export const ENUM_GENDER = [0, 1, 2, 3] as const + +export const ENUM_BADGES = [0, 1, 2, 3, 4] as const + +export const S2_LEVELS = [ + ...(process.env.NODE_ENV === 'development' + ? ([1, 2, 3, 4, 5, 6, 7, 8, 9] as const) + : ([] as const)), + 10, + 11, + 12, + 13, + 14, + 15, + 16, + 17, + 18, + 19, + 20, +] as const + +export const FORT_LEVELS = ['all', '1', '2', '3'] as const + +export const BADGES = [ + 'all', + 'badge_1', + 'badge_2', + 'badge_3', + 'badge_4', +] as const + +export const QUEST_SETS = ['with_ar', 'both', 'without_ar'] as const + +export const WAYFARER_OPTIONS = [ + 'rings', + 'includeSponsored', + 's14Cells', + 's17Cells', +] as const + +export const ENUM_TTH = [0, 1, 2] as const + +export const MIN_MAX = ['min', 'max'] as const + +export const ENABLED_ALL = ['enabled', 'all'] as const + +export const RADIUS_CHOICES = ['pokemon', 'gym'] as const + +export const METHODS = ['discord', 'telegram'] as const + +export const FILTER_SKIP_LIST = ['filter', 'enabled', 'legacy'] + +export const ALWAYS_EXCLUDED = new Set(['donor', 'blockedGuildNames', 'admin']) + +export const SCAN_MODES = ['confirmed', 'loading', 'error'] as const + +export const SCAN_SIZES = ['S', 'M', 'XL'] as const diff --git a/src/assets/css/holiday.css b/client/src/assets/css/holiday.css similarity index 100% rename from src/assets/css/holiday.css rename to client/src/assets/css/holiday.css diff --git a/src/assets/css/loading.css b/client/src/assets/css/loading.css similarity index 100% rename from src/assets/css/loading.css rename to client/src/assets/css/loading.css diff --git a/src/assets/css/main.css b/client/src/assets/css/main.css similarity index 100% rename from src/assets/css/main.css rename to client/src/assets/css/main.css diff --git a/src/assets/fallbackMarker.js b/client/src/assets/fallbackMarker.ts similarity index 100% rename from src/assets/fallbackMarker.js rename to client/src/assets/fallbackMarker.ts diff --git a/client/src/assets/theme.ts b/client/src/assets/theme.ts new file mode 100644 index 000000000..900a60e76 --- /dev/null +++ b/client/src/assets/theme.ts @@ -0,0 +1,189 @@ +import { useMemo } from 'react' +import { createTheme, responsiveFontSizes, darken } from '@mui/material/styles' +import dlv from 'dlv' +import { useMemory } from '@store/useMemory' +import { useStorage } from '@store/useStorage' + +const VALID_COLOR = + /^#([A-Fa-f0-9]{3,4}){1,2}$|^rgb\((\s*\d{1,3}\s*,){2}\s*\d{1,3}\s*\)$|^rgba\((\s*\d{1,3}\s*,){3}\s*(0?\.\d+|1\.0|1|\d{1,2}%)\s*\)$|^hsl\(\s*\d{1,3}(\s*,\s*\d{1,3}%){2}\s*\)$|^hsla\(\s*\d{1,3}(\s*,\s*\d{1,3}%){2}\s*,\s*(0?\.\d+|1\.0|1|\d{1,2}%)\s*\)$/ + +export function useCustomTheme() { + const { primary, secondary } = useMemory((s) => s.theme) + const darkMode = useStorage((s) => s.darkMode) + + if (darkMode) { + if (!document.body.classList.contains('dark')) { + document.body.classList.add('dark') + } + } else if (document.body.classList.contains('dark')) { + document.body.classList.remove('dark') + } + + return useMemo( + () => + responsiveFontSizes( + createTheme({ + palette: { + mode: darkMode ? 'dark' : 'light', + primary: { + main: primary, + }, + secondary: { + main: secondary, + contrastText: '#fff', + }, + info: { + main: '#2AB5F6', + contrastText: '#fff', + }, + // TODO: Augment Mui Types + discord: { + main: '#5865F2', + green: '#57F287', + yellow: '#FEE75C', + fuchsia: '#EB459E', + red: '#ED4245', + }, + }, + components: { + MuiPaper: { + styleOverrides: { + root: { + backgroundImage: 'none', + }, + }, + }, + MuiRating: { + styleOverrides: { + iconFilled: ({ theme }) => ({ + color: theme.palette.secondary.main, + }), + }, + }, + MuiListSubheader: { + defaultProps: { + disableSticky: true, + }, + }, + MuiTabs: { + defaultProps: { + textColor: 'inherit', + indicatorColor: 'secondary', + variant: 'fullWidth', + }, + styleOverrides: { + root: ({ theme: t }) => ({ + backgroundColor: + t.palette.grey[t.palette.mode === 'dark' ? 800 : 500], + width: '100%', + }), + }, + }, + MuiListItemText: { + styleOverrides: { + inset: { + paddingLeft: 32, + }, + }, + }, + MuiAccordion: { + defaultProps: { + disableGutters: true, + }, + styleOverrides: { + root: { + '&.Mui-expanded:before': { + opacity: 1, + }, + }, + }, + }, + MuiCardActions: { + styleOverrides: { + root: { + padding: '0px 8px', + }, + }, + }, + MuiSelect: { + defaultProps: { + size: 'small', + }, + }, + MuiAutocomplete: { + styleOverrides: { + inputRoot: { + paddingRight: `0px !important`, + }, + paper: { + borderTopLeftRadius: 0, + borderTopRightRadius: 0, + }, + }, + }, + MuiButtonBase: { + defaultProps: { + disableRipple: true, + }, + }, + MuiButton: { + defaultProps: { + disableRipple: true, + }, + styleOverrides: { + root: ({ theme, ownerState }) => { + const color = ownerState?.bgcolor + + if (typeof color === 'string') { + const backgroundColor = color + ? (typeof color === 'string' && color.includes('.')) || + color in theme.palette + ? dlv(theme.palette, color) + : color + : theme.palette.success.dark + const finalColor = + typeof backgroundColor === 'string' + ? backgroundColor + : backgroundColor?.main + + if (!VALID_COLOR.test(finalColor) || !finalColor) { + return + } + + return { + color: theme.palette.getContrastText(finalColor), + backgroundColor, + '&:hover': { + backgroundColor: darken(finalColor, 0.2), + }, + } + } + }, + }, + }, + MuiDialogTitle: { + styleOverrides: { + root: { + padding: '12px 24px', + }, + }, + }, + MuiDialogContent: { + styleOverrides: { + root: { + height: '100%', + }, + }, + }, + MuiSlider: { + defaultProps: { + size: 'small', + valueLabelDisplay: 'auto', + }, + }, + }, + }), + ), + [darkMode, primary, secondary], + ) +} diff --git a/src/components/BasicAccordion.jsx b/client/src/components/BasicAccordion.tsx similarity index 72% rename from src/components/BasicAccordion.jsx rename to client/src/components/BasicAccordion.tsx index ea932df7e..b38326f30 100644 --- a/src/components/BasicAccordion.jsx +++ b/client/src/components/BasicAccordion.tsx @@ -1,40 +1,38 @@ -// @ts-check import * as React from 'react' import ExpandMoreIcon from '@mui/icons-material/ExpandMore' import Typography from '@mui/material/Typography' import Accordion from '@mui/material/Accordion' import AccordionSummary from '@mui/material/AccordionSummary' import AccordionDetails from '@mui/material/AccordionDetails' - import { useStorage } from '@store/useStorage' const expandIcon = /** * A basic accordion component that already has the expand icon and state management - * @param {{ - * stateKey?: string - * title: string - * children: React.ReactNode - * } & import('@mui/material').AccordionDetailsProps} props - * @returns */ export function BasicAccordion({ title, stateKey = title, children, ...props -}) { +}: { + stateKey?: string + title: string + children: React.ReactNode +} & import('@mui/material').AccordionDetailsProps) { const expanded = useStorage((s) => !!s.expanded[stateKey]) /** @type {(e: unknown, isExpanded: boolean )=> void} */ - const handleChange = React.useCallback( - (_, isExpanded) => - useStorage.setState((prev) => ({ - expanded: { ...prev.expanded, [stateKey]: isExpanded }, - })), - [stateKey], - ) + const handleChange: (e: unknown, isExpanded: boolean) => void = + React.useCallback( + (_, isExpanded) => + useStorage.setState((prev) => ({ + expanded: { ...prev.expanded, [stateKey]: isExpanded }, + })), + [stateKey], + ) + return ( diff --git a/src/components/Config.jsx b/client/src/components/Config.tsx similarity index 89% rename from src/components/Config.jsx rename to client/src/components/Config.tsx index 9b7b5c804..9ad6feb2a 100644 --- a/src/components/Config.jsx +++ b/client/src/components/Config.tsx @@ -4,7 +4,6 @@ import * as React from 'react' import { useTranslation } from 'react-i18next' import { setUser } from '@sentry/react' import { Navigate } from 'react-router-dom' - import { useMemory } from '@store/useMemory' import { useStorage } from '@store/useStorage' import { getSettings } from '@services/fetches' @@ -45,10 +44,8 @@ export function Config({ children }) { }) } - /** @type {{ state: import('@store/useStorage').UseStorage}} */ - const localState = JSON.parse( - localStorage.getItem('local-state') || '{ "state": {} }', - ) + const localState: { state: import('@store/useStorage').UseStorage } = + JSON.parse(localStorage.getItem('local-state') || '{ "state": {} }') /** * @template T @@ -56,24 +53,22 @@ export function Config({ children }) { * @param {string} category * @returns {T} */ - const updatePositionState = (defaults, category) => { + function updatePositionState(defaults: T, category: string): T { if (localState?.state?.[category]) { return localState.state[category] } + return defaults } - const defaultLocation = /** @type {const} */ ([ - data.map.general.startLat, - data.map.general.startLon, - ]) - const location = /** @type {[number, number]} */ ( - updatePositionState(defaultLocation, 'location').map( - (x, i) => - x || - (i === 0 ? data.map.general.startLat : data.map.general.startLon), - ) - ) + const location = updatePositionState( + [data.map.general.startLat, data.map.general.startLon], + 'location', + ).map( + (x, i) => + x || + (i === 0 ? data.map.general.startLat : data.map.general.startLon), + ) as [number, number] const zoom = updatePositionState(data.map.general.startZoom, 'zoom') const safeZoom = diff --git a/src/components/ErrorBoundary.jsx b/client/src/components/ErrorBoundary.tsx similarity index 83% rename from src/components/ErrorBoundary.jsx rename to client/src/components/ErrorBoundary.tsx index 4a7c2b262..d0a0f7ffd 100644 --- a/src/components/ErrorBoundary.jsx +++ b/client/src/components/ErrorBoundary.tsx @@ -9,26 +9,37 @@ import Refresh from '@mui/icons-material/Refresh' import CopyIcon from '@mui/icons-material/FileCopy' import IconButton from '@mui/material/IconButton' import { withTranslation } from 'react-i18next' - import { sendError } from '@services/fetches' import { Notification } from './Notification' -/** @type {React.CSSProperties} */ -const defaultStyle = { +const defaultStyle: React.CSSProperties = { height: '100vh', width: '100vw', textAlign: 'center', } -// This component uses React Classes due to componentDidCatch() not being available in React Hooks -// Do not use this as a base for other components - -class ErrorCatcher extends React.Component { +/** + * This component uses React Classes due to componentDidCatch() not being available in React Hooks + * + * Do not use this as a base for other components + */ +class ErrorCatcher extends React.Component< + { + children?: React.ReactNode + style?: React.CSSProperties + noRefresh?: boolean + resettable?: boolean + variant?: 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6' + t: (key: string) => string + }, + { message: string; errorCount: number; reported: boolean; uuid: string } +> { static uuidv4() { return 'xxxxxxxx-r2m4-xxxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => { const r = (Math.random() * 16) | 0 const v = c == 'x' ? r : (r & 0x3) | 0x8 + return v.toString(16) }) } @@ -68,19 +79,19 @@ class ErrorCatcher extends React.Component { (process.env.NODE_ENV === 'development' ? 1 : 3) ? ( - + {this.props.t('react_error')} - + {this.state.message} {this.state.reported && ( - +
{this.props.t('reported_error')}:
@@ -98,10 +109,10 @@ class ErrorCatcher extends React.Component {

@@ -112,9 +123,9 @@ class ErrorCatcher extends React.Component {

@@ -125,8 +136,8 @@ class ErrorCatcher extends React.Component { ) : ( <> 0} cb={() => this.setState({ errorCount: 0 })} + open={this.state.errorCount > 0} severity="error" title="react_error" > diff --git a/src/components/Global.jsx b/client/src/components/Global.tsx similarity index 99% rename from src/components/Global.jsx rename to client/src/components/Global.tsx index 22abddd5a..b182f697e 100644 --- a/src/components/Global.jsx +++ b/client/src/components/Global.tsx @@ -1,17 +1,16 @@ -// @ts-check -import * as React from 'react' import GlobalStyles from '@mui/material/GlobalStyles' import { darken, lighten } from '@mui/material/styles' - import { useMemory } from '@store/useMemory' function ApplyGlobal() { const online = useMemory((s) => s.online) + return ( { const darkMode = theme.palette.mode === 'dark' const grey = darkMode ? 900 : 50 + return { // Global body: { diff --git a/src/components/I.jsx b/client/src/components/I.tsx similarity index 63% rename from src/components/I.jsx rename to client/src/components/I.tsx index 7987605ff..14ffac5bf 100644 --- a/src/components/I.jsx +++ b/client/src/components/I.tsx @@ -1,13 +1,10 @@ -// @ts-check +import type { ButtonProps } from '@mui/material' import { styled } from '@mui/material/styles' -// TODO: Figure out how to type this - -/** @type {import('react').FC} */ export const I = styled('i', { shouldForwardProp: (prop) => prop !== 'color' && prop !== 'size', -})(({ theme, style, size, color }) => ({ +})<{ size?: ButtonProps['size'] }>(({ theme, style, size, color }) => ({ paddingLeft: 1.5, fontSize: size === 'small' ? 18 : 24, color: color || theme.palette.text.primary, diff --git a/src/components/Img.jsx b/client/src/components/Img.tsx similarity index 51% rename from src/components/Img.jsx rename to client/src/components/Img.tsx index 3f4429882..337bb0e8c 100644 --- a/src/components/Img.jsx +++ b/client/src/components/Img.tsx @@ -4,15 +4,18 @@ import { styled } from '@mui/material/styles' import Typography from '@mui/material/Typography' import { useMemory } from '@store/useMemory' import { useTranslateById } from '@hooks/useTranslateById' + import { NameTT } from './popups/NameTT' -/** - * @typedef {React.ImgHTMLAttributes} ImgProps - * @typedef {Pick & { sx?: import('@mui/material').SxProps }} ExtraProps - * @typedef {ImgProps & Partial} Props - */ +export interface ImgProps extends React.ImgHTMLAttributes {} + +type ExtraProps = Pick< + React.CSSProperties, + 'maxWidth' | 'minWidth' | 'maxHeight' | 'minHeight' | 'zIndex' +> & { sx?: import('@mui/material').SxProps } + +type Props = ImgProps & Partial -/** @type {React.FC} */ export const Img = styled('img', { shouldForwardProp: (prop) => prop !== 'maxWidth' && @@ -20,59 +23,46 @@ export const Img = styled('img', { prop !== 'zIndex' && prop !== 'minHeight' && prop !== 'minWidth', -})( - ( - /** @type {Props} */ { maxWidth, maxHeight, minHeight, minWidth, zIndex }, - ) => ({ - maxWidth, - maxHeight, - minHeight, - minWidth, - zIndex, - }), -) +})(({ maxWidth, maxHeight, minHeight, minWidth, zIndex }) => ({ + maxWidth, + maxHeight, + minHeight, + minWidth, + zIndex, +})) /** * A small wrapper around the Img component to display an icon next to text * * The image defaults to 15x15px - * @param {import('@mui/material').TypographyProps & { - * src: string, - * alt?: string, - * imgMaxWidth?: number, - * imgMaxHeight?: number - * }} props - * @returns */ -export const TextWithIcon = ({ +export function TextWithIcon({ children, src, alt = typeof children === 'string' ? children : src, imgMaxHeight = 15, imgMaxWidth = 15, ...props -}) => ( - - {children} -   - {alt} - -) +}: import('@mui/material').TypographyProps & { + src: string + alt?: string + imgMaxWidth?: number + imgMaxHeight?: number +}) { + return ( + + {children} +   + {alt} + + ) +} -/** - * - * @param {{ - * id: number, - * form?: number, - * evolution?: number, - * gender?: number, - * costume?: number, - * alignment?: number, - * shiny?: boolean, - * bread?: number, - * } & Omit} props - * @returns - */ export const PokemonImg = ({ id, form = 0, @@ -83,7 +73,16 @@ export const PokemonImg = ({ shiny = false, bread = 0, ...props -}) => { +}: { + id: number + form?: number + evolution?: number + gender?: number + costume?: number + alignment?: number + shiny?: boolean + bread?: number +} & Omit) => { const src = useMemory((s) => s.Icons.getPokemon( id, diff --git a/client/src/components/Loading.tsx b/client/src/components/Loading.tsx new file mode 100644 index 000000000..3a05205f2 --- /dev/null +++ b/client/src/components/Loading.tsx @@ -0,0 +1,21 @@ +import * as React from 'react' +import Box, { BoxProps } from '@mui/material/Box' +import CircularProgress from '@mui/material/CircularProgress' +import Typography from '@mui/material/Typography' + +export function Loading({ children, ...props }: BoxProps) { + return ( + + + + {children} + + + ) +} diff --git a/client/src/components/Menu.tsx b/client/src/components/Menu.tsx new file mode 100644 index 000000000..de30dd9c7 --- /dev/null +++ b/client/src/components/Menu.tsx @@ -0,0 +1,172 @@ +import type { AdvCategories, Available } from '@rm/types' + +import * as React from 'react' +import Box from '@mui/material/Box' +import DialogContent from '@mui/material/DialogContent' +import Typography from '@mui/material/Typography' +import { useTranslation } from 'react-i18next' +import ExpandMoreIcon from '@mui/icons-material/ExpandMore' +import Collapse from '@mui/material/Collapse' +import IconButton from '@mui/material/IconButton' +import { useMemory } from '@store/useMemory' +import { useLayoutStore } from '@store/useLayoutStore' +import { useFilter } from '@hooks/useFilter' +import { Footer, FooterButton } from '@components/dialogs/Footer' +import { applyToAll } from '@utils/applyToAll' +import { useGetAvailable } from '@hooks/useGetAvailable' +import { applyToAllWebhooks, useWebhookStore } from '@store/useWebhookStore' +import { useAnalytics } from '@hooks/useAnalytics' + +import { OptionsContainer } from './filters/OptionsContainer' +import { VirtualGrid } from './virtual/VirtualGrid' +import { GenericSearch } from './inputs/GenericSearch' + +interface Props { + category: T + webhookCategory?: string + children: (index: number, key: string) => React.ReactNode + categories?: (keyof Available)[] + extraButtons?: FooterButton[] +} + +export function Menu({ + category, + webhookCategory, + children, + categories, + extraButtons, +}: Props) { + useGetAvailable(category) + useAnalytics(`/advanced/${category}`) + const isMobile = useMemory((s) => s.isMobile) + const { t } = useTranslation() + + const [filterDrawer, setFilterDrawer] = React.useState(false) + + const footerButtons: FooterButton[] = React.useMemo( + () => [ + { + name: 'help', + action: () => + useLayoutStore.setState({ help: { open: true, category } }), + icon: 'Help', + }, + { + name: '', + disabled: true, + }, + { + name: 'apply_to_all', + action: () => + (webhookCategory ? useWebhookStore : useLayoutStore).setState({ + [webhookCategory ? 'advanced' : 'advancedFilter']: { + open: true, + id: 'global', + category: webhookCategory || category, + selectedIds: useMemory.getState().advMenuFiltered[category], + }, + }), + icon: category === 'pokemon' || webhookCategory ? 'Tune' : 'FormatSize', + }, + { + name: 'disable_all', + action: () => + webhookCategory + ? applyToAllWebhooks( + false, + useMemory.getState().advMenuFiltered[category], + ) + : applyToAll( + { enabled: false }, + category, + useMemory.getState().advMenuFiltered[category], + ), + icon: 'Clear', + color: 'error', + }, + { + name: 'enable_all', + action: () => + webhookCategory + ? applyToAllWebhooks( + true, + useMemory.getState().advMenuFiltered[category], + ) + : applyToAll( + { enabled: true }, + category, + useMemory.getState().advMenuFiltered[category], + !webhookCategory, + ), + icon: 'Check', + color: 'success', + }, + ...(extraButtons ?? []), + ], + [category, webhookCategory, extraButtons], + ) + + return ( + <> + + {!isMobile && ( + + + + )} + + + + {isMobile && ( + setFilterDrawer((prev) => !prev)}> + + + )} + + + {isMobile && ( + + + + )} + + {children} + + + + +