Start huge UI refactor

This commit is contained in:
Théophile Diot 2024-08-26 23:39:58 +01:00
parent 6e2c22b96b
commit b32171aae9
No known key found for this signature in database
GPG key ID: FA995104A0BA376A
2251 changed files with 39915 additions and 318799 deletions

View file

@ -61,8 +61,8 @@ repos:
hooks:
- id: codespell
name: Codespell Spell Checker
exclude: (^src/(ui/templates|common/core/.+/files|bw/loading)/.+.html|modsecurity-rules.conf.*|src/ui/static/js/lottie-web.min.js)$
entry: codespell --ignore-regex="(tabEl|Widgits)" --skip src/ui/static/js/utils/flatpickr.js,src/ui/static/css/style.css,CHANGELOG.md,CODE_OF_CONDUCT.md,src/ui/client/build.py
exclude: (^src/(ui/templates|common/core/.+/files|bw/loading)/.+.html|modsecurity-rules.conf.*|src/ui/app/static/(fonts|libs)/.+)$
entry: codespell --ignore-regex="(tabEl|Widgits)" --skip CHANGELOG.md,CODE_OF_CONDUCT.md,src/ui/client/build.py
language: python
types: [text]

View file

@ -1,4 +1,4 @@
mike==2.1.3
mkdocs-material[imaging]==9.5.32
mkdocs-material[imaging]==9.5.33
mkdocs-print-site-plugin==2.5.0
pytablewriter==1.2.0

View file

@ -211,21 +211,21 @@ ghp-import==2.1.0 \
--hash=sha256:8337dd7b50877f163d4c0289bc1f1c7f127550241988d568c1db512c4324a619 \
--hash=sha256:9c535c4c61193c2df8871222567d7fd7e5014d835f97dc7b7439069e2413d343
# via mkdocs
idna==3.7 \
--hash=sha256:028ff3aadf0609c1fd278d8ea3089299412a7a8b9bd005dd08b9f8285bcb5cfc \
--hash=sha256:82fee1fc78add43492d3a1898bfa6d8a904cc97d8427f683ed8e798d07761aa0
idna==3.8 \
--hash=sha256:050b4e5baadcd44d760cedbd2b8e639f2ff89bbc7a5730fcc662954303377aac \
--hash=sha256:d838c2c0ed6fced7693d5e8ab8e734d5f8fda53a039c0164afb0b82e771e3603
# via requests
importlib-metadata==8.3.0 \
--hash=sha256:42817a4a0be5845d22c6e212db66a94ad261e2318d80b3e0d363894a79df2b67 \
--hash=sha256:9c8fa6e8ea0f9516ad5c8db9246a731c948193c7754d3babb0114a05b27dd364
importlib-metadata==8.4.0 \
--hash=sha256:66f342cc6ac9818fc6ff340576acd24d65ba0b3efabb2b4ac08b598965a4a2f1 \
--hash=sha256:9a547d3bc3608b025f93d403fdd1aae741c24fbb8314df4b155675742ce303c5
# via
# markdown
# mike
# mkdocs
# mkdocs-get-deps
importlib-resources==6.4.3 \
--hash=sha256:2d6dfe3b9e055f72495c2085890837fc8c758984e209115c8792bddcb762cd93 \
--hash=sha256:4a202b9b9d38563b46da59221d77bb73862ab5d79d461307bcb826d725448b98
importlib-resources==6.4.4 \
--hash=sha256:20600c8b7361938dc0bb2d5ec0297802e575df486f5a544fa414da65e13721f7 \
--hash=sha256:dda242603d1c9cd836c3368b1174ed74cb4049ecd209e7a1a0104620c18c5c11
# via mike
jinja2==3.1.4 \
--hash=sha256:4a3aee7acbbe7303aede8e9648d13b8bf88a429282aa6122a993f0ac800cb369 \
@ -332,9 +332,9 @@ mkdocs-get-deps==0.2.0 \
--hash=sha256:162b3d129c7fad9b19abfdcb9c1458a651628e4b1dea628ac68790fb3061c60c \
--hash=sha256:2bf11d0b133e77a0dd036abeeb06dec8775e46efa526dc70667d8863eefc6134
# via mkdocs
mkdocs-material==9.5.32 \
--hash=sha256:38ed66e6d6768dde4edde022554553e48b2db0d26d1320b19e2e2b9da0be1120 \
--hash=sha256:f3704f46b63d31b3cd35c0055a72280bed825786eccaf19c655b44e0cd2c6b3f
mkdocs-material==9.5.33 \
--hash=sha256:d23a8b5e3243c9b2f29cdfe83051104a8024b767312dc8fde05ebe91ad55d89d \
--hash=sha256:dbc79cf0fdc6e2c366aa987de8b0c9d4e2bb9f156e7466786ba2fd0f9bf7ffca
# via
# -r requirements.in
# mkdocs-print-site-plugin
@ -352,16 +352,17 @@ packaging==24.1 \
# via
# mkdocs
# typepy
paginate==0.5.6 \
--hash=sha256:5e6007b6a9398177a7e1648d04fdd9f8c9766a1a945bceac82f1929e8c78af2d
paginate==0.5.7 \
--hash=sha256:22bd083ab41e1a8b4f3690544afb2c60c25e5c9a63a30fa2f483f6c60c8e5945 \
--hash=sha256:b885e2af73abcf01d9559fd5216b57ef722f8c42affbb63942377668e35c7591
# via mkdocs-material
pathspec==0.12.1 \
--hash=sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08 \
--hash=sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712
# via mkdocs
pathvalidate==3.2.0 \
--hash=sha256:5e8378cf6712bff67fbe7a8307d99fa8c1a0cb28aa477056f8fc374f0dff24ad \
--hash=sha256:cc593caa6299b22b37f228148257997e2fa850eea2daf7e4cc9205cef6908dee
pathvalidate==3.2.1 \
--hash=sha256:9a6255eb8f63c9e2135b9be97a5ce08f10230128c4ae7b3e935378b82b22c4c9 \
--hash=sha256:f5d07b1e2374187040612a1fcd2bcb2919f8db180df254c9581bb90bf903377d
# via pytablewriter
pillow==10.4.0 \
--hash=sha256:02a2be69f9c9b8c1e97cf2713e789d4e398c751ecfd9967c18d0ce304efbf885 \
@ -463,9 +464,9 @@ pymdown-extensions==10.9 \
--hash=sha256:6ff740bcd99ec4172a938970d42b96128bdc9d4b9bcad72494f29921dc69b753 \
--hash=sha256:d323f7e90d83c86113ee78f3fe62fc9dee5f56b54d912660703ea1816fed5626
# via mkdocs-material
pyparsing==3.1.2 \
--hash=sha256:a1bac0ce561155ecc3ed78ca94d3c9378656ad4c94c1270de543f621420f94ad \
--hash=sha256:f9db75911801ed778fe61bb643079ff86601aca99fcae6345aa67292038fb742
pyparsing==3.1.4 \
--hash=sha256:a6a7ee4235a3f944aa1fa2249307708f893fe5717dc603503c6c7969c070fb7c \
--hash=sha256:f86ec8d1a83f11977c9a6ea7598e8c27fc5cddfa5b07ea2241edbbde1d7bc032
# via mike
pytablewriter==1.2.0 \
--hash=sha256:0204a4bb684a22140d640f2599f09e137bcdc18b3dd49426f4a555016e246b46 \
@ -636,9 +637,9 @@ requests==2.32.3 \
# importlib-resources
# The following packages are considered to be unsafe in a requirements file:
setuptools==73.0.0 \
--hash=sha256:3c08705fadfc8c7c445cf4d98078f0fafb9225775b2b4e8447e40348f82597c0 \
--hash=sha256:f2bfcce7ae1784d90b04c57c2802e8649e1976530bb25dc72c2b078d3ecf4864
setuptools==73.0.1 \
--hash=sha256:b208925fcb9f7af924ed2dc04708ea89791e24bde0d3020b27df0e116088b34e \
--hash=sha256:d59a3e788ab7e012ab2c4baed1b376da6366883ee20d7a5fc426816e3d7b1193
# via mkdocs-material
six==1.16.0 \
--hash=sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926 \
@ -716,7 +717,7 @@ webencodings==0.5.1 \
# via
# cssselect2
# tinycss2
zipp==3.20.0 \
--hash=sha256:0145e43d89664cfe1a2e533adc75adafed82fe2da404b4bbb6b026c0157bdb31 \
--hash=sha256:58da6168be89f0be59beb194da1250516fdaa062ccebd30127ac65d30045e10d
zipp==3.20.1 \
--hash=sha256:9960cd8967c8f85a56f920d5d507274e74f9ff813a0ab8889a5b5be2daf44064 \
--hash=sha256:c22b14cc4763c5a5b04134207736c107db42e9d3ef2d9779d465f5f1bcba572b
# via pytablewriter

View file

@ -103,13 +103,9 @@ services:
dockerfile: ./src/ui/Dockerfile
volumes:
- bw-logs:/var/log/bunkerweb
- ../../src/ui/pages:/usr/share/bunkerweb/ui/pages:ro
- ../../src/ui/src:/usr/share/bunkerweb/ui/src:ro
- ../../src/ui/app:/usr/share/bunkerweb/ui/app:ro
- ../../src/ui/gunicorn.conf.py:/usr/share/bunkerweb/ui/gunicorn.conf.py:ro
- ../../src/ui/main.py:/usr/share/bunkerweb/ui/main.py:ro
- ../../src/ui/models.py:/usr/share/bunkerweb/ui/models.py:ro
- ../../src/ui/ui_database.py:/usr/share/bunkerweb/ui/ui_database.py:ro
- ../../src/ui/utils.py:/usr/share/bunkerweb/ui/utils.py:ro
environment:
<<: *env
ADMIN_USERNAME: "admin"

View file

@ -100,13 +100,9 @@ services:
dockerfile: ./src/ui/Dockerfile
volumes:
- bw-logs:/var/log/bunkerweb
- ../../src/ui/pages:/usr/share/bunkerweb/ui/pages:ro
- ../../src/ui/src:/usr/share/bunkerweb/ui/src:ro
- ../../src/ui/app:/usr/share/bunkerweb/ui/app:ro
- ../../src/ui/gunicorn.conf.py:/usr/share/bunkerweb/ui/gunicorn.conf.py:ro
- ../../src/ui/main.py:/usr/share/bunkerweb/ui/main.py:ro
- ../../src/ui/models.py:/usr/share/bunkerweb/ui/models.py:ro
- ../../src/ui/ui_database.py:/usr/share/bunkerweb/ui/ui_database.py:ro
- ../../src/ui/utils.py:/usr/share/bunkerweb/ui/utils.py:ro
environment:
<<: *env
ADMIN_USERNAME: "admin"

View file

@ -100,13 +100,9 @@ services:
dockerfile: ./src/ui/Dockerfile
volumes:
- bw-logs:/var/log/bunkerweb
- ../../src/ui/pages:/usr/share/bunkerweb/ui/pages:ro
- ../../src/ui/src:/usr/share/bunkerweb/ui/src:ro
- ../../src/ui/app:/usr/share/bunkerweb/ui/app:ro
- ../../src/ui/gunicorn.conf.py:/usr/share/bunkerweb/ui/gunicorn.conf.py:ro
- ../../src/ui/main.py:/usr/share/bunkerweb/ui/main.py:ro
- ../../src/ui/models.py:/usr/share/bunkerweb/ui/models.py:ro
- ../../src/ui/ui_database.py:/usr/share/bunkerweb/ui/ui_database.py:ro
- ../../src/ui/utils.py:/usr/share/bunkerweb/ui/utils.py:ro
environment:
<<: *env
DEBUG: "1"

View file

@ -52,6 +52,10 @@ services:
DISABLE_DEFAULT_SERVER: "yes"
USE_CLIENT_CACHE: "yes"
USE_GZIP: "yes"
USE_MODSECURITY: "no"
USE_BAD_BEHAVIOR: "no"
USE_LIMIT_REQ: "no"
USE_LIMIT_CONN: "no"
EXTERNAL_PLUGIN_URLS: "https://github.com/bunkerity/bunkerweb-plugins/archive/refs/heads/dev.zip"
CUSTOM_CONF_MODSEC_CRS_reqbody-suppress: "SecRuleRemoveById 200002"
www.example.com_USE_UI: "yes"
@ -85,13 +89,9 @@ services:
dockerfile: ./src/ui/Dockerfile
volumes:
- bw-logs:/var/log/bunkerweb
- ../../src/ui/pages:/usr/share/bunkerweb/ui/pages:ro
- ../../src/ui/src:/usr/share/bunkerweb/ui/src:ro
- ../../src/ui/app:/usr/share/bunkerweb/ui/app:ro
- ../../src/ui/gunicorn.conf.py:/usr/share/bunkerweb/ui/gunicorn.conf.py:ro
- ../../src/ui/main.py:/usr/share/bunkerweb/ui/main.py:ro
- ../../src/ui/models.py:/usr/share/bunkerweb/ui/models.py:ro
- ../../src/ui/ui_database.py:/usr/share/bunkerweb/ui/ui_database.py:ro
- ../../src/ui/utils.py:/usr/share/bunkerweb/ui/utils.py:ro
environment:
<<: *env
ADMIN_USERNAME: "admin"

View file

@ -51,6 +51,10 @@ services:
DISABLE_DEFAULT_SERVER: "yes"
USE_CLIENT_CACHE: "yes"
USE_GZIP: "yes"
USE_MODSECURITY: "no"
USE_BAD_BEHAVIOR: "no"
USE_LIMIT_REQ: "no"
USE_LIMIT_CONN: "no"
www.example.com_USE_UI: "yes"
www.example.com_USE_REVERSE_PROXY: "yes"
www.example.com_REVERSE_PROXY_URL: "/admin"
@ -82,13 +86,9 @@ services:
dockerfile: ./src/ui/Dockerfile
volumes:
- bw-logs:/var/log/bunkerweb
- ../../src/ui/pages:/usr/share/bunkerweb/ui/pages:ro
- ../../src/ui/src:/usr/share/bunkerweb/ui/src:ro
- ../../src/ui/app:/usr/share/bunkerweb/ui/app:ro
- ../../src/ui/gunicorn.conf.py:/usr/share/bunkerweb/ui/gunicorn.conf.py:ro
- ../../src/ui/main.py:/usr/share/bunkerweb/ui/main.py:ro
- ../../src/ui/models.py:/usr/share/bunkerweb/ui/models.py:ro
- ../../src/ui/ui_database.py:/usr/share/bunkerweb/ui/ui_database.py:ro
- ../../src/ui/utils.py:/usr/share/bunkerweb/ui/utils.py:ro
environment:
<<: *env
ADMIN_USERNAME: "admin"

View file

@ -74,13 +74,9 @@ services:
dockerfile: ./src/ui/Dockerfile
volumes:
- bw-logs:/var/log/bunkerweb
- ../../src/ui/pages:/usr/share/bunkerweb/ui/pages:ro
- ../../src/ui/src:/usr/share/bunkerweb/ui/src:ro
- ../../src/ui/app:/usr/share/bunkerweb/ui/app:ro
- ../../src/ui/gunicorn.conf.py:/usr/share/bunkerweb/ui/gunicorn.conf.py:ro
- ../../src/ui/main.py:/usr/share/bunkerweb/ui/main.py:ro
- ../../src/ui/models.py:/usr/share/bunkerweb/ui/models.py:ro
- ../../src/ui/ui_database.py:/usr/share/bunkerweb/ui/ui_database.py:ro
- ../../src/ui/utils.py:/usr/share/bunkerweb/ui/utils.py:ro
environment:
<<: *env
DEBUG: "1"

View file

@ -114,9 +114,9 @@ google-auth==2.34.0 \
--hash=sha256:72fd4733b80b6d777dcde515628a9eb4a577339437012874ea286bca7261ee65 \
--hash=sha256:8eb87396435c19b20d32abd2f984e31c191a15284af72eb922f10e5bde9c04cc
# via kubernetes
idna==3.7 \
--hash=sha256:028ff3aadf0609c1fd278d8ea3089299412a7a8b9bd005dd08b9f8285bcb5cfc \
--hash=sha256:82fee1fc78add43492d3a1898bfa6d8a904cc97d8427f683ed8e798d07761aa0
idna==3.8 \
--hash=sha256:050b4e5baadcd44d760cedbd2b8e639f2ff89bbc7a5730fcc662954303377aac \
--hash=sha256:d838c2c0ed6fced7693d5e8ab8e734d5f8fda53a039c0164afb0b82e771e3603
# via requests
kubernetes==30.1.0 \
--hash=sha256:41e4c77af9f28e7a6c314e3bd06a8c6229ddd787cad684e0ab9f69b498e98ebc \

View file

@ -104,9 +104,9 @@ charset-normalizer==3.3.2 \
--hash=sha256:fd1abc0d89e30cc4e02e4064dc67fcc51bd941eb395c502aac3ec19fab46b519 \
--hash=sha256:ff8fa367d09b717b2a17a052544193ad76cd49979c805768879cb63d9ca50561
# via requests
idna==3.7 \
--hash=sha256:028ff3aadf0609c1fd278d8ea3089299412a7a8b9bd005dd08b9f8285bcb5cfc \
--hash=sha256:82fee1fc78add43492d3a1898bfa6d8a904cc97d8427f683ed8e798d07761aa0
idna==3.8 \
--hash=sha256:050b4e5baadcd44d760cedbd2b8e639f2ff89bbc7a5730fcc662954303377aac \
--hash=sha256:d838c2c0ed6fced7693d5e8ab8e734d5f8fda53a039c0164afb0b82e771e3603
# via requests
jinja2==3.1.4 \
--hash=sha256:4a3aee7acbbe7303aede8e9648d13b8bf88a429282aa6122a993f0ac800cb369 \

View file

@ -2,6 +2,6 @@ pip==24.2
pip-compile-multi==2.6.4
pip-tools==7.4.1
pip-upgrader==1.4.15
setuptools==73.0.0
setuptools==73.0.1
tomli==2.0.1
wheel==0.44.0

View file

@ -117,13 +117,13 @@ colorclass==2.2.2 \
docopt==0.6.2 \
--hash=sha256:49b3a825280bd66b3aa83585ef59c4a8c82f2c8a522dbe754a8bc8d08c85c491
# via pip-upgrader
idna==3.7 \
--hash=sha256:028ff3aadf0609c1fd278d8ea3089299412a7a8b9bd005dd08b9f8285bcb5cfc \
--hash=sha256:82fee1fc78add43492d3a1898bfa6d8a904cc97d8427f683ed8e798d07761aa0
idna==3.8 \
--hash=sha256:050b4e5baadcd44d760cedbd2b8e639f2ff89bbc7a5730fcc662954303377aac \
--hash=sha256:d838c2c0ed6fced7693d5e8ab8e734d5f8fda53a039c0164afb0b82e771e3603
# via requests
importlib-metadata==8.3.0 \
--hash=sha256:42817a4a0be5845d22c6e212db66a94ad261e2318d80b3e0d363894a79df2b67 \
--hash=sha256:9c8fa6e8ea0f9516ad5c8db9246a731c948193c7754d3babb0114a05b27dd364
importlib-metadata==8.4.0 \
--hash=sha256:66f342cc6ac9818fc6ff340576acd24d65ba0b3efabb2b4ac08b598965a4a2f1 \
--hash=sha256:9a547d3bc3608b025f93d403fdd1aae741c24fbb8314df4b155675742ce303c5
# via build
packaging==24.1 \
--hash=sha256:026ed72c8ed3fcce5bf8950572258698927fd1dbda10a5e981cdf0ac37f4f002 \
@ -163,9 +163,9 @@ requests==2.32.3 \
# via
# -r requirements-deps.in
# pip-tools
setuptools==73.0.0 \
--hash=sha256:3c08705fadfc8c7c445cf4d98078f0fafb9225775b2b4e8447e40348f82597c0 \
--hash=sha256:f2bfcce7ae1784d90b04c57c2802e8649e1976530bb25dc72c2b078d3ecf4864
setuptools==73.0.1 \
--hash=sha256:b208925fcb9f7af924ed2dc04708ea89791e24bde0d3020b27df0e116088b34e \
--hash=sha256:d59a3e788ab7e012ab2c4baed1b376da6366883ee20d7a5fc426816e3d7b1193
# via pip-upgrader
terminaltables==3.1.10 \
--hash=sha256:ba6eca5cb5ba02bba4c9f4f985af80c54ec3dccf94cfcd190154386255e47543 \
@ -192,9 +192,9 @@ wheel==0.44.0 \
# via
# -r requirements-deps.in
# pip-tools
zipp==3.20.0 \
--hash=sha256:0145e43d89664cfe1a2e533adc75adafed82fe2da404b4bbb6b026c0157bdb31 \
--hash=sha256:58da6168be89f0be59beb194da1250516fdaa062ccebd30127ac65d30045e10d
zipp==3.20.1 \
--hash=sha256:9960cd8967c8f85a56f920d5d507274e74f9ff813a0ab8889a5b5be2daf44064 \
--hash=sha256:c22b14cc4763c5a5b04134207736c107db42e9d3ef2d9779d465f5f1bcba572b
# via
# -r requirements-deps.in
# pip-tools

View file

@ -1,4 +1,4 @@
pip==24.2
pip-tools==7.4.1
setuptools==73.0.0
setuptools==73.0.1
wheel==0.44.0

View file

@ -12,9 +12,9 @@ click==8.1.7 \
--hash=sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28 \
--hash=sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de
# via pip-tools
importlib-metadata==8.3.0 \
--hash=sha256:42817a4a0be5845d22c6e212db66a94ad261e2318d80b3e0d363894a79df2b67 \
--hash=sha256:9c8fa6e8ea0f9516ad5c8db9246a731c948193c7754d3babb0114a05b27dd364
importlib-metadata==8.4.0 \
--hash=sha256:66f342cc6ac9818fc6ff340576acd24d65ba0b3efabb2b4ac08b598965a4a2f1 \
--hash=sha256:9a547d3bc3608b025f93d403fdd1aae741c24fbb8314df4b155675742ce303c5
# via build
packaging==24.1 \
--hash=sha256:026ed72c8ed3fcce5bf8950572258698927fd1dbda10a5e981cdf0ac37f4f002 \
@ -36,9 +36,9 @@ pyproject-hooks==1.1.0 \
# via
# -r requirements.in
# pip-tools
setuptools==73.0.0 \
--hash=sha256:3c08705fadfc8c7c445cf4d98078f0fafb9225775b2b4e8447e40348f82597c0 \
--hash=sha256:f2bfcce7ae1784d90b04c57c2802e8649e1976530bb25dc72c2b078d3ecf4864
setuptools==73.0.1 \
--hash=sha256:b208925fcb9f7af924ed2dc04708ea89791e24bde0d3020b27df0e116088b34e \
--hash=sha256:d59a3e788ab7e012ab2c4baed1b376da6366883ee20d7a5fc426816e3d7b1193
# via
# build
# pip-tools
@ -54,9 +54,9 @@ wheel==0.44.0 \
# via
# -r requirements.in
# pip-tools
zipp==3.20.0 \
--hash=sha256:0145e43d89664cfe1a2e533adc75adafed82fe2da404b4bbb6b026c0157bdb31 \
--hash=sha256:58da6168be89f0be59beb194da1250516fdaa062ccebd30127ac65d30045e10d
zipp==3.20.1 \
--hash=sha256:9960cd8967c8f85a56f920d5d507274e74f9ff813a0ab8889a5b5be2daf44064 \
--hash=sha256:c22b14cc4763c5a5b04134207736c107db42e9d3ef2d9779d465f5f1bcba572b
# via
# -r requirements.in
# pip-tools

View file

@ -223,13 +223,13 @@ distro==1.9.0 \
--hash=sha256:2fa77c6fd8940f116ee1d6b94a2f90b13b5ea8d019b98bc8bafdcabcdd9bdbed \
--hash=sha256:7bffd925d65168f85027d8da9af6bddab658135b840670a223589bc0c8ef02b2
# via certbot
idna==3.7 \
--hash=sha256:028ff3aadf0609c1fd278d8ea3089299412a7a8b9bd005dd08b9f8285bcb5cfc \
--hash=sha256:82fee1fc78add43492d3a1898bfa6d8a904cc97d8427f683ed8e798d07761aa0
idna==3.8 \
--hash=sha256:050b4e5baadcd44d760cedbd2b8e639f2ff89bbc7a5730fcc662954303377aac \
--hash=sha256:d838c2c0ed6fced7693d5e8ab8e734d5f8fda53a039c0164afb0b82e771e3603
# via requests
importlib-metadata==8.3.0 \
--hash=sha256:42817a4a0be5845d22c6e212db66a94ad261e2318d80b3e0d363894a79df2b67 \
--hash=sha256:9c8fa6e8ea0f9516ad5c8db9246a731c948193c7754d3babb0114a05b27dd364
importlib-metadata==8.4.0 \
--hash=sha256:66f342cc6ac9818fc6ff340576acd24d65ba0b3efabb2b4ac08b598965a4a2f1 \
--hash=sha256:9a547d3bc3608b025f93d403fdd1aae741c24fbb8314df4b155675742ce303c5
# via certbot
josepy==1.14.0 \
--hash=sha256:308b3bf9ce825ad4d4bba76372cf19b5dc1c2ce96a9d298f9642975e64bd13dd \
@ -351,9 +351,9 @@ schedule==1.2.2 \
# via importlib-metadata
# The following packages are considered to be unsafe in a requirements file:
setuptools==73.0.0 \
--hash=sha256:3c08705fadfc8c7c445cf4d98078f0fafb9225775b2b4e8447e40348f82597c0 \
--hash=sha256:f2bfcce7ae1784d90b04c57c2802e8649e1976530bb25dc72c2b078d3ecf4864
setuptools==73.0.1 \
--hash=sha256:b208925fcb9f7af924ed2dc04708ea89791e24bde0d3020b27df0e116088b34e \
--hash=sha256:d59a3e788ab7e012ab2c4baed1b376da6366883ee20d7a5fc426816e3d7b1193
# via -r requirements.in
six==1.16.0 \
--hash=sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926 \
@ -363,9 +363,9 @@ urllib3==2.2.2 \
--hash=sha256:a448b2f64d686155468037e1ace9f2d2199776e17f0a46610480d311f73e3472 \
--hash=sha256:dd505485549a7a552833da5e6063639d0d177c04f23bc3864e41e5dc5f612168
# via requests
zipp==3.20.0 \
--hash=sha256:0145e43d89664cfe1a2e533adc75adafed82fe2da404b4bbb6b026c0157bdb31 \
--hash=sha256:58da6168be89f0be59beb194da1250516fdaa062ccebd30127ac65d30045e10d
zipp==3.20.1 \
--hash=sha256:9960cd8967c8f85a56f920d5d507274e74f9ff813a0ab8889a5b5be2daf44064 \
--hash=sha256:c22b14cc4763c5a5b04134207736c107db42e9d3ef2d9779d465f5f1bcba572b
# via
# acme
# certbot

