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