commit 70325c3c0d3bbb65706f397a749f9102fcaaea48 Author: Андрей Сумин Date: Fri Feb 28 21:52:48 2025 +0300 init diff --git a/README.md b/README.md new file mode 100644 index 0000000..bedd1d8 --- /dev/null +++ b/README.md @@ -0,0 +1 @@ +# project_name diff --git a/compose.yaml b/compose.yaml new file mode 100644 index 0000000..716f387 --- /dev/null +++ b/compose.yaml @@ -0,0 +1,360 @@ +name: project_name + +services: + backend: + build: + context: ./services/backend + dockerfile: Dockerfile + depends_on: + backend-initdb: + restart: false + condition: service_completed_successfully + required: true + postgres: + restart: false + condition: service_healthy + required: true + redis: + restart: false + condition: service_healthy + required: true + minio: + restart: false + condition: service_healthy + required: true + env_file: + - path: ./infrastructure/backend/.env.template + required: true + - path: ./infrastructure/backend/.env + required: false + ports: + - name: web + target: 8080 + published: 8080 + host_ip: 127.0.0.1 + protocol: tcp + restart: unless-stopped + + backend-initdb: + build: + context: ./services/backend + dockerfile: Dockerfile + command: ./scripts/initdb + depends_on: + postgres: + restart: false + condition: service_healthy + required: true + redis: + restart: false + condition: service_healthy + required: true + minio: + restart: false + condition: service_healthy + required: true + env_file: + - path: ./infrastructure/backend/.env.template + required: true + - path: ./infrastructure/backend/.env + required: false + + backend-staticfiles: + build: + context: ./services/backend + dockerfile: Dockerfile.staticfiles + env_file: + - path: ./infrastructure/backend/.env.template + required: true + - path: ./infrastructure/backend/.env + required: false + healthcheck: + test: ["CMD", "service", "nginx", "status", "||", " exit 1"] + interval: 1m30s + timeout: 5s + start_period: 5s + start_interval: 2s + retries: 5 + ports: + - name: web + target: 80 + published: 13241 + host_ip: 127.0.0.1 + protocol: tcp + restart: unless-stopped + + backend-celery-worker: + build: + context: ./services/backend + dockerfile: Dockerfile + command: celery -A config worker -l INFO + depends_on: + redis: + restart: false + condition: service_healthy + required: true + env_file: + - path: ./infrastructure/backend/.env.template + required: true + - path: ./infrastructure/backend/.env + required: false + healthcheck: + test: ["CMD", "celery", "-A", "config", "inspect", "ping"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 10s + start_interval: 2s + restart: unless-stopped + + celery-exporter: + image: docker.io/danihodovic/celery-exporter:0.11.1 + depends_on: + redis: + restart: false + condition: service_healthy + required: true + env_file: + - path: ./infrastructure/celery-exporter/.env.template + required: true + - path: ./infrastructure/celery-exporter/.env + required: false + restart: unless-stopped + + redis: + image: docker.io/redis:7-alpine3.21 + command: redis-server /usr/local/etc/redis/redis.conf + configs: + - source: redis_config + target: /usr/local/etc/redis/redis.conf + env_file: + - path: ./infrastructure/redis/.env.template + required: true + - path: ./infrastructure/redis/.env + required: false + healthcheck: + test: ["CMD", "redis-cli", "ping"] + interval: 1m30s + timeout: 5s + start_period: 5s + start_interval: 2s + retries: 5 + restart: unless-stopped + shm_size: 4mb + volumes: + - type: volume + source: redis_data + target: /data + + redis-exporter: + image: docker.io/oliver006/redis_exporter:v1.67.0-alpine + depends_on: + redis: + restart: false + condition: service_healthy + required: true + env_file: + - path: ./infrastructure/redis-exporter/.env.template + required: true + - path: ./infrastructure/redis-exporter/.env + required: false + restart: unless-stopped + shm_size: 4mb + + postgres: + image: docker.io/postgres:17-alpine3.21 + configs: + - source: postgres_config + target: /etc/postgresql/postgresql.conf + env_file: + - path: ./infrastructure/postgres/.env.template + required: true + - path: ./infrastructure/postgres/.env + required: false + healthcheck: + test: ["CMD", "pg_isready"] + interval: 1m30s + timeout: 5s + start_period: 5s + start_interval: 2s + retries: 5 + oom_kill_disable: true + restart: unless-stopped + secrets: + - source: postgres_password + target: /run/secrets/postgres_password + shm_size: 128mb + volumes: + - type: volume + source: postgres_data + target: /var/lib/postgresql/data + + postgres-exporter: + image: quay.io/prometheuscommunity/postgres-exporter:v0.16.0 + depends_on: + postgres: + restart: false + condition: service_healthy + required: true + env_file: + - path: ./infrastructure/postgres-exporter/.env.template + required: true + - path: ./infrastructure/postgres-exporter/.env + required: false + restart: unless-stopped + shm_size: 4mb + + pgadmin: + image: docker.io/dpage/pgadmin4:9 + configs: + - source: pgadmin_servers + target: /pgadmin4/servers.json + depends_on: + postgres: + restart: false + condition: service_healthy + required: true + env_file: + - path: ./infrastructure/pgadmin/.env.template + required: true + - path: ./infrastructure/pgadmin/.env + required: false + healthcheck: + test: ["CMD", "wget", "-O", "-", "http://localhost:80/misc/ping"] + interval: 1m30s + timeout: 5s + start_period: 5s + start_interval: 2s + retries: 5 + ports: + - name: web + target: 80 + published: 13242 + host_ip: 127.0.0.1 + protocol: tcp + restart: unless-stopped + secrets: + - source: pgadmin_password + target: /run/secrets/pgadmin_password + shm_size: 4mb + volumes: + - type: volume + source: pgadmin_data + target: /var/lib/pgadmin + + grafana: + image: docker.io/grafana/grafana-oss:11.5.0 + configs: + - source: grafana_config + target: /usr/share/grafana/conf/defaults.ini + entrypoint: ["/etc/grafana/scripts/entrypoint.sh"] + healthcheck: + test: ["CMD", "wget", "-O", "-", "http://localhost:3000/api/health"] + interval: 1m30s + timeout: 5s + start_period: 5s + start_interval: 2s + retries: 5 + ports: + - name: web + target: 3000 + published: 13243 + host_ip: 127.0.0.1 + protocol: tcp + restart: unless-stopped + shm_size: 4mb + volumes: + - type: volume + source: grafana_data + target: /var/lib/grafana + - type: bind + source: ./infrastructure/grafana/provisioning + target: /etc/grafana/provisioning + - type: bind + source: ./infrastructure/grafana/scripts + target: /etc/grafana/scripts + + minio: + command: server --console-address ":9001" + image: docker.io/minio/minio:RELEASE.2025-02-03T21-03-04Z + healthcheck: + test: ["CMD", "mc", "ready", "local"] + interval: 1m30s + timeout: 5s + start_period: 5s + start_interval: 2s + retries: 5 + env_file: + - path: ./infrastructure/minio/.env.template + required: true + - path: ./infrastructure/minio/.env + required: false + ports: + - name: api + target: 9000 + published: 13244 + host_ip: 127.0.0.1 + protocol: tcp + - name: console + target: 9001 + published: 13245 + host_ip: 127.0.0.1 + protocol: tcp + restart: unless-stopped + volumes: + - type: volume + source: minio_data + target: /data + + prometheus: + image: docker.io/prom/prometheus:v3.1.0 + command: + - "--config.file=/etc/prometheus/prometheus.yaml" + configs: + - source: prometheus_config + target: /etc/prometheus/prometheus.yaml + healthcheck: + test: ["CMD", "wget", "-O", "-", "http://localhost:9090/-/healthy"] + interval: 1m30s + timeout: 5s + start_period: 5s + start_interval: 2s + retries: 5 + ports: + - name: web + target: 9090 + published: 13246 + host_ip: 127.0.0.1 + protocol: tcp + restart: unless-stopped + shm_size: 4mb + volumes: + - type: volume + source: prometheus_data + target: /prometheus + +volumes: + redis_data: + postgres_data: + pgadmin_data: + grafana_data: + prometheus_data: + minio_data: + +configs: + redis_config: + file: ./infrastructure/redis/redis.conf + postgres_config: + file: ./infrastructure/postgres/postgresql.conf + pgadmin_servers: + file: ./infrastructure/pgadmin/servers.json + grafana_config: + file: ./infrastructure/grafana/grafana.ini + prometheus_config: + file: ./infrastructure/prometheus/prometheus.yaml + +secrets: + postgres_password: + file: ./infrastructure/postgres/password + pgadmin_password: + file: ./infrastructure/pgadmin/password diff --git a/services/backend/.dockerignore b/services/backend/.dockerignore new file mode 100644 index 0000000..6886653 --- /dev/null +++ b/services/backend/.dockerignore @@ -0,0 +1,185 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +.python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +Pipfile.lock + +# UV +# Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +uv.lock + +# poetry +# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control +poetry.lock + +# pdm +# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. +#pdm.lock +# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it +# in version control. +# https://pdm.fming.dev/latest/usage/project/#working-with-version-control +.pdm.toml +.pdm-python +.pdm-build/ + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# PyCharm +# JetBrains specific template is maintained in a separate JetBrains.gitignore that can +# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore +# and can be added to the global gitignore or merged into this file. For a more nuclear +# option (not recommended) you can uncomment the following to ignore the entire idea folder. +.idea/ + +# PyPI configuration file +.pypirc + +# Ruff files +.ruff_cache + +# Docker files +Dockerfile +Dockerfile.staticfiles +.dockerignore + +# Git files +.git +.gitignore + +# Template env file +.env.template + +# Collected static files +static diff --git a/services/backend/.env.template b/services/backend/.env.template new file mode 100644 index 0000000..75997b2 --- /dev/null +++ b/services/backend/.env.template @@ -0,0 +1,31 @@ +# Change all vars before going to production and remove all comments (!) +# Below all environment variables and default values + +DJANGO_SECRET_KEY=very_insecure_key +DJANGO_DEBUG=False +DJANGO_ALLOWED_HOSTS=localhost,127.0.0.1 +DJANGO_CSRF_TRUSTED_ORIGINS=http://localhost,http://127.0.0.1 +DJANGO_CORS_ALLOWED_ORIGINS=* +DJANGO_INTERNAL_IPS=127.0.0.1 +DJANGO_LANGUAGE_CODE=en-us +DJANGO_STATIC_URL=static/ +REDIS_URI=redis://localhost:6379 +DJANGO_DB_URI=sqlite:///db.sqlite3 + + +# Storages + +MINIO_ENDPOINT= +MINIO_CUSTOM_ENDPOINT_URL= +MINIO_ACCESS_KEY= +MINIO_SECRET_KEY= +MINIO_USE_HTTPS=False +MINIO_MEDIA_BUCKET_NAME=projectname-media + + +# Applyable if you installing using docker compose + +DJANGO_CREATE_SUPERUSER=False +DJANGO_SUPERUSER_USERNAME= +DJANGO_SUPERUSER_EMAIL= +DJANGO_SUPERUSER_PASSWORD= diff --git a/services/backend/.gitignore b/services/backend/.gitignore new file mode 100644 index 0000000..657dc4c --- /dev/null +++ b/services/backend/.gitignore @@ -0,0 +1,173 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +.python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +Pipfile.lock + +# UV +# Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +uv.lock + +# poetry +# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control +poetry.lock + +# pdm +# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. +#pdm.lock +# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it +# in version control. +# https://pdm.fming.dev/latest/usage/project/#working-with-version-control +.pdm.toml +.pdm-python +.pdm-build/ + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# PyCharm +# JetBrains specific template is maintained in a separate JetBrains.gitignore that can +# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore +# and can be added to the global gitignore or merged into this file. For a more nuclear +# option (not recommended) you can uncomment the following to ignore the entire idea folder. +.idea/ + +# PyPI configuration file +.pypirc + +# Ruff files +.ruff_cache + +# Collected static files +static diff --git a/services/backend/Dockerfile b/services/backend/Dockerfile new file mode 100644 index 0000000..b55f660 --- /dev/null +++ b/services/backend/Dockerfile @@ -0,0 +1,42 @@ +# Stage 1: Install dependencies +FROM docker.io/python:3.11-alpine3.20 AS builder + +COPY --from=ghcr.io/astral-sh/uv:0.4.30 /uv /uvx /bin/ + +WORKDIR /app + +ENV PYTHONDONTWRITEBYTECODE=1 \ + PYTHONUNBUFFERED=1 \ + PYTHONOPTIMIZE=2 \ + UV_COMPILE_BYTECODE=1 \ + UV_PROJECT_ENVIRONMENT=/opt/venv + +COPY pyproject.toml . + +RUN uv sync --no-dev --no-install-project --no-cache + + +# Stage 2: Start the application +FROM docker.io/python:3.11-alpine3.20 + +WORKDIR /app + +COPY --from=builder /opt/venv /opt/venv + +COPY . . + +RUN adduser -D -g '' app && chown -R app:app ./ + +USER app + +ENV PYTHONDONTWRITEBYTECODE=1 \ + PYTHONUNBUFFERED=1 \ + PYTHONOPTIMIZE=2 \ + PATH="/opt/venv/bin:$PATH" + +EXPOSE 8080 + +HEALTHCHECK --interval=30s --timeout=5s --start-period=5s --start-interval=2s --retries=3 \ + CMD wget --no-verbose --tries=1 --spider http://127.0.0.1:8080/health?format=json || exit 1 + +CMD gunicorn config.wsgi --workers=8 -b 0.0.0.0:8080 --access-logfile - --error-logfile - diff --git a/services/backend/Dockerfile.staticfiles b/services/backend/Dockerfile.staticfiles new file mode 100644 index 0000000..5150bf5 --- /dev/null +++ b/services/backend/Dockerfile.staticfiles @@ -0,0 +1,27 @@ +# Stage 1: Install dependencies and compile staticfiles +FROM docker.io/python:3.11-alpine3.20 AS builder + +COPY --from=ghcr.io/astral-sh/uv:0.4.30 /uv /uvx /bin/ + +WORKDIR /app + +ENV PYTHONDONTWRITEBYTECODE=1 \ + PYTHONUNBUFFERED=1 \ + PYTHONOPTIMIZE=2 \ + UV_COMPILE_BYTECODE=1 \ + UV_PROJECT_ENVIRONMENT=/opt/venv + +COPY pyproject.toml . + +RUN uv sync --no-dev --no-install-project --no-cache + +COPY . . + +RUN uv run python manage.py collectstatic --noinput + +# Stage 2: Start nginx and serve staticfiles +FROM docker.io/nginx:latest + +COPY --from=builder /app/static /usr/share/nginx/html + +CMD ["nginx", "-g", "daemon off;"] diff --git a/services/backend/README.md b/services/backend/README.md new file mode 100644 index 0000000..7a06d20 --- /dev/null +++ b/services/backend/README.md @@ -0,0 +1,147 @@ +# project_name Backend + +## Prerequisites + +Ensure you have the following installed on your system: + +- [Python](https://www.python.org/) (>=3.10,<3.12) +- [uv](https://docs.astral.sh/uv/) +- [Docker](https://www.docker.com/) (for containerized setup) + +## Basic setup + +### Installation + +#### Clone the project + +```bash +git clone project_name +``` + +#### Go to the project directory + +```bash +cd project_name/services/backend +``` + +#### Customize environment + +```bash +cp .env.template .env +``` + +And setup env vars according to your needs. + +#### Install dependencies + +##### For dev environment + +```bash +uv sync --all-extras +``` + +##### For prod environment + +```bash +uv sync --no-dev +``` + +#### Running + +##### Apply migrations + +```bash +uv run python manage.py migrate +``` + +##### Start celery worker + +```bash +celery -A config worker -l INFO +``` + +##### Start server + +In dev mode: + +```bash +uv run python manage.py runserver +``` + +In prod mode: + +```bash +uv run gunicorn config.wsgi +``` + +## Containerized setup + +### Clone the project + +```bash +git clone project_name +``` + +### Go to the project directory + +```bash +cd project_name/services/backend +``` + +### Build docker image + +```bash +docker build -t project_name-backend . +``` + +### Customize environment + +Customize environment with `docker run` command (or bind .env file to container), for all environment vars and default values see [.env.template](./.env.template). + +### Run docker image + +#### Backend + +```bash +docker run -p 8080:8080 --name project_name-backend project_name-backend +``` + +#### Celery worker + +```bash +docker run --name project_name-celery-worker project_name-backend celery -A config worker -l INFO +``` + +Backend will be available on [127.0.0.1:8080](http://127.0.0.1:8080). + +## Testing + +### Clone the project + +```bash +git clone project_name +``` + +### Go to the project directory + +```bash +cd project_name/services/backend +``` + +### Install dependencies + +```bash +uv sync --all-extras +``` + +### Run tests + +```bash +uv run coverage run --source="." manage.py test +``` + +### Check coverage + +```bash +uv run coverage report +``` diff --git a/services/backend/api/__init__.py b/services/backend/api/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/services/backend/api/urls.py b/services/backend/api/urls.py new file mode 100644 index 0000000..ffeb361 --- /dev/null +++ b/services/backend/api/urls.py @@ -0,0 +1,10 @@ +from django.urls import path +from health_check.views import MainView + +from api.v1.router import router as api_v1_router + +urlpatterns = [ + path("", api_v1_router.urls), + # Health endpoint + path("health", MainView.as_view(), name="health_check_home"), +] diff --git a/services/backend/api/v1/__init__.py b/services/backend/api/v1/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/services/backend/api/v1/handlers.py b/services/backend/api/v1/handlers.py new file mode 100644 index 0000000..d09fe0a --- /dev/null +++ b/services/backend/api/v1/handlers.py @@ -0,0 +1,121 @@ +import logging +from collections.abc import Callable +from http import HTTPStatus as status +from typing import Any + +import django.core.exceptions +import django.http +import ninja.errors +from django.http import HttpRequest, HttpResponse +from ninja import NinjaAPI + +from config.errors import ConflictError, ForbiddenError + +logger = logging.getLogger("django") + + +def handle_validation_error( + request: HttpRequest, + exc: ninja.errors.ValidationError, + router: NinjaAPI, +) -> HttpResponse: + return router.create_response( + request, + {"detail": exc.errors}, + status=status.BAD_REQUEST, + ) + + +def handle_django_validation_error( + request: HttpRequest, + exc: django.core.exceptions.ValidationError, + router: NinjaAPI, +) -> HttpResponse: + detail = list(exc) + + if hasattr(exc, "error_dict"): + detail = dict(exc) + + return router.create_response( + request, + {"detail": detail}, + status=status.BAD_REQUEST, + ) + + +def handle_authentication_error( + request: HttpRequest, + exc: ninja.errors.AuthenticationError, + router: NinjaAPI, +) -> HttpResponse: + return router.create_response( + request, + {"detail": status.UNAUTHORIZED.phrase}, + status=status.UNAUTHORIZED, + ) + + +def handle_forbidden_error( + request: HttpRequest, + exc: ForbiddenError, + router: NinjaAPI, +) -> HttpResponse: + return router.create_response( + request, + {"detail": exc.message}, + status=status.FORBIDDEN, + ) + + +def handle_not_found_error( + request: HttpRequest, + exc: Exception, + router: NinjaAPI, +) -> HttpResponse: + return router.create_response( + request, + {"detail": status.NOT_FOUND.phrase}, + status=status.NOT_FOUND, + ) + + +def handle_conflict_error( + request: HttpRequest, + exc: ConflictError, + router: NinjaAPI, +) -> HttpResponse: + detail = list(exc.validation_error) + + if hasattr(exc, "error_dict"): + detail = dict(exc.validation_error) + + return router.create_response( + request, + {"detail": detail}, + status=status.CONFLICT, + ) + + +def handle_unknown_exception( + request: HttpRequest, + exc: Exception, + router: NinjaAPI, +) -> HttpResponse: + logger.exception(exc) + + return router.create_response( + request, + {"detail": status.INTERNAL_SERVER_ERROR.phrase}, + status=status.INTERNAL_SERVER_ERROR, + ) + + +exception_handlers: list[tuple[Any, Callable]] = [ + (ninja.errors.ValidationError, handle_validation_error), + (django.core.exceptions.ValidationError, handle_django_validation_error), + (ninja.errors.AuthenticationError, handle_authentication_error), + (ForbiddenError, handle_forbidden_error), + (django.http.Http404, handle_not_found_error), + (ConflictError, handle_conflict_error), + (Exception, handle_unknown_exception), +] diff --git a/services/backend/api/v1/ping/__init__.py b/services/backend/api/v1/ping/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/services/backend/api/v1/ping/schemas.py b/services/backend/api/v1/ping/schemas.py new file mode 100644 index 0000000..07b7eb8 --- /dev/null +++ b/services/backend/api/v1/ping/schemas.py @@ -0,0 +1,5 @@ +from ninja import Schema + + +class PingOut(Schema): + status: str = "ok" diff --git a/services/backend/api/v1/ping/views.py b/services/backend/api/v1/ping/views.py new file mode 100644 index 0000000..a6d78d7 --- /dev/null +++ b/services/backend/api/v1/ping/views.py @@ -0,0 +1,18 @@ +from http import HTTPStatus as status + +from django.http import HttpRequest +from ninja import Router + +from api.v1.ping import schemas + +router = Router(tags=["ping"]) + + +@router.get( + "", + response={ + status.OK: schemas.PingOut, + }, +) +def ping(request: HttpRequest) -> tuple[status, schemas.PingOut]: + return status.OK, schemas.PingOut diff --git a/services/backend/api/v1/router.py b/services/backend/api/v1/router.py new file mode 100644 index 0000000..b494b4b --- /dev/null +++ b/services/backend/api/v1/router.py @@ -0,0 +1,23 @@ +from functools import partial + +from ninja import NinjaAPI + +from api.v1 import handlers +from api.v1.ping.views import router as ping_router + +router = NinjaAPI( + title="DataRush API", + version="1", + description="API docs for DataRush", + openapi_url="/docs/openapi.json", +) + + +router.add_router( + "ping", + ping_router, +) + + +for exception, handler in handlers.exception_handlers: + router.add_exception_handler(exception, partial(handler, router=router)) diff --git a/services/backend/api/v1/schemas.py b/services/backend/api/v1/schemas.py new file mode 100644 index 0000000..c58b291 --- /dev/null +++ b/services/backend/api/v1/schemas.py @@ -0,0 +1,24 @@ +from http import HTTPStatus as status +from typing import Any + +from ninja import Schema + + +class BadRequestError(Schema): + detail: Any + + +class UnauthorizedError(Schema): + detail: str = status.UNAUTHORIZED.phrase + + +class ForbiddenError(Schema): + detail: str = status.FORBIDDEN.phrase + + +class NotFoundError(Schema): + detail: str = status.NOT_FOUND.phrase + + +class ConflictError(Schema): + detail: Any diff --git a/services/backend/apps/__init__.py b/services/backend/apps/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/services/backend/apps/core/__init__.py b/services/backend/apps/core/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/services/backend/apps/core/apps.py b/services/backend/apps/core/apps.py new file mode 100644 index 0000000..a37dd7d --- /dev/null +++ b/services/backend/apps/core/apps.py @@ -0,0 +1,9 @@ +import contextlib + +from django.apps import AppConfig +from django.core.cache import cache + + +class CoreConfig(AppConfig): + name = "apps.core" + label = "core" diff --git a/services/backend/apps/core/models.py b/services/backend/apps/core/models.py new file mode 100644 index 0000000..27daed8 --- /dev/null +++ b/services/backend/apps/core/models.py @@ -0,0 +1,48 @@ +import uuid +from typing import Any + +from django.core.exceptions import ValidationError +from django.db import models + +from config.errors import ConflictError + + +class BaseModel(models.Model): + id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) + + class Meta: + abstract = True + + def save(self, *args: Any, **kwargs: Any) -> None: + self.validate() + + super().save(*args, **kwargs) + + def validate( + self, + validate_unique: bool = True, + validate_constraints: bool = True, + include: list[models.Field] | None = None, + ) -> None: + self.full_clean( + validate_unique=False, + validate_constraints=False, + exclude=( + field.name + for field in set(self._meta.get_fields()) - set(include) + ) + if include + else None, + ) + + if validate_unique: + try: + self.validate_unique() + except ValidationError as e: + raise ConflictError(e) from None + + if validate_constraints: + try: + self.validate_constraints() + except ValidationError as e: + raise ConflictError(e) from None diff --git a/services/backend/config/__init__.py b/services/backend/config/__init__.py new file mode 100644 index 0000000..3eb91a6 --- /dev/null +++ b/services/backend/config/__init__.py @@ -0,0 +1,3 @@ +from config.celery import app as celery_app + +__all__ = ("celery_app",) diff --git a/services/backend/config/asgi.py b/services/backend/config/asgi.py new file mode 100644 index 0000000..513bb2b --- /dev/null +++ b/services/backend/config/asgi.py @@ -0,0 +1,9 @@ +"""ASGI config for project_name.""" + +import os + +from django.core.asgi import get_asgi_application + +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings") + +application = get_asgi_application() diff --git a/services/backend/config/celery.py b/services/backend/config/celery.py new file mode 100644 index 0000000..ecad09a --- /dev/null +++ b/services/backend/config/celery.py @@ -0,0 +1,10 @@ +import os + +from celery import Celery + +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings") + +app = Celery("project_name") + +app.config_from_object("django.conf:settings", namespace="CELERY") +app.autodiscover_tasks() diff --git a/services/backend/config/errors.py b/services/backend/config/errors.py new file mode 100644 index 0000000..9729b26 --- /dev/null +++ b/services/backend/config/errors.py @@ -0,0 +1,13 @@ +from http import HTTPStatus as status + +from django.core.exceptions import ValidationError + + +class ConflictError(Exception): + def __init__(self, validation_error: ValidationError) -> None: + self.validation_error = validation_error + + +class ForbiddenError(Exception): + def __init__(self, message: str = status.FORBIDDEN.phrase) -> None: + self.message = message diff --git a/services/backend/config/handlers.py b/services/backend/config/handlers.py new file mode 100644 index 0000000..7adeead --- /dev/null +++ b/services/backend/config/handlers.py @@ -0,0 +1,43 @@ +from http import HTTPStatus as status + +from django.http import HttpRequest, JsonResponse + + +def handler400( + request: HttpRequest, + exception: Exception | None = None, +) -> JsonResponse: + return JsonResponse( + status=status.BAD_REQUEST, + data={"detail": status.BAD_REQUEST.phrase}, + ) + + +def handler403( + request: HttpRequest, + exception: Exception | None = None, +) -> JsonResponse: + return JsonResponse( + status=status.FORBIDDEN, + data={"detail": status.FORBIDDEN.phrase}, + ) + + +def handler404( + request: HttpRequest, + exception: Exception | None = None, +) -> JsonResponse: + return JsonResponse( + status=status.NOT_FOUND, + data={"detail": status.NOT_FOUND.phrase}, + ) + + +def handler500( + request: HttpRequest, + exception: Exception | None = None, +) -> JsonResponse: + return JsonResponse( + status=status.INTERNAL_SERVER_ERROR, + data={"detail": status.INTERNAL_SERVER_ERROR.phrase}, + ) diff --git a/services/backend/config/settings.py b/services/backend/config/settings.py new file mode 100644 index 0000000..6e2a3b1 --- /dev/null +++ b/services/backend/config/settings.py @@ -0,0 +1,587 @@ +"""Django settings for project_name.""" + +import contextlib +import logging +from collections.abc import Callable +from pathlib import Path + +import django_stubs_ext +import environ +from django.utils.translation import gettext_lazy as _ + +BASE_DIR = Path(__file__).resolve().parent.parent + +env = environ.Env() +environ.Env.read_env(BASE_DIR / ".env") + +django_stubs_ext.monkeypatch() + + +# Common settings + +DEBUG = env("DJANGO_DEBUG", default=False) + +ALLOWED_HOSTS = env( + "DJANGO_ALLOWED_HOSTS", + list, + default=["localhost", "127.0.0.1"], +) + + +# Integrations + +YANDEX_CLOUD_FOLDER_ID = env("YANDEX_CLOUD_FOLDER_ID", default=None) + +YANDEX_CLOUD_API_KEY = env("YANDEX_CLOUD_API_KEY", default=None) + +YANDEX_CLOUD_INTEGRATION_ENABLED = ( + YANDEX_CLOUD_FOLDER_ID and YANDEX_CLOUD_API_KEY +) + + +# Register healthchecks + +# plugin_dir.register(SomeHealthCheckClass) + + +# Caching + +REDIS_URI = env("REDIS_URI", default="redis://localhost:6379") + +CACHES = { + "default": { + "BACKEND": "django.core.cache.backends.redis.RedisCache", + "LOCATION": REDIS_URI, + "TIMEOUT": None, + "KEY_PREFIX": "backend", + "VERSION": 1, + }, +} + + +# Celery + +CELERY_BROKER_URL = REDIS_URI + +CELERY_RESULT_BACKEND = REDIS_URI + +CELERY_TIMEZONE = "UTC" + +CELERY_WORKER_SEND_TASK_EVENTS = True + +CELERY_TASK_SEND_SENT_EVENT = True + +CELERY_TASK_TRACK_STARTED = True + + +# Database + +DB_URI = env.db_url("DJANGO_DB_URI", default="sqlite:///db.sqlite3") + +DATABASES = {"default": {**DB_URI, "CONN_MAX_AGE": 50}} + +DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField" + + +# Password validation + +AUTH_PASSWORD_VALIDATORS = [ + { + "NAME": ( + "django.contrib.auth." + "password_validation.UserAttributeSimilarityValidator" + ), + }, + { + "NAME": ( + "django.contrib.auth.password_validation.MinimumLengthValidator" + ), + }, + { + "NAME": ( + "django.contrib.auth.password_validation.CommonPasswordValidator" + ), + }, + { + "NAME": ( + "django.contrib.auth.password_validation.NumericPasswordValidator" + ), + }, +] + + +# Static files (CSS, JavaScript, Images) + +STATIC_ROOT = BASE_DIR / "static" + +STATIC_URL = env("DJANGO_STATIC_URL", default="static/") + +STATICFILES_DIRS: list[str] = [] + +STATICFILES_STORAGE = "django.contrib.staticfiles.storage.StaticFilesStorage" + +STATICFILES_FINDERS = [ + "django.contrib.staticfiles.finders.FileSystemFinder", + "django.contrib.staticfiles.finders.AppDirectoriesFinder", +] + + +# Files + +FILE_UPLOAD_MAX_MEMORY_SIZE = 2621440 + +# Minio + +MINIO_STORAGE_ENDPOINT = env("MINIO_ENDPOINT", default=None) + +MINIO_STORAGE_ACCESS_KEY = env("MINIO_ACCESS_KEY", default=None) + +MINIO_STORAGE_SECRET_KEY = env("MINIO_SECRET_KEY", default=None) + +MINIO_STORAGE_USE_HTTPS = env("MINIO_USE_HTTPS", default=False) + +MINIO_STORAGE_MEDIA_BUCKET_NAME = env( + "MINIO_MEDIA_BUCKET_NAME", default="projectname-media" +) + +MINIO_STORAGE_AUTO_CREATE_MEDIA_BUCKET = True + +MINIO_STORAGE_AUTO_CREATE_MEDIA_POLICY = "GET_ONLY" + +MINIO_DEFAULT_CUSTOM_ENDPOINT_URL = ( + "https://" + if MINIO_STORAGE_USE_HTTPS + else "http://" + str(MINIO_STORAGE_ENDPOINT) +) + +MINIO_STORAGE_MEDIA_URL = ( + env("MINIO_CUSTOM_ENDPOINT_URL", default=MINIO_DEFAULT_CUSTOM_ENDPOINT_URL) + + "/" + f"{MINIO_STORAGE_MEDIA_BUCKET_NAME}" +) + +MINIO_STORAGE_DEFAULT_ACL = "public-read" + +STORAGES = { + "default": { + "BACKEND": "minio_storage.storage.MinioMediaStorage", + }, + "staticfiles": { + "BACKEND": "django.contrib.staticfiles.storage.StaticFilesStorage", + }, +} + + +# Cors + +CORS_ALLOWED_ORIGINS_FROM_ENV = env("DJANGO_CORS_ALLOWED_ORIGINS", list, ["*"]) + +if CORS_ALLOWED_ORIGINS_FROM_ENV == ["*"]: + CORS_ALLOW_ALL_ORIGINS = True +else: + CORS_ALLOWED_ORIGINS = CORS_ALLOWED_ORIGINS_FROM_ENV + + +# Forms + +FORM_RENDERER = "django.forms.renderers.DjangoTemplates" + +FORMS_URLFIELD_ASSUME_HTTPS = False + + +# Internationalization + +DATE_FORMAT = "N j, Y" + +DATE_INPUT_FORMATS = [ + "%Y-%m-%d", # '2006-10-25' + "%m/%d/%Y", # '10/25/2006' + "%m/%d/%y", # '10/25/06' + "%b %d %Y", # 'Oct 25 2006' + "%b %d, %Y", # 'Oct 25, 2006' + "%d %b %Y", # '25 Oct 2006' + "%d %b, %Y", # '25 Oct, 2006' + "%B %d %Y", # 'October 25 2006' + "%B %d, %Y", # 'October 25, 2006' + "%d %B %Y", # '25 October 2006' + "%d %B, %Y", # '25 October, 2006' +] + +DATETIME_FORMAT = "N j, Y, H:i:s" + +DATETIME_INPUT_FORMATS = [ + "%Y-%m-%d %H:%M:%S", # '2006-10-25 14:30:59' + "%Y-%m-%d %H:%M:%S.%f", # '2006-10-25 14:30:59.000200' + "%Y-%m-%d %H:%M", # '2006-10-25 14:30' + "%m/%d/%Y %H:%M:%S", # '10/25/2006 14:30:59' + "%m/%d/%Y %H:%M:%S.%f", # '10/25/2006 14:30:59.000200' + "%m/%d/%Y %H:%M", # '10/25/2006 14:30' + "%m/%d/%y %H:%M:%S", # '10/25/06 14:30:59' + "%m/%d/%y %H:%M:%S.%f", # '10/25/06 14:30:59.000200' + "%m/%d/%y %H:%M", # '10/25/06 14:30' +] + +DECIMAL_SEPARATOR = "." + +FIRST_DAY_OF_WEEK = 1 + +FORMAT_MODULE_PATH = None + +LANGUAGE_CODE = env("DJANGO_LANGUAGE_CODE", default="en-us") + +LANGUAGES = [("en", _("English")), ("ru", _("Russian"))] + +LOCALE_PATHS: list[str] = [] + +MONTH_DAY_FORMAT = "F j" + +NUMBER_GROUPING = 0 + +SHORT_DATE_FORMAT = "m/d/Y" + +SHORT_DATETIME_FORMAT = "m/d/Y H:i:s" + +THOUSAND_SEPARATOR = "," + +TIME_FORMAT = "H:i:s" + +TIME_INPUT_FORMATS = [ + "%H:%M:%S", # '14:30:59' + "%H:%M:%S.%f", # '14:30:59.000200' + "%H:%M", # '14:30' +] + +TIME_ZONE = "UTC" + +USE_I18N = True + +USE_THOUSAND_SEPARATOR = True + +USE_TZ = True + +YEAR_MONTH_FORMAT = "F Y" + + +# HTTP + +DATA_UPLOAD_MAX_MEMORY_SIZE = None + +DATA_UPLOAD_MAX_NUMBER_FIELDS = None + +DATA_UPLOAD_MAX_NUMBER_FILES = None + +DEFAULT_CHARSET = "utf-8" + +FORCE_SCRIPT_NAME = None + +INTERNAL_IPS = env( + "DJANGO_INTERNAL_IPS", + list, + default=["127.0.0.1"], +) + +MIDDLEWARE = [ + "django_guid.middleware.guid_middleware", + "corsheaders.middleware.CorsMiddleware", + "django.contrib.sessions.middleware.SessionMiddleware", + "django.middleware.csrf.CsrfViewMiddleware", + "django.contrib.auth.middleware.AuthenticationMiddleware", + "django.contrib.messages.middleware.MessageMiddleware", +] + +SIGNING_BACKEND = "django.core.signing.TimestampSigner" + +USE_X_FORWARDED_HOST = False + +USE_X_FORWARDED_PORT = False + +WSGI_APPLICATION = "config.wsgi.application" + + +# Logging + +LOGGER_NAME = "project_name" + +LOGGER = logging.getLogger(LOGGER_NAME) + +LOGGING_FILTERS = { + "require_debug_true": { + "()": "django.utils.log.RequireDebugTrue", + }, + "require_debug_false": { + "()": "django.utils.log.RequireDebugFalse", + }, + "correlation_id": { + "()": "django_guid.log_filters.CorrelationId", + }, +} + +LOGGING_FORMATTERS = { + "json": { + "()": "pythonjsonlogger.jsonlogger.JsonFormatter", + "format": ( + "{levelname}{correlation_id}{asctime}" + "{name}{pathname}{lineno}{message}" + ), + "style": "{", + }, + "text": { + "()": "colorlog.ColoredFormatter", + "format": ( + "{log_color}[{levelname}]{reset} " + "{light_black}{asctime} {name} | {pathname}:{lineno}{reset}\n" + "{bold_black}{message}{reset}" + ), + "log_colors": { + "DEBUG": "bold_green", + "INFO": "bold_cyan", + "WARNING": "bold_yellow", + "ERROR": "bold_red", + "CRITICAL": "bold_purple", + }, + "style": "{", + }, +} + +LOGGING_HANDLERS = { + "console_debug": { + "class": "logging.StreamHandler", + "level": "DEBUG", + "filters": ["require_debug_true"], + "formatter": "text", + }, + "console_prod": { + "class": "logging.StreamHandler", + "level": "INFO", + "filters": ["require_debug_false", "correlation_id"], + "formatter": "json", + }, +} + +LOGGING_LOGGERS = { + "django": { + "handlers": ["console_debug", "console_prod"], + "level": "INFO" if DEBUG else "ERROR", + "propagate": False, + }, + "django.request": { + "handlers": ["console_debug", "console_prod"], + "level": "INFO" if DEBUG else "ERROR", + "propagate": False, + }, + "django.server": { + "handlers": ["console_debug"], + "level": "INFO", + "filters": ["require_debug_true"], + "propagate": False, + }, + "django.template": {"handlers": []}, + "django.db.backends.schema": {"handlers": []}, + "django.security": {"handlers": [], "propagate": True}, + "django.db.backends": { + "handlers": ["console_debug"], + "filters": ["require_debug_true"], + "level": "DEBUG", + "propagate": False, + }, + "health-check": { + "handlers": ["console_debug", "console_prod"], + "level": "INFO" if DEBUG else "ERROR", + "propagate": False, + }, + LOGGER_NAME: { + "handlers": ["console_debug", "console_prod"], + "level": "DEBUG" if DEBUG else "INFO", + "propagate": False, + }, + "root": { + "handlers": ["console_debug", "console_prod"], + "level": "INFO" if DEBUG else "ERROR", + "propagate": False, + }, +} + +LOGGING = { + "version": 1, + "disable_existing_loggers": False, + "filters": LOGGING_FILTERS, + "formatters": LOGGING_FORMATTERS, + "handlers": LOGGING_HANDLERS, + "loggers": LOGGING_LOGGERS, +} + +LOGGING_CONFIG = "logging.config.dictConfig" + + +# Models + +ABSOLUTE_URL_OVERRIDES: dict[str, Callable] = {} + +FIXTURE_DIRS: list[str] = [] + +INSTALLED_APPS = [ + # Build-in apps + "django.contrib.admin", + "django.contrib.auth", + "django.contrib.contenttypes", + "django.contrib.sessions", + "django.contrib.messages", + "django.contrib.staticfiles", + # Healthcheck + "health_check", + "health_check.db", + "health_check.cache", + "health_check.storage", + "health_check.contrib.migrations", + "health_check.contrib.celery", + "health_check.contrib.celery_ping", + # Third-party apps + "corsheaders", + "django_extensions", + "django_guid", + "ninja", + "minio_storage", + # Internal apps + "apps.core", +] + +# GUID + +DJANGO_GUID = { + "GUID_HEADER_NAME": "Correlation-ID", + "VALIDATE_GUID": True, + "RETURN_HEADER": True, + "EXPOSE_HEADER": True, + "INTEGRATIONS": [], + "IGNORE_URLS": [], + "UUID_LENGTH": 32, +} + + +# Security + +LANGUAGE_COOKIE_AGE = 31449600 + +LANGUAGE_COOKIE_DOMAIN = None + +LANGUAGE_COOKIE_HTTPONLY = False + +LANGUAGE_COOKIE_NAME = "django_language" + +LANGUAGE_COOKIE_PATH = "/" + +LANGUAGE_COOKIE_SAMESITE = "Lax" + +LANGUAGE_COOKIE_SECURE = False + +SECURE_PROXY_SSL_HEADER = None + +CSRF_COOKIE_AGE = 31449600 + +CSRF_COOKIE_DOMAIN = None + +CSRF_COOKIE_HTTPONLY = False + +CSRF_COOKIE_NAME = "djangocsrftoken" + +CSRF_COOKIE_PATH = "/" + +CSRF_COOKIE_SAMESITE = "Lax" + +CSRF_COOKIE_SECURE = False + +CSRF_FAILURE_VIEW = "django.views.csrf.csrf_failure" + +CSRF_HEADER_NAME = "HTTP_X_CSRFTOKEN" + +CSRF_TRUSTED_ORIGINS = env( + "DJANGO_CSRF_TRUSTED_ORIGINS", + list, + default=["http://localhost", "http://127.0.0.1"], +) + +CSRF_USE_SESSIONS = False + +SECRET_KEY = env("DJANGO_SECRET_KEY", default="very_insecure_key") + +SECRET_KEY_FALLBACKS: list[str] = [] + + +# Sessions + +SESSION_CACHE_ALIAS = "default" + +SESSION_COOKIE_AGE = 1209600 + +SESSION_COOKIE_DOMAIN = None + +SESSION_COOKIE_HTTPONLY = True + +SESSION_COOKIE_NAME = "djangosessionid" + +SESSION_COOKIE_PATH = "/" + +SESSION_COOKIE_SAMESITE = "Lax" + +SESSION_COOKIE_SECURE = False + +SESSION_ENGINE = "django.contrib.sessions.backends.db" + +SESSION_EXPIRE_AT_BROWSER_CLOSE = False + +SESSION_FILE_PATH = None + +SESSION_SAVE_EVERY_REQUEST = False + +SESSION_SERIALIZER = "django.contrib.sessions.serializers.JSONSerializer" + + +# Templates + +TEMPLATES = [ + { + "BACKEND": "django.template.backends.django.DjangoTemplates", + "DIRS": [], + "APP_DIRS": True, + "OPTIONS": { + "autoescape": True, + "context_processors": [ + "django.template.context_processors.debug", + "django.template.context_processors.request", + "django.contrib.auth.context_processors.auth", + "django.contrib.messages.context_processors.messages", + ], + "debug": DEBUG, + "string_if_invalid": "", + "file_charset": "utf-8", + }, + }, +] + + +# Testing + +TEST_NON_SERIALIZED_APPS: list[str] = [] + +TEST_RUNNER = "django.test.runner.DiscoverRunner" + + +# URLs + +ROOT_URLCONF = "config.urls" + + +# debug-toolbar + +DEBUG_TOOLBAR_ENABLED = False + +with contextlib.suppress(Exception): + import debug_toolbar # noqa: F401 + + DEBUG_TOOLBAR_ENABLED = True + +DEBUG_TOOLBAR_CONFIG = {"SHOW_COLLAPSED": True, "UPDATE_ON_FETCH": True} + +if DEBUG and DEBUG_TOOLBAR_ENABLED: + INSTALLED_APPS.append("debug_toolbar") + MIDDLEWARE.append("debug_toolbar.middleware.DebugToolbarMiddleware") diff --git a/services/backend/config/urls.py b/services/backend/config/urls.py new file mode 100644 index 0000000..4dc0330 --- /dev/null +++ b/services/backend/config/urls.py @@ -0,0 +1,34 @@ +"""URL configuration for project_name.""" + +from django.conf import settings +from django.contrib import admin +from django.urls import include, path + +from config import handlers + +admin.site.site_title = "project_name" +admin.site.site_header = "project_name" +admin.site.index_title = "project_name" + + +urlpatterns = [ + # Admin urls + path("admin/", admin.site.urls), + # API urls + path("", include("api.urls")), +] + + +if settings.DEBUG and settings.DEBUG_TOOLBAR_ENABLED: + from debug_toolbar.toolbar import debug_toolbar_urls + + urlpatterns += debug_toolbar_urls() + + +handler400 = handlers.handler400 + +handler403 = handlers.handler403 + +handler404 = handlers.handler404 + +handler500 = handlers.handler500 diff --git a/services/backend/config/wsgi.py b/services/backend/config/wsgi.py new file mode 100644 index 0000000..478b263 --- /dev/null +++ b/services/backend/config/wsgi.py @@ -0,0 +1,9 @@ +"""WSGI config for project_name.""" + +import os + +from django.core.wsgi import get_wsgi_application + +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings") + +application = get_wsgi_application() diff --git a/services/backend/manage.py b/services/backend/manage.py new file mode 100755 index 0000000..f821d3a --- /dev/null +++ b/services/backend/manage.py @@ -0,0 +1,22 @@ +#!/usr/bin/env python +"""Django's command-line utility for administrative tasks.""" + +import os +import sys + + +def main() -> None: + """Run administrative tasks.""" + os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings") + try: + from django.core.management import execute_from_command_line + except ImportError as exc: + e = """Couldn't import Django. Are you sure it's installed and " + "available on your PYTHONPATH environment variable? Did you " + "forget to activate a virtual environment?""" + raise ImportError(e) from exc + execute_from_command_line(sys.argv) + + +if __name__ == "__main__": + main() diff --git a/services/backend/pyproject.toml b/services/backend/pyproject.toml new file mode 100644 index 0000000..fc82dc8 --- /dev/null +++ b/services/backend/pyproject.toml @@ -0,0 +1,160 @@ +[project] +name = "project_name-backend" +version = "0.1.0" +readme = "README.md" +requires-python = ">=3.10,<3.12" +dependencies = [ + "celery>=5.4.0", + "colorlog>=6.9.0", + "django-cors-headers>=4.6.0", + "django-environ>=0.11.2", + "django-extensions>=3.2.3", + "django-guid>=3.5.0", + "django-health-check>=3.18.3", + "django-minio-storage>=0.5.7", + "django-ninja>=1.3.0", + "django-stubs-ext>=5.1.3", + "gunicorn>=23.0.0", + "httpx>=0.28.1", + "pillow>=11.1.0", + "psycopg2-binary>=2.9.10", + "pydantic>=2.10.5", + "pyjwt>=2.10.1", + "python-json-logger>=3.2.1", + "pytz>=2024.2", + "redis>=5.2.1", +] + +[dependency-groups] +dev = [ + "coverage>=7.6.12", + "django-debug-toolbar>=4.4.6", + "django-stubs[compatible-mypy]>=5.1.3", + "mypy>=1.15.0", + "ruff>=0.9.3", +] + +[tool.ruff] +builtins = [] +cache-dir = ".ruff_cache" +exclude = [ + ".bzr", + ".direnv", + ".eggs", + ".git", + ".git-rewrite", + ".hg", + ".mypy_cache", + ".nox", + ".pants.d", + ".pytype", + ".ruff_cache", + ".svn", + ".tox", + ".venv", + "__pypackages__", + "_build", + "buck-out", + "dist", + "migrations", + "node_modules", + "venv", +] +extend-exclude = [] +extend-include = [] +fix = false +fix-only = false +force-exclude = true +include = ["*.py", "*.pyi", "*.ipynb", "**/pyproject.toml"] +indent-width = 4 +line-length = 79 +namespace-packages = [] +output-format = "full" +preview = false +required-version = ">=0.8.4" +respect-gitignore = true +show-fixes = true +src = [".", "src"] +target-version = "py310" +unsafe-fixes = false + +[tool.ruff.analyze] +detect-string-imports = true +direction = "Dependencies" +exclude = [] +include-dependencies = {} +preview = false + +[tool.ruff.format] +docstring-code-format = true +docstring-code-line-length = 79 +exclude = [] +indent-style = "space" +line-ending = "lf" +preview = false +quote-style = "double" +skip-magic-trailing-comma = false + +[tool.ruff.lint] +allowed-confusables = ["ℹ"] +dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$" +exclude = ["tests.py"] +explicit-preview-rules = false +extend-fixable = [] +extend-per-file-ignores = {} +extend-safe-fixes = [] +extend-select = [] +extend-unsafe-fixes = [] +external = [] +fixable = ["ALL"] +ignore = [ + "ARG", + "D", + "ANN401", + "COM812", + "DJ001", + "DJ007", + "FBT001", + "FBT002", + "N813", + "PLR2004", + "PT009", + "PT027", + "RUF001", + "S311", +] +logger-objects = [] +per-file-ignores = {} +preview = false +select = ["ALL"] +task-tags = ["TODO", "FIXME", "HACK", "WORKOUT"] +typing-modules = [] +unfixable = [] + +[tool.ruff.lint.pylint] +max-args = 6 + +[tool.mypy] +plugins = ["mypy_django_plugin.main"] +ignore_missing_imports = true +strict = false +show_error_context = false +no_implicit_optional = false + +[tool.django-stubs] +django_settings_module = "config.settings" +strict_settings = false + +[tool.coverage.run] +omit = [ + "manage.py", + "config/wsgi.py", + "config/asgi.py", + "config/urls.py", + "config/settings.py", + "config/handlers.py", + "config/errors.py", +] + +[tool.coverage.report] +skip_covered = true diff --git a/services/backend/scripts/check b/services/backend/scripts/check new file mode 100755 index 0000000..f72deb4 --- /dev/null +++ b/services/backend/scripts/check @@ -0,0 +1,12 @@ +#!/bin/sh + +GREEN='\033[1;32m' +NC='\033[0m' + +uvx ruff format . +uvx ruff check . --fix +printf "${GREEN}Linters/formatters runned${NC}\n" + +uv run python manage.py makemigrations --check +uv run python manage.py test +printf "${GREEN}Tests runned${NC}\n" diff --git a/services/backend/scripts/initdb b/services/backend/scripts/initdb new file mode 100755 index 0000000..f2d64eb --- /dev/null +++ b/services/backend/scripts/initdb @@ -0,0 +1,11 @@ +#!/bin/sh + +python manage.py migrate +if [ $? -ne 0 ]; then + echo "Migration failed" + exit 1 +fi + +if [ "$DJANGO_CREATE_SUPERUSER" = "True" ]; then + python manage.py createsuperuser --noinput --username "$DJANGO_SUPERUSER_USERNAME" --email "$DJANGO_SUPERUSER_EMAIL" || true +fi diff --git a/services/frontend/.gitignore b/services/frontend/.gitignore new file mode 100644 index 0000000..a547bf3 --- /dev/null +++ b/services/frontend/.gitignore @@ -0,0 +1,24 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/services/frontend/.prettierrc b/services/frontend/.prettierrc new file mode 100644 index 0000000..b4bfed3 --- /dev/null +++ b/services/frontend/.prettierrc @@ -0,0 +1,3 @@ +{ + "plugins": ["prettier-plugin-tailwindcss"] +} diff --git a/services/frontend/README.md b/services/frontend/README.md new file mode 100644 index 0000000..74872fd --- /dev/null +++ b/services/frontend/README.md @@ -0,0 +1,50 @@ +# React + TypeScript + Vite + +This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules. + +Currently, two official plugins are available: + +- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/README.md) uses [Babel](https://babeljs.io/) for Fast Refresh +- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh + +## Expanding the ESLint configuration + +If you are developing a production application, we recommend updating the configuration to enable type aware lint rules: + +- Configure the top-level `parserOptions` property like this: + +```js +export default tseslint.config({ + languageOptions: { + // other options... + parserOptions: { + project: ['./tsconfig.node.json', './tsconfig.app.json'], + tsconfigRootDir: import.meta.dirname, + }, + }, +}) +``` + +- Replace `tseslint.configs.recommended` to `tseslint.configs.recommendedTypeChecked` or `tseslint.configs.strictTypeChecked` +- Optionally add `...tseslint.configs.stylisticTypeChecked` +- Install [eslint-plugin-react](https://github.com/jsx-eslint/eslint-plugin-react) and update the config: + +```js +// eslint.config.js +import react from 'eslint-plugin-react' + +export default tseslint.config({ + // Set the react version + settings: { react: { version: '18.3' } }, + plugins: { + // Add the react plugin + react, + }, + rules: { + // other rules... + // Enable its recommended rules + ...react.configs.recommended.rules, + ...react.configs['jsx-runtime'].rules, + }, +}) +``` diff --git a/services/frontend/bun.lock b/services/frontend/bun.lock new file mode 100644 index 0000000..47cb214 --- /dev/null +++ b/services/frontend/bun.lock @@ -0,0 +1,530 @@ +{ + "lockfileVersion": 1, + "workspaces": { + "": { + "name": "frontend", + "dependencies": { + "@radix-ui/react-slot": "^1.1.2", + "@tailwindcss/vite": "^4.0.9", + "class-variance-authority": "^0.7.1", + "clsx": "^2.1.1", + "lucide-react": "^0.476.0", + "ofetch": "^1.4.1", + "react": "^19.0.0", + "react-dom": "^19.0.0", + "react-router": "^7.2.0", + "tailwind-merge": "^3.0.2", + "tailwindcss": "^4.0.9", + "tailwindcss-animate": "^1.0.7", + }, + "devDependencies": { + "@eslint/js": "^9.21.0", + "@types/node": "^22.13.5", + "@types/react": "^19.0.10", + "@types/react-dom": "^19.0.4", + "@vitejs/plugin-react-swc": "^3.8.0", + "eslint": "^9.21.0", + "eslint-plugin-react-hooks": "^5.0.0", + "eslint-plugin-react-refresh": "^0.4.19", + "globals": "^15.15.0", + "prettier": "^3.5.2", + "prettier-plugin-tailwindcss": "^0.6.11", + "typescript": "~5.7.2", + "typescript-eslint": "^8.24.1", + "vite": "^6.2.0", + "vite-tsconfig-paths": "^5.1.4", + }, + }, + }, + "packages": { + "@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.0", "", { "os": "aix", "cpu": "ppc64" }, "sha512-O7vun9Sf8DFjH2UtqK8Ku3LkquL9SZL8OLY1T5NZkA34+wG3OQF7cl4Ql8vdNzM6fzBbYfLaiRLIOZ+2FOCgBQ=="], + + "@esbuild/android-arm": ["@esbuild/android-arm@0.25.0", "", { "os": "android", "cpu": "arm" }, "sha512-PTyWCYYiU0+1eJKmw21lWtC+d08JDZPQ5g+kFyxP0V+es6VPPSUhM6zk8iImp2jbV6GwjX4pap0JFbUQN65X1g=="], + + "@esbuild/android-arm64": ["@esbuild/android-arm64@0.25.0", "", { "os": "android", "cpu": "arm64" }, "sha512-grvv8WncGjDSyUBjN9yHXNt+cq0snxXbDxy5pJtzMKGmmpPxeAmAhWxXI+01lU5rwZomDgD3kJwulEnhTRUd6g=="], + + "@esbuild/android-x64": ["@esbuild/android-x64@0.25.0", "", { "os": "android", "cpu": "x64" }, "sha512-m/ix7SfKG5buCnxasr52+LI78SQ+wgdENi9CqyCXwjVR2X4Jkz+BpC3le3AoBPYTC9NHklwngVXvbJ9/Akhrfg=="], + + "@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.25.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-mVwdUb5SRkPayVadIOI78K7aAnPamoeFR2bT5nszFUZ9P8UpK4ratOdYbZZXYSqPKMHfS1wdHCJk1P1EZpRdvw=="], + + "@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.25.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-DgDaYsPWFTS4S3nWpFcMn/33ZZwAAeAFKNHNa1QN0rI4pUjgqf0f7ONmXf6d22tqTY+H9FNdgeaAa+YIFUn2Rg=="], + + "@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.25.0", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-VN4ocxy6dxefN1MepBx/iD1dH5K8qNtNe227I0mnTRjry8tj5MRk4zprLEdG8WPyAPb93/e4pSgi1SoHdgOa4w=="], + + "@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.25.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-mrSgt7lCh07FY+hDD1TxiTyIHyttn6vnjesnPoVDNmDfOmggTLXRv8Id5fNZey1gl/V2dyVK1VXXqVsQIiAk+A=="], + + "@esbuild/linux-arm": ["@esbuild/linux-arm@0.25.0", "", { "os": "linux", "cpu": "arm" }, "sha512-vkB3IYj2IDo3g9xX7HqhPYxVkNQe8qTK55fraQyTzTX/fxaDtXiEnavv9geOsonh2Fd2RMB+i5cbhu2zMNWJwg=="], + + "@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.25.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-9QAQjTWNDM/Vk2bgBl17yWuZxZNQIF0OUUuPZRKoDtqF2k4EtYbpyiG5/Dk7nqeK6kIJWPYldkOcBqjXjrUlmg=="], + + "@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.25.0", "", { "os": "linux", "cpu": "ia32" }, "sha512-43ET5bHbphBegyeqLb7I1eYn2P/JYGNmzzdidq/w0T8E2SsYL1U6un2NFROFRg1JZLTzdCoRomg8Rvf9M6W6Gg=="], + + "@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.25.0", "", { "os": "linux", "cpu": "none" }, "sha512-fC95c/xyNFueMhClxJmeRIj2yrSMdDfmqJnyOY4ZqsALkDrrKJfIg5NTMSzVBr5YW1jf+l7/cndBfP3MSDpoHw=="], + + "@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.25.0", "", { "os": "linux", "cpu": "none" }, "sha512-nkAMFju7KDW73T1DdH7glcyIptm95a7Le8irTQNO/qtkoyypZAnjchQgooFUDQhNAy4iu08N79W4T4pMBwhPwQ=="], + + "@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.25.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-NhyOejdhRGS8Iwv+KKR2zTq2PpysF9XqY+Zk77vQHqNbo/PwZCzB5/h7VGuREZm1fixhs4Q/qWRSi5zmAiO4Fw=="], + + "@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.25.0", "", { "os": "linux", "cpu": "none" }, "sha512-5S/rbP5OY+GHLC5qXp1y/Mx//e92L1YDqkiBbO9TQOvuFXM+iDqUNG5XopAnXoRH3FjIUDkeGcY1cgNvnXp/kA=="], + + "@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.25.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-XM2BFsEBz0Fw37V0zU4CXfcfuACMrppsMFKdYY2WuTS3yi8O1nFOhil/xhKTmE1nPmVyvQJjJivgDT+xh8pXJA=="], + + "@esbuild/linux-x64": ["@esbuild/linux-x64@0.25.0", "", { "os": "linux", "cpu": "x64" }, "sha512-9yl91rHw/cpwMCNytUDxwj2XjFpxML0y9HAOH9pNVQDpQrBxHy01Dx+vaMu0N1CKa/RzBD2hB4u//nfc+Sd3Cw=="], + + "@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.25.0", "", { "os": "none", "cpu": "arm64" }, "sha512-RuG4PSMPFfrkH6UwCAqBzauBWTygTvb1nxWasEJooGSJ/NwRw7b2HOwyRTQIU97Hq37l3npXoZGYMy3b3xYvPw=="], + + "@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.25.0", "", { "os": "none", "cpu": "x64" }, "sha512-jl+qisSB5jk01N5f7sPCsBENCOlPiS/xptD5yxOx2oqQfyourJwIKLRA2yqWdifj3owQZCL2sn6o08dBzZGQzA=="], + + "@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.25.0", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-21sUNbq2r84YE+SJDfaQRvdgznTD8Xc0oc3p3iW/a1EVWeNj/SdUCbm5U0itZPQYRuRTW20fPMWMpcrciH2EJw=="], + + "@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.25.0", "", { "os": "openbsd", "cpu": "x64" }, "sha512-2gwwriSMPcCFRlPlKx3zLQhfN/2WjJ2NSlg5TKLQOJdV0mSxIcYNTMhk3H3ulL/cak+Xj0lY1Ym9ysDV1igceg=="], + + "@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.25.0", "", { "os": "sunos", "cpu": "x64" }, "sha512-bxI7ThgLzPrPz484/S9jLlvUAHYMzy6I0XiU1ZMeAEOBcS0VePBFxh1JjTQt3Xiat5b6Oh4x7UC7IwKQKIJRIg=="], + + "@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.25.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-ZUAc2YK6JW89xTbXvftxdnYy3m4iHIkDtK3CLce8wg8M2L+YZhIvO1DKpxrd0Yr59AeNNkTiic9YLf6FTtXWMw=="], + + "@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.25.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-eSNxISBu8XweVEWG31/JzjkIGbGIJN/TrRoiSVZwZ6pkC6VX4Im/WV2cz559/TXLcYbcrDN8JtKgd9DJVIo8GA=="], + + "@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.0", "", { "os": "win32", "cpu": "x64" }, "sha512-ZENoHJBxA20C2zFzh6AI4fT6RraMzjYw4xKWemRTRmRVtN9c5DcH9r/f2ihEkMjOW5eGgrwCslG/+Y/3bL+DHQ=="], + + "@eslint-community/eslint-utils": ["@eslint-community/eslint-utils@4.4.1", "", { "dependencies": { "eslint-visitor-keys": "^3.4.3" }, "peerDependencies": { "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" } }, "sha512-s3O3waFUrMV8P/XaF/+ZTp1X9XBZW1a4B97ZnjQF2KYWaFD2A8KyFBsrsfSjEmjn3RGWAIuvlneuZm3CUK3jbA=="], + + "@eslint-community/regexpp": ["@eslint-community/regexpp@4.12.1", "", {}, "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ=="], + + "@eslint/config-array": ["@eslint/config-array@0.19.2", "", { "dependencies": { "@eslint/object-schema": "^2.1.6", "debug": "^4.3.1", "minimatch": "^3.1.2" } }, "sha512-GNKqxfHG2ySmJOBSHg7LxeUx4xpuCoFjacmlCoYWEbaPXLwvfIjixRI12xCQZeULksQb23uiA8F40w5TojpV7w=="], + + "@eslint/core": ["@eslint/core@0.12.0", "", { "dependencies": { "@types/json-schema": "^7.0.15" } }, "sha512-cmrR6pytBuSMTaBweKoGMwu3EiHiEC+DoyupPmlZ0HxBJBtIxwe+j/E4XPIKNx+Q74c8lXKPwYawBf5glsTkHg=="], + + "@eslint/eslintrc": ["@eslint/eslintrc@3.3.0", "", { "dependencies": { "ajv": "^6.12.4", "debug": "^4.3.2", "espree": "^10.0.1", "globals": "^14.0.0", "ignore": "^5.2.0", "import-fresh": "^3.2.1", "js-yaml": "^4.1.0", "minimatch": "^3.1.2", "strip-json-comments": "^3.1.1" } }, "sha512-yaVPAiNAalnCZedKLdR21GOGILMLKPyqSLWaAjQFvYA2i/ciDi8ArYVr69Anohb6cH2Ukhqti4aFnYyPm8wdwQ=="], + + "@eslint/js": ["@eslint/js@9.21.0", "", {}, "sha512-BqStZ3HX8Yz6LvsF5ByXYrtigrV5AXADWLAGc7PH/1SxOb7/FIYYMszZZWiUou/GB9P2lXWk2SV4d+Z8h0nknw=="], + + "@eslint/object-schema": ["@eslint/object-schema@2.1.6", "", {}, "sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA=="], + + "@eslint/plugin-kit": ["@eslint/plugin-kit@0.2.7", "", { "dependencies": { "@eslint/core": "^0.12.0", "levn": "^0.4.1" } }, "sha512-JubJ5B2pJ4k4yGxaNLdbjrnk9d/iDz6/q8wOilpIowd6PJPgaxCuHBnBszq7Ce2TyMrywm5r4PnKm6V3iiZF+g=="], + + "@humanfs/core": ["@humanfs/core@0.19.1", "", {}, "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA=="], + + "@humanfs/node": ["@humanfs/node@0.16.6", "", { "dependencies": { "@humanfs/core": "^0.19.1", "@humanwhocodes/retry": "^0.3.0" } }, "sha512-YuI2ZHQL78Q5HbhDiBA1X4LmYdXCKCMQIfw0pw7piHJwyREFebJUvrQN4cMssyES6x+vfUbx1CIpaQUKYdQZOw=="], + + "@humanwhocodes/module-importer": ["@humanwhocodes/module-importer@1.0.1", "", {}, "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA=="], + + "@humanwhocodes/retry": ["@humanwhocodes/retry@0.4.2", "", {}, "sha512-xeO57FpIu4p1Ri3Jq/EXq4ClRm86dVF2z/+kvFnyqVYRavTZmaFaUBbWCOuuTh0o/g7DSsk6kc2vrS4Vl5oPOQ=="], + + "@nodelib/fs.scandir": ["@nodelib/fs.scandir@2.1.5", "", { "dependencies": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" } }, "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g=="], + + "@nodelib/fs.stat": ["@nodelib/fs.stat@2.0.5", "", {}, "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A=="], + + "@nodelib/fs.walk": ["@nodelib/fs.walk@1.2.8", "", { "dependencies": { "@nodelib/fs.scandir": "2.1.5", "fastq": "^1.6.0" } }, "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg=="], + + "@radix-ui/react-compose-refs": ["@radix-ui/react-compose-refs@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Y9VzoRDSJtgFMUCoiZBDVo084VQ5hfpXxVE+NgkdNsjiDBByiImMZKKhxMwCbdHvhlENG6a833CbFkOQvTricw=="], + + "@radix-ui/react-slot": ["@radix-ui/react-slot@1.1.2", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-YAKxaiGsSQJ38VzKH86/BPRC4rh+b1Jpa+JneA5LRE7skmLPNAyeG8kPJj/oo4STLvlrs8vkf/iYyc3A5stYCQ=="], + + "@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.34.8", "", { "os": "android", "cpu": "arm" }, "sha512-q217OSE8DTp8AFHuNHXo0Y86e1wtlfVrXiAlwkIvGRQv9zbc6mE3sjIVfwI8sYUyNxwOg0j/Vm1RKM04JcWLJw=="], + + "@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.34.8", "", { "os": "android", "cpu": "arm64" }, "sha512-Gigjz7mNWaOL9wCggvoK3jEIUUbGul656opstjaUSGC3eT0BM7PofdAJaBfPFWWkXNVAXbaQtC99OCg4sJv70Q=="], + + "@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.34.8", "", { "os": "darwin", "cpu": "arm64" }, "sha512-02rVdZ5tgdUNRxIUrFdcMBZQoaPMrxtwSb+/hOfBdqkatYHR3lZ2A2EGyHq2sGOd0Owk80oV3snlDASC24He3Q=="], + + "@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.34.8", "", { "os": "darwin", "cpu": "x64" }, "sha512-qIP/elwR/tq/dYRx3lgwK31jkZvMiD6qUtOycLhTzCvrjbZ3LjQnEM9rNhSGpbLXVJYQ3rq39A6Re0h9tU2ynw=="], + + "@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.34.8", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-IQNVXL9iY6NniYbTaOKdrlVP3XIqazBgJOVkddzJlqnCpRi/yAeSOa8PLcECFSQochzqApIOE1GHNu3pCz+BDA=="], + + "@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.34.8", "", { "os": "freebsd", "cpu": "x64" }, "sha512-TYXcHghgnCqYFiE3FT5QwXtOZqDj5GmaFNTNt3jNC+vh22dc/ukG2cG+pi75QO4kACohZzidsq7yKTKwq/Jq7Q=="], + + "@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.34.8", "", { "os": "linux", "cpu": "arm" }, "sha512-A4iphFGNkWRd+5m3VIGuqHnG3MVnqKe7Al57u9mwgbyZ2/xF9Jio72MaY7xxh+Y87VAHmGQr73qoKL9HPbXj1g=="], + + "@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.34.8", "", { "os": "linux", "cpu": "arm" }, "sha512-S0lqKLfTm5u+QTxlFiAnb2J/2dgQqRy/XvziPtDd1rKZFXHTyYLoVL58M/XFwDI01AQCDIevGLbQrMAtdyanpA=="], + + "@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.34.8", "", { "os": "linux", "cpu": "arm64" }, "sha512-jpz9YOuPiSkL4G4pqKrus0pn9aYwpImGkosRKwNi+sJSkz+WU3anZe6hi73StLOQdfXYXC7hUfsQlTnjMd3s1A=="], + + "@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.34.8", "", { "os": "linux", "cpu": "arm64" }, "sha512-KdSfaROOUJXgTVxJNAZ3KwkRc5nggDk+06P6lgi1HLv1hskgvxHUKZ4xtwHkVYJ1Rep4GNo+uEfycCRRxht7+Q=="], + + "@rollup/rollup-linux-loongarch64-gnu": ["@rollup/rollup-linux-loongarch64-gnu@4.34.8", "", { "os": "linux", "cpu": "none" }, "sha512-NyF4gcxwkMFRjgXBM6g2lkT58OWztZvw5KkV2K0qqSnUEqCVcqdh2jN4gQrTn/YUpAcNKyFHfoOZEer9nwo6uQ=="], + + "@rollup/rollup-linux-powerpc64le-gnu": ["@rollup/rollup-linux-powerpc64le-gnu@4.34.8", "", { "os": "linux", "cpu": "ppc64" }, "sha512-LMJc999GkhGvktHU85zNTDImZVUCJ1z/MbAJTnviiWmmjyckP5aQsHtcujMjpNdMZPT2rQEDBlJfubhs3jsMfw=="], + + "@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.34.8", "", { "os": "linux", "cpu": "none" }, "sha512-xAQCAHPj8nJq1PI3z8CIZzXuXCstquz7cIOL73HHdXiRcKk8Ywwqtx2wrIy23EcTn4aZ2fLJNBB8d0tQENPCmw=="], + + "@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.34.8", "", { "os": "linux", "cpu": "s390x" }, "sha512-DdePVk1NDEuc3fOe3dPPTb+rjMtuFw89gw6gVWxQFAuEqqSdDKnrwzZHrUYdac7A7dXl9Q2Vflxpme15gUWQFA=="], + + "@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.34.8", "", { "os": "linux", "cpu": "x64" }, "sha512-8y7ED8gjxITUltTUEJLQdgpbPh1sUQ0kMTmufRF/Ns5tI9TNMNlhWtmPKKHCU0SilX+3MJkZ0zERYYGIVBYHIA=="], + + "@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.34.8", "", { "os": "linux", "cpu": "x64" }, "sha512-SCXcP0ZpGFIe7Ge+McxY5zKxiEI5ra+GT3QRxL0pMMtxPfpyLAKleZODi1zdRHkz5/BhueUrYtYVgubqe9JBNQ=="], + + "@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.34.8", "", { "os": "win32", "cpu": "arm64" }, "sha512-YHYsgzZgFJzTRbth4h7Or0m5O74Yda+hLin0irAIobkLQFRQd1qWmnoVfwmKm9TXIZVAD0nZ+GEb2ICicLyCnQ=="], + + "@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.34.8", "", { "os": "win32", "cpu": "ia32" }, "sha512-r3NRQrXkHr4uWy5TOjTpTYojR9XmF0j/RYgKCef+Ag46FWUTltm5ziticv8LdNsDMehjJ543x/+TJAek/xBA2w=="], + + "@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.34.8", "", { "os": "win32", "cpu": "x64" }, "sha512-U0FaE5O1BCpZSeE6gBl3c5ObhePQSfk9vDRToMmTkbhCOgW4jqvtS5LGyQ76L1fH8sM0keRp4uDTsbjiUyjk0g=="], + + "@swc/core": ["@swc/core@1.11.1", "", { "dependencies": { "@swc/counter": "^0.1.3", "@swc/types": "^0.1.18" }, "optionalDependencies": { "@swc/core-darwin-arm64": "1.11.1", "@swc/core-darwin-x64": "1.11.1", "@swc/core-linux-arm-gnueabihf": "1.11.1", "@swc/core-linux-arm64-gnu": "1.11.1", "@swc/core-linux-arm64-musl": "1.11.1", "@swc/core-linux-x64-gnu": "1.11.1", "@swc/core-linux-x64-musl": "1.11.1", "@swc/core-win32-arm64-msvc": "1.11.1", "@swc/core-win32-ia32-msvc": "1.11.1", "@swc/core-win32-x64-msvc": "1.11.1" }, "peerDependencies": { "@swc/helpers": "*" }, "optionalPeers": ["@swc/helpers"] }, "sha512-67+lBHZ1lAJQKoOhBHl9DE2iugPYAulRVArZjoF+DnIY3G9wLXCXxw5It0IaCnzvJVvUPxGmr0rHViXKBDP5Vg=="], + + "@swc/core-darwin-arm64": ["@swc/core-darwin-arm64@1.11.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-bJbqZ51JghEZ8WaFetofkfkS3MWsS/V3vDvY+0r+SlLeocZwf8q8/GqcafnElHcU+zLV6yTi13fJwUce6ULiUQ=="], + + "@swc/core-darwin-x64": ["@swc/core-darwin-x64@1.11.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-9GGEoN0uxkLg3KocOVzMfe9c9/DxESXclsL/U2xVLa3pTFB5YnXhiCP5YBT/3Q7nSGLD+R2ALqkNlDoueUjvPw=="], + + "@swc/core-linux-arm-gnueabihf": ["@swc/core-linux-arm-gnueabihf@1.11.1", "", { "os": "linux", "cpu": "arm" }, "sha512-Lt7l/l0nfSTUzsWcVY3dtOPl5RtgCJ+Ya8IG4Aa3l6c7kLc6Sx4JpylpEIY9yhGidDy/uQ8KUg5kqUPtUrXrvQ=="], + + "@swc/core-linux-arm64-gnu": ["@swc/core-linux-arm64-gnu@1.11.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-oe826cfuGukctTSpDjk7RJRDEJihQMAzvO5tdWK0wcy+zvMPFyH5Fg6cW0X4ST3M7fcV91/1T/iuiiD2SVamYw=="], + + "@swc/core-linux-arm64-musl": ["@swc/core-linux-arm64-musl@1.11.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-ABb4pnYeQp/JBJS5Qd2apTwOzpzrTebQFUiFjk0WgTKIr9T6SL3tLXMjgvbSXIath+1HnbCKFUwDXNQhgGFFTg=="], + + "@swc/core-linux-x64-gnu": ["@swc/core-linux-x64-gnu@1.11.1", "", { "os": "linux", "cpu": "x64" }, "sha512-E09TcHv40bV0mOHTKquZw0IOcQ+lzzpQjyOhCa7+GBpbS3eg5/35Gu7DfToN2bomz74LPKW/l7jZRG+ZNOYNHQ=="], + + "@swc/core-linux-x64-musl": ["@swc/core-linux-x64-musl@1.11.1", "", { "os": "linux", "cpu": "x64" }, "sha512-cuW4r7GbvQt9uv+rGdYLHUjDvGjHmr1nYE7iFVk6r4i+byZuXBK6M7P1p+/dTzacshOc05I9n/eUV+Hfjp9a3A=="], + + "@swc/core-win32-arm64-msvc": ["@swc/core-win32-arm64-msvc@1.11.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-H8Q78GwaKnCL4isHx8JRTRi6vUU6iMLbpegS2jzWWC1On7EePhkLx2eR8nEsaRIQB6rc3WqdIj74OgOpNoPi7g=="], + + "@swc/core-win32-ia32-msvc": ["@swc/core-win32-ia32-msvc@1.11.1", "", { "os": "win32", "cpu": "ia32" }, "sha512-Rx7cZ0OvqMb16fgmUSlPWQbH1+X355IDJhVQpUlpL+ezD/kkWmJix+4u2GVE/LHrfbdyZ4sjjIzSsCQxJV05Mw=="], + + "@swc/core-win32-x64-msvc": ["@swc/core-win32-x64-msvc@1.11.1", "", { "os": "win32", "cpu": "x64" }, "sha512-6bEEC/XU1lwYzUXY7BXj3nhe7iBF9+i9dVo+hbiVxXZMrD0LUd+7urOBM3NtVnDsUaR6Ge/g7aR+OfpgYscKOg=="], + + "@swc/counter": ["@swc/counter@0.1.3", "", {}, "sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ=="], + + "@swc/types": ["@swc/types@0.1.18", "", { "dependencies": { "@swc/counter": "^0.1.3" } }, "sha512-NZghLaQvF3eFdj2DUjGkpwaunbZYaRcxciHINnwA4n3FrLAI8hKFOBqs2wkcOiLQfWkIdfuG6gBkNFrkPNji5g=="], + + "@tailwindcss/node": ["@tailwindcss/node@4.0.9", "", { "dependencies": { "enhanced-resolve": "^5.18.1", "jiti": "^2.4.2", "tailwindcss": "4.0.9" } }, "sha512-tOJvdI7XfJbARYhxX+0RArAhmuDcczTC46DGCEziqxzzbIaPnfYaIyRT31n4u8lROrsO7Q6u/K9bmQHL2uL1bQ=="], + + "@tailwindcss/oxide": ["@tailwindcss/oxide@4.0.9", "", { "optionalDependencies": { "@tailwindcss/oxide-android-arm64": "4.0.9", "@tailwindcss/oxide-darwin-arm64": "4.0.9", "@tailwindcss/oxide-darwin-x64": "4.0.9", "@tailwindcss/oxide-freebsd-x64": "4.0.9", "@tailwindcss/oxide-linux-arm-gnueabihf": "4.0.9", "@tailwindcss/oxide-linux-arm64-gnu": "4.0.9", "@tailwindcss/oxide-linux-arm64-musl": "4.0.9", "@tailwindcss/oxide-linux-x64-gnu": "4.0.9", "@tailwindcss/oxide-linux-x64-musl": "4.0.9", "@tailwindcss/oxide-win32-arm64-msvc": "4.0.9", "@tailwindcss/oxide-win32-x64-msvc": "4.0.9" } }, "sha512-eLizHmXFqHswJONwfqi/WZjtmWZpIalpvMlNhTM99/bkHtUs6IqgI1XQ0/W5eO2HiRQcIlXUogI2ycvKhVLNcA=="], + + "@tailwindcss/oxide-android-arm64": ["@tailwindcss/oxide-android-arm64@4.0.9", "", { "os": "android", "cpu": "arm64" }, "sha512-YBgy6+2flE/8dbtrdotVInhMVIxnHJPbAwa7U1gX4l2ThUIaPUp18LjB9wEH8wAGMBZUb//SzLtdXXNBHPUl6Q=="], + + "@tailwindcss/oxide-darwin-arm64": ["@tailwindcss/oxide-darwin-arm64@4.0.9", "", { "os": "darwin", "cpu": "arm64" }, "sha512-pWdl4J2dIHXALgy2jVkwKBmtEb73kqIfMpYmcgESr7oPQ+lbcQ4+tlPeVXaSAmang+vglAfFpXQCOvs/aGSqlw=="], + + "@tailwindcss/oxide-darwin-x64": ["@tailwindcss/oxide-darwin-x64@4.0.9", "", { "os": "darwin", "cpu": "x64" }, "sha512-4Dq3lKp0/C7vrRSkNPtBGVebEyWt9QPPlQctxJ0H3MDyiQYvzVYf8jKow7h5QkWNe8hbatEqljMj/Y0M+ERYJg=="], + + "@tailwindcss/oxide-freebsd-x64": ["@tailwindcss/oxide-freebsd-x64@4.0.9", "", { "os": "freebsd", "cpu": "x64" }, "sha512-k7U1RwRODta8x0uealtVt3RoWAWqA+D5FAOsvVGpYoI6ObgmnzqWW6pnVwz70tL8UZ/QXjeMyiICXyjzB6OGtQ=="], + + "@tailwindcss/oxide-linux-arm-gnueabihf": ["@tailwindcss/oxide-linux-arm-gnueabihf@4.0.9", "", { "os": "linux", "cpu": "arm" }, "sha512-NDDjVweHz2zo4j+oS8y3KwKL5wGCZoXGA9ruJM982uVJLdsF8/1AeKvUwKRlMBpxHt1EdWJSAh8a0Mfhl28GlQ=="], + + "@tailwindcss/oxide-linux-arm64-gnu": ["@tailwindcss/oxide-linux-arm64-gnu@4.0.9", "", { "os": "linux", "cpu": "arm64" }, "sha512-jk90UZ0jzJl3Dy1BhuFfRZ2KP9wVKMXPjmCtY4U6fF2LvrjP5gWFJj5VHzfzHonJexjrGe1lMzgtjriuZkxagg=="], + + "@tailwindcss/oxide-linux-arm64-musl": ["@tailwindcss/oxide-linux-arm64-musl@4.0.9", "", { "os": "linux", "cpu": "arm64" }, "sha512-3eMjyTC6HBxh9nRgOHzrc96PYh1/jWOwHZ3Kk0JN0Kl25BJ80Lj9HEvvwVDNTgPg154LdICwuFLuhfgH9DULmg=="], + + "@tailwindcss/oxide-linux-x64-gnu": ["@tailwindcss/oxide-linux-x64-gnu@4.0.9", "", { "os": "linux", "cpu": "x64" }, "sha512-v0D8WqI/c3WpWH1kq/HP0J899ATLdGZmENa2/emmNjubT0sWtEke9W9+wXeEoACuGAhF9i3PO5MeyditpDCiWQ=="], + + "@tailwindcss/oxide-linux-x64-musl": ["@tailwindcss/oxide-linux-x64-musl@4.0.9", "", { "os": "linux", "cpu": "x64" }, "sha512-Kvp0TCkfeXyeehqLJr7otsc4hd/BUPfcIGrQiwsTVCfaMfjQZCG7DjI+9/QqPZha8YapLA9UoIcUILRYO7NE1Q=="], + + "@tailwindcss/oxide-win32-arm64-msvc": ["@tailwindcss/oxide-win32-arm64-msvc@4.0.9", "", { "os": "win32", "cpu": "arm64" }, "sha512-m3+60T/7YvWekajNq/eexjhV8z10rswcz4BC9bioJ7YaN+7K8W2AmLmG0B79H14m6UHE571qB0XsPus4n0QVgQ=="], + + "@tailwindcss/oxide-win32-x64-msvc": ["@tailwindcss/oxide-win32-x64-msvc@4.0.9", "", { "os": "win32", "cpu": "x64" }, "sha512-dpc05mSlqkwVNOUjGu/ZXd5U1XNch1kHFJ4/cHkZFvaW1RzbHmRt24gvM8/HC6IirMxNarzVw4IXVtvrOoZtxA=="], + + "@tailwindcss/vite": ["@tailwindcss/vite@4.0.9", "", { "dependencies": { "@tailwindcss/node": "4.0.9", "@tailwindcss/oxide": "4.0.9", "lightningcss": "^1.29.1", "tailwindcss": "4.0.9" }, "peerDependencies": { "vite": "^5.2.0 || ^6" } }, "sha512-BIKJO+hwdIsN7V6I7SziMZIVHWWMsV/uCQKYEbeiGRDRld+TkqyRRl9+dQ0MCXbhcVr+D9T/qX2E84kT7V281g=="], + + "@types/cookie": ["@types/cookie@0.6.0", "", {}, "sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA=="], + + "@types/estree": ["@types/estree@1.0.6", "", {}, "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw=="], + + "@types/json-schema": ["@types/json-schema@7.0.15", "", {}, "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="], + + "@types/node": ["@types/node@22.13.5", "", { "dependencies": { "undici-types": "~6.20.0" } }, "sha512-+lTU0PxZXn0Dr1NBtC7Y8cR21AJr87dLLU953CWA6pMxxv/UDc7jYAY90upcrie1nRcD6XNG5HOYEDtgW5TxAg=="], + + "@types/react": ["@types/react@19.0.10", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-JuRQ9KXLEjaUNjTWpzuR231Z2WpIwczOkBEIvbHNCzQefFIT0L8IqE6NV6ULLyC1SI/i234JnDoMkfg+RjQj2g=="], + + "@types/react-dom": ["@types/react-dom@19.0.4", "", { "peerDependencies": { "@types/react": "^19.0.0" } }, "sha512-4fSQ8vWFkg+TGhePfUzVmat3eC14TXYSsiiDSLI0dVLsrm9gZFABjPy/Qu6TKgl1tq1Bu1yDsuQgY3A3DOjCcg=="], + + "@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.25.0", "", { "dependencies": { "@eslint-community/regexpp": "^4.10.0", "@typescript-eslint/scope-manager": "8.25.0", "@typescript-eslint/type-utils": "8.25.0", "@typescript-eslint/utils": "8.25.0", "@typescript-eslint/visitor-keys": "8.25.0", "graphemer": "^1.4.0", "ignore": "^5.3.1", "natural-compare": "^1.4.0", "ts-api-utils": "^2.0.1" }, "peerDependencies": { "@typescript-eslint/parser": "^8.0.0 || ^8.0.0-alpha.0", "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.8.0" } }, "sha512-VM7bpzAe7JO/BFf40pIT1lJqS/z1F8OaSsUB3rpFJucQA4cOSuH2RVVVkFULN+En0Djgr29/jb4EQnedUo95KA=="], + + "@typescript-eslint/parser": ["@typescript-eslint/parser@8.25.0", "", { "dependencies": { "@typescript-eslint/scope-manager": "8.25.0", "@typescript-eslint/types": "8.25.0", "@typescript-eslint/typescript-estree": "8.25.0", "@typescript-eslint/visitor-keys": "8.25.0", "debug": "^4.3.4" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.8.0" } }, "sha512-4gbs64bnbSzu4FpgMiQ1A+D+urxkoJk/kqlDJ2W//5SygaEiAP2B4GoS7TEdxgwol2el03gckFV9lJ4QOMiiHg=="], + + "@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.25.0", "", { "dependencies": { "@typescript-eslint/types": "8.25.0", "@typescript-eslint/visitor-keys": "8.25.0" } }, "sha512-6PPeiKIGbgStEyt4NNXa2ru5pMzQ8OYKO1hX1z53HMomrmiSB+R5FmChgQAP1ro8jMtNawz+TRQo/cSXrauTpg=="], + + "@typescript-eslint/type-utils": ["@typescript-eslint/type-utils@8.25.0", "", { "dependencies": { "@typescript-eslint/typescript-estree": "8.25.0", "@typescript-eslint/utils": "8.25.0", "debug": "^4.3.4", "ts-api-utils": "^2.0.1" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.8.0" } }, "sha512-d77dHgHWnxmXOPJuDWO4FDWADmGQkN5+tt6SFRZz/RtCWl4pHgFl3+WdYCn16+3teG09DY6XtEpf3gGD0a186g=="], + + "@typescript-eslint/types": ["@typescript-eslint/types@8.25.0", "", {}, "sha512-+vUe0Zb4tkNgznQwicsvLUJgZIRs6ITeWSCclX1q85pR1iOiaj+4uZJIUp//Z27QWu5Cseiw3O3AR8hVpax7Aw=="], + + "@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@8.25.0", "", { "dependencies": { "@typescript-eslint/types": "8.25.0", "@typescript-eslint/visitor-keys": "8.25.0", "debug": "^4.3.4", "fast-glob": "^3.3.2", "is-glob": "^4.0.3", "minimatch": "^9.0.4", "semver": "^7.6.0", "ts-api-utils": "^2.0.1" }, "peerDependencies": { "typescript": ">=4.8.4 <5.8.0" } }, "sha512-ZPaiAKEZ6Blt/TPAx5Ot0EIB/yGtLI2EsGoY6F7XKklfMxYQyvtL+gT/UCqkMzO0BVFHLDlzvFqQzurYahxv9Q=="], + + "@typescript-eslint/utils": ["@typescript-eslint/utils@8.25.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.4.0", "@typescript-eslint/scope-manager": "8.25.0", "@typescript-eslint/types": "8.25.0", "@typescript-eslint/typescript-estree": "8.25.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.8.0" } }, "sha512-syqRbrEv0J1wywiLsK60XzHnQe/kRViI3zwFALrNEgnntn1l24Ra2KvOAWwWbWZ1lBZxZljPDGOq967dsl6fkA=="], + + "@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.25.0", "", { "dependencies": { "@typescript-eslint/types": "8.25.0", "eslint-visitor-keys": "^4.2.0" } }, "sha512-kCYXKAum9CecGVHGij7muybDfTS2sD3t0L4bJsEZLkyrXUImiCTq1M3LG2SRtOhiHFwMR9wAFplpT6XHYjTkwQ=="], + + "@vitejs/plugin-react-swc": ["@vitejs/plugin-react-swc@3.8.0", "", { "dependencies": { "@swc/core": "^1.10.15" }, "peerDependencies": { "vite": "^4 || ^5 || ^6" } }, "sha512-T4sHPvS+DIqDP51ifPqa9XIRAz/kIvIi8oXcnOZZgHmMotgmmdxe/DD5tMFlt5nuIRzT0/QuiwmKlH0503Aapw=="], + + "acorn": ["acorn@8.14.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA=="], + + "acorn-jsx": ["acorn-jsx@5.3.2", "", { "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ=="], + + "ajv": ["ajv@6.12.6", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g=="], + + "ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], + + "argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="], + + "balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], + + "brace-expansion": ["brace-expansion@1.1.11", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA=="], + + "braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="], + + "callsites": ["callsites@3.1.0", "", {}, "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ=="], + + "chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], + + "class-variance-authority": ["class-variance-authority@0.7.1", "", { "dependencies": { "clsx": "^2.1.1" } }, "sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg=="], + + "clsx": ["clsx@2.1.1", "", {}, "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA=="], + + "color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="], + + "color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="], + + "concat-map": ["concat-map@0.0.1", "", {}, "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="], + + "cookie": ["cookie@1.0.2", "", {}, "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA=="], + + "cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="], + + "csstype": ["csstype@3.1.3", "", {}, "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="], + + "debug": ["debug@4.4.0", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA=="], + + "deep-is": ["deep-is@0.1.4", "", {}, "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ=="], + + "destr": ["destr@2.0.3", "", {}, "sha512-2N3BOUU4gYMpTP24s5rF5iP7BDr7uNTCs4ozw3kf/eKfvWSIu93GEBi5m427YoyJoeOzQ5smuu4nNAPGb8idSQ=="], + + "detect-libc": ["detect-libc@1.0.3", "", { "bin": { "detect-libc": "./bin/detect-libc.js" } }, "sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg=="], + + "enhanced-resolve": ["enhanced-resolve@5.18.1", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.2.0" } }, "sha512-ZSW3ma5GkcQBIpwZTSRAI8N71Uuwgs93IezB7mf7R60tC8ZbJideoDNKjHn2O9KIlx6rkGTTEk1xUCK2E1Y2Yg=="], + + "esbuild": ["esbuild@0.25.0", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.0", "@esbuild/android-arm": "0.25.0", "@esbuild/android-arm64": "0.25.0", "@esbuild/android-x64": "0.25.0", "@esbuild/darwin-arm64": "0.25.0", "@esbuild/darwin-x64": "0.25.0", "@esbuild/freebsd-arm64": "0.25.0", "@esbuild/freebsd-x64": "0.25.0", "@esbuild/linux-arm": "0.25.0", "@esbuild/linux-arm64": "0.25.0", "@esbuild/linux-ia32": "0.25.0", "@esbuild/linux-loong64": "0.25.0", "@esbuild/linux-mips64el": "0.25.0", "@esbuild/linux-ppc64": "0.25.0", "@esbuild/linux-riscv64": "0.25.0", "@esbuild/linux-s390x": "0.25.0", "@esbuild/linux-x64": "0.25.0", "@esbuild/netbsd-arm64": "0.25.0", "@esbuild/netbsd-x64": "0.25.0", "@esbuild/openbsd-arm64": "0.25.0", "@esbuild/openbsd-x64": "0.25.0", "@esbuild/sunos-x64": "0.25.0", "@esbuild/win32-arm64": "0.25.0", "@esbuild/win32-ia32": "0.25.0", "@esbuild/win32-x64": "0.25.0" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-BXq5mqc8ltbaN34cDqWuYKyNhX8D/Z0J1xdtdQ8UcIIIyJyz+ZMKUt58tF3SrZ85jcfN/PZYhjR5uDQAYNVbuw=="], + + "escape-string-regexp": ["escape-string-regexp@4.0.0", "", {}, "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="], + + "eslint": ["eslint@9.21.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.12.1", "@eslint/config-array": "^0.19.2", "@eslint/core": "^0.12.0", "@eslint/eslintrc": "^3.3.0", "@eslint/js": "9.21.0", "@eslint/plugin-kit": "^0.2.7", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", "@types/estree": "^1.0.6", "@types/json-schema": "^7.0.15", "ajv": "^6.12.4", "chalk": "^4.0.0", "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", "eslint-scope": "^8.2.0", "eslint-visitor-keys": "^4.2.0", "espree": "^10.3.0", "esquery": "^1.5.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^8.0.0", "find-up": "^5.0.0", "glob-parent": "^6.0.2", "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "json-stable-stringify-without-jsonify": "^1.0.1", "lodash.merge": "^4.6.2", "minimatch": "^3.1.2", "natural-compare": "^1.4.0", "optionator": "^0.9.3" }, "peerDependencies": { "jiti": "*" }, "optionalPeers": ["jiti"], "bin": { "eslint": "bin/eslint.js" } }, "sha512-KjeihdFqTPhOMXTt7StsDxriV4n66ueuF/jfPNC3j/lduHwr/ijDwJMsF+wyMJethgiKi5wniIE243vi07d3pg=="], + + "eslint-plugin-react-hooks": ["eslint-plugin-react-hooks@5.1.0", "", { "peerDependencies": { "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0" } }, "sha512-mpJRtPgHN2tNAvZ35AMfqeB3Xqeo273QxrHJsbBEPWODRM4r0yB6jfoROqKEYrOn27UtRPpcpHc2UqyBSuUNTw=="], + + "eslint-plugin-react-refresh": ["eslint-plugin-react-refresh@0.4.19", "", { "peerDependencies": { "eslint": ">=8.40" } }, "sha512-eyy8pcr/YxSYjBoqIFSrlbn9i/xvxUFa8CjzAYo9cFjgGXqq1hyjihcpZvxRLalpaWmueWR81xn7vuKmAFijDQ=="], + + "eslint-scope": ["eslint-scope@8.2.0", "", { "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^5.2.0" } }, "sha512-PHlWUfG6lvPc3yvP5A4PNyBL1W8fkDUccmI21JUu/+GKZBoH/W5u6usENXUrWFRsyoW5ACUjFGgAFQp5gUlb/A=="], + + "eslint-visitor-keys": ["eslint-visitor-keys@4.2.0", "", {}, "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw=="], + + "espree": ["espree@10.3.0", "", { "dependencies": { "acorn": "^8.14.0", "acorn-jsx": "^5.3.2", "eslint-visitor-keys": "^4.2.0" } }, "sha512-0QYC8b24HWY8zjRnDTL6RiHfDbAWn63qb4LMj1Z4b076A4une81+z03Kg7l7mn/48PUTqoLptSXez8oknU8Clg=="], + + "esquery": ["esquery@1.6.0", "", { "dependencies": { "estraverse": "^5.1.0" } }, "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg=="], + + "esrecurse": ["esrecurse@4.3.0", "", { "dependencies": { "estraverse": "^5.2.0" } }, "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag=="], + + "estraverse": ["estraverse@5.3.0", "", {}, "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA=="], + + "esutils": ["esutils@2.0.3", "", {}, "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g=="], + + "fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="], + + "fast-glob": ["fast-glob@3.3.3", "", { "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", "glob-parent": "^5.1.2", "merge2": "^1.3.0", "micromatch": "^4.0.8" } }, "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg=="], + + "fast-json-stable-stringify": ["fast-json-stable-stringify@2.1.0", "", {}, "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw=="], + + "fast-levenshtein": ["fast-levenshtein@2.0.6", "", {}, "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw=="], + + "fastq": ["fastq@1.19.1", "", { "dependencies": { "reusify": "^1.0.4" } }, "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ=="], + + "file-entry-cache": ["file-entry-cache@8.0.0", "", { "dependencies": { "flat-cache": "^4.0.0" } }, "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ=="], + + "fill-range": ["fill-range@7.1.1", "", { "dependencies": { "to-regex-range": "^5.0.1" } }, "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg=="], + + "find-up": ["find-up@5.0.0", "", { "dependencies": { "locate-path": "^6.0.0", "path-exists": "^4.0.0" } }, "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng=="], + + "flat-cache": ["flat-cache@4.0.1", "", { "dependencies": { "flatted": "^3.2.9", "keyv": "^4.5.4" } }, "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw=="], + + "flatted": ["flatted@3.3.3", "", {}, "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg=="], + + "fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="], + + "glob-parent": ["glob-parent@6.0.2", "", { "dependencies": { "is-glob": "^4.0.3" } }, "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A=="], + + "globals": ["globals@15.15.0", "", {}, "sha512-7ACyT3wmyp3I61S4fG682L0VA2RGD9otkqGJIwNUMF1SWUombIIk+af1unuDYgMm082aHYwD+mzJvv9Iu8dsgg=="], + + "globrex": ["globrex@0.1.2", "", {}, "sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg=="], + + "graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="], + + "graphemer": ["graphemer@1.4.0", "", {}, "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag=="], + + "has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="], + + "ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="], + + "import-fresh": ["import-fresh@3.3.1", "", { "dependencies": { "parent-module": "^1.0.0", "resolve-from": "^4.0.0" } }, "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ=="], + + "imurmurhash": ["imurmurhash@0.1.4", "", {}, "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA=="], + + "is-extglob": ["is-extglob@2.1.1", "", {}, "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ=="], + + "is-glob": ["is-glob@4.0.3", "", { "dependencies": { "is-extglob": "^2.1.1" } }, "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg=="], + + "is-number": ["is-number@7.0.0", "", {}, "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng=="], + + "isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="], + + "jiti": ["jiti@2.4.2", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-rg9zJN+G4n2nfJl5MW3BMygZX56zKPNVEYYqq7adpmMh4Jn2QNEwhvQlFy6jPVdcod7txZtKHWnyZiA3a0zP7A=="], + + "js-yaml": ["js-yaml@4.1.0", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA=="], + + "json-buffer": ["json-buffer@3.0.1", "", {}, "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ=="], + + "json-schema-traverse": ["json-schema-traverse@0.4.1", "", {}, "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="], + + "json-stable-stringify-without-jsonify": ["json-stable-stringify-without-jsonify@1.0.1", "", {}, "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw=="], + + "keyv": ["keyv@4.5.4", "", { "dependencies": { "json-buffer": "3.0.1" } }, "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw=="], + + "levn": ["levn@0.4.1", "", { "dependencies": { "prelude-ls": "^1.2.1", "type-check": "~0.4.0" } }, "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ=="], + + "lightningcss": ["lightningcss@1.29.1", "", { "dependencies": { "detect-libc": "^1.0.3" }, "optionalDependencies": { "lightningcss-darwin-arm64": "1.29.1", "lightningcss-darwin-x64": "1.29.1", "lightningcss-freebsd-x64": "1.29.1", "lightningcss-linux-arm-gnueabihf": "1.29.1", "lightningcss-linux-arm64-gnu": "1.29.1", "lightningcss-linux-arm64-musl": "1.29.1", "lightningcss-linux-x64-gnu": "1.29.1", "lightningcss-linux-x64-musl": "1.29.1", "lightningcss-win32-arm64-msvc": "1.29.1", "lightningcss-win32-x64-msvc": "1.29.1" } }, "sha512-FmGoeD4S05ewj+AkhTY+D+myDvXI6eL27FjHIjoyUkO/uw7WZD1fBVs0QxeYWa7E17CUHJaYX/RUGISCtcrG4Q=="], + + "lightningcss-darwin-arm64": ["lightningcss-darwin-arm64@1.29.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-HtR5XJ5A0lvCqYAoSv2QdZZyoHNttBpa5EP9aNuzBQeKGfbyH5+UipLWvVzpP4Uml5ej4BYs5I9Lco9u1fECqw=="], + + "lightningcss-darwin-x64": ["lightningcss-darwin-x64@1.29.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-k33G9IzKUpHy/J/3+9MCO4e+PzaFblsgBjSGlpAaFikeBFm8B/CkO3cKU9oI4g+fjS2KlkLM/Bza9K/aw8wsNA=="], + + "lightningcss-freebsd-x64": ["lightningcss-freebsd-x64@1.29.1", "", { "os": "freebsd", "cpu": "x64" }, "sha512-0SUW22fv/8kln2LnIdOCmSuXnxgxVC276W5KLTwoehiO0hxkacBxjHOL5EtHD8BAXg2BvuhsJPmVMasvby3LiQ=="], + + "lightningcss-linux-arm-gnueabihf": ["lightningcss-linux-arm-gnueabihf@1.29.1", "", { "os": "linux", "cpu": "arm" }, "sha512-sD32pFvlR0kDlqsOZmYqH/68SqUMPNj+0pucGxToXZi4XZgZmqeX/NkxNKCPsswAXU3UeYgDSpGhu05eAufjDg=="], + + "lightningcss-linux-arm64-gnu": ["lightningcss-linux-arm64-gnu@1.29.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-0+vClRIZ6mmJl/dxGuRsE197o1HDEeeRk6nzycSy2GofC2JsY4ifCRnvUWf/CUBQmlrvMzt6SMQNMSEu22csWQ=="], + + "lightningcss-linux-arm64-musl": ["lightningcss-linux-arm64-musl@1.29.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-UKMFrG4rL/uHNgelBsDwJcBqVpzNJbzsKkbI3Ja5fg00sgQnHw/VrzUTEc4jhZ+AN2BvQYz/tkHu4vt1kLuJyw=="], + + "lightningcss-linux-x64-gnu": ["lightningcss-linux-x64-gnu@1.29.1", "", { "os": "linux", "cpu": "x64" }, "sha512-u1S+xdODy/eEtjADqirA774y3jLcm8RPtYztwReEXoZKdzgsHYPl0s5V52Tst+GKzqjebkULT86XMSxejzfISw=="], + + "lightningcss-linux-x64-musl": ["lightningcss-linux-x64-musl@1.29.1", "", { "os": "linux", "cpu": "x64" }, "sha512-L0Tx0DtaNUTzXv0lbGCLB/c/qEADanHbu4QdcNOXLIe1i8i22rZRpbT3gpWYsCh9aSL9zFujY/WmEXIatWvXbw=="], + + "lightningcss-win32-arm64-msvc": ["lightningcss-win32-arm64-msvc@1.29.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-QoOVnkIEFfbW4xPi+dpdft/zAKmgLgsRHfJalEPYuJDOWf7cLQzYg0DEh8/sn737FaeMJxHZRc1oBreiwZCjog=="], + + "lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.29.1", "", { "os": "win32", "cpu": "x64" }, "sha512-NygcbThNBe4JElP+olyTI/doBNGJvLs3bFCRPdvuCcxZCcCZ71B858IHpdm7L1btZex0FvCmM17FK98Y9MRy1Q=="], + + "locate-path": ["locate-path@6.0.0", "", { "dependencies": { "p-locate": "^5.0.0" } }, "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw=="], + + "lodash.merge": ["lodash.merge@4.6.2", "", {}, "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ=="], + + "lucide-react": ["lucide-react@0.476.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-x6cLTk8gahdUPje0hSgLN1/MgiJH+Xl90Xoxy9bkPAsMPOUiyRSKR4JCDPGVCEpyqnZXH3exFWNItcvra9WzUQ=="], + + "merge2": ["merge2@1.4.1", "", {}, "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg=="], + + "micromatch": ["micromatch@4.0.8", "", { "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1" } }, "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA=="], + + "minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="], + + "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], + + "nanoid": ["nanoid@3.3.8", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w=="], + + "natural-compare": ["natural-compare@1.4.0", "", {}, "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw=="], + + "node-fetch-native": ["node-fetch-native@1.6.6", "", {}, "sha512-8Mc2HhqPdlIfedsuZoc3yioPuzp6b+L5jRCRY1QzuWZh2EGJVQrGppC6V6cF0bLdbW0+O2YpqCA25aF/1lvipQ=="], + + "ofetch": ["ofetch@1.4.1", "", { "dependencies": { "destr": "^2.0.3", "node-fetch-native": "^1.6.4", "ufo": "^1.5.4" } }, "sha512-QZj2DfGplQAr2oj9KzceK9Hwz6Whxazmn85yYeVuS3u9XTMOGMRx0kO95MQ+vLsj/S/NwBDMMLU5hpxvI6Tklw=="], + + "optionator": ["optionator@0.9.4", "", { "dependencies": { "deep-is": "^0.1.3", "fast-levenshtein": "^2.0.6", "levn": "^0.4.1", "prelude-ls": "^1.2.1", "type-check": "^0.4.0", "word-wrap": "^1.2.5" } }, "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g=="], + + "p-limit": ["p-limit@3.1.0", "", { "dependencies": { "yocto-queue": "^0.1.0" } }, "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ=="], + + "p-locate": ["p-locate@5.0.0", "", { "dependencies": { "p-limit": "^3.0.2" } }, "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw=="], + + "parent-module": ["parent-module@1.0.1", "", { "dependencies": { "callsites": "^3.0.0" } }, "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g=="], + + "path-exists": ["path-exists@4.0.0", "", {}, "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w=="], + + "path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="], + + "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="], + + "picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], + + "postcss": ["postcss@8.5.3", "", { "dependencies": { "nanoid": "^3.3.8", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A=="], + + "prelude-ls": ["prelude-ls@1.2.1", "", {}, "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g=="], + + "prettier": ["prettier@3.5.2", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-lc6npv5PH7hVqozBR7lkBNOGXV9vMwROAPlumdBkX0wTbbzPu/U1hk5yL8p2pt4Xoc+2mkT8t/sow2YrV/M5qg=="], + + "prettier-plugin-tailwindcss": ["prettier-plugin-tailwindcss@0.6.11", "", { "peerDependencies": { "@ianvs/prettier-plugin-sort-imports": "*", "@prettier/plugin-pug": "*", "@shopify/prettier-plugin-liquid": "*", "@trivago/prettier-plugin-sort-imports": "*", "@zackad/prettier-plugin-twig": "*", "prettier": "^3.0", "prettier-plugin-astro": "*", "prettier-plugin-css-order": "*", "prettier-plugin-import-sort": "*", "prettier-plugin-jsdoc": "*", "prettier-plugin-marko": "*", "prettier-plugin-multiline-arrays": "*", "prettier-plugin-organize-attributes": "*", "prettier-plugin-organize-imports": "*", "prettier-plugin-sort-imports": "*", "prettier-plugin-style-order": "*", "prettier-plugin-svelte": "*" }, "optionalPeers": ["@ianvs/prettier-plugin-sort-imports", "@prettier/plugin-pug", "@shopify/prettier-plugin-liquid", "@trivago/prettier-plugin-sort-imports", "@zackad/prettier-plugin-twig", "prettier-plugin-astro", "prettier-plugin-css-order", "prettier-plugin-import-sort", "prettier-plugin-jsdoc", "prettier-plugin-marko", "prettier-plugin-multiline-arrays", "prettier-plugin-organize-attributes", "prettier-plugin-organize-imports", "prettier-plugin-sort-imports", "prettier-plugin-style-order", "prettier-plugin-svelte"] }, "sha512-YxaYSIvZPAqhrrEpRtonnrXdghZg1irNg4qrjboCXrpybLWVs55cW2N3juhspVJiO0JBvYJT8SYsJpc8OQSnsA=="], + + "punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="], + + "queue-microtask": ["queue-microtask@1.2.3", "", {}, "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A=="], + + "react": ["react@19.0.0", "", {}, "sha512-V8AVnmPIICiWpGfm6GLzCR/W5FXLchHop40W4nXBmdlEceh16rCN8O8LNWm5bh5XUX91fh7KpA+W0TgMKmgTpQ=="], + + "react-dom": ["react-dom@19.0.0", "", { "dependencies": { "scheduler": "^0.25.0" }, "peerDependencies": { "react": "^19.0.0" } }, "sha512-4GV5sHFG0e/0AD4X+ySy6UJd3jVl1iNsNHdpad0qhABJ11twS3TTBnseqsKurKcsNqCEFeGL3uLpVChpIO3QfQ=="], + + "react-router": ["react-router@7.2.0", "", { "dependencies": { "@types/cookie": "^0.6.0", "cookie": "^1.0.1", "set-cookie-parser": "^2.6.0", "turbo-stream": "2.4.0" }, "peerDependencies": { "react": ">=18", "react-dom": ">=18" }, "optionalPeers": ["react-dom"] }, "sha512-fXyqzPgCPZbqhrk7k3hPcCpYIlQ2ugIXDboHUzhJISFVy2DEPsmHgN588MyGmkIOv3jDgNfUE3kJi83L28s/LQ=="], + + "resolve-from": ["resolve-from@4.0.0", "", {}, "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g=="], + + "reusify": ["reusify@1.1.0", "", {}, "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw=="], + + "rollup": ["rollup@4.34.8", "", { "dependencies": { "@types/estree": "1.0.6" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.34.8", "@rollup/rollup-android-arm64": "4.34.8", "@rollup/rollup-darwin-arm64": "4.34.8", "@rollup/rollup-darwin-x64": "4.34.8", "@rollup/rollup-freebsd-arm64": "4.34.8", "@rollup/rollup-freebsd-x64": "4.34.8", "@rollup/rollup-linux-arm-gnueabihf": "4.34.8", "@rollup/rollup-linux-arm-musleabihf": "4.34.8", "@rollup/rollup-linux-arm64-gnu": "4.34.8", "@rollup/rollup-linux-arm64-musl": "4.34.8", "@rollup/rollup-linux-loongarch64-gnu": "4.34.8", "@rollup/rollup-linux-powerpc64le-gnu": "4.34.8", "@rollup/rollup-linux-riscv64-gnu": "4.34.8", "@rollup/rollup-linux-s390x-gnu": "4.34.8", "@rollup/rollup-linux-x64-gnu": "4.34.8", "@rollup/rollup-linux-x64-musl": "4.34.8", "@rollup/rollup-win32-arm64-msvc": "4.34.8", "@rollup/rollup-win32-ia32-msvc": "4.34.8", "@rollup/rollup-win32-x64-msvc": "4.34.8", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-489gTVMzAYdiZHFVA/ig/iYFllCcWFHMvUHI1rpFmkoUtRlQxqh6/yiNqnYibjMZ2b/+FUQwldG+aLsEt6bglQ=="], + + "run-parallel": ["run-parallel@1.2.0", "", { "dependencies": { "queue-microtask": "^1.2.2" } }, "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA=="], + + "scheduler": ["scheduler@0.25.0", "", {}, "sha512-xFVuu11jh+xcO7JOAGJNOXld8/TcEHK/4CituBUeUb5hqxJLj9YuemAEuvm9gQ/+pgXYfbQuqAkiYu+u7YEsNA=="], + + "semver": ["semver@7.7.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA=="], + + "set-cookie-parser": ["set-cookie-parser@2.7.1", "", {}, "sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ=="], + + "shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="], + + "shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="], + + "source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="], + + "strip-json-comments": ["strip-json-comments@3.1.1", "", {}, "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig=="], + + "supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], + + "tailwind-merge": ["tailwind-merge@3.0.2", "", {}, "sha512-l7z+OYZ7mu3DTqrL88RiKrKIqO3NcpEO8V/Od04bNpvk0kiIFndGEoqfuzvj4yuhRkHKjRkII2z+KS2HfPcSxw=="], + + "tailwindcss": ["tailwindcss@4.0.9", "", {}, "sha512-12laZu+fv1ONDRoNR9ipTOpUD7RN9essRVkX36sjxuRUInpN7hIiHN4lBd/SIFjbISvnXzp8h/hXzmU8SQQYhw=="], + + "tailwindcss-animate": ["tailwindcss-animate@1.0.7", "", { "peerDependencies": { "tailwindcss": ">=3.0.0 || insiders" } }, "sha512-bl6mpH3T7I3UFxuvDEXLxy/VuFxBk5bbzplh7tXI68mwMokNYd1t9qPBHlnyTwfa4JGC4zP516I1hYYtQ/vspA=="], + + "tapable": ["tapable@2.2.1", "", {}, "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ=="], + + "to-regex-range": ["to-regex-range@5.0.1", "", { "dependencies": { "is-number": "^7.0.0" } }, "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ=="], + + "ts-api-utils": ["ts-api-utils@2.0.1", "", { "peerDependencies": { "typescript": ">=4.8.4" } }, "sha512-dnlgjFSVetynI8nzgJ+qF62efpglpWRk8isUEWZGWlJYySCTD6aKvbUDu+zbPeDakk3bg5H4XpitHukgfL1m9w=="], + + "tsconfck": ["tsconfck@3.1.5", "", { "peerDependencies": { "typescript": "^5.0.0" }, "optionalPeers": ["typescript"], "bin": { "tsconfck": "bin/tsconfck.js" } }, "sha512-CLDfGgUp7XPswWnezWwsCRxNmgQjhYq3VXHM0/XIRxhVrKw0M1if9agzryh1QS3nxjCROvV+xWxoJO1YctzzWg=="], + + "turbo-stream": ["turbo-stream@2.4.0", "", {}, "sha512-FHncC10WpBd2eOmGwpmQsWLDoK4cqsA/UT/GqNoaKOQnT8uzhtCbg3EoUDMvqpOSAI0S26mr0rkjzbOO6S3v1g=="], + + "type-check": ["type-check@0.4.0", "", { "dependencies": { "prelude-ls": "^1.2.1" } }, "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew=="], + + "typescript": ["typescript@5.7.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw=="], + + "typescript-eslint": ["typescript-eslint@8.25.0", "", { "dependencies": { "@typescript-eslint/eslint-plugin": "8.25.0", "@typescript-eslint/parser": "8.25.0", "@typescript-eslint/utils": "8.25.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.8.0" } }, "sha512-TxRdQQLH4g7JkoFlYG3caW5v1S6kEkz8rqt80iQJZUYPq1zD1Ra7HfQBJJ88ABRaMvHAXnwRvRB4V+6sQ9xN5Q=="], + + "ufo": ["ufo@1.5.4", "", {}, "sha512-UsUk3byDzKd04EyoZ7U4DOlxQaD14JUKQl6/P7wiX4FNvUfm3XL246n9W5AmqwW5RSFJ27NAuM0iLscAOYUiGQ=="], + + "undici-types": ["undici-types@6.20.0", "", {}, "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg=="], + + "uri-js": ["uri-js@4.4.1", "", { "dependencies": { "punycode": "^2.1.0" } }, "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg=="], + + "vite": ["vite@6.2.0", "", { "dependencies": { "esbuild": "^0.25.0", "postcss": "^8.5.3", "rollup": "^4.30.1" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", "jiti": ">=1.21.0", "less": "*", "lightningcss": "^1.21.0", "sass": "*", "sass-embedded": "*", "stylus": "*", "sugarss": "*", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-7dPxoo+WsT/64rDcwoOjk76XHj+TqNTIvHKcuMQ1k4/SeHDaQt5GFAeLYzrimZrMpn/O6DtdI03WUjdxuPM0oQ=="], + + "vite-tsconfig-paths": ["vite-tsconfig-paths@5.1.4", "", { "dependencies": { "debug": "^4.1.1", "globrex": "^0.1.2", "tsconfck": "^3.0.3" }, "peerDependencies": { "vite": "*" }, "optionalPeers": ["vite"] }, "sha512-cYj0LRuLV2c2sMqhqhGpaO3LretdtMn/BVX4cPLanIZuwwrkVl+lK84E/miEXkCHWXuq65rhNN4rXsBcOB3S4w=="], + + "which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="], + + "word-wrap": ["word-wrap@1.2.5", "", {}, "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA=="], + + "yocto-queue": ["yocto-queue@0.1.0", "", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="], + + "@eslint-community/eslint-utils/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="], + + "@eslint/eslintrc/globals": ["globals@14.0.0", "", {}, "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ=="], + + "@humanfs/node/@humanwhocodes/retry": ["@humanwhocodes/retry@0.3.1", "", {}, "sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA=="], + + "@typescript-eslint/typescript-estree/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="], + + "fast-glob/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="], + + "@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@2.0.1", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA=="], + } +} diff --git a/services/frontend/components.json b/services/frontend/components.json new file mode 100644 index 0000000..27a2047 --- /dev/null +++ b/services/frontend/components.json @@ -0,0 +1,21 @@ +{ + "$schema": "https://ui.shadcn.com/schema.json", + "style": "new-york", + "rsc": false, + "tsx": true, + "tailwind": { + "config": "", + "css": "src/styles/globals.css", + "baseColor": "neutral", + "cssVariables": true, + "prefix": "" + }, + "aliases": { + "components": "@/components", + "utils": "@/shared/lib/utils", + "ui": "@/components/ui", + "lib": "@/shared/lib", + "hooks": "@/shared/hooks" + }, + "iconLibrary": "lucide" +} diff --git a/services/frontend/eslint.config.js b/services/frontend/eslint.config.js new file mode 100644 index 0000000..faa2e37 --- /dev/null +++ b/services/frontend/eslint.config.js @@ -0,0 +1,25 @@ +import js from "@eslint/js"; +import globals from "globals"; +import reactHooks from "eslint-plugin-react-hooks"; +import reactRefresh from "eslint-plugin-react-refresh"; +import tseslint from "typescript-eslint"; + +export default tseslint.config( + { ignores: ["dist"] }, + { + extends: [js.configs.recommended, ...tseslint.configs.recommended], + files: ["**/*.{ts,tsx}"], + languageOptions: { + ecmaVersion: 2020, + globals: globals.browser, + }, + plugins: { + "react-hooks": reactHooks, + "react-refresh": reactRefresh, + }, + rules: { + ...reactHooks.configs.recommended.rules, + "react-refresh/only-export-components": null, + }, + }, +); diff --git a/services/frontend/index.html b/services/frontend/index.html new file mode 100644 index 0000000..b687788 --- /dev/null +++ b/services/frontend/index.html @@ -0,0 +1,13 @@ + + + + + + + мегазордпобеда.рф + + +
+ + + diff --git a/services/frontend/package.json b/services/frontend/package.json new file mode 100644 index 0000000..59286e8 --- /dev/null +++ b/services/frontend/package.json @@ -0,0 +1,43 @@ +{ + "name": "frontend", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite --host", + "build": "tsc -b && vite build", + "lint": "eslint .", + "preview": "vite preview" + }, + "dependencies": { + "@radix-ui/react-slot": "^1.1.2", + "@tailwindcss/vite": "^4.0.9", + "class-variance-authority": "^0.7.1", + "clsx": "^2.1.1", + "lucide-react": "^0.476.0", + "ofetch": "^1.4.1", + "react": "^19.0.0", + "react-dom": "^19.0.0", + "react-router": "^7.2.0", + "tailwind-merge": "^3.0.2", + "tailwindcss": "^4.0.9", + "tailwindcss-animate": "^1.0.7" + }, + "devDependencies": { + "@eslint/js": "^9.21.0", + "@types/node": "^22.13.5", + "@types/react": "^19.0.10", + "@types/react-dom": "^19.0.4", + "@vitejs/plugin-react-swc": "^3.8.0", + "eslint": "^9.21.0", + "eslint-plugin-react-hooks": "^5.0.0", + "eslint-plugin-react-refresh": "^0.4.19", + "globals": "^15.15.0", + "prettier": "^3.5.2", + "prettier-plugin-tailwindcss": "^0.6.11", + "typescript": "~5.7.2", + "typescript-eslint": "^8.24.1", + "vite": "^6.2.0", + "vite-tsconfig-paths": "^5.1.4" + } +} diff --git a/services/frontend/public/logo.svg b/services/frontend/public/logo.svg new file mode 100644 index 0000000..c7881dd --- /dev/null +++ b/services/frontend/public/logo.svg @@ -0,0 +1,93 @@ + + + + diff --git a/services/frontend/src/App.tsx b/services/frontend/src/App.tsx new file mode 100644 index 0000000..daf09bc --- /dev/null +++ b/services/frontend/src/App.tsx @@ -0,0 +1,6 @@ +import { Routes } from "react-router"; +import "./styles/globals.css"; + +export default function App() { + return ; +} diff --git a/services/frontend/src/components/ui/button.tsx b/services/frontend/src/components/ui/button.tsx new file mode 100644 index 0000000..49dcad9 --- /dev/null +++ b/services/frontend/src/components/ui/button.tsx @@ -0,0 +1,58 @@ +import * as React from "react"; +import { Slot } from "@radix-ui/react-slot"; +import { cva, type VariantProps } from "class-variance-authority"; + +import { cn } from "@/shared/lib/utils"; + +const buttonVariants = cva( + "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-[color,box-shadow] disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive", + { + variants: { + variant: { + default: + "bg-primary text-primary-foreground shadow-xs hover:bg-primary/90", + destructive: + "bg-destructive text-white shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40", + outline: + "border border-input bg-background shadow-xs hover:bg-accent hover:text-accent-foreground", + secondary: + "bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80", + ghost: "hover:bg-accent hover:text-accent-foreground", + link: "text-primary underline-offset-4 hover:underline", + }, + size: { + default: "h-9 px-4 py-2 has-[>svg]:px-3", + sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5", + lg: "h-10 rounded-md px-6 has-[>svg]:px-4", + icon: "size-9", + }, + }, + defaultVariants: { + variant: "default", + size: "default", + }, + }, +); + +function Button({ + className, + variant, + size, + asChild = false, + ...props +}: React.ComponentProps<"button"> & + VariantProps & { + asChild?: boolean; + }) { + const Comp = asChild ? Slot : "button"; + + return ( + + ); +} + +export { Button, buttonVariants }; diff --git a/services/frontend/src/main.tsx b/services/frontend/src/main.tsx new file mode 100644 index 0000000..f046499 --- /dev/null +++ b/services/frontend/src/main.tsx @@ -0,0 +1,12 @@ +import { StrictMode } from "react"; +import { createRoot } from "react-dom/client"; +import { BrowserRouter } from "react-router"; +import App from "./App.tsx"; + +createRoot(document.getElementById("root")!).render( + + + + + , +); diff --git a/services/frontend/src/pages/.gitkeep b/services/frontend/src/pages/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/services/frontend/src/shared/lib/utils.ts b/services/frontend/src/shared/lib/utils.ts new file mode 100644 index 0000000..bd0c391 --- /dev/null +++ b/services/frontend/src/shared/lib/utils.ts @@ -0,0 +1,6 @@ +import { clsx, type ClassValue } from "clsx" +import { twMerge } from "tailwind-merge" + +export function cn(...inputs: ClassValue[]) { + return twMerge(clsx(inputs)) +} diff --git a/services/frontend/src/styles/globals.css b/services/frontend/src/styles/globals.css new file mode 100644 index 0000000..1f613d0 --- /dev/null +++ b/services/frontend/src/styles/globals.css @@ -0,0 +1,124 @@ +@import "tailwindcss"; + +@plugin "tailwindcss-animate"; + +@custom-variant dark (&:is(.dark *)); + +:root { + --background: oklch(1 0 0); + --foreground: oklch(0.145 0 0); + --card: oklch(1 0 0); + --card-foreground: oklch(0.145 0 0); + --popover: oklch(1 0 0); + --popover-foreground: oklch(0.145 0 0); + --primary: oklch(0.205 0 0); + --primary-foreground: oklch(0.985 0 0); + --secondary: oklch(0.97 0 0); + --secondary-foreground: oklch(0.205 0 0); + --muted: oklch(0.97 0 0); + --muted-foreground: oklch(0.556 0 0); + --accent: oklch(0.97 0 0); + --accent-foreground: oklch(0.205 0 0); + --destructive: oklch(0.577 0.245 27.325); + --destructive-foreground: oklch(0.577 0.245 27.325); + --border: oklch(0.922 0 0); + --input: oklch(0.922 0 0); + --ring: oklch(0.87 0 0); + --chart-1: oklch(0.646 0.222 41.116); + --chart-2: oklch(0.6 0.118 184.704); + --chart-3: oklch(0.398 0.07 227.392); + --chart-4: oklch(0.828 0.189 84.429); + --chart-5: oklch(0.769 0.188 70.08); + --radius: 0.625rem; + --sidebar: oklch(0.985 0 0); + --sidebar-foreground: oklch(0.145 0 0); + --sidebar-primary: oklch(0.205 0 0); + --sidebar-primary-foreground: oklch(0.985 0 0); + --sidebar-accent: oklch(0.97 0 0); + --sidebar-accent-foreground: oklch(0.205 0 0); + --sidebar-border: oklch(0.922 0 0); + --sidebar-ring: oklch(0.87 0 0); +} + +.dark { + --background: oklch(0.145 0 0); + --foreground: oklch(0.985 0 0); + --card: oklch(0.145 0 0); + --card-foreground: oklch(0.985 0 0); + --popover: oklch(0.145 0 0); + --popover-foreground: oklch(0.985 0 0); + --primary: oklch(0.985 0 0); + --primary-foreground: oklch(0.205 0 0); + --secondary: oklch(0.269 0 0); + --secondary-foreground: oklch(0.985 0 0); + --muted: oklch(0.269 0 0); + --muted-foreground: oklch(0.708 0 0); + --accent: oklch(0.269 0 0); + --accent-foreground: oklch(0.985 0 0); + --destructive: oklch(0.396 0.141 25.723); + --destructive-foreground: oklch(0.637 0.237 25.331); + --border: oklch(0.269 0 0); + --input: oklch(0.269 0 0); + --ring: oklch(0.439 0 0); + --chart-1: oklch(0.488 0.243 264.376); + --chart-2: oklch(0.696 0.17 162.48); + --chart-3: oklch(0.769 0.188 70.08); + --chart-4: oklch(0.627 0.265 303.9); + --chart-5: oklch(0.645 0.246 16.439); + --sidebar: oklch(0.205 0 0); + --sidebar-foreground: oklch(0.985 0 0); + --sidebar-primary: oklch(0.488 0.243 264.376); + --sidebar-primary-foreground: oklch(0.985 0 0); + --sidebar-accent: oklch(0.269 0 0); + --sidebar-accent-foreground: oklch(0.985 0 0); + --sidebar-border: oklch(0.269 0 0); + --sidebar-ring: oklch(0.439 0 0); +} + +@theme inline { + --color-background: var(--background); + --color-foreground: var(--foreground); + --color-card: var(--card); + --color-card-foreground: var(--card-foreground); + --color-popover: var(--popover); + --color-popover-foreground: var(--popover-foreground); + --color-primary: var(--primary); + --color-primary-foreground: var(--primary-foreground); + --color-secondary: var(--secondary); + --color-secondary-foreground: var(--secondary-foreground); + --color-muted: var(--muted); + --color-muted-foreground: var(--muted-foreground); + --color-accent: var(--accent); + --color-accent-foreground: var(--accent-foreground); + --color-destructive: var(--destructive); + --color-destructive-foreground: var(--destructive-foreground); + --color-border: var(--border); + --color-input: var(--input); + --color-ring: var(--ring); + --color-chart-1: var(--chart-1); + --color-chart-2: var(--chart-2); + --color-chart-3: var(--chart-3); + --color-chart-4: var(--chart-4); + --color-chart-5: var(--chart-5); + --radius-sm: calc(var(--radius) - 4px); + --radius-md: calc(var(--radius) - 2px); + --radius-lg: var(--radius); + --radius-xl: calc(var(--radius) + 4px); + --color-sidebar: var(--sidebar); + --color-sidebar-foreground: var(--sidebar-foreground); + --color-sidebar-primary: var(--sidebar-primary); + --color-sidebar-primary-foreground: var(--sidebar-primary-foreground); + --color-sidebar-accent: var(--sidebar-accent); + --color-sidebar-accent-foreground: var(--sidebar-accent-foreground); + --color-sidebar-border: var(--sidebar-border); + --color-sidebar-ring: var(--sidebar-ring); +} + +@layer base { + * { + @apply border-border outline-ring/50; + } + body { + @apply bg-background text-foreground; + } +} diff --git a/services/frontend/src/vite-env.d.ts b/services/frontend/src/vite-env.d.ts new file mode 100644 index 0000000..11f02fe --- /dev/null +++ b/services/frontend/src/vite-env.d.ts @@ -0,0 +1 @@ +/// diff --git a/services/frontend/tsconfig.app.json b/services/frontend/tsconfig.app.json new file mode 100644 index 0000000..0592374 --- /dev/null +++ b/services/frontend/tsconfig.app.json @@ -0,0 +1,30 @@ +{ + "compilerOptions": { + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", + "target": "ES2020", + "useDefineForClassFields": true, + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "module": "ESNext", + "skipLibCheck": true, + + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "isolatedModules": true, + "moduleDetection": "force", + "noEmit": true, + "jsx": "react-jsx", + + /* Linting */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedSideEffectImports": true, + + "paths": { + "@/*": ["./src/*"] + } + }, + "include": ["src"] +} diff --git a/services/frontend/tsconfig.json b/services/frontend/tsconfig.json new file mode 100644 index 0000000..c36d52a --- /dev/null +++ b/services/frontend/tsconfig.json @@ -0,0 +1,12 @@ +{ + "files": [], + "references": [ + { "path": "./tsconfig.app.json" }, + { "path": "./tsconfig.node.json" } + ], + "compilerOptions": { + "paths": { + "@/*": ["./src/*"] + } + } +} diff --git a/services/frontend/tsconfig.node.json b/services/frontend/tsconfig.node.json new file mode 100644 index 0000000..db0becc --- /dev/null +++ b/services/frontend/tsconfig.node.json @@ -0,0 +1,24 @@ +{ + "compilerOptions": { + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo", + "target": "ES2022", + "lib": ["ES2023"], + "module": "ESNext", + "skipLibCheck": true, + + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "isolatedModules": true, + "moduleDetection": "force", + "noEmit": true, + + /* Linting */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedSideEffectImports": true + }, + "include": ["vite.config.ts"] +} diff --git a/services/frontend/vite.config.ts b/services/frontend/vite.config.ts new file mode 100644 index 0000000..a41fc9d --- /dev/null +++ b/services/frontend/vite.config.ts @@ -0,0 +1,20 @@ +import { defineConfig } from "vite"; +import react from "@vitejs/plugin-react-swc"; +import tailwindcss from "@tailwindcss/vite"; +import tsconfigPaths from "vite-tsconfig-paths"; + +// https://vite.dev/config/ +export default defineConfig({ + build: { + outDir: 'dist', + assetsDir: 'assets', + emptyOutDir: true, + }, + plugins: [ + tsconfigPaths(), + react(), + tailwindcss() + ], + assetsInclude: ['**/*.png', '**/*.jpg', '**/*.jpeg', '**/*.svg'], + publicDir: 'public', +}); diff --git a/tests/README.md b/tests/README.md new file mode 100644 index 0000000..ff758b6 --- /dev/null +++ b/tests/README.md @@ -0,0 +1,19 @@ +# project_name Tests + +There is `unit` and `e2e` tests available, unit tests are placed all around `backend` serivce folder and `e2e` tests placed [here](./e2e/). + +## Running unit tests + +### Backend service + +See [services/backend/README.md](../services/backend/README.md#testing). + +## Unit tests coverage + +### Backend service + +image here + +## Running e2e tests + +See [tests/e2e/README.md](./e2e/README.md). diff --git a/tests/e2e/.env.template b/tests/e2e/.env.template new file mode 100644 index 0000000..014650c --- /dev/null +++ b/tests/e2e/.env.template @@ -0,0 +1,3 @@ +# Below all environment variables and default values + +BACKEND_BASE_URL=http://127.0.0.1:8080 diff --git a/tests/e2e/.gitignore b/tests/e2e/.gitignore new file mode 100644 index 0000000..b96e392 --- /dev/null +++ b/tests/e2e/.gitignore @@ -0,0 +1,170 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +.python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +Pipfile.lock + +# UV +# Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +uv.lock + +# poetry +# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control +poetry.lock + +# pdm +# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. +#pdm.lock +# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it +# in version control. +# https://pdm.fming.dev/latest/usage/project/#working-with-version-control +.pdm.toml +.pdm-python +.pdm-build/ + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# PyCharm +# JetBrains specific template is maintained in a separate JetBrains.gitignore that can +# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore +# and can be added to the global gitignore or merged into this file. For a more nuclear +# option (not recommended) you can uncomment the following to ignore the entire idea folder. +.idea/ + +# PyPI configuration file +.pypirc + +# Ruff files +.ruff_cache diff --git a/tests/e2e/README.md b/tests/e2e/README.md new file mode 100644 index 0000000..467ca99 --- /dev/null +++ b/tests/e2e/README.md @@ -0,0 +1,50 @@ +# E2E tests for project_name + +## Prerequisites + +Ensure you have the following installed on your system: + +- [Python](https://www.python.org/) (>=3.10,<3.12) +- [uv](https://docs.astral.sh/uv/) +- [Docker](https://www.docker.com/) +- [Docker compose](https://docs.docker.com/compose/) (latest versions) + +## Warning + +Plese note that containers will use ports from 13241 to 13245 and 8080, so there is must be no listeners on this ports range. + +## Clone the project + +```bash +git clone https://gitlab.prodcontest.ru/2025-final-projects-back/devitq.git +``` + +## Go to the project directory + +```bash +cd devitq/solution/tests/e2e +``` + +## Install dependencies + +```bash +uv sync --no-dev +``` + +## Customize environment (optional) + +```bash +cp .env.template .env +``` + +And setup env vars according to your needs. + +## Run tests + +```bash +uv run pytest . +``` + +## Results + +You will see something like `n passed in Ns` diff --git a/tests/e2e/conftest.py b/tests/e2e/conftest.py new file mode 100644 index 0000000..7750ce3 --- /dev/null +++ b/tests/e2e/conftest.py @@ -0,0 +1,67 @@ +import os +import subprocess +import time +from collections.abc import Generator + +import httpx +import pytest +from dotenv import load_dotenv + +load_dotenv() + +BACKEND_BASE_URL = os.getenv("BACKEND_BASE_URL", "http://127.0.0.1:8080") + + +@pytest.fixture(scope="session", autouse=True) +def docker_compose() -> Generator[None]: + # btw, this is just in case you've forgotten to shut down compose :) + subprocess.run( + executable="docker", + args=[ + "docker", + "compose", + "down", + ], + check=True, + ) + subprocess.run( + executable="docker", + args=[ + "docker", + "compose", + "--project-name", + "project_name", + "up", + "-d", + "--build", + "--force-recreate", + "--remove-orphans", + ], + check=True, + ) + time.sleep(5) + + yield + + subprocess.run( + executable="docker", + args=[ + "docker", + "compose", + "--project-name", + "project_name", + "down", + "-v", + ], + check=True, + ) + + +@pytest.fixture(scope="session") +def client() -> Generator[httpx.Client]: + with httpx.Client(base_url=BACKEND_BASE_URL, timeout=10.0) as client: + yield client + + +def pytest_collection_modifyitems(items: list[pytest.Item]) -> None: + items.sort(key=lambda item: "test_health" not in item.nodeid) diff --git a/tests/e2e/pyproject.toml b/tests/e2e/pyproject.toml new file mode 100644 index 0000000..01813d7 --- /dev/null +++ b/tests/e2e/pyproject.toml @@ -0,0 +1,109 @@ +[project] +name = "project_name-e2e-tests" +version = "0.1.0" +readme = "README.md" +requires-python = ">=3.10,<3.12" +dependencies = [ + "httpx>=0.28.1", + "pytest>=8.3.4", + "python-dotenv>=1.0.1", +] + +[dependency-groups] +dev = [ + "ruff>=0.9.6", +] + +[tool.ruff] +builtins = [] +cache-dir = ".ruff_cache" +exclude = [ + ".bzr", + ".direnv", + ".eggs", + ".git", + ".git-rewrite", + ".hg", + ".mypy_cache", + ".nox", + ".pants.d", + ".pytype", + ".ruff_cache", + ".svn", + ".tox", + ".venv", + "__pypackages__", + "_build", + "buck-out", + "dist", + "migrations", + "node_modules", + "venv", +] +extend-exclude = [] +extend-include = [] +fix = false +fix-only = false +force-exclude = true +include = ["*.py", "*.pyi", "*.ipynb", "**/pyproject.toml"] +indent-width = 4 +line-length = 79 +namespace-packages = [] +output-format = "full" +preview = false +required-version = ">=0.8.4" +respect-gitignore = true +show-fixes = true +src = [".", "src"] +target-version = "py310" +unsafe-fixes = false + +[tool.ruff.analyze] +detect-string-imports = true +direction = "Dependencies" +exclude = [] +include-dependencies = {} +preview = false + +[tool.ruff.format] +docstring-code-format = true +docstring-code-line-length = 79 +exclude = [] +indent-style = "space" +line-ending = "lf" +preview = false +quote-style = "double" +skip-magic-trailing-comma = false + +[tool.ruff.lint] +allowed-confusables = ["ℹ"] +dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$" +exclude = ["tests.py"] +explicit-preview-rules = false +extend-fixable = [] +extend-per-file-ignores = {} +extend-safe-fixes = [] +extend-select = [] +extend-unsafe-fixes = [] +external = [] +fixable = ["ALL"] +ignore = [ + "ARG", + "D", + "ANN401", + "COM812", + "FBT001", + "FBT002", + "N813", + "S101", +] +logger-objects = [] +per-file-ignores = {} +preview = false +select = ["ALL"] +task-tags = ["TODO", "FIXME", "HACK", "WORKOUT"] +typing-modules = [] +unfixable = [] + +[tool.ruff.lint.pylint] +max-args = 6 diff --git a/tests/e2e/pytest.ini b/tests/e2e/pytest.ini new file mode 100644 index 0000000..7dcb66b --- /dev/null +++ b/tests/e2e/pytest.ini @@ -0,0 +1,3 @@ +[pytest] +log_cli = true +log_level = INFO diff --git a/tests/e2e/scripts/check b/tests/e2e/scripts/check new file mode 100755 index 0000000..7f50734 --- /dev/null +++ b/tests/e2e/scripts/check @@ -0,0 +1,8 @@ +#!/bin/bash + +GREEN='\033[1;32m' +NC='\033[0m' + +uvx ruff format . +uvx ruff check . --fix +printf "${GREEN}Linters/formatters runned${NC}\n" diff --git a/tests/e2e/tests/__init__.py b/tests/e2e/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/e2e/tests/test_backend_health.py b/tests/e2e/tests/test_backend_health.py new file mode 100644 index 0000000..e506766 --- /dev/null +++ b/tests/e2e/tests/test_backend_health.py @@ -0,0 +1,29 @@ +import logging +from http import HTTPStatus as status + +from httpx import Client + +logging.basicConfig(level=logging.INFO) +logger = logging.getLogger(__name__) + + +def test_healthcheck(client: Client) -> None: + response = client.get("/health?format=json") + assert response.status_code == status.OK + + response_data = response.json() + + unhealthy_services = [ + service + for service, status in response_data.items() + if status != "working" + ] + + for service in unhealthy_services: + logger.error( + "Service %s unhealthy: %s", service, response_data[service] + ) + + assert not unhealthy_services, ( + f"Some services are unhealthy: {', '.join(unhealthy_services)}" + )