View file

@ -21,18 +21,6 @@ RUN export MAKEFLAGS="-j$(nproc)" && \
pip install --no-cache-dir --require-hashes --break-system-packages -r /tmp/requirements-deps.txt && \
pip install --no-cache-dir --require-hashes --target deps/python $(for file in $(ls /tmp/req/requirements*.txt) ; do echo "-r ${file}" ; done | xargs)
# Install node and npm to build vite frontend
RUN apk add --no-cache nodejs npm
COPY src/ui/client ui/client
# This will build the frontend and add files to the UI folder
WORKDIR /usr/share/bunkerweb/ui/client
RUN DOCKERFILE=yes python3 build.py
# We can delete the client folder after building the frontend
RUN rm -rf /usr/share/bunkerweb/ui/client
WORKDIR /usr/share/bunkerweb
# Copy files
@ -45,10 +33,7 @@ COPY src/common/settings.json settings.json
COPY src/common/utils utils
COPY src/common/helpers helpers
COPY src/VERSION VERSION
COPY src/ui/pages ui/pages
COPY src/ui/src ui/src
COPY src/ui/*.py ui/
COPY --chmod=750 src/ui/entrypoint.sh ui/
COPY src/ui ui
FROM python:3.12.5-alpine@sha256:c2f41e6a5a67bc39b95be3988dd19fbd05d1b82375c46d9826c592cca014d4de
@ -78,7 +63,8 @@ RUN echo "Docker" > INTEGRATION && \
for dir in $(echo "pro/plugins configs/http configs/stream configs/server-http configs/server-stream configs/default-server-http configs/default-server-stream configs/modsec configs/modsec-crs") ; do mkdir "/data/${dir}" ; done && \
chown -R root:ui INTEGRATION /data /var/cache/bunkerweb /var/lib/bunkerweb /etc/bunkerweb /var/tmp/bunkerweb /var/run/bunkerweb /var/log/bunkerweb && \
chmod -R 770 /data /var/cache/bunkerweb /var/lib/bunkerweb /etc/bunkerweb /var/tmp/bunkerweb /var/run/bunkerweb /var/log/bunkerweb && \
chmod 750 gen/*.py ui/*.py ui/src/*.py helpers/*.sh deps/python/bin/* && \
chmod 750 gen/*.py ui/*.sh helpers/*.sh deps/python/bin/* && \
find ui -name "*.py" -type f -exec chmod 750 {} \; && \
chmod 660 INTEGRATION && \
ln -s /proc/1/fd/1 /var/log/bunkerweb/ui-access.log && \
ln -s /proc/1/fd/2 /var/log/bunkerweb/ui.log

View file

@ -2,11 +2,10 @@ from logging import getLogger
from os import sep
from pathlib import Path
from src.config import Config
from src.instance import InstancesUtils
from src.ui_data import UIData
from ui_database import UIDatabase
from app.models.config import Config
from app.models.instance import InstancesUtils
from app.models.ui_data import UIData
from app.models.ui_database import UIDatabase
DB = UIDatabase(getLogger("UI"), log=False)
DATA = UIData(Path(sep, "var", "tmp", "bunkerweb").joinpath("ui_data.json"))

View file

@ -9,8 +9,7 @@ for deps_path in [join(sep, "usr", "share", "bunkerweb", *paths) for paths in ((
from bcrypt import checkpw
from flask_login import AnonymousUserMixin, UserMixin
from sqlalchemy.orm import declarative_base, relationship
from sqlalchemy import Boolean, DateTime, Column, Identity, Integer, String, ForeignKey, UnicodeText
from sqlalchemy import TEXT, Boolean, DateTime, Column, Identity, Integer, String, ForeignKey, UnicodeText
from model import METHODS_ENUM # type: ignore
@ -23,9 +22,6 @@ class AnonymousUser(AnonymousUserMixin):
password = ""
method = "manual"
admin = False
last_login_at = None
last_login_ip = None
login_count = 0
totp_secret = None
creation_date = datetime.now()
update_date = datetime.now()
@ -49,11 +45,6 @@ class Users(Base, UserMixin):
method = Column(METHODS_ENUM, nullable=False, default="manual")
admin = Column(Boolean, nullable=False, default=False)
# Trackable
last_login_at = Column(DateTime(timezone=True), nullable=True)
last_login_ip = Column(String(39), nullable=True)
login_count = Column(Integer, default=0, nullable=False)
# 2FA
totp_secret = Column(String(256), nullable=True)
@ -62,6 +53,7 @@ class Users(Base, UserMixin):
roles = relationship("RolesUsers", back_populates="user", cascade="all")
recovery_codes = relationship("UserRecoveryCodes", back_populates="user", cascade="all")
sessions = relationship("UserSessions", back_populates="user", cascade="all")
list_roles: list[str] = []
list_permissions: list[str] = []
list_recovery_codes: list[str] = []
@ -120,3 +112,16 @@ class Permissions(Base):
name = Column(String(64), primary_key=True)
roles = relationship("RolesPermissions", back_populates="permission", cascade="all")
class UserSessions(Base):
__tablename__ = "bw_ui_user_sessions"
id = Column(Integer, Identity(start=1, increment=1), primary_key=True)
user_name = Column(String(256), ForeignKey("bw_ui_users.username", onupdate="cascade", ondelete="cascade"), nullable=False)
ip = Column(String(39), nullable=False)
user_agent = Column(TEXT, nullable=False)
creation_date = Column(DateTime(timezone=True), nullable=False)
last_activity = Column(DateTime(timezone=True), nullable=False)
user = relationship("Users", back_populates="sessions")

View file

@ -9,9 +9,9 @@ from passlib.pwd import genword
from qrcode import make
from qrcode.image.svg import SvgImage
from models import Users
from dependencies import DATA
from utils import LIB_DIR, LOGGER, stop
from app.models.models import Users
from app.dependencies import DATA
from app.utils import LIB_DIR, LOGGER, stop
TOTP_SECRETS = getenv("TOTP_SECRETS", "")
@ -54,7 +54,7 @@ class Totp:
return self._totp.from_source(totp_secret).pretty_key()
def generate_recovery_codes(self) -> List[str]:
return ["-".join([pwd[i : i + 4] for i in range(0, len(pwd), 4)]) for pwd in genword(length=16, charset="hex", returns=5)] # noqa: E203
return ["-".join([pwd[i : i + 4] for i in range(0, len(pwd), 4)]) for pwd in genword(length=16, charset="hex", returns=6)] # noqa: E203
def verify_recovery_code(self, code: str, user: Users) -> Optional[str]:
"""Check if recovery code is valid for user."""

View file

@ -27,3 +27,7 @@ class UIData(dict):
def __delitem__(self, key):
super().__delitem__(key)
self._write_to_file()
def update(self, *args, **kwargs):
super().update(*args, **kwargs)
self._write_to_file()

View file

@ -19,7 +19,7 @@ from sqlalchemy.exc import IntegrityError
from Database import Database # type: ignore
from model import Metadata # type: ignore
from models import Base, Users, Roles, RolesUsers, UserRecoveryCodes, RolesPermissions, Permissions
from app.models.models import Base, Permissions, Roles, RolesPermissions, RolesUsers, Users, UserRecoveryCodes, UserSessions
class UIDatabase(Database):
@ -163,9 +163,6 @@ class UIDatabase(Database):
"email": ui_user.email,
"password": ui_user.password.encode("utf-8"),
"method": ui_user.method,
"last_login_at": ui_user.last_login_at,
"last_login_ip": ui_user.last_login_ip,
"login_count": ui_user.login_count,
"totp_secret": ui_user.totp_secret,
"creation_date": ui_user.creation_date,
"update_date": ui_user.update_date,
@ -234,19 +231,31 @@ class UIDatabase(Database):
password: bytes,
totp_secret: Optional[str],
*,
old_username: Optional[str] = None,
email: Optional[str] = None,
totp_recovery_codes: Optional[List[str]] = None,
method: str = "manual",
) -> str:
"""Update ui user."""
totp_changed = False
old_username = old_username or username
with self._db_session() as session:
if self.readonly:
return "The database is read-only, the changes will not be saved"
user = session.query(Users).filter_by(username=username).first()
user = session.query(Users).filter_by(username=old_username).first()
if not user:
return f"User {username} doesn't exist"
return f"User {old_username} doesn't exist"
if username != old_username:
if session.query(Users).with_entities(Users.username).filter_by(username=username).first():
return f"User {username} already exists"
user.username = username
session.query(RolesUsers).filter_by(user_name=old_username).update({"user_name": username})
session.query(UserRecoveryCodes).filter_by(user_name=old_username).update({"user_name": username})
session.query(UserSessions).filter_by(user_name=old_username).update({"user_name": username})
totp_changed = user.totp_secret != totp_secret
@ -290,16 +299,44 @@ class UIDatabase(Database):
return ""
def mark_ui_user_login(self, username: str, date: datetime, ip: str) -> str:
def mark_ui_user_login(self, username: str, date: datetime, ip: str, user_agent: str) -> Union[str, int]:
"""Mark ui user login."""
with self._db_session() as session:
if self.readonly:
return "The database is read-only, the changes will not be saved"
user = session.query(Users).filter_by(username=username).first()
if not user:
return f"User {username} doesn't exist"
user.last_login_at = date
user.last_login_ip = ip
user.login_count += 1
user_session = UserSessions(
user_name=username,
ip=ip,
user_agent=user_agent,
creation_date=date,
last_activity=date,
)
session.add(user_session)
try:
session.commit()
session.refresh(user_session)
session_id = user_session.id
return session_id
except BaseException as e:
return str(e)
def mark_ui_user_access(self, session_id: int, date: datetime) -> str:
"""Mark ui user access."""
with self._db_session() as session:
if self.readonly:
return "The database is read-only, the changes will not be saved"
user_session = session.query(UserSessions).filter_by(id=session_id).first()
if not user_session:
return f"Session {session_id} doesn't exist"
user_session.last_activity = date
try:
session.commit()
@ -441,3 +478,34 @@ class UIDatabase(Database):
return str(e)
return ""
def get_ui_user_last_session(self, username: str) -> Optional[UserSessions]:
"""Get ui user last session."""
with self._db_session() as session:
return session.query(UserSessions).filter_by(user_name=username).order_by(UserSessions.creation_date.desc()).first()
def get_ui_user_sessions(self, username: str) -> List[UserSessions]:
"""Get ui user sessions."""
with self._db_session() as session:
return session.query(UserSessions).filter_by(user_name=username).order_by(UserSessions.creation_date.desc()).limit(10).all()
def delete_ui_user_old_sessions(self, username: str) -> str:
"""Delete ui user old sessions."""
with self._db_session() as session:
if self.readonly:
return "The database is read-only, the changes will not be saved"
user = session.query(Users).filter_by(username=username).first()
if not user:
return f"User {username} doesn't exist"
sessions_to_delete = session.query(UserSessions).filter_by(user_name=username).order_by(UserSessions.creation_date.desc()).offset(1).all()
for session_to_delete in sessions_to_delete:
session.delete(session_to_delete)
try:
session.commit()
except BaseException as e:
return str(e)
return ""

View file

View file

@ -1,4 +1,3 @@
from base64 import b64encode
from datetime import datetime
from json import dumps, loads as json_loads
from math import floor
@ -8,12 +7,10 @@ from flask import Blueprint, flash, redirect, render_template, request, url_for
from flask_login import login_required
from redis import Redis, Sentinel
from builder.bans import bans_builder # type: ignore
from app.dependencies import BW_CONFIG, BW_INSTANCES_UTILS, DB
from app.utils import LOGGER
from dependencies import BW_CONFIG, BW_INSTANCES_UTILS, DB
from utils import LOGGER
from pages.utils import get_remain, handle_error, verify_data_in_form
from app.routes.utils import get_remain, handle_error, verify_data_in_form
bans = Blueprint("bans", __name__)
@ -218,5 +215,6 @@ def bans_page():
ban["ban_end_date"] = datetime.fromtimestamp(floor(timestamp_now + exp)).strftime("%Y/%m/%d at %H:%M:%S %Z")
reasons.add(ban["reason"])
builder = bans_builder(bans, list(reasons), list(remains))
return render_template("bans.html", data_server_builder=b64encode(dumps(builder).encode("utf-8")).decode("ascii"))
# builder = bans_builder(bans, list(reasons), list(remains))
# return render_template("bans.html", data_server_builder=b64encode(dumps(builder).encode("utf-8")).decode("ascii"))
return render_template("bans.html") # TODO

View file

@ -3,8 +3,8 @@ from os.path import join, sep
from flask import Blueprint, render_template
from flask_login import login_required
from dependencies import BW_CONFIG, DB
from utils import path_to_dict
from app.dependencies import BW_CONFIG, DB
from app.utils import path_to_dict
cache = Blueprint("cache", __name__)

View file

@ -5,10 +5,10 @@ from bs4 import BeautifulSoup
from flask import Blueprint, flash, redirect, render_template, request, url_for
from flask_login import login_required
from dependencies import BW_CONFIG, DATA, DB
from utils import LOGGER, PLUGIN_NAME_RX, path_to_dict
from app.dependencies import BW_CONFIG, DATA, DB
from app.utils import LOGGER, PLUGIN_NAME_RX, path_to_dict
from pages.utils import handle_error, verify_data_in_form
from app.routes.utils import handle_error, verify_data_in_form
configs = Blueprint("configs", __name__)

View file

@ -1,17 +1,13 @@
from base64 import b64encode
from contextlib import suppress
from json import dumps
from threading import Thread
from time import time
from flask import Blueprint, flash, redirect, render_template, request, url_for
from flask_login import login_required
from builder.global_config import global_config_builder # type: ignore
from app.dependencies import BW_CONFIG, DATA, DB
from dependencies import BW_CONFIG, DATA, DB
from pages.utils import handle_error, manage_bunkerweb, wait_applying
from app.routes.utils import handle_error, manage_bunkerweb, wait_applying
global_config = Blueprint("global_config", __name__)
@ -83,7 +79,8 @@ def global_config_page():
)
)
global_config = BW_CONFIG.get_config(global_only=True, methods=True)
plugins = BW_CONFIG.get_plugins()
builder = global_config_builder({}, plugins, global_config)
return render_template("global-config.html", data_server_builder=b64encode(dumps(builder).encode("utf-8")).decode("ascii"))
# global_config = BW_CONFIG.get_config(global_only=True, methods=True)
# plugins = BW_CONFIG.get_plugins()
# builder = global_config_builder({}, plugins, global_config)
# return render_template("global-config.html", data_server_builder=b64encode(dumps(builder).encode("utf-8")).decode("ascii"))
return render_template("global-config.html") # TODO

80
src/ui/app/routes/home.py Normal file
View file

@ -0,0 +1,80 @@
from flask import Blueprint, render_template
from flask_login import login_required
from app.dependencies import BW_CONFIG, BW_INSTANCES_UTILS # , DB
home = Blueprint("home", __name__)
@home.route("/home")
@login_required
def home_page():
"""
It returns the home page
:return: The home.html template is being rendered with the following variables:
check_version: a boolean indicating whether the local version is the same as the remote version
remote_version: the remote version
version: the local version
instances_number: the number of instances
services_number: the number of services
posts: a list of posts
"""
# try:
# r = get("https://github.com/bunkerity/bunkerweb/releases/latest", allow_redirects=True, timeout=5)
# r.raise_for_status()
# except BaseException:
# r = None
# remote_version = None
# if r and r.status_code == 200:
# remote_version = basename(r.url).strip().replace("v", "")
config = BW_CONFIG.get_config(with_drafts=True, filtered_settings=("SERVER_NAME",))
instances = BW_INSTANCES_UTILS.get_instances()
instance_health_count = 0
for instance in instances:
if instance.status == "up":
instance_health_count += 1
services = 0
services_scheduler_count = 0
services_ui_count = 0
services_autoconf_count = 0
for service in config["SERVER_NAME"]["value"].split(" "):
service_method = config.get(f"{service}_SERVER_NAME", {"method": "scheduler"})["method"]
if service_method == "scheduler":
services_scheduler_count += 1
elif service_method == "ui":
services_ui_count += 1
elif service_method == "autoconf":
services_autoconf_count += 1
services += 1
# metadata = DB.get_metadata()
# data = {
# "check_version": not remote_version or get_version() == remote_version,
# "remote_version": remote_version,
# "version": metadata["version"],
# "instances_number": len(instances),
# "services_number": services,
# "instance_health_count": instance_health_count,
# "services_scheduler_count": services_scheduler_count,
# "services_ui_count": services_ui_count,
# "services_autoconf_count": services_autoconf_count,
# "is_pro_version": metadata["is_pro"],
# "pro_status": metadata["pro_status"],
# "pro_services": metadata["pro_services"],
# "pro_overlapped": metadata["pro_overlapped"],
# "plugins_number": len(BW_CONFIG.get_plugins()),
# "plugins_errors": DB.get_plugins_errors(),
# }
# builder = home_builder(data)
# return render_template("home.html", data_server_builder=b64encode(dumps(builder).encode("utf-8")).decode("ascii"))
return render_template("home.html") # TODO

View file

@ -1,16 +1,12 @@
from base64 import b64encode
from json import dumps
from threading import Thread
from time import time
from typing import Literal
from flask import Blueprint, redirect, render_template, request, url_for
from flask_login import login_required
from builder.instances import instances_builder # type: ignore
from app.dependencies import BW_CONFIG, BW_INSTANCES_UTILS, DATA, DB
from dependencies import BW_CONFIG, BW_INSTANCES_UTILS, DATA, DB
from pages.utils import handle_error, manage_bunkerweb, verify_data_in_form
from app.routes.utils import handle_error, manage_bunkerweb, verify_data_in_form
instances = Blueprint("instances", __name__)
@ -41,8 +37,9 @@ def instances_page():
instances_methods.add(instance.method)
instances_healths.add(instance.status)
builder = instances_builder(instances, list(instances_types), list(instances_methods), list(instances_healths))
return render_template("instances.html", title="Instances", data_server_builder=b64encode(dumps(builder).encode("utf-8")).decode("ascii"))
# builder = instances_builder(instances, list(instances_types), list(instances_methods), list(instances_healths))
# return render_template("instances.html", title="Instances", data_server_builder=b64encode(dumps(builder).encode("utf-8")).decode("ascii"))
return render_template("instances.html") # TODO
@instances.route("/instances/new", methods=["PUT"])

View file

@ -1,14 +1,10 @@
from base64 import b64encode
from io import BytesIO
from json import dumps
from flask import Blueprint, jsonify, render_template, request, send_file
from flask_login import login_required
from werkzeug.utils import secure_filename
from builder.jobs import jobs_builder # type: ignore
from dependencies import DB
from app.dependencies import DB
jobs = Blueprint("jobs", __name__)
@ -16,8 +12,9 @@ jobs = Blueprint("jobs", __name__)
@jobs.route("/jobs", methods=["GET"])
@login_required
def jobs_page():
builder = jobs_builder(DB.get_jobs())
return render_template("jobs.html", data_server_builder=b64encode(dumps(builder).encode("utf-8")).decode("ascii"))
# builder = jobs_builder(DB.get_jobs())
# return render_template("jobs.html", data_server_builder=b64encode(dumps(builder).encode("utf-8")).decode("ascii"))
return render_template("jobs.html") # TODO
@jobs.route("/jobs/download", methods=["GET"])

View file

@ -3,8 +3,8 @@ from datetime import datetime
from flask import Blueprint, flash, redirect, render_template, request, session, url_for
from flask_login import current_user, login_user
from dependencies import DB
from utils import LOGGER
from app.dependencies import DB
from app.utils import LOGGER
login = Blueprint("login", __name__)
@ -19,28 +19,31 @@ def login_page():
fail = False
if request.method == "POST" and "username" in request.form and "password" in request.form:
LOGGER.debug(request.form)
LOGGER.warning(f"Login attempt from {request.remote_addr} with username \"{request.form['username']}\"")
ui_user = DB.get_ui_user(username=request.form["username"])
if ui_user and ui_user.username == request.form["username"] and ui_user.check_password(request.form["password"]):
# log the user in
session["user_agent"] = request.headers.get("User-Agent")
session["totp_validated"] = False
session["flash_messages"] = []
ui_user.last_login_at = datetime.now()
ui_user.last_login_ip = request.remote_addr
ui_user.login_count += 1
ret = DB.mark_ui_user_login(
ui_user.username,
datetime.now().astimezone(),
request.remote_addr,
request.headers.get("User-Agent"),
)
if isinstance(ret, str):
LOGGER.error(f"Couldn't mark the user login: {ret}")
else:
session["session_id"] = ret
DB.mark_ui_user_login(ui_user.username, ui_user.last_login_at, ui_user.last_login_ip)
if not login_user(ui_user, remember=request.form.get("remember") == "on"):
if not login_user(ui_user, remember=request.form.get("remember-me") == "on"):
flash("Couldn't log you in, please try again", "error")
return (render_template("login.html", error="Couldn't log you in, please try again"),)
LOGGER.info(
f"User {ui_user.username} logged in successfully for the {str(ui_user.login_count) + ('th' if 10 <= ui_user.login_count % 100 <= 20 else {1: 'st', 2: 'nd', 3: 'rd'}.get(ui_user.login_count % 10, 'th'))} time"
+ (" with remember me" if request.form.get("remember") == "on" else "")
)
LOGGER.info(f"User {ui_user.username} logged in successfully" + (" with remember me" if request.form.get("remember-me") == "on" else ""))
# redirect him to the page he originally wanted or to the home page
return redirect(url_for("loading", next=request.form.get("next") or url_for("home.home_page")))

View file

@ -1,5 +1,3 @@
from base64 import b64encode
from json import dumps
from os.path import isabs, sep
from pathlib import Path
@ -7,9 +5,7 @@ from flask import Blueprint, Response, render_template, request
from flask_login import login_required
from werkzeug.utils import secure_filename
from builder.logs import logs_builder # type: ignore
from pages.utils import error_message
from app.routes.utils import error_message
logs = Blueprint("logs", __name__)
@ -34,10 +30,11 @@ def logs_page():
if isabs(current_file) or ".." in current_file:
return error_message("Invalid file path", 400)
raw_logs = ""
if current_file:
with logs_path.joinpath(current_file).open(encoding="utf-8") as f:
raw_logs = f.read()
# raw_logs = ""
# if current_file:
# with logs_path.joinpath(current_file).open(encoding="utf-8") as f:
# raw_logs = f.read()
builder = logs_builder(files, current_file, raw_logs)
return render_template("logs.html", data_server_builder=b64encode(dumps(builder).encode("utf-8")).decode("ascii"))
# builder = logs_builder(files, current_file, raw_logs)
# return render_template("logs.html", data_server_builder=b64encode(dumps(builder).encode("utf-8")).decode("ascii"))
return render_template("logs.html") # TODO

View file

@ -1,16 +1,9 @@
from base64 import b64encode
from json import dumps
from flask import Blueprint, redirect, render_template, request, url_for
from flask_login import login_required
from builder.advanced_mode import advanced_mode_builder # type: ignore
from builder.easy_mode import easy_mode_builder # type: ignore
from builder.raw_mode import raw_mode_builder # type: ignore
from app.dependencies import DB
from dependencies import BW_CONFIG, DB
from pages.utils import get_service_data, handle_error, update_service
from app.routes.utils import get_service_data, handle_error, update_service
modes = Blueprint("modes", __name__)
@ -36,7 +29,7 @@ def services_modes():
if not request.args.get("mode"):
return handle_error("Mode type is missing to access /modes.", "services")
mode = request.args.get("mode")
# mode = request.args.get("mode")
service_name = request.args.get("service_name")
total_config = DB.get_config(methods=True, with_drafts=True)
service_names = total_config["SERVER_NAME"]["value"].split(" ")
@ -44,17 +37,18 @@ def services_modes():
if service_name and service_name not in service_names:
return handle_error("Service name not found to access advanced mode.", "services")
global_config = BW_CONFIG.get_config(global_only=True, methods=True)
plugins = BW_CONFIG.get_plugins()
# global_config = BW_CONFIG.get_config(global_only=True, methods=True)
# plugins = BW_CONFIG.get_plugins()
builder = None
templates_db = DB.get_templates()
# builder = None
# templates_db = DB.get_templates()
if mode == "raw":
builder = raw_mode_builder(templates_db, plugins, global_config, total_config, service_name or "new", not service_name)
elif mode == "advanced":
builder = advanced_mode_builder(templates_db, plugins, global_config, total_config, service_name or "new", not service_name)
elif mode == "easy":
builder = easy_mode_builder(templates_db, plugins, global_config, total_config, service_name or "new", not service_name)
# if mode == "raw":
# builder = raw_mode_builder(templates_db, plugins, global_config, total_config, service_name or "new", not service_name)
# elif mode == "advanced":
# builder = advanced_mode_builder(templates_db, plugins, global_config, total_config, service_name or "new", not service_name)
# elif mode == "easy":
# builder = easy_mode_builder(templates_db, plugins, global_config, total_config, service_name or "new", not service_name)
return render_template("modes.html", data_server_builder=b64encode(dumps(builder).encode("utf-8")).decode("ascii"))
# return render_template("modes.html", data_server_builder=b64encode(dumps(builder).encode("utf-8")).decode("ascii"))
return render_template("modes.html") # TODO

View file

@ -1,8 +1,7 @@
from base64 import b64encode
from copy import deepcopy
from importlib.machinery import SourceFileLoader
from io import BytesIO
from json import JSONDecodeError, dumps, loads as json_loads
from json import JSONDecodeError, loads as json_loads
from os import listdir
from os.path import basename, dirname, isabs, join, sep
from pathlib import Path
@ -22,12 +21,10 @@ from werkzeug.utils import secure_filename
from common_utils import bytes_hash # type: ignore
from builder.plugins import plugins_builder # type: ignore
from app.dependencies import BW_CONFIG, BW_INSTANCES_UTILS, DATA, DB
from app.utils import LOGGER, PLUGIN_NAME_RX, TMP_DIR
from dependencies import BW_CONFIG, BW_INSTANCES_UTILS, DATA, DB
from utils import LOGGER, PLUGIN_NAME_RX, TMP_DIR
from pages.utils import PLUGIN_ID_RX, PLUGIN_KEYS, error_message, handle_error, verify_data_in_form, wait_applying
from app.routes.utils import PLUGIN_ID_RX, PLUGIN_KEYS, error_message, handle_error, verify_data_in_form, wait_applying
plugins = Blueprint("plugins", __name__)
@ -345,8 +342,9 @@ def plugins_page():
if tmp_ui_path.is_dir():
rmtree(tmp_ui_path, ignore_errors=True)
builder = plugins_builder(DB.get_plugins())
return render_template("plugins.html", data_server_builder=b64encode(dumps(builder).encode("utf-8")).decode("ascii"))
# builder = plugins_builder(DB.get_plugins())
# return render_template("plugins.html", data_server_builder=b64encode(dumps(builder).encode("utf-8")).decode("ascii"))
return render_template("plugins.html") # TODO
@plugins.route("/plugins/delete", methods=["POST"])

View file

@ -1,16 +1,13 @@
from base64 import b64encode
from json import dumps
from flask import Blueprint, Response, flash, redirect, render_template, request, url_for, session
from flask import Blueprint, flash, redirect, render_template, request, url_for, session
from flask_login import current_user, login_required, logout_user
from user_agents import parse
from builder.profile import profile_builder # type: ignore
from app.models.totp import totp as TOTP
from src.totp import totp as TOTP
from app.dependencies import DB
from app.utils import USER_PASSWORD_RX, gen_password_hash
from dependencies import DB
from utils import USER_PASSWORD_RX, gen_password_hash
from pages.utils import handle_error, verify_data_in_form
from app.routes.utils import handle_error, verify_data_in_form
profile = Blueprint("profile", __name__)
@ -23,19 +20,28 @@ def profile_page():
session["tmp_totp_secret"] = TOTP.generate_totp_secret()
totp_qr_image = TOTP.generate_qrcode(current_user.get_id(), session["tmp_totp_secret"])
builder = profile_builder(
current_user if current_user.is_authenticated else None,
{
"is_totp": bool(current_user.totp_secret),
"totp_image": totp_qr_image,
"totp_recovery_codes": current_user.list_recovery_codes,
"is_recovery_refreshed": False,
"totp_secret": TOTP.get_totp_pretty_key(session.get("tmp_totp_secret", "")),
},
)
last_sessions = []
for db_session in DB.get_ui_user_sessions(current_user.username):
ua_data = parse(db_session.user_agent)
last_sessions.append(
{
"browser": ua_data.get_browser(),
"os": ua_data.get_os(),
"ip": db_session.ip,
"creation_date": db_session.creation_date.strftime("%Y-%m-%d %H:%M:%S %Z"),
"last_activity": db_session.last_activity.strftime("%Y-%m-%d %H:%M:%S %Z"),
}
)
# TODO: Show user backup codes after TOTP refresh + add refresh feature
return render_template("profile.html", data_server_builder=b64encode(dumps(builder).encode("utf-8")).decode("ascii"))
return render_template(
"profile.html",
is_totp=bool(current_user.totp_secret),
totp_qr_image=totp_qr_image,
totp_recovery_codes=session.pop("decrypted_recovery_codes", current_user.list_recovery_codes),
is_recovery_refreshed=session.pop("totp_refreshed", False),
totp_secret=TOTP.get_totp_pretty_key(session.get("tmp_totp_secret", "")),
last_sessions=last_sessions,
)
@profile.route("/profile/totp-refresh", methods=["POST"])
@ -47,9 +53,9 @@ def totp_refresh():
if not bool(current_user.totp_secret):
return handle_error("Two-factor authentication is not enabled.", "profile")
verify_data_in_form(data={"curr_password": None}, err_message="Missing current password parameter on /profile/totp-refresh.", redirect_url="profile")
verify_data_in_form(data={"password": None}, err_message="Missing current password parameter on /profile/totp-refresh.", redirect_url="profile")
if not current_user.check_password(request.form["curr_password"]):
if not current_user.check_password(request.form["password"]):
return handle_error("The current password is incorrect.", "profile")
totp_recovery_codes = TOTP.generate_recovery_codes()
@ -58,18 +64,11 @@ def totp_refresh():
if ret:
return handle_error(f"Couldn't refresh the recovery codes in the database: {ret}", "profile")
session["totp_refreshed"] = True
session["decrypted_recovery_codes"] = totp_recovery_codes
flash("The recovery codes have been successfully refreshed. The old ones are no longer valid.")
builder = profile_builder(
current_user if current_user.is_authenticated else None,
{
"is_totp": True,
"totp_image": "",
"totp_recovery_codes": totp_recovery_codes,
"is_recovery_refreshed": True,
"totp_secret": TOTP.get_totp_pretty_key(current_user.totp_secret),
},
)
return render_template("profile.html", data_server_builder=b64encode(dumps(builder).encode("utf-8")).decode("ascii"))
return redirect(url_for("profile.profile_page"))
@profile.route("/profile/totp-disable", methods=["POST"])
@ -81,9 +80,9 @@ def totp_disable():
if not bool(current_user.totp_secret):
return handle_error("Two-factor authentication is not enabled.", "profile")
verify_data_in_form(data={"curr_password": None}, err_message="Missing current password parameter on /profile/totp-disable.", redirect_url="profile")
verify_data_in_form(data={"password": None}, err_message="Missing current password parameter on /profile/totp-disable.", redirect_url="profile")
if not current_user.check_password(request.form["curr_password"]):
if not current_user.check_password(request.form["password"]):
return handle_error("The current password is incorrect.", "profile")
verify_data_in_form(data={"totp_token": None}, err_message="Missing totp token parameter on /profile/totp-enable.", redirect_url="profile")
@ -93,14 +92,14 @@ def totp_disable():
):
return handle_error("The totp token is invalid.", "profile")
ret = DB.update_ui_user(current_user.get_id(), current_user.password, None, method=current_user.method)
ret = DB.update_ui_user(current_user.get_id(), current_user.password.encode("utf-8"), None, method=current_user.method)
if ret:
return handle_error(f"Couldn't disable the two-factor authentication in the database: {ret}", "profile")
session["totp_validated"] = False
flash("The two-factor authentication has been successfully disabled.")
return redirect(url_for("profile.profile_page", message="Disabling two-factor authentication."))
return redirect(url_for("profile.profile_page"))
@profile.route("/profile/totp-enable", methods=["POST"])
@ -112,10 +111,10 @@ def totp_enable():
if bool(current_user.totp_secret):
return handle_error("Two-factor authentication is already enabled.", "profile")
verify_data_in_form(data={"curr_password": None}, err_message="Missing current password parameter on /profile/totp-enable.", redirect_url="profile")
verify_data_in_form(data={"password": None}, err_message="Missing current password parameter on /profile/totp-enable.", redirect_url="profile")
verify_data_in_form(data={"totp_token": None}, err_message="Missing totp token parameter on /profile/totp-enable.", redirect_url="profile")
if not current_user.check_password(request.form["curr_password"]):
if not current_user.check_password(request.form["password"]):
return handle_error("The current password is incorrect.", "profile")
if not TOTP.verify_totp(request.form["totp_token"], totp_secret=session.get("tmp_totp_secret", ""), user=current_user) and not TOTP.verify_recovery_code(
@ -128,7 +127,7 @@ def totp_enable():
ret = DB.update_ui_user(
current_user.get_id(),
current_user.password,
current_user.password.encode("utf-8"),
totp_secret,
totp_recovery_codes=totp_recovery_codes,
method=current_user.method,
@ -137,59 +136,51 @@ def totp_enable():
return handle_error(f"Couldn't enable the two-factor authentication in the database: {ret}", "profile")
session["totp_validated"] = True
session["totp_refreshed"] = True
session["decrypted_recovery_codes"] = totp_recovery_codes
flash("The two-factor authentication has been successfully enabled.")
builder = profile_builder(
current_user if current_user.is_authenticated else None,
{
"is_totp": True,
"totp_image": "",
"totp_recovery_codes": totp_recovery_codes,
"is_recovery_refreshed": True,
"totp_secret": TOTP.get_totp_pretty_key(totp_secret),
},
)
return render_template("profile.html", data_server_builder=b64encode(dumps(builder).encode("utf-8")).decode("ascii"))
return redirect(url_for("profile.profile_page"))
@profile.route("/profile/edit/<string:field>", methods=["POST"])
@profile.route("/profile/edit", methods=["POST"])
@login_required
def edit_profile(field: str):
if field not in ("email", "password"):
return Response(status=404)
def edit_profile():
if DB.readonly:
return handle_error("Database is in read-only mode", "profile")
verify_data_in_form(data={"curr_password": None}, err_message=f"Missing current password parameter on /profile/edit/{field}.", redirect_url="profile")
verify_data_in_form(data={"password": None}, err_message="Missing current password parameter on /profile/edit.", redirect_url="profile")
if not current_user.check_password(request.form["curr_password"]):
if not current_user.check_password(request.form["password"]):
return handle_error("The current password is incorrect.", "profile")
user_data = {
"username": current_user.get_id(),
"password": current_user.password,
"password": current_user.password.encode("utf-8"),
"email": current_user.email,
"totp_secret": current_user.totp_secret,
"method": current_user.method,
}
if field == "email":
verify_data_in_form(data={"email": None}, err_message="Missing email parameter on /profile/edit/email.", redirect_url="profile")
if "username" in request.form:
verify_data_in_form(data={"email": None}, err_message="Missing email parameter on /profile/edit.", redirect_url="profile")
if len(request.form["email"]) > 256:
return handle_error("The email is too long. It must be less than 256 characters.", "profile")
if request.form["email"] and request.form["email"] != current_user.email:
if len(request.form["email"]) > 256:
return handle_error("The email is too long. It must be less than 256 characters.", "profile")
user_data["email"] = request.form["email"] or None
user_data["email"] = request.form["email"]
elif field == "password":
verify_data_in_form(
data={"new_password": None, "new_password_check": None},
err_message="Missing new password or confirm password parameter on /profile/edit/password.",
redirect_url="profile",
)
if request.form["username"] and request.form["username"] != current_user.get_id():
if len(request.form["username"]) > 256:
return handle_error("The username is too long. It must be less than 256 characters.", "profile")
user_data["username"] = request.form["username"]
if request.form["email"] == (current_user.email or "") and request.form["username"] == current_user.get_id():
return handle_error("The username and email are the same as the current ones.", "profile")
elif "new_password" in request.form:
verify_data_in_form(
data={"new_password_confirm": None},
err_message="Missing new password confirm parameter on /profile/edit/password.",
err_message="Missing new password confirm parameter on /profile/edit.",
redirect_url="profile",
)
@ -200,16 +191,39 @@ def edit_profile(field: str):
"The new password is not strong enough. It must contain at least 8 characters, including at least 1 uppercase letter, 1 lowercase letter, 1 number and 1 special character (#@?!$%^&*-).",
"profile",
)
elif current_user.check_password(request.form["new_password"]):
return handle_error("The new password is the same as the current one.", "profile")
user_data["password"] = gen_password_hash(request.form["new_password"])
else:
return handle_error("No fields were updated.", "profile")
ret = DB.update_ui_user(**user_data)
ret = DB.update_ui_user(**user_data, old_username=current_user.get_id())
if ret:
return handle_error(f"Couldn't update the admin user in the database: {ret}", "profile")
if field == "password":
if "new_password" in request.form:
session.clear()
logout_user()
flash(f"The {field} has been successfully updated.")
return redirect(url_for("profile.profile_page" if field == "email" else "login.login_page"))
flash("The profile has been successfully updated.")
return redirect(url_for("profile.profile_page"))
@profile.route("/profile/wipe-old-sessions", methods=["POST"])
@login_required
def wipe_old_sessions():
if DB.readonly:
return handle_error("Database is in read-only mode", "profile")
verify_data_in_form(data={"password": None}, err_message="Missing current password parameter on /profile/wipe-old-sessions.", redirect_url="profile")
if not current_user.check_password(request.form["password"]):
return handle_error("The current password is incorrect.", "profile")
ret = DB.delete_ui_user_old_sessions(current_user.username)
if ret:
return handle_error(f"Couldn't wipe the old sessions in the database: {ret}", "profile")
flash("The old sessions have been successfully wiped.")
return redirect(url_for("profile.profile_page") + "#sessions")

View file

@ -1,13 +1,9 @@
from base64 import b64encode
from json import dumps
from math import floor
from flask import Blueprint, render_template
from flask_login import login_required
from dependencies import BW_INSTANCES_UTILS
from builder.reports import reports_builder # type: ignore
from app.dependencies import BW_INSTANCES_UTILS
reports = Blueprint("reports", __name__)
@ -35,5 +31,6 @@ def reports_page():
methods.add(report["method"])
codes.add(report["code"])
builder = reports_builder(reports_items, list(reasons), list(countries), list(methods), list(codes))
return render_template("reports.html", data_server_builder=b64encode(dumps(builder).encode("utf-8")).decode("ascii"))
# builder = reports_builder(reports_items, list(reasons), list(countries), list(methods), list(codes))
# return render_template("reports.html", data_server_builder=b64encode(dumps(builder).encode("utf-8")).decode("ascii"))
return render_template("reports.html")

View file

@ -1,14 +1,9 @@
from base64 import b64encode
from json import dumps
from flask import Blueprint, redirect, render_template, request, url_for
from flask_login import login_required
from builder.services import services_builder # type: ignore
from app.dependencies import DB
from dependencies import DB
from pages.utils import get_service_data, handle_error, update_service
from app.routes.utils import get_service_data, handle_error, update_service
services = Blueprint("services", __name__)
@ -59,5 +54,6 @@ def services_page():
services.sort(key=lambda x: x["SERVER_NAME"]["value"])
builder = services_builder(services)
return render_template("services.html", data_server_builder=b64encode(dumps(builder).encode("utf-8")).decode("ascii"))
# builder = services_builder(services)
# return render_template("services.html", data_server_builder=b64encode(dumps(builder).encode("utf-8")).decode("ascii"))
return render_template("services.html")

View file

@ -6,10 +6,10 @@ from time import time
from flask import Blueprint, Response, flash, redirect, render_template, request, url_for
from dependencies import BW_CONFIG, DATA, DB
from utils import USER_PASSWORD_RX, gen_password_hash
from app.dependencies import BW_CONFIG, DATA, DB
from app.utils import USER_PASSWORD_RX, gen_password_hash
from pages.utils import REVERSE_PROXY_PATH, handle_error, manage_bunkerweb
from app.routes.utils import REVERSE_PROXY_PATH, handle_error, manage_bunkerweb
setup = Blueprint("setup", __name__)

View file

@ -1,11 +1,9 @@
from flask import Blueprint, redirect, render_template, request, session, url_for
from flask import Blueprint, flash, redirect, render_template, request, session, url_for
from flask_login import current_user, login_required
from src.totp import totp as TOTP
from dependencies import DB
from pages.utils import handle_error, verify_data_in_form
from app.dependencies import DB
from app.models.totp import totp as TOTP
from app.routes.utils import handle_error, verify_data_in_form
totp = Blueprint("totp", __name__)
@ -20,6 +18,7 @@ def totp_page():
recovery_code = TOTP.verify_recovery_code(request.form["totp_token"], user=current_user)
if not recovery_code:
return handle_error("The token is invalid.", "totp")
flash(f"You've used one of your recovery codes. You have {len(current_user.list_recovery_codes)} left.")
DB.use_ui_user_recovery_code(current_user.get_id(), recovery_code)
session["totp_validated"] = True

View file

@ -6,14 +6,14 @@ from threading import Thread
from time import sleep, time
from typing import Any, Dict, Optional, Tuple, Union
from flask import Response, flash, redirect, request, url_for
from flask import Response, flash, redirect, request, session, url_for
from qrcode.main import QRCode
from regex import compile as re_compile
from src.instance import Instance
from app.models.instance import Instance
from dependencies import BW_CONFIG, DATA, DB
from utils import LOGGER
from app.dependencies import BW_CONFIG, DATA, DB
from app.utils import LOGGER
LOG_RX = re_compile(r"^(?P<date>\d+/\d+/\d+\s\d+:\d+:\d+)\s\[(?P<level>[a-z]+)\]\s\d+#\d+:\s(?P<message>[^\n]+)$")
@ -111,6 +111,9 @@ def manage_bunkerweb(method: str, *args, operation: str = "reloads", is_draft: b
else:
flash(f["content"])
if "flash_messages" in session:
session["flash_messages"].append((f["content"], f["type"], datetime.now().strftime("%Y-%m-%d %H:%M:%S %Z")))
DATA["TO_FLASH"] = []
DATA["RELOADING"] = False

24887
src/ui/app/static/css/core.css Normal file

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,194 @@
.menu .app-brand.main {
height: 64px;
margin-top: 12px;
display: flex;
justify-content: center;
align-items: center;
}
.app-brand-link {
display: flex;
justify-content: center;
align-items: center;
width: 100%;
}
.app-brand-logo.main {
width: auto;
flex-shrink: 0;
display: flex;
justify-content: center;
align-items: center;
}
.app-brand-logo.main svg {
width: 50%;
height: auto;
display: block;
margin: auto;
}
.app-brand-text.main {
font-size: 1.75rem;
letter-spacing: -0.5px;
}
/* ! For .layout-navbar-fixed added fix padding top to .layout-page */
/* Detached navbar */
.layout-navbar-fixed
.layout-wrapper:not(.layout-horizontal):not(.layout-without-menu)
.layout-page {
padding-top: 74px !important;
}
/* Default navbar */
.layout-navbar-fixed .layout-wrapper:not(.layout-without-menu) .layout-page {
padding-top: 64px !important;
}
.docs-page
.layout-navbar-fixed.layout-wrapper:not(.layout-without-menu)
.layout-page,
.docs-page
.layout-menu-fixed.layout-wrapper:not(.layout-without-menu)
.layout-page {
padding-top: 62px !important;
}
/* Navbar page z-index issue solution */
.content-wrapper .navbar {
z-index: auto;
}
/*
* Content
******************************************************************************/
.main-blocks > * {
display: block !important;
}
.main-inline-spacing > * {
margin: 1rem 0.375rem 0 0 !important;
}
/* ? .main-vertical-spacing class is used to have vertical margins between elements. To remove margin-top from the first-child, use .main-only-element class with .main-vertical-spacing class. For example, we have used this class in forms-input-groups.html file. */
.main-vertical-spacing > * {
margin-top: 1rem !important;
margin-bottom: 0 !important;
}
.main-vertical-spacing.main-only-element > :first-child {
margin-top: 0 !important;
}
.main-vertical-spacing-lg > * {
margin-top: 1.875rem !important;
margin-bottom: 0 !important;
}
.main-vertical-spacing-lg.main-only-element > :first-child {
margin-top: 0 !important;
}
.main-vertical-spacing-xl > * {
margin-top: 5rem !important;
margin-bottom: 0 !important;
}
.main-vertical-spacing-xl.main-only-element > :first-child {
margin-top: 0 !important;
}
.rtl-only {
display: none !important;
text-align: left !important;
direction: ltr !important;
}
[dir="rtl"] .rtl-only {
display: block !important;
}
/* Dropdown buttons going out of small screens */
@media (max-width: 576px) {
#dropdown-variation-main .btn-group .text-truncate {
width: 231px;
position: relative;
}
#dropdown-variation-main .btn-group .text-truncate::after {
position: absolute;
top: 45%;
right: 0.65rem;
}
}
/*
* Layout main
******************************************************************************/
.layout-main-wrapper {
display: -webkit-box;
display: -ms-flexbox;
display: flex;
-webkit-box-align: center;
-ms-flex-align: center;
align-items: center;
-webkit-box-orient: vertical;
-webkit-box-direction: normal;
-ms-flex-direction: column;
flex-direction: column;
margin-top: 1rem;
}
.layout-main-placeholder img {
width: 900px;
}
.layout-main-info {
text-align: center;
margin-top: 1rem;
}
/*
* Custom
******************************************************************************/
.badge-dot {
padding: 0.35rem;
font-size: 0.6rem;
animation: pulsate 1.7s infinite;
}
.badge-dot-text {
font-size: 0.6rem;
animation: pulsate 1.7s infinite;
}
@keyframes pulsate {
0% {
transform: scale(1);
opacity: 1;
}
50% {
transform: scale(1.5);
opacity: 0.5;
}
100% {
transform: scale(1);
opacity: 1;
}
}
.w-30 {
width: 30% !important;
}
.badge-center-sm {
padding: 2.5px;
line-height: 1.2;
height: 1.25rem;
width: 1.25rem;
font-size: 0.7rem;
}
.badge-center-sm i {
font-size: 0.8rem;
}
.bg-bw-green {
background-color: #2eac68;
}

