mirror of https://github.com/cocosolos/ixion.git
Initial commit
This commit is contained in:
commit
ba172f599c
|
|
@ -0,0 +1,17 @@
|
||||||
|
FROM nikolaik/python-nodejs:latest
|
||||||
|
|
||||||
|
ENV PYTHONDONTWRITEBYTECODE 1
|
||||||
|
ENV PYTHONUNBUFFERED 1
|
||||||
|
|
||||||
|
RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \
|
||||||
|
&& apt-get -y install --no-install-recommends git \
|
||||||
|
&& pip install --upgrade pip && npm update -g npm
|
||||||
|
|
||||||
|
WORKDIR /workspace
|
||||||
|
|
||||||
|
COPY . .
|
||||||
|
RUN pip install --no-cache-dir -r /workspace/api/requirements.txt
|
||||||
|
WORKDIR /workspace/client
|
||||||
|
RUN npm install
|
||||||
|
|
||||||
|
CMD ["tail", "-f", "/dev/null"]
|
||||||
|
|
@ -0,0 +1,75 @@
|
||||||
|
{
|
||||||
|
"name": "Ixion",
|
||||||
|
"dockerComposeFile": "docker-compose.yml",
|
||||||
|
"service": "workspace",
|
||||||
|
"containerEnv": {
|
||||||
|
"GIT_EDITOR": "code --wait"
|
||||||
|
},
|
||||||
|
"workspaceFolder": "/workspace",
|
||||||
|
"postCreateCommand": "git config --global --add safe.directory /workspace",
|
||||||
|
"shutdownAction": "stopCompose",
|
||||||
|
"forwardPorts": [8000, 3000],
|
||||||
|
"portsAttributes": {
|
||||||
|
"8000": {
|
||||||
|
"label": "api"
|
||||||
|
},
|
||||||
|
"3000": {
|
||||||
|
"label": "client"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"features": {
|
||||||
|
"ghcr.io/devcontainers/features/docker-outside-of-docker:latest": {}
|
||||||
|
},
|
||||||
|
"mounts": [
|
||||||
|
"source=vscode-extensions,target=/root/.vscode-server/extensions,type=volume",
|
||||||
|
],
|
||||||
|
"customizations": {
|
||||||
|
"vscode": {
|
||||||
|
"extensions": [
|
||||||
|
"EditorConfig.EditorConfig",
|
||||||
|
"dbaeumer.vscode-eslint",
|
||||||
|
"esbenp.prettier-vscode",
|
||||||
|
"formulahendry.auto-rename-tag",
|
||||||
|
"idered.npm",
|
||||||
|
"christian-kohler.npm-intellisense",
|
||||||
|
"xabikos.JavaScriptSnippets",
|
||||||
|
"dsznajder.es7-react-js-snippets",
|
||||||
|
"eamodio.gitlens",
|
||||||
|
"donjayamanne.githistory",
|
||||||
|
"humao.rest-client",
|
||||||
|
"ms-azuretools.vscode-docker",
|
||||||
|
"bradlc.vscode-tailwindcss",
|
||||||
|
"christian-kohler.path-intellisense",
|
||||||
|
"csstools.postcss",
|
||||||
|
// api
|
||||||
|
"ms-python.black-formatter",
|
||||||
|
"ms-python.python",
|
||||||
|
"ms-python.vscode-pylance",
|
||||||
|
"batisteo.vscode-django"
|
||||||
|
],
|
||||||
|
"settings": {
|
||||||
|
"[python]": {
|
||||||
|
"editor.defaultFormatter": "ms-python.black-formatter"
|
||||||
|
},
|
||||||
|
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
||||||
|
"editor.formatOnSave": true,
|
||||||
|
"eslint.format.enable": true,
|
||||||
|
"eslint.lintTask.enable": true,
|
||||||
|
"eslint.workingDirectories": ["./client"],
|
||||||
|
"eslint.validate": [
|
||||||
|
"javascript",
|
||||||
|
"javascriptreact",
|
||||||
|
"typescript",
|
||||||
|
"typescriptreact"
|
||||||
|
],
|
||||||
|
"editor.codeActionsOnSave": {
|
||||||
|
"source.organizeImports": "explicit",
|
||||||
|
"source.fixAll.eslint": "explicit"
|
||||||
|
},
|
||||||
|
"tailwindCSS.includeLanguages": {
|
||||||
|
"plaintext": "javascript"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,63 @@
|
||||||
|
services:
|
||||||
|
workspace:
|
||||||
|
build:
|
||||||
|
context: ..
|
||||||
|
dockerfile: .devcontainer/Dockerfile
|
||||||
|
image: ixion-dev:latest
|
||||||
|
command: sleep infinity
|
||||||
|
volumes:
|
||||||
|
- /var/run/docker.sock:/var/run/docker-host.sock
|
||||||
|
- ixion-dev-node_modules:/workspace/client/node_modules
|
||||||
|
- ixion-dev-static:/workspace/api/static
|
||||||
|
- ..:/workspace:cached
|
||||||
|
client:
|
||||||
|
image: ixion-dev:latest
|
||||||
|
working_dir: /workspace/client
|
||||||
|
command: npm run dev
|
||||||
|
volumes:
|
||||||
|
- ixion-dev-node_modules:/workspace/client/node_modules
|
||||||
|
- ../client:/workspace/client:cached
|
||||||
|
ports:
|
||||||
|
- 3000:3000
|
||||||
|
depends_on:
|
||||||
|
- workspace
|
||||||
|
- api
|
||||||
|
api:
|
||||||
|
image: ixion-dev:latest
|
||||||
|
working_dir: /workspace/api
|
||||||
|
entrypoint: /workspace/api/entrypoint.sh
|
||||||
|
command: python manage.py runserver 0.0.0.0:8000
|
||||||
|
environment:
|
||||||
|
- DEBUG=true
|
||||||
|
volumes:
|
||||||
|
- ixion-dev-static:/workspace/api/static
|
||||||
|
- ../api:/workspace/api:cached
|
||||||
|
ports:
|
||||||
|
- 8000:8000
|
||||||
|
depends_on:
|
||||||
|
- workspace
|
||||||
|
- redis
|
||||||
|
celery:
|
||||||
|
image: ixion-dev:latest
|
||||||
|
working_dir: /workspace/api
|
||||||
|
command: celery -A api worker -l info
|
||||||
|
volumes:
|
||||||
|
- ../api:/workspace/api:cached
|
||||||
|
depends_on:
|
||||||
|
- workspace
|
||||||
|
- redis
|
||||||
|
celery-beat:
|
||||||
|
image: ixion-dev:latest
|
||||||
|
working_dir: /workspace/api
|
||||||
|
command: celery -A api beat -l info
|
||||||
|
volumes:
|
||||||
|
- ../api:/workspace/api:cached
|
||||||
|
depends_on:
|
||||||
|
- workspace
|
||||||
|
- redis
|
||||||
|
redis:
|
||||||
|
image: redis:alpine
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
ixion-dev-node_modules:
|
||||||
|
ixion-dev-static:
|
||||||
|
|
@ -0,0 +1,10 @@
|
||||||
|
# http://editorconfig.org
|
||||||
|
root = true
|
||||||
|
|
||||||
|
[*]
|
||||||
|
indent_style = space
|
||||||
|
indent_size = 2
|
||||||
|
charset = utf-8
|
||||||
|
trim_trailing_whitespace = true
|
||||||
|
end_of_line = lf
|
||||||
|
insert_final_newline = true
|
||||||
|
|
@ -0,0 +1,217 @@
|
||||||
|
## GITATTRIBUTES FOR WEB PROJECTS
|
||||||
|
#
|
||||||
|
# These settings are for any web project.
|
||||||
|
#
|
||||||
|
# Details per file setting:
|
||||||
|
# text These files should be normalized (i.e. convert CRLF to LF).
|
||||||
|
# binary These files are binary and should be left untouched.
|
||||||
|
#
|
||||||
|
# Note that binary is a macro for -text -diff.
|
||||||
|
######################################################################
|
||||||
|
|
||||||
|
# Auto detect
|
||||||
|
## Handle line endings automatically for files detected as
|
||||||
|
## text and leave all files detected as binary untouched.
|
||||||
|
## This will handle all files NOT defined below.
|
||||||
|
* text=auto
|
||||||
|
|
||||||
|
# Source code
|
||||||
|
*.bash text eol=lf
|
||||||
|
*.bat text eol=crlf
|
||||||
|
*.cmd text eol=crlf
|
||||||
|
*.coffee text
|
||||||
|
*.css text diff=css
|
||||||
|
*.htm text diff=html
|
||||||
|
*.html text diff=html
|
||||||
|
*.inc text
|
||||||
|
*.ini text
|
||||||
|
*.js text
|
||||||
|
*.mjs text
|
||||||
|
*.cjs text
|
||||||
|
*.json text
|
||||||
|
*.jsx text
|
||||||
|
*.less text
|
||||||
|
*.ls text
|
||||||
|
*.map text -diff
|
||||||
|
*.od text
|
||||||
|
*.onlydata text
|
||||||
|
*.php text diff=php
|
||||||
|
*.pl text
|
||||||
|
*.ps1 text eol=crlf
|
||||||
|
*.py text diff=python
|
||||||
|
*.rb text diff=ruby
|
||||||
|
*.sass text
|
||||||
|
*.scm text
|
||||||
|
*.scss text diff=css
|
||||||
|
*.sh text eol=lf
|
||||||
|
.husky/* text eol=lf
|
||||||
|
*.sql text
|
||||||
|
*.styl text
|
||||||
|
*.tag text
|
||||||
|
*.ts text
|
||||||
|
*.tsx text
|
||||||
|
*.xml text
|
||||||
|
*.xhtml text diff=html
|
||||||
|
|
||||||
|
# Docker
|
||||||
|
Dockerfile text
|
||||||
|
|
||||||
|
# Documentation
|
||||||
|
*.ipynb text eol=lf
|
||||||
|
*.markdown text diff=markdown
|
||||||
|
*.md text diff=markdown
|
||||||
|
*.mdwn text diff=markdown
|
||||||
|
*.mdown text diff=markdown
|
||||||
|
*.mkd text diff=markdown
|
||||||
|
*.mkdn text diff=markdown
|
||||||
|
*.mdtxt text
|
||||||
|
*.mdtext text
|
||||||
|
*.txt text
|
||||||
|
AUTHORS text
|
||||||
|
CHANGELOG text
|
||||||
|
CHANGES text
|
||||||
|
CONTRIBUTING text
|
||||||
|
COPYING text
|
||||||
|
copyright text
|
||||||
|
*COPYRIGHT* text
|
||||||
|
INSTALL text
|
||||||
|
license text
|
||||||
|
LICENSE text
|
||||||
|
NEWS text
|
||||||
|
readme text
|
||||||
|
*README* text
|
||||||
|
TODO text
|
||||||
|
|
||||||
|
# Templates
|
||||||
|
*.dot text
|
||||||
|
*.ejs text
|
||||||
|
*.erb text
|
||||||
|
*.haml text
|
||||||
|
*.handlebars text
|
||||||
|
*.hbs text
|
||||||
|
*.hbt text
|
||||||
|
*.jade text
|
||||||
|
*.latte text
|
||||||
|
*.mustache text
|
||||||
|
*.njk text
|
||||||
|
*.phtml text
|
||||||
|
*.svelte text
|
||||||
|
*.tmpl text
|
||||||
|
*.tpl text
|
||||||
|
*.twig text
|
||||||
|
*.vue text
|
||||||
|
|
||||||
|
# Configs
|
||||||
|
*.cnf text
|
||||||
|
*.conf text
|
||||||
|
*.config text
|
||||||
|
.editorconfig text
|
||||||
|
.env text
|
||||||
|
.gitattributes text
|
||||||
|
.gitconfig text
|
||||||
|
.htaccess text
|
||||||
|
*.lock text -diff
|
||||||
|
package.json text eol=lf
|
||||||
|
package-lock.json text eol=lf -diff
|
||||||
|
pnpm-lock.yaml text eol=lf -diff
|
||||||
|
.prettierrc text
|
||||||
|
yarn.lock text -diff
|
||||||
|
*.toml text
|
||||||
|
*.yaml text
|
||||||
|
*.yml text
|
||||||
|
browserslist text
|
||||||
|
Makefile text
|
||||||
|
makefile text
|
||||||
|
# Fixes syntax highlighting on GitHub to allow comments
|
||||||
|
tsconfig.json linguist-language=JSON-with-Comments
|
||||||
|
|
||||||
|
# Heroku
|
||||||
|
Procfile text
|
||||||
|
|
||||||
|
# Graphics
|
||||||
|
*.ai binary
|
||||||
|
*.bmp binary
|
||||||
|
*.eps binary
|
||||||
|
*.gif binary
|
||||||
|
*.gifv binary
|
||||||
|
*.ico binary
|
||||||
|
*.jng binary
|
||||||
|
*.jp2 binary
|
||||||
|
*.jpg binary
|
||||||
|
*.jpeg binary
|
||||||
|
*.jpx binary
|
||||||
|
*.jxr binary
|
||||||
|
*.pdf binary
|
||||||
|
*.png binary
|
||||||
|
*.psb binary
|
||||||
|
*.psd binary
|
||||||
|
# SVG treated as an asset (binary) by default.
|
||||||
|
*.svg text
|
||||||
|
# If you want to treat it as binary,
|
||||||
|
# use the following line instead.
|
||||||
|
# *.svg binary
|
||||||
|
*.svgz binary
|
||||||
|
*.tif binary
|
||||||
|
*.tiff binary
|
||||||
|
*.wbmp binary
|
||||||
|
*.webp binary
|
||||||
|
|
||||||
|
# Audio
|
||||||
|
*.kar binary
|
||||||
|
*.m4a binary
|
||||||
|
*.mid binary
|
||||||
|
*.midi binary
|
||||||
|
*.mp3 binary
|
||||||
|
*.ogg binary
|
||||||
|
*.ra binary
|
||||||
|
|
||||||
|
# Video
|
||||||
|
*.3gpp binary
|
||||||
|
*.3gp binary
|
||||||
|
*.as binary
|
||||||
|
*.asf binary
|
||||||
|
*.asx binary
|
||||||
|
*.avi binary
|
||||||
|
*.fla binary
|
||||||
|
*.flv binary
|
||||||
|
*.m4v binary
|
||||||
|
*.mng binary
|
||||||
|
*.mov binary
|
||||||
|
*.mp4 binary
|
||||||
|
*.mpeg binary
|
||||||
|
*.mpg binary
|
||||||
|
*.ogv binary
|
||||||
|
*.swc binary
|
||||||
|
*.swf binary
|
||||||
|
*.webm binary
|
||||||
|
|
||||||
|
# Archives
|
||||||
|
*.7z binary
|
||||||
|
*.gz binary
|
||||||
|
*.jar binary
|
||||||
|
*.rar binary
|
||||||
|
*.tar binary
|
||||||
|
*.zip binary
|
||||||
|
|
||||||
|
# Fonts
|
||||||
|
*.ttf binary
|
||||||
|
*.eot binary
|
||||||
|
*.otf binary
|
||||||
|
*.woff binary
|
||||||
|
*.woff2 binary
|
||||||
|
|
||||||
|
# Executables
|
||||||
|
*.exe binary
|
||||||
|
*.pyc binary
|
||||||
|
# Prevents massive diffs caused by vendored, minified files
|
||||||
|
**/.yarn/releases/** binary
|
||||||
|
**/.yarn/plugins/** binary
|
||||||
|
|
||||||
|
# RC files (like .babelrc or .eslintrc)
|
||||||
|
*.*rc text
|
||||||
|
|
||||||
|
# Ignore files (like .npmignore or .gitignore)
|
||||||
|
*.*ignore text
|
||||||
|
|
||||||
|
# Prevents massive diffs from built files
|
||||||
|
dist/* binary
|
||||||
|
|
@ -0,0 +1,452 @@
|
||||||
|
# Created by https://www.toptal.com/developers/gitignore/api/python,django,node,react,git,visualstudiocode,dotenv
|
||||||
|
# Edit at https://www.toptal.com/developers/gitignore?templates=python,django,node,react,git,visualstudiocode,dotenv
|
||||||
|
|
||||||
|
### Django ###
|
||||||
|
*.log
|
||||||
|
*.pot
|
||||||
|
*.pyc
|
||||||
|
__pycache__/
|
||||||
|
local_settings.py
|
||||||
|
db.sqlite3
|
||||||
|
db.sqlite3-journal
|
||||||
|
media
|
||||||
|
|
||||||
|
# If your build process includes running collectstatic, then you probably don't need or want to include staticfiles/
|
||||||
|
# in your Git repository. Update and uncomment the following line accordingly.
|
||||||
|
api/static/
|
||||||
|
|
||||||
|
### Django.Python Stack ###
|
||||||
|
# Byte-compiled / optimized / DLL files
|
||||||
|
*.py[cod]
|
||||||
|
*$py.class
|
||||||
|
|
||||||
|
# C extensions
|
||||||
|
*.so
|
||||||
|
|
||||||
|
# Distribution / packaging
|
||||||
|
.Python
|
||||||
|
build/
|
||||||
|
develop-eggs/
|
||||||
|
dist/
|
||||||
|
downloads/
|
||||||
|
eggs/
|
||||||
|
.eggs/
|
||||||
|
lib/
|
||||||
|
lib64/
|
||||||
|
parts/
|
||||||
|
sdist/
|
||||||
|
var/
|
||||||
|
wheels/
|
||||||
|
share/python-wheels/
|
||||||
|
*.egg-info/
|
||||||
|
.installed.cfg
|
||||||
|
*.egg
|
||||||
|
MANIFEST
|
||||||
|
|
||||||
|
# PyInstaller
|
||||||
|
# Usually these files are written by a python script from a template
|
||||||
|
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
||||||
|
*.manifest
|
||||||
|
*.spec
|
||||||
|
|
||||||
|
# Installer logs
|
||||||
|
pip-log.txt
|
||||||
|
pip-delete-this-directory.txt
|
||||||
|
|
||||||
|
# Unit test / coverage reports
|
||||||
|
htmlcov/
|
||||||
|
.tox/
|
||||||
|
.nox/
|
||||||
|
.coverage
|
||||||
|
.coverage.*
|
||||||
|
.cache
|
||||||
|
nosetests.xml
|
||||||
|
coverage.xml
|
||||||
|
*.cover
|
||||||
|
*.py,cover
|
||||||
|
.hypothesis/
|
||||||
|
.pytest_cache/
|
||||||
|
cover/
|
||||||
|
|
||||||
|
# Translations
|
||||||
|
*.mo
|
||||||
|
|
||||||
|
# Django stuff:
|
||||||
|
|
||||||
|
# Flask stuff:
|
||||||
|
instance/
|
||||||
|
.webassets-cache
|
||||||
|
|
||||||
|
# Scrapy stuff:
|
||||||
|
.scrapy
|
||||||
|
|
||||||
|
# Sphinx documentation
|
||||||
|
docs/_build/
|
||||||
|
|
||||||
|
# PyBuilder
|
||||||
|
.pybuilder/
|
||||||
|
target/
|
||||||
|
|
||||||
|
# Jupyter Notebook
|
||||||
|
.ipynb_checkpoints
|
||||||
|
|
||||||
|
# IPython
|
||||||
|
profile_default/
|
||||||
|
ipython_config.py
|
||||||
|
|
||||||
|
# pyenv
|
||||||
|
# For a library or package, you might want to ignore these files since the code is
|
||||||
|
# intended to run in multiple environments; otherwise, check them in:
|
||||||
|
# .python-version
|
||||||
|
|
||||||
|
# pipenv
|
||||||
|
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
||||||
|
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
||||||
|
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
||||||
|
# install all needed dependencies.
|
||||||
|
#Pipfile.lock
|
||||||
|
|
||||||
|
# poetry
|
||||||
|
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
|
||||||
|
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
||||||
|
# commonly ignored for libraries.
|
||||||
|
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
|
||||||
|
#poetry.lock
|
||||||
|
|
||||||
|
# pdm
|
||||||
|
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
|
||||||
|
#pdm.lock
|
||||||
|
# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
|
||||||
|
# in version control.
|
||||||
|
# https://pdm.fming.dev/#use-with-ide
|
||||||
|
.pdm.toml
|
||||||
|
|
||||||
|
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
|
||||||
|
__pypackages__/
|
||||||
|
|
||||||
|
# Celery stuff
|
||||||
|
celerybeat-schedule
|
||||||
|
celerybeat.pid
|
||||||
|
|
||||||
|
# SageMath parsed files
|
||||||
|
*.sage.py
|
||||||
|
|
||||||
|
# Environments
|
||||||
|
.env
|
||||||
|
.venv
|
||||||
|
env/
|
||||||
|
venv/
|
||||||
|
ENV/
|
||||||
|
env.bak/
|
||||||
|
venv.bak/
|
||||||
|
|
||||||
|
# Spyder project settings
|
||||||
|
.spyderproject
|
||||||
|
.spyproject
|
||||||
|
|
||||||
|
# Rope project settings
|
||||||
|
.ropeproject
|
||||||
|
|
||||||
|
# mkdocs documentation
|
||||||
|
/site
|
||||||
|
|
||||||
|
# mypy
|
||||||
|
.mypy_cache/
|
||||||
|
.dmypy.json
|
||||||
|
dmypy.json
|
||||||
|
|
||||||
|
# Pyre type checker
|
||||||
|
.pyre/
|
||||||
|
|
||||||
|
# pytype static type analyzer
|
||||||
|
.pytype/
|
||||||
|
|
||||||
|
# Cython debug symbols
|
||||||
|
cython_debug/
|
||||||
|
|
||||||
|
# PyCharm
|
||||||
|
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
|
||||||
|
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
|
||||||
|
# and can be added to the global gitignore or merged into this file. For a more nuclear
|
||||||
|
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
||||||
|
#.idea/
|
||||||
|
|
||||||
|
### dotenv ###
|
||||||
|
|
||||||
|
### Git ###
|
||||||
|
# Created by git for backups. To disable backups in Git:
|
||||||
|
# $ git config --global mergetool.keepBackup false
|
||||||
|
*.orig
|
||||||
|
|
||||||
|
# Created by git when using merge tools for conflicts
|
||||||
|
*.BACKUP.*
|
||||||
|
*.BASE.*
|
||||||
|
*.LOCAL.*
|
||||||
|
*.REMOTE.*
|
||||||
|
*_BACKUP_*.txt
|
||||||
|
*_BASE_*.txt
|
||||||
|
*_LOCAL_*.txt
|
||||||
|
*_REMOTE_*.txt
|
||||||
|
|
||||||
|
### Node ###
|
||||||
|
# Logs
|
||||||
|
logs
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
lerna-debug.log*
|
||||||
|
.pnpm-debug.log*
|
||||||
|
|
||||||
|
# Diagnostic reports (https://nodejs.org/api/report.html)
|
||||||
|
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
|
||||||
|
|
||||||
|
# Runtime data
|
||||||
|
pids
|
||||||
|
*.pid
|
||||||
|
*.seed
|
||||||
|
*.pid.lock
|
||||||
|
|
||||||
|
# Directory for instrumented libs generated by jscoverage/JSCover
|
||||||
|
lib-cov
|
||||||
|
|
||||||
|
# Coverage directory used by tools like istanbul
|
||||||
|
coverage
|
||||||
|
*.lcov
|
||||||
|
|
||||||
|
# nyc test coverage
|
||||||
|
.nyc_output
|
||||||
|
|
||||||
|
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
|
||||||
|
.grunt
|
||||||
|
|
||||||
|
# Bower dependency directory (https://bower.io/)
|
||||||
|
bower_components
|
||||||
|
|
||||||
|
# node-waf configuration
|
||||||
|
.lock-wscript
|
||||||
|
|
||||||
|
# Compiled binary addons (https://nodejs.org/api/addons.html)
|
||||||
|
build/Release
|
||||||
|
|
||||||
|
# Dependency directories
|
||||||
|
node_modules/
|
||||||
|
jspm_packages/
|
||||||
|
|
||||||
|
# Snowpack dependency directory (https://snowpack.dev/)
|
||||||
|
web_modules/
|
||||||
|
|
||||||
|
# TypeScript cache
|
||||||
|
*.tsbuildinfo
|
||||||
|
|
||||||
|
# Optional npm cache directory
|
||||||
|
.npm
|
||||||
|
|
||||||
|
# Optional eslint cache
|
||||||
|
.eslintcache
|
||||||
|
|
||||||
|
# Optional stylelint cache
|
||||||
|
.stylelintcache
|
||||||
|
|
||||||
|
# Microbundle cache
|
||||||
|
.rpt2_cache/
|
||||||
|
.rts2_cache_cjs/
|
||||||
|
.rts2_cache_es/
|
||||||
|
.rts2_cache_umd/
|
||||||
|
|
||||||
|
# Optional REPL history
|
||||||
|
.node_repl_history
|
||||||
|
|
||||||
|
# Output of 'npm pack'
|
||||||
|
*.tgz
|
||||||
|
|
||||||
|
# Yarn Integrity file
|
||||||
|
.yarn-integrity
|
||||||
|
|
||||||
|
# dotenv environment variable files
|
||||||
|
.env.development.local
|
||||||
|
.env.test.local
|
||||||
|
.env.production.local
|
||||||
|
.env.local
|
||||||
|
|
||||||
|
# parcel-bundler cache (https://parceljs.org/)
|
||||||
|
.parcel-cache
|
||||||
|
|
||||||
|
# Next.js build output
|
||||||
|
.next
|
||||||
|
out
|
||||||
|
|
||||||
|
# Nuxt.js build / generate output
|
||||||
|
.nuxt
|
||||||
|
dist
|
||||||
|
|
||||||
|
# Gatsby files
|
||||||
|
.cache/
|
||||||
|
# Comment in the public line in if your project uses Gatsby and not Next.js
|
||||||
|
# https://nextjs.org/blog/next-9-1#public-directory-support
|
||||||
|
# public
|
||||||
|
|
||||||
|
# vuepress build output
|
||||||
|
.vuepress/dist
|
||||||
|
|
||||||
|
# vuepress v2.x temp and cache directory
|
||||||
|
.temp
|
||||||
|
|
||||||
|
# Docusaurus cache and generated files
|
||||||
|
.docusaurus
|
||||||
|
|
||||||
|
# Serverless directories
|
||||||
|
.serverless/
|
||||||
|
|
||||||
|
# FuseBox cache
|
||||||
|
.fusebox/
|
||||||
|
|
||||||
|
# DynamoDB Local files
|
||||||
|
.dynamodb/
|
||||||
|
|
||||||
|
# TernJS port file
|
||||||
|
.tern-port
|
||||||
|
|
||||||
|
# Stores VSCode versions used for testing VSCode extensions
|
||||||
|
.vscode-test
|
||||||
|
|
||||||
|
# yarn v2
|
||||||
|
.yarn/cache
|
||||||
|
.yarn/unplugged
|
||||||
|
.yarn/build-state.yml
|
||||||
|
.yarn/install-state.gz
|
||||||
|
.pnp.*
|
||||||
|
|
||||||
|
### Node Patch ###
|
||||||
|
# Serverless Webpack directories
|
||||||
|
.webpack/
|
||||||
|
|
||||||
|
# Optional stylelint cache
|
||||||
|
|
||||||
|
# SvelteKit build / generate output
|
||||||
|
.svelte-kit
|
||||||
|
|
||||||
|
### Python ###
|
||||||
|
# Byte-compiled / optimized / DLL files
|
||||||
|
|
||||||
|
# C extensions
|
||||||
|
|
||||||
|
# Distribution / packaging
|
||||||
|
|
||||||
|
# PyInstaller
|
||||||
|
# Usually these files are written by a python script from a template
|
||||||
|
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
||||||
|
|
||||||
|
# Installer logs
|
||||||
|
|
||||||
|
# Unit test / coverage reports
|
||||||
|
|
||||||
|
# Translations
|
||||||
|
|
||||||
|
# Django stuff:
|
||||||
|
|
||||||
|
# Flask stuff:
|
||||||
|
|
||||||
|
# Scrapy stuff:
|
||||||
|
|
||||||
|
# Sphinx documentation
|
||||||
|
|
||||||
|
# PyBuilder
|
||||||
|
|
||||||
|
# Jupyter Notebook
|
||||||
|
|
||||||
|
# IPython
|
||||||
|
|
||||||
|
# pyenv
|
||||||
|
# For a library or package, you might want to ignore these files since the code is
|
||||||
|
# intended to run in multiple environments; otherwise, check them in:
|
||||||
|
# .python-version
|
||||||
|
|
||||||
|
# pipenv
|
||||||
|
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
||||||
|
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
||||||
|
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
||||||
|
# install all needed dependencies.
|
||||||
|
|
||||||
|
# poetry
|
||||||
|
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
|
||||||
|
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
||||||
|
# commonly ignored for libraries.
|
||||||
|
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
|
||||||
|
|
||||||
|
# pdm
|
||||||
|
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
|
||||||
|
# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
|
||||||
|
# in version control.
|
||||||
|
# https://pdm.fming.dev/#use-with-ide
|
||||||
|
|
||||||
|
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
|
||||||
|
|
||||||
|
# Celery stuff
|
||||||
|
|
||||||
|
# SageMath parsed files
|
||||||
|
|
||||||
|
# Environments
|
||||||
|
|
||||||
|
# Spyder project settings
|
||||||
|
|
||||||
|
# Rope project settings
|
||||||
|
|
||||||
|
# mkdocs documentation
|
||||||
|
|
||||||
|
# mypy
|
||||||
|
|
||||||
|
# Pyre type checker
|
||||||
|
|
||||||
|
# pytype static type analyzer
|
||||||
|
|
||||||
|
# Cython debug symbols
|
||||||
|
|
||||||
|
# PyCharm
|
||||||
|
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
|
||||||
|
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
|
||||||
|
# and can be added to the global gitignore or merged into this file. For a more nuclear
|
||||||
|
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
||||||
|
|
||||||
|
### Python Patch ###
|
||||||
|
# Poetry local configuration file - https://python-poetry.org/docs/configuration/#local-configuration
|
||||||
|
poetry.toml
|
||||||
|
|
||||||
|
# ruff
|
||||||
|
.ruff_cache/
|
||||||
|
|
||||||
|
# LSP config files
|
||||||
|
pyrightconfig.json
|
||||||
|
|
||||||
|
### react ###
|
||||||
|
.DS_*
|
||||||
|
**/*.backup.*
|
||||||
|
**/*.back.*
|
||||||
|
|
||||||
|
node_modules
|
||||||
|
|
||||||
|
*.sublime*
|
||||||
|
|
||||||
|
psd
|
||||||
|
thumb
|
||||||
|
sketch
|
||||||
|
|
||||||
|
### VisualStudioCode ###
|
||||||
|
.vscode/*
|
||||||
|
!.vscode/settings.json
|
||||||
|
!.vscode/tasks.json
|
||||||
|
!.vscode/launch.json
|
||||||
|
!.vscode/extensions.json
|
||||||
|
!.vscode/*.code-snippets
|
||||||
|
|
||||||
|
# Local History for Visual Studio Code
|
||||||
|
.history/
|
||||||
|
|
||||||
|
# Built Visual Studio Code Extensions
|
||||||
|
*.vsix
|
||||||
|
|
||||||
|
### VisualStudioCode Patch ###
|
||||||
|
# Ignore all local history of files
|
||||||
|
.history
|
||||||
|
.ionide
|
||||||
|
|
||||||
|
# End of https://www.toptal.com/developers/gitignore/api/python,django,node,react,git,visualstudiocode,dotenv
|
||||||
|
|
@ -0,0 +1,21 @@
|
||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright 2023 Corey Sotiropoulos
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
|
|
@ -0,0 +1,53 @@
|
||||||
|
# Ixion
|
||||||
|
|
||||||
|
## Description
|
||||||
|
|
||||||
|
Ixion is designed to catalog public [LandSandBoat](https://github.com/LandSandBoat/server) servers. The API accepts POST requests with the `url` parameter which are fetched with the `/api/settings` subdirectory, looking for some required LSB settings as a json response. Valid LSB URLs are saved and periodically checked and updated accordingly. When a previously valid URL has been inactive for 24 hours, it is removed from the database.
|
||||||
|
|
||||||
|
The React client fetches server data from the API and displays it in a relevant way to users. Filtering is built into the client for now. When the API response fails or is empty, a default LSB demo server is displayed, pulling some info from GitHub.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- **Automatic Verification**: Verifies LSB server URL by sending a request to the public API. [Requires the LSB server to enable the HTTP server.](https://github.com/LandSandBoat/server/blob/df311283c4abb779d212e2b8af6734b0d8d11ad7/settings/default/network.lua#L33)
|
||||||
|
- **Automatic Deletion**: Deletes LSB servers from the database if their API is found to be invalid after a configurable period of inactivity ([default 24hrs](./api/.env.example#L8)).
|
||||||
|
- **Periodic Checks**: Runs update checks on all LSB servers in the database.
|
||||||
|
- **Server Filtering**: Filter LSB servers by different parameters.
|
||||||
|
- **Dockerized Deployment**: The application is containerized for easy deployment and development.
|
||||||
|
|
||||||
|
## Setup Instructions
|
||||||
|
|
||||||
|
### Production
|
||||||
|
|
||||||
|
#### API
|
||||||
|
|
||||||
|
In the `api` directory copy `.env.example` to `.env` and set details accordingly.
|
||||||
|
|
||||||
|
```sh
|
||||||
|
docker compose up
|
||||||
|
```
|
||||||
|
|
||||||
|
The API will be running on port 8000.
|
||||||
|
|
||||||
|
#### Client
|
||||||
|
|
||||||
|
In the `client` directory:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
npm install
|
||||||
|
npm run build
|
||||||
|
npm start
|
||||||
|
```
|
||||||
|
|
||||||
|
Build output is in `dist/`. Client will be running on port 3000.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Development
|
||||||
|
|
||||||
|
Download the [Dev Containers](https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.remote-containers) extension for VS Code and open the folder in the container. This also works well in codespaces.
|
||||||
|
|
||||||
|
If using Windows, highly recommend using WSL and storing the repo in the WSL filesystem.
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
MIT
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
# TODO
|
||||||
|
|
||||||
|
## Frontend
|
||||||
|
|
||||||
|
- Diff server settings with default settings, show changed settings only. (Chips?)
|
||||||
|
- Display active sessions over time on graph. (Active hours?)
|
||||||
|
|
||||||
|
## api
|
||||||
|
|
||||||
|
- Track active sessions over time.
|
||||||
|
- User accounts for voting/ranking, claiming ownership.
|
||||||
|
|
@ -0,0 +1,4 @@
|
||||||
|
# http://editorconfig.org
|
||||||
|
|
||||||
|
[*]
|
||||||
|
indent_size = 4
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
SECRET_KEY=insecure
|
||||||
|
DEBUG=False
|
||||||
|
ALLOWED_HOSTS=api.ixion.dev
|
||||||
|
CSRF_TRUSTED_ORIGINS=https://api.ixion.dev
|
||||||
|
CORS_ALLOWED_ORIGINS=https://ixion.dev
|
||||||
|
|
||||||
|
# API configuration
|
||||||
|
SERVER_INACTIVITY_TIMEOUT=24
|
||||||
|
|
@ -0,0 +1,14 @@
|
||||||
|
FROM python:3-alpine
|
||||||
|
|
||||||
|
ENV PIP_DISABLE_PIP_VERSION_CHECK 1
|
||||||
|
ENV PYTHONDONTWRITEBYTECODE 1
|
||||||
|
ENV PYTHONUNBUFFERED 1
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
COPY ./requirements.txt .
|
||||||
|
RUN pip install --no-cache-dir -r requirements.txt
|
||||||
|
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
ENTRYPOINT ["/app/entrypoint.sh"]
|
||||||
|
|
@ -0,0 +1,5 @@
|
||||||
|
# This will make sure the app is always imported when
|
||||||
|
# Django starts so that shared_task will use this app.
|
||||||
|
from .celery import app as celery_app
|
||||||
|
|
||||||
|
__all__ = ("celery_app",)
|
||||||
|
|
@ -0,0 +1,16 @@
|
||||||
|
"""
|
||||||
|
ASGI config for api project.
|
||||||
|
|
||||||
|
It exposes the ASGI callable as a module-level variable named ``application``.
|
||||||
|
|
||||||
|
For more information on this file, see
|
||||||
|
https://docs.djangoproject.com/en/5.0/howto/deployment/asgi/
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
|
||||||
|
from django.core.asgi import get_asgi_application
|
||||||
|
|
||||||
|
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "api.settings")
|
||||||
|
|
||||||
|
application = get_asgi_application()
|
||||||
|
|
@ -0,0 +1,34 @@
|
||||||
|
import os
|
||||||
|
from celery import Celery
|
||||||
|
from datetime import timedelta
|
||||||
|
import api.tasks
|
||||||
|
from dotenv import load_dotenv
|
||||||
|
|
||||||
|
load_dotenv() # take environment variables from .env.
|
||||||
|
|
||||||
|
# Set the default Django settings module for the 'celery' program.
|
||||||
|
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "api.settings")
|
||||||
|
|
||||||
|
app = Celery("api")
|
||||||
|
|
||||||
|
# Using a string here means the worker doesn't have to serialize
|
||||||
|
# the configuration object to child processes.
|
||||||
|
# - namespace='CELERY' means all celery-related configuration keys
|
||||||
|
# should have a `CELERY_` prefix.
|
||||||
|
app.config_from_object("django.conf:settings", namespace="CELERY")
|
||||||
|
|
||||||
|
# Load task modules from all registered Django apps.
|
||||||
|
app.autodiscover_tasks()
|
||||||
|
|
||||||
|
app.conf.broker_url = f"redis://redis:6379"
|
||||||
|
app.conf.result_backend = f"redis://redis:6379"
|
||||||
|
|
||||||
|
app.conf.beat_schedule = {
|
||||||
|
"verify-urls-every-hour": {
|
||||||
|
"task": "api.tasks.verify_urls_task",
|
||||||
|
"schedule": timedelta(hours=1),
|
||||||
|
# "schedule": timedelta(minutes=1),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
app.conf.timezone = "UTC"
|
||||||
|
|
@ -0,0 +1,141 @@
|
||||||
|
"""
|
||||||
|
Django settings for api project.
|
||||||
|
|
||||||
|
Generated by 'django-admin startproject' using Django 5.0.
|
||||||
|
|
||||||
|
For more information on this file, see
|
||||||
|
https://docs.djangoproject.com/en/5.0/topics/settings/
|
||||||
|
|
||||||
|
For the full list of settings and their values, see
|
||||||
|
https://docs.djangoproject.com/en/5.0/ref/settings/
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
# Build paths inside the project like this: BASE_DIR / 'subdir'.
|
||||||
|
BASE_DIR = Path(__file__).resolve().parent.parent
|
||||||
|
|
||||||
|
|
||||||
|
# Quick-start development settings - unsuitable for production
|
||||||
|
# See https://docs.djangoproject.com/en/5.0/howto/deployment/checklist/
|
||||||
|
|
||||||
|
# SECURITY WARNING: keep the secret key used in production secret!
|
||||||
|
SECRET_KEY = os.getenv("SECRET_KEY", default="insecure")
|
||||||
|
|
||||||
|
# SECURITY WARNING: don't run with debug turned on in production!
|
||||||
|
DEBUG = os.getenv("DEBUG", default="False").lower() == "true"
|
||||||
|
|
||||||
|
ALLOWED_HOSTS = os.getenv("ALLOWED_HOSTS", default="localhost,127.0.0.1,[::1]").split(
|
||||||
|
","
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# Application definition
|
||||||
|
|
||||||
|
INSTALLED_APPS = [
|
||||||
|
"django.contrib.admin",
|
||||||
|
"django.contrib.auth",
|
||||||
|
"django.contrib.contenttypes",
|
||||||
|
"django.contrib.sessions",
|
||||||
|
"django.contrib.messages",
|
||||||
|
"django.contrib.staticfiles",
|
||||||
|
"corsheaders",
|
||||||
|
"rest_framework",
|
||||||
|
"v1",
|
||||||
|
]
|
||||||
|
|
||||||
|
MIDDLEWARE = [
|
||||||
|
"corsheaders.middleware.CorsMiddleware",
|
||||||
|
"django.middleware.security.SecurityMiddleware",
|
||||||
|
"django.contrib.sessions.middleware.SessionMiddleware",
|
||||||
|
"django.middleware.common.CommonMiddleware",
|
||||||
|
"django.middleware.csrf.CsrfViewMiddleware",
|
||||||
|
"django.contrib.auth.middleware.AuthenticationMiddleware",
|
||||||
|
"django.contrib.messages.middleware.MessageMiddleware",
|
||||||
|
"django.middleware.clickjacking.XFrameOptionsMiddleware",
|
||||||
|
]
|
||||||
|
|
||||||
|
ROOT_URLCONF = "api.urls"
|
||||||
|
|
||||||
|
TEMPLATES = [
|
||||||
|
{
|
||||||
|
"BACKEND": "django.template.backends.django.DjangoTemplates",
|
||||||
|
"DIRS": [],
|
||||||
|
"APP_DIRS": True,
|
||||||
|
"OPTIONS": {
|
||||||
|
"context_processors": [
|
||||||
|
"django.template.context_processors.debug",
|
||||||
|
"django.template.context_processors.request",
|
||||||
|
"django.contrib.auth.context_processors.auth",
|
||||||
|
"django.contrib.messages.context_processors.messages",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
WSGI_APPLICATION = "api.wsgi.application"
|
||||||
|
|
||||||
|
|
||||||
|
# Database
|
||||||
|
# https://docs.djangoproject.com/en/5.0/ref/settings/#databases
|
||||||
|
|
||||||
|
DATABASES = {
|
||||||
|
"default": {
|
||||||
|
"ENGINE": "django.db.backends.sqlite3",
|
||||||
|
"NAME": BASE_DIR / "db.sqlite3",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# Password validation
|
||||||
|
# https://docs.djangoproject.com/en/5.0/ref/settings/#auth-password-validators
|
||||||
|
|
||||||
|
AUTH_PASSWORD_VALIDATORS = [
|
||||||
|
{
|
||||||
|
"NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"NAME": "django.contrib.auth.password_validation.MinimumLengthValidator",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"NAME": "django.contrib.auth.password_validation.CommonPasswordValidator",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"NAME": "django.contrib.auth.password_validation.NumericPasswordValidator",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
# Internationalization
|
||||||
|
# https://docs.djangoproject.com/en/5.0/topics/i18n/
|
||||||
|
|
||||||
|
LANGUAGE_CODE = "en-us"
|
||||||
|
|
||||||
|
TIME_ZONE = "UTC"
|
||||||
|
|
||||||
|
USE_I18N = True
|
||||||
|
|
||||||
|
USE_TZ = True
|
||||||
|
|
||||||
|
|
||||||
|
# Static files (CSS, JavaScript, Images)
|
||||||
|
# https://docs.djangoproject.com/en/5.0/howto/static-files/
|
||||||
|
|
||||||
|
STATIC_URL = "/static/"
|
||||||
|
STATIC_ROOT = os.path.join(BASE_DIR, "static")
|
||||||
|
|
||||||
|
# Default primary key field type
|
||||||
|
# https://docs.djangoproject.com/en/5.0/ref/settings/#default-auto-field
|
||||||
|
|
||||||
|
DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"
|
||||||
|
|
||||||
|
CSRF_TRUSTED_ORIGINS = os.getenv(
|
||||||
|
"CSRF_TRUSTED_ORIGINS", default="http://localhost:3000"
|
||||||
|
).split(",")
|
||||||
|
CORS_ALLOWED_ORIGINS = os.getenv(
|
||||||
|
"CORS_ALLOWED_ORIGINS", default="http://localhost:3000"
|
||||||
|
).split(",")
|
||||||
|
CORS_ALLOW_ALL_ORIGINS = (
|
||||||
|
os.getenv("CORS_ALLOW_ALL_ORIGINS", default="False").lower() == "true"
|
||||||
|
)
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
from celery import shared_task
|
||||||
|
from django.core.management import call_command
|
||||||
|
|
||||||
|
|
||||||
|
@shared_task
|
||||||
|
def verify_urls_task():
|
||||||
|
call_command("verify_urls")
|
||||||
|
|
@ -0,0 +1,29 @@
|
||||||
|
"""
|
||||||
|
URL configuration for api project.
|
||||||
|
|
||||||
|
The `urlpatterns` list routes URLs to views. For more information please see:
|
||||||
|
https://docs.djangoproject.com/en/5.0/topics/http/urls/
|
||||||
|
Examples:
|
||||||
|
Function views
|
||||||
|
1. Add an import: from my_app import views
|
||||||
|
2. Add a URL to urlpatterns: path('', views.home, name='home')
|
||||||
|
Class-based views
|
||||||
|
1. Add an import: from other_app.views import Home
|
||||||
|
2. Add a URL to urlpatterns: path('', Home.as_view(), name='home')
|
||||||
|
Including another URLconf
|
||||||
|
1. Import the include() function: from django.urls import include, path
|
||||||
|
2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
|
||||||
|
"""
|
||||||
|
|
||||||
|
from django.contrib import admin
|
||||||
|
from django.urls import path, include
|
||||||
|
from rest_framework import routers
|
||||||
|
from v1.views import server
|
||||||
|
|
||||||
|
router = routers.DefaultRouter()
|
||||||
|
router.register(r"servers", server.ServerViewSet, "v1")
|
||||||
|
|
||||||
|
urlpatterns = [
|
||||||
|
path("admin/", admin.site.urls),
|
||||||
|
path("v1/", include(router.urls)),
|
||||||
|
]
|
||||||
|
|
@ -0,0 +1,16 @@
|
||||||
|
"""
|
||||||
|
WSGI config for api project.
|
||||||
|
|
||||||
|
It exposes the WSGI callable as a module-level variable named ``application``.
|
||||||
|
|
||||||
|
For more information on this file, see
|
||||||
|
https://docs.djangoproject.com/en/5.0/howto/deployment/wsgi/
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
|
||||||
|
from django.core.wsgi import get_wsgi_application
|
||||||
|
|
||||||
|
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "api.settings")
|
||||||
|
|
||||||
|
application = get_wsgi_application()
|
||||||
|
|
@ -0,0 +1,30 @@
|
||||||
|
services:
|
||||||
|
api:
|
||||||
|
build: .
|
||||||
|
command: gunicorn api.wsgi:application --bind 0.0.0.0:8000
|
||||||
|
restart: always
|
||||||
|
volumes:
|
||||||
|
- .:/app
|
||||||
|
ports:
|
||||||
|
- 8000:8000
|
||||||
|
depends_on:
|
||||||
|
- redis
|
||||||
|
celery:
|
||||||
|
build: .
|
||||||
|
command: celery -A api worker -l info
|
||||||
|
restart: always
|
||||||
|
volumes:
|
||||||
|
- .:/app
|
||||||
|
depends_on:
|
||||||
|
- redis
|
||||||
|
celery-beat:
|
||||||
|
build: .
|
||||||
|
command: celery -A api beat -l info
|
||||||
|
restart: always
|
||||||
|
volumes:
|
||||||
|
- .:/app
|
||||||
|
depends_on:
|
||||||
|
- redis
|
||||||
|
redis:
|
||||||
|
image: redis:alpine
|
||||||
|
restart: always
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
python manage.py collectstatic --noinput
|
||||||
|
python manage.py makemigrations
|
||||||
|
python manage.py migrate
|
||||||
|
|
||||||
|
exec "$@"
|
||||||
|
|
@ -0,0 +1,22 @@
|
||||||
|
#!/usr/bin/env python
|
||||||
|
"""Django's command-line utility for administrative tasks."""
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
"""Run administrative tasks."""
|
||||||
|
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "api.settings")
|
||||||
|
try:
|
||||||
|
from django.core.management import execute_from_command_line
|
||||||
|
except ImportError as exc:
|
||||||
|
raise ImportError(
|
||||||
|
"Couldn't import Django. Are you sure it's installed and "
|
||||||
|
"available on your PYTHONPATH environment variable? Did you "
|
||||||
|
"forget to activate a virtual environment?"
|
||||||
|
) from exc
|
||||||
|
execute_from_command_line(sys.argv)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
django
|
||||||
|
djangorestframework
|
||||||
|
django-cors-headers
|
||||||
|
requests
|
||||||
|
celery
|
||||||
|
redis
|
||||||
|
python-dotenv
|
||||||
|
gunicorn
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
from django.contrib import admin
|
||||||
|
from .models.server import Server
|
||||||
|
|
||||||
|
|
||||||
|
class ServerAdmin(admin.ModelAdmin):
|
||||||
|
display = "url, settings, created, updated"
|
||||||
|
|
||||||
|
|
||||||
|
# Register your models here.
|
||||||
|
|
||||||
|
admin.site.register(Server, ServerAdmin)
|
||||||
|
|
@ -0,0 +1,6 @@
|
||||||
|
from django.apps import AppConfig
|
||||||
|
|
||||||
|
|
||||||
|
class V1Config(AppConfig):
|
||||||
|
default_auto_field = "django.db.models.BigAutoField"
|
||||||
|
name = "v1"
|
||||||
|
|
@ -0,0 +1,4 @@
|
||||||
|
import logging
|
||||||
|
|
||||||
|
logging.basicConfig(level=logging.INFO)
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
@ -0,0 +1,25 @@
|
||||||
|
# api/v1/management/commands/verify_urls.py
|
||||||
|
|
||||||
|
from django.core.management.base import BaseCommand
|
||||||
|
from v1.common.logging import logger
|
||||||
|
from v1.models.server import Server
|
||||||
|
from concurrent.futures import ThreadPoolExecutor
|
||||||
|
|
||||||
|
|
||||||
|
class Command(BaseCommand):
|
||||||
|
help = "Verify URLs and delete invalid Server objects"
|
||||||
|
|
||||||
|
def handle(self, *args, **kwargs):
|
||||||
|
logger.info("Verifying URLs...")
|
||||||
|
self.verify_urls()
|
||||||
|
|
||||||
|
def verify_urls(self):
|
||||||
|
# Define a function to process each server
|
||||||
|
def process_server(server):
|
||||||
|
if not server.save():
|
||||||
|
logger.info(f"Deleting Server object with URL: {server.url}")
|
||||||
|
server.delete()
|
||||||
|
|
||||||
|
# Use ThreadPoolExecutor to parallelize the task
|
||||||
|
with ThreadPoolExecutor() as executor:
|
||||||
|
executor.map(process_server, Server.objects.all())
|
||||||
|
|
@ -0,0 +1,99 @@
|
||||||
|
import os
|
||||||
|
import requests
|
||||||
|
from django.db import models
|
||||||
|
from django.core.validators import URLValidator
|
||||||
|
from django.core.exceptions import ValidationError
|
||||||
|
from urllib.parse import urlparse
|
||||||
|
from v1.common.logging import logger
|
||||||
|
|
||||||
|
|
||||||
|
required_settings = [
|
||||||
|
"MAIN.SERVER_NAME",
|
||||||
|
"MAIN.MAX_LEVEL",
|
||||||
|
"LOGIN.LOGIN_LIMIT",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class OptionalSchemeURLValidator(URLValidator):
|
||||||
|
def __call__(self, value):
|
||||||
|
if "://" not in value:
|
||||||
|
# Validate as if it were http://
|
||||||
|
value = "http://" + value
|
||||||
|
super(OptionalSchemeURLValidator, self).__call__(value)
|
||||||
|
|
||||||
|
|
||||||
|
class Server(models.Model):
|
||||||
|
url = models.CharField(
|
||||||
|
max_length=400,
|
||||||
|
null=False,
|
||||||
|
validators=[OptionalSchemeURLValidator()],
|
||||||
|
)
|
||||||
|
settings = models.JSONField(null=True, editable=False)
|
||||||
|
active_sessions = models.IntegerField(null=True, editable=False)
|
||||||
|
created = models.DateTimeField(auto_now_add=True)
|
||||||
|
updated = models.DateTimeField(auto_now=True)
|
||||||
|
inactivity_counter = models.IntegerField(null=True, editable=False)
|
||||||
|
|
||||||
|
def _str_(self):
|
||||||
|
return self.url
|
||||||
|
|
||||||
|
def save(self, *args, **kwargs):
|
||||||
|
# Prepend a default scheme if the URL does not include one
|
||||||
|
if not self.url.startswith(("http://", "https://")):
|
||||||
|
self.url = f"http://{self.url}"
|
||||||
|
|
||||||
|
# Extract the domain name from the validated URL
|
||||||
|
parsed_url = urlparse(self.url)
|
||||||
|
self.url = parsed_url.netloc.lower()
|
||||||
|
|
||||||
|
# Check if server URL is unique
|
||||||
|
existing_server = Server.objects.filter(url=self.url).first()
|
||||||
|
if existing_server and existing_server.pk != self.pk:
|
||||||
|
raise ValidationError("A server with this URL already exists.")
|
||||||
|
|
||||||
|
# Validate the server API
|
||||||
|
if not self.parse_server_api():
|
||||||
|
if (
|
||||||
|
self.inactivity_counter is None
|
||||||
|
or self.inactivity_counter
|
||||||
|
>= int(os.getenv("SERVER_INACTIVITY_TIMEOUT", default="24")) - 1
|
||||||
|
):
|
||||||
|
return False
|
||||||
|
self.inactivity_counter += 1
|
||||||
|
else:
|
||||||
|
if self.settings["API.DO_NOT_TRACK"]:
|
||||||
|
return False
|
||||||
|
self.inactivity_counter = 0
|
||||||
|
|
||||||
|
# Proceed with saving the object
|
||||||
|
logger.info(f"Saving Server object with URL: {self.url}")
|
||||||
|
super(Server, self).save(*args, **kwargs)
|
||||||
|
return True
|
||||||
|
|
||||||
|
def parse_server_api(self):
|
||||||
|
try:
|
||||||
|
# Request server settings from API
|
||||||
|
response = requests.get(f"http://{self.url}/api/settings")
|
||||||
|
response.raise_for_status() # Raise an exception for HTTP errors (4xx and 5xx)
|
||||||
|
json_data = response.json()
|
||||||
|
|
||||||
|
# Validate the JSON structure
|
||||||
|
if not isinstance(json_data, dict):
|
||||||
|
return False
|
||||||
|
|
||||||
|
for setting in required_settings:
|
||||||
|
if setting not in json_data:
|
||||||
|
return False
|
||||||
|
|
||||||
|
self.settings = json_data
|
||||||
|
|
||||||
|
# Request the active session count from API
|
||||||
|
response = requests.get(f"http://{self.url}/api/sessions")
|
||||||
|
response.raise_for_status() # Raise an exception for HTTP errors (4xx and 5xx)
|
||||||
|
session_count = response.text
|
||||||
|
if session_count.isdigit():
|
||||||
|
self.active_sessions = int(session_count)
|
||||||
|
return True
|
||||||
|
|
||||||
|
except requests.RequestException:
|
||||||
|
return False
|
||||||
|
|
@ -0,0 +1,15 @@
|
||||||
|
from rest_framework import serializers
|
||||||
|
from v1.models.server import Server
|
||||||
|
from django.core.exceptions import ValidationError
|
||||||
|
|
||||||
|
|
||||||
|
class ServerSerializer(serializers.ModelSerializer):
|
||||||
|
class Meta:
|
||||||
|
model = Server
|
||||||
|
fields = "__all__"
|
||||||
|
|
||||||
|
def create(self, validated_data):
|
||||||
|
try:
|
||||||
|
return super().create(validated_data)
|
||||||
|
except ValidationError as e:
|
||||||
|
raise serializers.ValidationError({"url": e.messages})
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
from django.test import TestCase
|
||||||
|
|
||||||
|
# Create your tests here.
|
||||||
|
|
@ -0,0 +1,60 @@
|
||||||
|
from rest_framework import viewsets, mixins
|
||||||
|
from django.db.models.fields.json import KT
|
||||||
|
from v1.serializers.server import ServerSerializer
|
||||||
|
from v1.models.server import Server
|
||||||
|
|
||||||
|
|
||||||
|
class ServerView(
|
||||||
|
mixins.CreateModelMixin,
|
||||||
|
mixins.RetrieveModelMixin,
|
||||||
|
mixins.ListModelMixin,
|
||||||
|
viewsets.GenericViewSet,
|
||||||
|
):
|
||||||
|
serializer_class = ServerSerializer
|
||||||
|
queryset = Server.objects.all()
|
||||||
|
|
||||||
|
|
||||||
|
class ServerViewSet(
|
||||||
|
mixins.CreateModelMixin,
|
||||||
|
mixins.RetrieveModelMixin,
|
||||||
|
mixins.ListModelMixin,
|
||||||
|
viewsets.GenericViewSet,
|
||||||
|
):
|
||||||
|
queryset = Server.objects.all()
|
||||||
|
serializer_class = ServerSerializer
|
||||||
|
|
||||||
|
CONFIG_MAPPING = {
|
||||||
|
"name": {
|
||||||
|
"annotate_field": "settings__MAIN.SERVER_NAME",
|
||||||
|
"filter_type": "startswith",
|
||||||
|
},
|
||||||
|
"level_cap_min": {
|
||||||
|
"annotate_field": "settings__MAIN.MAX_LEVEL",
|
||||||
|
"filter_type": "gte",
|
||||||
|
},
|
||||||
|
"level_cap_max": {
|
||||||
|
"annotate_field": "settings__MAIN.MAX_LEVEL",
|
||||||
|
"filter_type": "lte",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
def get_queryset(self):
|
||||||
|
queryset = self.queryset
|
||||||
|
|
||||||
|
# Iterate through the configuration mapping and apply dynamic annotation and filtering
|
||||||
|
for param, config in self.CONFIG_MAPPING.items():
|
||||||
|
value = self.request.query_params.get(param)
|
||||||
|
if value:
|
||||||
|
annotate_field = config["annotate_field"]
|
||||||
|
filter_type = config["filter_type"]
|
||||||
|
annotate_name = f"{param}_annotated"
|
||||||
|
|
||||||
|
# Annotate the queryset with the specified annotation field and filter type
|
||||||
|
queryset = queryset.annotate(
|
||||||
|
**{annotate_name: KT(annotate_field)}
|
||||||
|
).filter(**{f"{annotate_name}__{filter_type}": value})
|
||||||
|
|
||||||
|
# TODO: This probably isn't very efficient
|
||||||
|
queryset._result_cache = None
|
||||||
|
|
||||||
|
return queryset
|
||||||
|
|
@ -0,0 +1,33 @@
|
||||||
|
module.exports = {
|
||||||
|
env: {
|
||||||
|
browser: true,
|
||||||
|
es2021: true,
|
||||||
|
},
|
||||||
|
extends: [
|
||||||
|
'airbnb',
|
||||||
|
'airbnb-typescript',
|
||||||
|
'airbnb/hooks',
|
||||||
|
'plugin:react/recommended',
|
||||||
|
'plugin:@typescript-eslint/recommended',
|
||||||
|
'plugin:prettier/recommended',
|
||||||
|
'plugin:tailwindcss/recommended',
|
||||||
|
],
|
||||||
|
parser: '@typescript-eslint/parser',
|
||||||
|
parserOptions: {
|
||||||
|
ecmaVersion: 'latest',
|
||||||
|
sourceType: 'module',
|
||||||
|
project: ['./tsconfig.json', './tsconfig.node.json'],
|
||||||
|
tsconfigRootDir: __dirname,
|
||||||
|
},
|
||||||
|
plugins: [
|
||||||
|
'react',
|
||||||
|
'@typescript-eslint',
|
||||||
|
'prettier',
|
||||||
|
'tailwindcss',
|
||||||
|
'react-refresh',
|
||||||
|
],
|
||||||
|
rules: {
|
||||||
|
'react/react-in-jsx-scope': 0,
|
||||||
|
'react-refresh/only-export-components': 'warn',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,9 @@
|
||||||
|
const config = {
|
||||||
|
trailingComma: 'es5',
|
||||||
|
tabWidth: 2,
|
||||||
|
semi: true,
|
||||||
|
singleQuote: true,
|
||||||
|
plugins: ['prettier-plugin-tailwindcss']
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = config;
|
||||||
|
|
@ -0,0 +1,25 @@
|
||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<link rel="icon" type="image/png" href="/favicon.png" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
|
||||||
|
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||||
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||||
|
<link
|
||||||
|
rel="stylesheet"
|
||||||
|
href="https://fonts.googleapis.com/css2?family=Roboto:wght@300;400;500;600;700&display=swap"
|
||||||
|
/>
|
||||||
|
<link
|
||||||
|
rel="stylesheet"
|
||||||
|
href="https://fonts.googleapis.com/icon?family=Material+Icons"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<title>IXION - FFXI Private Server Directory</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="root"></div>
|
||||||
|
<script type="module" src="/src/main.tsx"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,52 @@
|
||||||
|
{
|
||||||
|
"name": "frontend",
|
||||||
|
"private": true,
|
||||||
|
"version": "0.0.0",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite",
|
||||||
|
"build": "tsc && vite build",
|
||||||
|
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
|
||||||
|
"preview": "vite preview",
|
||||||
|
"test": "vitest"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@emotion/react": "^11.11.3",
|
||||||
|
"@emotion/styled": "^11.11.0",
|
||||||
|
"@mui/icons-material": "^5.15.6",
|
||||||
|
"@mui/material": "^5.15.6",
|
||||||
|
"react": "^18.2.0",
|
||||||
|
"react-dom": "^18.2.0",
|
||||||
|
"react-router-dom": "^6.21.3"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@testing-library/jest-dom": "^6.1.6",
|
||||||
|
"@testing-library/react": "^14.1.2",
|
||||||
|
"@types/jsdom": "^21.1.6",
|
||||||
|
"@types/react": "^18.2.66",
|
||||||
|
"@types/react-dom": "^18.2.22",
|
||||||
|
"@typescript-eslint/eslint-plugin": "^6.16.0",
|
||||||
|
"@typescript-eslint/parser": "^6.16.0",
|
||||||
|
"@vitejs/plugin-react-swc": "^3.5.0",
|
||||||
|
"autoprefixer": "^10.4.17",
|
||||||
|
"eslint": "^8.2.0",
|
||||||
|
"eslint-config-airbnb": "^19.0.4",
|
||||||
|
"eslint-config-airbnb-typescript": "^17.1.0",
|
||||||
|
"eslint-config-prettier": "^9.1.0",
|
||||||
|
"eslint-plugin-import": "^2.25.3",
|
||||||
|
"eslint-plugin-jsx-a11y": "^6.5.1",
|
||||||
|
"eslint-plugin-prettier": "^5.1.2",
|
||||||
|
"eslint-plugin-react": "^7.33.2",
|
||||||
|
"eslint-plugin-react-hooks": "^4.3.0",
|
||||||
|
"eslint-plugin-react-refresh": "^0.4.5",
|
||||||
|
"eslint-plugin-tailwindcss": "^3.14.2",
|
||||||
|
"jsdom": "^24.0.0",
|
||||||
|
"postcss": "^8.4.33",
|
||||||
|
"prettier": "^3.1.1",
|
||||||
|
"prettier-plugin-tailwindcss": "^0.5.11",
|
||||||
|
"tailwindcss": "^3.4.1",
|
||||||
|
"typescript": "^5.2.2",
|
||||||
|
"vite": "^5.0.8",
|
||||||
|
"vitest": "^1.1.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,6 @@
|
||||||
|
export default {
|
||||||
|
plugins: {
|
||||||
|
tailwindcss: {},
|
||||||
|
autoprefixer: {},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,31 @@
|
||||||
|
import { render, screen } from '@testing-library/react';
|
||||||
|
import { describe, it } from 'vitest';
|
||||||
|
|
||||||
|
import { MemoryRouter } from 'react-router-dom';
|
||||||
|
import { App, WrappedApp } from './App';
|
||||||
|
|
||||||
|
describe('App', () => {
|
||||||
|
it('Renders hello world', () => {
|
||||||
|
// ARRANGE
|
||||||
|
render(<WrappedApp />);
|
||||||
|
// ACT
|
||||||
|
// EXPECT
|
||||||
|
expect(
|
||||||
|
screen.getByRole('heading', {
|
||||||
|
level: 1,
|
||||||
|
})
|
||||||
|
).toHaveTextContent('Hello, world!');
|
||||||
|
});
|
||||||
|
it('Renders NotFound if invalid path', () => {
|
||||||
|
render(
|
||||||
|
<MemoryRouter initialEntries={['/idonotexist']}>
|
||||||
|
<App />
|
||||||
|
</MemoryRouter>
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
screen.getByRole('heading', {
|
||||||
|
level: 1,
|
||||||
|
})
|
||||||
|
).toHaveTextContent('Not Found');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -0,0 +1,77 @@
|
||||||
|
import { Box, Container } from '@mui/material';
|
||||||
|
import { useState } from 'react';
|
||||||
|
import { HashRouter, Route, Routes } from 'react-router-dom';
|
||||||
|
import AlertComponent from './components/Alert';
|
||||||
|
import Footer from './components/Footer';
|
||||||
|
import Header from './components/Header';
|
||||||
|
import SearchState from './data/SearchState';
|
||||||
|
import ServerData from './data/ServerData';
|
||||||
|
import About from './pages/About';
|
||||||
|
import Home from './pages/Home';
|
||||||
|
import NotFound from './pages/NotFound';
|
||||||
|
|
||||||
|
export function App() {
|
||||||
|
const [alertInfo, setAlertInfo] = useState<{
|
||||||
|
message: string;
|
||||||
|
severity: 'error' | 'warning' | 'info' | 'success';
|
||||||
|
} | null>(null);
|
||||||
|
const [servers, setServers] = useState<ServerData[]>([]);
|
||||||
|
|
||||||
|
const [searchName, setSearchName] = useState('');
|
||||||
|
const [searchMultibox, setSearchMultibox] = useState<string[] | null>(null);
|
||||||
|
const [searchTrusts, setSearchTrusts] = useState<string[] | null>(null);
|
||||||
|
const [searchLevelSync, setSearchLevelSync] = useState<string[] | null>(null);
|
||||||
|
const [searchMaxLevel, setSearchMaxLevel] = useState<number[]>([1, 99]);
|
||||||
|
const [searchExpansions, setSearchExpansions] = useState<string[] | null>(
|
||||||
|
null
|
||||||
|
);
|
||||||
|
|
||||||
|
const searchState: SearchState = {
|
||||||
|
name: { value: searchName, setValue: setSearchName },
|
||||||
|
multibox: { value: searchMultibox, setValue: setSearchMultibox },
|
||||||
|
trusts: { value: searchTrusts, setValue: setSearchTrusts },
|
||||||
|
levelSync: { value: searchLevelSync, setValue: setSearchLevelSync },
|
||||||
|
maxLevel: { value: searchMaxLevel, setValue: setSearchMaxLevel },
|
||||||
|
expansions: { value: searchExpansions, setValue: setSearchExpansions },
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex min-h-screen flex-col">
|
||||||
|
<Header
|
||||||
|
setAlertInfo={setAlertInfo}
|
||||||
|
servers={servers}
|
||||||
|
setServers={setServers}
|
||||||
|
searchState={searchState}
|
||||||
|
/>
|
||||||
|
<AlertComponent alertInfo={alertInfo} setAlertInfo={setAlertInfo} />
|
||||||
|
<Container className="grow p-0">
|
||||||
|
<Box className="m-2">
|
||||||
|
<Routes>
|
||||||
|
<Route
|
||||||
|
path="/"
|
||||||
|
element={
|
||||||
|
<Home
|
||||||
|
servers={servers}
|
||||||
|
setServers={setServers}
|
||||||
|
searchState={searchState}
|
||||||
|
setAlertInfo={setAlertInfo}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<Route path="/about" element={<About />} />
|
||||||
|
<Route path="*" element={<NotFound />} />
|
||||||
|
</Routes>
|
||||||
|
</Box>
|
||||||
|
</Container>
|
||||||
|
<Footer />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function WrappedApp() {
|
||||||
|
return (
|
||||||
|
<HashRouter>
|
||||||
|
<App />
|
||||||
|
</HashRouter>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,85 @@
|
||||||
|
import { DemoServerData } from './data/ServerData';
|
||||||
|
|
||||||
|
export const fetchDemo = async () => {
|
||||||
|
const repoUrl = 'https://api.github.com/repos/LandSandBoat/server';
|
||||||
|
try {
|
||||||
|
const response = await fetch(repoUrl);
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(
|
||||||
|
`(${response.status.toString()} - ${response.statusText || 'unknown'})`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const gitHubData = await response.json();
|
||||||
|
const demoData = {
|
||||||
|
...DemoServerData,
|
||||||
|
active_sessions: gitHubData.stargazers_count,
|
||||||
|
updated: gitHubData.updated_at,
|
||||||
|
};
|
||||||
|
return demoData;
|
||||||
|
} catch (err) {
|
||||||
|
return DemoServerData;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const fetchData = async (queryParams = {}) => {
|
||||||
|
const queryString = new URLSearchParams(queryParams).toString();
|
||||||
|
const url = `${import.meta.env.PROD ? 'https://api.ixion.dev' : 'http://localhost:8000'}/v1/servers/${queryString ? `?${queryString}` : ''}`;
|
||||||
|
try {
|
||||||
|
const response = await fetch(url, {
|
||||||
|
signal: AbortSignal.timeout(5000),
|
||||||
|
});
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(
|
||||||
|
`(${response.status.toString()} - ${response.statusText || 'unknown'})`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return await response.json();
|
||||||
|
} catch (err) {
|
||||||
|
if (err instanceof Error) {
|
||||||
|
throw new Error(`${err.message} - ${url}`);
|
||||||
|
} else {
|
||||||
|
throw new Error('Something went wrong.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const postData = async (inputText: string) => {
|
||||||
|
try {
|
||||||
|
const response = await fetch(
|
||||||
|
`${import.meta.env.PROD ? 'https://api.ixion.dev' : 'http://localhost:8000'}/v1/servers/`,
|
||||||
|
{
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ url: inputText }),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
let message = `Server verification failed!`;
|
||||||
|
let success = false;
|
||||||
|
if (!response.ok) {
|
||||||
|
// bad url or duplicate
|
||||||
|
if (response.status === 400) {
|
||||||
|
const responseData = await response.json();
|
||||||
|
message = responseData.url;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
message,
|
||||||
|
success,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
const responseData = await response.json();
|
||||||
|
if (responseData.id !== null) {
|
||||||
|
message = 'Added new server!';
|
||||||
|
success = true;
|
||||||
|
}
|
||||||
|
return { message, success, data: responseData };
|
||||||
|
} catch (err) {
|
||||||
|
let message = 'An unknown error occurred.';
|
||||||
|
if (err instanceof Error) {
|
||||||
|
message = err.message;
|
||||||
|
}
|
||||||
|
return { message, success: false };
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,44 @@
|
||||||
|
/* eslint-disable react/jsx-props-no-spreading */
|
||||||
|
import { styled } from '@mui/material';
|
||||||
|
import MuiAccordion, { AccordionProps } from '@mui/material/Accordion';
|
||||||
|
import MuiAccordionDetails, {
|
||||||
|
AccordionDetailsProps,
|
||||||
|
} from '@mui/material/AccordionDetails';
|
||||||
|
import MuiAccordionSummary, {
|
||||||
|
AccordionSummaryProps,
|
||||||
|
} from '@mui/material/AccordionSummary';
|
||||||
|
|
||||||
|
export const Accordion = styled((props: AccordionProps) => (
|
||||||
|
<MuiAccordion disableGutters elevation={0} square {...props} />
|
||||||
|
))(({ theme }) => ({
|
||||||
|
border: `1px solid ${theme.palette.divider}`,
|
||||||
|
'&:not(:last-child)': {
|
||||||
|
border: 0,
|
||||||
|
},
|
||||||
|
'&::before': {
|
||||||
|
display: 'none',
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
export const AccordionSummary = styled((props: AccordionSummaryProps) => (
|
||||||
|
<MuiAccordionSummary {...props} />
|
||||||
|
))(({ theme }) => ({
|
||||||
|
backgroundColor:
|
||||||
|
theme.palette.mode === 'dark'
|
||||||
|
? 'rgba(255, 255, 255, .05)'
|
||||||
|
: 'rgba(0, 0, 0, .02)',
|
||||||
|
'& .MuiAccordionSummary-content': {
|
||||||
|
margin: theme.spacing(0),
|
||||||
|
},
|
||||||
|
boxShadow: '0 0 10px rgba(0, 0, 0, 0.1)', // Adjust the size and opacity as needed
|
||||||
|
minHeight: '32px',
|
||||||
|
}));
|
||||||
|
|
||||||
|
export const AccordionDetails = styled((props: AccordionDetailsProps) => (
|
||||||
|
<MuiAccordionDetails {...props} />
|
||||||
|
))(({ theme }) => ({
|
||||||
|
backgroundColor:
|
||||||
|
theme.palette.mode === 'dark'
|
||||||
|
? 'rgba(255, 255, 255, .03)'
|
||||||
|
: 'rgba(0, 0, 0, .04)',
|
||||||
|
}));
|
||||||
|
|
@ -0,0 +1,155 @@
|
||||||
|
import { Add } from '@mui/icons-material';
|
||||||
|
import {
|
||||||
|
Box,
|
||||||
|
CircularProgress,
|
||||||
|
IconButton,
|
||||||
|
InputBase,
|
||||||
|
Slide,
|
||||||
|
Tooltip,
|
||||||
|
alpha,
|
||||||
|
styled,
|
||||||
|
} from '@mui/material';
|
||||||
|
import { useRef, useState } from 'react';
|
||||||
|
import { postData } from '../apiUtil';
|
||||||
|
import ServerData from '../data/ServerData';
|
||||||
|
import { AlertResponse } from './Alert';
|
||||||
|
|
||||||
|
const AddServerInput = styled('div')(({ theme }) => ({
|
||||||
|
position: 'relative',
|
||||||
|
borderRadius: theme.shape.borderRadius,
|
||||||
|
backgroundColor: alpha(theme.palette.common.white, 0.15),
|
||||||
|
'&:hover': {
|
||||||
|
backgroundColor: alpha(theme.palette.common.white, 0.25),
|
||||||
|
},
|
||||||
|
marginRight: theme.spacing(2),
|
||||||
|
marginLeft: 0,
|
||||||
|
width: '100%',
|
||||||
|
[theme.breakpoints.up('sm')]: {
|
||||||
|
marginLeft: theme.spacing(3),
|
||||||
|
width: 'auto',
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
const StyledInputBase = styled(InputBase)(({ theme }) => ({
|
||||||
|
color: 'inherit',
|
||||||
|
'& .MuiInputBase-input': {
|
||||||
|
padding: theme.spacing(1, 1, 1, 1),
|
||||||
|
transition: theme.transitions.create('width'),
|
||||||
|
width: '100%',
|
||||||
|
[theme.breakpoints.up('md')]: {
|
||||||
|
width: '20ch',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
export default function AddServer({
|
||||||
|
setAlertInfo,
|
||||||
|
servers,
|
||||||
|
setServers,
|
||||||
|
}: {
|
||||||
|
setAlertInfo: React.Dispatch<React.SetStateAction<AlertResponse>>;
|
||||||
|
servers: ServerData[];
|
||||||
|
setServers: React.Dispatch<React.SetStateAction<ServerData[]>>;
|
||||||
|
}) {
|
||||||
|
const containerRef = useRef<HTMLElement>(null);
|
||||||
|
const inputRef = useRef<HTMLInputElement>(null);
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
const [showAddServer, setShowAddServer] = useState(false);
|
||||||
|
const toggleShowAddServer = () => {
|
||||||
|
setShowAddServer((prev) => !prev);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSubmit = async (event: React.KeyboardEvent<HTMLFormElement>) => {
|
||||||
|
event.preventDefault();
|
||||||
|
if (inputRef.current) {
|
||||||
|
const inputText = inputRef.current.value;
|
||||||
|
setIsLoading(true);
|
||||||
|
const response = await postData(inputText);
|
||||||
|
if (response.success) {
|
||||||
|
setAlertInfo({
|
||||||
|
message: response.message,
|
||||||
|
severity: 'success',
|
||||||
|
});
|
||||||
|
if (response.data && typeof response.data === 'object') {
|
||||||
|
setServers([response.data, ...servers]);
|
||||||
|
}
|
||||||
|
if (inputRef.current) {
|
||||||
|
inputRef.current.value = '';
|
||||||
|
toggleShowAddServer();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
setAlertInfo({
|
||||||
|
message: response.message || 'Failed to send data to backend.',
|
||||||
|
severity: 'error',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleKeyUp = async (event: React.KeyboardEvent<HTMLInputElement>) => {
|
||||||
|
switch (event.key) {
|
||||||
|
case 'Escape': {
|
||||||
|
if (inputRef.current) {
|
||||||
|
if (inputRef.current.value === '') {
|
||||||
|
toggleShowAddServer();
|
||||||
|
} else {
|
||||||
|
inputRef.current.value = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Box ref={containerRef} className="overflow-hidden pl-4">
|
||||||
|
<Slide
|
||||||
|
in={showAddServer}
|
||||||
|
direction="left"
|
||||||
|
container={containerRef.current}
|
||||||
|
>
|
||||||
|
<AddServerInput>
|
||||||
|
<form onSubmit={handleSubmit}>
|
||||||
|
<StyledInputBase
|
||||||
|
inputRef={inputRef}
|
||||||
|
placeholder="URL"
|
||||||
|
inputProps={{ 'aria-label': 'add-server' }}
|
||||||
|
onKeyUp={handleKeyUp}
|
||||||
|
disabled={isLoading}
|
||||||
|
endAdornment={
|
||||||
|
isLoading && (
|
||||||
|
<CircularProgress
|
||||||
|
size={20}
|
||||||
|
sx={{
|
||||||
|
position: 'absolute',
|
||||||
|
right: '10px',
|
||||||
|
color: (theme) => alpha(theme.palette.grey[500], 0.5),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</form>
|
||||||
|
</AddServerInput>
|
||||||
|
</Slide>
|
||||||
|
</Box>
|
||||||
|
<Tooltip title="Add a server." arrow disableInteractive>
|
||||||
|
<IconButton
|
||||||
|
onClick={() => {
|
||||||
|
toggleShowAddServer();
|
||||||
|
setTimeout(() => {
|
||||||
|
if (inputRef.current) {
|
||||||
|
inputRef.current.focus();
|
||||||
|
}
|
||||||
|
}, 300);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Add />
|
||||||
|
</IconButton>
|
||||||
|
</Tooltip>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,32 @@
|
||||||
|
import { Alert, Box, Collapse } from '@mui/material';
|
||||||
|
|
||||||
|
export type AlertResponse = {
|
||||||
|
message: string;
|
||||||
|
severity: 'error' | 'warning' | 'info' | 'success';
|
||||||
|
} | null;
|
||||||
|
|
||||||
|
export default function AlertComponent({
|
||||||
|
alertInfo,
|
||||||
|
setAlertInfo,
|
||||||
|
}: {
|
||||||
|
alertInfo: AlertResponse;
|
||||||
|
setAlertInfo: React.Dispatch<React.SetStateAction<AlertResponse>>;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<Box className="m-0">
|
||||||
|
{alertInfo && (
|
||||||
|
<Collapse in>
|
||||||
|
<Alert
|
||||||
|
severity={alertInfo.severity}
|
||||||
|
onClose={() => setAlertInfo(null)}
|
||||||
|
variant="filled"
|
||||||
|
className="py-0"
|
||||||
|
square
|
||||||
|
>
|
||||||
|
{alertInfo.message}
|
||||||
|
</Alert>
|
||||||
|
</Collapse>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,16 @@
|
||||||
|
import { Card, CardContent, Typography } from '@mui/material';
|
||||||
|
|
||||||
|
export default function ErrorCard({ error }: { error: string }) {
|
||||||
|
return (
|
||||||
|
<Card className="mb-2">
|
||||||
|
<CardContent className="p-4">
|
||||||
|
<Typography align="center" variant="h6">
|
||||||
|
¯\_(ツ)_/¯
|
||||||
|
</Typography>
|
||||||
|
<Typography align="center" variant="subtitle1">
|
||||||
|
{error}
|
||||||
|
</Typography>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,32 @@
|
||||||
|
import { Brightness4, Brightness7, QuestionMark } from '@mui/icons-material';
|
||||||
|
import { AppBar, Box, Toolbar, Tooltip, useTheme } from '@mui/material';
|
||||||
|
import IconButton from '@mui/material/IconButton';
|
||||||
|
import { useContext } from 'react';
|
||||||
|
import { Link } from 'react-router-dom';
|
||||||
|
import { ThemeContext } from '../context/ThemeContext';
|
||||||
|
|
||||||
|
function scrollToTop() {
|
||||||
|
window.scrollTo({ top: 0, behavior: 'smooth' }); // Scrolls to the top of the page smoothly
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function Footer() {
|
||||||
|
const theme = useTheme();
|
||||||
|
const { switchColorMode } = useContext(ThemeContext);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box className="bottom-0 w-full flex-none">
|
||||||
|
<AppBar position="relative">
|
||||||
|
<Toolbar className="min-h-min justify-end">
|
||||||
|
<Tooltip title="About" arrow disableInteractive>
|
||||||
|
<IconButton component={Link} to="/about" onClick={scrollToTop}>
|
||||||
|
<QuestionMark />
|
||||||
|
</IconButton>
|
||||||
|
</Tooltip>
|
||||||
|
<IconButton onClick={switchColorMode}>
|
||||||
|
{theme.palette.mode === 'dark' ? <Brightness7 /> : <Brightness4 />}
|
||||||
|
</IconButton>
|
||||||
|
</Toolbar>
|
||||||
|
</AppBar>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,115 @@
|
||||||
|
import { Refresh, Search } from '@mui/icons-material';
|
||||||
|
import {
|
||||||
|
AppBar,
|
||||||
|
Box,
|
||||||
|
LinearProgress,
|
||||||
|
Toolbar,
|
||||||
|
Tooltip,
|
||||||
|
Typography,
|
||||||
|
} from '@mui/material';
|
||||||
|
import IconButton from '@mui/material/IconButton';
|
||||||
|
import { useState } from 'react';
|
||||||
|
import { Link } from 'react-router-dom';
|
||||||
|
import { fetchData, fetchDemo } from '../apiUtil';
|
||||||
|
import SearchState from '../data/SearchState';
|
||||||
|
import ServerData from '../data/ServerData';
|
||||||
|
import AddServer from './AddServer';
|
||||||
|
import { AlertResponse } from './Alert';
|
||||||
|
import SearchServers from './SearchServers';
|
||||||
|
|
||||||
|
export default function Header({
|
||||||
|
setAlertInfo,
|
||||||
|
servers,
|
||||||
|
setServers,
|
||||||
|
searchState,
|
||||||
|
}: {
|
||||||
|
setAlertInfo: React.Dispatch<React.SetStateAction<AlertResponse>>;
|
||||||
|
servers: ServerData[];
|
||||||
|
setServers: React.Dispatch<React.SetStateAction<ServerData[]>>;
|
||||||
|
searchState: SearchState;
|
||||||
|
}) {
|
||||||
|
const [fetchLoading, setFetchLoading] = useState(0);
|
||||||
|
const [showSearchServer, setShowSearchServer] = useState(false);
|
||||||
|
const toggleShowSearchServer = () => {
|
||||||
|
setShowSearchServer((prev) => !prev);
|
||||||
|
};
|
||||||
|
|
||||||
|
const fetchServerData = async () => {
|
||||||
|
let data: ServerData[] = [];
|
||||||
|
try {
|
||||||
|
setFetchLoading(25);
|
||||||
|
data = await fetchData();
|
||||||
|
} catch (err) {
|
||||||
|
if (err instanceof Error) {
|
||||||
|
setAlertInfo({
|
||||||
|
message: err.message,
|
||||||
|
severity: 'error',
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
setAlertInfo({
|
||||||
|
message: 'An unknown error occurred.',
|
||||||
|
severity: 'error',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
setFetchLoading(50);
|
||||||
|
if (data.length === 0) {
|
||||||
|
data.push(await fetchDemo());
|
||||||
|
}
|
||||||
|
setFetchLoading(75);
|
||||||
|
setServers(data);
|
||||||
|
setFetchLoading(100);
|
||||||
|
setTimeout(() => {
|
||||||
|
setFetchLoading(0);
|
||||||
|
}, 500);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box>
|
||||||
|
<AppBar position="static">
|
||||||
|
<Toolbar className="min-h-min py-1">
|
||||||
|
<Typography
|
||||||
|
component={Link}
|
||||||
|
to="/"
|
||||||
|
variant="h6"
|
||||||
|
sx={{
|
||||||
|
color: 'inherit',
|
||||||
|
textDecoration: 'none',
|
||||||
|
userSelect: 'none',
|
||||||
|
flexGrow: 1,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
IXION
|
||||||
|
</Typography>
|
||||||
|
<AddServer
|
||||||
|
setAlertInfo={setAlertInfo}
|
||||||
|
servers={servers}
|
||||||
|
setServers={setServers}
|
||||||
|
/>
|
||||||
|
<Tooltip arrow disableInteractive title="Refresh server data.">
|
||||||
|
<IconButton onClick={fetchServerData} disabled={fetchLoading !== 0}>
|
||||||
|
<Refresh />
|
||||||
|
</IconButton>
|
||||||
|
</Tooltip>
|
||||||
|
<Tooltip arrow disableInteractive title="Filter servers.">
|
||||||
|
<IconButton onClick={toggleShowSearchServer}>
|
||||||
|
<Search />
|
||||||
|
</IconButton>
|
||||||
|
</Tooltip>
|
||||||
|
</Toolbar>
|
||||||
|
</AppBar>
|
||||||
|
<LinearProgress
|
||||||
|
variant="determinate"
|
||||||
|
value={fetchLoading}
|
||||||
|
sx={{
|
||||||
|
visibility: fetchLoading === 0 ? 'hidden' : 'visible',
|
||||||
|
height: fetchLoading === 0 ? 0 : '1px',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<SearchServers
|
||||||
|
showSearchServer={showSearchServer}
|
||||||
|
searchState={searchState}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,429 @@
|
||||||
|
import {
|
||||||
|
Container,
|
||||||
|
Grid,
|
||||||
|
Input,
|
||||||
|
Slider,
|
||||||
|
TextField,
|
||||||
|
ToggleButton,
|
||||||
|
ToggleButtonGroup,
|
||||||
|
Tooltip,
|
||||||
|
Typography,
|
||||||
|
} from '@mui/material';
|
||||||
|
import { useEffect, useRef, useState } from 'react';
|
||||||
|
import SearchState from '../data/SearchState';
|
||||||
|
|
||||||
|
export default function SearchServers({
|
||||||
|
showSearchServer,
|
||||||
|
searchState,
|
||||||
|
}: {
|
||||||
|
showSearchServer: boolean;
|
||||||
|
searchState: SearchState;
|
||||||
|
}) {
|
||||||
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
|
const [contentHeight, setContentHeight] = useState(0);
|
||||||
|
const minDistance = 0;
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (showSearchServer && containerRef.current) {
|
||||||
|
setContentHeight(containerRef.current.scrollHeight);
|
||||||
|
} else {
|
||||||
|
setContentHeight(0);
|
||||||
|
}
|
||||||
|
}, [showSearchServer]);
|
||||||
|
|
||||||
|
const handleSearchName = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
searchState.name.setValue(event.target.value);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSearchMultibox = (
|
||||||
|
_event: React.MouseEvent<HTMLElement>,
|
||||||
|
newSearchMultibox: string[]
|
||||||
|
) => {
|
||||||
|
searchState.multibox.setValue(newSearchMultibox);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSearchTrusts = (
|
||||||
|
_event: React.MouseEvent<HTMLElement>,
|
||||||
|
newSearchTrusts: string[]
|
||||||
|
) => {
|
||||||
|
searchState.trusts.setValue(newSearchTrusts);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSearchLevelSync = (
|
||||||
|
_event: React.MouseEvent<HTMLElement>,
|
||||||
|
newSearchLevelSync: string[]
|
||||||
|
) => {
|
||||||
|
searchState.levelSync.setValue(newSearchLevelSync);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSearchMaxLevelMin = (
|
||||||
|
event: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>
|
||||||
|
) => {
|
||||||
|
let newValue = parseInt(event.target.value, 10);
|
||||||
|
if (Number.isNaN(newValue) || newValue < 1) {
|
||||||
|
newValue = 1;
|
||||||
|
} else if (newValue > 99) {
|
||||||
|
newValue = 99;
|
||||||
|
}
|
||||||
|
searchState.maxLevel.setValue([
|
||||||
|
Math.min(newValue, searchState.maxLevel.value[1] - minDistance),
|
||||||
|
searchState.maxLevel.value[1],
|
||||||
|
]);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSearchMaxLevelMax = (
|
||||||
|
event: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>
|
||||||
|
) => {
|
||||||
|
let newValue = parseInt(event.target.value, 10);
|
||||||
|
if (Number.isNaN(newValue) || newValue < 1) {
|
||||||
|
newValue = 1;
|
||||||
|
} else if (newValue > 99) {
|
||||||
|
newValue = 99;
|
||||||
|
}
|
||||||
|
searchState.maxLevel.setValue([
|
||||||
|
searchState.maxLevel.value[0],
|
||||||
|
Math.max(newValue, searchState.maxLevel.value[0] + minDistance),
|
||||||
|
]);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSearchMaxLevel = (
|
||||||
|
_event: Event,
|
||||||
|
newValue: number | number[],
|
||||||
|
activeThumb: number
|
||||||
|
) => {
|
||||||
|
if (!Array.isArray(newValue)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (activeThumb === 0) {
|
||||||
|
searchState.maxLevel.setValue([
|
||||||
|
Math.min(newValue[0], searchState.maxLevel.value[1] - minDistance),
|
||||||
|
searchState.maxLevel.value[1],
|
||||||
|
]);
|
||||||
|
} else {
|
||||||
|
searchState.maxLevel.setValue([
|
||||||
|
searchState.maxLevel.value[0],
|
||||||
|
Math.max(newValue[1], searchState.maxLevel.value[0] + minDistance),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSearchExpansions = (
|
||||||
|
_event: React.MouseEvent<HTMLElement>,
|
||||||
|
newSearchExpansions: string[]
|
||||||
|
) => {
|
||||||
|
if (newSearchExpansions.length === 0) {
|
||||||
|
searchState.expansions.setValue(null);
|
||||||
|
} else if (
|
||||||
|
searchState.expansions.value &&
|
||||||
|
searchState.expansions.value.includes('none') &&
|
||||||
|
newSearchExpansions.length > 1
|
||||||
|
) {
|
||||||
|
searchState.expansions.setValue(
|
||||||
|
newSearchExpansions.filter((item) => item !== 'none')
|
||||||
|
);
|
||||||
|
} else if (
|
||||||
|
searchState.expansions.value &&
|
||||||
|
!searchState.expansions.value.includes('none') &&
|
||||||
|
newSearchExpansions.includes('none')
|
||||||
|
) {
|
||||||
|
searchState.expansions.setValue(['none']);
|
||||||
|
} else {
|
||||||
|
searchState.expansions.setValue(newSearchExpansions);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Container
|
||||||
|
ref={containerRef}
|
||||||
|
className="overflow-hidden"
|
||||||
|
sx={{
|
||||||
|
transition: 'all 0.3s ease',
|
||||||
|
maxHeight: showSearchServer ? contentHeight : 0,
|
||||||
|
backgroundColor: (theme) =>
|
||||||
|
theme.palette.mode === 'dark'
|
||||||
|
? 'rgba(255, 255, 255, .06)'
|
||||||
|
: 'rgba(0, 0, 0, .06)',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Grid container spacing={2} className="py-2">
|
||||||
|
{/* Name */}
|
||||||
|
<Grid item xs={12}>
|
||||||
|
<TextField
|
||||||
|
value={searchState.name.value}
|
||||||
|
onChange={handleSearchName}
|
||||||
|
variant="standard"
|
||||||
|
autoComplete="false"
|
||||||
|
fullWidth
|
||||||
|
label="Name"
|
||||||
|
size="small"
|
||||||
|
className="pb-2"
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
{/* Max Level */}
|
||||||
|
<Grid
|
||||||
|
item
|
||||||
|
xs={12}
|
||||||
|
className="pt-0"
|
||||||
|
display="flex"
|
||||||
|
flexDirection="column"
|
||||||
|
alignItems="center"
|
||||||
|
>
|
||||||
|
<Typography id="max-level-slider" variant="caption">
|
||||||
|
Max Level
|
||||||
|
</Typography>
|
||||||
|
</Grid>
|
||||||
|
<Grid item xs={12} className="flex pt-0">
|
||||||
|
<Input
|
||||||
|
value={searchState.maxLevel.value[0]}
|
||||||
|
size="small"
|
||||||
|
onChange={handleSearchMaxLevelMin}
|
||||||
|
// onBlur={handleBlur}
|
||||||
|
inputProps={{
|
||||||
|
step: 1,
|
||||||
|
min: 1,
|
||||||
|
max: 99,
|
||||||
|
type: 'number',
|
||||||
|
'aria-labelledby': 'max-level-min',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Slider
|
||||||
|
aria-labelledby="max-level-slider"
|
||||||
|
value={searchState.maxLevel.value}
|
||||||
|
onChange={handleSearchMaxLevel}
|
||||||
|
valueLabelDisplay="auto"
|
||||||
|
disableSwap
|
||||||
|
className="mx-4"
|
||||||
|
size="small"
|
||||||
|
min={1}
|
||||||
|
max={99}
|
||||||
|
/>
|
||||||
|
<Input
|
||||||
|
value={searchState.maxLevel.value[1]}
|
||||||
|
size="small"
|
||||||
|
onChange={handleSearchMaxLevelMax}
|
||||||
|
// onBlur={handleBlur}
|
||||||
|
inputProps={{
|
||||||
|
step: 1,
|
||||||
|
min: 1,
|
||||||
|
max: 99,
|
||||||
|
type: 'number',
|
||||||
|
'aria-labelledby': 'max-level-max',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
{/* Expansion */}
|
||||||
|
<Grid
|
||||||
|
item
|
||||||
|
xs={12}
|
||||||
|
display="flex"
|
||||||
|
flexDirection="column"
|
||||||
|
alignItems="center"
|
||||||
|
>
|
||||||
|
<Tooltip
|
||||||
|
title="Enabled expansions, exact match."
|
||||||
|
arrow
|
||||||
|
disableInteractive
|
||||||
|
placement="top"
|
||||||
|
>
|
||||||
|
<Typography id="expansions-button-group" variant="caption">
|
||||||
|
Expansions
|
||||||
|
</Typography>
|
||||||
|
</Tooltip>
|
||||||
|
<ToggleButtonGroup
|
||||||
|
value={searchState.expansions.value}
|
||||||
|
onChange={handleSearchExpansions}
|
||||||
|
size="small"
|
||||||
|
aria-labelledby="expansions-button-group"
|
||||||
|
sx={{
|
||||||
|
display: 'flex',
|
||||||
|
width: '100%',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ToggleButton
|
||||||
|
key="none"
|
||||||
|
value="none"
|
||||||
|
aria-label="none-enabled"
|
||||||
|
className="py-0"
|
||||||
|
sx={{ width: '20%' }}
|
||||||
|
>
|
||||||
|
None
|
||||||
|
</ToggleButton>
|
||||||
|
<ToggleButton
|
||||||
|
key="rotz"
|
||||||
|
value="rotz"
|
||||||
|
aria-label="rotz-enabled"
|
||||||
|
className="py-0"
|
||||||
|
sx={{ width: '20%' }}
|
||||||
|
>
|
||||||
|
RotZ
|
||||||
|
</ToggleButton>
|
||||||
|
<ToggleButton
|
||||||
|
key="cop"
|
||||||
|
value="cop"
|
||||||
|
aria-label="cop-enabled"
|
||||||
|
className="py-0"
|
||||||
|
sx={{ width: '20%' }}
|
||||||
|
>
|
||||||
|
CoP
|
||||||
|
</ToggleButton>
|
||||||
|
<ToggleButton
|
||||||
|
key="toau"
|
||||||
|
value="toau"
|
||||||
|
aria-label="toau-enabled"
|
||||||
|
className="py-0"
|
||||||
|
sx={{ width: '20%' }}
|
||||||
|
>
|
||||||
|
ToAU
|
||||||
|
</ToggleButton>
|
||||||
|
<ToggleButton
|
||||||
|
key="wotg"
|
||||||
|
value="wotg"
|
||||||
|
aria-label="wotg-enabled"
|
||||||
|
className="py-0"
|
||||||
|
sx={{ width: '20%' }}
|
||||||
|
>
|
||||||
|
WotG
|
||||||
|
</ToggleButton>
|
||||||
|
<ToggleButton
|
||||||
|
key="soa"
|
||||||
|
value="soa"
|
||||||
|
aria-label="soa-enabled"
|
||||||
|
className="py-0"
|
||||||
|
sx={{ width: '20%' }}
|
||||||
|
>
|
||||||
|
SoA
|
||||||
|
</ToggleButton>
|
||||||
|
</ToggleButtonGroup>
|
||||||
|
</Grid>
|
||||||
|
{/* Multiboxing */}
|
||||||
|
<Grid
|
||||||
|
item
|
||||||
|
xs={12}
|
||||||
|
display="flex"
|
||||||
|
flexDirection="column"
|
||||||
|
alignItems="center"
|
||||||
|
>
|
||||||
|
<Typography id="multibox-button-group" variant="caption">
|
||||||
|
Multiboxing
|
||||||
|
</Typography>
|
||||||
|
<ToggleButtonGroup
|
||||||
|
value={searchState.multibox.value}
|
||||||
|
onChange={handleSearchMultibox}
|
||||||
|
size="small"
|
||||||
|
aria-labelledby="multibox-button-group"
|
||||||
|
sx={{
|
||||||
|
display: 'flex',
|
||||||
|
width: '100%',
|
||||||
|
}}
|
||||||
|
exclusive
|
||||||
|
>
|
||||||
|
<ToggleButton
|
||||||
|
value="none"
|
||||||
|
aria-label="no multiboxing"
|
||||||
|
className="py-0"
|
||||||
|
sx={{ width: '33.33%' }}
|
||||||
|
>
|
||||||
|
None
|
||||||
|
</ToggleButton>
|
||||||
|
<ToggleButton
|
||||||
|
value="limited"
|
||||||
|
aria-label="limited multiboxing"
|
||||||
|
className="py-0"
|
||||||
|
sx={{ width: '33.33%' }}
|
||||||
|
>
|
||||||
|
Limited
|
||||||
|
</ToggleButton>
|
||||||
|
<ToggleButton
|
||||||
|
value="unlimited"
|
||||||
|
aria-label="unlimited multiboxing"
|
||||||
|
className="py-0"
|
||||||
|
sx={{ width: '33.33%' }}
|
||||||
|
>
|
||||||
|
Unlimited
|
||||||
|
</ToggleButton>
|
||||||
|
</ToggleButtonGroup>
|
||||||
|
</Grid>
|
||||||
|
{/* Trusts */}
|
||||||
|
<Grid
|
||||||
|
item
|
||||||
|
xs={6}
|
||||||
|
display="flex"
|
||||||
|
flexDirection="column"
|
||||||
|
alignItems="center"
|
||||||
|
>
|
||||||
|
<Typography id="trust-button-group" variant="caption">
|
||||||
|
Trusts
|
||||||
|
</Typography>
|
||||||
|
<ToggleButtonGroup
|
||||||
|
value={searchState.trusts.value}
|
||||||
|
onChange={handleSearchTrusts}
|
||||||
|
size="small"
|
||||||
|
aria-labelledby="trust-button-group"
|
||||||
|
exclusive
|
||||||
|
sx={{
|
||||||
|
display: 'flex',
|
||||||
|
width: '100%',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ToggleButton
|
||||||
|
value="disabled"
|
||||||
|
aria-label="trust disabled"
|
||||||
|
className="py-0"
|
||||||
|
sx={{ width: '50%' }}
|
||||||
|
>
|
||||||
|
Disabled
|
||||||
|
</ToggleButton>
|
||||||
|
<ToggleButton
|
||||||
|
value="enabled"
|
||||||
|
aria-label="trust enabled"
|
||||||
|
className="py-0"
|
||||||
|
sx={{ width: '50%' }}
|
||||||
|
>
|
||||||
|
Enabled
|
||||||
|
</ToggleButton>
|
||||||
|
</ToggleButtonGroup>
|
||||||
|
</Grid>
|
||||||
|
{/* Level Sync */}
|
||||||
|
<Grid
|
||||||
|
item
|
||||||
|
xs={6}
|
||||||
|
display="flex"
|
||||||
|
flexDirection="column"
|
||||||
|
alignItems="center"
|
||||||
|
>
|
||||||
|
<Typography id="level-sync-button-group" variant="caption">
|
||||||
|
Level Sync
|
||||||
|
</Typography>
|
||||||
|
<ToggleButtonGroup
|
||||||
|
value={searchState.levelSync.value}
|
||||||
|
onChange={handleSearchLevelSync}
|
||||||
|
size="small"
|
||||||
|
aria-labelledby="level-sync-button-group"
|
||||||
|
exclusive
|
||||||
|
sx={{
|
||||||
|
display: 'flex',
|
||||||
|
width: '100%',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ToggleButton
|
||||||
|
value="disabled"
|
||||||
|
aria-label="level-sync-disabled"
|
||||||
|
className="py-0"
|
||||||
|
sx={{ width: '50%' }}
|
||||||
|
>
|
||||||
|
Disabled
|
||||||
|
</ToggleButton>
|
||||||
|
<ToggleButton
|
||||||
|
value="enabled"
|
||||||
|
aria-label="level-sync-enabled"
|
||||||
|
className="py-0"
|
||||||
|
sx={{ width: '50%' }}
|
||||||
|
>
|
||||||
|
Enabled
|
||||||
|
</ToggleButton>
|
||||||
|
</ToggleButtonGroup>
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
</Container>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,338 @@
|
||||||
|
import {
|
||||||
|
ContentCopy,
|
||||||
|
ExpandMore,
|
||||||
|
Launch,
|
||||||
|
Public,
|
||||||
|
PublicOff,
|
||||||
|
Warning,
|
||||||
|
} from '@mui/icons-material';
|
||||||
|
import {
|
||||||
|
Box,
|
||||||
|
Card,
|
||||||
|
CardContent,
|
||||||
|
Divider,
|
||||||
|
IconButton,
|
||||||
|
Table,
|
||||||
|
TableBody,
|
||||||
|
TableCell,
|
||||||
|
TableContainer,
|
||||||
|
TableRow,
|
||||||
|
ToggleButton,
|
||||||
|
ToggleButtonGroup,
|
||||||
|
Tooltip,
|
||||||
|
Typography,
|
||||||
|
alpha,
|
||||||
|
} from '@mui/material';
|
||||||
|
import { useState } from 'react';
|
||||||
|
import { Link } from 'react-router-dom';
|
||||||
|
import ServerData, { ServerSettingsInfo } from '../data/ServerData';
|
||||||
|
import { Accordion, AccordionDetails, AccordionSummary } from './Accordion';
|
||||||
|
|
||||||
|
export default function ServerCard({ server }: { server: ServerData }) {
|
||||||
|
const [clipboardTooltip, setClipboardTooltip] = useState('Copy server URL.');
|
||||||
|
const [clipboardTooltipOpen, setClipboardTooltipOpen] = useState(false);
|
||||||
|
const handleClipboardTooltipClose = () => {
|
||||||
|
setClipboardTooltipOpen(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleClipboardTooltipOpen = () => {
|
||||||
|
setClipboardTooltipOpen(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const copyServerUrlToClipboard = async (text: string) => {
|
||||||
|
try {
|
||||||
|
navigator.clipboard.writeText(text);
|
||||||
|
setClipboardTooltip('Copied server URL!');
|
||||||
|
handleClipboardTooltipOpen();
|
||||||
|
setTimeout(() => {
|
||||||
|
handleClipboardTooltipClose();
|
||||||
|
setClipboardTooltip('Copy Server URL.');
|
||||||
|
}, 3000);
|
||||||
|
} catch (error) {
|
||||||
|
handleClipboardTooltipOpen();
|
||||||
|
if (error instanceof Error) {
|
||||||
|
setClipboardTooltip(error.message);
|
||||||
|
} else {
|
||||||
|
setClipboardTooltip('Something went wrong!');
|
||||||
|
}
|
||||||
|
setTimeout(() => {
|
||||||
|
handleClipboardTooltipClose();
|
||||||
|
setClipboardTooltip('Copy Server URL.');
|
||||||
|
}, 3000);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
function formatExternalUrl(url: string): string {
|
||||||
|
// Check if the URL starts with a valid protocol
|
||||||
|
if (!/^https?:\/\//i.test(url)) {
|
||||||
|
// If not, prepend 'https://' to the URL
|
||||||
|
return `https://${url}`;
|
||||||
|
}
|
||||||
|
// Otherwise, return the original URL
|
||||||
|
return url;
|
||||||
|
}
|
||||||
|
|
||||||
|
const expansions = [
|
||||||
|
<ToggleButton
|
||||||
|
key="rotz"
|
||||||
|
value="rotz"
|
||||||
|
aria-label="rotz-enabled"
|
||||||
|
className="py-0"
|
||||||
|
disabled
|
||||||
|
>
|
||||||
|
RotZ
|
||||||
|
</ToggleButton>,
|
||||||
|
<ToggleButton
|
||||||
|
key="cop"
|
||||||
|
value="cop"
|
||||||
|
aria-label="cop-enabled"
|
||||||
|
className="py-0"
|
||||||
|
disabled
|
||||||
|
>
|
||||||
|
CoP
|
||||||
|
</ToggleButton>,
|
||||||
|
<ToggleButton
|
||||||
|
key="toau"
|
||||||
|
value="toau"
|
||||||
|
aria-label="toau-enabled"
|
||||||
|
className="py-0"
|
||||||
|
disabled
|
||||||
|
>
|
||||||
|
ToAU
|
||||||
|
</ToggleButton>,
|
||||||
|
<ToggleButton
|
||||||
|
key="wotg"
|
||||||
|
value="wotg"
|
||||||
|
aria-label="wotg-enabled"
|
||||||
|
className="py-0"
|
||||||
|
disabled
|
||||||
|
>
|
||||||
|
WotG
|
||||||
|
</ToggleButton>,
|
||||||
|
<ToggleButton
|
||||||
|
key="soa"
|
||||||
|
value="soa"
|
||||||
|
aria-label="soa-enabled"
|
||||||
|
className="py-0"
|
||||||
|
disabled
|
||||||
|
>
|
||||||
|
SoA
|
||||||
|
</ToggleButton>,
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card className="mb-2">
|
||||||
|
<Accordion className="my-0">
|
||||||
|
<AccordionSummary expandIcon={<ExpandMore />}>
|
||||||
|
<Box className="flex items-center justify-center">
|
||||||
|
{server.settings['LOGIN.MAINT_MODE'] === 1 ? (
|
||||||
|
<Tooltip
|
||||||
|
arrow
|
||||||
|
disableInteractive
|
||||||
|
title="Server is undergoing maintenance."
|
||||||
|
>
|
||||||
|
<Warning color="warning" />
|
||||||
|
</Tooltip>
|
||||||
|
) : (
|
||||||
|
<div>
|
||||||
|
{server.inactivity_counter > 0 ? (
|
||||||
|
<Tooltip
|
||||||
|
arrow
|
||||||
|
disableInteractive
|
||||||
|
title={`Offline for ${server.inactivity_counter} hours.`}
|
||||||
|
>
|
||||||
|
<PublicOff color="error" />
|
||||||
|
</Tooltip>
|
||||||
|
) : (
|
||||||
|
<Tooltip arrow disableInteractive title="Server is online.">
|
||||||
|
<Public color="success" />
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
<CardContent className="grow py-1">
|
||||||
|
<Box className="flex content-center">
|
||||||
|
<Typography
|
||||||
|
variant="h5"
|
||||||
|
color={(theme) => alpha(theme.palette.text.primary, 0.87)}
|
||||||
|
sx={{ lineHeight: 1.0 }}
|
||||||
|
>
|
||||||
|
{server.settings['MAIN.SERVER_NAME']}
|
||||||
|
</Typography>
|
||||||
|
{typeof server.settings['API.WEBSITE'] === 'string' &&
|
||||||
|
server.settings['API.WEBSITE'] !== '' && (
|
||||||
|
<Tooltip arrow disableInteractive title="Visit website.">
|
||||||
|
<IconButton
|
||||||
|
component={Link}
|
||||||
|
to={formatExternalUrl(server.settings['API.WEBSITE'])}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener"
|
||||||
|
size="small"
|
||||||
|
className="py-0"
|
||||||
|
onClick={(event) => {
|
||||||
|
event.stopPropagation();
|
||||||
|
}}
|
||||||
|
disableRipple
|
||||||
|
>
|
||||||
|
<Launch
|
||||||
|
sx={{
|
||||||
|
fontSize: (theme) =>
|
||||||
|
theme.typography.caption.fontSize,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</IconButton>
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
<Box className="flex content-center">
|
||||||
|
<Typography
|
||||||
|
variant="subtitle2"
|
||||||
|
color={(theme) => alpha(theme.palette.text.primary, 0.6)}
|
||||||
|
>
|
||||||
|
{server.url}
|
||||||
|
</Typography>
|
||||||
|
{window.isSecureContext && (
|
||||||
|
<Tooltip
|
||||||
|
arrow
|
||||||
|
disableInteractive
|
||||||
|
title={clipboardTooltip}
|
||||||
|
open={
|
||||||
|
clipboardTooltip === 'Copied server URL!' ||
|
||||||
|
clipboardTooltipOpen
|
||||||
|
}
|
||||||
|
onOpen={handleClipboardTooltipOpen}
|
||||||
|
onClose={handleClipboardTooltipClose}
|
||||||
|
>
|
||||||
|
<IconButton
|
||||||
|
size="small"
|
||||||
|
className="py-0"
|
||||||
|
onClick={(event) => {
|
||||||
|
event.stopPropagation();
|
||||||
|
copyServerUrlToClipboard(server.url);
|
||||||
|
}}
|
||||||
|
disableRipple
|
||||||
|
>
|
||||||
|
<ContentCopy
|
||||||
|
sx={{
|
||||||
|
fontSize: (theme) => theme.typography.caption.fontSize,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</IconButton>
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
<ToggleButtonGroup
|
||||||
|
size="small"
|
||||||
|
value={[
|
||||||
|
server.settings['LOGIN.RISE_OF_ZILART'] && 'rotz',
|
||||||
|
server.settings['LOGIN.CHAINS_OF_PROMATHIA'] && 'cop',
|
||||||
|
server.settings['LOGIN.TREASURES_OF_AHT_URGHAN'] && 'toau',
|
||||||
|
server.settings['LOGIN.WINGS_OF_THE_GODDESS'] && 'wotg',
|
||||||
|
server.settings['LOGIN.SEEKERS_OF_ADOULIN'] && 'soa',
|
||||||
|
]}
|
||||||
|
sx={{ '& button': { lineHeight: 1.0 } }}
|
||||||
|
>
|
||||||
|
{expansions}
|
||||||
|
</ToggleButtonGroup>
|
||||||
|
</CardContent>
|
||||||
|
<Box className="flex items-center justify-center">
|
||||||
|
<Typography
|
||||||
|
variant="h5"
|
||||||
|
color={(theme) => theme.palette.text.secondary}
|
||||||
|
>
|
||||||
|
{`Lv.${server.settings['MAIN.MAX_LEVEL']}`}
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
</AccordionSummary>
|
||||||
|
<AccordionDetails className="p-2">
|
||||||
|
<TableContainer>
|
||||||
|
<Table size="small">
|
||||||
|
<TableBody>
|
||||||
|
<TableRow>
|
||||||
|
<TableCell>
|
||||||
|
<Tooltip
|
||||||
|
title={
|
||||||
|
ServerSettingsInfo['LOGIN.LOGIN_LIMIT'].description
|
||||||
|
}
|
||||||
|
arrow
|
||||||
|
disableInteractive
|
||||||
|
>
|
||||||
|
<Typography variant="caption" sx={{ userSelect: 'none' }}>
|
||||||
|
{`${ServerSettingsInfo['LOGIN.LOGIN_LIMIT'].name}: `}
|
||||||
|
{server.settings['LOGIN.LOGIN_LIMIT'] === 0
|
||||||
|
? 'unlimited'
|
||||||
|
: server.settings['LOGIN.LOGIN_LIMIT']}
|
||||||
|
</Typography>
|
||||||
|
</Tooltip>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<Tooltip
|
||||||
|
title={
|
||||||
|
ServerSettingsInfo['MAIN.ENABLE_TRUST_CASTING']
|
||||||
|
.description
|
||||||
|
}
|
||||||
|
arrow
|
||||||
|
disableInteractive
|
||||||
|
>
|
||||||
|
<Typography variant="caption" sx={{ userSelect: 'none' }}>
|
||||||
|
{`${ServerSettingsInfo['MAIN.ENABLE_TRUST_CASTING'].name}: `}
|
||||||
|
{server.settings['MAIN.ENABLE_TRUST_CASTING'] === 1
|
||||||
|
? 'Enabled'
|
||||||
|
: 'Disabled'}
|
||||||
|
</Typography>
|
||||||
|
</Tooltip>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<Tooltip
|
||||||
|
title={
|
||||||
|
ServerSettingsInfo['MAP.LEVEL_SYNC_ENABLE'].description
|
||||||
|
}
|
||||||
|
arrow
|
||||||
|
disableInteractive
|
||||||
|
>
|
||||||
|
<Typography variant="caption" sx={{ userSelect: 'none' }}>
|
||||||
|
{`${ServerSettingsInfo['MAP.LEVEL_SYNC_ENABLE'].name}: `}
|
||||||
|
{server.settings['MAP.LEVEL_SYNC_ENABLE']
|
||||||
|
? 'Enabled'
|
||||||
|
: 'Disabled'}
|
||||||
|
</Typography>
|
||||||
|
</Tooltip>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
<TableRow sx={{ '& > *': { borderBottom: 'unset' } }}>
|
||||||
|
<TableCell>
|
||||||
|
<Tooltip
|
||||||
|
title={ServerSettingsInfo['MAP.SPEED_MOD'].description}
|
||||||
|
arrow
|
||||||
|
disableInteractive
|
||||||
|
>
|
||||||
|
<Typography variant="caption" sx={{ userSelect: 'none' }}>
|
||||||
|
{`${ServerSettingsInfo['MAP.SPEED_MOD'].name}: `}
|
||||||
|
{typeof server.settings['MAP.SPEED_MOD'] === 'number'
|
||||||
|
? (server.settings['MAP.SPEED_MOD'] < 0 ? '' : '+') +
|
||||||
|
((50 + server.settings['MAP.SPEED_MOD']) / 50 - 1) *
|
||||||
|
100
|
||||||
|
: '???'}
|
||||||
|
%
|
||||||
|
</Typography>
|
||||||
|
</Tooltip>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</TableContainer>
|
||||||
|
</AccordionDetails>
|
||||||
|
</Accordion>
|
||||||
|
<Divider />
|
||||||
|
<CardContent className="flex justify-between py-1">
|
||||||
|
<Typography variant="caption" sx={{ userSelect: 'none' }}>
|
||||||
|
{server.active_sessions} active sessions
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="caption" sx={{ userSelect: 'none' }}>
|
||||||
|
Updated: {new Date(server.updated).toLocaleString()}
|
||||||
|
</Typography>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,73 @@
|
||||||
|
import { useMediaQuery } from '@mui/material';
|
||||||
|
import { createTheme, StyledEngineProvider } from '@mui/material/styles';
|
||||||
|
import ThemeProvider from '@mui/material/styles/ThemeProvider';
|
||||||
|
import { createContext, ReactNode, useMemo, useState } from 'react';
|
||||||
|
|
||||||
|
type ThemeContextType = {
|
||||||
|
switchColorMode: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
type ThemeProviderProps = {
|
||||||
|
children: ReactNode;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ThemeContext = createContext<ThemeContextType>({
|
||||||
|
switchColorMode: () => {},
|
||||||
|
});
|
||||||
|
|
||||||
|
export function ThemeContextProvider({ children }: ThemeProviderProps) {
|
||||||
|
const rootElement = document.getElementById('root');
|
||||||
|
const prefersDarkMode = useMediaQuery('(prefers-color-scheme: dark)');
|
||||||
|
const [mode, setMode] = useState<'light' | 'dark'>(
|
||||||
|
prefersDarkMode ? 'dark' : 'light'
|
||||||
|
);
|
||||||
|
const switchColorMode = () => {
|
||||||
|
setMode((prevMode) => (prevMode === 'light' ? 'dark' : 'light'));
|
||||||
|
};
|
||||||
|
const theme = useMemo(
|
||||||
|
() =>
|
||||||
|
createTheme({
|
||||||
|
palette: {
|
||||||
|
mode,
|
||||||
|
},
|
||||||
|
// All `Portal`-related components need to have the the main app wrapper element as a container
|
||||||
|
// so that they are in the subtree under the element used in the `important` option of the Tailwind's config.
|
||||||
|
components: {
|
||||||
|
MuiPopover: {
|
||||||
|
defaultProps: {
|
||||||
|
container: rootElement,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
MuiPopper: {
|
||||||
|
defaultProps: {
|
||||||
|
container: rootElement,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
MuiDialog: {
|
||||||
|
defaultProps: {
|
||||||
|
container: rootElement,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
MuiModal: {
|
||||||
|
defaultProps: {
|
||||||
|
container: rootElement,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
[rootElement, mode]
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<StyledEngineProvider injectFirst>
|
||||||
|
<ThemeContext.Provider
|
||||||
|
value={
|
||||||
|
// eslint-disable-next-line react/jsx-no-constructed-context-values
|
||||||
|
{ switchColorMode }
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<ThemeProvider theme={theme}>{children}</ThemeProvider>
|
||||||
|
</ThemeContext.Provider>
|
||||||
|
</StyledEngineProvider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,13 @@
|
||||||
|
interface SearchStateTemplate<T> {
|
||||||
|
value: T;
|
||||||
|
setValue: React.Dispatch<React.SetStateAction<T>>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default interface SearchState {
|
||||||
|
name: SearchStateTemplate<string>;
|
||||||
|
multibox: SearchStateTemplate<string[] | null>;
|
||||||
|
trusts: SearchStateTemplate<string[] | null>;
|
||||||
|
levelSync: SearchStateTemplate<string[] | null>;
|
||||||
|
maxLevel: SearchStateTemplate<number[]>;
|
||||||
|
expansions: SearchStateTemplate<string[] | null>;
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,301 @@
|
||||||
|
{
|
||||||
|
"LOGIN.ACCOUNT_CREATION": true,
|
||||||
|
"LOGIN.A_CRYSTALLINE_PROPHECY": true,
|
||||||
|
"LOGIN.A_MOOGLE_KUPOD_ETAT": true,
|
||||||
|
"LOGIN.A_SHANTOTTO_ASCENSION": true,
|
||||||
|
"LOGIN.CHAINS_OF_PROMATHIA": true,
|
||||||
|
"LOGIN.CHARACTER_CREATION": true,
|
||||||
|
"LOGIN.CHARACTER_DELETION": true,
|
||||||
|
"LOGIN.CLIENT_VER": "30240327_0",
|
||||||
|
"LOGIN.DISABLE_MOB_NPC_CHAR_NAMES": false,
|
||||||
|
"LOGIN.HEROES_OF_ABYSSEA": true,
|
||||||
|
"LOGIN.LOGIN_LIMIT": 0,
|
||||||
|
"LOGIN.LOG_USER_IP": false,
|
||||||
|
"LOGIN.MAINT_MODE": 0,
|
||||||
|
"LOGIN.MOG_WARDROBE_3": true,
|
||||||
|
"LOGIN.MOG_WARDROBE_4": true,
|
||||||
|
"LOGIN.MOG_WARDROBE_5": true,
|
||||||
|
"LOGIN.MOG_WARDROBE_6": true,
|
||||||
|
"LOGIN.MOG_WARDROBE_7": true,
|
||||||
|
"LOGIN.MOG_WARDROBE_8": true,
|
||||||
|
"LOGIN.RISE_OF_ZILART": true,
|
||||||
|
"LOGIN.SCARS_OF_ABYSSEA": true,
|
||||||
|
"LOGIN.SECURE_TOKEN": false,
|
||||||
|
"LOGIN.SEEKERS_OF_ADOULIN": true,
|
||||||
|
"LOGIN.TREASURES_OF_AHT_URGHAN": true,
|
||||||
|
"LOGIN.VER_LOCK": 2,
|
||||||
|
"LOGIN.VISIONS_OF_ABYSSEA": true,
|
||||||
|
"LOGIN.WINGS_OF_THE_GODDESS": true,
|
||||||
|
"MAIN.ABSORB_SPELL_AMOUNT": 8,
|
||||||
|
"MAIN.ABSORB_SPELL_TICK": 9,
|
||||||
|
"MAIN.ABYSSEA_BONUSLIGHT_AMOUNT": 0,
|
||||||
|
"MAIN.ABYSSEA_LIGHTS_DROP_RATE": 80,
|
||||||
|
"MAIN.ACTIVATE_LAMP_TIME": 6000,
|
||||||
|
"MAIN.ADVANCED_JOB_LEVEL": 30,
|
||||||
|
"MAIN.AF1_QUEST_LEVEL": 40,
|
||||||
|
"MAIN.AF2_QUEST_LEVEL": 50,
|
||||||
|
"MAIN.AF3_QUEST_LEVEL": 50,
|
||||||
|
"MAIN.ALLOW_MULTIPLE_EXP_RINGS": 0,
|
||||||
|
"MAIN.ALL_MAPS": 0,
|
||||||
|
"MAIN.AQUAVEIL_COUNTER": 1,
|
||||||
|
"MAIN.ASSAULT_MINIMUM": 1,
|
||||||
|
"MAIN.BAYLD_RATE": 1,
|
||||||
|
"MAIN.BETWEEN_2DYNA_WAIT_TIME": 24,
|
||||||
|
"MAIN.BIO_OVERWRITE": 0,
|
||||||
|
"MAIN.BLINK_SHADOWS": 2,
|
||||||
|
"MAIN.BLUE_POWER": 1,
|
||||||
|
"MAIN.BOOK_EXP_RATE": 1,
|
||||||
|
"MAIN.BYPASS_EXP_RING_ONE_PER_WEEK": 0,
|
||||||
|
"MAIN.CAPACITY_RATE": 1,
|
||||||
|
"MAIN.CAP_CURRENCY_ACCOLADES": 99999,
|
||||||
|
"MAIN.CAP_CURRENCY_BALLISTA": 2000,
|
||||||
|
"MAIN.CAP_CURRENCY_SPARKS": 99999,
|
||||||
|
"MAIN.CAP_CURRENCY_VALOR": 50000,
|
||||||
|
"MAIN.CASKET_DROP_RATE": 0.1,
|
||||||
|
"MAIN.CHEST_MAX_ILLUSION_TIME": 3600,
|
||||||
|
"MAIN.CHEST_MIN_ILLUSION_TIME": 1800,
|
||||||
|
"MAIN.CHOCOBO_RAISING_DISABLE_RETIREMENT": false,
|
||||||
|
"MAIN.CHOCOBO_RAISING_GIL_MULTIPLIER": 1,
|
||||||
|
"MAIN.CHOCOBO_RAISING_STAT_GROWTH_CAP": 512,
|
||||||
|
"MAIN.CHOCOBO_RAISING_STAT_NEG_MULTIPLIER": 1,
|
||||||
|
"MAIN.CHOCOBO_RAISING_STAT_POS_MULTIPLIER": 1,
|
||||||
|
"MAIN.COFFER_MAX_ILLUSION_TIME": 3600,
|
||||||
|
"MAIN.COFFER_MIN_ILLUSION_TIME": 1800,
|
||||||
|
"MAIN.COSMO_CLEANSE_BASE_COST": 15000,
|
||||||
|
"MAIN.CURE_POWER": 1,
|
||||||
|
"MAIN.CURRENCY_EXCHANGE_RATE": 100,
|
||||||
|
"MAIN.DAILY_TALLY_AMOUNT": 10,
|
||||||
|
"MAIN.DAILY_TALLY_LIMIT": 50000,
|
||||||
|
"MAIN.DARK_POWER": 1,
|
||||||
|
"MAIN.DEBUG_CHOCOBO_RAISING": false,
|
||||||
|
"MAIN.DIA_OVERWRITE": 1,
|
||||||
|
"MAIN.DIGGING_RATE": 85,
|
||||||
|
"MAIN.DIG_ABUNDANCE_BONUS": 0,
|
||||||
|
"MAIN.DIG_FATIGUE": 1,
|
||||||
|
"MAIN.DIG_GRANT_BORE": 0,
|
||||||
|
"MAIN.DIG_GRANT_BURROW": 0,
|
||||||
|
"MAIN.DISABLE_INACTIVITY_WATCHDOG": false,
|
||||||
|
"MAIN.DISABLE_PARTY_EXP_PENALTY": false,
|
||||||
|
"MAIN.DIVINE_POWER": 1,
|
||||||
|
"MAIN.DYNA_LEVEL_MIN": 65,
|
||||||
|
"MAIN.DYNA_MIDNIGHT_RESET": true,
|
||||||
|
"MAIN.ELEMENTAL_DEBUFF_DURATION": 120,
|
||||||
|
"MAIN.ELEMENTAL_POWER": 1,
|
||||||
|
"MAIN.ENABLE_ABYSSEA": 1,
|
||||||
|
"MAIN.ENABLE_ACP": 1,
|
||||||
|
"MAIN.ENABLE_AMK": 1,
|
||||||
|
"MAIN.ENABLE_ASA": 1,
|
||||||
|
"MAIN.ENABLE_CHOCOBO_RAISING": false,
|
||||||
|
"MAIN.ENABLE_COP": 1,
|
||||||
|
"MAIN.ENABLE_COP_ZONE_CAP": 0,
|
||||||
|
"MAIN.ENABLE_DAILY_TALLY": 1,
|
||||||
|
"MAIN.ENABLE_EXCHANGE_100S_TO_1S": false,
|
||||||
|
"MAIN.ENABLE_EXCHANGE_LIMIT": 1,
|
||||||
|
"MAIN.ENABLE_FIELD_MANUALS": 1,
|
||||||
|
"MAIN.ENABLE_GARRISON": true,
|
||||||
|
"MAIN.ENABLE_GROUNDS_TOMES": 1,
|
||||||
|
"MAIN.ENABLE_IMMUNOBREAK": true,
|
||||||
|
"MAIN.ENABLE_LOGIN_CAMPAIGN": 0,
|
||||||
|
"MAIN.ENABLE_MAGIAN_TRIALS": 1,
|
||||||
|
"MAIN.ENABLE_MONSTROSITY": 0,
|
||||||
|
"MAIN.ENABLE_NYZUL_CASKETS": true,
|
||||||
|
"MAIN.ENABLE_ROE": 1,
|
||||||
|
"MAIN.ENABLE_ROE_TIMED": 1,
|
||||||
|
"MAIN.ENABLE_ROV": 1,
|
||||||
|
"MAIN.ENABLE_SOA": 1,
|
||||||
|
"MAIN.ENABLE_SURVIVAL_GUIDE": 1,
|
||||||
|
"MAIN.ENABLE_TOAU": 1,
|
||||||
|
"MAIN.ENABLE_TRUST_ALTER_EGO_EXPO": 0,
|
||||||
|
"MAIN.ENABLE_TRUST_ALTER_EGO_EXPO_ANNOUNCE": 0,
|
||||||
|
"MAIN.ENABLE_TRUST_ALTER_EGO_EXTRAVAGANZA": 0,
|
||||||
|
"MAIN.ENABLE_TRUST_ALTER_EGO_EXTRAVAGANZA_ANNOUNCE": 0,
|
||||||
|
"MAIN.ENABLE_TRUST_CASTING": 1,
|
||||||
|
"MAIN.ENABLE_TRUST_CUSTOM_ENGAGEMENT": 0,
|
||||||
|
"MAIN.ENABLE_TRUST_QUESTS": 1,
|
||||||
|
"MAIN.ENABLE_TVR": 1,
|
||||||
|
"MAIN.ENABLE_VIGIL_DROPS": true,
|
||||||
|
"MAIN.ENABLE_VOIDWALKER": 1,
|
||||||
|
"MAIN.ENABLE_VOIDWATCH": 1,
|
||||||
|
"MAIN.ENABLE_WOTG": 1,
|
||||||
|
"MAIN.ENM_COOLDOWN": 120,
|
||||||
|
"MAIN.EQUIP_FROM_OTHER_CONTAINERS": false,
|
||||||
|
"MAIN.EXCAVATION_BREAK_CHANCE": 33,
|
||||||
|
"MAIN.EXCAVATION_RATE": 50,
|
||||||
|
"MAIN.EXPLORER_MOOGLE_LV": 10,
|
||||||
|
"MAIN.EXP_RATE": 1,
|
||||||
|
"MAIN.FORCE_SPAWN_QM_RESET_TIME": 300,
|
||||||
|
"MAIN.FOV_REWARD_ALLIANCE": 0,
|
||||||
|
"MAIN.FREE_COP_DYNAMIS": 0,
|
||||||
|
"MAIN.FRIGICITE_TIME": 30,
|
||||||
|
"MAIN.GARRISON_LOCKOUT": 1800,
|
||||||
|
"MAIN.GARRISON_NATION_BYPASS": false,
|
||||||
|
"MAIN.GARRISON_ONCE_PER_WEEK": true,
|
||||||
|
"MAIN.GARRISON_PARTY_LIMIT": 18,
|
||||||
|
"MAIN.GARRISON_RANK": 2,
|
||||||
|
"MAIN.GARRISON_TIME_LIMIT": 1800,
|
||||||
|
"MAIN.GIL_RATE": 1,
|
||||||
|
"MAIN.GOBBIE_BOX_MIN_AGE": 45,
|
||||||
|
"MAIN.GOV_REWARD_ALLIANCE": 1,
|
||||||
|
"MAIN.HALLOWEEN_2005": 0,
|
||||||
|
"MAIN.HALLOWEEN_YEAR_ROUND": 0,
|
||||||
|
"MAIN.HARVESTING_BREAK_CHANCE": 33,
|
||||||
|
"MAIN.HARVESTING_RATE": 50,
|
||||||
|
"MAIN.HEALING_TP_CHANGE": -100,
|
||||||
|
"MAIN.HOMEPOINT_TELEPORT": 1,
|
||||||
|
"MAIN.INACTIVITY_WATCHDOG_PERIOD": 2000,
|
||||||
|
"MAIN.INITIAL_LEVEL_CAP": 50,
|
||||||
|
"MAIN.ITEM_POWER": 1,
|
||||||
|
"MAIN.LANTERNS_STAY_LIT": 1200,
|
||||||
|
"MAIN.LOGGING_BREAK_CHANCE": 33,
|
||||||
|
"MAIN.LOGGING_RATE": 50,
|
||||||
|
"MAIN.MAX_LEVEL": 99,
|
||||||
|
"MAIN.MINING_BREAK_CHANCE": 33,
|
||||||
|
"MAIN.MINING_RATE": 50,
|
||||||
|
"MAIN.MONSTROSITY_DONT_WIPE_BUFFS": 0,
|
||||||
|
"MAIN.MONSTROSITY_INFAMY_MESSAGING": 0,
|
||||||
|
"MAIN.MONSTROSITY_INFAMY_RATIO": 0.1,
|
||||||
|
"MAIN.MONSTROSITY_PVP_MODE": 0,
|
||||||
|
"MAIN.MONSTROSITY_PVP_ZONE_BYPASS": 0,
|
||||||
|
"MAIN.MONSTROSITY_TELEPORT_TO_FERETORY": 0,
|
||||||
|
"MAIN.MONSTROSITY_TRIGGER_NPCS": 0,
|
||||||
|
"MAIN.NEW_CHARACTER_CUTSCENE": 1,
|
||||||
|
"MAIN.NINJUTSU_POWER": 1,
|
||||||
|
"MAIN.NM_LOTTERY_CHANCE": 1,
|
||||||
|
"MAIN.NM_LOTTERY_COOLDOWN": 1,
|
||||||
|
"MAIN.NORMAL_MOB_MAX_LEVEL_RANGE_MAX": 0,
|
||||||
|
"MAIN.NORMAL_MOB_MAX_LEVEL_RANGE_MIN": 0,
|
||||||
|
"MAIN.NUMBER_OF_DM_EARRINGS": 1,
|
||||||
|
"MAIN.OLDSCHOOL_G1": false,
|
||||||
|
"MAIN.OLDSCHOOL_G2": false,
|
||||||
|
"MAIN.PRISMATIC_HOURGLASS_COST": 50000,
|
||||||
|
"MAIN.REGIME_REWARD_THRESHOLD": 15,
|
||||||
|
"MAIN.REGIME_WAIT": 1,
|
||||||
|
"MAIN.RELIC_2ND_UPGRADE_WAIT_TIME": 7200,
|
||||||
|
"MAIN.RELIC_3RD_UPGRADE_WAIT_TIME": 3600,
|
||||||
|
"MAIN.RESTRICT_CONTENT": 0,
|
||||||
|
"MAIN.RIVERNE_PORTERS": 120,
|
||||||
|
"MAIN.ROE_EXP_RATE": 1,
|
||||||
|
"MAIN.RUNIC_DISK_SAVE": true,
|
||||||
|
"MAIN.SERVER_MESSAGE": "Please visit https://github.com/LandSandBoat/server for the latest information on the project.?Thank you, and we hope you enjoy sailing the sands!",
|
||||||
|
"MAIN.SERVER_NAME": "Nameless",
|
||||||
|
"MAIN.SHOP_PRICE": 1,
|
||||||
|
"MAIN.SNEAK_INVIS_DURATION_MULTIPLIER": 1,
|
||||||
|
"MAIN.SPARKS_RATE": 1,
|
||||||
|
"MAIN.SPIKE_EFFECT_DURATION": 180,
|
||||||
|
"MAIN.START_GIL": 10,
|
||||||
|
"MAIN.START_INVENTORY": 30,
|
||||||
|
"MAIN.STONESKIN_CAP": 350,
|
||||||
|
"MAIN.SUBJOB_QUEST_LEVEL": 18,
|
||||||
|
"MAIN.TABS_RATE": 1,
|
||||||
|
"MAIN.TIMELESS_HOURGLASS_COST": 500000,
|
||||||
|
"MAIN.TRUST_ALTER_EGO_EXPO_MESSAGE": "? ????? The Alter Ego Expo Campaign is active! ?????Trusts gain the benefits of Increased HP, MP, and Status Resistances!",
|
||||||
|
"MAIN.TRUST_ALTER_EGO_EXTRAVAGANZA_MESSAGE": "? ????? The Alter Ego Extravaganza Campaign is active! ?????This is an excellent time to fill out your roster of Trusts!",
|
||||||
|
"MAIN.UNLOCK_OUTPOST_WARPS": 0,
|
||||||
|
"MAIN.USE_ADOULIN_WEAPON_SKILL_CHANGES": true,
|
||||||
|
"MAIN.USE_OLD_CURE_FORMULA": false,
|
||||||
|
"MAIN.USE_OLD_MAGIC_DAMAGE": false,
|
||||||
|
"MAIN.WEAPON_SKILL_POWER": 1,
|
||||||
|
"MAIN.WEEKLY_EXCHANGE_LIMIT": 100000,
|
||||||
|
"MAP.ABILITY_RECAST_MULTIPLIER": 1,
|
||||||
|
"MAP.AH_BASE_FEE_SINGLE": 1,
|
||||||
|
"MAP.AH_BASE_FEE_STACKS": 4,
|
||||||
|
"MAP.AH_LIST_LIMIT": 7,
|
||||||
|
"MAP.AH_MAX_FEE": 10000,
|
||||||
|
"MAP.AH_TAX_RATE_SINGLE": 1,
|
||||||
|
"MAP.AH_TAX_RATE_STACKS": 0.5,
|
||||||
|
"MAP.ALL_JOBS_WIDESCAN": true,
|
||||||
|
"MAP.ALL_MOBS_GIL_BONUS": 0,
|
||||||
|
"MAP.ALTER_EGO_HP_MULTIPLIER": 1,
|
||||||
|
"MAP.ALTER_EGO_MP_MULTIPLIER": 1,
|
||||||
|
"MAP.ALTER_EGO_SKILL_MULTIPLIER": 1,
|
||||||
|
"MAP.ALTER_EGO_STAT_MULTIPLIER": 1,
|
||||||
|
"MAP.ANTICHEAT_ENABLED": true,
|
||||||
|
"MAP.ANTICHEAT_JAIL_DISABLE": false,
|
||||||
|
"MAP.AUDIT_CHAT": false,
|
||||||
|
"MAP.AUDIT_GM_CMD": false,
|
||||||
|
"MAP.AUDIT_LINKSHELL": false,
|
||||||
|
"MAP.AUDIT_PARTY": false,
|
||||||
|
"MAP.AUDIT_SAY": false,
|
||||||
|
"MAP.AUDIT_SHOUT": false,
|
||||||
|
"MAP.AUDIT_TELL": false,
|
||||||
|
"MAP.AUDIT_UNITY": false,
|
||||||
|
"MAP.AUDIT_YELL": false,
|
||||||
|
"MAP.BATTLE_CAP_TWEAK": 0,
|
||||||
|
"MAP.BLOCK_OLD_SKILLUP_STYLE": false,
|
||||||
|
"MAP.BLOCK_TELL_TO_HIDDEN_GM": false,
|
||||||
|
"MAP.BLOOD_PACT_SHARED_TIMER": false,
|
||||||
|
"MAP.CAPACITY_RATE": 1,
|
||||||
|
"MAP.CRAFT_AMOUNT_MULTIPLIER": 1,
|
||||||
|
"MAP.CRAFT_CHANCE_MULTIPLIER": 1,
|
||||||
|
"MAP.CRAFT_COMMON_CAP": 700,
|
||||||
|
"MAP.CRAFT_MODERN_SYSTEM": true,
|
||||||
|
"MAP.CRAFT_SPECIALIZATION_POINTS": 400,
|
||||||
|
"MAP.DESPAWN_JUGPETS_BELOW_MINIMUM_LEVEL": false,
|
||||||
|
"MAP.DISABLE_GEAR_SCALING": false,
|
||||||
|
"MAP.DROP_RATE_MULTIPLIER": 1,
|
||||||
|
"MAP.ENABLE_ITEM_RECYCLE_BIN": true,
|
||||||
|
"MAP.ENMITY_CAP": 30000,
|
||||||
|
"MAP.EXP_LOSS_LEVEL": 31,
|
||||||
|
"MAP.EXP_LOSS_RATE": 1,
|
||||||
|
"MAP.EXP_PARTY_GAP_NO_EXP": 0,
|
||||||
|
"MAP.EXP_PARTY_GAP_PENALTIES": true,
|
||||||
|
"MAP.EXP_RATE": 1,
|
||||||
|
"MAP.EXP_RETAIN": 0,
|
||||||
|
"MAP.FAME_MULTIPLIER": 1,
|
||||||
|
"MAP.FELLOW_TP_MULTIPLIER": 1,
|
||||||
|
"MAP.FISHING_ENABLE": false,
|
||||||
|
"MAP.FISHING_SKILL_MULTIPLIER": 1,
|
||||||
|
"MAP.GARDEN_DAY_MATTERS": false,
|
||||||
|
"MAP.GARDEN_MH_AURA_MATTERS": false,
|
||||||
|
"MAP.GARDEN_MOONPHASE_MATTERS": false,
|
||||||
|
"MAP.GARDEN_POT_MATTERS": false,
|
||||||
|
"MAP.GUARD_OLD_SKILLUP_STYLE": false,
|
||||||
|
"MAP.HEALING_TICK_DELAY": 10,
|
||||||
|
"MAP.INCLUDE_MOB_SJ": false,
|
||||||
|
"MAP.KEEP_JUGPET_THROUGH_ZONING": false,
|
||||||
|
"MAP.LEVEL_SYNC_ENABLE": true,
|
||||||
|
"MAP.LIGHTLUGGAGE_BLOCK": 4,
|
||||||
|
"MAP.LV_CAP_MISSION_BCNM": false,
|
||||||
|
"MAP.MAX_GIL_BONUS": 9999,
|
||||||
|
"MAP.MAX_MERIT_POINTS": 30,
|
||||||
|
"MAP.MAX_TIME_LASTUPDATE": 60,
|
||||||
|
"MAP.MINIMUM_LEVEL_CONQUEST_INFUENCE_LOSS": 6,
|
||||||
|
"MAP.MOB_ADDITIONAL_TIME_TO_DEAGGRO": 0,
|
||||||
|
"MAP.MOB_GIL_MULTIPLIER": 1,
|
||||||
|
"MAP.MOB_HP_MULTIPLIER": 1,
|
||||||
|
"MAP.MOB_MP_MULTIPLIER": 1,
|
||||||
|
"MAP.MOB_NO_DESPAWN": false,
|
||||||
|
"MAP.MOB_SPEED_MOD": 0,
|
||||||
|
"MAP.MOB_STAT_MULTIPLIER": 1,
|
||||||
|
"MAP.MOB_TP_MULTIPLIER": 1,
|
||||||
|
"MAP.MOUNT_SPEED_MOD": 0,
|
||||||
|
"MAP.NM_HP_MULTIPLIER": 1,
|
||||||
|
"MAP.NM_MP_MULTIPLIER": 1,
|
||||||
|
"MAP.NM_STAT_MULTIPLIER": 1,
|
||||||
|
"MAP.PACKETGUARD_ENABLED": true,
|
||||||
|
"MAP.PARRY_OLD_SKILLUP_STYLE": false,
|
||||||
|
"MAP.PET_TP_MULTIPLIER": 1,
|
||||||
|
"MAP.PLAYER_HP_MULTIPLIER": 1,
|
||||||
|
"MAP.PLAYER_MP_MULTIPLIER": 1,
|
||||||
|
"MAP.PLAYER_STAT_MULTIPLIER": 1,
|
||||||
|
"MAP.PLAYER_TP_MULTIPLIER": 1,
|
||||||
|
"MAP.PREVENT_UNENGAGED_WS": false,
|
||||||
|
"MAP.REPORT_LUA_ERRORS_TO_PLAYER_LEVEL": 6,
|
||||||
|
"MAP.SETVAR_RETRY_MAX": 3,
|
||||||
|
"MAP.SJ_MP_DIVISOR": 2,
|
||||||
|
"MAP.SKILLUP_AMOUNT_MULTIPLIER": 1,
|
||||||
|
"MAP.SKILLUP_BLOODPACT": true,
|
||||||
|
"MAP.SKILLUP_CHANCE_MULTIPLIER": 1,
|
||||||
|
"MAP.SPEED_MOD": 0,
|
||||||
|
"MAP.SUBJOB_RATIO": 1,
|
||||||
|
"MAP.TRUST_TP_MULTIPLIER": 1,
|
||||||
|
"MAP.VANADIEL_TIME_EPOCH": 0,
|
||||||
|
"MAP.WS_POINTS_BASE": 1,
|
||||||
|
"MAP.WS_POINTS_SKILLCHAIN": 1,
|
||||||
|
"MAP.YELL_COOLDOWN": 30,
|
||||||
|
"SEARCH.DEBUG_OUT_PACKETS": false,
|
||||||
|
"SEARCH.EXPIRE_AUCTIONS": true,
|
||||||
|
"SEARCH.EXPIRE_DAYS": 3,
|
||||||
|
"SEARCH.EXPIRE_INTERVAL": 3600,
|
||||||
|
"SEARCH.OMIT_NO_HISTORY": false
|
||||||
|
}
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 12 KiB |
|
|
@ -0,0 +1,8 @@
|
||||||
|
@tailwind base;
|
||||||
|
@tailwind components;
|
||||||
|
@tailwind utilities;
|
||||||
|
|
||||||
|
* {
|
||||||
|
-webkit-user-drag: none; /* Safari */
|
||||||
|
user-drag: none;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,21 @@
|
||||||
|
import { CssBaseline } from '@mui/material';
|
||||||
|
import { StyledEngineProvider } from '@mui/material/styles';
|
||||||
|
import { StrictMode } from 'react';
|
||||||
|
import ReactDOM from 'react-dom/client';
|
||||||
|
import { WrappedApp } from './App';
|
||||||
|
import { ThemeContextProvider } from './context/ThemeContext';
|
||||||
|
import './index.css';
|
||||||
|
|
||||||
|
const rootElement = document.getElementById('root');
|
||||||
|
const root = ReactDOM.createRoot(rootElement!);
|
||||||
|
|
||||||
|
root.render(
|
||||||
|
<StrictMode>
|
||||||
|
<StyledEngineProvider injectFirst>
|
||||||
|
<ThemeContextProvider>
|
||||||
|
<CssBaseline />
|
||||||
|
<WrappedApp />
|
||||||
|
</ThemeContextProvider>
|
||||||
|
</StyledEngineProvider>
|
||||||
|
</StrictMode>
|
||||||
|
);
|
||||||
|
|
@ -0,0 +1,166 @@
|
||||||
|
import { ExpandMore } from '@mui/icons-material';
|
||||||
|
import {
|
||||||
|
Accordion,
|
||||||
|
AccordionDetails,
|
||||||
|
AccordionSummary,
|
||||||
|
Box,
|
||||||
|
Card,
|
||||||
|
CardContent,
|
||||||
|
Paper,
|
||||||
|
Typography,
|
||||||
|
alpha,
|
||||||
|
} from '@mui/material';
|
||||||
|
import { Link } from 'react-router-dom';
|
||||||
|
|
||||||
|
export default function About() {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Card className="mb-3">
|
||||||
|
<CardContent>
|
||||||
|
<Box className="mb-3">
|
||||||
|
<Typography align="center" variant="h6">
|
||||||
|
What is this?
|
||||||
|
</Typography>
|
||||||
|
<Typography
|
||||||
|
component="p"
|
||||||
|
variant="body1"
|
||||||
|
align="justify"
|
||||||
|
color={(theme) => alpha(theme.palette.text.primary, 0.87)}
|
||||||
|
>
|
||||||
|
Ixion is a catalog of FFXI private servers running the{' '}
|
||||||
|
<Link to="https://github.com/LandSandBoat/server">
|
||||||
|
LandSandBoat
|
||||||
|
</Link>{' '}
|
||||||
|
software (or serving their API). Servers are updated every hour,
|
||||||
|
and are automatically removed after 24 hours of failed updates.
|
||||||
|
All information is pulled directly from the servers and Ixion
|
||||||
|
makes no guarantees as to how current or correct the information
|
||||||
|
is and is in no way affiliated with any listed server. Always use
|
||||||
|
a different strong and unique password for every server (and just
|
||||||
|
in general).
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
<Box className="mb-3">
|
||||||
|
<Typography align="center" variant="h6">
|
||||||
|
How can I add a server?
|
||||||
|
</Typography>
|
||||||
|
<Typography
|
||||||
|
component="p"
|
||||||
|
variant="body1"
|
||||||
|
align="justify"
|
||||||
|
color={(theme) => alpha(theme.palette.text.primary, 0.87)}
|
||||||
|
>
|
||||||
|
Click the + button at the top of the page then enter the URL you
|
||||||
|
use to connect to the server. The server must be serving the LSB
|
||||||
|
API at the <b>/api</b> subdirectory of their URL.
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
<Box className="mb-3">
|
||||||
|
<Typography align="center" variant="h6">
|
||||||
|
Other Resources
|
||||||
|
</Typography>
|
||||||
|
<Typography
|
||||||
|
component="p"
|
||||||
|
variant="body1"
|
||||||
|
align="justify"
|
||||||
|
color={(theme) => alpha(theme.palette.text.primary, 0.87)}
|
||||||
|
>
|
||||||
|
Additional servers can be found on the XiPrivateServers{' '}
|
||||||
|
<Link to="https://github.com/XiPrivateServers/Servers/blob/main/SERVERS.md">
|
||||||
|
GitHub
|
||||||
|
</Link>
|
||||||
|
. Discussion can be found on the{' '}
|
||||||
|
<Link to="https://www.reddit.com/r/FFXIPrivateServers/">
|
||||||
|
subreddit
|
||||||
|
</Link>{' '}
|
||||||
|
or in the <Link to="https://discord.gg/nYF6gNv">Discord</Link>. If
|
||||||
|
you want to setup your own server, check out{' '}
|
||||||
|
<Link to="https://github.com/LandSandBoat/server/wiki/Quick-Start-Guide">
|
||||||
|
the guide
|
||||||
|
</Link>{' '}
|
||||||
|
and don't forget to enable the HTTP server and list yourself
|
||||||
|
here!
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
<Card>
|
||||||
|
<Accordion className="my-0">
|
||||||
|
<AccordionSummary expandIcon={<ExpandMore />}>
|
||||||
|
<Typography variant="h5">For server owners:</Typography>
|
||||||
|
</AccordionSummary>
|
||||||
|
<AccordionDetails>
|
||||||
|
<Box className="mb-3">
|
||||||
|
<Typography align="center" variant="h6">
|
||||||
|
Why can't I add my server?
|
||||||
|
</Typography>
|
||||||
|
<Typography
|
||||||
|
component="p"
|
||||||
|
variant="body1"
|
||||||
|
align="justify"
|
||||||
|
color={(theme) => alpha(theme.palette.text.primary, 0.87)}
|
||||||
|
>
|
||||||
|
To be searchable, you need to enable the HTTP server on the LSB
|
||||||
|
world server by setting <b>ENABLE_HTTP</b> in <b>network.lua</b>{' '}
|
||||||
|
to <b>true</b> and make sure requests to the <b>/api</b>{' '}
|
||||||
|
subdirectory of your domain reach it (e.g.
|
||||||
|
http://play.myserver.com/api/).
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
<Box className="mb-3">
|
||||||
|
<Typography align="center" variant="h6">
|
||||||
|
What if my server isn't running LSB?
|
||||||
|
</Typography>
|
||||||
|
<Typography
|
||||||
|
component="p"
|
||||||
|
variant="body1"
|
||||||
|
align="justify"
|
||||||
|
color={(theme) => alpha(theme.palette.text.primary, 0.87)}
|
||||||
|
>
|
||||||
|
If you're not on LSB, you could fake the API response.
|
||||||
|
Right now the only required settings are <b>MAIN.SERVER_NAME</b>
|
||||||
|
, <b>MAIN.MAX_LEVEL</b>, and <b>LOGIN.LOGIN_LIMIT</b>.
|
||||||
|
You'll want to look at the LSB settings and serve whatever
|
||||||
|
relevant changes you've made using the LSB equivalent.
|
||||||
|
Settings should be served at <b>/api/settings</b> and total
|
||||||
|
active sessions should be served at <b>/api/sessions</b>.
|
||||||
|
Enabled expansions are controlled by the LOGIN settings.
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
<Box className="mb-3">
|
||||||
|
<Typography align="center" variant="h6">
|
||||||
|
How can I add a link to the server website?
|
||||||
|
</Typography>
|
||||||
|
<Typography
|
||||||
|
component="span"
|
||||||
|
variant="body1"
|
||||||
|
align="justify"
|
||||||
|
color={(theme) => alpha(theme.palette.text.primary, 0.87)}
|
||||||
|
>
|
||||||
|
Create a new file <b>settings/api.lua</b> in your server
|
||||||
|
directory.
|
||||||
|
<Paper elevation={2}>
|
||||||
|
<pre className="m-0 p-2">
|
||||||
|
<code>
|
||||||
|
{`xi = xi or {}
|
||||||
|
xi.settings = xi.settings or {}
|
||||||
|
|
||||||
|
xi.settings.api =
|
||||||
|
{
|
||||||
|
WEBSITE = '',
|
||||||
|
DO_NOT_TRACK = false,
|
||||||
|
}`}
|
||||||
|
</code>
|
||||||
|
</pre>
|
||||||
|
</Paper>
|
||||||
|
The world server needs to be restarted after any changes to any
|
||||||
|
of the settings. If you set <b>DO_NOT_TRACK</b> to <b>true</b>,
|
||||||
|
your server will be removed on the next hourly update.
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
</AccordionDetails>
|
||||||
|
</Accordion>
|
||||||
|
</Card>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,182 @@
|
||||||
|
import { Box } from '@mui/material';
|
||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
import { fetchData, fetchDemo } from '../apiUtil';
|
||||||
|
import { AlertResponse } from '../components/Alert';
|
||||||
|
import ErrorCard from '../components/ErrorCard';
|
||||||
|
import ServerCard from '../components/ServerCard';
|
||||||
|
import SearchState from '../data/SearchState';
|
||||||
|
import ServerData from '../data/ServerData';
|
||||||
|
|
||||||
|
export default function Home({
|
||||||
|
servers,
|
||||||
|
setServers,
|
||||||
|
searchState,
|
||||||
|
setAlertInfo,
|
||||||
|
}: {
|
||||||
|
servers: ServerData[];
|
||||||
|
setServers: React.Dispatch<React.SetStateAction<ServerData[]>>;
|
||||||
|
searchState: SearchState;
|
||||||
|
setAlertInfo: React.Dispatch<React.SetStateAction<AlertResponse>>;
|
||||||
|
}) {
|
||||||
|
const [error, setError] = useState<string>('');
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const fetchServerData = async () => {
|
||||||
|
let data: ServerData[] = [];
|
||||||
|
try {
|
||||||
|
data = await fetchData();
|
||||||
|
} catch (err) {
|
||||||
|
if (err instanceof Error) {
|
||||||
|
setAlertInfo({
|
||||||
|
message: err.message,
|
||||||
|
severity: 'error',
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
setError('An unknown error occurred.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (data.length === 0) {
|
||||||
|
data.push(await fetchDemo());
|
||||||
|
}
|
||||||
|
setServers(data);
|
||||||
|
};
|
||||||
|
|
||||||
|
fetchServerData();
|
||||||
|
}, [setAlertInfo, setServers]);
|
||||||
|
|
||||||
|
const filterServers = (server: ServerData): boolean => {
|
||||||
|
if (
|
||||||
|
searchState.name.value.length > 0 &&
|
||||||
|
typeof server.settings['MAIN.SERVER_NAME'] === 'string'
|
||||||
|
) {
|
||||||
|
const serverName = server.settings['MAIN.SERVER_NAME'].toLowerCase();
|
||||||
|
if (!serverName.includes(searchState.name.value.toLowerCase())) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof server.settings['MAIN.MAX_LEVEL'] === 'number') {
|
||||||
|
if (server.settings['MAIN.MAX_LEVEL'] < searchState.maxLevel.value[0]) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (server.settings['MAIN.MAX_LEVEL'] > searchState.maxLevel.value[1]) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
searchState.trusts.value &&
|
||||||
|
typeof server.settings['MAIN.ENABLE_TRUST_CASTING'] === 'number'
|
||||||
|
) {
|
||||||
|
const serverTrusts = server.settings['MAIN.ENABLE_TRUST_CASTING'] === 1;
|
||||||
|
const searchEnabled = searchState.trusts.value.includes('enabled');
|
||||||
|
const searchDisabled = searchState.trusts.value.includes('disabled');
|
||||||
|
if (serverTrusts && searchDisabled && !searchEnabled) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!serverTrusts && searchEnabled && !searchDisabled) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
searchState.levelSync.value &&
|
||||||
|
typeof server.settings['MAP.LEVEL_SYNC_ENABLE'] === 'boolean'
|
||||||
|
) {
|
||||||
|
const serverLevelSync = server.settings['MAP.LEVEL_SYNC_ENABLE'];
|
||||||
|
const searchEnabled = searchState.levelSync.value.includes('enabled');
|
||||||
|
const searchDisabled = searchState.levelSync.value.includes('disabled');
|
||||||
|
if (serverLevelSync && searchDisabled && !searchEnabled) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!serverLevelSync && searchEnabled && !searchDisabled) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (searchState.expansions.value) {
|
||||||
|
const searchNoneEnabled = searchState.expansions.value.includes('none');
|
||||||
|
const searchRotzEnabled = searchState.expansions.value.includes('rotz');
|
||||||
|
const serverRotzEnabled =
|
||||||
|
server.settings['LOGIN.RISE_OF_ZILART'] === true;
|
||||||
|
const searchCopEnabled = searchState.expansions.value.includes('cop');
|
||||||
|
const serverCopEnabled =
|
||||||
|
server.settings['LOGIN.CHAINS_OF_PROMATHIA'] === true;
|
||||||
|
const searchToauEnabled = searchState.expansions.value.includes('toau');
|
||||||
|
const serverToauEnabled =
|
||||||
|
server.settings['LOGIN.TREASURES_OF_AHT_URGHAN'] === true;
|
||||||
|
const searchWotgEnabled = searchState.expansions.value.includes('wotg');
|
||||||
|
const serverWotgEnabled =
|
||||||
|
server.settings['LOGIN.WINGS_OF_THE_GODDESS'] === true;
|
||||||
|
const searchSoaEnabled = searchState.expansions.value.includes('soa');
|
||||||
|
const serverSoaEnabled =
|
||||||
|
server.settings['LOGIN.SEEKERS_OF_ADOULIN'] === true;
|
||||||
|
if (
|
||||||
|
searchNoneEnabled &&
|
||||||
|
(serverRotzEnabled ||
|
||||||
|
serverCopEnabled ||
|
||||||
|
serverToauEnabled ||
|
||||||
|
serverWotgEnabled ||
|
||||||
|
serverSoaEnabled)
|
||||||
|
) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (searchRotzEnabled !== serverRotzEnabled) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (searchCopEnabled !== serverCopEnabled) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (searchToauEnabled !== serverToauEnabled) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (searchWotgEnabled !== serverWotgEnabled) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (searchSoaEnabled !== serverSoaEnabled) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (searchState.multibox.value) {
|
||||||
|
const serverMultibox = server.settings['LOGIN.LOGIN_LIMIT'];
|
||||||
|
if (typeof serverMultibox === 'number') {
|
||||||
|
if (
|
||||||
|
searchState.multibox.value.includes('none') &&
|
||||||
|
serverMultibox !== 1
|
||||||
|
) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
searchState.multibox.value.includes('unlimited') &&
|
||||||
|
serverMultibox !== 0
|
||||||
|
) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
searchState.multibox.value.includes('limited') &&
|
||||||
|
!searchState.multibox.value.includes('unlimited') &&
|
||||||
|
serverMultibox < 2
|
||||||
|
) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const filteredServers = servers.filter(filterServers);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box>
|
||||||
|
{error ? (
|
||||||
|
<ErrorCard error={error} />
|
||||||
|
) : (
|
||||||
|
filteredServers.map((server: ServerData) => (
|
||||||
|
<ServerCard key={server.id} server={server} />
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,5 @@
|
||||||
|
import ErrorCard from '../components/ErrorCard';
|
||||||
|
|
||||||
|
export default function NotFound() {
|
||||||
|
return <ErrorCard error="Page not found." />;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,2 @@
|
||||||
|
// eslint-disable-next-line import/no-extraneous-dependencies
|
||||||
|
import '@testing-library/jest-dom';
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
/// <reference types="vite/client" />
|
||||||
|
|
@ -0,0 +1,13 @@
|
||||||
|
import type { Config } from 'tailwindcss';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
content: ['./index.html', './src/**/*.{js,ts,jsx,tsx}'],
|
||||||
|
important: '#root',
|
||||||
|
theme: {
|
||||||
|
extend: {},
|
||||||
|
},
|
||||||
|
corePlugins: {
|
||||||
|
preflight: false,
|
||||||
|
},
|
||||||
|
plugins: [],
|
||||||
|
} satisfies Config;
|
||||||
|
|
@ -0,0 +1,32 @@
|
||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2020",
|
||||||
|
"useDefineForClassFields": true,
|
||||||
|
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
||||||
|
"module": "ESNext",
|
||||||
|
"skipLibCheck": true,
|
||||||
|
|
||||||
|
/* Bundler mode */
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"allowImportingTsExtensions": true,
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"noEmit": true,
|
||||||
|
"jsx": "react-jsx",
|
||||||
|
"types": ["vitest/globals"],
|
||||||
|
|
||||||
|
/* Linting */
|
||||||
|
"strict": true,
|
||||||
|
"noUnusedLocals": true,
|
||||||
|
"noUnusedParameters": true,
|
||||||
|
"noFallthroughCasesInSwitch": true
|
||||||
|
},
|
||||||
|
"include": [
|
||||||
|
"vite.config.ts",
|
||||||
|
".eslintrc.cjs",
|
||||||
|
"tailwind.config.ts",
|
||||||
|
"postcss.config.js",
|
||||||
|
"src"
|
||||||
|
],
|
||||||
|
"references": [{ "path": "./tsconfig.node.json" }]
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,10 @@
|
||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"composite": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"module": "ESNext",
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"allowSyntheticDefaultImports": true
|
||||||
|
},
|
||||||
|
"include": ["vite.config.ts"]
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,28 @@
|
||||||
|
/* eslint-disable import/no-extraneous-dependencies */
|
||||||
|
/// <reference types="vitest" />
|
||||||
|
/// <reference types="vite/client" />
|
||||||
|
|
||||||
|
import react from '@vitejs/plugin-react-swc';
|
||||||
|
import { defineConfig } from 'vite';
|
||||||
|
|
||||||
|
// https://vitejs.dev/config/
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [react()],
|
||||||
|
server: {
|
||||||
|
watch: {
|
||||||
|
usePolling: true,
|
||||||
|
},
|
||||||
|
host: true, // needed for the Docker Container port mapping to work
|
||||||
|
strictPort: true,
|
||||||
|
port: 3000,
|
||||||
|
hmr: {
|
||||||
|
//port: 3010,
|
||||||
|
host: 'localhost'
|
||||||
|
},
|
||||||
|
},
|
||||||
|
test: {
|
||||||
|
globals: true,
|
||||||
|
environment: 'jsdom',
|
||||||
|
setupFiles: ['./src/setupTests.ts'],
|
||||||
|
},
|
||||||
|
});
|
||||||
Loading…
Reference in New Issue