View file

@ -0,0 +1,31 @@
.img-fluid {
max-width: 45%;
}
.layout-main-wrapper {
text-align: center;
}
.layout-main-info {
margin-top: 2rem;
}
.layout-main-info h2 {
color: #fff;
}
@keyframes pulseAnimation {
0% {
transform: scale(1);
}
50% {
transform: scale(1.1);
}
100% {
transform: scale(1);
}
}
.pulsating {
animation: pulseAnimation 1.5s infinite ease-in-out;
}

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,63 @@
@font-face {
font-family: "Public Sans";
font-style: italic;
font-weight: 300;
font-display: swap;
src: url(../fonts/Public_sans/300-italic.ttf), format("truetype");
}
@font-face {
font-family: "Public Sans";
font-style: italic;
font-weight: 500;
font-display: swap;
src: url(../fonts/Public_sans/500-italic.ttf), format("truetype");
}
@font-face {
font-family: "Public Sans";
font-style: italic;
font-weight: 600;
font-display: swap;
src: url(../fonts/Public_sans/600-italic.ttf), format("truetype");
}
@font-face {
font-family: "Public Sans";
font-style: italic;
font-weight: 700;
font-display: swap;
src: url(../fonts/Public_sans/700-italic.ttf), format("truetype");
}
@font-face {
font-family: "Public Sans";
font-style: normal;
font-weight: 300;
font-display: swap;
src: url(../fonts/Public_sans/300-normal.ttf), format("truetype");
}
@font-face {
font-family: "Public Sans";
font-style: normal;
font-weight: 400;
font-display: swap;
src: url(../fonts/Public_sans/400-normal.ttf), format("truetype");
}
@font-face {
font-family: "Public Sans";
font-style: normal;
font-weight: 500;
font-display: swap;
src: url(../fonts/Public_sans/500-normal.ttf), format("truetype");
}
@font-face {
font-family: "Public Sans";
font-style: normal;
font-weight: 600;
font-display: swap;
src: url(../fonts/Public_sans/600-normal.ttf), format("truetype");
}
@font-face {
font-family: "Public Sans";
font-style: normal;
font-weight: 700;
font-display: swap;
src: url(../fonts/Public_sans/700-normal.ttf), format("truetype");
}

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

File diff suppressed because one or more lines are too long

Binary file not shown.

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

Binary file not shown.

Binary file not shown.

View file

Before

Width:  |  Height:  |  Size: 37 KiB

After

Width:  |  Height:  |  Size: 37 KiB

View file

Before

Width:  |  Height:  |  Size: 48 KiB

After

Width:  |  Height:  |  Size: 48 KiB

View file

Before

Width:  |  Height:  |  Size: 53 KiB

After

Width:  |  Height:  |  Size: 53 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

View file

@ -0,0 +1,477 @@
/*!
* github-buttons v2.29.0
* (c) 2024 なつき
* @license BSD-2-Clause
*/
!(function () {
"use strict";
var e = window.document,
o = e.location,
t = window.Math,
r = window.HTMLElement,
a = window.XMLHttpRequest,
n = "github-button",
i = "https://buttons.github.io/buttons.html",
c = "github.com",
l = "https://api." + c,
d = a && "prototype" in a && "withCredentials" in a.prototype,
s =
d &&
r &&
"attachShadow" in r.prototype &&
!("prototype" in r.prototype.attachShadow),
u = function (e, o) {
for (var t = 0, r = e.length; t < r; t++) o(e[t]);
},
f = function (e) {
return function (o, t, r) {
var a = e.createElement(o);
if (null != t)
for (var n in t) {
var i = t[n];
null != i && (null != a[n] ? (a[n] = i) : a.setAttribute(n, i));
}
return (
null != r &&
u(r, function (o) {
a.appendChild("string" == typeof o ? e.createTextNode(o) : o);
}),
a
);
};
},
h = f(e),
g = function (e) {
var o;
return function () {
o || ((o = 1), e.apply(this, arguments));
};
},
p = function (e, o) {
return {}.hasOwnProperty.call(e, o);
},
b = function (e) {
return ("" + e).toLowerCase();
},
v = function (e, o, t, r) {
null == o && (o = "&"),
null == t && (t = "="),
null == r && (r = window.decodeURIComponent);
var a = {};
return (
u(e.split(o), function (e) {
if ("" !== e) {
var o = e.split(t);
a[r(o[0])] = null != o[1] ? r(o.slice(1).join(t)) : void 0;
}
}),
a
);
},
m = function (e, o, t) {
e.addEventListener
? e.addEventListener(o, t, !1)
: e.attachEvent("on" + o, t);
},
w = function (e, o, t) {
e.removeEventListener
? e.removeEventListener(o, t, !1)
: e.detachEvent("on" + o, t);
},
k = function (e, o, t) {
var r = function () {
return w(e, o, r), t.apply(this, arguments);
};
m(e, o, r);
},
x = function (e, o, t) {
if (null != e.readyState) {
var r = "readystatechange",
a = function () {
if (o.test(e.readyState))
return w(e, r, a), t.apply(this, arguments);
};
m(e, r, a);
}
},
y = {
light:
".btn:focus-visible,.social-count:focus-visible{outline:2px solid #0969da;outline-offset:-2px}.btn{color:#25292e;background-color:#ebf0f4;border-color:#d1d9e0;background-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg'%3e%3clinearGradient id='o' x2='0' y2='1'%3e%3cstop stop-color='%23f6f8fa'/%3e%3cstop offset='90%25' stop-color='%23ebf0f4'/%3e%3c/linearGradient%3e%3crect width='100%25' height='100%25' fill='url(%23o)'/%3e%3c/svg%3e\");background-image:-moz-linear-gradient(top, #f6f8fa, #ebf0f4 90%);background-image:linear-gradient(180deg, #f6f8fa, #ebf0f4 90%);filter:progid:DXImageTransform.Microsoft.Gradient(startColorstr='#FFF6F8FA', endColorstr='#FFEAEFF3')}:root .btn{filter:none}.btn:hover,.btn:focus{background-color:#e5eaee;background-position:0 -0.5em;border-color:#d1d9e0;background-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg'%3e%3clinearGradient id='o' x2='0' y2='1'%3e%3cstop stop-color='%23eff2f5'/%3e%3cstop offset='90%25' stop-color='%23e5eaee'/%3e%3c/linearGradient%3e%3crect width='100%25' height='100%25' fill='url(%23o)'/%3e%3c/svg%3e\");background-image:-moz-linear-gradient(top, #eff2f5, #e5eaee 90%);background-image:linear-gradient(180deg, #eff2f5, #e5eaee 90%);filter:progid:DXImageTransform.Microsoft.Gradient(startColorstr='#FFEFF2F5', endColorstr='#FFE4E9ED')}:root .btn:hover,:root .btn:focus{filter:none}.btn:active{background-color:#e6eaef;border-color:#d1d9e0;background-image:none;filter:none}.social-count{color:#25292e;background-color:#fff;border-color:#d1d9e0}.social-count:hover,.social-count:focus{color:#0969da}.octicon-heart{color:#bf3989}",
light_high_contrast:
".btn:focus-visible,.social-count:focus-visible{outline:2px solid #0349b4;outline-offset:-2px}.btn{color:#25292e;background-color:#e0e6eb;border-color:#454c54;background-image:none;filter:none}.btn:hover,.btn:focus{background-color:#d0d7e0;background-position:0 -0.5em;border-color:#454c54;background-image:none;filter:none}.btn:active{background-color:#d1d9e0;border-color:#454c54}.social-count{color:#25292e;background-color:#fff;border-color:#454c54}.social-count:hover,.social-count:focus{color:#023b95}.octicon-heart{color:#7d0c57}",
dark: ".btn:focus-visible,.social-count:focus-visible{outline:2px solid #1f6feb;outline-offset:-2px}.btn{color:#f0f6fc;background-color:#1a2026;border-color:#3d444d;background-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg'%3e%3clinearGradient id='o' x2='0' y2='1'%3e%3cstop stop-color='%23212830'/%3e%3cstop offset='90%25' stop-color='%231a2026'/%3e%3c/linearGradient%3e%3crect width='100%25' height='100%25' fill='url(%23o)'/%3e%3c/svg%3e\");background-image:-moz-linear-gradient(top, #212830, #1a2026 90%);background-image:linear-gradient(180deg, #212830, #1a2026 90%);filter:progid:DXImageTransform.Microsoft.Gradient(startColorstr='#FF212830', endColorstr='#FF191F25')}:root .btn{filter:none}.btn:hover,.btn:focus{background-color:#1f242c;background-position:0 -0.5em;border-color:#3d444d;background-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg'%3e%3clinearGradient id='o' x2='0' y2='1'%3e%3cstop stop-color='%23262c36'/%3e%3cstop offset='90%25' stop-color='%231f242c'/%3e%3c/linearGradient%3e%3crect width='100%25' height='100%25' fill='url(%23o)'/%3e%3c/svg%3e\");background-image:-moz-linear-gradient(top, #262c36, #1f242c 90%);background-image:linear-gradient(180deg, #262c36, #1f242c 90%);filter:progid:DXImageTransform.Microsoft.Gradient(startColorstr='#FF262C36', endColorstr='#FF1E232B')}:root .btn:hover,:root .btn:focus{filter:none}.btn:active{background-color:#2a313c;border-color:#3d444d;background-image:none;filter:none}.social-count{color:#f0f6fc;background-color:#0d1117;border-color:#3d444d}.social-count:hover,.social-count:focus{color:#388bfd}.octicon-heart{color:#db61a2}",
dark_dimmed:
".btn:focus-visible,.social-count:focus-visible{outline:2px solid #316dca;outline-offset:-2px}.btn{color:#d1d7e0;background-color:#232932;border-color:#3d444d;background-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg'%3e%3clinearGradient id='o' x2='0' y2='1'%3e%3cstop stop-color='%232a313c'/%3e%3cstop offset='90%25' stop-color='%23232932'/%3e%3c/linearGradient%3e%3crect width='100%25' height='100%25' fill='url(%23o)'/%3e%3c/svg%3e\");background-image:-moz-linear-gradient(top, #2a313c, #232932 90%);background-image:linear-gradient(180deg, #2a313c, #232932 90%);filter:progid:DXImageTransform.Microsoft.Gradient(startColorstr='#FF2A313C', endColorstr='#FF222831')}:root .btn{filter:none}.btn:hover,.btn:focus{background-color:#282f38;background-position:0 -0.5em;border-color:#3d444d;background-image:url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg'%3e%3clinearGradient id='o' x2='0' y2='1'%3e%3cstop stop-color='%232f3742'/%3e%3cstop offset='90%25' stop-color='%23282f38'/%3e%3c/linearGradient%3e%3crect width='100%25' height='100%25' fill='url(%23o)'/%3e%3c/svg%3e\");background-image:-moz-linear-gradient(top, #2f3742, #282f38 90%);background-image:linear-gradient(180deg, #2f3742, #282f38 90%);filter:progid:DXImageTransform.Microsoft.Gradient(startColorstr='#FF2F3742', endColorstr='#FF272E37')}:root .btn:hover,:root .btn:focus{filter:none}.btn:active{background-color:#3d444d;border-color:#3d444d;background-image:none;filter:none}.social-count{color:#d1d7e0;background-color:#212830;border-color:#3d444d}.social-count:hover,.social-count:focus{color:#4184e4}.octicon-heart{color:#c96198}",
dark_high_contrast:
".btn:focus-visible,.social-count:focus-visible{outline:2px solid #409eff;outline-offset:-2px}.btn{color:#fff;background-color:#262c36;border-color:#b7bdc8;background-image:none;filter:none}.btn:hover,.btn:focus{background-color:#232932;background-position:0 -0.5em;border-color:#b7bdc8;background-image:none;filter:none}.btn:active{background-color:#2f3742;border-color:#b7bdc8}.social-count{color:#fff;background-color:#010409;border-color:#b7bdc8}.social-count:hover,.social-count:focus{color:#5cacff}.octicon-heart{color:#ff90c8}",
},
C = function (e, o) {
return (
"@media(prefers-color-scheme:" + e + "){" + y[p(y, o) ? o : e] + "}"
);
},
M = {
"comment-discussion": {
heights: {
16: {
width: 16,
path: '<path d="M1.75 1h8.5c.966 0 1.75.784 1.75 1.75v5.5A1.75 1.75 0 0 1 10.25 10H7.061l-2.574 2.573A1.458 1.458 0 0 1 2 11.543V10h-.25A1.75 1.75 0 0 1 0 8.25v-5.5C0 1.784.784 1 1.75 1ZM1.5 2.75v5.5c0 .138.112.25.25.25h1a.75.75 0 0 1 .75.75v2.19l2.72-2.72a.749.749 0 0 1 .53-.22h3.5a.25.25 0 0 0 .25-.25v-5.5a.25.25 0 0 0-.25-.25h-8.5a.25.25 0 0 0-.25.25Zm13 2a.25.25 0 0 0-.25-.25h-.5a.75.75 0 0 1 0-1.5h.5c.966 0 1.75.784 1.75 1.75v5.5A1.75 1.75 0 0 1 14.25 12H14v1.543a1.458 1.458 0 0 1-2.487 1.03L9.22 12.28a.749.749 0 0 1 .326-1.275.749.749 0 0 1 .734.215l2.22 2.22v-2.19a.75.75 0 0 1 .75-.75h1a.25.25 0 0 0 .25-.25Z"></path>',
},
},
},
download: {
heights: {
16: {
width: 16,
path: '<path d="M2.75 14A1.75 1.75 0 0 1 1 12.25v-2.5a.75.75 0 0 1 1.5 0v2.5c0 .138.112.25.25.25h10.5a.25.25 0 0 0 .25-.25v-2.5a.75.75 0 0 1 1.5 0v2.5A1.75 1.75 0 0 1 13.25 14Z"></path><path d="M7.25 7.689V2a.75.75 0 0 1 1.5 0v5.689l1.97-1.969a.749.749 0 1 1 1.06 1.06l-3.25 3.25a.749.749 0 0 1-1.06 0L4.22 6.78a.749.749 0 1 1 1.06-1.06l1.97 1.969Z"></path>',
},
},
},
eye: {
heights: {
16: {
width: 16,
path: '<path d="M8 2c1.981 0 3.671.992 4.933 2.078 1.27 1.091 2.187 2.345 2.637 3.023a1.62 1.62 0 0 1 0 1.798c-.45.678-1.367 1.932-2.637 3.023C11.67 13.008 9.981 14 8 14c-1.981 0-3.671-.992-4.933-2.078C1.797 10.83.88 9.576.43 8.898a1.62 1.62 0 0 1 0-1.798c.45-.677 1.367-1.931 2.637-3.022C4.33 2.992 6.019 2 8 2ZM1.679 7.932a.12.12 0 0 0 0 .136c.411.622 1.241 1.75 2.366 2.717C5.176 11.758 6.527 12.5 8 12.5c1.473 0 2.825-.742 3.955-1.715 1.124-.967 1.954-2.096 2.366-2.717a.12.12 0 0 0 0-.136c-.412-.621-1.242-1.75-2.366-2.717C10.824 4.242 9.473 3.5 8 3.5c-1.473 0-2.825.742-3.955 1.715-1.124.967-1.954 2.096-2.366 2.717ZM8 10a2 2 0 1 1-.001-3.999A2 2 0 0 1 8 10Z"></path>',
},
},
},
heart: {
heights: {
16: {
width: 16,
path: '<path d="m8 14.25.345.666a.75.75 0 0 1-.69 0l-.008-.004-.018-.01a7.152 7.152 0 0 1-.31-.17 22.055 22.055 0 0 1-3.434-2.414C2.045 10.731 0 8.35 0 5.5 0 2.836 2.086 1 4.25 1 5.797 1 7.153 1.802 8 3.02 8.847 1.802 10.203 1 11.75 1 13.914 1 16 2.836 16 5.5c0 2.85-2.045 5.231-3.885 6.818a22.066 22.066 0 0 1-3.744 2.584l-.018.01-.006.003h-.002ZM4.25 2.5c-1.336 0-2.75 1.164-2.75 3 0 2.15 1.58 4.144 3.365 5.682A20.58 20.58 0 0 0 8 13.393a20.58 20.58 0 0 0 3.135-2.211C12.92 9.644 14.5 7.65 14.5 5.5c0-1.836-1.414-3-2.75-3-1.373 0-2.609.986-3.029 2.456a.749.749 0 0 1-1.442 0C6.859 3.486 5.623 2.5 4.25 2.5Z"></path>',
},
},
},
"issue-opened": {
heights: {
16: {
width: 16,
path: '<path d="M8 9.5a1.5 1.5 0 1 0 0-3 1.5 1.5 0 0 0 0 3Z"></path><path d="M8 0a8 8 0 1 1 0 16A8 8 0 0 1 8 0ZM1.5 8a6.5 6.5 0 1 0 13 0 6.5 6.5 0 0 0-13 0Z"></path>',
},
},
},
"mark-github": {
heights: {
16: {
width: 16,
path: '<path d="M8 0c4.42 0 8 3.58 8 8a8.013 8.013 0 0 1-5.45 7.59c-.4.08-.55-.17-.55-.38 0-.27.01-1.13.01-2.2 0-.75-.25-1.23-.54-1.48 1.78-.2 3.65-.88 3.65-3.95 0-.88-.31-1.59-.82-2.15.08-.2.36-1.02-.08-2.12 0 0-.67-.22-2.2.82-.64-.18-1.32-.27-2-.27-.68 0-1.36.09-2 .27-1.53-1.03-2.2-.82-2.2-.82-.44 1.1-.16 1.92-.08 2.12-.51.56-.82 1.28-.82 2.15 0 3.06 1.86 3.75 3.64 3.95-.23.2-.44.55-.51 1.07-.46.21-1.61.55-2.33-.66-.15-.24-.6-.83-1.23-.82-.67.01-.27.38.01.53.34.19.73.9.82 1.13.16.45.68 1.31 2.69.94 0 .67.01 1.3.01 1.49 0 .21-.15.45-.55.38A7.995 7.995 0 0 1 0 8c0-4.42 3.58-8 8-8Z"></path>',
},
},
},
package: {
heights: {
16: {
width: 16,
path: '<path d="m8.878.392 5.25 3.045c.54.314.872.89.872 1.514v6.098a1.75 1.75 0 0 1-.872 1.514l-5.25 3.045a1.75 1.75 0 0 1-1.756 0l-5.25-3.045A1.75 1.75 0 0 1 1 11.049V4.951c0-.624.332-1.201.872-1.514L7.122.392a1.75 1.75 0 0 1 1.756 0ZM7.875 1.69l-4.63 2.685L8 7.133l4.755-2.758-4.63-2.685a.248.248 0 0 0-.25 0ZM2.5 5.677v5.372c0 .09.047.171.125.216l4.625 2.683V8.432Zm6.25 8.271 4.625-2.683a.25.25 0 0 0 .125-.216V5.677L8.75 8.432Z"></path>',
},
},
},
play: {
heights: {
16: {
width: 16,
path: '<path d="M8 0a8 8 0 1 1 0 16A8 8 0 0 1 8 0ZM1.5 8a6.5 6.5 0 1 0 13 0 6.5 6.5 0 0 0-13 0Zm4.879-2.773 4.264 2.559a.25.25 0 0 1 0 .428l-4.264 2.559A.25.25 0 0 1 6 10.559V5.442a.25.25 0 0 1 .379-.215Z"></path>',
},
},
},
"repo-forked": {
heights: {
16: {
width: 16,
path: '<path d="M5 5.372v.878c0 .414.336.75.75.75h4.5a.75.75 0 0 0 .75-.75v-.878a2.25 2.25 0 1 1 1.5 0v.878a2.25 2.25 0 0 1-2.25 2.25h-1.5v2.128a2.251 2.251 0 1 1-1.5 0V8.5h-1.5A2.25 2.25 0 0 1 3.5 6.25v-.878a2.25 2.25 0 1 1 1.5 0ZM5 3.25a.75.75 0 1 0-1.5 0 .75.75 0 0 0 1.5 0Zm6.75.75a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5Zm-3 8.75a.75.75 0 1 0-1.5 0 .75.75 0 0 0 1.5 0Z"></path>',
},
},
},
"repo-template": {
heights: {
16: {
width: 16,
path: '<path d="M13.25 8a.75.75 0 0 1 .75.75v4.5a.75.75 0 0 1-.75.75h-2.5a.75.75 0 0 1 0-1.5h1.75v-2h-.75a.75.75 0 0 1 0-1.5h.75v-.25a.75.75 0 0 1 .75-.75ZM5 12.25a.25.25 0 0 1 .25-.25h3.5a.25.25 0 0 1 .25.25v3.25a.25.25 0 0 1-.4.2l-1.45-1.087a.249.249 0 0 0-.3 0L5.4 15.7a.25.25 0 0 1-.4-.2ZM2.75 8a.75.75 0 0 1 .75.75v.268c.083-.012.166-.018.25-.018h.5a.75.75 0 0 1 0 1.5h-.5a.25.25 0 0 0-.25.25v.75c0 .28.114.532.3.714a.75.75 0 1 1-1.05 1.072A2.495 2.495 0 0 1 2 11.5V8.75A.75.75 0 0 1 2.75 8ZM11 .75a.75.75 0 0 1 .75-.75h1.5a.75.75 0 0 1 .75.75v1.5a.75.75 0 0 1-1.5 0V1.5h-.75A.75.75 0 0 1 11 .75Zm-5 0A.75.75 0 0 1 6.75 0h2.5a.75.75 0 0 1 0 1.5h-2.5A.75.75 0 0 1 6 .75Zm0 9A.75.75 0 0 1 6.75 9h2.5a.75.75 0 0 1 0 1.5h-2.5A.75.75 0 0 1 6 9.75ZM4.992.662a.75.75 0 0 1-.636.848c-.436.063-.783.41-.846.846a.751.751 0 0 1-1.485-.212A2.501 2.501 0 0 1 4.144.025a.75.75 0 0 1 .848.637ZM2.75 4a.75.75 0 0 1 .75.75v1.5a.75.75 0 0 1-1.5 0v-1.5A.75.75 0 0 1 2.75 4Zm10.5 0a.75.75 0 0 1 .75.75v1.5a.75.75 0 0 1-1.5 0v-1.5a.75.75 0 0 1 .75-.75Z"></path>',
},
},
},
star: {
heights: {
16: {
width: 16,
path: '<path d="M8 .25a.75.75 0 0 1 .673.418l1.882 3.815 4.21.612a.75.75 0 0 1 .416 1.279l-3.046 2.97.719 4.192a.751.751 0 0 1-1.088.791L8 12.347l-3.766 1.98a.75.75 0 0 1-1.088-.79l.72-4.194L.818 6.374a.75.75 0 0 1 .416-1.28l4.21-.611L7.327.668A.75.75 0 0 1 8 .25Zm0 2.445L6.615 5.5a.75.75 0 0 1-.564.41l-3.097.45 2.24 2.184a.75.75 0 0 1 .216.664l-.528 3.084 2.769-1.456a.75.75 0 0 1 .698 0l2.77 1.456-.53-3.084a.75.75 0 0 1 .216-.664l2.24-2.183-3.096-.45a.75.75 0 0 1-.564-.41L8 2.694Z"></path>',
},
},
},
},
Z = function (e, o) {
(e = b(e).replace(/^octicon-/, "")), p(M, e) || (e = "mark-github");
var t = o >= 24 && 24 in M[e].heights ? 24 : 16,
r = M[e].heights[t];
return (
'<svg viewBox="0 0 ' +
r.width +
" " +
t +
'" width="' +
(o * r.width) / t +
'" height="' +
o +
'" class="octicon octicon-' +
e +
'" aria-hidden="true">' +
r.path +
"</svg>"
);
},
A = {},
F = function (e, o) {
var t = A[e] || (A[e] = []);
if (!(t.push(o) > 1)) {
var r = g(function () {
for (delete A[e]; (o = t.shift()); ) o.apply(null, arguments);
});
if (d) {
var n = new a();
m(n, "abort", r),
m(n, "error", r),
m(n, "load", function () {
var e;
try {
e = JSON.parse(this.responseText);
} catch (e) {
return void r(e);
}
r(200 !== this.status, e);
}),
n.open("GET", e),
n.send();
} else {
var i = this || window;
i._ = function (e) {
(i._ = null), r(200 !== e.meta.status, e.data);
};
var c = f(i.document)("script", {
async: !0,
src: e + (-1 !== e.indexOf("?") ? "&" : "?") + "callback=_",
}),
l = function () {
i._ && i._({ meta: {} });
};
m(c, "load", l),
m(c, "error", l),
x(c, /de|m/, l),
i.document.getElementsByTagName("head")[0].appendChild(c);
}
}
},
E = function (e, o, t) {
var r = f(e.ownerDocument),
a = e.appendChild(r("style", { type: "text/css" })),
n =
"body{margin:0}a{text-decoration:none;outline:0}.widget{display:inline-block;overflow:hidden;font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Helvetica,Arial,sans-serif;font-size:0;line-height:0;white-space:nowrap}.btn,.social-count{position:relative;display:inline-block;display:inline-flex;height:14px;padding:2px 5px;font-size:11px;font-weight:600;line-height:14px;vertical-align:bottom;cursor:pointer;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;background-repeat:repeat-x;background-position:-1px -1px;background-size:110% 110%;border:1px solid}.btn{border-radius:.25em}.btn:not(:last-child){border-radius:.25em 0 0 .25em}.social-count{border-left:0;border-radius:0 .25em .25em 0}.widget-lg .btn,.widget-lg .social-count{height:16px;padding:5px 10px;font-size:12px;line-height:16px}.octicon{display:inline-block;vertical-align:text-top;fill:currentColor;overflow:visible}" +
(function (e) {
if (null == e) return y.light;
if (p(y, e)) return y[e];
var o = v(e, ";", ":", function (e) {
return e.replace(/^[ \t\n\f\r]+|[ \t\n\f\r]+$/g, "");
});
return (
y[p(y, o["no-preference"]) ? o["no-preference"] : "light"] +
C("light", o.light) +
C("dark", o.dark)
);
})(o["data-color-scheme"]);
a.styleSheet
? (a.styleSheet.cssText = n)
: a.appendChild(e.ownerDocument.createTextNode(n));
var i = "large" === b(o["data-size"]),
d = r(
"a",
{
className: "btn",
href: o.href,
rel: "noopener",
target: "_blank",
title: o.title || void 0,
"aria-label": o["aria-label"] || void 0,
innerHTML: Z(o["data-icon"], i ? 16 : 14) + "&nbsp;",
},
[r("span", {}, [o["data-text"] || ""])],
),
s = e.appendChild(
r("div", { className: "widget" + (i ? " widget-lg" : "") }, [d]),
),
u = d.hostname.replace(/\.$/, "");
if (("." + u).substring(u.length - 10) !== "." + c)
return d.removeAttribute("href"), void t(s);
var h = (" /" + d.pathname).split(/\/+/);
if (
((((u === c || u === "gist." + c) && "archive" === h[3]) ||
(u === c &&
"releases" === h[3] &&
("download" === h[4] ||
("latest" === h[4] && "download" === h[5]))) ||
u === "codeload." + c) &&
(d.target = "_top"),
"true" === b(o["data-show-count"]) &&
u === c &&
"marketplace" !== h[1] &&
"sponsors" !== h[1] &&
"orgs" !== h[1] &&
"users" !== h[1] &&
"-" !== h[1])
) {
var g, m;
if (!h[2] && h[1]) (m = "followers"), (g = "?tab=followers");
else if (!h[3] && h[2]) (m = "stargazers_count"), (g = "/stargazers");
else if (h[4] || "subscription" !== h[3])
if (h[4] || "fork" !== h[3]) {
if ("issues" !== h[3]) return void t(s);
(m = "open_issues_count"), (g = "/issues");
} else (m = "forks_count"), (g = "/forks");
else (m = "subscribers_count"), (g = "/watchers");
var w = h[2] ? "/repos/" + h[1] + "/" + h[2] : "/users/" + h[1];
F.call(this, l + w, function (e, o) {
if (!e) {
var a = o[m];
s.appendChild(
r(
"a",
{
className: "social-count",
href: o.html_url + g,
rel: "noopener",
target: "_blank",
"aria-label":
a +
" " +
m
.replace(/_count$/, "")
.replace("_", " ")
.slice(0, a < 2 ? -1 : void 0) +
" on GitHub",
},
[("" + a).replace(/\B(?=(\d{3})+(?!\d))/g, ",")],
),
);
}
t(s);
});
} else t(s);
},
L = window.devicePixelRatio || 1,
_ = function (e) {
return (L > 1 ? t.ceil((t.round(e * L) / L) * 2) / 2 : t.ceil(e)) || 0;
},
G = function (e, o) {
(e.style.width = o[0] + "px"), (e.style.height = o[1] + "px");
},
T = function (o, r) {
if (null != o && null != r)
if (
(o.getAttribute &&
(o = (function (e) {
var o = {
href: e.href,
title: e.title,
"aria-label": e.getAttribute("aria-label"),
};
return (
u(
["icon", "color-scheme", "text", "size", "show-count"],
function (t) {
var r = "data-" + t;
o[r] = e.getAttribute(r);
},
),
null == o["data-text"] &&
(o["data-text"] = e.textContent || e.innerText),
o
);
})(o)),
s)
) {
var a = h("span");
E(a.attachShadow({ mode: "closed" }), o, function () {
r(a);
});
} else {
var n = h("iframe", {
src: "javascript:0",
title: o.title || void 0,
allowtransparency: !0,
scrolling: "no",
frameBorder: 0,
});
G(n, [0, 0]), (n.style.border = "none");
var c = function () {
var a,
l = n.contentWindow;
try {
a = l.document.body;
} catch (o) {
return void e.body.appendChild(n.parentNode.removeChild(n));
}
w(n, "load", c),
E.call(l, a, o, function (e) {
var a = (function (e) {
var o = e.offsetWidth,
r = e.offsetHeight;
if (e.getBoundingClientRect) {
var a = e.getBoundingClientRect();
(o = t.max(o, _(a.width))), (r = t.max(r, _(a.height)));
}
return [o, r];
})(e);
n.parentNode.removeChild(n),
k(n, "load", function () {
G(n, a);
}),
(n.src =
i +
"#" +
(n.name = (function (e, o, t, r) {
null == o && (o = "&"),
null == t && (t = "="),
null == r && (r = window.encodeURIComponent);
var a = [];
for (var n in e) {
var i = e[n];
null != i && a.push(r(n) + t + r(i));
}
return a.join(o);
})(o))),
r(n);
});
};
m(n, "load", c), e.body.appendChild(n);
}
};
o.protocol + "//" + o.host + o.pathname === i
? E(e.body, v(window.name || o.hash.replace(/^#/, "")), function () {})
: (function (o) {
if (
"complete" === e.readyState ||
("loading" !== e.readyState && !e.documentElement.doScroll)
)
setTimeout(o);
else if (e.addEventListener) {
var t = g(o);
k(e, "DOMContentLoaded", t), k(window, "load", t);
} else x(e, /m/, o);
})(function () {
var o,
t = e.querySelectorAll
? e.querySelectorAll("a." + n)
: ((o = []),
u(e.getElementsByTagName("a"), function (e) {
-1 !==
(" " + e.className + " ")
.replace(/[ \t\n\f\r]+/g, " ")
.indexOf(" " + n + " ") && o.push(e);
}),
o);
u(t, function (e) {
T(e, function (o) {
e.parentNode.replaceChild(o, e);
});
});
});
})();

View file

@ -0,0 +1,29 @@
/**
* Config
* -------------------------------------------------------------------------------------
* ! IMPORTANT: Make sure you clear the browser local storage In order to see the config changes in the template.
* ! To clear local storage: (https://www.leadshook.com/help/how-to-clear-local-storage-in-google-chrome-browser/).
*/
"use strict";
// JS global variables
window.config = {
colors: {
primary: "#0b5577",
secondary: "#2eac68",
success: "#71dd37",
info: "#03c3ec",
warning: "#ffab00",
danger: "#ff3e1d",
dark: "#233446",
black: "#22303e",
white: "#fff",
cardColor: "#fff",
bodyBg: "#f5f5f9",
bodyColor: "#646E78",
headingColor: "#384551",
textMuted: "#a7acb2",
borderColor: "#e4e6e8",
},
};

View file

@ -0,0 +1,848 @@
/**
* Dashboard Analytics
*/
"use strict";
(function () {
let cardColor, headingColor, legendColor, labelColor, shadeColor, borderColor;
cardColor = config.colors.cardColor;
headingColor = config.colors.headingColor;
legendColor = config.colors.bodyColor;
labelColor = config.colors.textMuted;
borderColor = config.colors.borderColor;
// Order Area Chart
// --------------------------------------------------------------------
const orderAreaChartEl = document.querySelector("#orderChart"),
orderAreaChartConfig = {
chart: {
height: 80,
type: "area",
toolbar: {
show: false,
},
sparkline: {
enabled: true,
},
},
markers: {
size: 6,
colors: "transparent",
strokeColors: "transparent",
strokeWidth: 4,
discrete: [
{
fillColor: cardColor,
seriesIndex: 0,
dataPointIndex: 6,
strokeColor: config.colors.success,
strokeWidth: 2,
size: 6,
radius: 8,
},
],
hover: {
size: 7,
},
},
grid: {
show: false,
padding: {
right: 8,
},
},
colors: [config.colors.success],
fill: {
type: "gradient",
gradient: {
shade: shadeColor,
shadeIntensity: 0.8,
opacityFrom: 0.8,
opacityTo: 0.25,
stops: [0, 85, 100],
},
},
dataLabels: {
enabled: false,
},
stroke: {
width: 2,
curve: "smooth",
},
series: [
{
data: [180, 175, 275, 140, 205, 190, 295],
},
],
xaxis: {
show: false,
lines: {
show: false,
},
labels: {
show: false,
},
stroke: {
width: 0,
},
axisBorder: {
show: false,
},
},
yaxis: {
stroke: {
width: 0,
},
show: false,
},
};
if (typeof orderAreaChartEl !== undefined && orderAreaChartEl !== null) {
const orderAreaChart = new ApexCharts(
orderAreaChartEl,
orderAreaChartConfig,
);
orderAreaChart.render();
}
// Total Revenue Report Chart - Bar Chart
// --------------------------------------------------------------------
const totalRevenueChartEl = document.querySelector("#totalRevenueChart"),
totalRevenueChartOptions = {
series: [
{
name: new Date().getFullYear() - 1,
data: [18, 7, 15, 29, 18, 12, 9],
},
{
name: new Date().getFullYear() - 2,
data: [-13, -18, -9, -14, -5, -17, -15],
},
],
chart: {
height: 317,
stacked: true,
type: "bar",
toolbar: { show: false },
},
plotOptions: {
bar: {
horizontal: false,
columnWidth: "30%",
borderRadius: 8,
startingShape: "rounded",
endingShape: "rounded",
},
},
colors: [config.colors.primary, config.colors.info],
dataLabels: {
enabled: false,
},
stroke: {
curve: "smooth",
width: 6,
lineCap: "round",
colors: [cardColor],
},
legend: {
show: true,
horizontalAlign: "left",
position: "top",
markers: {
height: 8,
width: 8,
radius: 12,
offsetX: -5,
},
fontSize: "13px",
fontFamily: "Public Sans",
fontWeight: 400,
labels: {
colors: legendColor,
useSeriesColors: false,
},
itemMargin: {
horizontal: 10,
},
},
grid: {
strokeDashArray: 7,
borderColor: borderColor,
padding: {
top: 0,
bottom: -8,
left: 20,
right: 20,
},
},
fill: {
opacity: [1, 1],
},
xaxis: {
categories: ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul"],
labels: {
style: {
fontSize: "13px",
fontFamily: "Public Sans",
colors: labelColor,
},
},
axisTicks: {
show: false,
},
axisBorder: {
show: false,
},
},
yaxis: {
labels: {
style: {
fontSize: "13px",
fontFamily: "Public Sans",
colors: labelColor,
},
},
},
responsive: [
{
breakpoint: 1700,
options: {
plotOptions: {
bar: {
borderRadius: 10,
columnWidth: "35%",
},
},
},
},
{
breakpoint: 1440,
options: {
plotOptions: {
bar: {
borderRadius: 12,
columnWidth: "43%",
},
},
},
},
{
breakpoint: 1300,
options: {
plotOptions: {
bar: {
borderRadius: 11,
columnWidth: "45%",
},
},
},
},
{
breakpoint: 1200,
options: {
plotOptions: {
bar: {
borderRadius: 11,
columnWidth: "37%",
},
},
},
},
{
breakpoint: 1040,
options: {
plotOptions: {
bar: {
borderRadius: 12,
columnWidth: "45%",
},
},
},
},
{
breakpoint: 991,
options: {
plotOptions: {
bar: {
borderRadius: 12,
columnWidth: "33%",
},
},
},
},
{
breakpoint: 768,
options: {
plotOptions: {
bar: {
borderRadius: 11,
columnWidth: "28%",
},
},
},
},
{
breakpoint: 640,
options: {
plotOptions: {
bar: {
borderRadius: 11,
columnWidth: "30%",
},
},
},
},
{
breakpoint: 576,
options: {
plotOptions: {
bar: {
borderRadius: 10,
columnWidth: "38%",
},
},
},
},
{
breakpoint: 440,
options: {
plotOptions: {
bar: {
borderRadius: 10,
columnWidth: "50%",
},
},
},
},
{
breakpoint: 380,
options: {
plotOptions: {
bar: {
borderRadius: 9,
columnWidth: "60%",
},
},
},
},
],
states: {
hover: {
filter: {
type: "none",
},
},
active: {
filter: {
type: "none",
},
},
},
};
if (
typeof totalRevenueChartEl !== undefined &&
totalRevenueChartEl !== null
) {
const totalRevenueChart = new ApexCharts(
totalRevenueChartEl,
totalRevenueChartOptions,
);
totalRevenueChart.render();
}
// Growth Chart - Radial Bar Chart
// --------------------------------------------------------------------
const growthChartEl = document.querySelector("#growthChart"),
growthChartOptions = {
series: [78],
labels: ["Growth"],
chart: {
height: 240,
type: "radialBar",
},
plotOptions: {
radialBar: {
size: 150,
offsetY: 10,
startAngle: -150,
endAngle: 150,
hollow: {
size: "55%",
},
track: {
background: cardColor,
strokeWidth: "100%",
},
dataLabels: {
name: {
offsetY: 15,
color: legendColor,
fontSize: "15px",
fontWeight: "500",
fontFamily: "Public Sans",
},
value: {
offsetY: -25,
color: headingColor,
fontSize: "22px",
fontWeight: "500",
fontFamily: "Public Sans",
},
},
},
},
colors: [config.colors.primary],
fill: {
type: "gradient",
gradient: {
shade: "dark",
shadeIntensity: 0.5,
gradientToColors: [config.colors.primary],
inverseColors: true,
opacityFrom: 1,
opacityTo: 0.6,
stops: [30, 70, 100],
},
},
stroke: {
dashArray: 5,
},
grid: {
padding: {
top: -35,
bottom: -10,
},
},
states: {
hover: {
filter: {
type: "none",
},
},
active: {
filter: {
type: "none",
},
},
},
};
if (typeof growthChartEl !== undefined && growthChartEl !== null) {
const growthChart = new ApexCharts(growthChartEl, growthChartOptions);
growthChart.render();
}
// Revenue Bar Chart
// --------------------------------------------------------------------
const revenueBarChartEl = document.querySelector("#revenueChart"),
revenueBarChartConfig = {
chart: {
height: 95,
type: "bar",
toolbar: {
show: false,
},
},
plotOptions: {
bar: {
barHeight: "80%",
columnWidth: "75%",
startingShape: "rounded",
endingShape: "rounded",
borderRadius: 4,
distributed: true,
},
},
grid: {
show: false,
padding: {
top: -20,
bottom: -12,
left: -10,
right: 0,
},
},
colors: [
config.colors.primary,
config.colors.primary,
config.colors.primary,
config.colors.primary,
config.colors.primary,
config.colors.primary,
config.colors.primary,
],
dataLabels: {
enabled: false,
},
series: [
{
data: [40, 95, 60, 45, 90, 50, 75],
},
],
legend: {
show: false,
},
xaxis: {
categories: ["M", "T", "W", "T", "F", "S", "S"],
axisBorder: {
show: false,
},
axisTicks: {
show: false,
},
labels: {
style: {
colors: labelColor,
fontSize: "13px",
},
},
},
yaxis: {
labels: {
show: false,
},
},
};
if (typeof revenueBarChartEl !== undefined && revenueBarChartEl !== null) {
const revenueBarChart = new ApexCharts(
revenueBarChartEl,
revenueBarChartConfig,
);
revenueBarChart.render();
}
// Profit Report Line Chart
// --------------------------------------------------------------------
const profileReportChartEl = document.querySelector("#profileReportChart"),
profileReportChartConfig = {
chart: {
height: 75,
// width: 175,
type: "line",
toolbar: {
show: false,
},
dropShadow: {
enabled: true,
top: 10,
left: 5,
blur: 3,
color: config.colors.warning,
opacity: 0.15,
},
sparkline: {
enabled: true,
},
},
grid: {
show: false,
padding: {
right: 8,
},
},
colors: [config.colors.warning],
dataLabels: {
enabled: false,
},
stroke: {
width: 5,
curve: "smooth",
},
series: [
{
data: [110, 270, 145, 245, 205, 285],
},
],
xaxis: {
show: false,
lines: {
show: false,
},
labels: {
show: false,
},
axisBorder: {
show: false,
},
},
yaxis: {
show: false,
},
};
if (
typeof profileReportChartEl !== undefined &&
profileReportChartEl !== null
) {
const profileReportChart = new ApexCharts(
profileReportChartEl,
profileReportChartConfig,
);
profileReportChart.render();
}
// Order Statistics Chart
// --------------------------------------------------------------------
const chartOrderStatistics = document.querySelector("#orderStatisticsChart"),
orderChartConfig = {
chart: {
height: 145,
width: 110,
type: "donut",
},
labels: ["Electronic", "Sports", "Decor", "Fashion"],
series: [50, 85, 25, 40],
colors: [
config.colors.success,
config.colors.primary,
config.colors.secondary,
config.colors.info,
],
stroke: {
width: 5,
colors: [cardColor],
},
dataLabels: {
enabled: false,
formatter: function (val, opt) {
return parseInt(val) + "%";
},
},
legend: {
show: false,
},
grid: {
padding: {
top: 0,
bottom: 0,
right: 15,
},
},
states: {
hover: {
filter: { type: "none" },
},
active: {
filter: { type: "none" },
},
},
plotOptions: {
pie: {
donut: {
size: "75%",
labels: {
show: true,
value: {
fontSize: "18px",
fontFamily: "Public Sans",
fontWeight: 500,
color: headingColor,
offsetY: -17,
formatter: function (val) {
return parseInt(val) + "%";
},
},
name: {
offsetY: 17,
fontFamily: "Public Sans",
},
total: {
show: true,
fontSize: "13px",
color: legendColor,
label: "Weekly",
formatter: function (w) {
return "38%";
},
},
},
},
},
},
};
if (
typeof chartOrderStatistics !== undefined &&
chartOrderStatistics !== null
) {
const statisticsChart = new ApexCharts(
chartOrderStatistics,
orderChartConfig,
);
statisticsChart.render();
}
// Income Chart - Area chart
// --------------------------------------------------------------------
const incomeChartEl = document.querySelector("#incomeChart"),
incomeChartConfig = {
series: [
{
data: [21, 30, 22, 42, 26, 35, 29],
},
],
chart: {
height: 232,
parentHeightOffset: 0,
parentWidthOffset: 0,
toolbar: {
show: false,
},
type: "area",
},
dataLabels: {
enabled: false,
},
stroke: {
width: 3,
curve: "smooth",
},
legend: {
show: false,
},
markers: {
size: 6,
colors: "transparent",
strokeColors: "transparent",
strokeWidth: 4,
discrete: [
{
fillColor: config.colors.white,
seriesIndex: 0,
dataPointIndex: 6,
strokeColor: config.colors.primary,
strokeWidth: 2,
size: 6,
radius: 8,
},
],
hover: {
size: 7,
},
},
colors: [config.colors.primary],
fill: {
type: "gradient",
gradient: {
shade: shadeColor,
shadeIntensity: 0.6,
opacityFrom: 0.5,
opacityTo: 0.25,
stops: [0, 95, 100],
},
},
grid: {
borderColor: borderColor,
strokeDashArray: 8,
padding: {
top: -20,
bottom: -8,
left: 0,
right: 8,
},
},
xaxis: {
categories: ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul"],
axisBorder: {
show: false,
},
axisTicks: {
show: false,
},
labels: {
show: true,
style: {
fontSize: "13px",
colors: labelColor,
},
},
},
yaxis: {
labels: {
show: false,
},
min: 10,
max: 50,
tickAmount: 4,
},
};
if (typeof incomeChartEl !== undefined && incomeChartEl !== null) {
const incomeChart = new ApexCharts(incomeChartEl, incomeChartConfig);
incomeChart.render();
}
// Expenses Mini Chart - Radial Chart
// --------------------------------------------------------------------
const weeklyExpensesEl = document.querySelector("#expensesOfWeek"),
weeklyExpensesConfig = {
series: [65],
chart: {
width: 60,
height: 60,
type: "radialBar",
},
plotOptions: {
radialBar: {
startAngle: 0,
endAngle: 360,
strokeWidth: "8",
hollow: {
margin: 2,
size: "40%",
},
track: {
background: borderColor,
},
dataLabels: {
show: true,
name: {
show: false,
},
value: {
formatter: function (val) {
return "$" + parseInt(val);
},
offsetY: 5,
color: legendColor,
fontSize: "12px",
fontFamily: "Public Sans",
show: true,
},
},
},
},
fill: {
type: "solid",
colors: config.colors.primary,
},
stroke: {
lineCap: "round",
},
grid: {
padding: {
top: -10,
bottom: -15,
left: -10,
right: -10,
},
},
states: {
hover: {
filter: {
type: "none",
},
},
active: {
filter: {
type: "none",
},
},
},
};
if (typeof weeklyExpensesEl !== undefined && weeklyExpensesEl !== null) {
const weeklyExpenses = new ApexCharts(
weeklyExpensesEl,
weeklyExpensesConfig,
);
weeklyExpenses.render();
}
})();

View file

@ -0,0 +1,37 @@
/**
* Perfect Scrollbar
*/
"use strict";
document.addEventListener("DOMContentLoaded", function () {
(function () {
const verticalExample = document.getElementById("vertical-example"),
horizontalExample = document.getElementById("horizontal-example"),
horizVertExample = document.getElementById("both-scrollbars-example");
// Vertical Example
// --------------------------------------------------------------------
if (verticalExample) {
new PerfectScrollbar(verticalExample, {
wheelPropagation: false,
});
}
// Horizontal Example
// --------------------------------------------------------------------
if (horizontalExample) {
new PerfectScrollbar(horizontalExample, {
wheelPropagation: false,
suppressScrollY: true,
});
}
// Both vertical and Horizontal Example
// --------------------------------------------------------------------
if (horizVertExample) {
new PerfectScrollbar(horizVertExample, {
wheelPropagation: false,
});
}
})();
});

View file

@ -0,0 +1,11 @@
/**
* Form Basic Inputs
*/
"use strict";
(function () {
// Indeterminate checkbox
const checkbox = document.getElementById("defaultCheck2");
checkbox.indeterminate = true;
})();

View file

@ -0,0 +1,932 @@
// Constants
const TRANS_EVENTS = ["transitionend", "webkitTransitionEnd", "oTransitionEnd"];
const TRANS_PROPERTIES = [
"transition",
"MozTransition",
"webkitTransition",
"WebkitTransition",
"OTransition",
];
const INLINE_STYLES = `
.layout-menu-fixed .layout-navbar-full .layout-menu,
.layout-page {
padding-top: {navbarHeight}px !important;
}
.content-wrapper {
padding-bottom: {footerHeight}px !important;
}`;
// Guard
function requiredParam(name) {
throw new Error(`Parameter required${name ? `: \`${name}\`` : ""}`);
}
const Helpers = {
// Root Element
ROOT_EL: typeof window !== "undefined" ? document.documentElement : null,
// Large screens breakpoint
LAYOUT_BREAKPOINT: 1200,
// Resize delay in milliseconds
RESIZE_DELAY: 200,
menuPsScroll: null,
mainMenu: null,
// Internal variables
_curStyle: null,
_styleEl: null,
_resizeTimeout: null,
_resizeCallback: null,
_transitionCallback: null,
_transitionCallbackTimeout: null,
_listeners: [],
_initialized: false,
_autoUpdate: false,
_lastWindowHeight: 0,
// *******************************************************************************
// * Utilities
// ---
// Scroll To Active Menu Item
_scrollToActive(animate = false, duration = 500) {
const layoutMenu = this.getLayoutMenu();
if (!layoutMenu) return;
let activeEl = layoutMenu.querySelector("li.menu-item.active:not(.open)");
if (activeEl) {
// t = current time
// b = start value
// c = change in value
// d = duration
const easeInOutQuad = (t, b, c, d) => {
t /= d / 2;
if (t < 1) return (c / 2) * t * t + b;
t -= 1;
return (-c / 2) * (t * (t - 2) - 1) + b;
};
const element = this.getLayoutMenu().querySelector(".menu-inner");
if (typeof activeEl === "string") {
activeEl = document.querySelector(activeEl);
}
if (typeof activeEl !== "number") {
activeEl = activeEl.getBoundingClientRect().top + element.scrollTop;
}
// If active element's top position is less than 2/3 (66%) of menu height than do not scroll
if (activeEl < parseInt((element.clientHeight * 2) / 3, 10)) return;
const start = element.scrollTop;
const change = activeEl - start - parseInt(element.clientHeight / 2, 10);
const startDate = +new Date();
if (animate === true) {
const animateScroll = () => {
const currentDate = +new Date();
const currentTime = currentDate - startDate;
const val = easeInOutQuad(currentTime, start, change, duration);
element.scrollTop = val;
if (currentTime < duration) {
requestAnimationFrame(animateScroll);
} else {
element.scrollTop = change;
}
};
animateScroll();
} else {
element.scrollTop = change;
}
}
},
// ---
// Add classes
_addClass(cls, el = this.ROOT_EL) {
if (el && el.length !== undefined) {
// Add classes to multiple elements
el.forEach((e) => {
if (e) {
cls.split(" ").forEach((c) => e.classList.add(c));
}
});
} else if (el) {
// Add classes to single element
cls.split(" ").forEach((c) => el.classList.add(c));
}
},
// ---
// Remove classes
_removeClass(cls, el = this.ROOT_EL) {
if (el && el.length !== undefined) {
// Remove classes to multiple elements
el.forEach((e) => {
if (e) {
cls.split(" ").forEach((c) => e.classList.remove(c));
}
});
} else if (el) {
// Remove classes to single element
cls.split(" ").forEach((c) => el.classList.remove(c));
}
},
// Toggle classes
_toggleClass(el = this.ROOT_EL, cls1, cls2) {
if (el.classList.contains(cls1)) {
el.classList.replace(cls1, cls2);
} else {
el.classList.replace(cls2, cls1);
}
},
// ---
// Has class
_hasClass(cls, el = this.ROOT_EL) {
let result = false;
cls.split(" ").forEach((c) => {
if (el.classList.contains(c)) result = true;
});
return result;
},
_findParent(el, cls) {
if (
(el && el.tagName.toUpperCase() === "BODY") ||
el.tagName.toUpperCase() === "HTML"
)
return null;
el = el.parentNode;
while (
el &&
el.tagName.toUpperCase() !== "BODY" &&
!el.classList.contains(cls)
) {
el = el.parentNode;
}
el = el && el.tagName.toUpperCase() !== "BODY" ? el : null;
return el;
},
// ---
// Trigger window event
_triggerWindowEvent(name) {
if (typeof window === "undefined") return;
if (document.createEvent) {
let event;
if (typeof Event === "function") {
event = new Event(name);
} else {
event = document.createEvent("Event");
event.initEvent(name, false, true);
}
window.dispatchEvent(event);
} else {
window.fireEvent(`on${name}`, document.createEventObject());
}
},
// ---
// Trigger event
_triggerEvent(name) {
this._triggerWindowEvent(`layout${name}`);
this._listeners
.filter((listener) => listener.event === name)
.forEach((listener) => listener.callback.call(null));
},
// ---
// Update style
_updateInlineStyle(navbarHeight = 0, footerHeight = 0) {
if (!this._styleEl) {
this._styleEl = document.createElement("style");
this._styleEl.type = "text/css";
document.head.appendChild(this._styleEl);
}
const newStyle = INLINE_STYLES.replace(
/\{navbarHeight\}/gi,
navbarHeight,
).replace(/\{footerHeight\}/gi, footerHeight);
if (this._curStyle !== newStyle) {
this._curStyle = newStyle;
this._styleEl.textContent = newStyle;
}
},
// ---
// Remove style
_removeInlineStyle() {
if (this._styleEl) document.head.removeChild(this._styleEl);
this._styleEl = null;
this._curStyle = null;
},
// ---
// Redraw layout menu (Safari bugfix)
_redrawLayoutMenu() {
const layoutMenu = this.getLayoutMenu();
if (layoutMenu && layoutMenu.querySelector(".menu")) {
const inner = layoutMenu.querySelector(".menu-inner");
const { scrollTop } = inner;
const pageScrollTop = document.documentElement.scrollTop;
layoutMenu.style.display = "none";
// layoutMenu.offsetHeight
layoutMenu.style.display = "";
inner.scrollTop = scrollTop;
document.documentElement.scrollTop = pageScrollTop;
return true;
}
return false;
},
// ---
// Check for transition support
_supportsTransitionEnd() {
if (window.QUnit) return false;
const el = document.body || document.documentElement;
if (!el) return false;
let result = false;
TRANS_PROPERTIES.forEach((evnt) => {
if (typeof el.style[evnt] !== "undefined") result = true;
});
return result;
},
// ---
// Calculate current navbar height
_getNavbarHeight() {
const layoutNavbar = this.getLayoutNavbar();
if (!layoutNavbar) return 0;
if (!this.isSmallScreen())
return layoutNavbar.getBoundingClientRect().height;
// Needs some logic to get navbar height on small screens
const clonedEl = layoutNavbar.cloneNode(true);
clonedEl.id = null;
clonedEl.style.visibility = "hidden";
clonedEl.style.position = "absolute";
Array.prototype.slice
.call(clonedEl.querySelectorAll(".collapse.show"))
.forEach((el) => this._removeClass("show", el));
layoutNavbar.parentNode.insertBefore(clonedEl, layoutNavbar);
const navbarHeight = clonedEl.getBoundingClientRect().height;
clonedEl.parentNode.removeChild(clonedEl);
return navbarHeight;
},
// ---
// Get current footer height
_getFooterHeight() {
const layoutFooter = this.getLayoutFooter();
if (!layoutFooter) return 0;
return layoutFooter.getBoundingClientRect().height;
},
// ---
// Get animation duration of element
_getAnimationDuration(el) {
const duration = window.getComputedStyle(el).transitionDuration;
return parseFloat(duration) * (duration.indexOf("ms") !== -1 ? 1 : 1000);
},
// ---
// Set menu hover state
_setMenuHoverState(hovered) {
this[hovered ? "_addClass" : "_removeClass"]("layout-menu-hover");
},
// ---
// Toggle collapsed
_setCollapsed(collapsed) {
if (this.isSmallScreen()) {
if (collapsed) {
this._removeClass("layout-menu-expanded");
} else {
setTimeout(
() => {
this._addClass("layout-menu-expanded");
},
this._redrawLayoutMenu() ? 5 : 0,
);
}
}
},
// ---
// Add layout sivenav toggle animationEnd event
_bindLayoutAnimationEndEvent(modifier, cb) {
const menu = this.getMenu();
const duration = menu ? this._getAnimationDuration(menu) + 50 : 0;
if (!duration) {
modifier.call(this);
cb.call(this);
return;
}
this._transitionCallback = (e) => {
if (e.target !== menu) return;
this._unbindLayoutAnimationEndEvent();
cb.call(this);
};
TRANS_EVENTS.forEach((e) => {
menu.addEventListener(e, this._transitionCallback, false);
});
modifier.call(this);
this._transitionCallbackTimeout = setTimeout(() => {
this._transitionCallback.call(this, { target: menu });
}, duration);
},
// ---
// Remove layout sivenav toggle animationEnd event
_unbindLayoutAnimationEndEvent() {
const menu = this.getMenu();
if (this._transitionCallbackTimeout) {
clearTimeout(this._transitionCallbackTimeout);
this._transitionCallbackTimeout = null;
}
if (menu && this._transitionCallback) {
TRANS_EVENTS.forEach((e) => {
menu.removeEventListener(e, this._transitionCallback, false);
});
}
if (this._transitionCallback) {
this._transitionCallback = null;
}
},
// ---
// Bind delayed window resize event
_bindWindowResizeEvent() {
this._unbindWindowResizeEvent();
const cb = () => {
if (this._resizeTimeout) {
clearTimeout(this._resizeTimeout);
this._resizeTimeout = null;
}
this._triggerEvent("resize");
};
this._resizeCallback = () => {
if (this._resizeTimeout) clearTimeout(this._resizeTimeout);
this._resizeTimeout = setTimeout(cb, this.RESIZE_DELAY);
};
window.addEventListener("resize", this._resizeCallback, false);
},
// ---
// Unbind delayed window resize event
_unbindWindowResizeEvent() {
if (this._resizeTimeout) {
clearTimeout(this._resizeTimeout);
this._resizeTimeout = null;
}
if (this._resizeCallback) {
window.removeEventListener("resize", this._resizeCallback, false);
this._resizeCallback = null;
}
},
_bindMenuMouseEvents() {
if (this._menuMouseEnter && this._menuMouseLeave && this._windowTouchStart)
return;
const layoutMenu = this.getLayoutMenu();
if (!layoutMenu) return this._unbindMenuMouseEvents();
if (!this._menuMouseEnter) {
this._menuMouseEnter = () => {
if (this.isSmallScreen() || this._hasClass("layout-transitioning")) {
return this._setMenuHoverState(false);
}
return this._setMenuHoverState(false);
};
layoutMenu.addEventListener("mouseenter", this._menuMouseEnter, false);
layoutMenu.addEventListener("touchstart", this._menuMouseEnter, false);
}
if (!this._menuMouseLeave) {
this._menuMouseLeave = () => {
this._setMenuHoverState(false);
};
layoutMenu.addEventListener("mouseleave", this._menuMouseLeave, false);
}
if (!this._windowTouchStart) {
this._windowTouchStart = (e) => {
if (!e || !e.target || !this._findParent(e.target, ".layout-menu")) {
this._setMenuHoverState(false);
}
};
window.addEventListener("touchstart", this._windowTouchStart, true);
}
},
_unbindMenuMouseEvents() {
if (
!this._menuMouseEnter &&
!this._menuMouseLeave &&
!this._windowTouchStart
)
return;
const layoutMenu = this.getLayoutMenu();
if (this._menuMouseEnter) {
if (layoutMenu) {
layoutMenu.removeEventListener(
"mouseenter",
this._menuMouseEnter,
false,
);
layoutMenu.removeEventListener(
"touchstart",
this._menuMouseEnter,
false,
);
}
this._menuMouseEnter = null;
}
if (this._menuMouseLeave) {
if (layoutMenu) {
layoutMenu.removeEventListener(
"mouseleave",
this._menuMouseLeave,
false,
);
}
this._menuMouseLeave = null;
}
if (this._windowTouchStart) {
if (layoutMenu) {
window.addEventListener("touchstart", this._windowTouchStart, true);
}
this._windowTouchStart = null;
}
this._setMenuHoverState(false);
},
// *******************************************************************************
// * Methods
scrollToActive(animate = false) {
this._scrollToActive(animate);
},
// ---
// Collapse / expand layout
setCollapsed(collapsed = requiredParam("collapsed"), animate = true) {
const layoutMenu = this.getLayoutMenu();
if (!layoutMenu) return;
this._unbindLayoutAnimationEndEvent();
if (animate && this._supportsTransitionEnd()) {
this._addClass("layout-transitioning");
if (collapsed) this._setMenuHoverState(false);
this._bindLayoutAnimationEndEvent(
() => {
// Collapse / Expand
if (this.isSmallScreen) this._setCollapsed(collapsed);
},
() => {
this._removeClass("layout-transitioning");
this._triggerWindowEvent("resize");
this._triggerEvent("toggle");
this._setMenuHoverState(false);
},
);
} else {
this._addClass("layout-no-transition");
if (collapsed) this._setMenuHoverState(false);
// Collapse / Expand
this._setCollapsed(collapsed);
setTimeout(() => {
this._removeClass("layout-no-transition");
this._triggerWindowEvent("resize");
this._triggerEvent("toggle");
this._setMenuHoverState(false);
}, 1);
}
},
// ---
// Toggle layout
toggleCollapsed(animate = true) {
this.setCollapsed(!this.isCollapsed(), animate);
},
// ---
// Set layout positioning
setPosition(
fixed = requiredParam("fixed"),
offcanvas = requiredParam("offcanvas"),
) {
this._removeClass(
"layout-menu-offcanvas layout-menu-fixed layout-menu-fixed-offcanvas",
);
if (!fixed && offcanvas) {
this._addClass("layout-menu-offcanvas");
} else if (fixed && !offcanvas) {
this._addClass("layout-menu-fixed");
this._redrawLayoutMenu();
} else if (fixed && offcanvas) {
this._addClass("layout-menu-fixed-offcanvas");
this._redrawLayoutMenu();
}
this.update();
},
// *******************************************************************************
// * Getters
getLayoutMenu() {
return document.querySelector(".layout-menu");
},
getMenu() {
const layoutMenu = this.getLayoutMenu();
if (!layoutMenu) return null;
return !this._hasClass("menu", layoutMenu)
? layoutMenu.querySelector(".menu")
: layoutMenu;
},
getLayoutNavbar() {
return document.querySelector(".layout-navbar");
},
getLayoutFooter() {
return document.querySelector(".content-footer");
},
// *******************************************************************************
// * Update
update() {
if (
(this.getLayoutNavbar() &&
((!this.isSmallScreen() &&
this.isLayoutNavbarFull() &&
this.isFixed()) ||
this.isNavbarFixed())) ||
(this.getLayoutFooter() && this.isFooterFixed())
) {
this._updateInlineStyle(this._getNavbarHeight(), this._getFooterHeight());
}
this._bindMenuMouseEvents();
},
setAutoUpdate(enable = requiredParam("enable")) {
if (enable && !this._autoUpdate) {
this.on("resize.Helpers:autoUpdate", () => this.update());
this._autoUpdate = true;
} else if (!enable && this._autoUpdate) {
this.off("resize.Helpers:autoUpdate");
this._autoUpdate = false;
}
},
// *******************************************************************************
// * Tests
isRtl() {
return (
document.querySelector("body").getAttribute("dir") === "rtl" ||
document.querySelector("html").getAttribute("dir") === "rtl"
);
},
isMobileDevice() {
return (
typeof window.orientation !== "undefined" ||
navigator.userAgent.indexOf("IEMobile") !== -1
);
},
isSmallScreen() {
return (
(window.innerWidth ||
document.documentElement.clientWidth ||
document.body.clientWidth) < this.LAYOUT_BREAKPOINT
);
},
isLayoutNavbarFull() {
return !!document.querySelector(".layout-wrapper.layout-navbar-full");
},
isCollapsed() {
if (this.isSmallScreen()) {
return !this._hasClass("layout-menu-expanded");
}
return this._hasClass("layout-menu-collapsed");
},
isFixed() {
return this._hasClass("layout-menu-fixed layout-menu-fixed-offcanvas");
},
isNavbarFixed() {
return (
this._hasClass("layout-navbar-fixed") ||
(!this.isSmallScreen() && this.isFixed() && this.isLayoutNavbarFull())
);
},
isFooterFixed() {
return this._hasClass("layout-footer-fixed");
},
isLightStyle() {
return document.documentElement.classList.contains("light-style");
},
// *******************************************************************************
// * Events
on(event = requiredParam("event"), callback = requiredParam("callback")) {
const [_event] = event.split(".");
let [, ...namespace] = event.split(".");
// let [_event, ...namespace] = event.split('.')
namespace = namespace.join(".") || null;
this._listeners.push({ event: _event, namespace, callback });
},
off(event = requiredParam("event")) {
const [_event] = event.split(".");
let [, ...namespace] = event.split(".");
namespace = namespace.join(".") || null;
this._listeners
.filter(
(listener) =>
listener.event === _event && listener.namespace === namespace,
)
.forEach((listener) =>
this._listeners.splice(this._listeners.indexOf(listener), 1),
);
},
// *******************************************************************************
// * Life cycle
init() {
if (this._initialized) return;
this._initialized = true;
// Initialize `style` element
this._updateInlineStyle(0);
// Bind window resize event
this._bindWindowResizeEvent();
// Bind init event
this.off("init._Helpers");
this.on("init._Helpers", () => {
this.off("resize._Helpers:redrawMenu");
this.on("resize._Helpers:redrawMenu", () => {
// eslint-disable-next-line no-unused-expressions
this.isSmallScreen() && !this.isCollapsed() && this._redrawLayoutMenu();
});
// Force repaint in IE 10
if (
typeof document.documentMode === "number" &&
document.documentMode < 11
) {
this.off("resize._Helpers:ie10RepaintBody");
this.on("resize._Helpers:ie10RepaintBody", () => {
if (this.isFixed()) return;
const { scrollTop } = document.documentElement;
document.body.style.display = "none";
// document.body.offsetHeight
document.body.style.display = "block";
document.documentElement.scrollTop = scrollTop;
});
}
});
this._triggerEvent("init");
},
destroy() {
if (!this._initialized) return;
this._initialized = false;
this._removeClass("layout-transitioning");
this._removeInlineStyle();
this._unbindLayoutAnimationEndEvent();
this._unbindWindowResizeEvent();
this._unbindMenuMouseEvents();
this.setAutoUpdate(false);
this.off("init._Helpers");
// Remove all listeners except `init`
this._listeners
.filter((listener) => listener.event !== "init")
.forEach((listener) =>
this._listeners.splice(this._listeners.indexOf(listener), 1),
);
},
// ---
// Init Password Toggle
initPasswordToggle() {
const toggler = document.querySelectorAll(
".form-password-toggle i:not(.copy-to-clipboard)",
);
if (typeof toggler !== "undefined" && toggler !== null) {
toggler.forEach((el) => {
el.addEventListener("click", (e) => {
e.preventDefault();
const formPasswordToggle = el.closest(".form-password-toggle");
const formPasswordToggleInput =
formPasswordToggle.querySelector("input");
if (formPasswordToggleInput.getAttribute("type") === "text") {
formPasswordToggleInput.setAttribute("type", "password");
formPasswordToggle
.querySelector("i.bx-show")
.classList.replace("bx-show", "bx-hide");
} else if (
formPasswordToggleInput.getAttribute("type") === "password"
) {
formPasswordToggleInput.setAttribute("type", "text");
formPasswordToggle
.querySelector("i.bx-hide")
.classList.replace("bx-hide", "bx-show");
}
});
});
}
},
// ---
// Init Speech To Text
initSpeechToText() {
const SpeechRecognition =
window.SpeechRecognition || window.webkitSpeechRecognition;
const speechToText = document.querySelectorAll(".speech-to-text");
if (SpeechRecognition !== undefined && SpeechRecognition !== null) {
if (typeof speechToText !== "undefined" && speechToText !== null) {
const recognition = new SpeechRecognition();
const toggler = document.querySelectorAll(".speech-to-text i");
toggler.forEach((el) => {
let listening = false;
el.addEventListener("click", () => {
el.closest(".input-group").querySelector(".form-control").focus();
recognition.onspeechstart = () => {
listening = true;
};
if (listening === false) {
recognition.start();
}
recognition.onerror = () => {
listening = false;
};
recognition.onresult = (event) => {
el.closest(".input-group").querySelector(".form-control").value =
event.results[0][0].transcript;
};
recognition.onspeechend = () => {
listening = false;
recognition.stop();
};
});
});
}
}
},
// Ajax Call Promise
ajaxCall(url) {
return new Promise((resolve, reject) => {
const req = new XMLHttpRequest();
req.open("GET", url);
req.onload = () =>
req.status === 200
? resolve(req.response)
: reject(Error(req.statusText));
req.onerror = (e) => reject(Error(`Network Error: ${e}`));
req.send();
});
},
// ---
// SidebarToggle (Used in Apps)
initSidebarToggle() {
const sidebarToggler = document.querySelectorAll(
'[data-bs-toggle="sidebar"]',
);
sidebarToggler.forEach((el) => {
el.addEventListener("click", () => {
const target = el.getAttribute("data-target");
const overlay = el.getAttribute("data-overlay");
const appOverlay = document.querySelectorAll(".app-overlay");
const targetEl = document.querySelectorAll(target);
targetEl.forEach((tel) => {
tel.classList.toggle("show");
if (
typeof overlay !== "undefined" &&
overlay !== null &&
overlay !== false &&
typeof appOverlay !== "undefined"
) {
if (tel.classList.contains("show")) {
appOverlay[0].classList.add("show");
} else {
appOverlay[0].classList.remove("show");
}
appOverlay[0].addEventListener("click", (e) => {
e.currentTarget.classList.remove("show");
tel.classList.remove("show");
});
}
});
});
});
},
};
// *******************************************************************************
// * Initialization
if (typeof window !== "undefined") {
Helpers.init();
if (Helpers.isMobileDevice() && window.chrome) {
document.documentElement.classList.add("layout-menu-100vh");
}
// Update layout after page load
if (document.readyState === "complete") Helpers.update();
else
document.addEventListener("DOMContentLoaded", function onContentLoaded() {
Helpers.update();
document.removeEventListener("DOMContentLoaded", onContentLoaded);
});
}
// ---
window.Helpers = Helpers;

View file

@ -0,0 +1,322 @@
/**
* Main
*/
"use strict";
let menu, animate;
(function () {
// Initialize menu
//-----------------
let layoutMenuEl = document.querySelectorAll("#layout-menu");
layoutMenuEl.forEach(function (element) {
menu = new Menu(element, {
orientation: "vertical",
closeChildren: false,
});
// Change parameter to true if you want scroll animation
window.Helpers.scrollToActive((animate = false));
window.Helpers.mainMenu = menu;
});
// Initialize menu togglers and bind click on each
let menuToggler = document.querySelectorAll(".layout-menu-toggle");
menuToggler.forEach((item) => {
item.addEventListener("click", (event) => {
event.preventDefault();
window.Helpers.toggleCollapsed();
});
});
// Display menu toggle (layout-menu-toggle) on hover with delay
let delay = function (elem, callback) {
let timeout = null;
elem.onmouseenter = function () {
// Set timeout to be a timer which will invoke callback after 300ms (not for small screen)
if (!Helpers.isSmallScreen()) {
timeout = setTimeout(callback, 300);
} else {
timeout = setTimeout(callback, 0);
}
};
elem.onmouseleave = function () {
// Clear any timers set to timeout
document.querySelector(".layout-menu-toggle").classList.remove("d-block");
clearTimeout(timeout);
};
};
if (document.getElementById("layout-menu")) {
delay(document.getElementById("layout-menu"), function () {
// not for small screen
if (!Helpers.isSmallScreen()) {
document.querySelector(".layout-menu-toggle").classList.add("d-block");
}
});
}
// Display in main menu when menu scrolls
let menuInnerContainer = document.getElementsByClassName("menu-inner"),
menuInnerShadow = document.getElementsByClassName("menu-inner-shadow")[0];
if (menuInnerContainer.length > 0 && menuInnerShadow) {
menuInnerContainer[0].addEventListener("ps-scroll-y", function () {
if (this.querySelector(".ps__thumb-y").offsetTop) {
menuInnerShadow.style.display = "block";
} else {
menuInnerShadow.style.display = "none";
}
});
}
// Init helpers & misc
// --------------------
// Init BS Tooltip
const tooltipTriggerList = [].slice.call(
document.querySelectorAll('[data-bs-toggle="tooltip"]'),
);
tooltipTriggerList.map(function (tooltipTriggerEl) {
return new bootstrap.Tooltip(tooltipTriggerEl);
});
// Accordion active class
const accordionActiveFunction = function (e) {
if (e.type == "show.bs.collapse" || e.type == "show.bs.collapse") {
e.target.closest(".accordion-item").classList.add("active");
} else {
e.target.closest(".accordion-item").classList.remove("active");
}
};
const accordionTriggerList = [].slice.call(
document.querySelectorAll(".accordion"),
);
const accordionList = accordionTriggerList.map(function (accordionTriggerEl) {
accordionTriggerEl.addEventListener(
"show.bs.collapse",
accordionActiveFunction,
);
accordionTriggerEl.addEventListener(
"hide.bs.collapse",
accordionActiveFunction,
);
});
// Auto update layout based on screen size
window.Helpers.setAutoUpdate(true);
// Toggle Password Visibility
window.Helpers.initPasswordToggle();
// Speech To Text
window.Helpers.initSpeechToText();
// Manage menu expanded/collapsed with templateCustomizer & local storage
//------------------------------------------------------------------
// If current layout is horizontal OR current window screen is small (overlay menu) than return from here
if (window.Helpers.isSmallScreen()) {
return;
}
// If current layout is vertical and current window screen is > small
// Auto update menu collapsed/expanded based on the themeConfig
window.Helpers.setCollapsed(true, false);
})();
/**
* Custom
*/
class News {
constructor() {
this.BASE_URL = "https://www.bunkerweb.io/";
}
init() {
window.addEventListener("load", () => {
if (sessionStorage.getItem("lastRefetch") !== null) {
const storeStamp = sessionStorage.getItem("lastRefetch");
const nowStamp = Math.round(new Date().getTime() / 1000);
if (+nowStamp > storeStamp) {
sessionStorage.removeItem("lastRefetch");
sessionStorage.removeItem("lastNews");
}
}
if (sessionStorage.getItem("lastNews") !== null)
return this.render(JSON.parse(sessionStorage.getItem("lastNews")));
fetch("https://www.bunkerweb.io/api/posts/0/2")
.then((res) => {
return res.json();
})
.then((res) => {
const reverseData = res.data.reverse();
return this.render(reverseData);
})
.catch((e) => {});
});
}
render(lastNews) {
// store for next time if not the case
if (
!sessionStorage.getItem("lastNews") &&
!sessionStorage.getItem("lastRefetch")
) {
sessionStorage.setItem(
"lastRefetch",
Math.round(new Date().getTime() / 1000) + 3600,
);
sessionStorage.setItem("lastNews", JSON.stringify(lastNews));
const newsNumber = lastNews.length;
document.querySelector("#news-pill").insertAdjacentHTML(
"beforeend",
DOMPurify.sanitize(`<span class="badge rounded-pill badge-center-sm bg-danger ms-1_5"
>${newsNumber}</span
>`),
);
document.querySelector("#news-button").insertAdjacentHTML(
"beforeend",
DOMPurify.sanitize(`<span
class="badge-dot-text position-absolute top-0 start-100 translate-middle badge rounded-pill bg-danger"
>
${newsNumber}
<span class="visually-hidden">unread news</span>
</span>`),
);
}
const newsContainer = document.querySelector("[data-news-container]");
const lastItem = lastNews[0];
//remove default message
newsContainer.textContent = "";
document
.querySelector("[data-news-container]")
.insertAdjacentHTML(
"afterbegin",
`<div data-news-row class="row g-6 justify-content-center">`,
);
//render last news
lastNews.forEach((news) => {
//create html card from infos
const cardHTML = this.template(
news.title,
news.slug,
news.photo.url,
news.excerpt,
news.tags,
news.date,
news === lastItem,
);
const BASE_URL = this.BASE_URL;
let cleanHTML = DOMPurify.sanitize(cardHTML);
//add to DOM inside the created div
document
.querySelector("[data-news-row]")
.insertAdjacentHTML("afterbegin", cleanHTML);
document.querySelectorAll(`.blog-click-${news.slug}`).forEach((slug) => {
slug.addEventListener("click", function () {
window.open(
`${BASE_URL}blog/post/${news.slug}?utm_campaign=self&utm_source=ui`,
"_blank",
);
});
});
document.querySelectorAll(".blog-click-tag").forEach((tag) => {
tag.target = "_blank";
});
});
document
.querySelector("[data-news-container]")
.insertAdjacentHTML("beforeend", "</div>");
}
template(title, slug, img, excerpt, tags, date, last) {
//loop on tags to get list
let tagList = "";
tags.forEach((tag) => {
tagList += `<a
role="button"
href="${this.BASE_URL}/blog/tag/${tag.slug}?utm_campaign=self&utm_source=ui"
aria-pressed="true"
class="btn btn-sm btn-outline-primary"
target="_blank"
rel="noopener"
>
<span class="tf-icons bx bx-xs bx-purchase-tag bx-18px me-2"></span
>${tag.name}
</a>
`;
});
const card = `<div class="col-md-11 col-xl-11 ${last ? "" : "mb-1"}">
<div class="card">
<a
href="${this.BASE_URL}blog/post/${slug}?utm_campaign=self&utm_source=ui"
target="_blank"
rel="noopener"
><img class="card-img-top" src="${img}" alt="News image"
/></a>
<div class="card-body">
<h5 class="card-title">
<a
href="${
this.BASE_URL
}blog/post/${slug}?utm_campaign=self&utm_source=ui"
target="_blank"
rel="noopener"
>${title}</a
>
</h5>
<p class="card-text">${excerpt}</p>
<p class="d-flex flex-wrap">${tagList}</p>
<p class="card-text">
<small class="text-muted">Posted on : ${date}</small>
</p>
</div>
</div>
</div>
`;
return card;
}
}
const setNews = new News();
DOMPurify.addHook("afterSanitizeAttributes", function (node) {
// set all elements owning target to target=_blank
if ("target" in node) {
node.setAttribute("target", "_blank");
node.setAttribute("rel", "noopener");
}
});
document.addEventListener("DOMContentLoaded", function onContentLoaded() {
setNews.init();
// Generic Copy to Clipboard with Tooltip
$(".copy-to-clipboard").on("click", function () {
const input = $(this).closest(".input-group").find("input")[0];
// Use the Clipboard API
navigator.clipboard
.writeText(input.value)
.then(() => {
// Show tooltip
const button = $(this);
button.attr("data-bs-original-title", "Copied!").tooltip("show");
// Hide tooltip after 2 seconds
setTimeout(() => {
button.tooltip("hide").attr("data-bs-original-title", "");
}, 2000);
})
.catch((err) => {
console.error("Failed to copy text: ", err);
});
});
});

View file

@ -0,0 +1,653 @@
const TRANSITION_EVENTS = [
"transitionend",
"webkitTransitionEnd",
"oTransitionEnd",
];
// const TRANSITION_PROPERTIES = ['transition', 'MozTransition', 'webkitTransition', 'WebkitTransition', 'OTransition']
class Menu {
constructor(el, config = {}, _PS = null) {
this._el = el;
this._animate = config.animate !== false;
this._accordion = config.accordion !== false;
this._closeChildren = Boolean(config.closeChildren);
this._onOpen = config.onOpen || (() => {});
this._onOpened = config.onOpened || (() => {});
this._onClose = config.onClose || (() => {});
this._onClosed = config.onClosed || (() => {});
this._psScroll = null;
this._topParent = null;
this._menuBgClass = null;
el.classList.add("menu");
el.classList[this._animate ? "remove" : "add"]("menu-no-animation"); // check
el.classList.add("menu-vertical");
const PerfectScrollbarLib = _PS || window.PerfectScrollbar;
if (PerfectScrollbarLib) {
this._scrollbar = new PerfectScrollbarLib(
el.querySelector(".menu-inner"),
{
suppressScrollX: true,
wheelPropagation: !Menu._hasClass(
"layout-menu-fixed layout-menu-fixed-offcanvas",
),
},
);
window.Helpers.menuPsScroll = this._scrollbar;
} else {
el.querySelector(".menu-inner").classList.add("overflow-auto");
}
// Add data attribute for bg color class of menu
const menuClassList = el.classList;
for (let i = 0; i < menuClassList.length; i++) {
if (menuClassList[i].startsWith("bg-")) {
this._menuBgClass = menuClassList[i];
}
}
el.setAttribute("data-bg-class", this._menuBgClass);
this._bindEvents();
// Link menu instance to element
el.menuInstance = this;
}
_bindEvents() {
// Click Event
this._evntElClick = (e) => {
// Find top parent element
if (
e.target.closest("ul") &&
e.target.closest("ul").classList.contains("menu-inner")
) {
const menuItem = Menu._findParent(e.target, "menu-item", false);
// eslint-disable-next-line prefer-destructuring
if (menuItem) this._topParent = menuItem.childNodes[0];
}
const toggleLink = e.target.classList.contains("menu-toggle")
? e.target
: Menu._findParent(e.target, "menu-toggle", false);
if (toggleLink) {
e.preventDefault();
if (toggleLink.getAttribute("data-hover") !== "true") {
this.toggle(toggleLink);
}
}
};
if (window.Helpers.isMobileDevice)
this._el.addEventListener("click", this._evntElClick);
this._evntWindowResize = () => {
this.update();
if (this._lastWidth !== window.innerWidth) {
this._lastWidth = window.innerWidth;
this.update();
}
const horizontalMenuTemplate = document.querySelector(
"[data-template^='horizontal-menu']",
);
if (!this._horizontal && !horizontalMenuTemplate) this.manageScroll();
};
window.addEventListener("resize", this._evntWindowResize);
}
static childOf(/* child node */ c, /* parent node */ p) {
// returns boolean
if (c.parentNode) {
while ((c = c.parentNode) && c !== p);
return !!c;
}
return false;
}
_unbindEvents() {
if (this._evntElClick) {
this._el.removeEventListener("click", this._evntElClick);
this._evntElClick = null;
}
if (this._evntElMouseOver) {
this._el.removeEventListener("mouseover", this._evntElMouseOver);
this._evntElMouseOver = null;
}
if (this._evntElMouseOut) {
this._el.removeEventListener("mouseout", this._evntElMouseOut);
this._evntElMouseOut = null;
}
if (this._evntWindowResize) {
window.removeEventListener("resize", this._evntWindowResize);
this._evntWindowResize = null;
}
if (this._evntBodyClick) {
document.body.removeEventListener("click", this._evntBodyClick);
this._evntBodyClick = null;
}
if (this._evntInnerMousemove) {
this._inner.removeEventListener("mousemove", this._evntInnerMousemove);
this._evntInnerMousemove = null;
}
if (this._evntInnerMouseleave) {
this._inner.removeEventListener("mouseleave", this._evntInnerMouseleave);
this._evntInnerMouseleave = null;
}
}
static _isRoot(item) {
return !Menu._findParent(item, "menu-item", false);
}
static _findParent(el, cls, throwError = true) {
if (el.tagName.toUpperCase() === "BODY") return null;
el = el.parentNode;
while (el.tagName.toUpperCase() !== "BODY" && !el.classList.contains(cls)) {
el = el.parentNode;
}
el = el.tagName.toUpperCase() !== "BODY" ? el : null;
if (!el && throwError)
throw new Error(`Cannot find \`.${cls}\` parent element`);
return el;
}
static _findChild(el, cls) {
const items = el.childNodes;
const found = [];
for (let i = 0, l = items.length; i < l; i++) {
if (items[i].classList) {
let passed = 0;
for (let j = 0; j < cls.length; j++) {
if (items[i].classList.contains(cls[j])) passed += 1;
}
if (cls.length === passed) found.push(items[i]);
}
}
return found;
}
static _findMenu(item) {
let curEl = item.childNodes[0];
let menu = null;
while (curEl && !menu) {
if (curEl.classList && curEl.classList.contains("menu-sub")) menu = curEl;
curEl = curEl.nextSibling;
}
if (!menu)
throw new Error(
"Cannot find `.menu-sub` element for the current `.menu-toggle`",
);
return menu;
}
// Has class
static _hasClass(cls, el = window.Helpers.ROOT_EL) {
let result = false;
cls.split(" ").forEach((c) => {
if (el.classList.contains(c)) result = true;
});
return result;
}
open(el, closeChildren = this._closeChildren) {
const item = this._findUnopenedParent(
Menu._getItem(el, true),
closeChildren,
);
if (!item) return;
const toggleLink = Menu._getLink(item, true);
Menu._promisify(this._onOpen, this, item, toggleLink, Menu._findMenu(item))
.then(() => {
if (!this._horizontal || !Menu._isRoot(item)) {
if (this._animate && !this._horizontal) {
window.requestAnimationFrame(() =>
this._toggleAnimation(true, item, false),
);
if (this._accordion) this._closeOther(item, closeChildren);
} else if (this._animate) {
// eslint-disable-next-line no-unused-expressions
this._onOpened &&
this._onOpened(this, item, toggleLink, Menu._findMenu(item));
} else {
item.classList.add("open");
// eslint-disable-next-line no-unused-expressions
this._onOpened &&
this._onOpened(this, item, toggleLink, Menu._findMenu(item));
if (this._accordion) this._closeOther(item, closeChildren);
}
} else {
// eslint-disable-next-line no-unused-expressions
this._onOpened &&
this._onOpened(this, item, toggleLink, Menu._findMenu(item));
}
})
.catch(() => {});
}
close(el, closeChildren = this._closeChildren, _autoClose = false) {
const item = Menu._getItem(el, true);
const toggleLink = Menu._getLink(el, true);
if (!item.classList.contains("open") || item.classList.contains("disabled"))
return;
Menu._promisify(
this._onClose,
this,
item,
toggleLink,
Menu._findMenu(item),
_autoClose,
)
.then(() => {
if (!this._horizontal || !Menu._isRoot(item)) {
if (this._animate && !this._horizontal) {
window.requestAnimationFrame(() =>
this._toggleAnimation(false, item, closeChildren),
);
} else {
item.classList.remove("open");
if (closeChildren) {
const opened = item.querySelectorAll(".menu-item.open");
for (let i = 0, l = opened.length; i < l; i++)
opened[i].classList.remove("open");
}
// eslint-disable-next-line no-unused-expressions
this._onClosed &&
this._onClosed(this, item, toggleLink, Menu._findMenu(item));
}
} else {
// eslint-disable-next-line no-unused-expressions
this._onClosed &&
this._onClosed(this, item, toggleLink, Menu._findMenu(item));
}
})
.catch(() => {});
}
_closeOther(item, closeChildren) {
const opened = Menu._findChild(item.parentNode, ["menu-item", "open"]);
for (let i = 0, l = opened.length; i < l; i++) {
if (opened[i] !== item) this.close(opened[i], closeChildren);
}
}
toggle(el, closeChildren = this._closeChildren) {
const item = Menu._getItem(el, true);
// const toggleLink = Menu._getLink(el, true)
if (item.classList.contains("open")) this.close(item, closeChildren);
else this.open(item, closeChildren);
}
static _getItem(el, toggle) {
let item = null;
const selector = toggle ? "menu-toggle" : "menu-link";
if (el.classList.contains("menu-item")) {
if (Menu._findChild(el, [selector]).length) item = el;
} else if (el.classList.contains(selector)) {
item = el.parentNode.classList.contains("menu-item")
? el.parentNode
: null;
}
if (!item) {
throw new Error(
`${toggle ? "Toggable " : ""}\`.menu-item\` element not found.`,
);
}
return item;
}
static _getLink(el, toggle) {
let found = [];
const selector = toggle ? "menu-toggle" : "menu-link";
if (el.classList.contains(selector)) found = [el];
else if (el.classList.contains("menu-item"))
found = Menu._findChild(el, [selector]);
if (!found.length) throw new Error(`\`${selector}\` element not found.`);
return found[0];
}
_findUnopenedParent(item, closeChildren) {
let tree = [];
let parentItem = null;
while (item) {
if (item.classList.contains("disabled")) {
parentItem = null;
tree = [];
} else {
if (!item.classList.contains("open")) parentItem = item;
tree.push(item);
}
item = Menu._findParent(item, "menu-item", false);
}
if (!parentItem) return null;
if (tree.length === 1) return parentItem;
tree = tree.slice(0, tree.indexOf(parentItem));
for (let i = 0, l = tree.length; i < l; i++) {
tree[i].classList.add("open");
if (this._accordion) {
const openedItems = Menu._findChild(tree[i].parentNode, [
"menu-item",
"open",
]);
for (let j = 0, k = openedItems.length; j < k; j++) {
if (openedItems[j] !== tree[i]) {
openedItems[j].classList.remove("open");
if (closeChildren) {
const openedChildren =
openedItems[j].querySelectorAll(".menu-item.open");
for (let x = 0, z = openedChildren.length; x < z; x++) {
openedChildren[x].classList.remove("open");
}
}
}
}
}
}
return parentItem;
}
_toggleAnimation(open, item, closeChildren) {
const toggleLink = Menu._getLink(item, true);
const menu = Menu._findMenu(item);
Menu._unbindAnimationEndEvent(item);
const linkHeight = Math.round(toggleLink.getBoundingClientRect().height);
item.style.overflow = "hidden";
const clearItemStyle = () => {
item.classList.remove("menu-item-animating");
item.classList.remove("menu-item-closing");
item.style.overflow = null;
item.style.height = null;
this.update();
};
if (open) {
item.style.height = `${linkHeight}px`;
item.classList.add("menu-item-animating");
item.classList.add("open");
Menu._bindAnimationEndEvent(item, () => {
clearItemStyle();
this._onOpened(this, item, toggleLink, menu);
});
setTimeout(() => {
item.style.height = `${
linkHeight + Math.round(menu.getBoundingClientRect().height)
}px`;
}, 50);
} else {
item.style.height = `${
linkHeight + Math.round(menu.getBoundingClientRect().height)
}px`;
item.classList.add("menu-item-animating");
item.classList.add("menu-item-closing");
Menu._bindAnimationEndEvent(item, () => {
item.classList.remove("open");
clearItemStyle();
if (closeChildren) {
const opened = item.querySelectorAll(".menu-item.open");
for (let i = 0, l = opened.length; i < l; i++)
opened[i].classList.remove("open");
}
this._onClosed(this, item, toggleLink, menu);
});
setTimeout(() => {
item.style.height = `${linkHeight}px`;
}, 50);
}
}
static _bindAnimationEndEvent(el, handler) {
const cb = (e) => {
if (e.target !== el) return;
Menu._unbindAnimationEndEvent(el);
handler(e);
};
let duration = window.getComputedStyle(el).transitionDuration;
duration =
parseFloat(duration) * (duration.indexOf("ms") !== -1 ? 1 : 1000);
el._menuAnimationEndEventCb = cb;
TRANSITION_EVENTS.forEach((ev) =>
el.addEventListener(ev, el._menuAnimationEndEventCb, false),
);
el._menuAnimationEndEventTimeout = setTimeout(() => {
cb({ target: el });
}, duration + 50);
}
_getItemOffset(item) {
let curItem = this._inner.childNodes[0];
let left = 0;
while (curItem !== item) {
if (curItem.tagName) {
left += Math.round(curItem.getBoundingClientRect().width);
}
curItem = curItem.nextSibling;
}
return left;
}
static _promisify(fn, ...args) {
const result = fn(...args);
if (result instanceof Promise) {
return result;
}
if (result === false) {
return Promise.reject();
}
return Promise.resolve();
}
get _innerWidth() {
const items = this._inner.childNodes;
let width = 0;
for (let i = 0, l = items.length; i < l; i++) {
if (items[i].tagName) {
width += Math.round(items[i].getBoundingClientRect().width);
}
}
return width;
}
get _innerPosition() {
return parseInt(
this._inner.style[this._rtl ? "marginRight" : "marginLeft"] || "0px",
10,
);
}
set _innerPosition(value) {
this._inner.style[this._rtl ? "marginRight" : "marginLeft"] = `${value}px`;
return value;
}
static _unbindAnimationEndEvent(el) {
const cb = el._menuAnimationEndEventCb;
if (el._menuAnimationEndEventTimeout) {
clearTimeout(el._menuAnimationEndEventTimeout);
el._menuAnimationEndEventTimeout = null;
}
if (!cb) return;
TRANSITION_EVENTS.forEach((ev) => el.removeEventListener(ev, cb, false));
el._menuAnimationEndEventCb = null;
}
closeAll(closeChildren = this._closeChildren) {
const opened = this._el.querySelectorAll(".menu-inner > .menu-item.open");
for (let i = 0, l = opened.length; i < l; i++)
this.close(opened[i], closeChildren);
}
static setDisabled(el, disabled) {
Menu._getItem(el, false).classList[disabled ? "add" : "remove"]("disabled");
}
static isActive(el) {
return Menu._getItem(el, false).classList.contains("active");
}
static isOpened(el) {
return Menu._getItem(el, false).classList.contains("open");
}
static isDisabled(el) {
return Menu._getItem(el, false).classList.contains("disabled");
}
update() {
if (this._scrollbar) {
this._scrollbar.update();
}
}
manageScroll() {
const { PerfectScrollbar } = window;
const menuInner = document.querySelector(".menu-inner");
if (window.innerWidth < window.Helpers.LAYOUT_BREAKPOINT) {
if (this._scrollbar !== null) {
// window.Helpers.menuPsScroll.destroy()
this._scrollbar.destroy();
this._scrollbar = null;
}
menuInner.classList.add("overflow-auto");
} else {
if (this._scrollbar === null) {
const menuScroll = new PerfectScrollbar(
document.querySelector(".menu-inner"),
{
suppressScrollX: true,
wheelPropagation: false,
},
);
this._scrollbar = menuScroll;
}
menuInner.classList.remove("overflow-auto");
}
}
destroy() {
if (!this._el) return;
this._unbindEvents();
const items = this._el.querySelectorAll(".menu-item");
for (let i = 0, l = items.length; i < l; i++) {
Menu._unbindAnimationEndEvent(items[i]);
items[i].classList.remove("menu-item-animating");
items[i].classList.remove("open");
items[i].style.overflow = null;
items[i].style.height = null;
}
const menus = this._el.querySelectorAll(".menu-menu");
for (let i2 = 0, l2 = menus.length; i2 < l2; i2++) {
menus[i2].style.marginRight = null;
menus[i2].style.marginLeft = null;
}
this._el.classList.remove("menu-no-animation");
if (this._wrapper) {
this._prevBtn.parentNode.removeChild(this._prevBtn);
this._nextBtn.parentNode.removeChild(this._nextBtn);
this._wrapper.parentNode.insertBefore(this._inner, this._wrapper);
this._wrapper.parentNode.removeChild(this._wrapper);
this._inner.style.marginLeft = null;
this._inner.style.marginRight = null;
}
this._el.menuInstance = null;
delete this._el.menuInstance;
this._el = null;
this._animate = null;
this._accordion = null;
this._closeChildren = null;
this._onOpen = null;
this._onOpened = null;
this._onClose = null;
this._onClosed = null;
if (this._scrollbar) {
this._scrollbar.destroy();
this._scrollbar = null;
}
this._inner = null;
this._prevBtn = null;
this._wrapper = null;
this._nextBtn = null;
}
}
window.Menu = Menu;

View file

@ -0,0 +1,29 @@
/**
* Account Settings - Account
*/
"use strict";
document.addEventListener("DOMContentLoaded", function (e) {
(function () {
const deactivateAcc = document.querySelector("#formAccountDeactivation");
// Update/reset user image of account page
let accountUserImage = document.getElementById("uploadedAvatar");
const fileInput = document.querySelector(".account-file-input"),
resetFileInput = document.querySelector(".account-image-reset");
if (accountUserImage) {
const resetImage = accountUserImage.src;
fileInput.onchange = () => {
if (fileInput.files[0]) {
accountUserImage.src = window.URL.createObjectURL(fileInput.files[0]);
}
};
resetFileInput.onclick = () => {
fileInput.value = "";
accountUserImage.src = resetImage;
};
}
})();
});

View file

@ -0,0 +1,149 @@
$(document).ready(function () {
function validatePassword() {
const password = $("#new_password").val();
let isValid = true;
// Validate length
if (password.length >= 8) {
$("#length-check i")
.removeClass("bx-x text-danger")
.addClass("bx-check text-success");
} else {
isValid = false;
$("#length-check i")
.removeClass("bx-check text-success")
.addClass("bx-x text-danger");
}
// Validate uppercase letter
if (/[A-Z]/.test(password)) {
$("#uppercase-check i")
.removeClass("bx-x text-danger")
.addClass("bx-check text-success");
} else {
isValid = false;
$("#uppercase-check i")
.removeClass("bx-check text-success")
.addClass("bx-x text-danger");
}
// Validate number
if (/\d/.test(password)) {
$("#number-check i")
.removeClass("bx-x text-danger")
.addClass("bx-check text-success");
} else {
isValid = false;
$("#number-check i")
.removeClass("bx-check text-success")
.addClass("bx-x text-danger");
}
// Validate special character
if (/[ !"#$%&'()*+,\-./:;<=>?@[\\\]^_`{|}~]/.test(password)) {
$("#special-check i")
.removeClass("bx-x text-danger")
.addClass("bx-check text-success");
} else {
isValid = false;
$("#special-check i")
.removeClass("bx-check text-success")
.addClass("bx-x text-danger");
}
return isValid;
}
// Real-time validation as user types
$("#new_password").on("input", function () {
if (validatePassword()) {
$(this).removeClass("is-invalid");
$(this).addClass("is-valid");
}
});
function matchPassword() {
const newPassword = $("#new_password").val();
const confirmPassword = $("#new_password_confirm").val();
if (newPassword === confirmPassword) {
$("#new_password_confirm").removeClass("is-invalid");
$("#new_password_confirm").addClass("is-valid");
} else {
$("#new_password_confirm").removeClass("is-valid");
$("#new_password_confirm").addClass("is-invalid");
}
}
$("#new_password_confirm").on("input", function () {
if (matchPassword()) {
$(this).removeClass("is-invalid");
$(this).addClass("is-valid");
}
});
// Form submission validation
$("#formPasswordSettings").on("submit", function (e) {
const newPasswordInput = $("#new_password");
const confirmPasswordInput = $("#new_password_confirm");
let isValid = true;
// Check if passwords match
if (newPasswordInput.val() !== confirmPasswordInput.val()) {
isValid = false;
confirmPasswordInput.addClass("is-invalid");
} else {
confirmPasswordInput.removeClass("is-invalid");
confirmPasswordInput.addClass("is-valid");
}
// Validate password using real-time checks
if (!validatePassword()) {
isValid = false;
newPasswordInput.addClass("is-invalid");
} else {
newPasswordInput.removeClass("is-invalid");
newPasswordInput.addClass("is-valid");
}
// Prevent form submission if validation fails
if (!isValid) {
e.preventDefault();
}
});
// Listen for tab change
$('button[data-bs-toggle="tab"]').on("shown.bs.tab", function (e) {
// Get the target tab's ID (data-bs-target without the '#')
var target = $(e.target)
.data("bs-target")
.substring(1)
.replace("navs-pills-", "");
if (target === "profile") {
if (window.location.hash) {
history.pushState(
"",
document.title,
window.location.pathname + window.location.search,
);
}
return;
}
// Update the URL fragment
window.location.hash = target;
});
// On page load, activate the tab based on the URL fragment
var hash = window.location.hash;
if (hash) {
var targetTab = $(
`button[data-bs-target="#navs-pills-${hash.substring(1)}"]`,
);
if (targetTab.length) {
targetTab.tab("show");
}
}
});

View file

@ -0,0 +1,35 @@
/**
* UI Modals
*/
"use strict";
(function () {
// On hiding modal, remove iframe video/audio to stop playing
const youTubeModal = document.querySelector("#youTubeModal"),
youTubeModalVideo = youTubeModal.querySelector("iframe");
youTubeModal.addEventListener("hidden.bs.modal", function () {
youTubeModalVideo.setAttribute("src", "");
});
// Function to get and auto play youTube video
const autoPlayYouTubeModal = function () {
const modalTriggerList = [].slice.call(
document.querySelectorAll('[data-bs-toggle="modal"]'),
);
modalTriggerList.map(function (modalTriggerEl) {
modalTriggerEl.onclick = function () {
const theModal = this.getAttribute("data-bs-target"),
videoSRC = this.getAttribute("data-theVideo"),
videoSRCauto = `${videoSRC}?autoplay=1`,
modalVideo = document.querySelector(`${theModal} iframe`);
if (modalVideo) {
modalVideo.setAttribute("src", videoSRCauto);
}
};
});
};
// Calling function on load
autoPlayYouTubeModal();
})();

View file

@ -0,0 +1,18 @@
// /**
// * UI Tooltips & Popovers
// */
"use strict";
(function () {
const popoverTriggerList = [].slice.call(
document.querySelectorAll('[data-bs-toggle="popover"]'),
);
const popoverList = popoverTriggerList.map(function (popoverTriggerEl) {
// added { html: true, sanitize: false } option to render button in content area of popover
return new bootstrap.Popover(popoverTriggerEl, {
html: true,
sanitize: false,
});
});
})();

View file

@ -0,0 +1,47 @@
/**
* UI Toasts
*/
"use strict";
(function () {
// Bootstrap toasts example
// --------------------------------------------------------------------
const toastPlacementExample = document.querySelector(".toast-placement-ex"),
toastPlacementBtn = document.querySelector("#showToastPlacement");
let selectedType, selectedPlacement, toastPlacement;
// Dispose toast when open another
function toastDispose(toast) {
if (toast && toast._element !== null) {
if (toastPlacementExample) {
toastPlacementExample.classList.remove(selectedType);
DOMTokenList.prototype.remove.apply(
toastPlacementExample.classList,
selectedPlacement,
);
}
toast.dispose();
}
}
// Placement Button click
if (toastPlacementBtn) {
toastPlacementBtn.onclick = function () {
if (toastPlacement) {
toastDispose(toastPlacement);
}
selectedType = document.querySelector("#selectTypeOpt").value;
selectedPlacement = document
.querySelector("#selectPlacement")
.value.split(" ");
toastPlacementExample.classList.add(selectedType);
DOMTokenList.prototype.add.apply(
toastPlacementExample.classList,
selectedPlacement,
);
toastPlacement = new bootstrap.Toast(toastPlacementExample);
toastPlacement.show();
};
}
})();

View file

@ -0,0 +1,689 @@
@keyframes opaque {
0% {
opacity: 0;
}
to {
opacity: 1;
}
}
@keyframes resizeanim {
0%,
to {
opacity: 0;
}
}
.apexcharts-canvas {
position: relative;
user-select: none;
}
.apexcharts-canvas ::-webkit-scrollbar {
-webkit-appearance: none;
width: 6px;
}
.apexcharts-canvas ::-webkit-scrollbar-thumb {
border-radius: 4px;
background-color: rgba(0, 0, 0, 0.5);
box-shadow: 0 0 1px rgba(255, 255, 255, 0.5);
-webkit-box-shadow: 0 0 1px rgba(255, 255, 255, 0.5);
}
.apexcharts-inner {
position: relative;
}
.apexcharts-text tspan {
font-family: inherit;
}
rect.legend-mouseover-inactive,
.legend-mouseover-inactive rect,
.legend-mouseover-inactive path,
.legend-mouseover-inactive circle,
.legend-mouseover-inactive line,
.legend-mouseover-inactive text.apexcharts-yaxis-title-text,
.legend-mouseover-inactive text.apexcharts-yaxis-label {
transition: 0.15s ease all;
opacity: 0.2;
}
.apexcharts-legend-text {
padding-left: 15px;
margin-left: -15px;
}
.apexcharts-series-collapsed {
opacity: 0;
}
.apexcharts-tooltip {
border-radius: 5px;
box-shadow: 2px 2px 6px -4px #999;
cursor: default;
font-size: 14px;
left: 62px;
opacity: 0;
pointer-events: none;
position: absolute;
top: 20px;
display: flex;
flex-direction: column;
overflow: hidden;
white-space: nowrap;
z-index: 12;
transition: 0.15s ease all;
}
.apexcharts-tooltip.apexcharts-active {
opacity: 1;
transition: 0.15s ease all;
}
.apexcharts-tooltip.apexcharts-theme-light {
border: 1px solid #e3e3e3;
background: rgba(255, 255, 255, 0.96);
}
.apexcharts-tooltip.apexcharts-theme-dark {
color: #fff;
background: rgba(30, 30, 30, 0.8);
}
.apexcharts-tooltip * {
font-family: inherit;
}
.apexcharts-tooltip-title {
padding: 6px;
font-size: 15px;
margin-bottom: 4px;
}
.apexcharts-tooltip.apexcharts-theme-light .apexcharts-tooltip-title {
background: #eceff1;
border-bottom: 1px solid #ddd;
}
.apexcharts-tooltip.apexcharts-theme-dark .apexcharts-tooltip-title {
background: rgba(0, 0, 0, 0.7);
border-bottom: 1px solid #333;
}
.apexcharts-tooltip-text-goals-value,
.apexcharts-tooltip-text-y-value,
.apexcharts-tooltip-text-z-value {
display: inline-block;
margin-left: 5px;
font-weight: 600;
}
.apexcharts-tooltip-text-goals-label:empty,
.apexcharts-tooltip-text-goals-value:empty,
.apexcharts-tooltip-text-y-label:empty,
.apexcharts-tooltip-text-y-value:empty,
.apexcharts-tooltip-text-z-value:empty,
.apexcharts-tooltip-title:empty {
display: none;
}
.apexcharts-tooltip-text-goals-label,
.apexcharts-tooltip-text-goals-value {
padding: 6px 0 5px;
}
.apexcharts-tooltip-goals-group,
.apexcharts-tooltip-text-goals-label,
.apexcharts-tooltip-text-goals-value {
display: flex;
}
.apexcharts-tooltip-text-goals-label:not(:empty),
.apexcharts-tooltip-text-goals-value:not(:empty) {
margin-top: -6px;
}
.apexcharts-tooltip-marker {
width: 12px;
height: 12px;
position: relative;
top: 0;
margin-right: 10px;
border-radius: 50%;
}
.apexcharts-tooltip-series-group {
padding: 0 10px;
display: none;
text-align: left;
justify-content: left;
align-items: center;
}
.apexcharts-tooltip-series-group.apexcharts-active .apexcharts-tooltip-marker {
opacity: 1;
}
.apexcharts-tooltip-series-group.apexcharts-active,
.apexcharts-tooltip-series-group:last-child {
padding-bottom: 4px;
}
.apexcharts-tooltip-y-group {
padding: 6px 0 5px;
}
.apexcharts-custom-tooltip,
.apexcharts-tooltip-box {
padding: 4px 8px;
}
.apexcharts-tooltip-boxPlot {
display: flex;
flex-direction: column-reverse;
}
.apexcharts-tooltip-box > div {
margin: 4px 0;
}
.apexcharts-tooltip-box span.value {
font-weight: 700;
}
.apexcharts-tooltip-rangebar {
padding: 5px 8px;
}
.apexcharts-tooltip-rangebar .category {
font-weight: 600;
color: #777;
}
.apexcharts-tooltip-rangebar .series-name {
font-weight: 700;
display: block;
margin-bottom: 5px;
}
.apexcharts-xaxistooltip,
.apexcharts-yaxistooltip {
opacity: 0;
pointer-events: none;
color: #373d3f;
font-size: 13px;
text-align: center;
border-radius: 2px;
position: absolute;
z-index: 10;
background: #eceff1;
border: 1px solid #90a4ae;
}
.apexcharts-xaxistooltip {
padding: 9px 10px;
transition: 0.15s ease all;
}
.apexcharts-xaxistooltip.apexcharts-theme-dark {
background: rgba(0, 0, 0, 0.7);
border: 1px solid rgba(0, 0, 0, 0.5);
color: #fff;
}
.apexcharts-xaxistooltip:after,
.apexcharts-xaxistooltip:before {
left: 50%;
border: solid transparent;
content: " ";
height: 0;
width: 0;
position: absolute;
pointer-events: none;
}
.apexcharts-xaxistooltip:after {
border-color: transparent;
border-width: 6px;
margin-left: -6px;
}
.apexcharts-xaxistooltip:before {
border-color: transparent;
border-width: 7px;
margin-left: -7px;
}
.apexcharts-xaxistooltip-bottom:after,
.apexcharts-xaxistooltip-bottom:before {
bottom: 100%;
}
.apexcharts-xaxistooltip-top:after,
.apexcharts-xaxistooltip-top:before {
top: 100%;
}
.apexcharts-xaxistooltip-bottom:after {
border-bottom-color: #eceff1;
}
.apexcharts-xaxistooltip-bottom:before {
border-bottom-color: #90a4ae;
}
.apexcharts-xaxistooltip-bottom.apexcharts-theme-dark:after,
.apexcharts-xaxistooltip-bottom.apexcharts-theme-dark:before {
border-bottom-color: rgba(0, 0, 0, 0.5);
}
.apexcharts-xaxistooltip-top:after {
border-top-color: #eceff1;
}
.apexcharts-xaxistooltip-top:before {
border-top-color: #90a4ae;
}
.apexcharts-xaxistooltip-top.apexcharts-theme-dark:after,
.apexcharts-xaxistooltip-top.apexcharts-theme-dark:before {
border-top-color: rgba(0, 0, 0, 0.5);
}
.apexcharts-xaxistooltip.apexcharts-active {
opacity: 1;
transition: 0.15s ease all;
}
.apexcharts-yaxistooltip {
padding: 4px 10px;
}
.apexcharts-yaxistooltip.apexcharts-theme-dark {
background: rgba(0, 0, 0, 0.7);
border: 1px solid rgba(0, 0, 0, 0.5);
color: #fff;
}
.apexcharts-yaxistooltip:after,
.apexcharts-yaxistooltip:before {
top: 50%;
border: solid transparent;
content: " ";
height: 0;
width: 0;
position: absolute;
pointer-events: none;
}
.apexcharts-yaxistooltip:after {
border-color: transparent;
border-width: 6px;
margin-top: -6px;
}
.apexcharts-yaxistooltip:before {
border-color: transparent;
border-width: 7px;
margin-top: -7px;
}
.apexcharts-yaxistooltip-left:after,
.apexcharts-yaxistooltip-left:before {
left: 100%;
}
.apexcharts-yaxistooltip-right:after,
.apexcharts-yaxistooltip-right:before {
right: 100%;
}
.apexcharts-yaxistooltip-left:after {
border-left-color: #eceff1;
}
.apexcharts-yaxistooltip-left:before {
border-left-color: #90a4ae;
}
.apexcharts-yaxistooltip-left.apexcharts-theme-dark:after,
.apexcharts-yaxistooltip-left.apexcharts-theme-dark:before {
border-left-color: rgba(0, 0, 0, 0.5);
}
.apexcharts-yaxistooltip-right:after {
border-right-color: #eceff1;
}
.apexcharts-yaxistooltip-right:before {
border-right-color: #90a4ae;
}
.apexcharts-yaxistooltip-right.apexcharts-theme-dark:after,
.apexcharts-yaxistooltip-right.apexcharts-theme-dark:before {
border-right-color: rgba(0, 0, 0, 0.5);
}
.apexcharts-yaxistooltip.apexcharts-active {
opacity: 1;
}
.apexcharts-yaxistooltip-hidden {
display: none;
}
.apexcharts-xcrosshairs,
.apexcharts-ycrosshairs {
pointer-events: none;
opacity: 0;
transition: 0.15s ease all;
}
.apexcharts-xcrosshairs.apexcharts-active,
.apexcharts-ycrosshairs.apexcharts-active {
opacity: 1;
transition: 0.15s ease all;
}
.apexcharts-ycrosshairs-hidden {
opacity: 0;
}
.apexcharts-selection-rect {
cursor: move;
}
.svg_select_boundingRect,
.svg_select_points_rot {
pointer-events: none;
opacity: 0;
visibility: hidden;
}
.apexcharts-selection-rect + g .svg_select_boundingRect,
.apexcharts-selection-rect + g .svg_select_points_rot {
opacity: 0;
visibility: hidden;
}
.apexcharts-selection-rect + g .svg_select_points_l,
.apexcharts-selection-rect + g .svg_select_points_r {
cursor: ew-resize;
opacity: 1;
visibility: visible;
}
.svg_select_points {
fill: #efefef;
stroke: #333;
rx: 2;
}
.apexcharts-svg.apexcharts-zoomable.hovering-zoom {
cursor: crosshair;
}
.apexcharts-svg.apexcharts-zoomable.hovering-pan {
cursor: move;
}
.apexcharts-menu-icon,
.apexcharts-pan-icon,
.apexcharts-reset-icon,
.apexcharts-selection-icon,
.apexcharts-toolbar-custom-icon,
.apexcharts-zoom-icon,
.apexcharts-zoomin-icon,
.apexcharts-zoomout-icon {
cursor: pointer;
width: 20px;
height: 20px;
line-height: 24px;
color: #6e8192;
text-align: center;
}
.apexcharts-menu-icon svg,
.apexcharts-reset-icon svg,
.apexcharts-zoom-icon svg,
.apexcharts-zoomin-icon svg,
.apexcharts-zoomout-icon svg {
fill: #6e8192;
}
.apexcharts-selection-icon svg {
fill: #444;
transform: scale(0.76);
}
.apexcharts-theme-dark .apexcharts-menu-icon svg,
.apexcharts-theme-dark .apexcharts-pan-icon svg,
.apexcharts-theme-dark .apexcharts-reset-icon svg,
.apexcharts-theme-dark .apexcharts-selection-icon svg,
.apexcharts-theme-dark .apexcharts-toolbar-custom-icon svg,
.apexcharts-theme-dark .apexcharts-zoom-icon svg,
.apexcharts-theme-dark .apexcharts-zoomin-icon svg,
.apexcharts-theme-dark .apexcharts-zoomout-icon svg {
fill: #f3f4f5;
}
.apexcharts-canvas .apexcharts-reset-zoom-icon.apexcharts-selected svg,
.apexcharts-canvas .apexcharts-selection-icon.apexcharts-selected svg,
.apexcharts-canvas .apexcharts-zoom-icon.apexcharts-selected svg {
fill: #008ffb;
}
.apexcharts-theme-light .apexcharts-menu-icon:hover svg,
.apexcharts-theme-light .apexcharts-reset-icon:hover svg,
.apexcharts-theme-light
.apexcharts-selection-icon:not(.apexcharts-selected):hover
svg,
.apexcharts-theme-light
.apexcharts-zoom-icon:not(.apexcharts-selected):hover
svg,
.apexcharts-theme-light .apexcharts-zoomin-icon:hover svg,
.apexcharts-theme-light .apexcharts-zoomout-icon:hover svg {
fill: #333;
}
.apexcharts-menu-icon,
.apexcharts-selection-icon {
position: relative;
}
.apexcharts-reset-icon {
margin-left: 5px;
}
.apexcharts-menu-icon,
.apexcharts-reset-icon,
.apexcharts-zoom-icon {
transform: scale(0.85);
}
.apexcharts-zoomin-icon,
.apexcharts-zoomout-icon {
transform: scale(0.7);
}
.apexcharts-zoomout-icon {
margin-right: 3px;
}
.apexcharts-pan-icon {
transform: scale(0.62);
position: relative;
left: 1px;
top: 0;
}
.apexcharts-pan-icon svg {
fill: #fff;
stroke: #6e8192;
stroke-width: 2;
}
.apexcharts-pan-icon.apexcharts-selected svg {
stroke: #008ffb;
}
.apexcharts-pan-icon:not(.apexcharts-selected):hover svg {
stroke: #333;
}
.apexcharts-toolbar {
position: absolute;
z-index: 11;
max-width: 176px;
text-align: right;
border-radius: 3px;
padding: 0 6px 2px;
display: flex;
justify-content: space-between;
align-items: center;
}
.apexcharts-menu {
background: #fff;
position: absolute;
top: 100%;
border: 1px solid #ddd;
border-radius: 3px;
padding: 3px;
right: 10px;
opacity: 0;
min-width: 110px;
transition: 0.15s ease all;
pointer-events: none;
}
.apexcharts-menu.apexcharts-menu-open {
opacity: 1;
pointer-events: all;
transition: 0.15s ease all;
}
.apexcharts-menu-item {
padding: 6px 7px;
font-size: 12px;
cursor: pointer;
}
.apexcharts-theme-light .apexcharts-menu-item:hover {
background: #eee;
}
.apexcharts-theme-dark .apexcharts-menu {
background: rgba(0, 0, 0, 0.7);
color: #fff;
}
@media screen and (min-width: 768px) {
.apexcharts-canvas:hover .apexcharts-toolbar {
opacity: 1;
}
}
.apexcharts-canvas .apexcharts-element-hidden,
.apexcharts-datalabel.apexcharts-element-hidden,
.apexcharts-hide .apexcharts-series-points {
opacity: 0;
}
.apexcharts-hidden-element-shown {
opacity: 1;
transition: 0.25s ease all;
}
.apexcharts-datalabel,
.apexcharts-datalabel-label,
.apexcharts-datalabel-value,
.apexcharts-datalabels,
.apexcharts-pie-label {
cursor: default;
pointer-events: none;
}
.apexcharts-pie-label-delay {
opacity: 0;
animation-name: opaque;
animation-duration: 0.3s;
animation-fill-mode: forwards;
animation-timing-function: ease;
}
.apexcharts-radialbar-label {
cursor: pointer;
}
.apexcharts-annotation-rect,
.apexcharts-area-series .apexcharts-area,
.apexcharts-area-series
.apexcharts-series-markers
.apexcharts-marker.no-pointer-events,
.apexcharts-gridline,
.apexcharts-line,
.apexcharts-line-series
.apexcharts-series-markers
.apexcharts-marker.no-pointer-events,
.apexcharts-point-annotation-label,
.apexcharts-radar-series path:not(.apexcharts-marker),
.apexcharts-radar-series polygon,
.apexcharts-toolbar svg,
.apexcharts-tooltip .apexcharts-marker,
.apexcharts-xaxis-annotation-label,
.apexcharts-yaxis-annotation-label,
.apexcharts-zoom-rect {
pointer-events: none;
}
.apexcharts-tooltip-active .apexcharts-marker {
transition: 0.15s ease all;
}
.resize-triggers {
animation: 1ms resizeanim;
visibility: hidden;
opacity: 0;
height: 100%;
width: 100%;
overflow: hidden;
}
.contract-trigger:before,
.resize-triggers,
.resize-triggers > div {
content: " ";
display: block;
position: absolute;
top: 0;
left: 0;
}
.resize-triggers > div {
height: 100%;
width: 100%;
background: #eee;
overflow: auto;
}
.contract-trigger:before {
overflow: hidden;
width: 200%;
height: 200%;
}
.apexcharts-bar-goals-markers {
pointer-events: none;
}
.apexcharts-bar-shadows {
pointer-events: none;
}
.apexcharts-rangebar-goals-markers {
pointer-events: none;
}

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,55 @@
{
"name": "ar",
"options": {
"months": [
"يناير",
"فبراير",
"مارس",
"أبريل",
"مايو",
"يونيو",
"يوليو",
"أغسطس",
"سبتمبر",
"أكتوبر",
"نوفمبر",
"ديسمبر"
],
"shortMonths": [
"يناير",
"فبراير",
"مارس",
"أبريل",
"مايو",
"يونيو",
"يوليو",
"أغسطس",
"سبتمبر",
"أكتوبر",
"نوفمبر",
"ديسمبر"
],
"days": [
"الأحد",
"الإثنين",
"الثلاثاء",
"الأربعاء",
"الخميس",
"الجمعة",
"السبت"
],
"shortDays": ["أحد", "إثنين", "ثلاثاء", "أربعاء", "خميس", "جمعة", "سبت"],
"toolbar": {
"exportToSVG": "تحميل بصيغة SVG",
"exportToPNG": "تحميل بصيغة PNG",
"exportToCSV": "تحميل بصيغة CSV",
"menu": "القائمة",
"selection": "تحديد",
"selectionZoom": "تكبير التحديد",
"zoomIn": "تكبير",
"zoomOut": "تصغير",
"pan": "تحريك",
"reset": "إعادة التعيين"
}
}
}

View file

@ -0,0 +1,55 @@
{
"name": "be-cyrl",
"options": {
"months": [
"Студзень",
"Люты",
"Сакавік",
"Красавік",
"Травень",
"Чэрвень",
"Ліпень",
"Жнівень",
"Верасень",
"Кастрычнік",
"Лістапад",
"Сьнежань"
],
"shortMonths": [
"Сту",
"Лют",
"Сак",
"Кра",
"Тра",
"Чэр",
"Ліп",
"Жні",
"Вер",
"Кас",
"Ліс",
"Сьн"
],
"days": [
"Нядзеля",
"Панядзелак",
"Аўторак",
"Серада",
"Чацьвер",
"Пятніца",
"Субота"
],
"shortDays": ["Нд", "Пн", "Аў", "Ср", "Чц", "Пт", "Сб"],
"toolbar": {
"exportToSVG": "Спампаваць SVG",
"exportToPNG": "Спампаваць PNG",
"exportToCSV": "Спампаваць CSV",
"menu": "Мэню",
"selection": "Вылучэньне",
"selectionZoom": "Вылучэньне з маштабаваньнем",
"zoomIn": "Наблізіць",
"zoomOut": "Аддаліць",
"pan": "Ссоўваньне",
"reset": "Скінуць маштабаваньне"
}
}
}

View file

@ -0,0 +1,55 @@
{
"name": "be-latn",
"options": {
"months": [
"Studzień",
"Luty",
"Sakavik",
"Krasavik",
"Travień",
"Červień",
"Lipień",
"Žnivień",
"Vierasień",
"Kastryčnik",
"Listapad",
"Śniežań"
],
"shortMonths": [
"Stu",
"Lut",
"Sak",
"Kra",
"Tra",
"Čer",
"Lip",
"Žni",
"Vie",
"Kas",
"Lis",
"Śni"
],
"days": [
"Niadziela",
"Paniadziełak",
"Aŭtorak",
"Sierada",
"Čaćvier",
"Piatnica",
"Subota"
],
"shortDays": ["Nd", "Pn", "Aŭ", "Sr", "Čć", "Pt", "Sb"],
"toolbar": {
"exportToSVG": "Spampavać SVG",
"exportToPNG": "Spampavać PNG",
"exportToCSV": "Spampavać CSV",
"menu": "Meniu",
"selection": "Vyłučeńnie",
"selectionZoom": "Vyłučeńnie z maštabavańniem",
"zoomIn": "Nablizić",
"zoomOut": "Addalić",
"pan": "Ssoŭvańnie",
"reset": "Skinuć maštabavańnie"
}
}
}

View file

@ -0,0 +1,55 @@
{
"name": "ca",
"options": {
"months": [
"Gener",
"Febrer",
"Març",
"Abril",
"Maig",
"Juny",
"Juliol",
"Agost",
"Setembre",
"Octubre",
"Novembre",
"Desembre"
],
"shortMonths": [
"Gen.",
"Febr.",
"Març",
"Abr.",
"Maig",
"Juny",
"Jul.",
"Ag.",
"Set.",
"Oct.",
"Nov.",
"Des."
],
"days": [
"Diumenge",
"Dilluns",
"Dimarts",
"Dimecres",
"Dijous",
"Divendres",
"Dissabte"
],
"shortDays": ["Dg", "Dl", "Dt", "Dc", "Dj", "Dv", "Ds"],
"toolbar": {
"exportToSVG": "Descarregar SVG",
"exportToPNG": "Descarregar PNG",
"exportToCSV": "Descarregar CSV",
"menu": "Menú",
"selection": "Seleccionar",
"selectionZoom": "Seleccionar Zoom",
"zoomIn": "Augmentar",
"zoomOut": "Disminuir",
"pan": "Navegació",
"reset": "Reiniciar Zoom"
}
}
}

View file

@ -0,0 +1,55 @@
{
"name": "cs",
"options": {
"months": [
"Leden",
"Únor",
"Březen",
"Duben",
"Květen",
"Červen",
"Červenec",
"Srpen",
"Září",
"Říjen",
"Listopad",
"Prosinec"
],
"shortMonths": [
"Led",
"Úno",
"Bře",
"Dub",
"Kvě",
"Čvn",
"Čvc",
"Srp",
"Zář",
"Říj",
"Lis",
"Pro"
],
"days": [
"Neděle",
"Pondělí",
"Úterý",
"Středa",
"Čtvrtek",
"Pátek",
"Sobota"
],
"shortDays": ["Ne", "Po", "Út", "St", "Čt", "Pá", "So"],
"toolbar": {
"exportToSVG": "Stáhnout SVG",
"exportToPNG": "Stáhnout PNG",
"exportToCSV": "Stáhnout CSV",
"menu": "Menu",
"selection": "Vybrat",
"selectionZoom": "Zoom: Vybrat",
"zoomIn": "Zoom: Přiblížit",
"zoomOut": "Zoom: Oddálit",
"pan": "Přesouvat",
"reset": "Resetovat"
}
}
}

View file

@ -0,0 +1,55 @@
{
"name": "da",
"options": {
"months": [
"januar",
"februar",
"marts",
"april",
"maj",
"juni",
"juli",
"august",
"september",
"oktober",
"november",
"december"
],
"shortMonths": [
"jan",
"feb",
"mar",
"apr",
"maj",
"jun",
"jul",
"aug",
"sep",
"okt",
"nov",
"dec"
],
"days": [
"Søndag",
"Mandag",
"Tirsdag",
"Onsdag",
"Torsdag",
"Fredag",
"Lørdag"
],
"shortDays": ["Søn", "Man", "Tir", "Ons", "Tor", "Fre", "Lør"],
"toolbar": {
"exportToSVG": "Download SVG",
"exportToPNG": "Download PNG",
"exportToCSV": "Download CSV",
"menu": "Menu",
"selection": "Valg",
"selectionZoom": "Zoom til valg",
"zoomIn": "Zoom ind",
"zoomOut": "Zoom ud",
"pan": "Panorér",
"reset": "Nulstil zoom"
}
}
}

View file

@ -0,0 +1,55 @@
{
"name": "de",
"options": {
"months": [
"Januar",
"Februar",
"März",
"April",
"Mai",
"Juni",
"Juli",
"August",
"September",
"Oktober",
"November",
"Dezember"
],
"shortMonths": [
"Jan",
"Feb",
"Mär",
"Apr",
"Mai",
"Jun",
"Jul",
"Aug",
"Sep",
"Okt",
"Nov",
"Dez"
],
"days": [
"Sonntag",
"Montag",
"Dienstag",
"Mittwoch",
"Donnerstag",
"Freitag",
"Samstag"
],
"shortDays": ["So", "Mo", "Di", "Mi", "Do", "Fr", "Sa"],
"toolbar": {
"exportToSVG": "SVG speichern",
"exportToPNG": "PNG speichern",
"exportToCSV": "CSV speichern",
"menu": "Menü",
"selection": "Auswahl",
"selectionZoom": "Auswahl vergrößern",
"zoomIn": "Vergrößern",
"zoomOut": "Verkleinern",
"pan": "Verschieben",
"reset": "Zoom zurücksetzen"
}
}
}

View file

@ -0,0 +1,55 @@
{
"name": "el",
"options": {
"months": [
"Ιανουάριος",
"Φεβρουάριος",
"Μάρτιος",
"Απρίλιος",
"Μάιος",
"Ιούνιος",
"Ιούλιος",
"Αύγουστος",
"Σεπτέμβριος",
"Οκτώβριος",
"Νοέμβριος",
"Δεκέμβριος"
],
"shortMonths": [
"Ιαν",
"Φευ",
"Μαρ",
"Απρ",
"Μάι",
"Ιουν",
"Ιουλ",
"Αυγ",
"Σεπ",
"Οκτ",
"Νοε",
"Δεκ"
],
"days": [
"Κυριακή",
"Δευτέρα",
"Τρίτη",
"Τετάρτη",
"Πέμπτη",
"Παρασκευή",
"Σάββατο"
],
"shortDays": ["Κυρ", "Δευ", "Τρι", "Τετ", "Πεμ", "Παρ", "Σαβ"],
"toolbar": {
"exportToSVG": "Λήψη SVG",
"exportToPNG": "Λήψη PNG",
"exportToCSV": "Λήψη CSV",
"menu": "Menu",
"selection": "Επιλογή",
"selectionZoom": "Μεγένθυση βάση επιλογής",
"zoomIn": "Μεγένθυνση",
"zoomOut": "Σμίκρυνση",
"pan": "Μετατόπιση",
"reset": "Επαναφορά μεγένθυνσης"
}
}
}

View file

@ -0,0 +1,55 @@
{
"name": "en",
"options": {
"months": [
"January",
"February",
"March",
"April",
"May",
"June",
"July",
"August",
"September",
"October",
"November",
"December"
],
"shortMonths": [
"Jan",
"Feb",
"Mar",
"Apr",
"May",
"Jun",
"Jul",
"Aug",
"Sep",
"Oct",
"Nov",
"Dec"
],
"days": [
"Sunday",
"Monday",
"Tuesday",
"Wednesday",
"Thursday",
"Friday",
"Saturday"
],
"shortDays": ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"],
"toolbar": {
"exportToSVG": "Download SVG",
"exportToPNG": "Download PNG",
"exportToCSV": "Download CSV",
"menu": "Menu",
"selection": "Selection",
"selectionZoom": "Selection Zoom",
"zoomIn": "Zoom In",
"zoomOut": "Zoom Out",
"pan": "Panning",
"reset": "Reset Zoom"
}
}
}

View file

@ -0,0 +1,55 @@
{
"name": "es",
"options": {
"months": [
"Enero",
"Febrero",
"Marzo",
"Abril",
"Mayo",
"Junio",
"Julio",
"Agosto",
"Septiembre",
"Octubre",
"Noviembre",
"Diciembre"
],
"shortMonths": [
"Ene",
"Feb",
"Mar",
"Abr",
"May",
"Jun",
"Jul",
"Ago",
"Sep",
"Oct",
"Nov",
"Dic"
],
"days": [
"Domingo",
"Lunes",
"Martes",
"Miércoles",
"Jueves",
"Viernes",
"Sábado"
],
"shortDays": ["Dom", "Lun", "Mar", "Mie", "Jue", "Vie", "Sab"],
"toolbar": {
"exportToSVG": "Descargar SVG",
"exportToPNG": "Descargar PNG",
"exportToCSV": "Descargar CSV",
"menu": "Menu",
"selection": "Seleccionar",
"selectionZoom": "Seleccionar Zoom",
"zoomIn": "Aumentar",
"zoomOut": "Disminuir",
"pan": "Navegación",
"reset": "Reiniciar Zoom"
}
}
}

View file

@ -0,0 +1,55 @@
{
"name": "et",
"options": {
"months": [
"jaanuar",
"veebruar",
"märts",
"aprill",
"mai",
"juuni",
"juuli",
"august",
"september",
"oktoober",
"november",
"detsember"
],
"shortMonths": [
"jaan",
"veebr",
"märts",
"apr",
"mai",
"juuni",
"juuli",
"aug",
"sept",
"okt",
"nov",
"dets"
],
"days": [
"pühapäev",
"esmaspäev",
"teisipäev",
"kolmapäev",
"neljapäev",
"reede",
"laupäev"
],
"shortDays": ["P", "E", "T", "K", "N", "R", "L"],
"toolbar": {
"exportToSVG": "Lae alla SVG",
"exportToPNG": "Lae alla PNG",
"exportToCSV": "Lae alla CSV",
"menu": "Menüü",
"selection": "Valik",
"selectionZoom": "Valiku suum",
"zoomIn": "Suurenda",
"zoomOut": "Vähenda",
"pan": "Panoraamimine",
"reset": "Lähtesta suum"
}
}
}

Some files were not shown because too many files have changed in this diff Show more