diff --git a/.github/workflows/ci.yml b/.github/workflows/backend.yml
similarity index 81%
rename from .github/workflows/ci.yml
rename to .github/workflows/backend.yml
index 42038215..76d300a4 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/backend.yml
@@ -1,16 +1,18 @@
-name: Build and Test
+name: Backend
on:
pull_request:
- types: [ opened, synchronize, reopened, closed ]
+ types: [opened, synchronize, reopened, closed]
branches:
- main
+ - prod
paths:
- - 'application/backend/**'
+ - "application/backend/**"
push:
branches:
- main
+ - prod
paths:
- - 'application/backend/**'
+ - "application/backend/**"
jobs:
build-and-test:
runs-on: ubuntu-latest
diff --git a/.github/workflows/frontend.yml b/.github/workflows/frontend.yml
new file mode 100644
index 00000000..7061a1d3
--- /dev/null
+++ b/.github/workflows/frontend.yml
@@ -0,0 +1,33 @@
+name: Frontend
+on:
+ pull_request:
+ types: [opened, synchronize, reopened, closed]
+ branches:
+ - main
+ - prod
+ paths:
+ - "application/frontend/**"
+ push:
+ branches:
+ - main
+ - prod
+ paths:
+ - "application/frontend/**"
+jobs:
+ build:
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout Code
+ uses: actions/checkout@v2
+ - name: Set up Node 20.x
+ uses: actions/setup-node@v3
+ with:
+ node-version: 20.x
+ cache: "npm"
+ cache-dependency-path: application/frontend/package-lock.json
+ - name: Install dependencies
+ working-directory: application/frontend
+ run: npm ci
+ - name: Build
+ working-directory: application/frontend
+ run: npm run build:production
diff --git a/.github/workflows/next-frontend.yml b/.github/workflows/next-frontend.yml
new file mode 100644
index 00000000..c6ef12fb
--- /dev/null
+++ b/.github/workflows/next-frontend.yml
@@ -0,0 +1,59 @@
+name: Next Frontend
+on:
+ pull_request:
+ types: [opened, synchronize, reopened, closed]
+ branches:
+ - main
+ - prod
+ paths:
+ - "application/next-frontend/**"
+ push:
+ branches:
+ - main
+ - prod
+ paths:
+ - "application/next-frontend/**"
+
+jobs:
+ linting:
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout Code
+ uses: actions/checkout@v2
+ - name: Set up Node 20.x
+ uses: actions/setup-node@v3
+ with:
+ node-version: 20.x
+ cache: "npm"
+ cache-dependency-path: application/next-frontend/package-lock.json
+ - name: Install dependencies
+ working-directory: application/next-frontend
+ run: npm ci
+ - name: Linting
+ working-directory: application/next-frontend
+ run: npm run lint
+ - name: Typechecking
+ working-directory: application/next-frontend
+ run: npm run typecheck
+
+ # Test that the project compiles
+ test-build:
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout Code
+ uses: actions/checkout@v2
+ - name: Set up Node 20.x
+ uses: actions/setup-node@v3
+ with:
+ node-version: 20.x
+ cache: "npm"
+ cache-dependency-path: application/next-frontend/package-lock.json
+ - name: Install dependencies
+ working-directory: application/next-frontend
+ run: npm ci
+ - name: Build
+ working-directory: application/next-frontend
+ run: npm run build
+ env:
+ # Prevents the build from failing, but doesn't do anything when testing build
+ NEXT_PUBLIC_BACKEND_URI: http://somerandomurl.test
diff --git a/.github/workflows/versioning.yml b/.github/workflows/versioning.yml
index 61d3cb7d..4094e941 100644
--- a/.github/workflows/versioning.yml
+++ b/.github/workflows/versioning.yml
@@ -5,50 +5,38 @@ on:
- closed
branches:
- main
+ - prod
paths:
- application/**
- infrastructure/**
- .github/workflows/versioning.yml
jobs:
- build:
+ commit-version:
runs-on: ubuntu-22.04
steps:
- - uses: actions/checkout@v3
- with:
- ref: ${{ github.sha }}
- fetch-depth: '0'
- - name: Set up Node.js
- uses: actions/setup-node@v3
- - name: Install dependencies
- run: npm install
- working-directory: application/frontend
- - name: Build bundle
- run: npm run build:production
- working-directory: application/frontend
- env:
- BACKEND_URI: https://api.bot.blank.pizza
- - name: Remove build gitignore
- run: rm public/.gitignore
- working-directory: application/frontend
- - uses: stefanzweifel/git-auto-commit-action@v4
- with:
- commit_message: Automated Build
- branch: build
- push_options: '--force'
- create_branch: true
+ - uses: actions/checkout@v3
+ with:
+ ref: ${{ github.sha }}
+ fetch-depth: "0"
+ - uses: stefanzweifel/git-auto-commit-action@v4
+ with:
+ commit_message: Automated Versioning
+ branch: build
+ push_options: "--force"
+ create_branch: true
create-tag:
- needs: build
+ needs: commit-version
runs-on: ubuntu-22.04
steps:
- - uses: actions/checkout@v3
- with:
- ref: build
- fetch-depth: '0'
+ - uses: actions/checkout@v3
+ with:
+ ref: build
+ fetch-depth: "0"
- - name: Bump version and push tag
- uses: anothrNick/github-tag-action@1.55.0
- env:
- GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- WITH_V: true
- DEFAULT_BRANCH: build
+ - name: Bump version and push tag
+ uses: anothrNick/github-tag-action@1.55.0
+ env:
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ WITH_V: true
+ DEFAULT_BRANCH: build
diff --git a/README.md b/README.md
index 0a23578b..77597cda 100644
--- a/README.md
+++ b/README.md
@@ -3,6 +3,7 @@
![Infrastructure Diagram](https://github.com//blankoslo/Pizza.v2/blob/main/README/infrastructure.png?raw=true)
## Slack App Bot setup
+
1. Go to `https://api.slack.com/apps/`
2. Click the `Create New App` button
3. Choose `from scratch`
@@ -13,7 +14,7 @@
8. Click `Event Subscriptions`
9. Turn on `Enable Events`
10. Open `Subscribe to bot events`
-11. Add the events `file_shared` `message.channels` `message.im` `app_uninstalled` `tokens_revoked`
+11. Add the events `file_shared` `message.channels` `message.im` `app_uninstalled` `tokens_revoked` `member_left_channel`
12. Click `OAuth & Permissions` in the menu
13. Go down to `Scopes` and click `Add an OAuth Scope` and add the scopes found in `/application/backend/api/slack`'s GET `/install` method.
14. Go to `App Home`
@@ -25,29 +26,35 @@
20. Click `Create new command`
21. Fill in `/set-pizza-channel` under `command`, fill in `This sets the current channel as the pizza channel` under `Short Description`
22. Click `save`
-23. Click `Basic Information` in the menu
-24. Under App Credentials, copy `SLACK_CLIENT_ID`, `SLACK_CLIENT_SECRET`, `SLACK_SIGNING_SECRET`
-25. You now have `SLACK_APP_TOKEN`, `SLACK_CLIENT_ID`, `SLACK_CLIENT_SECRET`, `SLACK_SIGNING_SECRET` which is needed in terraform or docker-compose
+23. Add another command `/pizzabot-admin-panel` under `command`, fill in `Link for administrating the pizzabot` under `Short Description`
+24. Click `Basic Information` in the menu
+25. Under App Credentials, copy `SLACK_CLIENT_ID`, `SLACK_CLIENT_SECRET`, `SLACK_SIGNING_SECRET`
+26. You now have `SLACK_APP_TOKEN`, `SLACK_CLIENT_ID`, `SLACK_CLIENT_SECRET`, `SLACK_SIGNING_SECRET` which is needed in terraform or docker-compose
## How to run the system
### Development
+
The frontend, backend, bot, message queue and database can all be run with docker compose by running `docker-compose up`. Optionally you can do `docker-compose up -d [service]` to only start one or more service. During development all services run behind an nginx instance to simplify their interactions. The ports are 80 and 443.
As we use Ouath2 for authentication we are forced to use https. Nginx needs valid ssl certificates, so you are gonna need to generate one with the command `openssl req -x509 -nodes -newkey rsa:4096 -keyout nginx-selfsigned.key -out nginx-selfsigned.crt -sha256 -days 365` and add it to `application/containers/development`
-**NB:** You'll need to supply the docker-compose file with slack credentials as mention in the `Slack App Bot setup` section. You'll also need to supply cloudinary credentials if you want the uninstall handler to properly delete images, you should also update the `upload_preset` in `handle_file_share` in `bot/src/slack/__init__.py` to point to your own cloudinary account.
+**NB:** You'll need to supply the docker-compose file with slack credentials as mention in the `Slack App Bot setup` section. These are defined in a `.env` file in `application/containers/development` folder of the project. This file is not commited to the repository, so you have to create it manually. You can use the `.env.example` file as a template inside `application/containers/development`. You'll also need to supply cloudinary credentials if you want the uninstall handler to properly delete images, you should also update the `upload_preset` in `handle_file_share` in `bot/src/slack/__init__.py` to point to your own cloudinary account.
### Good to know
+
Locales doesnt work properly in the alpine container used, meaning it's not a bug if stuff is localized wrong, such as the time string send in pizza event invites.
### Production
+
#### Terraform Cloud
+
This repository is connected to Terraform Cloud where it is automatically planned and then manually applied whenever a new tag is created.
The branch used in Terraform Cloud is the `Build` branch, which gets created on every new version. This branch is the same as master, but it also contains the build files for the frontend application.
A tag is automatically created through GitHub actions when a PR is merged into Main.
#### Heroku
+
We are using terraform to describe the infrastructure, which can be found in the `/infrastructure` folder. In addition to this the backend/bot have `Procfile`, `runtime.txt`, and `.locales` files that describe the process, heroku runtime and additional locales to include. While the frontend have `.static` in the `public` folder to indicate the application folder for the nginx buildpacker, and a `.gitignore` file to keep the files and folder in git.
1. Go into the `infrastructure` folder and run `terraform apply` (not needed if using Terraform Cloud).
@@ -56,21 +63,27 @@ We are using terraform to describe the infrastructure, which can be found in the
4. Create a CNAME record with the hostname specified in the main terraform file for both the frontend and the backend and point them to the `DNS TARGET`s from heroku. After a while routing and SSL should work flawlesly.
Infrastructure:
-* Backend-app: contains the database, papertrail instance, Rabbitmq instance, and backend application
-* Bot-app: contains an attachement to the database, an attachement to the papertrail instance, an attachement to the Rabbitmq instance, the bot worker
-* Frontend-app: contains a nginx instance with the build files from the `public` folder
+
+- Backend-app: contains the database, papertrail instance, Rabbitmq instance, and backend application
+- Bot-app: contains an attachement to the database, an attachement to the papertrail instance, an attachement to the Rabbitmq instance, the bot worker
+- Frontend-app: contains a nginx instance with the build files from the `public` folder
## Tests
+
### Backend
+
The tests for this application were written using the pytest testing framework. The tests cover the blueprints, services, and broker handlers.
To run the backend tests, navigate to the backend directory of the project and run the following command:
+
```
python3 -m pytest tests
```
+
Optionally you can add the options `-k [name of test suit or test function]` (to only run certain tests), `-s` (to show print statements) and `-v`/`-vv` (to make tests more verbose).
### Bot
+
TODO
## Contributing Guidelines
diff --git a/application/backend/app/api/auth.py b/application/backend/app/api/auth.py
index fa882486..70e0e1e8 100644
--- a/application/backend/app/api/auth.py
+++ b/application/backend/app/api/auth.py
@@ -5,7 +5,7 @@
from app.repositories.user_repository import UserRepository
from app.models.user_schema import UserSchema
from app.auth import auth
-from flask_jwt_extended import create_access_token, create_refresh_token, jwt_required, get_jwt_identity
+from flask_jwt_extended import create_access_token, create_refresh_token, jwt_required, get_jwt_identity, set_access_cookies, unset_jwt_cookies
from app.services.slack_organization_service import SlackOrganizationService
from app.services.injector import injector
@@ -39,9 +39,11 @@ def get(self):
slack_provider_cfg = get_slack_provider_cfg()
authorization_endpoint = slack_provider_cfg["authorization_endpoint"]
+ base_url = current_app.config["FRONTEND_URI"]
+ base_url = base_url.rstrip("/")
+
# Use library to construct the request for Google login and provide
# scopes that let you retrieve user's profile from Google
- base_url = current_app.config["FRONTEND_URI"].rstrip('/')
request_uri = auth.client.prepare_request_uri(
authorization_endpoint,
redirect_uri = f'{base_url}/login/callback',
@@ -55,7 +57,10 @@ def get(self):
@bp.route("/login/callback")
class Auth(views.MethodView):
def get(self):
- base_url = current_app.config["FRONTEND_URI"].rstrip('/')
+
+ base_url = current_app.config["FRONTEND_URI"]
+ base_url = base_url.rstrip("/")
+
code = request.args.get("code")
authorization_response = f'{base_url}/login/callback?'
for key in request.args.keys():
@@ -114,5 +119,15 @@ def get(self):
}
access_token = create_access_token(identity=user, additional_claims=additional_claims)
refresh_token = create_refresh_token(identity=user, additional_claims=additional_claims)
- return jsonify(access_token=access_token, refresh_token=refresh_token)
+ response = jsonify(access_token=access_token, refresh_token=refresh_token)
+ set_access_cookies(response, access_token)
+ return response
return abort(401, message = "User email not available or not verified by Slack.")
+
+
+@bp.route("/logout")
+class Auth(views.MethodView):
+ def delete(self):
+ response = jsonify(msg="Successfully logged out")
+ unset_jwt_cookies(response)
+ return response
diff --git a/application/backend/app/api/crud/slack_users.py b/application/backend/app/api/crud/slack_users.py
index 582cf52b..842fe47a 100644
--- a/application/backend/app/api/crud/slack_users.py
+++ b/application/backend/app/api/crud/slack_users.py
@@ -1,9 +1,11 @@
-from flask import views
+import requests
+from flask import jsonify, views
from flask_smorest import Blueprint, abort
from app.models.slack_user_schema import SlackUserResponseSchema, SlackUserUpdateSchema, SlackUserQueryArgsSchema
from flask_jwt_extended import jwt_required, current_user
from app.services.injector import injector
from app.services.slack_user_service import SlackUserService
+from app.services.slack_organization_service import SlackOrganizationService
bp = Blueprint("users", "users", url_prefix="/users", description="Operations on users")
@@ -19,6 +21,45 @@ def get(self, args, pagination_parameters):
total, slack_users = slack_user_service.get(filters=args, page=pagination_parameters.page, per_page=pagination_parameters.page_size, order_by_ascending=True, team_id=current_user.slack_organization_id)
pagination_parameters.item_count = total
return slack_users
+
+@bp.route("/current-channel")
+class SlackChannel(views.MethodView):
+ @jwt_required()
+ def get(self):
+ """Get current channel info"""
+ slack_organization = injector.get(SlackOrganizationService)
+ org = slack_organization.get_by_id(team_id=current_user.slack_organization_id)
+ channel_id = org.channel_id
+
+ if channel_id is None:
+ return jsonify({"channel_name": None, "channel_id": None, "users": [] })
+
+ # Request channel info from slack
+ request_url = f'https://slack.com/api/conversations.info?channel={channel_id}'
+ res = requests.get(request_url, headers={
+ "Content-Type": "application/json",
+ "Authorization": f"Bearer {org.access_token}"
+ }).json()
+
+ if not res["ok"]:
+ abort(500, message = "Error getting channel info")
+
+ channel_name = res["channel"]["name"]
+
+ slack_user_service = injector.get(SlackUserService)
+ count, slack_users = slack_user_service.get(filters={"slack_organization_id": current_user.slack_organization_id})
+ returned_users = [
+ {
+ "slack_id": user.slack_id,
+ "current_username": user.current_username,
+ "active": user.active,
+ "priority": user.priority,
+ "email": user.email,
+ "first_seen": user.first_seen,
+ } for user in slack_users
+ ]
+
+ return jsonify({"channel_name": channel_name, "channel_id": channel_id, "users": returned_users })
@bp.route("/")
class SlackUsersById(views.MethodView):
diff --git a/application/backend/app/api/slack.py b/application/backend/app/api/slack.py
index eb00a5f1..dabf3780 100644
--- a/application/backend/app/api/slack.py
+++ b/application/backend/app/api/slack.py
@@ -29,7 +29,10 @@ def get(self):
'users:read.email',
'commands'
]
- frontend_base_url = current_app.config["FRONTEND_URI"].rstrip('/')
+
+ frontend_base_url = current_app.config["FRONTEND_URI"]
+ frontend_base_url = frontend_base_url.rstrip("/")
+
callback_redirect_uri = f'{frontend_base_url}/slack/callback'
client_id = current_app.config["SLACK_CLIENT_ID"]
if client_id == None:
@@ -55,7 +58,9 @@ def post(self):
logger.warn("client_id or client_secret is None")
return abort(500)
- frontend_base_url = current_app.config["FRONTEND_URI"].rstrip('/')
+ frontend_base_url = current_app.config["FRONTEND_URI"]
+ frontend_base_url = frontend_base_url.rstrip("/")
+
callback_redirect_uri = f'{frontend_base_url}/slack/callback'
data = {
@@ -71,6 +76,7 @@ def post(self):
logger.error(response["error"])
return abort(500)
+
if response['is_enterprise_install']:
logger.warn("NOT SUPPORTED: User tried to install app into enterprise workspace.")
return abort(501)
@@ -85,12 +91,25 @@ def post(self):
'bot_user_id': response['bot_user_id'],
'access_token': response['access_token']
}
+
+ is_reinstalled = slack_organization_service.get_by_id(schema_data['team_id']) is not None
+
slack_organization = schema.load(schema_data)
slack_organization_service.upsert(slack_organization)
BrokerService.publish("new_slack_organization_event", NewSlackOrganizationEventSchema().load({
'team_id': schema_data['team_id'],
- 'bot_token': schema_data['access_token']
+ 'bot_token': schema_data['access_token'],
+ 'user_who_installed': response['authed_user']['id']
}))
- return Response(status=200)
+ if is_reinstalled:
+ logger.info(f"Bot reinstalled for team: {schema_data['team_id']}")
+ return jsonify({
+ "message": f"Bot has been re-added to the workspace '{schema_data['team_name']}'."
+ })
+
+
+ return jsonify({
+ "message": f"Bot has been added to the workspace '{schema_data['team_name']}'."
+ })
diff --git a/application/backend/app/application.py b/application/backend/app/application.py
index 5a52dd4a..e2330480 100644
--- a/application/backend/app/application.py
+++ b/application/backend/app/application.py
@@ -6,7 +6,7 @@
from app.db import db, migrate
from app.api import api, ma
-from app.auth import auth, jwt
+from app.auth import auth, jwt, refresh_cookie
from app.services.broker import broker
from app.services.injector import injector
from app.services.invitation_service import InvitationService
@@ -104,19 +104,24 @@ def add_headers(response):
response.headers['Pragma'] = 'no-cache'
response.headers['Expires'] = '0'
response.headers['Content-Type'] = 'application/json; charset=utf-8'
+ response = refresh_cookie(response)
return response
+
# Set up CORS
if app.config["ENV"] == "production":
origins = [FRONTEND_URI]
resources_origins = {"origins": origins}
- elif app.config["ENV"] == "development" or app.config["ENV"] == "test":
- origins = ["https://localhost"]
+ elif app.config["ENV"] == "development":
+ origins = ["https://localhost:4434"]
+ resources_origins = {"origins": origins}
+ elif app.config["ENV"] == "test":
+ origins = ["https://localhost:4000"]
resources_origins = {"origins": origins}
CORS(
app,
resources={r"/api/*": resources_origins},
- allow_headers=["Authorization", "Content-Type"],
+ allow_headers=["Authorization", "Content-Type", "X-CSRF-TOKEN"],
expose_headers=["X-Pagination"],
methods=["OPTIONS", "HEAD", "GET", "POST", "PUT", "PATCH", "DELETE"],
supports_credentials=True,
diff --git a/application/backend/app/auth/__init__.py b/application/backend/app/auth/__init__.py
index 2e100c5a..f3d43f9d 100644
--- a/application/backend/app/auth/__init__.py
+++ b/application/backend/app/auth/__init__.py
@@ -1,16 +1,22 @@
from oauthlib.oauth2 import WebApplicationClient
-from flask_jwt_extended import JWTManager
+from flask_jwt_extended import JWTManager, get_jwt, get_jwt_identity, set_access_cookies, create_access_token
from app.repositories.user_repository import UserRepository
+from datetime import datetime, timedelta, timezone
+
+from app.models.user_schema import UserSchema
+
class AuthClient():
- client: WebApplicationClient
+ client: WebApplicationClient
+
+ def __init__(self, app=None, **kwargs):
+ if (app):
+ self.init_app(app, **kwargs)
+
+ def init_app(self, app, **kwargs):
+ self.client = WebApplicationClient(
+ app.config["SLACK_CLIENT_ID"], kwargs=kwargs)
- def __init__(self, app = None, **kwargs):
- if (app):
- self.init_app(app, **kwargs)
-
- def init_app(self, app, **kwargs):
- self.client = WebApplicationClient(app.config["SLACK_CLIENT_ID"], kwargs=kwargs)
auth = AuthClient()
jwt = JWTManager()
@@ -30,3 +36,25 @@ def user_lookup_callback(_jwt_header, jwt_data):
identity = jwt_data["sub"]
user = UserRepository.get_by_id(identity)
return user
+
+
+def refresh_cookie(response):
+ try:
+ exp_timestamp = get_jwt()["exp"]
+ now = datetime.now(timezone.utc)
+ target_timestamp = datetime.timestamp(now + timedelta(minutes=30))
+ if target_timestamp > exp_timestamp:
+ identity = get_jwt_identity()
+ user = UserRepository.get_by_id(identity)
+ json_user = UserSchema().dump(user)
+ additional_claims = {
+ # TODO handle roles
+ "user": {**json_user, "roles": []}
+ }
+ access_token = create_access_token(
+ identity=user, additional_claims=additional_claims)
+ set_access_cookies(response, access_token)
+ return response
+ except (RuntimeError, KeyError):
+ # Case where there is not a valid JWT. Just return the original response
+ return response
diff --git a/application/backend/app/config.py b/application/backend/app/config.py
index efcdc501..1c376a7a 100644
--- a/application/backend/app/config.py
+++ b/application/backend/app/config.py
@@ -17,14 +17,16 @@ class Base(object):
OPENAPI_SWAGGER_UI_PATH = "/swagger"
# The following is equivalent to OPENAPI_SWAGGER_UI_VERSION = '3.19.5'
OPENAPI_SWAGGER_UI_URL = "https://cdnjs.cloudflare.com/ajax/libs/swagger-ui/3.19.5/"
- # JWT
- JWT_ACCESS_TOKEN_EXPIRES = timedelta(minutes=30)
+ # JWT TODO: JWT_SECRET_KEY ?
+ JWT_ACCESS_TOKEN_EXPIRES = timedelta(minutes=60)
JWT_REFRESH_TOKEN_EXPIRES = timedelta(days=30)
+ JWT_TOKEN_LOCATION = ["headers", "cookies"]
+ JWT_COOKIE_SECURE = True
class Test(Base):
DEBUG = True
- FRONTEND_URI = 'localhost'
+ FRONTEND_URI = 'https://localhost'
SLACK_CLIENT_ID = 'dontCareSlackClientId'
SLACK_CLIENT_SECRET = 'dontCareSlackClientSecret'
MQ_EVENT_QUEUE = 'dontCareMqEventQueue'
@@ -49,3 +51,4 @@ class Production(Base):
CLOUDINARY_CLOUD_NAME = os.environ.get('CLOUDINARY_CLOUD_NAME')
CLOUDINARY_API_KEY = os.environ.get('CLOUDINARY_API_KEY')
CLOUDINARY_API_SECRET = os.environ.get('CLOUDINARY_API_SECRET')
+ JWT_COOKIE_DOMAIN = os.environ.get("DOMAIN") if 'DOMAIN' in os.environ else None
diff --git a/application/backend/app/repositories/event_repository.py b/application/backend/app/repositories/event_repository.py
index 81ba504f..5976d0a6 100644
--- a/application/backend/app/repositories/event_repository.py
+++ b/application/backend/app/repositories/event_repository.py
@@ -81,3 +81,17 @@ def get_event_by_id_if_ready_to_finalize(cls, event_id, session=db.session):
.group_by(cls.id, cls.time, cls.restaurant_id) \
.having(func.count(cls.id) == cls.people_per_event)
return query.first()
+
+ @classmethod
+ def get_scheduled_events_for_user(cls, user_id, team_id, session=db.session):
+ query = session.query(cls.id, cls.restaurant_id, cls.time, Invitation.rsvp, cls.finalized, Invitation.slack_message_ts, Invitation.slack_message_channel) \
+ .join(Invitation, cls.id == Invitation.event_id) \
+ .filter(
+ and_(
+ Invitation.slack_id == user_id,
+ cls.slack_organization_id == team_id,
+ cls.time > datetime.now(),
+ )
+ )
+
+ return query.all()
diff --git a/application/backend/app/repositories/slack_organization_repository.py b/application/backend/app/repositories/slack_organization_repository.py
index 1892ee43..bb63bedb 100644
--- a/application/backend/app/repositories/slack_organization_repository.py
+++ b/application/backend/app/repositories/slack_organization_repository.py
@@ -1,6 +1,23 @@
+from datetime import datetime
+
+from app.db import db
+
from app.models.slack_organization import SlackOrganization
+from app.models.event import Event
from app.models.mixins import CrudMixin
-
class SlackOrganizationRepository(SlackOrganization, CrudMixin):
- pass
+ @classmethod
+ def get_scheduled_events(cls, team_id = None, session=db.session):
+ query = session.query(cls.team_id, Event.id, Event.time) \
+ .outerjoin(
+ Event,
+ cls.team_id == Event.slack_organization_id
+ ) \
+ .filter(
+ team_id == cls.team_id if team_id is not None else True,
+ Event.time > datetime.now()
+ )
+
+ res = query.count(), query.all()
+ return res
diff --git a/application/backend/app/services/broker/handlers/action.py b/application/backend/app/services/broker/handlers/action.py
index 17e8f3e1..56dfa5fd 100644
--- a/application/backend/app/services/broker/handlers/action.py
+++ b/application/backend/app/services/broker/handlers/action.py
@@ -103,8 +103,9 @@ def set_slack_channel(request: dict):
success = True
old_channel_id = None
+ scheduled_events_count = None
try:
- old_channel_id, slack_organization = slack_organization_service.set_channel(team_id=team_id, channel_id=channel_id)
+ old_channel_id, scheduled_events_count, slack_organization = slack_organization_service.set_channel(team_id=team_id, channel_id=channel_id)
except Exception as e:
logger.error(e)
success = False
@@ -112,6 +113,8 @@ def set_slack_channel(request: dict):
response = {'success': success}
if old_channel_id is not None:
response['old_channel_id'] = old_channel_id
+ if scheduled_events_count:
+ response['scheduled_events_count'] = scheduled_events_count
return response
diff --git a/application/backend/app/services/broker/handlers/get.py b/application/backend/app/services/broker/handlers/get.py
index b5bf1b61..eb44c321 100644
--- a/application/backend/app/services/broker/handlers/get.py
+++ b/application/backend/app/services/broker/handlers/get.py
@@ -2,12 +2,14 @@
from app.services.broker.schemas.get_unanswered_invitations import GetUnansweredInvitationsResponseSchema, GetUnansweredInvitationsDataSchema
from app.services.broker.schemas.get_invited_unanswered_user_ids import GetInvitedUnansweredUserIdsResponseSchema
+from app.services.broker.schemas.get_scheduled_events_for_user import GetScheduledEventsForUserRequestSchema, GetScheduledEventsForUserResponseSchema
from app.services.broker.schemas.get_slack_installation import GetSlackInstallationRequestSchema, GetSlackInstallationResponseSchema
from app.services.broker.schemas.get_slack_organizations import GetSlackOrganizationsResponseSchema
from app.services.invitation_service import InvitationService
from app.services.slack_organization_service import SlackOrganizationService
from app.services.slack_user_service import SlackUserService
+from app.services.event_service import EventService
from app.services.injector import injector
from app.models.enums import RSVP
@@ -71,6 +73,25 @@ def get_invited_unanswered_user_ids():
return {'user_ids': response_data}
+@MessageHandler.handle('get_scheduled_events_for_user', incoming_schema=GetScheduledEventsForUserRequestSchema, outgoing_schema = GetScheduledEventsForUserResponseSchema)
+def get_scheduled_events_for_user(request: dict):
+ event_service = injector.get(EventService)
+ events = event_service.get_scheduled_events_for_user(request['user_id'], request['team_id'])
+ response_data = []
+ for event in events:
+ data = {
+ "event_id": event.id,
+ "restaurant_id": event.restaurant_id,
+ "time": event.time.isoformat(),
+ "responded": event.rsvp,
+ "event_finalized": event.finalized,
+ "slack_message_channel": event.slack_message_channel,
+ "slack_message_ts": event.slack_message_ts,
+ }
+ response_data.append(data)
+
+ return {'events': response_data}
+
@MessageHandler.handle('get_slack_installation', incoming_schema = GetSlackInstallationRequestSchema, outgoing_schema = GetSlackInstallationResponseSchema)
def get_slack_installation(request: dict):
@@ -81,7 +102,8 @@ def get_slack_installation(request: dict):
'team_id': slack_organization.team_id,
'app_id': slack_organization.app_id,
'bot_user_id': slack_organization.bot_user_id,
- 'access_token': slack_organization.access_token
+ 'access_token': slack_organization.access_token,
+ 'channel_id': slack_organization.channel_id,
}
if slack_organization.team_name:
diff --git a/application/backend/app/services/broker/handlers/update.py b/application/backend/app/services/broker/handlers/update.py
index 01e5d920..0f525aa5 100644
--- a/application/backend/app/services/broker/handlers/update.py
+++ b/application/backend/app/services/broker/handlers/update.py
@@ -45,15 +45,21 @@ def update_slack_user(request: dict):
slack_user_service = injector.get(SlackUserService)
users_to_update = request['users_to_update']
- response = True
+ success = True
updated_users = []
failed_users = []
for user in users_to_update:
try:
- result = slack_user_service.update(user['slack_id'], {
- 'current_username': user['current_username'],
- 'email': user['email']
- }, user['team_id'])
+ to_update = {}
+ if 'current_username' in user:
+ to_update['current_username'] = user['current_username']
+ if 'email' in user:
+ to_update['email'] = user['email']
+ if 'active' in user:
+ to_update['active'] = user['active']
+ result = slack_user_service.update(user['slack_id'], to_update, user['team_id'])
+
+ # if user doesnt exist, so add them
if result is None:
slack_user_service.add({
'slack_id': user['slack_id'],
@@ -61,13 +67,14 @@ def update_slack_user(request: dict):
'email': user['email']
}, user['team_id'])
updated_users.append(user['slack_id'])
+
except Exception as e:
logger.warning(e)
- response = False
+ success = False
failed_users.append(user['slack_id'])
return {
- 'success': response,
+ 'success': success,
'updated_users': updated_users,
'failed_users': failed_users
}
diff --git a/application/backend/app/services/broker/schemas/get_scheduled_events_for_user.py b/application/backend/app/services/broker/schemas/get_scheduled_events_for_user.py
new file mode 100644
index 00000000..020ca975
--- /dev/null
+++ b/application/backend/app/services/broker/schemas/get_scheduled_events_for_user.py
@@ -0,0 +1,21 @@
+from marshmallow import fields, Schema
+from app.models.enums import RSVP
+from marshmallow_enum import EnumField
+
+class GetScheduledEventsForUserRequestSchema(Schema):
+ user_id = fields.Str(required=True)
+ team_id = fields.Str(required=True)
+
+
+class ScheduledEventsForUserDataSchema(Schema):
+ event_id = fields.UUID(required=True)
+ restaurant_id = fields.UUID(required=True)
+ time = fields.Str(required=True)
+ responded = EnumField(RSVP, by_value=True, required=True)
+ event_finalized = fields.Bool(required=True)
+ slack_message_channel = fields.Str()
+ slack_message_ts = fields.Str()
+
+class GetScheduledEventsForUserResponseSchema(Schema):
+ events = fields.List(fields.Nested(ScheduledEventsForUserDataSchema), required=True)
+
diff --git a/application/backend/app/services/broker/schemas/get_slack_installation.py b/application/backend/app/services/broker/schemas/get_slack_installation.py
index 8014134e..4d1a7516 100644
--- a/application/backend/app/services/broker/schemas/get_slack_installation.py
+++ b/application/backend/app/services/broker/schemas/get_slack_installation.py
@@ -13,3 +13,4 @@ class GetSlackInstallationResponseSchema(Schema):
app_id = fields.Str(required=True)
bot_user_id = fields.Str(required=True)
access_token = fields.Str(required=True)
+ channel_id = fields.Str(required=True, allow_none=True)
diff --git a/application/backend/app/services/broker/schemas/new_slack_organization_event.py b/application/backend/app/services/broker/schemas/new_slack_organization_event.py
index cc9fcd04..22371281 100644
--- a/application/backend/app/services/broker/schemas/new_slack_organization_event.py
+++ b/application/backend/app/services/broker/schemas/new_slack_organization_event.py
@@ -3,3 +3,4 @@
class NewSlackOrganizationEventSchema(Schema):
team_id = fields.Str(required=True)
bot_token = fields.Str(required=True)
+ user_who_installed = fields.Str(required=True)
diff --git a/application/backend/app/services/broker/schemas/set_slack_channel.py b/application/backend/app/services/broker/schemas/set_slack_channel.py
index 1357eb9a..b21f7563 100644
--- a/application/backend/app/services/broker/schemas/set_slack_channel.py
+++ b/application/backend/app/services/broker/schemas/set_slack_channel.py
@@ -9,3 +9,4 @@ class SetSlackChannelRequestSchema(Schema):
class SetSlackChannelResponseSchema(Schema):
success = fields.Boolean(required=True)
old_channel_id = fields.Str()
+ scheduled_events_count = fields.Integer()
diff --git a/application/backend/app/services/broker/schemas/update_slack_user.py b/application/backend/app/services/broker/schemas/update_slack_user.py
index 42b01c99..a30d792f 100644
--- a/application/backend/app/services/broker/schemas/update_slack_user.py
+++ b/application/backend/app/services/broker/schemas/update_slack_user.py
@@ -5,6 +5,7 @@ class SlackUserUpdate(Schema):
current_username = fields.Str()
email = fields.Str()
team_id = fields.Str(required=True)
+ active = fields.Boolean()
class UpdateSlackUserRequestSchema(Schema):
users_to_update = fields.Nested(SlackUserUpdate, required=True, many=True)
diff --git a/application/backend/app/services/event_service.py b/application/backend/app/services/event_service.py
index a4bf6314..fe9e9287 100644
--- a/application/backend/app/services/event_service.py
+++ b/application/backend/app/services/event_service.py
@@ -6,6 +6,7 @@
from app.repositories.restaurant_repository import RestaurantRepository
from app.repositories.group_repository import GroupRepository
from app.repositories.invitation_repository import InvitationRepository
+from app.repositories.slack_organization_repository import SlackOrganizationRepository
from app.models.event_schema import EventSchema
from app.services.broker.schemas.deleted_event_event import DeletedEventEventSchema
from app.services.broker.schemas.updated_event_event import UpdatedEventEventSchema
@@ -14,7 +15,7 @@
class EventService:
def get_events_in_need_of_invitations(self):
- days_in_advance_to_invite = current_app.config["DAYS_IN_ADVANCE_TO_INVITE"]
+ days_in_advance_to_invite = int(current_app.config["DAYS_IN_ADVANCE_TO_INVITE"])
return EventRepository.get_events_in_need_of_invitations(days_in_advance_to_invite=days_in_advance_to_invite)
def finalize_event_if_complete(self, event_id):
@@ -88,6 +89,11 @@ def delete(self, event_id, team_id):
def add(self, data, team_id):
data.slack_organization_id = team_id
+ slack_org = SlackOrganizationRepository.get_by_id(team_id)
+ # If channel_id is none, we cant update users about events
+ if slack_org is None or slack_org.channel_id is None:
+ return None
+
restaurant = RestaurantRepository.get_by_id(data.restaurant_id)
if restaurant.slack_organization_id != team_id:
@@ -133,3 +139,6 @@ def update(self, event_id, data, team_id):
BrokerService.publish("updated_event", queue_event)
return updated_event
+
+ def get_scheduled_events_for_user(self, user_id, team_id):
+ return EventRepository.get_scheduled_events_for_user(user_id, team_id)
diff --git a/application/backend/app/services/slack_organization_service.py b/application/backend/app/services/slack_organization_service.py
index 4c61f0a7..46600df0 100644
--- a/application/backend/app/services/slack_organization_service.py
+++ b/application/backend/app/services/slack_organization_service.py
@@ -20,6 +20,9 @@ def _delete_cloudinary_images_for_slack_organization(self, team_id, next_cursor=
self._delete_cloudinary_images_for_slack_organization(team_id=team_id, next_cursor=res['next_cursor'])
def delete(self, team_id, enterprise_id=None):
+ if not cloudinary.config().cloud_name:
+ self.logger.warn("Cloudinary not configured. Skipping deletion of cloudinary images.")
+ return SlackOrganizationRepository.delete(id=team_id)
try:
self._delete_cloudinary_images_for_slack_organization(team_id=team_id)
return SlackOrganizationRepository.delete(id=team_id)
@@ -32,7 +35,15 @@ def upsert(self, schema):
return SlackOrganizationRepository.upsert(schema=schema)
def set_channel(self, team_id, channel_id):
+
+ scheduled_events_count, scheduled_events = SlackOrganizationRepository.get_scheduled_events(team_id=team_id)
+
slack_organization = SlackOrganizationRepository.get_by_id(id=team_id)
old_channel_id = slack_organization.channel_id
+
+ # if scheduled events exist, dont update channel
+ if scheduled_events_count > 0:
+ return old_channel_id, scheduled_events_count, None
+
slack_organization.channel_id = channel_id
- return old_channel_id, SlackOrganizationRepository.upsert(schema=slack_organization)
+ return old_channel_id, 0, SlackOrganizationRepository.upsert(schema=slack_organization)
diff --git a/application/backend/tests/api/test_slack.py b/application/backend/tests/api/test_slack.py
index f3b8aae5..5db63210 100644
--- a/application/backend/tests/api/test_slack.py
+++ b/application/backend/tests/api/test_slack.py
@@ -32,7 +32,10 @@ def test_callback(self, db, mock_broker, mocker):
},
'app_id': 'dontCareAppId',
'bot_user_id': 'dontCareUserId',
- 'access_token': 'dontCareAccessToken'
+ 'access_token': 'dontCareAccessToken',
+ 'authed_user': {
+ 'id': 'dontCareAuthedUserId'
+ },
}
response = self.client.post('/api/slack/callback', method='post', json={'code': 'dontCareCode'})
diff --git a/application/backend/tests/services/broker_handlers/test_get_handler.py b/application/backend/tests/services/broker_handlers/test_get_handler.py
index 4ea587ca..97579efc 100644
--- a/application/backend/tests/services/broker_handlers/test_get_handler.py
+++ b/application/backend/tests/services/broker_handlers/test_get_handler.py
@@ -97,6 +97,7 @@ def test_get_slack_installation(self, mock_broker, slack_organizations, rpc_queu
'team_id': slack_organization.team_id,
'app_id': slack_organization.app_id,
'bot_user_id': slack_organization.bot_user_id,
+ 'channel_id': slack_organization.channel_id,
'access_token': slack_organization.access_token
}
diff --git a/application/backend/tests/services/test_event_service.py b/application/backend/tests/services/test_event_service.py
index 2494ac0f..680423cc 100644
--- a/application/backend/tests/services/test_event_service.py
+++ b/application/backend/tests/services/test_event_service.py
@@ -121,7 +121,7 @@ def test_add(self, slack_organizations, restaurants, groups, event_service):
test_events = Event.query.all()
assert len(test_events) == 1
- def test_update(self, db, slack_organizations, events, groups, restaurants, mock_broker, event_service):
+ def test_update(self, db, slack_organizations, events, groups, restaurants, event_service, mock_broker):
team_id = slack_organizations[0].team_id
event = events.get(team_id)[0]
group = groups.get(team_id)[0]
@@ -148,3 +148,35 @@ def test_update(self, db, slack_organizations, events, groups, restaurants, mock
mock_broker.send.assert_called()
assert len(mock_broker.send.call_args_list) == 1
assert mock_broker.send.call_args_list[0].kwargs['body']['type'] == 'updated_event'
+
+ def test_get_scheduled_events_for_user(self, slack_organizations, events, invitations, slack_users, event_service):
+ team_id = slack_organizations[0].team_id
+ slack_user = slack_users.get(team_id)[0].slack_id
+ team_events = events.get(team_id)
+
+ user_invitations = list(filter(
+ lambda x: x.slack_id == slack_user,
+ invitations.get(team_id)
+ ))
+
+ date_now = datetime.now(pytz.timezone('Europe/Oslo'))
+ scheduled_events = []
+
+ for user_invitation in user_invitations:
+ for event in team_events:
+ if user_invitation.event_id == event.id and date_now < event.time:
+ scheduled_events.append({**vars(event), **vars(user_invitation)})
+
+
+ scheduled_event_from_service = event_service.get_scheduled_events_for_user(user_id=slack_user, team_id=team_id)
+
+ assert len(user_invitations) == len(scheduled_event_from_service)
+
+ for test_event in scheduled_events:
+ event_from_service = next(event_from_service for event_from_service in scheduled_event_from_service
+ if event_from_service.id == test_event['id'])
+ assert test_event['time'] == event_from_service.time
+ assert test_event['rsvp'] == event_from_service.rsvp
+ assert test_event['finalized'] == event_from_service.finalized
+
+
diff --git a/application/bot/main.py b/application/bot/main.py
index aedf9f97..e5d1587e 100644
--- a/application/bot/main.py
+++ b/application/bot/main.py
@@ -1,11 +1,8 @@
import locale
import threading
-import pytz
import logging
import sys
-from src.api.bot_api import BotApiConfiguration
-
from src.scheduler import scheduler
from src.slack import slack_handler
@@ -14,6 +11,7 @@
from src.broker.amqp_connection import AmqpConnection
from src.broker.amqp_connection_pool import AmqpConnectionPool
from src.broker.handlers import on_message
+from src.i18n import Translator
def setup_logger():
@@ -45,13 +43,14 @@ def consume():
def main():
# Set up injector
- api_config = BotApiConfiguration(pytz.timezone('Europe/Oslo'))
- injector.binder.bind(BotApiConfiguration, to=api_config)
-
# Set up logging
logger = setup_logger()
injector.binder.bind(logging.Logger, to=logger, scope=singleton)
+ # set up translator
+ translator = Translator(language_folder="./src/lang")
+ injector.binder.bind(Translator, to=translator, scope=singleton)
+
# Try setting locale
try:
locale.setlocale(locale.LC_ALL, "nb_NO.utf8")
@@ -61,7 +60,7 @@ def main():
# Set up rabbitmq
setup_connection_pool()
setup_consumption_queue_listener()
-
+
# start scheduler
scheduler.start()
diff --git a/application/bot/src/api/bot_api.py b/application/bot/src/api/bot_api.py
index a93c231d..d9701564 100644
--- a/application/bot/src/api/bot_api.py
+++ b/application/bot/src/api/bot_api.py
@@ -10,18 +10,17 @@
from src.broker.broker_client import BrokerClient
from src.injector import injector
import logging
+from src.i18n import Translator
-class BotApiConfiguration:
- def __init__(self, timezone):
- self.timezone = timezone
+frontend_uri = os.environ["FRONTEND_URI"] if "FRONTEND_URI" in os.environ else None
class BotApi:
@inject
- def __init__(self, config: BotApiConfiguration, logger: logging.Logger):
+ def __init__(self, logger: logging.Logger):
self.REPLY_DEADLINE_IN_HOURS = int(os.environ["REPLY_DEADLINE_IN_HOURS"])
self.HOURS_BETWEEN_REMINDERS = int(os.environ["HOURS_BETWEEN_REMINDERS"])
- self.timezone = config.timezone
- self.logger = logger
+ self.logger: logging.Logger = logger
+ self.translator: Translator = injector.get(Translator)
def __enter__(self):
self.client = injector.get(BrokerClient)
@@ -30,48 +29,127 @@ def __enter__(self):
def __exit__(self, type, value, traceback):
self.client.disconnect()
- def welcome(self, slack_client, team_id):
- channel_id = self.join_channel(slack_client=slack_client, team_id=team_id)
+ def welcome(self, slack_client, user_who_installed):
self.send_slack_message(
- channel_id=channel_id,
- text="Hei! Jeg er pizzabot. Hvis dere vil endre hvilke kanal jeg bruker så kan dere gå inn i riktig kanal og bruke kommandoen '/set-pizza-channel'. Hvis kanalen er privat må dere legge meg til først.",
+ channel_id=user_who_installed,
+ text=self.translator.translate("botWelcome"),
+ slack_client=slack_client
+ )
+ self.send_slack_message(
+ channel_id=user_who_installed,
+ text=self.translator.translate("adminPanelURLCommand", adminPanelURL=f"{frontend_uri}/admin"),
slack_client=slack_client
)
+
- def join_channel(self, slack_client, team_id, channel_id=None):
- # Get default channel if non is given
- if channel_id is None:
- default_channel = slack_client.get_default_channel()
- channel_id = default_channel["id"]
+ def join_channel(self, slack_client, team_id, channel_id):
# Log and exit if we were unable to find a channel
if channel_id is None:
- self.logger.error("Was unable to find channel")
+ self.logger.error("Bot cannot join None channel")
return None
# Join channel
join_success = slack_client.join_channel(channel_id)
# If we were unable to join the channel
if not join_success:
- self.logger.error("Was unable to join channel %s", channel_id)
- return
-
- # Send a rpc message to set the channel
+ self.logger.error("Was unable to join channel %s in team: %s", channel_id, team_id)
+ return None
+
+ # Send a rpc message to set the channel in db
set_channel_response = self.client.set_slack_channel(channel_id=channel_id, team_id=team_id)
+
if not set_channel_response['success']:
+ self.send_slack_message(
+ channel_id=channel_id,
+ text=self.translator.translate("pizzaChannelError"),
+ slack_client=slack_client
+ )
+ self.logger.error("Was unable to join channel %s", channel_id)
+ # Leave the channel we joined after sending the error message to slack
+ leave_success = slack_client.leave_channel(channel_id)
+ return None
+
+ # If there are scheduled events
+ elif 'scheduled_events_count' in set_channel_response and set_channel_response['scheduled_events_count'] > 0:
+ self.send_slack_message(
+ channel_id=channel_id,
+ text=self.translator.translate(
+ "pizzaChannelErrorScheduledEvents",
+ count=set_channel_response['scheduled_events_count']
+ ),
+ slack_client=slack_client
+ )
+ leave_success = slack_client.leave_channel(channel_id)
return None
+
+ old_channel = set_channel_response['old_channel_id'] if 'old_channel_id' in set_channel_response else None
+
# Leave channel if old and new channel isnt the same
# they can be the same if someone reinstall the app
- if 'old_channel_id' in set_channel_response and channel_id != set_channel_response['old_channel_id']:
+ if old_channel is not None and channel_id != old_channel:
leave_success = slack_client.leave_channel(set_channel_response['old_channel_id'])
# If we were unable to leave the channel, we dont exit function as it isnt critical to leave
if not leave_success:
self.logger.error("Was unable to leave channel %s", channel_id)
- # return channel
return channel_id
+
+ def handle_user_left_channel(self, team_id, user_id, channel_id, slack_client):
+ slack_installation = self.client.get_slack_installation(team_id=team_id)
+ if slack_installation is None or 'channel_id' not in slack_installation:
+ self.logger.error("Failed to get slack installation for team %s", team_id)
+ return
+ if slack_installation['channel_id'] != channel_id:
+ return
+
+ scheduled_events = self.client.get_scheduled_events_for_user(team_id=team_id, user_id=user_id)
+ if scheduled_events == False:
+ self.logger.error("Failed to get scheduled events for user %s", user_id)
+ return
+
+ self.logger.info(f"User {user_id} left channel in organization {team_id}")
+
+ # set non active
+ user_to_update = {
+ 'id': user_id,
+ 'team_id': team_id,
+ 'active': False
+ }
+
+ self.client.update_slack_users(slack_users=[user_to_update])
+
+ # Respond to invited events
+ for scheduled_event in scheduled_events:
+ rsvp = scheduled_event['responded']
+ success = False
+ if rsvp == RSVP.not_attending:
+ continue
+ elif rsvp == RSVP.attending:
+ success = self.withdraw_invitation(event_id=scheduled_event['event_id'], slack_id=user_id)
+ elif rsvp == RSVP.unanswered:
+ success = self.decline_invitation(event_id=scheduled_event['event_id'], slack_id=user_id)
+
+ if success:
+ # TODO: fix that ts is overwritten by event handler from bakend on RSVP updated.
+ self.send_pizza_invited_but_left_channel(
+ channel_id=scheduled_event['slack_message_channel'],
+ event_id=scheduled_event['event_id'],
+ ts=scheduled_event['slack_message_ts'],
+ slack_client=slack_client,
+ prev_answer=rsvp
+ )
+ else:
+ self.logger.error("Failed to decline invitation after leaving channel for user %s", user_id)
+
+ # Send message to user that they no longer will be invited to events
+ self.send_slack_message(
+ channel_id=scheduled_event['slack_message_channel'],
+ text=self.translator.translate("userLeftPizzaChannel"),
+ slack_client=slack_client
+ )
def invite_multiple_if_needed(self):
events = self.client.invite_multiple_if_needed()
@@ -87,15 +165,14 @@ def invite_if_needed(self, event, slack_client):
restaurant_name = event['restaurant_name']
# timestamp (timestamp) is converted to UTC timestamp by psycopg2
- # Convert timestamp to Norwegian timestamp
- timestamp = pytz.utc.localize(event_time.replace(tzinfo=None), is_dst=None).astimezone(self.timezone)
-
+ # Convert timestamp to appropriate timestamp
+ timestamp = self.translator.format_timestamp(timestamp=event_time)
for user_id in invited_users:
slack_message = self.send_pizza_invite(
channel_id=user_id,
event_id=str(event_id),
place=restaurant_name,
- datetime=timestamp.strftime("%A %d. %B kl %H:%M"),
+ datetime=timestamp.strftime("%A %d. %B %H:%M"),
deadline=self.REPLY_DEADLINE_IN_HOURS,
slack_client = slack_client
)
@@ -130,7 +207,7 @@ def send_reminders(self):
if invitation['reminded_at'] < remind_timestamp:
slack_client.send_slack_message(
channel_id=invitation['slack_id'],
- text="Hei du! Jeg hørte ikke noe mer? Er du gira?"
+ text=self.translator.translate("eventReminder")
)
was_updated = self.client.update_invitation(
slack_id=invitation['slack_id'],
@@ -146,8 +223,8 @@ def send_reminders(self):
def send_event_finalized(self, timestamp, restaurant_name, slack_ids, channel_id, slack_client):
self.logger.info("Finalizing event %s %s", timestamp, restaurant_name)
- # Convert timestamp to Norwegian timestamp
- timestamp = pytz.utc.localize(timestamp.replace(tzinfo=None), is_dst=None).astimezone(self.timezone)
+ # Convert timestamp to appropriate timestamp
+ timestamp = self.translator.format_timestamp(timestamp=timestamp)
# Create slack @-id-strings
users = ['<@%s>' % user for user in slack_ids]
ids_string = ", ".join(users)
@@ -158,30 +235,33 @@ def send_event_finalized(self, timestamp, restaurant_name, slack_ids, channel_id
# Send the finalization Slack message
slack_client.send_slack_message(
channel_id=channel_id,
- text="Halloi! %s! Dere skal spise 🍕 på %s, %s. %s booker bord, og %s legger ut for maten. Blank betaler!" % (ids_string, restaurant_name, timestamp.strftime("%A %d. %B kl %H:%M"), booker, payer)
+ text=self.translator.translate("eventFinalized", user_ids=ids_string, restaurant_name=restaurant_name, time_stamp=timestamp.strftime("%A %d. %B %H:%M"), booker=booker, payer=payer)
)
def send_event_unfinalized(self, timestamp, restaurant_name, slack_ids, channel_id, slack_client):
self.logger.info("Unfinalizing event %s %s", timestamp, restaurant_name)
- # Convert timestamp to Norwegian timestamp
- timestamp = pytz.utc.localize(timestamp.replace(tzinfo=None), is_dst=None).astimezone(self.timezone)
+ # Convert timestamp to appropriate timestamp
+ timestamp = self.translator.format_timestamp(timestamp=timestamp)
# Create slack @-id-strings
users = ['<@%s>' % user for user in slack_ids]
ids_string = ", ".join(users)
# Send message that the event unfinalized
slack_client.send_slack_message(
channel_id=channel_id,
- text="Halloi! %s! Hvis den som meldte seg av besøket til %s %s skulle betale eller booke så må nesten en av dere andre sørge for det. I mellomtiden letes det etter en erstatter." % (ids_string, restaurant_name, timestamp.strftime("%A %d. %B kl %H:%M"))
+ text=self.translator.translate("eventUnfinalized", user_ids=ids_string, restaurant_name=restaurant_name, time_stamp=timestamp.strftime("%A %d. %B %H:%M"))
)
# Invite more users for the event
self.invite_multiple_if_needed()
def send_user_withdrew_after_finalization(self, user_id, timestamp, restaurant_name, channel_id, slack_client):
self.logger.info("User %s withdrew from event %s %s", user_id, timestamp, restaurant_name)
+
+ timestamp=self.translator.format_timestamp(timestamp=timestamp)
+
# Send message that the user withdrew
slack_client.send_slack_message(
channel_id=channel_id,
- text="Halloi! <@%s> meldte seg nettopp av besøket til %s %s." % (user_id, restaurant_name, timestamp.strftime("%A %d. %B kl %H:%M"))
+ text=self.translator.translate("userWithdrawAfterFinalization", user_id=user_id, restaurant_name=restaurant_name, time_stamp=timestamp.strftime("%A %d. %B %H:%M"))
)
# Invite more users for the event
self.invite_multiple_if_needed()
@@ -219,7 +299,7 @@ def auto_reply(self):
# Send the user a message that the invite expired
slack_client.send_slack_message(
channel_id=invitation['slack_id'],
- text="Neivel, da antar jeg du ikke kan/gidder. Håper du blir med neste gang! 🤞"
+ text=self.translator.translate("autoReplyNoAttending")
)
self.logger.info("%s didn't answer. Setting RSVP to not attending." % invitation['slack_id'])
else:
@@ -260,10 +340,10 @@ def update_invitation_answer(self, slack_id, event_id, answer: RSVP):
)
def accept_invitation(self, event_id, slack_id):
- self.update_invitation_answer(slack_id=slack_id, event_id=event_id, answer=RSVP.attending)
+ return self.update_invitation_answer(slack_id=slack_id, event_id=event_id, answer=RSVP.attending)
def decline_invitation(self, event_id, slack_id):
- self.update_invitation_answer(slack_id=slack_id, event_id=event_id, answer=RSVP.not_attending)
+ return self.update_invitation_answer(slack_id=slack_id, event_id=event_id, answer=RSVP.not_attending)
def withdraw_invitation(self, event_id, slack_id):
return self.client.withdraw_invitation(event_id=event_id, slack_id=slack_id)
@@ -279,11 +359,23 @@ def sync_users_from_organizations(self):
for slack_organization in slack_organizations:
self.sync_users_from_organization(team_id=slack_organization['team_id'], bot_token=slack_organization['bot_token'])
+ # This updates all users on all user for all organizations, regardless if they are in the channel or not. This wont scale
+ # TODO: optimize this. Might need another message to the backend to get active users and see if the list has changed
def sync_users_from_organization(self, team_id, bot_token):
+ installation_info = self.client.get_slack_installation(team_id=team_id)
+ if installation_info is None:
+ self.logger.error("Failed to sync users in workspace %s" % team_id)
+ return
+
+ channel_id = installation_info['channel_id'] if 'channel_id' in installation_info else None
+ if channel_id is None:
+ self.logger.info("Cannot sync team %s, channel id not set" % team_id)
+ return
+
slack_client = SlackApi(token=bot_token)
- all_slack_users = slack_client.get_slack_users()
- slack_users = slack_client.get_real_users(all_slack_users)
- response = self.client.update_slack_user(slack_users)
+
+ users_to_update = slack_client.get_users_to_update_by_channel(channel_id=channel_id)
+ response = self.client.update_slack_users(users_to_update)
updated_users = response['updated_users']
for user in updated_users:
@@ -310,7 +402,7 @@ def inform_users_unfinalized_event_got_cancelled(self, time, restaurant_name, sl
# Send the user a message that the event has been cancelled
slack_client.send_slack_message(
channel_id=slack_id,
- text="Halloi! Besøket til %s, %s har blitt kansellert. Sorry!" % (restaurant_name, time.strftime("%A %d. %B kl %H:%M"))
+ text=self.translator.translate("unfinalizedEventCancelled", restaurant_name=restaurant_name, time_stamp=time.strftime("%A %d. %B %H:%M"))
)
self.logger.info("Informed user: %s" % slack_id)
@@ -322,7 +414,7 @@ def inform_users_finalized_event_got_cancelled(self, time, restaurant_name, slac
self.logger.info("finalized event got cancelled for users %s" % ", ".join(slack_user_ids))
slack_client.send_slack_message(
channel_id=channel_id,
- text="Halloi! %s! Besøket til %s, %s har blitt kansellert. Sorry!" % (ids_string, restaurant_name, time.strftime("%A %d. %B kl %H:%M"),)
+ text=self.translator.translate("finalizedEventCancelled", user_ids=ids_string, restaurant_name=restaurant_name, time_stamp=time.strftime("%A %d. %B %H:%M"))
)
# Update invitation message - remove buttons and tell user it has been cancelled
for slack_user_data in slack_data:
@@ -342,7 +434,7 @@ def inform_users_unfinalized_event_got_updated(self, old_time, time, old_restaur
for slack_id in slack_ids:
slack_client.send_slack_message(
channel_id=slack_id,
- text="Halloi! Besøket til %s, %s har blit endret til %s, %s." % (old_restaurant_name, old_time.strftime("%A %d. %B kl %H:%M"), restaurant_name, time.strftime("%A %d. %B kl %H:%M"))
+ text=self.translator.translate("unfinalizedEventUpdate", old_restaurant_name=old_restaurant_name, old_time_stamp=old_time.strftime("%A %d. %B %H:%M"),restaurant_name=restaurant_name, time_stamp=time.strftime("%A %d. %B %H:%M"))
)
self.logger.info("Informed user: %s" % slack_id)
@@ -352,7 +444,7 @@ def inform_users_finalized_event_got_updated(self, old_time, time, old_restauran
self.logger.info("finalized event got updated for users %s" % ", ".join(slack_ids))
slack_client.send_slack_message(
channel_id=channel_id,
- text="Halloi! %s! Besøket til %s, %s har blit endret til %s, %s." % (ids_string, old_restaurant_name, old_time.strftime("%A %d. %B kl %H:%M"), restaurant_name, time.strftime("%A %d. %B kl %H:%M"))
+ text=self.translator.translate("finalizedEventUpdate", user_ids=ids_string, old_restaurant_name=old_restaurant_name, old_time_stamp=old_time.strftime("%A %d. %B %H:%M"),restaurant_name=restaurant_name, time_stamp=time.strftime("%A %d. %B %H:%M"))
)
def send_slack_message(self, channel_id, text, slack_client, blocks=None, thread_ts=None):
@@ -362,20 +454,20 @@ def update_slack_message(self, channel_id, ts, slack_client, text=None, blocks=N
return slack_client.update_slack_message(channel_id, ts, text, blocks)
def send_pizza_invite(self, channel_id, event_id, place, datetime, deadline, slack_client):
- top_level_title_text = f"Pizzainvitasjon: {place}, {datetime}"
+ top_level_title_text = self.translator.translate("topLevelPizzaInvitation", restaurant_name=place, time_stamp=datetime)
blocks = [
{
"type": "header",
"text": {
"type": "plain_text",
- "text": "Pizzainvitasjon"
+ "text": self.translator.translate("pizzaInvitationHeader")
}
},
{
"type": "section",
"text": {
"type": "plain_text",
- "text": f"Du er invitert til :pizza: på {place}, {datetime}. Pls svar innen {deadline} timer :pray:. Kan du?"
+ "text": self.translator.translate("pizzaInvitationBody", restaurant_name=place, time_stamp=datetime, deadline=deadline)
}
},
{
@@ -388,7 +480,7 @@ def send_pizza_invite(self, channel_id, event_id, place, datetime, deadline, sla
"type": "button",
"text": {
"type": "plain_text",
- "text": "Hells yesss!!! 🍕🍕🍕"
+ "text": self.translator.translate("pizzaInvitationAttendButton")
},
"value": event_id,
"action_id": "rsvp_yes",
@@ -397,7 +489,7 @@ def send_pizza_invite(self, channel_id, event_id, place, datetime, deadline, sla
"type": "button",
"text": {
"type": "plain_text",
- "text": "Nah ☹️"
+ "text": self.translator.translate("pizzaInvitationNoAttendButton")
},
"value": event_id,
"action_id": "rsvp_no",
@@ -421,7 +513,7 @@ def send_pizza_invite_loading(self, channel_id, ts, old_blocks, event_id, slack_
"type": "section",
"text": {
"type": "mrkdwn",
- "text": ":hourglass_flowing_sand: Behandler forespørselen din...",
+ "text": self.translator.translate("inviteLoading")
}
}
]
@@ -435,7 +527,7 @@ def send_pizza_invite_not_among_invited_users(self, channel_id, ts, old_blocks,
"type": "section",
"text": {
"type": "plain_text",
- "text": "Kunne ikke oppdatere invitasjonen. Du var ikke blant de inviterte.",
+ "text": self.translator.translate("inviteNotAmongUsers")
}
}
]
@@ -480,7 +572,7 @@ def send_update_pizza_invite_unanswered(self, channel_id, ts, event_id, slack_cl
"type": "button",
"text": {
"type": "plain_text",
- "text": "Hells yesss!!! 🍕🍕🍕"
+ "text": self.translator.translate("pizzaInvitationAttendButton")
},
"value": str(event_id),
"action_id": "rsvp_yes",
@@ -489,7 +581,7 @@ def send_update_pizza_invite_unanswered(self, channel_id, ts, event_id, slack_cl
"type": "button",
"text": {
"type": "plain_text",
- "text": "Nah ☹️"
+ "text": self.translator.translate("pizzaInvitationNoAttendButton")
},
"value": str(event_id),
"action_id": "rsvp_no",
@@ -506,7 +598,7 @@ def send_pizza_invite_answered(self, channel_id, ts, event_id, old_blocks, atten
"type": "section",
"text": {
"type": "plain_text",
- "text": f"Du har takket {'ja. Sweet! 🤙' if attending else 'nei. Ok 😕'}",
+ "text": self.translator.translate("pizzaInviteAnswerAttend") if attending else self.translator.translate("pizzaInviteAnswerNoAttend") ,
}
}
]
@@ -518,13 +610,13 @@ def send_pizza_invite_answered(self, channel_id, ts, event_id, old_blocks, atten
"type": "section",
"text": {
"type": "mrkdwn",
- "text": "Hvis noe skulle skje så kan du melde deg av ved å klikke på knappen!"
+ "text": self.translator.translate("unsubscribeBody")
},
"accessory": {
"type": "button",
"text": {
"type": "plain_text",
- "text": "Meld meg av"
+ "text": self.translator.translate("unsubscribeButton")
},
"value": str(event_id),
"action_id": "rsvp_withdraw"
@@ -543,7 +635,7 @@ def send_invitation_invalidated_event_cancelled(self, channel_id, ts, old_blocks
"type": "section",
"text": {
"type": "plain_text",
- "text": "Arrangementet har blitt avlyst.",
+ "text": self.translator.translate("invalidatedEventCancelled")
}
}
]
@@ -557,7 +649,7 @@ def send_invitation_expired(self, channel_id, ts, old_blocks, slack_client):
"type": "section",
"text": {
"type": "plain_text",
- "text": "Invitasjonen er utløpt.",
+ "text": self.translator.translate("invitationExpired")
}
}
]
@@ -571,7 +663,7 @@ def send_pizza_invite_withdraw(self, channel_id, ts, old_blocks, slack_client):
"type": "section",
"text": {
"type": "plain_text",
- "text": "Du har meldt deg av. Ok 😕",
+ "text": self.translator.translate("inviteWithdrawn")
}
}
]
@@ -585,9 +677,26 @@ def send_pizza_invite_withdraw_failure(self, channel_id, ts, old_blocks, slack_c
"type": "section",
"text": {
"type": "plain_text",
- "text": "Pizza arrangementet er over. Avmelding er ikke mulig.",
+ "text": self.translator.translate("inviteWithdrawnFailure")
}
}
]
blocks = old_blocks + new_blocks
return slack_client.update_slack_message(channel_id=channel_id, ts=ts, blocks=blocks)
+
+
+ def send_pizza_invited_but_left_channel(self, channel_id, ts, slack_client, event_id, prev_answer: RSVP):
+ self.logger.info('user left and sent message of widthdrawal for %s, %s' % (channel_id, event_id))
+ invitation_message = slack_client.get_slack_message(channel_id, ts)
+ blocks = invitation_message["blocks"][0:3]
+ new_blocks = [
+ {
+ "type": "section",
+ "text": {
+ "type": "plain_text",
+ "text": self.translator.translate("invitedButLeftChannelWithdrawn") if prev_answer == RSVP.attending else self.translator.translate("invitedButLeftChannelUnanswered")
+ }
+ }
+ ]
+ blocks = blocks + new_blocks
+ return slack_client.update_slack_message(channel_id=channel_id, ts=ts, blocks=blocks)
diff --git a/application/bot/src/api/slack_api.py b/application/bot/src/api/slack_api.py
index 0685fd94..45c524a6 100644
--- a/application/bot/src/api/slack_api.py
+++ b/application/bot/src/api/slack_api.py
@@ -14,6 +14,42 @@ def __init__(self, client=None, token=None):
raise ValueError("Either 'client' or 'token' must be provided.")
self.logger = injector.get(logging.Logger)
+ # Returns a list of users in the workspace,
+ # with their active status updated.
+ def get_users_to_update_by_channel(self, channel_id):
+ # list of user ids in channel
+ members = self.get_channel_users(channel_id=channel_id)
+
+ # list of full user info in workspace
+ full_users = self.get_slack_users()
+ users_to_update = self.get_real_users(full_users)
+
+ for user in users_to_update:
+ user["active"] = user["id"] in members
+ user["email"] = user["profile"]["email"]
+
+ return users_to_update
+
+
+ def get_channel_users(self, channel_id: str) -> list[str]:
+ first_page = self.client.conversations_members(channel=channel_id)
+
+ if not first_page["ok"]:
+ self.logger(first_page["error"])
+ return []
+
+ members = first_page["members"]
+
+ # Continue to loop over pages to find the default channel
+ next_cursor = first_page["response_metadata"]["next_cursor"]
+ while next_cursor != "":
+ page = self.client.conversations_list(cursor=next_cursor)
+ next_cursor = page["response_metadata"]["next_cursor"]
+
+ members = members.extend(page["members"])
+
+ return members
+
def get_slack_users(self):
first_page = self.client.users_list()
@@ -34,7 +70,9 @@ def get_slack_users(self):
return members
def get_real_users(self, all_users):
- return [u for u in all_users if not u['deleted'] and not u['is_bot'] and not u['is_restricted'] and not u['name'] == "slackbot"] # type : list
+ # 'is_restricted': is multichannel guest.
+ # 'is_ultra_restricted': is singlechannel guest.
+ return [u for u in all_users if not u['deleted'] and not u['is_bot'] and not u['name'] == "slackbot"] # type : list
def send_slack_message(self, channel_id, text=None, blocks=None, thread_ts=None):
return self.client.chat_postMessage(
diff --git a/application/bot/src/broker/broker_client.py b/application/bot/src/broker/broker_client.py
index dc4256af..e5ebedf7 100644
--- a/application/bot/src/broker/broker_client.py
+++ b/application/bot/src/broker/broker_client.py
@@ -20,6 +20,7 @@
from src.broker.schemas.get_slack_organizations import GetSlackOrganizationsResponseSchema
from src.broker.schemas.deleted_slack_organization_event import DeletedSlackOrganizationEventSchema
from src.broker.schemas.set_slack_channel import SetSlackChannelRequestSchema, SetSlackChannelResponseSchema
+from src.broker.schemas.get_schedule_events_for_user import GetScheduledEventsForUserRequestSchema, GetScheduledEventsForUserResponseSchema
class BrokerClient:
messages = {}
@@ -165,23 +166,27 @@ def get_invited_unanswered_user_ids(self):
response = response_schema.load(response_payload)
return response['user_ids']
- def update_slack_user(self, slack_users):
+ def update_slack_users(self, slack_users):
request_payload = {
'users_to_update': []
}
- for slack_user in slack_users:
- slack_id = slack_user['id']
- current_username = slack_user['name']
- email = slack_user['profile']['email']
- team_id = slack_user['team_id']
-
- request_payload['users_to_update'].append({
- 'slack_id': slack_id,
- 'current_username': current_username,
- 'email': email,
- 'team_id': team_id
- })
+ for slack_user in slack_users:
+ fields_to_update = {
+ 'slack_id': slack_user['id'],
+ 'team_id': slack_user['team_id']
+ }
+ if 'name' in slack_user:
+ fields_to_update['current_username'] = slack_user['name']
+ if 'current_username' in slack_user:
+ fields_to_update['current_username'] = slack_user['current_username']
+ if 'email' in slack_user:
+ fields_to_update['email'] = slack_user['email']
+ if 'active' in slack_user:
+ fields_to_update['active'] = slack_user['active']
+
+ request_payload['users_to_update'].append(fields_to_update)
+
request_payload_schema = UpdateSlackUserRequestSchema()
response_payload = self._call(self._create_request("update_slack_user", request_payload_schema.load(request_payload)))
response_schema = UpdateSlackUserResponseSchema()
@@ -221,3 +226,16 @@ def withdraw_invitation(self, event_id, slack_id):
response_schema = WithdrawInvitationResponseSchema()
response = response_schema.load(response_payload)
return response['success']
+
+ def get_scheduled_events_for_user(self, user_id, team_id):
+ request_payload = {
+ "user_id": user_id,
+ 'team_id': team_id
+ }
+ request_payload_schema = GetScheduledEventsForUserRequestSchema()
+ response_payload = self._call(self._create_request("get_scheduled_events_for_user", request_payload_schema.load(request_payload)))
+ if response_payload is None:
+ return False
+ response_schema = GetScheduledEventsForUserResponseSchema()
+ response = response_schema.load(response_payload)
+ return response['events']
diff --git a/application/bot/src/broker/handlers/new_slack_organization.py b/application/bot/src/broker/handlers/new_slack_organization.py
index add0477d..5a576186 100644
--- a/application/bot/src/broker/handlers/new_slack_organization.py
+++ b/application/bot/src/broker/handlers/new_slack_organization.py
@@ -9,7 +9,6 @@
def new_slack_organization_event(event: dict):
with injector.get(BotApi) as ba:
slack_client = SlackApi(token=event['bot_token'])
- ba.welcome(slack_client=slack_client, team_id=event['team_id'])
- ba.sync_users_from_organization(team_id=event['team_id'], bot_token=event['bot_token'])
+ ba.welcome(slack_client=slack_client, user_who_installed=event['user_who_installed'])
diff --git a/application/bot/src/broker/schemas/get_schedule_events_for_user.py b/application/bot/src/broker/schemas/get_schedule_events_for_user.py
new file mode 100644
index 00000000..edac24e8
--- /dev/null
+++ b/application/bot/src/broker/schemas/get_schedule_events_for_user.py
@@ -0,0 +1,21 @@
+from marshmallow import fields, Schema
+from src.rsvp import RSVP
+from marshmallow_enum import EnumField
+
+class GetScheduledEventsForUserRequestSchema(Schema):
+ user_id = fields.Str(required=True)
+ team_id = fields.Str(required=True)
+
+
+class ScheduledEventsForUserDataSchema(Schema):
+ event_id = fields.UUID(required=True)
+ restaurant_id = fields.UUID(required=True)
+ time = fields.Str(required=True)
+ responded = EnumField(RSVP, by_value=True, required=True)
+ event_finalized = fields.Bool(required=True)
+ slack_message_channel = fields.Str()
+ slack_message_ts = fields.Str()
+
+class GetScheduledEventsForUserResponseSchema(Schema):
+ events = fields.List(fields.Nested(ScheduledEventsForUserDataSchema), required=True)
+
diff --git a/application/bot/src/broker/schemas/get_slack_installation.py b/application/bot/src/broker/schemas/get_slack_installation.py
index 9e65a90b..d286591d 100644
--- a/application/bot/src/broker/schemas/get_slack_installation.py
+++ b/application/bot/src/broker/schemas/get_slack_installation.py
@@ -13,3 +13,4 @@ class GetSlackInstallationResponseSchema(Schema):
app_id = fields.Str(required=True)
bot_user_id = fields.Str(required=True)
access_token = fields.Str(required=True)
+ channel_id = fields.Str(required=True, allow_none=True)
diff --git a/application/bot/src/broker/schemas/new_slack_organization_event.py b/application/bot/src/broker/schemas/new_slack_organization_event.py
index cc9fcd04..22371281 100644
--- a/application/bot/src/broker/schemas/new_slack_organization_event.py
+++ b/application/bot/src/broker/schemas/new_slack_organization_event.py
@@ -3,3 +3,4 @@
class NewSlackOrganizationEventSchema(Schema):
team_id = fields.Str(required=True)
bot_token = fields.Str(required=True)
+ user_who_installed = fields.Str(required=True)
diff --git a/application/bot/src/broker/schemas/set_slack_channel.py b/application/bot/src/broker/schemas/set_slack_channel.py
index 1357eb9a..b21f7563 100644
--- a/application/bot/src/broker/schemas/set_slack_channel.py
+++ b/application/bot/src/broker/schemas/set_slack_channel.py
@@ -9,3 +9,4 @@ class SetSlackChannelRequestSchema(Schema):
class SetSlackChannelResponseSchema(Schema):
success = fields.Boolean(required=True)
old_channel_id = fields.Str()
+ scheduled_events_count = fields.Integer()
diff --git a/application/bot/src/broker/schemas/update_slack_user.py b/application/bot/src/broker/schemas/update_slack_user.py
index 42b01c99..a30d792f 100644
--- a/application/bot/src/broker/schemas/update_slack_user.py
+++ b/application/bot/src/broker/schemas/update_slack_user.py
@@ -5,6 +5,7 @@ class SlackUserUpdate(Schema):
current_username = fields.Str()
email = fields.Str()
team_id = fields.Str(required=True)
+ active = fields.Boolean()
class UpdateSlackUserRequestSchema(Schema):
users_to_update = fields.Nested(SlackUserUpdate, required=True, many=True)
diff --git a/application/bot/src/i18n.py b/application/bot/src/i18n.py
new file mode 100644
index 00000000..d02bc189
--- /dev/null
+++ b/application/bot/src/i18n.py
@@ -0,0 +1,53 @@
+import json
+import os
+import glob
+from src.injector import injector
+import logging
+from string import Template
+import pytz
+import datetime
+
+#valid timezones can be found here
+#https://gist.github.com/heyalexej/8bf688fd67d7199be4a1682b3eec7568
+lang_timezone_map = {
+ "no": "Europe/Oslo",
+ "en": "Europe/London"
+}
+
+supported_format = ["json"]
+
+class Translator:
+ def __init__(self, language_folder="./lang", default_locale="en") -> None:
+ self.data = {}
+ self.locale = default_locale
+ self.logger: logging.Logger = injector.get(logging.Logger)
+ self.timezone = pytz.timezone(lang_timezone_map[self.locale])
+
+ for filename in glob.glob(os.path.join(language_folder, '*.json')):
+ loc = os.path.splitext(os.path.basename(filename))[0]
+ with open(filename, encoding="utf-8", mode="r") as f:
+ self.data[loc] = json.load(f)
+
+ def set_locale(self, locale):
+ if locale in self.data and locale in lang_timezone_map:
+ self.locale = locale
+ self.timezone = pytz.timezone(lang_timezone_map[locale])
+ else:
+ self.logger.warn(f"Unvalid locale: {locale}, fallback to default locale: {self.locale}")
+
+ def translate(self, key, **kwargs):
+ if key in self.data[self.locale]:
+ text = self.data[self.locale][key]
+
+ try:
+ return Template(text).substitute(**kwargs)
+ except (KeyError, ValueError) as e:
+ self.logger.warn(e)
+ return Template(text).safe_substitute(**kwargs)
+
+ else:
+ self.logger.warn(f"The key '{key}' does not match any text. Defaults text to key")
+ return key
+
+ def format_timestamp(self, timestamp: datetime.datetime):
+ return pytz.utc.localize(timestamp.replace(tzinfo=None), is_dst=None).astimezone(self.timezone)
diff --git a/application/bot/src/lang/en.json b/application/bot/src/lang/en.json
new file mode 100644
index 00000000..32fe187b
--- /dev/null
+++ b/application/bot/src/lang/en.json
@@ -0,0 +1,35 @@
+{
+ "thanksForFile": "Thanks for the file! 🤙",
+ "pizzaChannelError": "Something went wrong. Couldn't set Pizza channel.",
+ "pizzaChannelErrorScheduledEvents": "There are $count scheduled events in the previous pizza channel. Please cancel them in the admin panel before switching channel.",
+ "pizzaChannelConfirm": "Pizza channel has now been set to <#$channel_id>",
+ "botWelcome": "Hello! I'm the pizza bot. If you want to change the channel I use, you can go to the appropriate channel and use the command '/set-pizza-channel'. If the channel is private, you need to add me first.",
+ "eventReminder": "Hey there! I didn't hear any more from you. Are you coming?",
+ "eventFinalized": "Hello there! $user_ids! You're going to have 🍕 at $restaurant_name, $time_stamp. $booker is booking the table!",
+ "eventUnfinalized": "Hello there! $user_ids! If the one who withdrew from the visit to $restaurant_name $time_stamp was supposed to book, then one of you others will have to take care of that. Meanwhile, a replacement is being sought.",
+ "userWithdrawAfterFinalization": "Hello there! <@$user_id> just withdrew from the visit to $restaurant_name $time_stamp.",
+ "autoReplyNoAttending": "Alright, I guess you can't or don't want to. Hope you'll join next time! 🤞",
+ "unfinalizedEventCancelled": "Hello there! The visit to $restaurant_name, $time_stamp has been canceled. Sorry!",
+ "finalizedEventCancelled": "Hello there! $user_ids! The visit to $restaurant_name, $time_stamp has been canceled. Sorry!",
+ "unfinalizedEventUpdate": "Hello there! The visit to $old_restaurant_name, $old_time_stamp has been updated to $restaurant_name, $time_stamp.",
+ "finalizedEventUpdate": "Hello there! $user_ids! The visit to $old_restaurant_name, $old_time_stamp has been updated to $restaurant_name, $time_stamp.",
+ "topLevelPizzaInvitation": "Pizza Invitation: $restaurant_name, $time_stamp",
+ "pizzaInvitationHeader": "Pizza Invitation",
+ "pizzaInvitationBody": "You're invited to have :pizza: at $restaurant_name, $time_stamp. Please respond within $deadline hours :pray:. Will you join in?",
+ "inviteLoading": ":hourglass_flowing_sand: Processing your request...",
+ "inviteNotAmongUsers": "Couldn't update the invitation. You were not among the invited.",
+ "pizzaInvitationAttendButton": "Hells yesss!!! 🍕🍕🍕",
+ "pizzaInvitationNoAttendButton": "Nah ☹️",
+ "pizzaInviteAnswerAttend": "You've accepted. Sweet! 🤙",
+ "pizzaInviteAnswerNoAttend": "You've declined. Ok 😕",
+ "unsubscribeBody": "If something comes up, you can unsubscribe by clicking the button!",
+ "unsubscribeButton": "Unsubscribe me",
+ "invalidatedEventCancelled": "The event has been canceled.",
+ "invitationExpired": "The invitation has expired.",
+ "inviteWithdrawn": "You've withdrawn. Ok 😕",
+ "inviteWithdrawnFailure": "The pizza event is over. Withdrawal is not possible.",
+ "invitedButLeftChannelUnanswered": "Since you left the PizzaBot channel, I assume you do not want to attend 😕",
+ "invitedButLeftChannelWithdrawn": "Since you left the PizzaBot channel, I assume you do not want to attend 😕 You have been withdrawn from the event",
+ "userLeftPizzaChannel": "You left the PizzaBot channel 😕 You will no longer receive invitations to future events.",
+ "adminPanelURLCommand": "The admin panel can be found <$adminPanelURL | here>"
+}
diff --git a/application/bot/src/lang/no.json b/application/bot/src/lang/no.json
new file mode 100644
index 00000000..9a8bc8f6
--- /dev/null
+++ b/application/bot/src/lang/no.json
@@ -0,0 +1,35 @@
+{
+ "thanksForFile": "Takk for fil! 🤙",
+ "pizzaChannelError": "Noe gikk galt. Klarte ikke å sette Pizzakanal",
+ "pizzaChannelErrorScheduledEvents": "Det er $count planlagte arrangementer i den forrige pizza kanalen. Vennligst slett i adminpanelet dem før du endrer kanal.",
+ "pizzaChannelConfirm": "Pizzakanal er nå satt til <#$channel_id>",
+ "botWelcome": "Hei! Jeg er pizzabot. Hvis dere vil endre hvilke kanal jeg bruker så kan dere gå inn i riktig kanal og bruke kommandoen '/set-pizza-channel'. Hvis kanalen er privat må dere legge meg til først.",
+ "eventReminder": "Hei du! Jeg hørte ikke noe mer? Er du gira?",
+ "eventFinalized": "Halloi! $user_ids! Dere skal spise 🍕 på $restaurant_name, $time_stamp. $booker booker bord!",
+ "eventUnfinalized": "Halloi! $user_ids! Hvis den som meldte seg av besøket til $restaurant_name $time_stamp skulle betale eller booke så må nesten en av dere andre sørge for det. I mellomtiden letes det etter en erstatter.",
+ "userWithdrawAfterFinalization": "Halloi! <@$user_id> meldte seg nettopp av besøket til $restaurant_name $time_stamp.",
+ "autoReplyNoAttending": "Neivel, da antar jeg du ikke kan/gidder. Håper du blir med neste gang! 🤞",
+ "unfinalizedEventCancelled": "Halloi! Besøket til $restaurant_name, $time_stamp har blitt kansellert. Sorry!",
+ "finalizedEventCnacelled": "Halloi! $user_ids! Besøket til $restaurant_name, $time_stamp har blitt kansellert. Sorry!",
+ "unfinalizedEventUpdate": "Halloi! Besøket til $old_restaurant_name, $old_time_stamp har blit endret til $restaurant_name, $time_stamp.",
+ "finalizedEvnetUpdate": "Halloi! $user_ids! Besøket til $old_restaurant_name, $old_time_stamp har blit endret til $restaurant_name, $time_stamp.",
+ "topLevelPizzaInvitation": "Pizzainvitasjon: $restaurant_name$, $time_stamp",
+ "pizzaInvitationHeader": "Pizzainvitasjon",
+ "pizzaInvitationBody": "Du er invitert til :pizza: på $restaurant_name, $time_stamp. Pls svar innen $deadline timer :pray:. Kan du?",
+ "inviteLoading": ":hourglass_flowing_sand: Behandler forespørselen din...",
+ "inviteNotAmongUsers": "Kunne ikke oppdatere invitasjonen. Du var ikke blant de inviterte.",
+ "pizzaInvitationAttendButton": "Hells yesss!!! 🍕🍕🍕",
+ "pizzaInvitationNoAttendButton": "Nah ☹️",
+ "pizzaInviteAnswerAttend": "Du har takket ja. Sweet! 🤙",
+ "pizzaInviteAnswerNoAttend": "Du har takket nei. Ok 😕",
+ "unsubscribeBody": "Hvis noe skulle skje så kan du melde deg av ved å klikke på knappen!",
+ "unsubscribeButton": "Meld meg av",
+ "invalidatedEventCacelled": "Arrangementet har blitt avlyst.",
+ "invitationExpired": "Invitasjonen er utløpt.",
+ "inviteWithdrawn": "Du har meldt deg av. Ok 😕",
+ "inviteWithdrawnFailure": "Arrangementet er over. Avmelding er ikke mulig.",
+ "invitedButLeftChannelUnanswered": "Siden du forlot PizzaBot kanalen, antar jeg du ikke vil være med 😕",
+ "invitedButLeftChannelWithdrawn": "Siden du forlot PizzaBot kanalen, antar jeg du ikke vil være med 😕 Du har blitt avmeldt arrangementet.",
+ "userLeftPizzaChannel": "Du har forlatt PizzaBot kanalen 😕 Du vil ikke lenger motta invitasjoner til fremtidige arrangementer.",
+ "adminPanelURLCommand": "Adminpanelet kan du finne <$adminPanelURL | her>"
+}
diff --git a/application/bot/src/slack/__init__.py b/application/bot/src/slack/__init__.py
index 29ec4257..4f57cafc 100644
--- a/application/bot/src/slack/__init__.py
+++ b/application/bot/src/slack/__init__.py
@@ -9,15 +9,19 @@
from slack_bolt.oauth.oauth_settings import OAuthSettings
from slack_sdk.oauth.state_store import FileOAuthStateStore
-from src.api.bot_api import BotApi, BotApiConfiguration
+from src.api.bot_api import BotApi
from src.injector import injector
from src.slack.installation_store import BrokerInstallationStore
from src.api.slack_api import SlackApi
+from src.i18n import Translator
+
slack_signing_secret = os.environ["SLACK_SIGNING_SECRET"]
client_id = os.environ["SLACK_CLIENT_ID"],
client_secret = os.environ["SLACK_CLIENT_SECRET"],
slack_app_token = os.environ["SLACK_APP_TOKEN"]
+frontend_uri = os.environ["FRONTEND_URI"] if "FRONTEND_URI" in os.environ else None
+
slack_app = App(
signing_secret=slack_signing_secret,
@@ -67,6 +71,21 @@ def handle_event(body, say, context):
if "subtype" in event and event["subtype"] == 'file_share':
handle_file_share(event=event, say=say, token=token, client=client)
+
+@slack_app.event("member_left_channel")
+@request_time_monitor()
+def handle_event(body, say, context):
+ event = body["event"]
+ client = SlackApi(client=context["client"])
+ # Handle a user leaving a channel
+ if "channel" in event and "user" in event and "team" in event:
+ channel_id = event["channel"]
+ team_id = event["team"]
+ user_id = event["user"]
+ with injector.get(BotApi) as ba:
+ ba.handle_user_left_channel(channel_id=channel_id, team_id=team_id, user_id=user_id, slack_client=client)
+
+
def handle_rsvp(body, ack, attending, client):
user = body["user"]
user_id = user["id"]
@@ -138,11 +157,13 @@ def handle_rsvp_withdraw(ack, body, context):
def handle_file_share(event, say, token, client):
+ return
+ translator = injector.get(Translator)
channel = event["channel"]
if 'files' in event and 'thread_ts' not in event:
files = event['files']
with injector.get(BotApi) as ba:
- ba.send_slack_message(channel_id=channel, text=u'Takk for fil! 🤙', slack_client=client)
+ ba.send_slack_message(channel_id=channel, text=translator.translate("thanksForFile"), slack_client=client)
headers = {u'Authorization': u'Bearer %s' % token}
for file in files:
r = requests.get(
@@ -150,7 +171,7 @@ def handle_file_share(event, say, token, client):
b64 = base64.b64encode(r.content).decode('utf-8')
payload = {
'file': 'data:image;base64,%s' % b64,
- 'upload_preset': 'blank.pizza.v2',
+ 'upload_preset': 'blank.pizza.v2', # TODO: Change to own preset based on pizzabot v3 and org
'tags': ','.join(['pizza', file['user_team']])
}
r2 = requests.post(
@@ -163,24 +184,35 @@ def handle_file_share(event, say, token, client):
@slack_app.command("/set-pizza-channel")
def handle_some_command(ack, body, say, context):
+ translator = injector.get(Translator)
ack()
with injector.get(BotApi) as ba:
team_id = body["team_id"]
message_channel_id = body["channel_id"]
client = SlackApi(client=context["client"])
channel_id = ba.join_channel(client, team_id, message_channel_id)
- if channel_id is None:
- ba.send_slack_message(
- channel_id=message_channel_id,
- text='Noe gikk galt. Klarte ikke å sette Pizza kanal',
- slack_client=client
- )
- else:
+ if channel_id is not None:
ba.send_slack_message(
channel_id=channel_id,
- text='Pizza kanal er nå satt til <#%s>' % channel_id,
+ text=translator.translate("pizzaChannelConfirm", channel_id=channel_id),
slack_client=client
)
+ ba.sync_users_from_organization(team_id=team_id, bot_token=context["token"])
+
+@slack_app.command("/pizzabot-admin-panel")
+def handle_some_command(ack, body, say, context):
+ translator = injector.get(Translator)
+ ack()
+ with injector.get(BotApi) as ba:
+ team_id = body["team_id"]
+ message_channel_id = body["channel_id"]
+ client = SlackApi(client=context["client"])
+
+ ba.send_slack_message(
+ channel_id=message_channel_id,
+ text=translator.translate("adminPanelURLCommand", adminPanelURL=f"{frontend_uri}/admin"),
+ slack_client=client
+ )
# This only exists to make bolt not throw a warning that we dont handle the file_shared event
# We dont use this as we use the message event with subtype file_shared as that one
diff --git a/application/bot/src/slack/lmdb.py b/application/bot/src/slack/lmdb.py
index 2ca59291..550c1f54 100644
--- a/application/bot/src/slack/lmdb.py
+++ b/application/bot/src/slack/lmdb.py
@@ -16,8 +16,15 @@ def put(self, key, value):
def get(self, key):
with self.env.begin() as txn:
value = txn.get(key.encode('utf-8'))
- if value is not None:
- value = value.decode('utf-8')
+
+ if value is None:
+ return None
+
+ value = value.decode('utf-8')
+
+ if value == "null":
+ return None
+
return value
def delete(self, key):
diff --git a/application/containers/development/.env.example b/application/containers/development/.env.example
new file mode 100644
index 00000000..3b50cd34
--- /dev/null
+++ b/application/containers/development/.env.example
@@ -0,0 +1,16 @@
+# -------------------- For development -------------------
+# Create a file named ".env" in this directory and copy the contents of this file with your credentials into it.
+# Slack variables
+SLACK_APP_TOKEN=
+SLACK_CLIENT_ID=
+SLACK_CLIENT_SECRET=
+SLACK_SIGNING_SECRET=
+
+# Cloudinary for image uploads (optional for development)
+CLOUDINARY_CLOUD_NAME=
+CLOUDINARY_API_KEY=
+CLOUDINARY_API_SECRET=
+
+# URI's - Dont change unless host and ports are changed
+FRONTEND_URI=https://localhost:4000 # exposed port routed from nginx
+BACKEND_URI=http://backend:3000 # docker-defined host w/ port from flask
diff --git a/application/containers/development/Dockerfile.next-frontend b/application/containers/development/Dockerfile.next-frontend
new file mode 100644
index 00000000..6c1ff79d
--- /dev/null
+++ b/application/containers/development/Dockerfile.next-frontend
@@ -0,0 +1,5 @@
+FROM node:20
+
+WORKDIR /srv/next-frontend
+
+CMD npm cache verify && npm install && npm run dev
diff --git a/application/containers/development/docker-compose.yml b/application/containers/development/docker-compose.yml
index 98b47b27..89bf33d4 100644
--- a/application/containers/development/docker-compose.yml
+++ b/application/containers/development/docker-compose.yml
@@ -1,19 +1,19 @@
-version: '3.8'
+version: "3.8"
x-common-variables: &common-variables
PYTHONUNBUFFERED: 1
PYTHONPATH: /srv/bot
x-common-slack-variables: &common-slack-variables
- SLACK_APP_TOKEN: ""
- SLACK_CLIENT_ID: ""
- SLACK_CLIENT_SECRET: ""
- SLACK_SIGNING_SECRET: ""
+ SLACK_APP_TOKEN: "${SLACK_APP_TOKEN}"
+ SLACK_CLIENT_ID: "${SLACK_CLIENT_ID}"
+ SLACK_CLIENT_SECRET: "${SLACK_CLIENT_SECRET}"
+ SLACK_SIGNING_SECRET: "${SLACK_SIGNING_SECRET}"
x-common-cloudinary-variables: &common-cloudinary-variables
- CLOUDINARY_CLOUD_NAME: ""
- CLOUDINARY_API_KEY: ""
- CLOUDINARY_API_SECRET: ""
+ CLOUDINARY_CLOUD_NAME: "${CLOUDINARY_CLOUD_NAME}"
+ CLOUDINARY_API_KEY: "${CLOUDINARY_API_KEY}"
+ CLOUDINARY_API_SECRET: "${CLOUDINARY_API_SECRET}"
x-common-rabbitmq-variables: &common-rabbitmq-variables
MQ_URL: amqp://guest:guest@rabbitmq:5672/%2F
@@ -38,6 +38,23 @@ services:
- backend
networks:
- proxy-network
+ next-frontend:
+ build:
+ context: ../../
+ dockerfile: containers/development/Dockerfile.next-frontend
+ expose:
+ - 4000
+ volumes:
+ - ../../next-frontend:/srv/next-frontend
+ environment:
+ - CHOKIDAR_USEPOLLING=true
+ - WDS_SOCKET_PORT=0
+ - WATCHPACK_POLLING=true
+ - NEXT_PUBLIC_BACKEND_URI=${BACKEND_URI}
+ depends_on:
+ - backend
+ networks:
+ - proxy-network
backend:
build:
context: ../../
@@ -59,16 +76,19 @@ services:
DB_PASSWD: "postgres"
DB_PORT: "5432"
DAYS_IN_ADVANCE_TO_INVITE: 10
- <<: *common-variables
- <<: *common-rabbitmq-variables
SECRET_KEY: "V3ryS3cr3tK3y"
FLASK_ENV: "production"
FLASK_APP: "main.py"
FLASK_RUN_HOST: "0.0.0.0"
FLASK_RUN_PORT: "3000"
- FRONTEND_URI: "https://localhost"
- <<: *common-slack-variables
- <<: *common-cloudinary-variables
+ FRONTEND_URI: "${FRONTEND_URI}"
+ <<:
+ [
+ *common-slack-variables,
+ *common-variables,
+ *common-rabbitmq-variables,
+ *common-cloudinary-variables,
+ ]
networks:
- database-network
- proxy-network
@@ -80,9 +100,9 @@ services:
volumes:
- ../../bot:/srv/bot
environment:
- <<: *common-variables
- <<: *common-rabbitmq-variables
- <<: *common-slack-variables
+ FRONTEND_URI: "${FRONTEND_URI}"
+ <<:
+ [*common-variables, *common-rabbitmq-variables, *common-slack-variables]
REPLY_DEADLINE_IN_HOURS: 24
HOURS_BETWEEN_REMINDERS: 4
restart: always
@@ -96,7 +116,7 @@ services:
- rabbitmq_network
rabbitmq:
image: rabbitmq:3-management-alpine
- container_name: 'rabbitmq'
+ container_name: "rabbitmq"
ports:
- 5672:5672
- 15672:15672
@@ -108,7 +128,7 @@ services:
timeout: 5s
retries: 5
database:
- image: postgres
+ image: postgres:15
restart: always
volumes:
- database_data:/var/lib/postgressql/data
@@ -131,14 +151,16 @@ services:
build:
context: ./
dockerfile: Dockerfile.nginx
- depends_on:
+ depends_on:
- frontend
+ - next-frontend
- backend
networks:
- proxy-network
ports:
- - 80:80
- - 443:443
+ - 8000:80
+ - 4434:443
+ - 4000:4000
networks:
database-network:
diff --git a/application/containers/development/nginx.conf b/application/containers/development/nginx.conf
index d3e9a634..0c93323c 100644
--- a/application/containers/development/nginx.conf
+++ b/application/containers/development/nginx.conf
@@ -4,6 +4,7 @@ server {
return 302 https://$host$request_uri;
}
+# react frontend
server {
client_body_buffer_size 32k;
client_header_buffer_size 8k;
@@ -46,6 +47,7 @@ server {
proxy_set_header Connection "upgrade";
}
+ # hot module reloading
location ^~ /ws {
proxy_pass http://frontend:3000;
proxy_set_header Host $host;
@@ -79,6 +81,97 @@ server {
proxy_set_header X-Forwarded-Proto $scheme;
}
+ location ^~ /doc {
+ if ($request_uri ~ ^([^.\?]*[^/])$) {
+ return 301 $1/;
+ }
+ rewrite /doc/(.*) /doc/$1 break;
+ proxy_pass http://backend:3000;
+
+ proxy_set_header Host $http_host;
+ proxy_set_header X-Real-IP $remote_addr;
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+ proxy_set_header X-Forwarded-Proto $scheme;
+ }
+}
+
+# next frontend
+server {
+ client_body_buffer_size 32k;
+ client_header_buffer_size 8k;
+ large_client_header_buffers 4 32k;
+ proxy_buffers 4 256k;
+ proxy_buffer_size 128k;
+ proxy_busy_buffers_size 256k;
+
+ listen 4000 default_server ssl;
+ listen [::]:4000 default_server ssl;
+
+ ssl_certificate /etc/ssl/certs/nginx-selfsigned.crt;
+ ssl_certificate_key /etc/ssl/private/nginx-selfsigned.key;
+
+ server_name test_server_2;
+
+ root /var/www/;
+ index index.html index.htm;
+
+ error_page 497 https://$host:4000$request_uri;
+
+ location / {
+ proxy_pass http://next-frontend:4000;
+ proxy_set_header Host $http_host;
+ proxy_set_header X-Real-IP $remote_addr;
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+ proxy_set_header X-Forwarded-Proto $scheme;
+ }
+
+ location ^~ /sockjs-node {
+ proxy_pass http://next-frontend:4000;
+ proxy_set_header Host $host;
+ proxy_set_header X-Real-IP $remote_addr;
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+
+ proxy_redirect off;
+
+ proxy_http_version 1.1;
+ proxy_set_header Upgrade $http_upgrade;
+ proxy_set_header Connection "upgrade";
+ }
+
+ # hot module reloading
+ location ^~ /_next {
+ proxy_pass http://next-frontend:4000;
+ proxy_set_header Host $host;
+ proxy_set_header X-Real-IP $remote_addr;
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+
+ proxy_redirect off;
+
+ proxy_http_version 1.1;
+ proxy_set_header Upgrade $http_upgrade;
+ proxy_set_header Connection "upgrade";
+ }
+
+ location ^~ /api {
+ rewrite /api/(.*) /api/$1 break;
+ proxy_pass http://backend:3000;
+
+ proxy_set_header Host $http_host;
+ proxy_set_header X-Real-IP $remote_addr;
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+ proxy_set_header X-Forwarded-Proto $scheme;
+ }
+
+ location ^~ /swagger {
+ rewrite /swagger/(.*) /doc/swagger break;
+ proxy_pass http://backend:3000;
+
+ proxy_set_header Host $http_host;
+ proxy_set_header X-Real-IP $remote_addr;
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+ proxy_set_header X-Forwarded-Proto $scheme;
+ }
+
location ^~ /doc {
if ($request_uri ~ ^([^.\?]*[^/])$) {
return 301 $1/;
diff --git a/application/frontend/.eslintrc.json b/application/frontend/.eslintrc.json
index 075289fb..1a6c073e 100644
--- a/application/frontend/.eslintrc.json
+++ b/application/frontend/.eslintrc.json
@@ -28,4 +28,4 @@
"version": "detect"
}
}
-}
\ No newline at end of file
+}
diff --git a/application/frontend/src/api/httpClient.ts b/application/frontend/src/api/httpClient.ts
index 9ca219c5..c717a38c 100644
--- a/application/frontend/src/api/httpClient.ts
+++ b/application/frontend/src/api/httpClient.ts
@@ -7,10 +7,19 @@ import { RefreshJWT } from './AuthService';
const baseUrl = process.env.BACKEND_URI ? `${process.env.BACKEND_URI.replace(/\/+$/, '')}/api` : '/api';
+function getCookie(name: string) {
+ const value = `; ${document.cookie}`;
+ const parts = value.split(`; ${name}=`);
+ if (parts && parts.length === 2) return parts.pop()?.split(';')?.shift();
+}
+
export const httpClient = (token?: string): AxiosInstance => {
const headers: AxiosRequestHeaders = {};
+ const cookie = getCookie('csrf_access_token');
- if (token) {
+ if (cookie) {
+ headers['X-CSRF-TOKEN'] = cookie;
+ } else if (token) {
headers.Authorization = `Bearer ${token}`;
}
diff --git a/application/frontend/src/assets/locales/nb/translation.json b/application/frontend/src/assets/locales/nb/translation.json
index d18322ec..d024cfad 100644
--- a/application/frontend/src/assets/locales/nb/translation.json
+++ b/application/frontend/src/assets/locales/nb/translation.json
@@ -16,7 +16,7 @@
"login": {
"logo": "Pizzabot av",
"loginButton": "Logg inn med Slack",
- "installButton": "Legg til PizzaBot?",
+ "installButton": "Legg til PizzaBot? ",
"error": {
"401": "Noe gikk galt. Er epostadressen din verifisert?",
"403": "Noe gikk galt. Er PizzaBot installert i ditt workspace?"
diff --git a/application/next-frontend/.eslintrc.json b/application/next-frontend/.eslintrc.json
new file mode 100644
index 00000000..abaad8b1
--- /dev/null
+++ b/application/next-frontend/.eslintrc.json
@@ -0,0 +1,12 @@
+{
+ "parser": "@typescript-eslint/parser",
+ "plugins": ["@typescript-eslint", "prettier", "tailwindcss"],
+ "extends": [
+ "next/core-web-vitals",
+ "plugin:@typescript-eslint/recommended",
+ "prettier",
+ "plugin:prettier/recommended",
+ "plugin:tailwindcss/recommended"
+ ],
+ "ignorePatterns": [".idea/*", "dist/*", "public/*", "node_modules/*", ".prettierrc.js", "webpack.*.js", ".next/*"]
+}
diff --git a/application/next-frontend/.gitignore b/application/next-frontend/.gitignore
new file mode 100644
index 00000000..70baf44c
--- /dev/null
+++ b/application/next-frontend/.gitignore
@@ -0,0 +1,32 @@
+# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
+
+# dependencies
+/node_modules
+/.pnp
+.pnp.js
+
+# testing
+/coverage
+
+# next.js
+/.next/
+/out/
+
+# misc
+.DS_Store
+*.pem
+
+# debug
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+
+# local env files
+.env*.local
+
+# vercel
+.vercel
+
+# typescript
+*.tsbuildinfo
+next-env.d.ts
diff --git a/application/next-frontend/.husky/pre-commit b/application/next-frontend/.husky/pre-commit
new file mode 100755
index 00000000..89e324ec
--- /dev/null
+++ b/application/next-frontend/.husky/pre-commit
@@ -0,0 +1,11 @@
+#!/bin/sh
+. "$(dirname "$0")/_/husky.sh"
+
+if ! git diff --cached --exit-code -s application/next-frontend
+then
+ echo "Files changed in next-frontend"
+ cd application/next-frontend
+ npx lint-staged
+ npm run typecheck
+ cd ../..
+fi
diff --git a/application/next-frontend/.prettierrc.js b/application/next-frontend/.prettierrc.js
new file mode 100644
index 00000000..7592c3c9
--- /dev/null
+++ b/application/next-frontend/.prettierrc.js
@@ -0,0 +1,7 @@
+module.exports = {
+ semi: false,
+ trailingComma: 'all',
+ singleQuote: true,
+ printWidth: 120,
+ tabWidth: 4,
+};
diff --git a/application/next-frontend/README.md b/application/next-frontend/README.md
new file mode 100644
index 00000000..965a1228
--- /dev/null
+++ b/application/next-frontend/README.md
@@ -0,0 +1,38 @@
+This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app).
+
+## Getting Started
+
+First, run the development server:
+
+```bash
+npm run dev
+# or
+yarn dev
+# or
+pnpm dev
+```
+
+Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
+
+You can start editing the page by modifying `pages/index.tsx`. The page auto-updates as you edit the file.
+
+[API routes](https://nextjs.org/docs/api-routes/introduction) can be accessed on [http://localhost:3000/api/hello](http://localhost:3000/api/hello). This endpoint can be edited in `pages/api/hello.ts`.
+
+The `pages/api` directory is mapped to `/api/*`. Files in this directory are treated as [API routes](https://nextjs.org/docs/api-routes/introduction) instead of React pages.
+
+This project uses [`next/font`](https://nextjs.org/docs/basic-features/font-optimization) to automatically optimize and load Inter, a custom Google Font.
+
+## Learn More
+
+To learn more about Next.js, take a look at the following resources:
+
+- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
+- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
+
+You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome!
+
+## Deploy on Vercel
+
+The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
+
+Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details.
diff --git a/application/next-frontend/middleware.ts b/application/next-frontend/middleware.ts
new file mode 100644
index 00000000..ccd9a2cd
--- /dev/null
+++ b/application/next-frontend/middleware.ts
@@ -0,0 +1,10 @@
+import { NextFetchEvent, NextRequest, NextResponse } from 'next/server'
+
+type Environment = 'production' | 'development' | 'test'
+export function middleware(req: NextRequest, ev: NextFetchEvent) {
+ const currentEnv: Environment = process.env.NODE_ENV
+ if (currentEnv === 'production' && req.nextUrl.protocol !== 'https:') {
+ return NextResponse.redirect(`https://${req.headers.get('host')}${req.nextUrl.pathname}`, 301)
+ }
+ return NextResponse.next()
+}
diff --git a/application/next-frontend/next.config.js b/application/next-frontend/next.config.js
new file mode 100644
index 00000000..38e25b4d
--- /dev/null
+++ b/application/next-frontend/next.config.js
@@ -0,0 +1,5 @@
+/** @type {import('next').NextConfig} */
+
+const nextConfig = { reactStrictMode: true }
+
+module.export = nextConfig
diff --git a/application/next-frontend/package-lock.json b/application/next-frontend/package-lock.json
new file mode 100644
index 00000000..8f375711
--- /dev/null
+++ b/application/next-frontend/package-lock.json
@@ -0,0 +1,6233 @@
+{
+ "name": "next-frontend",
+ "version": "0.1.0",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {
+ "": {
+ "name": "next-frontend",
+ "version": "0.1.0",
+ "dependencies": {
+ "@emotion/react": "^11.11.1",
+ "@emotion/styled": "^11.11.0",
+ "@hookform/resolvers": "^3.2.0",
+ "@mui/material": "^5.14.5",
+ "autoprefixer": "10.4.14",
+ "jwt-decode": "^3.1.2",
+ "next": "13.4.13",
+ "ordinal": "^1.0.3",
+ "postcss": "8.4.27",
+ "react": "18.2.0",
+ "react-datepicker": "^4.16.0",
+ "react-dom": "18.2.0",
+ "react-hook-form": "^7.45.4",
+ "react-hot-toast": "^2.4.1",
+ "swr": "^2.2.1",
+ "tailwindcss": "3.3.3",
+ "zod": "^3.22.2"
+ },
+ "devDependencies": {
+ "@types/node": "20.4.9",
+ "@types/react": "18.2.20",
+ "@types/react-datepicker": "^4.15.0",
+ "@types/react-dom": "18.2.7",
+ "@typescript-eslint/eslint-plugin": "^5.33.1",
+ "eslint": "8.46.0",
+ "eslint-config-next": "13.4.13",
+ "eslint-config-prettier": "9.0.0",
+ "eslint-plugin-prettier": "5.0.0",
+ "eslint-plugin-tailwindcss": "^3.13.0",
+ "husky": "^8.0.1",
+ "lint-staged": "^13.0.3",
+ "prettier": "3.0.1",
+ "prettier-plugin-tailwindcss": "^0.4.1",
+ "typescript": "5.1.6"
+ },
+ "engines": {
+ "node": ">=20.x"
+ }
+ },
+ "node_modules/@aashutoshrathi/word-wrap": {
+ "version": "1.2.6",
+ "resolved": "https://registry.npmjs.org/@aashutoshrathi/word-wrap/-/word-wrap-1.2.6.tgz",
+ "integrity": "sha512-1Yjs2SvM8TflER/OD3cOjhWWOZb58A2t7wpE2S9XfBYTiIl+XFhQG2bjy4Pu1I+EAlCNUzRDYDdFwFYUKvXcIA==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/@alloc/quick-lru": {
+ "version": "5.2.0",
+ "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz",
+ "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/@babel/code-frame": {
+ "version": "7.22.13",
+ "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.22.13.tgz",
+ "integrity": "sha512-XktuhWlJ5g+3TJXc5upd9Ks1HutSArik6jf2eAjYFyIOf4ej3RN+184cZbzDvbPnuTJIUhPKKJE3cIsYTiAT3w==",
+ "dependencies": {
+ "@babel/highlight": "^7.22.13",
+ "chalk": "^2.4.2"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/code-frame/node_modules/ansi-styles": {
+ "version": "3.2.1",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz",
+ "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==",
+ "dependencies": {
+ "color-convert": "^1.9.0"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/@babel/code-frame/node_modules/chalk": {
+ "version": "2.4.2",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz",
+ "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==",
+ "dependencies": {
+ "ansi-styles": "^3.2.1",
+ "escape-string-regexp": "^1.0.5",
+ "supports-color": "^5.3.0"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/@babel/code-frame/node_modules/color-convert": {
+ "version": "1.9.3",
+ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz",
+ "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==",
+ "dependencies": {
+ "color-name": "1.1.3"
+ }
+ },
+ "node_modules/@babel/code-frame/node_modules/color-name": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz",
+ "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw=="
+ },
+ "node_modules/@babel/code-frame/node_modules/escape-string-regexp": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz",
+ "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==",
+ "engines": {
+ "node": ">=0.8.0"
+ }
+ },
+ "node_modules/@babel/code-frame/node_modules/has-flag": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz",
+ "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==",
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/@babel/code-frame/node_modules/supports-color": {
+ "version": "5.5.0",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
+ "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==",
+ "dependencies": {
+ "has-flag": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/@babel/helper-module-imports": {
+ "version": "7.22.15",
+ "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.22.15.tgz",
+ "integrity": "sha512-0pYVBnDKZO2fnSPCrgM/6WMc7eS20Fbok+0r88fp+YtWVLZrp4CkafFGIp+W0VKw4a22sgebPT99y+FDNMdP4w==",
+ "dependencies": {
+ "@babel/types": "^7.22.15"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-string-parser": {
+ "version": "7.22.5",
+ "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.22.5.tgz",
+ "integrity": "sha512-mM4COjgZox8U+JcXQwPijIZLElkgEpO5rsERVDJTc2qfCDfERyob6k5WegS14SX18IIjv+XD+GrqNumY5JRCDw==",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-validator-identifier": {
+ "version": "7.22.20",
+ "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz",
+ "integrity": "sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/highlight": {
+ "version": "7.22.20",
+ "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.22.20.tgz",
+ "integrity": "sha512-dkdMCN3py0+ksCgYmGG8jKeGA/8Tk+gJwSYYlFGxG5lmhfKNoAy004YpLxpS1W2J8m/EK2Ew+yOs9pVRwO89mg==",
+ "dependencies": {
+ "@babel/helper-validator-identifier": "^7.22.20",
+ "chalk": "^2.4.2",
+ "js-tokens": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/highlight/node_modules/ansi-styles": {
+ "version": "3.2.1",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz",
+ "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==",
+ "dependencies": {
+ "color-convert": "^1.9.0"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/@babel/highlight/node_modules/chalk": {
+ "version": "2.4.2",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz",
+ "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==",
+ "dependencies": {
+ "ansi-styles": "^3.2.1",
+ "escape-string-regexp": "^1.0.5",
+ "supports-color": "^5.3.0"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/@babel/highlight/node_modules/color-convert": {
+ "version": "1.9.3",
+ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz",
+ "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==",
+ "dependencies": {
+ "color-name": "1.1.3"
+ }
+ },
+ "node_modules/@babel/highlight/node_modules/color-name": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz",
+ "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw=="
+ },
+ "node_modules/@babel/highlight/node_modules/escape-string-regexp": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz",
+ "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==",
+ "engines": {
+ "node": ">=0.8.0"
+ }
+ },
+ "node_modules/@babel/highlight/node_modules/has-flag": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz",
+ "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==",
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/@babel/highlight/node_modules/supports-color": {
+ "version": "5.5.0",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
+ "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==",
+ "dependencies": {
+ "has-flag": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/@babel/runtime": {
+ "version": "7.22.10",
+ "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.22.10.tgz",
+ "integrity": "sha512-21t/fkKLMZI4pqP2wlmsQAWnYW1PDyKyyUV4vCi+B25ydmdaYTKXPwCj0BzSUnZf4seIiYvSA3jcZ3gdsMFkLQ==",
+ "dependencies": {
+ "regenerator-runtime": "^0.14.0"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/types": {
+ "version": "7.22.19",
+ "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.22.19.tgz",
+ "integrity": "sha512-P7LAw/LbojPzkgp5oznjE6tQEIWbp4PkkfrZDINTro9zgBRtI324/EYsiSI7lhPbpIQ+DCeR2NNmMWANGGfZsg==",
+ "dependencies": {
+ "@babel/helper-string-parser": "^7.22.5",
+ "@babel/helper-validator-identifier": "^7.22.19",
+ "to-fast-properties": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@emotion/babel-plugin": {
+ "version": "11.11.0",
+ "resolved": "https://registry.npmjs.org/@emotion/babel-plugin/-/babel-plugin-11.11.0.tgz",
+ "integrity": "sha512-m4HEDZleaaCH+XgDDsPF15Ht6wTLsgDTeR3WYj9Q/k76JtWhrJjcP4+/XlG8LGT/Rol9qUfOIztXeA84ATpqPQ==",
+ "dependencies": {
+ "@babel/helper-module-imports": "^7.16.7",
+ "@babel/runtime": "^7.18.3",
+ "@emotion/hash": "^0.9.1",
+ "@emotion/memoize": "^0.8.1",
+ "@emotion/serialize": "^1.1.2",
+ "babel-plugin-macros": "^3.1.0",
+ "convert-source-map": "^1.5.0",
+ "escape-string-regexp": "^4.0.0",
+ "find-root": "^1.1.0",
+ "source-map": "^0.5.7",
+ "stylis": "4.2.0"
+ }
+ },
+ "node_modules/@emotion/cache": {
+ "version": "11.11.0",
+ "resolved": "https://registry.npmjs.org/@emotion/cache/-/cache-11.11.0.tgz",
+ "integrity": "sha512-P34z9ssTCBi3e9EI1ZsWpNHcfY1r09ZO0rZbRO2ob3ZQMnFI35jB536qoXbkdesr5EUhYi22anuEJuyxifaqAQ==",
+ "dependencies": {
+ "@emotion/memoize": "^0.8.1",
+ "@emotion/sheet": "^1.2.2",
+ "@emotion/utils": "^1.2.1",
+ "@emotion/weak-memoize": "^0.3.1",
+ "stylis": "4.2.0"
+ }
+ },
+ "node_modules/@emotion/hash": {
+ "version": "0.9.1",
+ "resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.9.1.tgz",
+ "integrity": "sha512-gJB6HLm5rYwSLI6PQa+X1t5CFGrv1J1TWG+sOyMCeKz2ojaj6Fnl/rZEspogG+cvqbt4AE/2eIyD2QfLKTBNlQ=="
+ },
+ "node_modules/@emotion/is-prop-valid": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-1.2.1.tgz",
+ "integrity": "sha512-61Mf7Ufx4aDxx1xlDeOm8aFFigGHE4z+0sKCa+IHCeZKiyP9RLD0Mmx7m8b9/Cf37f7NAvQOOJAbQQGVr5uERw==",
+ "dependencies": {
+ "@emotion/memoize": "^0.8.1"
+ }
+ },
+ "node_modules/@emotion/memoize": {
+ "version": "0.8.1",
+ "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.8.1.tgz",
+ "integrity": "sha512-W2P2c/VRW1/1tLox0mVUalvnWXxavmv/Oum2aPsRcoDJuob75FC3Y8FbpfLwUegRcxINtGUMPq0tFCvYNTBXNA=="
+ },
+ "node_modules/@emotion/react": {
+ "version": "11.11.1",
+ "resolved": "https://registry.npmjs.org/@emotion/react/-/react-11.11.1.tgz",
+ "integrity": "sha512-5mlW1DquU5HaxjLkfkGN1GA/fvVGdyHURRiX/0FHl2cfIfRxSOfmxEH5YS43edp0OldZrZ+dkBKbngxcNCdZvA==",
+ "dependencies": {
+ "@babel/runtime": "^7.18.3",
+ "@emotion/babel-plugin": "^11.11.0",
+ "@emotion/cache": "^11.11.0",
+ "@emotion/serialize": "^1.1.2",
+ "@emotion/use-insertion-effect-with-fallbacks": "^1.0.1",
+ "@emotion/utils": "^1.2.1",
+ "@emotion/weak-memoize": "^0.3.1",
+ "hoist-non-react-statics": "^3.3.1"
+ },
+ "peerDependencies": {
+ "react": ">=16.8.0"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@emotion/serialize": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/@emotion/serialize/-/serialize-1.1.2.tgz",
+ "integrity": "sha512-zR6a/fkFP4EAcCMQtLOhIgpprZOwNmCldtpaISpvz348+DP4Mz8ZoKaGGCQpbzepNIUWbq4w6hNZkwDyKoS+HA==",
+ "dependencies": {
+ "@emotion/hash": "^0.9.1",
+ "@emotion/memoize": "^0.8.1",
+ "@emotion/unitless": "^0.8.1",
+ "@emotion/utils": "^1.2.1",
+ "csstype": "^3.0.2"
+ }
+ },
+ "node_modules/@emotion/sheet": {
+ "version": "1.2.2",
+ "resolved": "https://registry.npmjs.org/@emotion/sheet/-/sheet-1.2.2.tgz",
+ "integrity": "sha512-0QBtGvaqtWi+nx6doRwDdBIzhNdZrXUppvTM4dtZZWEGTXL/XE/yJxLMGlDT1Gt+UHH5IX1n+jkXyytE/av7OA=="
+ },
+ "node_modules/@emotion/styled": {
+ "version": "11.11.0",
+ "resolved": "https://registry.npmjs.org/@emotion/styled/-/styled-11.11.0.tgz",
+ "integrity": "sha512-hM5Nnvu9P3midq5aaXj4I+lnSfNi7Pmd4EWk1fOZ3pxookaQTNew6bp4JaCBYM4HVFZF9g7UjJmsUmC2JlxOng==",
+ "dependencies": {
+ "@babel/runtime": "^7.18.3",
+ "@emotion/babel-plugin": "^11.11.0",
+ "@emotion/is-prop-valid": "^1.2.1",
+ "@emotion/serialize": "^1.1.2",
+ "@emotion/use-insertion-effect-with-fallbacks": "^1.0.1",
+ "@emotion/utils": "^1.2.1"
+ },
+ "peerDependencies": {
+ "@emotion/react": "^11.0.0-rc.0",
+ "react": ">=16.8.0"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@emotion/unitless": {
+ "version": "0.8.1",
+ "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.8.1.tgz",
+ "integrity": "sha512-KOEGMu6dmJZtpadb476IsZBclKvILjopjUii3V+7MnXIQCYh8W3NgNcgwo21n9LXZX6EDIKvqfjYxXebDwxKmQ=="
+ },
+ "node_modules/@emotion/use-insertion-effect-with-fallbacks": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/@emotion/use-insertion-effect-with-fallbacks/-/use-insertion-effect-with-fallbacks-1.0.1.tgz",
+ "integrity": "sha512-jT/qyKZ9rzLErtrjGgdkMBn2OP8wl0G3sQlBb3YPryvKHsjvINUhVaPFfP+fpBcOkmrVOVEEHQFJ7nbj2TH2gw==",
+ "peerDependencies": {
+ "react": ">=16.8.0"
+ }
+ },
+ "node_modules/@emotion/utils": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/@emotion/utils/-/utils-1.2.1.tgz",
+ "integrity": "sha512-Y2tGf3I+XVnajdItskUCn6LX+VUDmP6lTL4fcqsXAv43dnlbZiuW4MWQW38rW/BVWSE7Q/7+XQocmpnRYILUmg=="
+ },
+ "node_modules/@emotion/weak-memoize": {
+ "version": "0.3.1",
+ "resolved": "https://registry.npmjs.org/@emotion/weak-memoize/-/weak-memoize-0.3.1.tgz",
+ "integrity": "sha512-EsBwpc7hBUJWAsNPBmJy4hxWx12v6bshQsldrVmjxJoc3isbxhOrF2IcCpaXxfvq03NwkI7sbsOLXbYuqF/8Ww=="
+ },
+ "node_modules/@eslint-community/eslint-utils": {
+ "version": "4.4.0",
+ "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz",
+ "integrity": "sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==",
+ "dev": true,
+ "dependencies": {
+ "eslint-visitor-keys": "^3.3.0"
+ },
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "peerDependencies": {
+ "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0"
+ }
+ },
+ "node_modules/@eslint-community/regexpp": {
+ "version": "4.6.2",
+ "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.6.2.tgz",
+ "integrity": "sha512-pPTNuaAG3QMH+buKyBIGJs3g/S5y0caxw0ygM3YyE6yJFySwiGGSzA+mM3KJ8QQvzeLh3blwgSonkFjgQdxzMw==",
+ "dev": true,
+ "engines": {
+ "node": "^12.0.0 || ^14.0.0 || >=16.0.0"
+ }
+ },
+ "node_modules/@eslint/eslintrc": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.1.tgz",
+ "integrity": "sha512-9t7ZA7NGGK8ckelF0PQCfcxIUzs1Md5rrO6U/c+FIQNanea5UZC0wqKXH4vHBccmu4ZJgZ2idtPeW7+Q2npOEA==",
+ "dev": true,
+ "dependencies": {
+ "ajv": "^6.12.4",
+ "debug": "^4.3.2",
+ "espree": "^9.6.0",
+ "globals": "^13.19.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"
+ },
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/@eslint/js": {
+ "version": "8.46.0",
+ "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.46.0.tgz",
+ "integrity": "sha512-a8TLtmPi8xzPkCbp/OGFUo5yhRkHM2Ko9kOWP4znJr0WAhWyThaw3PnwX4vOTWOAMsV2uRt32PPDcEz63esSaA==",
+ "dev": true,
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ }
+ },
+ "node_modules/@hookform/resolvers": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/@hookform/resolvers/-/resolvers-3.2.0.tgz",
+ "integrity": "sha512-skXQHhLxq0Sz2xDwCyv5dygBCtXJe1GmWwxDzfdtl0X6agD6qcyTG8HrZWkjJoy8AkiLARqYvSYJ8z7+Nwmi7w==",
+ "peerDependencies": {
+ "react-hook-form": "^7.0.0"
+ }
+ },
+ "node_modules/@humanwhocodes/config-array": {
+ "version": "0.11.10",
+ "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.10.tgz",
+ "integrity": "sha512-KVVjQmNUepDVGXNuoRRdmmEjruj0KfiGSbS8LVc12LMsWDQzRXJ0qdhN8L8uUigKpfEHRhlaQFY0ib1tnUbNeQ==",
+ "dev": true,
+ "dependencies": {
+ "@humanwhocodes/object-schema": "^1.2.1",
+ "debug": "^4.1.1",
+ "minimatch": "^3.0.5"
+ },
+ "engines": {
+ "node": ">=10.10.0"
+ }
+ },
+ "node_modules/@humanwhocodes/module-importer": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz",
+ "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==",
+ "dev": true,
+ "engines": {
+ "node": ">=12.22"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/nzakas"
+ }
+ },
+ "node_modules/@humanwhocodes/object-schema": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz",
+ "integrity": "sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==",
+ "dev": true
+ },
+ "node_modules/@jridgewell/gen-mapping": {
+ "version": "0.3.3",
+ "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz",
+ "integrity": "sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ==",
+ "dependencies": {
+ "@jridgewell/set-array": "^1.0.1",
+ "@jridgewell/sourcemap-codec": "^1.4.10",
+ "@jridgewell/trace-mapping": "^0.3.9"
+ },
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/@jridgewell/resolve-uri": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.1.tgz",
+ "integrity": "sha512-dSYZh7HhCDtCKm4QakX0xFpsRDqjjtZf/kjI/v3T3Nwt5r8/qz/M19F9ySyOqU94SXBmeG9ttTul+YnR4LOxFA==",
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/@jridgewell/set-array": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.2.tgz",
+ "integrity": "sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==",
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/@jridgewell/sourcemap-codec": {
+ "version": "1.4.15",
+ "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz",
+ "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg=="
+ },
+ "node_modules/@jridgewell/trace-mapping": {
+ "version": "0.3.19",
+ "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.19.tgz",
+ "integrity": "sha512-kf37QtfW+Hwx/buWGMPcR60iF9ziHa6r/CZJIHbmcm4+0qrXiVdxegAH0F6yddEVQ7zdkjcGCgCzUu+BcbhQxw==",
+ "dependencies": {
+ "@jridgewell/resolve-uri": "^3.1.0",
+ "@jridgewell/sourcemap-codec": "^1.4.14"
+ }
+ },
+ "node_modules/@mui/base": {
+ "version": "5.0.0-beta.11",
+ "resolved": "https://registry.npmjs.org/@mui/base/-/base-5.0.0-beta.11.tgz",
+ "integrity": "sha512-FdKZGPd8qmC3ZNke7CNhzcEgToc02M6WYZc9hcBsNQ17bgAd3s9F//1bDDYgMVBYxDM71V0sv/hBHlOY4I1ZVA==",
+ "dependencies": {
+ "@babel/runtime": "^7.22.6",
+ "@emotion/is-prop-valid": "^1.2.1",
+ "@mui/types": "^7.2.4",
+ "@mui/utils": "^5.14.5",
+ "@popperjs/core": "^2.11.8",
+ "clsx": "^2.0.0",
+ "prop-types": "^15.8.1",
+ "react-is": "^18.2.0"
+ },
+ "engines": {
+ "node": ">=12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/mui"
+ },
+ "peerDependencies": {
+ "@types/react": "^17.0.0 || ^18.0.0",
+ "react": "^17.0.0 || ^18.0.0",
+ "react-dom": "^17.0.0 || ^18.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@mui/base/node_modules/react-is": {
+ "version": "18.2.0",
+ "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz",
+ "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w=="
+ },
+ "node_modules/@mui/core-downloads-tracker": {
+ "version": "5.14.5",
+ "resolved": "https://registry.npmjs.org/@mui/core-downloads-tracker/-/core-downloads-tracker-5.14.5.tgz",
+ "integrity": "sha512-+wpGH1USwPcKMFPMvXqYPC6fEvhxM3FzxC8lyDiNK/imLyyJ6y2DPb1Oue7OGIKJWBmYBqrWWtfovrxd1aJHTA==",
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/mui"
+ }
+ },
+ "node_modules/@mui/material": {
+ "version": "5.14.5",
+ "resolved": "https://registry.npmjs.org/@mui/material/-/material-5.14.5.tgz",
+ "integrity": "sha512-4qa4GMfuZH0Ai3mttk5ccXP8a3sf7aPlAJwyMrUSz6h9hPri6BPou94zeu3rENhhmKLby9S/W1y+pmficy8JKA==",
+ "dependencies": {
+ "@babel/runtime": "^7.22.6",
+ "@mui/base": "5.0.0-beta.11",
+ "@mui/core-downloads-tracker": "^5.14.5",
+ "@mui/system": "^5.14.5",
+ "@mui/types": "^7.2.4",
+ "@mui/utils": "^5.14.5",
+ "@types/react-transition-group": "^4.4.6",
+ "clsx": "^2.0.0",
+ "csstype": "^3.1.2",
+ "prop-types": "^15.8.1",
+ "react-is": "^18.2.0",
+ "react-transition-group": "^4.4.5"
+ },
+ "engines": {
+ "node": ">=12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/mui"
+ },
+ "peerDependencies": {
+ "@emotion/react": "^11.5.0",
+ "@emotion/styled": "^11.3.0",
+ "@types/react": "^17.0.0 || ^18.0.0",
+ "react": "^17.0.0 || ^18.0.0",
+ "react-dom": "^17.0.0 || ^18.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@emotion/react": {
+ "optional": true
+ },
+ "@emotion/styled": {
+ "optional": true
+ },
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@mui/material/node_modules/react-is": {
+ "version": "18.2.0",
+ "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz",
+ "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w=="
+ },
+ "node_modules/@mui/private-theming": {
+ "version": "5.14.5",
+ "resolved": "https://registry.npmjs.org/@mui/private-theming/-/private-theming-5.14.5.tgz",
+ "integrity": "sha512-cC4C5RrpXpDaaZyH9QwmPhRLgz+f2SYbOty3cPkk4qPSOSfif2ZEcDD9HTENKDDd9deB+xkPKzzZhi8cxIx8Ig==",
+ "dependencies": {
+ "@babel/runtime": "^7.22.6",
+ "@mui/utils": "^5.14.5",
+ "prop-types": "^15.8.1"
+ },
+ "engines": {
+ "node": ">=12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/mui"
+ },
+ "peerDependencies": {
+ "@types/react": "^17.0.0 || ^18.0.0",
+ "react": "^17.0.0 || ^18.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@mui/styled-engine": {
+ "version": "5.13.2",
+ "resolved": "https://registry.npmjs.org/@mui/styled-engine/-/styled-engine-5.13.2.tgz",
+ "integrity": "sha512-VCYCU6xVtXOrIN8lcbuPmoG+u7FYuOERG++fpY74hPpEWkyFQG97F+/XfTQVYzlR2m7nPjnwVUgATcTCMEaMvw==",
+ "dependencies": {
+ "@babel/runtime": "^7.21.0",
+ "@emotion/cache": "^11.11.0",
+ "csstype": "^3.1.2",
+ "prop-types": "^15.8.1"
+ },
+ "engines": {
+ "node": ">=12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/mui"
+ },
+ "peerDependencies": {
+ "@emotion/react": "^11.4.1",
+ "@emotion/styled": "^11.3.0",
+ "react": "^17.0.0 || ^18.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@emotion/react": {
+ "optional": true
+ },
+ "@emotion/styled": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@mui/system": {
+ "version": "5.14.5",
+ "resolved": "https://registry.npmjs.org/@mui/system/-/system-5.14.5.tgz",
+ "integrity": "sha512-mextXZHDeGcR7E1kx43TRARrVXy+gI4wzpUgNv7MqZs1dvTVXQGVeAT6ydj9d6FUqHBPMNLGV/21vJOrpqsL+w==",
+ "dependencies": {
+ "@babel/runtime": "^7.22.6",
+ "@mui/private-theming": "^5.14.5",
+ "@mui/styled-engine": "^5.13.2",
+ "@mui/types": "^7.2.4",
+ "@mui/utils": "^5.14.5",
+ "clsx": "^2.0.0",
+ "csstype": "^3.1.2",
+ "prop-types": "^15.8.1"
+ },
+ "engines": {
+ "node": ">=12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/mui"
+ },
+ "peerDependencies": {
+ "@emotion/react": "^11.5.0",
+ "@emotion/styled": "^11.3.0",
+ "@types/react": "^17.0.0 || ^18.0.0",
+ "react": "^17.0.0 || ^18.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@emotion/react": {
+ "optional": true
+ },
+ "@emotion/styled": {
+ "optional": true
+ },
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@mui/types": {
+ "version": "7.2.4",
+ "resolved": "https://registry.npmjs.org/@mui/types/-/types-7.2.4.tgz",
+ "integrity": "sha512-LBcwa8rN84bKF+f5sDyku42w1NTxaPgPyYKODsh01U1fVstTClbUoSA96oyRBnSNyEiAVjKm6Gwx9vjR+xyqHA==",
+ "peerDependencies": {
+ "@types/react": "*"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@mui/utils": {
+ "version": "5.14.5",
+ "resolved": "https://registry.npmjs.org/@mui/utils/-/utils-5.14.5.tgz",
+ "integrity": "sha512-6Hzw63VR9C5xYv+CbjndoRLU6Gntal8rJ5W+GUzkyHrGWIyYPWZPa6AevnyGioySNETATe1H9oXS8f/7qgIHJA==",
+ "dependencies": {
+ "@babel/runtime": "^7.22.6",
+ "@types/prop-types": "^15.7.5",
+ "@types/react-is": "^18.2.1",
+ "prop-types": "^15.8.1",
+ "react-is": "^18.2.0"
+ },
+ "engines": {
+ "node": ">=12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/mui"
+ },
+ "peerDependencies": {
+ "react": "^17.0.0 || ^18.0.0"
+ }
+ },
+ "node_modules/@mui/utils/node_modules/react-is": {
+ "version": "18.2.0",
+ "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz",
+ "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w=="
+ },
+ "node_modules/@next/env": {
+ "version": "13.4.13",
+ "resolved": "https://registry.npmjs.org/@next/env/-/env-13.4.13.tgz",
+ "integrity": "sha512-fwz2QgVg08v7ZL7KmbQBLF2PubR/6zQdKBgmHEl3BCyWTEDsAQEijjw2gbFhI1tcKfLdOOJUXntz5vZ4S0Polg=="
+ },
+ "node_modules/@next/eslint-plugin-next": {
+ "version": "13.4.13",
+ "resolved": "https://registry.npmjs.org/@next/eslint-plugin-next/-/eslint-plugin-next-13.4.13.tgz",
+ "integrity": "sha512-RpZeXlPxQ9FLeYN84XHDqRN20XxmVNclYCraLYdifRsmibtcWUWdwE/ANp2C8kgesFRsvwfsw6eOkYNl9sLJ3A==",
+ "dev": true,
+ "dependencies": {
+ "glob": "7.1.7"
+ }
+ },
+ "node_modules/@next/swc-darwin-arm64": {
+ "version": "13.4.13",
+ "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-13.4.13.tgz",
+ "integrity": "sha512-ZptVhHjzUuivnXMNCJ6lER33HN7lC+rZ01z+PM10Ows21NHFYMvGhi5iXkGtBDk6VmtzsbqnAjnx4Oz5um0FjA==",
+ "cpu": [
+ "arm64"
+ ],
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/@next/swc-darwin-x64": {
+ "version": "13.4.13",
+ "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-13.4.13.tgz",
+ "integrity": "sha512-t9nTiWCLApw8W4G1kqJyYP7y6/7lyal3PftmRturIxAIBlZss9wrtVN8nci50StDHmIlIDxfguYIEGVr9DbFTg==",
+ "cpu": [
+ "x64"
+ ],
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/@next/swc-linux-arm64-gnu": {
+ "version": "13.4.13",
+ "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-13.4.13.tgz",
+ "integrity": "sha512-xEHUqC8eqR5DHe8SOmMnDU1K3ggrJ28uIKltrQAwqFSSSmzjnN/XMocZkcVhuncuxYrpbri0iMQstRyRVdQVWg==",
+ "cpu": [
+ "arm64"
+ ],
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/@next/swc-linux-arm64-musl": {
+ "version": "13.4.13",
+ "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-13.4.13.tgz",
+ "integrity": "sha512-sNf3MnLAm8rquSSAoeD9nVcdaDeRYOeey4stOWOyWIgbBDtP+C93amSgH/LPTDoUV7gNiU6f+ghepTjTjRgIUQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/@next/swc-linux-x64-gnu": {
+ "version": "13.4.13",
+ "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-13.4.13.tgz",
+ "integrity": "sha512-WhcRaJJSHyx9OWmKjjz+OWHumiPZWRqmM/09Bt7Up4UqUJFFhGExeztR4trtv3rflvULatu9IH/nTV8fUUgaMA==",
+ "cpu": [
+ "x64"
+ ],
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/@next/swc-linux-x64-musl": {
+ "version": "13.4.13",
+ "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-13.4.13.tgz",
+ "integrity": "sha512-+Y4LLhOWWZQIDKVwr2R17lq2KSN0F1c30QVgGIWfnjjHpH8nrIWHEndhqYU+iFuW8It78CiJjQKTw4f51HD7jA==",
+ "cpu": [
+ "x64"
+ ],
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/@next/swc-win32-arm64-msvc": {
+ "version": "13.4.13",
+ "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-13.4.13.tgz",
+ "integrity": "sha512-rWurdOR20uxjfqd1X9vDAgv0Jb26KjyL8akF9CBeFqX8rVaBAnW/Wf6A2gYEwyYY4Bai3T7p1kro6DFrsvBAAw==",
+ "cpu": [
+ "arm64"
+ ],
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/@next/swc-win32-ia32-msvc": {
+ "version": "13.4.13",
+ "resolved": "https://registry.npmjs.org/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-13.4.13.tgz",
+ "integrity": "sha512-E8bSPwRuY5ibJ3CzLQmJEt8qaWrPYuUTwnrwygPUEWoLzD5YRx9SD37oXRdU81TgGwDzCxpl7z5Nqlfk50xAog==",
+ "cpu": [
+ "ia32"
+ ],
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/@next/swc-win32-x64-msvc": {
+ "version": "13.4.13",
+ "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-13.4.13.tgz",
+ "integrity": "sha512-4KlyC6jWRubPnppgfYsNTPeWfGCxtWLh5vaOAW/kdzAk9widqho8Qb5S4K2vHmal1tsURi7Onk2MMCV1phvyqA==",
+ "cpu": [
+ "x64"
+ ],
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/@nodelib/fs.scandir": {
+ "version": "2.1.5",
+ "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
+ "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==",
+ "dependencies": {
+ "@nodelib/fs.stat": "2.0.5",
+ "run-parallel": "^1.1.9"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/@nodelib/fs.stat": {
+ "version": "2.0.5",
+ "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz",
+ "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==",
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/@nodelib/fs.walk": {
+ "version": "1.2.8",
+ "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz",
+ "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==",
+ "dependencies": {
+ "@nodelib/fs.scandir": "2.1.5",
+ "fastq": "^1.6.0"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/@pkgr/utils": {
+ "version": "2.4.2",
+ "resolved": "https://registry.npmjs.org/@pkgr/utils/-/utils-2.4.2.tgz",
+ "integrity": "sha512-POgTXhjrTfbTV63DiFXav4lBHiICLKKwDeaKn9Nphwj7WH6m0hMMCaJkMyRWjgtPFyRKRVoMXXjczsTQRDEhYw==",
+ "dev": true,
+ "dependencies": {
+ "cross-spawn": "^7.0.3",
+ "fast-glob": "^3.3.0",
+ "is-glob": "^4.0.3",
+ "open": "^9.1.0",
+ "picocolors": "^1.0.0",
+ "tslib": "^2.6.0"
+ },
+ "engines": {
+ "node": "^12.20.0 || ^14.18.0 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/unts"
+ }
+ },
+ "node_modules/@popperjs/core": {
+ "version": "2.11.8",
+ "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz",
+ "integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==",
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/popperjs"
+ }
+ },
+ "node_modules/@rushstack/eslint-patch": {
+ "version": "1.3.3",
+ "resolved": "https://registry.npmjs.org/@rushstack/eslint-patch/-/eslint-patch-1.3.3.tgz",
+ "integrity": "sha512-0xd7qez0AQ+MbHatZTlI1gu5vkG8r7MYRUJAHPAHJBmGLs16zpkrpAVLvjQKQOqaXPDUBwOiJzNc00znHSCVBw==",
+ "dev": true
+ },
+ "node_modules/@swc/helpers": {
+ "version": "0.5.1",
+ "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.1.tgz",
+ "integrity": "sha512-sJ902EfIzn1Fa+qYmjdQqh8tPsoxyBz+8yBKC2HKUxyezKJFwPGOn7pv4WY6QuQW//ySQi5lJjA/ZT9sNWWNTg==",
+ "dependencies": {
+ "tslib": "^2.4.0"
+ }
+ },
+ "node_modules/@types/json-schema": {
+ "version": "7.0.12",
+ "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.12.tgz",
+ "integrity": "sha512-Hr5Jfhc9eYOQNPYO5WLDq/n4jqijdHNlDXjuAQkkt+mWdQR+XJToOHrsD4cPaMXpn6KO7y2+wM8AZEs8VpBLVA==",
+ "dev": true
+ },
+ "node_modules/@types/json5": {
+ "version": "0.0.29",
+ "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz",
+ "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==",
+ "dev": true
+ },
+ "node_modules/@types/node": {
+ "version": "20.4.9",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-20.4.9.tgz",
+ "integrity": "sha512-8e2HYcg7ohnTUbHk8focoklEQYvemQmu9M/f43DZVx43kHn0tE3BY/6gSDxS7k0SprtS0NHvj+L80cGLnoOUcQ==",
+ "dev": true
+ },
+ "node_modules/@types/parse-json": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.0.tgz",
+ "integrity": "sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA=="
+ },
+ "node_modules/@types/prop-types": {
+ "version": "15.7.5",
+ "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.5.tgz",
+ "integrity": "sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w=="
+ },
+ "node_modules/@types/react": {
+ "version": "18.2.20",
+ "resolved": "https://registry.npmjs.org/@types/react/-/react-18.2.20.tgz",
+ "integrity": "sha512-WKNtmsLWJM/3D5mG4U84cysVY31ivmyw85dE84fOCk5Hx78wezB/XEjVPWl2JTZ5FkEeaTJf+VgUAUn3PE7Isw==",
+ "dependencies": {
+ "@types/prop-types": "*",
+ "@types/scheduler": "*",
+ "csstype": "^3.0.2"
+ }
+ },
+ "node_modules/@types/react-datepicker": {
+ "version": "4.15.0",
+ "resolved": "https://registry.npmjs.org/@types/react-datepicker/-/react-datepicker-4.15.0.tgz",
+ "integrity": "sha512-kr10s8ex4+MmCJmzdhA9kfmoMQBmsW5uDYDlH+8f/PgStrp7rRaz23Y/cvTiMgvESVq8ujDh4SOo6jlSwEw13g==",
+ "dev": true,
+ "dependencies": {
+ "@popperjs/core": "^2.9.2",
+ "@types/react": "*",
+ "date-fns": "^2.0.1",
+ "react-popper": "^2.2.5"
+ }
+ },
+ "node_modules/@types/react-dom": {
+ "version": "18.2.7",
+ "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.2.7.tgz",
+ "integrity": "sha512-GRaAEriuT4zp9N4p1i8BDBYmEyfo+xQ3yHjJU4eiK5NDa1RmUZG+unZABUTK4/Ox/M+GaHwb6Ow8rUITrtjszA==",
+ "dev": true,
+ "dependencies": {
+ "@types/react": "*"
+ }
+ },
+ "node_modules/@types/react-is": {
+ "version": "18.2.1",
+ "resolved": "https://registry.npmjs.org/@types/react-is/-/react-is-18.2.1.tgz",
+ "integrity": "sha512-wyUkmaaSZEzFZivD8F2ftSyAfk6L+DfFliVj/mYdOXbVjRcS87fQJLTnhk6dRZPuJjI+9g6RZJO4PNCngUrmyw==",
+ "dependencies": {
+ "@types/react": "*"
+ }
+ },
+ "node_modules/@types/react-transition-group": {
+ "version": "4.4.6",
+ "resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.6.tgz",
+ "integrity": "sha512-VnCdSxfcm08KjsJVQcfBmhEQAPnLB8G08hAxn39azX1qYBQ/5RVQuoHuKIcfKOdncuaUvEpFKFzEvbtIMsfVew==",
+ "dependencies": {
+ "@types/react": "*"
+ }
+ },
+ "node_modules/@types/scheduler": {
+ "version": "0.16.3",
+ "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.3.tgz",
+ "integrity": "sha512-5cJ8CB4yAx7BH1oMvdU0Jh9lrEXyPkar6F9G/ERswkCuvP4KQZfZkSjcMbAICCpQTN4OuZn8tz0HiKv9TGZgrQ=="
+ },
+ "node_modules/@types/semver": {
+ "version": "7.5.0",
+ "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.0.tgz",
+ "integrity": "sha512-G8hZ6XJiHnuhQKR7ZmysCeJWE08o8T0AXtk5darsCaTVsYZhhgUrq53jizaR2FvsoeCwJhlmwTjkXBY5Pn/ZHw==",
+ "dev": true
+ },
+ "node_modules/@typescript-eslint/eslint-plugin": {
+ "version": "5.62.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.62.0.tgz",
+ "integrity": "sha512-TiZzBSJja/LbhNPvk6yc0JrX9XqhQ0hdh6M2svYfsHGejaKFIAGd9MQ+ERIMzLGlN/kZoYIgdxFV0PuljTKXag==",
+ "dev": true,
+ "dependencies": {
+ "@eslint-community/regexpp": "^4.4.0",
+ "@typescript-eslint/scope-manager": "5.62.0",
+ "@typescript-eslint/type-utils": "5.62.0",
+ "@typescript-eslint/utils": "5.62.0",
+ "debug": "^4.3.4",
+ "graphemer": "^1.4.0",
+ "ignore": "^5.2.0",
+ "natural-compare-lite": "^1.4.0",
+ "semver": "^7.3.7",
+ "tsutils": "^3.21.0"
+ },
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "@typescript-eslint/parser": "^5.0.0",
+ "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0"
+ },
+ "peerDependenciesMeta": {
+ "typescript": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@typescript-eslint/parser": {
+ "version": "5.62.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.62.0.tgz",
+ "integrity": "sha512-VlJEV0fOQ7BExOsHYAGrgbEiZoi8D+Bl2+f6V2RrXerRSylnp+ZBHmPvaIa8cz0Ajx7WO7Z5RqfgYg7ED1nRhA==",
+ "dev": true,
+ "dependencies": {
+ "@typescript-eslint/scope-manager": "5.62.0",
+ "@typescript-eslint/types": "5.62.0",
+ "@typescript-eslint/typescript-estree": "5.62.0",
+ "debug": "^4.3.4"
+ },
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0"
+ },
+ "peerDependenciesMeta": {
+ "typescript": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@typescript-eslint/scope-manager": {
+ "version": "5.62.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.62.0.tgz",
+ "integrity": "sha512-VXuvVvZeQCQb5Zgf4HAxc04q5j+WrNAtNh9OwCsCgpKqESMTu3tF/jhZ3xG6T4NZwWl65Bg8KuS2uEvhSfLl0w==",
+ "dev": true,
+ "dependencies": {
+ "@typescript-eslint/types": "5.62.0",
+ "@typescript-eslint/visitor-keys": "5.62.0"
+ },
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ }
+ },
+ "node_modules/@typescript-eslint/type-utils": {
+ "version": "5.62.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-5.62.0.tgz",
+ "integrity": "sha512-xsSQreu+VnfbqQpW5vnCJdq1Z3Q0U31qiWmRhr98ONQmcp/yhiPJFPq8MXiJVLiksmOKSjIldZzkebzHuCGzew==",
+ "dev": true,
+ "dependencies": {
+ "@typescript-eslint/typescript-estree": "5.62.0",
+ "@typescript-eslint/utils": "5.62.0",
+ "debug": "^4.3.4",
+ "tsutils": "^3.21.0"
+ },
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "eslint": "*"
+ },
+ "peerDependenciesMeta": {
+ "typescript": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@typescript-eslint/types": {
+ "version": "5.62.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.62.0.tgz",
+ "integrity": "sha512-87NVngcbVXUahrRTqIK27gD2t5Cu1yuCXxbLcFtCzZGlfyVWWh8mLHkoxzjsB6DDNnvdL+fW8MiwPEJyGJQDgQ==",
+ "dev": true,
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ }
+ },
+ "node_modules/@typescript-eslint/typescript-estree": {
+ "version": "5.62.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.62.0.tgz",
+ "integrity": "sha512-CmcQ6uY7b9y694lKdRB8FEel7JbU/40iSAPomu++SjLMntB+2Leay2LO6i8VnJk58MtE9/nQSFIH6jpyRWyYzA==",
+ "dev": true,
+ "dependencies": {
+ "@typescript-eslint/types": "5.62.0",
+ "@typescript-eslint/visitor-keys": "5.62.0",
+ "debug": "^4.3.4",
+ "globby": "^11.1.0",
+ "is-glob": "^4.0.3",
+ "semver": "^7.3.7",
+ "tsutils": "^3.21.0"
+ },
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependenciesMeta": {
+ "typescript": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@typescript-eslint/utils": {
+ "version": "5.62.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.62.0.tgz",
+ "integrity": "sha512-n8oxjeb5aIbPFEtmQxQYOLI0i9n5ySBEY/ZEHHZqKQSFnxio1rv6dthascc9dLuwrL0RC5mPCxB7vnAVGAYWAQ==",
+ "dev": true,
+ "dependencies": {
+ "@eslint-community/eslint-utils": "^4.2.0",
+ "@types/json-schema": "^7.0.9",
+ "@types/semver": "^7.3.12",
+ "@typescript-eslint/scope-manager": "5.62.0",
+ "@typescript-eslint/types": "5.62.0",
+ "@typescript-eslint/typescript-estree": "5.62.0",
+ "eslint-scope": "^5.1.1",
+ "semver": "^7.3.7"
+ },
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0"
+ }
+ },
+ "node_modules/@typescript-eslint/visitor-keys": {
+ "version": "5.62.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.62.0.tgz",
+ "integrity": "sha512-07ny+LHRzQXepkGg6w0mFY41fVUNBrL2Roj/++7V1txKugfjm/Ci/qSND03r2RhlJhJYMcTn9AhhSSqQp0Ysyw==",
+ "dev": true,
+ "dependencies": {
+ "@typescript-eslint/types": "5.62.0",
+ "eslint-visitor-keys": "^3.3.0"
+ },
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ }
+ },
+ "node_modules/acorn": {
+ "version": "8.10.0",
+ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.10.0.tgz",
+ "integrity": "sha512-F0SAmZ8iUtS//m8DmCTA0jlh6TDKkHQyK6xc6V4KDTyZKA9dnvX9/3sRTVQrWm79glUAZbnmmNcdYwUIHWVybw==",
+ "dev": true,
+ "bin": {
+ "acorn": "bin/acorn"
+ },
+ "engines": {
+ "node": ">=0.4.0"
+ }
+ },
+ "node_modules/acorn-jsx": {
+ "version": "5.3.2",
+ "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz",
+ "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==",
+ "dev": true,
+ "peerDependencies": {
+ "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0"
+ }
+ },
+ "node_modules/aggregate-error": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz",
+ "integrity": "sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==",
+ "dev": true,
+ "dependencies": {
+ "clean-stack": "^2.0.0",
+ "indent-string": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/ajv": {
+ "version": "6.12.6",
+ "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
+ "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
+ "dev": true,
+ "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"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/epoberezkin"
+ }
+ },
+ "node_modules/ansi-escapes": {
+ "version": "4.3.2",
+ "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz",
+ "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==",
+ "dev": true,
+ "dependencies": {
+ "type-fest": "^0.21.3"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/ansi-escapes/node_modules/type-fest": {
+ "version": "0.21.3",
+ "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz",
+ "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==",
+ "dev": true,
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/ansi-regex": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
+ "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/ansi-styles": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
+ "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
+ "dev": true,
+ "dependencies": {
+ "color-convert": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+ }
+ },
+ "node_modules/any-promise": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz",
+ "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A=="
+ },
+ "node_modules/anymatch": {
+ "version": "3.1.3",
+ "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz",
+ "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==",
+ "dependencies": {
+ "normalize-path": "^3.0.0",
+ "picomatch": "^2.0.4"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/arg": {
+ "version": "5.0.2",
+ "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz",
+ "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg=="
+ },
+ "node_modules/argparse": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
+ "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
+ "dev": true
+ },
+ "node_modules/aria-query": {
+ "version": "5.3.0",
+ "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz",
+ "integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==",
+ "dev": true,
+ "dependencies": {
+ "dequal": "^2.0.3"
+ }
+ },
+ "node_modules/array-buffer-byte-length": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.0.tgz",
+ "integrity": "sha512-LPuwb2P+NrQw3XhxGc36+XSvuBPopovXYTR9Ew++Du9Yb/bx5AzBfrIsBoj0EZUifjQU+sHL21sseZ3jerWO/A==",
+ "dev": true,
+ "dependencies": {
+ "call-bind": "^1.0.2",
+ "is-array-buffer": "^3.0.1"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/array-includes": {
+ "version": "3.1.6",
+ "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.6.tgz",
+ "integrity": "sha512-sgTbLvL6cNnw24FnbaDyjmvddQ2ML8arZsgaJhoABMoplz/4QRhtrYS+alr1BUM1Bwp6dhx8vVCBSLG+StwOFw==",
+ "dev": true,
+ "dependencies": {
+ "call-bind": "^1.0.2",
+ "define-properties": "^1.1.4",
+ "es-abstract": "^1.20.4",
+ "get-intrinsic": "^1.1.3",
+ "is-string": "^1.0.7"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/array-union": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz",
+ "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/array.prototype.findlastindex": {
+ "version": "1.2.2",
+ "resolved": "https://registry.npmjs.org/array.prototype.findlastindex/-/array.prototype.findlastindex-1.2.2.tgz",
+ "integrity": "sha512-tb5thFFlUcp7NdNF6/MpDk/1r/4awWG1FIz3YqDf+/zJSTezBb+/5WViH41obXULHVpDzoiCLpJ/ZO9YbJMsdw==",
+ "dev": true,
+ "dependencies": {
+ "call-bind": "^1.0.2",
+ "define-properties": "^1.1.4",
+ "es-abstract": "^1.20.4",
+ "es-shim-unscopables": "^1.0.0",
+ "get-intrinsic": "^1.1.3"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/array.prototype.flat": {
+ "version": "1.3.1",
+ "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.3.1.tgz",
+ "integrity": "sha512-roTU0KWIOmJ4DRLmwKd19Otg0/mT3qPNt0Qb3GWW8iObuZXxrjB/pzn0R3hqpRSWg4HCwqx+0vwOnWnvlOyeIA==",
+ "dev": true,
+ "dependencies": {
+ "call-bind": "^1.0.2",
+ "define-properties": "^1.1.4",
+ "es-abstract": "^1.20.4",
+ "es-shim-unscopables": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/array.prototype.flatmap": {
+ "version": "1.3.1",
+ "resolved": "https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.3.1.tgz",
+ "integrity": "sha512-8UGn9O1FDVvMNB0UlLv4voxRMze7+FpHyF5mSMRjWHUMlpoDViniy05870VlxhfgTnLbpuwTzvD76MTtWxB/mQ==",
+ "dev": true,
+ "dependencies": {
+ "call-bind": "^1.0.2",
+ "define-properties": "^1.1.4",
+ "es-abstract": "^1.20.4",
+ "es-shim-unscopables": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/array.prototype.tosorted": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/array.prototype.tosorted/-/array.prototype.tosorted-1.1.1.tgz",
+ "integrity": "sha512-pZYPXPRl2PqWcsUs6LOMn+1f1532nEoPTYowBtqLwAW+W8vSVhkIGnmOX1t/UQjD6YGI0vcD2B1U7ZFGQH9jnQ==",
+ "dev": true,
+ "dependencies": {
+ "call-bind": "^1.0.2",
+ "define-properties": "^1.1.4",
+ "es-abstract": "^1.20.4",
+ "es-shim-unscopables": "^1.0.0",
+ "get-intrinsic": "^1.1.3"
+ }
+ },
+ "node_modules/arraybuffer.prototype.slice": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.1.tgz",
+ "integrity": "sha512-09x0ZWFEjj4WD8PDbykUwo3t9arLn8NIzmmYEJFpYekOAQjpkGSyrQhNoRTcwwcFRu+ycWF78QZ63oWTqSjBcw==",
+ "dev": true,
+ "dependencies": {
+ "array-buffer-byte-length": "^1.0.0",
+ "call-bind": "^1.0.2",
+ "define-properties": "^1.2.0",
+ "get-intrinsic": "^1.2.1",
+ "is-array-buffer": "^3.0.2",
+ "is-shared-array-buffer": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/ast-types-flow": {
+ "version": "0.0.7",
+ "resolved": "https://registry.npmjs.org/ast-types-flow/-/ast-types-flow-0.0.7.tgz",
+ "integrity": "sha512-eBvWn1lvIApYMhzQMsu9ciLfkBY499mFZlNqG+/9WR7PVlroQw0vG30cOQQbaKz3sCEc44TAOu2ykzqXSNnwag==",
+ "dev": true
+ },
+ "node_modules/astral-regex": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-2.0.0.tgz",
+ "integrity": "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/autoprefixer": {
+ "version": "10.4.14",
+ "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.14.tgz",
+ "integrity": "sha512-FQzyfOsTlwVzjHxKEqRIAdJx9niO6VCBCoEwax/VLSoQF29ggECcPuBqUMZ+u8jCZOPSy8b8/8KnuFbp0SaFZQ==",
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/postcss/"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/autoprefixer"
+ }
+ ],
+ "dependencies": {
+ "browserslist": "^4.21.5",
+ "caniuse-lite": "^1.0.30001464",
+ "fraction.js": "^4.2.0",
+ "normalize-range": "^0.1.2",
+ "picocolors": "^1.0.0",
+ "postcss-value-parser": "^4.2.0"
+ },
+ "bin": {
+ "autoprefixer": "bin/autoprefixer"
+ },
+ "engines": {
+ "node": "^10 || ^12 || >=14"
+ },
+ "peerDependencies": {
+ "postcss": "^8.1.0"
+ }
+ },
+ "node_modules/available-typed-arrays": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.5.tgz",
+ "integrity": "sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw==",
+ "dev": true,
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/axe-core": {
+ "version": "4.7.2",
+ "resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.7.2.tgz",
+ "integrity": "sha512-zIURGIS1E1Q4pcrMjp+nnEh+16G56eG/MUllJH8yEvw7asDo7Ac9uhC9KIH5jzpITueEZolfYglnCGIuSBz39g==",
+ "dev": true,
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/axobject-query": {
+ "version": "3.2.1",
+ "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-3.2.1.tgz",
+ "integrity": "sha512-jsyHu61e6N4Vbz/v18DHwWYKK0bSWLqn47eeDSKPB7m8tqMHF9YJ+mhIk2lVteyZrY8tnSj/jHOv4YiTCuCJgg==",
+ "dev": true,
+ "dependencies": {
+ "dequal": "^2.0.3"
+ }
+ },
+ "node_modules/babel-plugin-macros": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/babel-plugin-macros/-/babel-plugin-macros-3.1.0.tgz",
+ "integrity": "sha512-Cg7TFGpIr01vOQNODXOOaGz2NpCU5gl8x1qJFbb6hbZxR7XrcE2vtbAsTAbJ7/xwJtUuJEw8K8Zr/AE0LHlesg==",
+ "dependencies": {
+ "@babel/runtime": "^7.12.5",
+ "cosmiconfig": "^7.0.0",
+ "resolve": "^1.19.0"
+ },
+ "engines": {
+ "node": ">=10",
+ "npm": ">=6"
+ }
+ },
+ "node_modules/balanced-match": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
+ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="
+ },
+ "node_modules/big-integer": {
+ "version": "1.6.51",
+ "resolved": "https://registry.npmjs.org/big-integer/-/big-integer-1.6.51.tgz",
+ "integrity": "sha512-GPEid2Y9QU1Exl1rpO9B2IPJGHPSupF5GnVIP0blYvNOMer2bTvSWs1jGOUg04hTmu67nmLsQ9TBo1puaotBHg==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.6"
+ }
+ },
+ "node_modules/binary-extensions": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz",
+ "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/bplist-parser": {
+ "version": "0.2.0",
+ "resolved": "https://registry.npmjs.org/bplist-parser/-/bplist-parser-0.2.0.tgz",
+ "integrity": "sha512-z0M+byMThzQmD9NILRniCUXYsYpjwnlO8N5uCFaCqIOpqRsJCrQL9NK3JsD67CN5a08nF5oIL2bD6loTdHOuKw==",
+ "dev": true,
+ "dependencies": {
+ "big-integer": "^1.6.44"
+ },
+ "engines": {
+ "node": ">= 5.10.0"
+ }
+ },
+ "node_modules/brace-expansion": {
+ "version": "1.1.11",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
+ "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
+ "dependencies": {
+ "balanced-match": "^1.0.0",
+ "concat-map": "0.0.1"
+ }
+ },
+ "node_modules/braces": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz",
+ "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==",
+ "dependencies": {
+ "fill-range": "^7.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/browserslist": {
+ "version": "4.21.10",
+ "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.21.10.tgz",
+ "integrity": "sha512-bipEBdZfVH5/pwrvqc+Ub0kUPVfGUhlKxbvfD+z1BDnPEO/X98ruXGA1WP5ASpAFKan7Qr6j736IacbZQuAlKQ==",
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/browserslist"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/browserslist"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "dependencies": {
+ "caniuse-lite": "^1.0.30001517",
+ "electron-to-chromium": "^1.4.477",
+ "node-releases": "^2.0.13",
+ "update-browserslist-db": "^1.0.11"
+ },
+ "bin": {
+ "browserslist": "cli.js"
+ },
+ "engines": {
+ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
+ }
+ },
+ "node_modules/bundle-name": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/bundle-name/-/bundle-name-3.0.0.tgz",
+ "integrity": "sha512-PKA4BeSvBpQKQ8iPOGCSiell+N8P+Tf1DlwqmYhpe2gAhKPHn8EYOxVT+ShuGmhg8lN8XiSlS80yiExKXrURlw==",
+ "dev": true,
+ "dependencies": {
+ "run-applescript": "^5.0.0"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/busboy": {
+ "version": "1.6.0",
+ "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz",
+ "integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==",
+ "dependencies": {
+ "streamsearch": "^1.1.0"
+ },
+ "engines": {
+ "node": ">=10.16.0"
+ }
+ },
+ "node_modules/call-bind": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz",
+ "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==",
+ "dev": true,
+ "dependencies": {
+ "function-bind": "^1.1.1",
+ "get-intrinsic": "^1.0.2"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/callsites": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
+ "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/camelcase-css": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz",
+ "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==",
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/caniuse-lite": {
+ "version": "1.0.30001519",
+ "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001519.tgz",
+ "integrity": "sha512-0QHgqR+Jv4bxHMp8kZ1Kn8CH55OikjKJ6JmKkZYP1F3D7w+lnFXF70nG5eNfsZS89jadi5Ywy5UCSKLAglIRkg==",
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/browserslist"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/caniuse-lite"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ]
+ },
+ "node_modules/chalk": {
+ "version": "4.1.2",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
+ "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
+ "dev": true,
+ "dependencies": {
+ "ansi-styles": "^4.1.0",
+ "supports-color": "^7.1.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/chalk?sponsor=1"
+ }
+ },
+ "node_modules/chokidar": {
+ "version": "3.5.3",
+ "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz",
+ "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==",
+ "funding": [
+ {
+ "type": "individual",
+ "url": "https://paulmillr.com/funding/"
+ }
+ ],
+ "dependencies": {
+ "anymatch": "~3.1.2",
+ "braces": "~3.0.2",
+ "glob-parent": "~5.1.2",
+ "is-binary-path": "~2.1.0",
+ "is-glob": "~4.0.1",
+ "normalize-path": "~3.0.0",
+ "readdirp": "~3.6.0"
+ },
+ "engines": {
+ "node": ">= 8.10.0"
+ },
+ "optionalDependencies": {
+ "fsevents": "~2.3.2"
+ }
+ },
+ "node_modules/chokidar/node_modules/glob-parent": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
+ "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
+ "dependencies": {
+ "is-glob": "^4.0.1"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/classnames": {
+ "version": "2.3.2",
+ "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.3.2.tgz",
+ "integrity": "sha512-CSbhY4cFEJRe6/GQzIk5qXZ4Jeg5pcsP7b5peFSDpffpe1cqjASH/n9UTjBwOp6XpMSTwQ8Za2K5V02ueA7Tmw=="
+ },
+ "node_modules/clean-stack": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz",
+ "integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==",
+ "dev": true,
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/cli-cursor": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz",
+ "integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==",
+ "dev": true,
+ "dependencies": {
+ "restore-cursor": "^3.1.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/cli-truncate": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-3.1.0.tgz",
+ "integrity": "sha512-wfOBkjXteqSnI59oPcJkcPl/ZmwvMMOj340qUIY1SKZCv0B9Cf4D4fAucRkIKQmsIuYK3x1rrgU7MeGRruiuiA==",
+ "dev": true,
+ "dependencies": {
+ "slice-ansi": "^5.0.0",
+ "string-width": "^5.0.0"
+ },
+ "engines": {
+ "node": "^12.20.0 || ^14.13.1 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/client-only": {
+ "version": "0.0.1",
+ "resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz",
+ "integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA=="
+ },
+ "node_modules/clsx": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.0.0.tgz",
+ "integrity": "sha512-rQ1+kcj+ttHG0MKVGBUXwayCCF1oh39BF5COIpRzuCEv8Mwjv0XucrI2ExNTOn9IlLifGClWQcU9BrZORvtw6Q==",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/color-convert": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
+ "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
+ "dev": true,
+ "dependencies": {
+ "color-name": "~1.1.4"
+ },
+ "engines": {
+ "node": ">=7.0.0"
+ }
+ },
+ "node_modules/color-name": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
+ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
+ "dev": true
+ },
+ "node_modules/colorette": {
+ "version": "2.0.20",
+ "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz",
+ "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==",
+ "dev": true
+ },
+ "node_modules/commander": {
+ "version": "10.0.1",
+ "resolved": "https://registry.npmjs.org/commander/-/commander-10.0.1.tgz",
+ "integrity": "sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==",
+ "dev": true,
+ "engines": {
+ "node": ">=14"
+ }
+ },
+ "node_modules/concat-map": {
+ "version": "0.0.1",
+ "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
+ "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="
+ },
+ "node_modules/convert-source-map": {
+ "version": "1.9.0",
+ "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz",
+ "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A=="
+ },
+ "node_modules/cosmiconfig": {
+ "version": "7.1.0",
+ "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.1.0.tgz",
+ "integrity": "sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA==",
+ "dependencies": {
+ "@types/parse-json": "^4.0.0",
+ "import-fresh": "^3.2.1",
+ "parse-json": "^5.0.0",
+ "path-type": "^4.0.0",
+ "yaml": "^1.10.0"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/cosmiconfig/node_modules/yaml": {
+ "version": "1.10.2",
+ "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz",
+ "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==",
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/cross-spawn": {
+ "version": "7.0.3",
+ "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz",
+ "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==",
+ "dev": true,
+ "dependencies": {
+ "path-key": "^3.1.0",
+ "shebang-command": "^2.0.0",
+ "which": "^2.0.1"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/cssesc": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz",
+ "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==",
+ "bin": {
+ "cssesc": "bin/cssesc"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/csstype": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.2.tgz",
+ "integrity": "sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ=="
+ },
+ "node_modules/damerau-levenshtein": {
+ "version": "1.0.8",
+ "resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz",
+ "integrity": "sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==",
+ "dev": true
+ },
+ "node_modules/date-fns": {
+ "version": "2.30.0",
+ "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.30.0.tgz",
+ "integrity": "sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw==",
+ "dependencies": {
+ "@babel/runtime": "^7.21.0"
+ },
+ "engines": {
+ "node": ">=0.11"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/date-fns"
+ }
+ },
+ "node_modules/debug": {
+ "version": "4.3.4",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
+ "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==",
+ "dev": true,
+ "dependencies": {
+ "ms": "2.1.2"
+ },
+ "engines": {
+ "node": ">=6.0"
+ },
+ "peerDependenciesMeta": {
+ "supports-color": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/deep-is": {
+ "version": "0.1.4",
+ "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz",
+ "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==",
+ "dev": true
+ },
+ "node_modules/default-browser": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/default-browser/-/default-browser-4.0.0.tgz",
+ "integrity": "sha512-wX5pXO1+BrhMkSbROFsyxUm0i/cJEScyNhA4PPxc41ICuv05ZZB/MX28s8aZx6xjmatvebIapF6hLEKEcpneUA==",
+ "dev": true,
+ "dependencies": {
+ "bundle-name": "^3.0.0",
+ "default-browser-id": "^3.0.0",
+ "execa": "^7.1.1",
+ "titleize": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=14.16"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/default-browser-id": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/default-browser-id/-/default-browser-id-3.0.0.tgz",
+ "integrity": "sha512-OZ1y3y0SqSICtE8DE4S8YOE9UZOJ8wO16fKWVP5J1Qz42kV9jcnMVFrEE/noXb/ss3Q4pZIH79kxofzyNNtUNA==",
+ "dev": true,
+ "dependencies": {
+ "bplist-parser": "^0.2.0",
+ "untildify": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/define-lazy-prop": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-3.0.0.tgz",
+ "integrity": "sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==",
+ "dev": true,
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/define-properties": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.0.tgz",
+ "integrity": "sha512-xvqAVKGfT1+UAvPwKTVw/njhdQ8ZhXK4lI0bCIuCMrp2up9nPnaDftrLtmpTazqd1o+UY4zgzU+avtMbDP+ldA==",
+ "dev": true,
+ "dependencies": {
+ "has-property-descriptors": "^1.0.0",
+ "object-keys": "^1.1.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/dequal": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz",
+ "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==",
+ "dev": true,
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/didyoumean": {
+ "version": "1.2.2",
+ "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz",
+ "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw=="
+ },
+ "node_modules/dir-glob": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz",
+ "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==",
+ "dev": true,
+ "dependencies": {
+ "path-type": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/dlv": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz",
+ "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA=="
+ },
+ "node_modules/doctrine": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz",
+ "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==",
+ "dev": true,
+ "dependencies": {
+ "esutils": "^2.0.2"
+ },
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/dom-helpers": {
+ "version": "5.2.1",
+ "resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz",
+ "integrity": "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==",
+ "dependencies": {
+ "@babel/runtime": "^7.8.7",
+ "csstype": "^3.0.2"
+ }
+ },
+ "node_modules/eastasianwidth": {
+ "version": "0.2.0",
+ "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz",
+ "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==",
+ "dev": true
+ },
+ "node_modules/electron-to-chromium": {
+ "version": "1.4.490",
+ "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.490.tgz",
+ "integrity": "sha512-6s7NVJz+sATdYnIwhdshx/N/9O6rvMxmhVoDSDFdj6iA45gHR8EQje70+RYsF4GeB+k0IeNSBnP7yG9ZXJFr7A=="
+ },
+ "node_modules/emoji-regex": {
+ "version": "9.2.2",
+ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz",
+ "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==",
+ "dev": true
+ },
+ "node_modules/enhanced-resolve": {
+ "version": "5.15.0",
+ "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.15.0.tgz",
+ "integrity": "sha512-LXYT42KJ7lpIKECr2mAXIaMldcNCh/7E0KBKOu4KSfkHmP+mZmSs+8V5gBAqisWBy0OO4W5Oyys0GO1Y8KtdKg==",
+ "dev": true,
+ "dependencies": {
+ "graceful-fs": "^4.2.4",
+ "tapable": "^2.2.0"
+ },
+ "engines": {
+ "node": ">=10.13.0"
+ }
+ },
+ "node_modules/error-ex": {
+ "version": "1.3.2",
+ "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz",
+ "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==",
+ "dependencies": {
+ "is-arrayish": "^0.2.1"
+ }
+ },
+ "node_modules/es-abstract": {
+ "version": "1.22.1",
+ "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.22.1.tgz",
+ "integrity": "sha512-ioRRcXMO6OFyRpyzV3kE1IIBd4WG5/kltnzdxSCqoP8CMGs/Li+M1uF5o7lOkZVFjDs+NLesthnF66Pg/0q0Lw==",
+ "dev": true,
+ "dependencies": {
+ "array-buffer-byte-length": "^1.0.0",
+ "arraybuffer.prototype.slice": "^1.0.1",
+ "available-typed-arrays": "^1.0.5",
+ "call-bind": "^1.0.2",
+ "es-set-tostringtag": "^2.0.1",
+ "es-to-primitive": "^1.2.1",
+ "function.prototype.name": "^1.1.5",
+ "get-intrinsic": "^1.2.1",
+ "get-symbol-description": "^1.0.0",
+ "globalthis": "^1.0.3",
+ "gopd": "^1.0.1",
+ "has": "^1.0.3",
+ "has-property-descriptors": "^1.0.0",
+ "has-proto": "^1.0.1",
+ "has-symbols": "^1.0.3",
+ "internal-slot": "^1.0.5",
+ "is-array-buffer": "^3.0.2",
+ "is-callable": "^1.2.7",
+ "is-negative-zero": "^2.0.2",
+ "is-regex": "^1.1.4",
+ "is-shared-array-buffer": "^1.0.2",
+ "is-string": "^1.0.7",
+ "is-typed-array": "^1.1.10",
+ "is-weakref": "^1.0.2",
+ "object-inspect": "^1.12.3",
+ "object-keys": "^1.1.1",
+ "object.assign": "^4.1.4",
+ "regexp.prototype.flags": "^1.5.0",
+ "safe-array-concat": "^1.0.0",
+ "safe-regex-test": "^1.0.0",
+ "string.prototype.trim": "^1.2.7",
+ "string.prototype.trimend": "^1.0.6",
+ "string.prototype.trimstart": "^1.0.6",
+ "typed-array-buffer": "^1.0.0",
+ "typed-array-byte-length": "^1.0.0",
+ "typed-array-byte-offset": "^1.0.0",
+ "typed-array-length": "^1.0.4",
+ "unbox-primitive": "^1.0.2",
+ "which-typed-array": "^1.1.10"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/es-set-tostringtag": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.0.1.tgz",
+ "integrity": "sha512-g3OMbtlwY3QewlqAiMLI47KywjWZoEytKr8pf6iTC8uJq5bIAH52Z9pnQ8pVL6whrCto53JZDuUIsifGeLorTg==",
+ "dev": true,
+ "dependencies": {
+ "get-intrinsic": "^1.1.3",
+ "has": "^1.0.3",
+ "has-tostringtag": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-shim-unscopables": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.0.0.tgz",
+ "integrity": "sha512-Jm6GPcCdC30eMLbZ2x8z2WuRwAws3zTBBKuusffYVUrNj/GVSUAZ+xKMaUpfNDR5IbyNA5LJbaecoUVbmUcB1w==",
+ "dev": true,
+ "dependencies": {
+ "has": "^1.0.3"
+ }
+ },
+ "node_modules/es-to-primitive": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz",
+ "integrity": "sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==",
+ "dev": true,
+ "dependencies": {
+ "is-callable": "^1.1.4",
+ "is-date-object": "^1.0.1",
+ "is-symbol": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/escalade": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz",
+ "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/escape-string-regexp": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
+ "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/eslint": {
+ "version": "8.46.0",
+ "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.46.0.tgz",
+ "integrity": "sha512-cIO74PvbW0qU8e0mIvk5IV3ToWdCq5FYG6gWPHHkx6gNdjlbAYvtfHmlCMXxjcoVaIdwy/IAt3+mDkZkfvb2Dg==",
+ "dev": true,
+ "dependencies": {
+ "@eslint-community/eslint-utils": "^4.2.0",
+ "@eslint-community/regexpp": "^4.6.1",
+ "@eslint/eslintrc": "^2.1.1",
+ "@eslint/js": "^8.46.0",
+ "@humanwhocodes/config-array": "^0.11.10",
+ "@humanwhocodes/module-importer": "^1.0.1",
+ "@nodelib/fs.walk": "^1.2.8",
+ "ajv": "^6.12.4",
+ "chalk": "^4.0.0",
+ "cross-spawn": "^7.0.2",
+ "debug": "^4.3.2",
+ "doctrine": "^3.0.0",
+ "escape-string-regexp": "^4.0.0",
+ "eslint-scope": "^7.2.2",
+ "eslint-visitor-keys": "^3.4.2",
+ "espree": "^9.6.1",
+ "esquery": "^1.4.2",
+ "esutils": "^2.0.2",
+ "fast-deep-equal": "^3.1.3",
+ "file-entry-cache": "^6.0.1",
+ "find-up": "^5.0.0",
+ "glob-parent": "^6.0.2",
+ "globals": "^13.19.0",
+ "graphemer": "^1.4.0",
+ "ignore": "^5.2.0",
+ "imurmurhash": "^0.1.4",
+ "is-glob": "^4.0.0",
+ "is-path-inside": "^3.0.3",
+ "js-yaml": "^4.1.0",
+ "json-stable-stringify-without-jsonify": "^1.0.1",
+ "levn": "^0.4.1",
+ "lodash.merge": "^4.6.2",
+ "minimatch": "^3.1.2",
+ "natural-compare": "^1.4.0",
+ "optionator": "^0.9.3",
+ "strip-ansi": "^6.0.1",
+ "text-table": "^0.2.0"
+ },
+ "bin": {
+ "eslint": "bin/eslint.js"
+ },
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/eslint-config-next": {
+ "version": "13.4.13",
+ "resolved": "https://registry.npmjs.org/eslint-config-next/-/eslint-config-next-13.4.13.tgz",
+ "integrity": "sha512-EXAh5h1yG/YTNa5YdskzaSZncBjKjvFe2zclMCi2KXyTsXha22wB6MPs/U7idB6a2qjpBdbZcruQY1TWjfNMZw==",
+ "dev": true,
+ "dependencies": {
+ "@next/eslint-plugin-next": "13.4.13",
+ "@rushstack/eslint-patch": "^1.1.3",
+ "@typescript-eslint/parser": "^5.4.2 || ^6.0.0",
+ "eslint-import-resolver-node": "^0.3.6",
+ "eslint-import-resolver-typescript": "^3.5.2",
+ "eslint-plugin-import": "^2.26.0",
+ "eslint-plugin-jsx-a11y": "^6.5.1",
+ "eslint-plugin-react": "^7.31.7",
+ "eslint-plugin-react-hooks": "5.0.0-canary-7118f5dd7-20230705"
+ },
+ "peerDependencies": {
+ "eslint": "^7.23.0 || ^8.0.0",
+ "typescript": ">=3.3.1"
+ },
+ "peerDependenciesMeta": {
+ "typescript": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/eslint-config-prettier": {
+ "version": "9.0.0",
+ "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-9.0.0.tgz",
+ "integrity": "sha512-IcJsTkJae2S35pRsRAwoCE+925rJJStOdkKnLVgtE+tEpqU0EVVM7OqrwxqgptKdX29NUwC82I5pXsGFIgSevw==",
+ "dev": true,
+ "bin": {
+ "eslint-config-prettier": "bin/cli.js"
+ },
+ "peerDependencies": {
+ "eslint": ">=7.0.0"
+ }
+ },
+ "node_modules/eslint-import-resolver-node": {
+ "version": "0.3.9",
+ "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.9.tgz",
+ "integrity": "sha512-WFj2isz22JahUv+B788TlO3N6zL3nNJGU8CcZbPZvVEkBPaJdCV4vy5wyghty5ROFbCRnm132v8BScu5/1BQ8g==",
+ "dev": true,
+ "dependencies": {
+ "debug": "^3.2.7",
+ "is-core-module": "^2.13.0",
+ "resolve": "^1.22.4"
+ }
+ },
+ "node_modules/eslint-import-resolver-node/node_modules/debug": {
+ "version": "3.2.7",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz",
+ "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==",
+ "dev": true,
+ "dependencies": {
+ "ms": "^2.1.1"
+ }
+ },
+ "node_modules/eslint-import-resolver-typescript": {
+ "version": "3.6.0",
+ "resolved": "https://registry.npmjs.org/eslint-import-resolver-typescript/-/eslint-import-resolver-typescript-3.6.0.tgz",
+ "integrity": "sha512-QTHR9ddNnn35RTxlaEnx2gCxqFlF2SEN0SE2d17SqwyM7YOSI2GHWRYp5BiRkObTUNYPupC/3Fq2a0PpT+EKpg==",
+ "dev": true,
+ "dependencies": {
+ "debug": "^4.3.4",
+ "enhanced-resolve": "^5.12.0",
+ "eslint-module-utils": "^2.7.4",
+ "fast-glob": "^3.3.1",
+ "get-tsconfig": "^4.5.0",
+ "is-core-module": "^2.11.0",
+ "is-glob": "^4.0.3"
+ },
+ "engines": {
+ "node": "^14.18.0 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/unts/projects/eslint-import-resolver-ts"
+ },
+ "peerDependencies": {
+ "eslint": "*",
+ "eslint-plugin-import": "*"
+ }
+ },
+ "node_modules/eslint-module-utils": {
+ "version": "2.8.0",
+ "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.8.0.tgz",
+ "integrity": "sha512-aWajIYfsqCKRDgUfjEXNN/JlrzauMuSEy5sbd7WXbtW3EH6A6MpwEh42c7qD+MqQo9QMJ6fWLAeIJynx0g6OAw==",
+ "dev": true,
+ "dependencies": {
+ "debug": "^3.2.7"
+ },
+ "engines": {
+ "node": ">=4"
+ },
+ "peerDependenciesMeta": {
+ "eslint": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/eslint-module-utils/node_modules/debug": {
+ "version": "3.2.7",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz",
+ "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==",
+ "dev": true,
+ "dependencies": {
+ "ms": "^2.1.1"
+ }
+ },
+ "node_modules/eslint-plugin-import": {
+ "version": "2.28.0",
+ "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.28.0.tgz",
+ "integrity": "sha512-B8s/n+ZluN7sxj9eUf7/pRFERX0r5bnFA2dCaLHy2ZeaQEAz0k+ZZkFWRFHJAqxfxQDx6KLv9LeIki7cFdwW+Q==",
+ "dev": true,
+ "dependencies": {
+ "array-includes": "^3.1.6",
+ "array.prototype.findlastindex": "^1.2.2",
+ "array.prototype.flat": "^1.3.1",
+ "array.prototype.flatmap": "^1.3.1",
+ "debug": "^3.2.7",
+ "doctrine": "^2.1.0",
+ "eslint-import-resolver-node": "^0.3.7",
+ "eslint-module-utils": "^2.8.0",
+ "has": "^1.0.3",
+ "is-core-module": "^2.12.1",
+ "is-glob": "^4.0.3",
+ "minimatch": "^3.1.2",
+ "object.fromentries": "^2.0.6",
+ "object.groupby": "^1.0.0",
+ "object.values": "^1.1.6",
+ "resolve": "^1.22.3",
+ "semver": "^6.3.1",
+ "tsconfig-paths": "^3.14.2"
+ },
+ "engines": {
+ "node": ">=4"
+ },
+ "peerDependencies": {
+ "eslint": "^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8"
+ }
+ },
+ "node_modules/eslint-plugin-import/node_modules/debug": {
+ "version": "3.2.7",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz",
+ "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==",
+ "dev": true,
+ "dependencies": {
+ "ms": "^2.1.1"
+ }
+ },
+ "node_modules/eslint-plugin-import/node_modules/doctrine": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz",
+ "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==",
+ "dev": true,
+ "dependencies": {
+ "esutils": "^2.0.2"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/eslint-plugin-import/node_modules/semver": {
+ "version": "6.3.1",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
+ "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
+ "dev": true,
+ "bin": {
+ "semver": "bin/semver.js"
+ }
+ },
+ "node_modules/eslint-plugin-jsx-a11y": {
+ "version": "6.7.1",
+ "resolved": "https://registry.npmjs.org/eslint-plugin-jsx-a11y/-/eslint-plugin-jsx-a11y-6.7.1.tgz",
+ "integrity": "sha512-63Bog4iIethyo8smBklORknVjB0T2dwB8Mr/hIC+fBS0uyHdYYpzM/Ed+YC8VxTjlXHEWFOdmgwcDn1U2L9VCA==",
+ "dev": true,
+ "dependencies": {
+ "@babel/runtime": "^7.20.7",
+ "aria-query": "^5.1.3",
+ "array-includes": "^3.1.6",
+ "array.prototype.flatmap": "^1.3.1",
+ "ast-types-flow": "^0.0.7",
+ "axe-core": "^4.6.2",
+ "axobject-query": "^3.1.1",
+ "damerau-levenshtein": "^1.0.8",
+ "emoji-regex": "^9.2.2",
+ "has": "^1.0.3",
+ "jsx-ast-utils": "^3.3.3",
+ "language-tags": "=1.0.5",
+ "minimatch": "^3.1.2",
+ "object.entries": "^1.1.6",
+ "object.fromentries": "^2.0.6",
+ "semver": "^6.3.0"
+ },
+ "engines": {
+ "node": ">=4.0"
+ },
+ "peerDependencies": {
+ "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8"
+ }
+ },
+ "node_modules/eslint-plugin-jsx-a11y/node_modules/semver": {
+ "version": "6.3.1",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
+ "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
+ "dev": true,
+ "bin": {
+ "semver": "bin/semver.js"
+ }
+ },
+ "node_modules/eslint-plugin-prettier": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.0.0.tgz",
+ "integrity": "sha512-AgaZCVuYDXHUGxj/ZGu1u8H8CYgDY3iG6w5kUFw4AzMVXzB7VvbKgYR4nATIN+OvUrghMbiDLeimVjVY5ilq3w==",
+ "dev": true,
+ "dependencies": {
+ "prettier-linter-helpers": "^1.0.0",
+ "synckit": "^0.8.5"
+ },
+ "engines": {
+ "node": "^14.18.0 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/prettier"
+ },
+ "peerDependencies": {
+ "@types/eslint": ">=8.0.0",
+ "eslint": ">=8.0.0",
+ "prettier": ">=3.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@types/eslint": {
+ "optional": true
+ },
+ "eslint-config-prettier": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/eslint-plugin-react": {
+ "version": "7.33.1",
+ "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.33.1.tgz",
+ "integrity": "sha512-L093k0WAMvr6VhNwReB8VgOq5s2LesZmrpPdKz/kZElQDzqS7G7+DnKoqT+w4JwuiGeAhAvHO0fvy0Eyk4ejDA==",
+ "dev": true,
+ "dependencies": {
+ "array-includes": "^3.1.6",
+ "array.prototype.flatmap": "^1.3.1",
+ "array.prototype.tosorted": "^1.1.1",
+ "doctrine": "^2.1.0",
+ "estraverse": "^5.3.0",
+ "jsx-ast-utils": "^2.4.1 || ^3.0.0",
+ "minimatch": "^3.1.2",
+ "object.entries": "^1.1.6",
+ "object.fromentries": "^2.0.6",
+ "object.hasown": "^1.1.2",
+ "object.values": "^1.1.6",
+ "prop-types": "^15.8.1",
+ "resolve": "^2.0.0-next.4",
+ "semver": "^6.3.1",
+ "string.prototype.matchall": "^4.0.8"
+ },
+ "engines": {
+ "node": ">=4"
+ },
+ "peerDependencies": {
+ "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8"
+ }
+ },
+ "node_modules/eslint-plugin-react-hooks": {
+ "version": "5.0.0-canary-7118f5dd7-20230705",
+ "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-5.0.0-canary-7118f5dd7-20230705.tgz",
+ "integrity": "sha512-AZYbMo/NW9chdL7vk6HQzQhT+PvTAEVqWk9ziruUoW2kAOcN5qNyelv70e0F1VNQAbvutOC9oc+xfWycI9FxDw==",
+ "dev": true,
+ "engines": {
+ "node": ">=10"
+ },
+ "peerDependencies": {
+ "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0"
+ }
+ },
+ "node_modules/eslint-plugin-react/node_modules/doctrine": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz",
+ "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==",
+ "dev": true,
+ "dependencies": {
+ "esutils": "^2.0.2"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/eslint-plugin-react/node_modules/resolve": {
+ "version": "2.0.0-next.4",
+ "resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.4.tgz",
+ "integrity": "sha512-iMDbmAWtfU+MHpxt/I5iWI7cY6YVEZUQ3MBgPQ++XD1PELuJHIl82xBmObyP2KyQmkNB2dsqF7seoQQiAn5yDQ==",
+ "dev": true,
+ "dependencies": {
+ "is-core-module": "^2.9.0",
+ "path-parse": "^1.0.7",
+ "supports-preserve-symlinks-flag": "^1.0.0"
+ },
+ "bin": {
+ "resolve": "bin/resolve"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/eslint-plugin-react/node_modules/semver": {
+ "version": "6.3.1",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
+ "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
+ "dev": true,
+ "bin": {
+ "semver": "bin/semver.js"
+ }
+ },
+ "node_modules/eslint-plugin-tailwindcss": {
+ "version": "3.13.0",
+ "resolved": "https://registry.npmjs.org/eslint-plugin-tailwindcss/-/eslint-plugin-tailwindcss-3.13.0.tgz",
+ "integrity": "sha512-Fcep4KDRLWaK3KmkQbdyKHG0P4GdXFmXdDaweTIPcgOP60OOuWFbh1++dufRT28Q4zpKTKaHwTsXPJ4O/EjU2Q==",
+ "dev": true,
+ "dependencies": {
+ "fast-glob": "^3.2.5",
+ "postcss": "^8.4.4"
+ },
+ "engines": {
+ "node": ">=12.13.0"
+ },
+ "peerDependencies": {
+ "tailwindcss": "^3.3.2"
+ }
+ },
+ "node_modules/eslint-scope": {
+ "version": "5.1.1",
+ "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz",
+ "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==",
+ "dev": true,
+ "dependencies": {
+ "esrecurse": "^4.3.0",
+ "estraverse": "^4.1.1"
+ },
+ "engines": {
+ "node": ">=8.0.0"
+ }
+ },
+ "node_modules/eslint-scope/node_modules/estraverse": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz",
+ "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==",
+ "dev": true,
+ "engines": {
+ "node": ">=4.0"
+ }
+ },
+ "node_modules/eslint-visitor-keys": {
+ "version": "3.4.2",
+ "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.2.tgz",
+ "integrity": "sha512-8drBzUEyZ2llkpCA67iYrgEssKDUu68V8ChqqOfFupIaG/LCVPUT+CoGJpT77zJprs4T/W7p07LP7zAIMuweVw==",
+ "dev": true,
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/eslint/node_modules/eslint-scope": {
+ "version": "7.2.2",
+ "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz",
+ "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==",
+ "dev": true,
+ "dependencies": {
+ "esrecurse": "^4.3.0",
+ "estraverse": "^5.2.0"
+ },
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/espree": {
+ "version": "9.6.1",
+ "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz",
+ "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==",
+ "dev": true,
+ "dependencies": {
+ "acorn": "^8.9.0",
+ "acorn-jsx": "^5.3.2",
+ "eslint-visitor-keys": "^3.4.1"
+ },
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/esquery": {
+ "version": "1.5.0",
+ "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.5.0.tgz",
+ "integrity": "sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg==",
+ "dev": true,
+ "dependencies": {
+ "estraverse": "^5.1.0"
+ },
+ "engines": {
+ "node": ">=0.10"
+ }
+ },
+ "node_modules/esrecurse": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz",
+ "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==",
+ "dev": true,
+ "dependencies": {
+ "estraverse": "^5.2.0"
+ },
+ "engines": {
+ "node": ">=4.0"
+ }
+ },
+ "node_modules/estraverse": {
+ "version": "5.3.0",
+ "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz",
+ "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==",
+ "dev": true,
+ "engines": {
+ "node": ">=4.0"
+ }
+ },
+ "node_modules/esutils": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz",
+ "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/execa": {
+ "version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/execa/-/execa-7.2.0.tgz",
+ "integrity": "sha512-UduyVP7TLB5IcAQl+OzLyLcS/l32W/GLg+AhHJ+ow40FOk2U3SAllPwR44v4vmdFwIWqpdwxxpQbF1n5ta9seA==",
+ "dev": true,
+ "dependencies": {
+ "cross-spawn": "^7.0.3",
+ "get-stream": "^6.0.1",
+ "human-signals": "^4.3.0",
+ "is-stream": "^3.0.0",
+ "merge-stream": "^2.0.0",
+ "npm-run-path": "^5.1.0",
+ "onetime": "^6.0.0",
+ "signal-exit": "^3.0.7",
+ "strip-final-newline": "^3.0.0"
+ },
+ "engines": {
+ "node": "^14.18.0 || ^16.14.0 || >=18.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/sindresorhus/execa?sponsor=1"
+ }
+ },
+ "node_modules/fast-deep-equal": {
+ "version": "3.1.3",
+ "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
+ "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
+ "dev": true
+ },
+ "node_modules/fast-diff": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.3.0.tgz",
+ "integrity": "sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==",
+ "dev": true
+ },
+ "node_modules/fast-glob": {
+ "version": "3.3.1",
+ "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.1.tgz",
+ "integrity": "sha512-kNFPyjhh5cKjrUltxs+wFx+ZkbRaxxmZ+X0ZU31SOsxCEtP9VPgtq2teZw1DebupL5GmDaNQ6yKMMVcM41iqDg==",
+ "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.4"
+ },
+ "engines": {
+ "node": ">=8.6.0"
+ }
+ },
+ "node_modules/fast-glob/node_modules/glob-parent": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
+ "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
+ "dependencies": {
+ "is-glob": "^4.0.1"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/fast-json-stable-stringify": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz",
+ "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==",
+ "dev": true
+ },
+ "node_modules/fast-levenshtein": {
+ "version": "2.0.6",
+ "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz",
+ "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==",
+ "dev": true
+ },
+ "node_modules/fastq": {
+ "version": "1.15.0",
+ "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.15.0.tgz",
+ "integrity": "sha512-wBrocU2LCXXa+lWBt8RoIRD89Fi8OdABODa/kEnyeyjS5aZO5/GNvI5sEINADqP/h8M29UHTHUb53sUu5Ihqdw==",
+ "dependencies": {
+ "reusify": "^1.0.4"
+ }
+ },
+ "node_modules/file-entry-cache": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz",
+ "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==",
+ "dev": true,
+ "dependencies": {
+ "flat-cache": "^3.0.4"
+ },
+ "engines": {
+ "node": "^10.12.0 || >=12.0.0"
+ }
+ },
+ "node_modules/fill-range": {
+ "version": "7.0.1",
+ "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz",
+ "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==",
+ "dependencies": {
+ "to-regex-range": "^5.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/find-root": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/find-root/-/find-root-1.1.0.tgz",
+ "integrity": "sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng=="
+ },
+ "node_modules/find-up": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz",
+ "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==",
+ "dev": true,
+ "dependencies": {
+ "locate-path": "^6.0.0",
+ "path-exists": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/flat-cache": {
+ "version": "3.0.4",
+ "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.0.4.tgz",
+ "integrity": "sha512-dm9s5Pw7Jc0GvMYbshN6zchCA9RgQlzzEZX3vylR9IqFfS8XciblUXOKfW6SiuJ0e13eDYZoZV5wdrev7P3Nwg==",
+ "dev": true,
+ "dependencies": {
+ "flatted": "^3.1.0",
+ "rimraf": "^3.0.2"
+ },
+ "engines": {
+ "node": "^10.12.0 || >=12.0.0"
+ }
+ },
+ "node_modules/flatted": {
+ "version": "3.2.7",
+ "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.7.tgz",
+ "integrity": "sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ==",
+ "dev": true
+ },
+ "node_modules/for-each": {
+ "version": "0.3.3",
+ "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz",
+ "integrity": "sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==",
+ "dev": true,
+ "dependencies": {
+ "is-callable": "^1.1.3"
+ }
+ },
+ "node_modules/fraction.js": {
+ "version": "4.2.0",
+ "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.2.0.tgz",
+ "integrity": "sha512-MhLuK+2gUcnZe8ZHlaaINnQLl0xRIGRfcGk2yl8xoQAfHrSsL3rYu6FCmBdkdbhc9EPlwyGHewaRsvwRMJtAlA==",
+ "engines": {
+ "node": "*"
+ },
+ "funding": {
+ "type": "patreon",
+ "url": "https://www.patreon.com/infusion"
+ }
+ },
+ "node_modules/fs.realpath": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
+ "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw=="
+ },
+ "node_modules/function-bind": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz",
+ "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A=="
+ },
+ "node_modules/function.prototype.name": {
+ "version": "1.1.5",
+ "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.5.tgz",
+ "integrity": "sha512-uN7m/BzVKQnCUF/iW8jYea67v++2u7m5UgENbHRtdDVclOUP+FMPlCNdmk0h/ysGyo2tavMJEDqJAkJdRa1vMA==",
+ "dev": true,
+ "dependencies": {
+ "call-bind": "^1.0.2",
+ "define-properties": "^1.1.3",
+ "es-abstract": "^1.19.0",
+ "functions-have-names": "^1.2.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/functions-have-names": {
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz",
+ "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==",
+ "dev": true,
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/get-intrinsic": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.1.tgz",
+ "integrity": "sha512-2DcsyfABl+gVHEfCOaTrWgyt+tb6MSEGmKq+kI5HwLbIYgjgmMcV8KQ41uaKz1xxUcn9tJtgFbQUEVcEbd0FYw==",
+ "dev": true,
+ "dependencies": {
+ "function-bind": "^1.1.1",
+ "has": "^1.0.3",
+ "has-proto": "^1.0.1",
+ "has-symbols": "^1.0.3"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/get-stream": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz",
+ "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==",
+ "dev": true,
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/get-symbol-description": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.0.0.tgz",
+ "integrity": "sha512-2EmdH1YvIQiZpltCNgkuiUnyukzxM/R6NDJX31Ke3BG1Nq5b0S2PhX59UKi9vZpPDQVdqn+1IcaAwnzTT5vCjw==",
+ "dev": true,
+ "dependencies": {
+ "call-bind": "^1.0.2",
+ "get-intrinsic": "^1.1.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/get-tsconfig": {
+ "version": "4.7.0",
+ "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.7.0.tgz",
+ "integrity": "sha512-pmjiZ7xtB8URYm74PlGJozDNyhvsVLUcpBa8DZBG3bWHwaHa9bPiRpiSfovw+fjhwONSCWKRyk+JQHEGZmMrzw==",
+ "dev": true,
+ "dependencies": {
+ "resolve-pkg-maps": "^1.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1"
+ }
+ },
+ "node_modules/glob": {
+ "version": "7.1.7",
+ "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.7.tgz",
+ "integrity": "sha512-OvD9ENzPLbegENnYP5UUfJIirTg4+XwMWGaQfQTY0JenxNvvIKP3U3/tAQSPIu/lHxXYSZmpXlUHeqAIdKzBLQ==",
+ "dev": true,
+ "dependencies": {
+ "fs.realpath": "^1.0.0",
+ "inflight": "^1.0.4",
+ "inherits": "2",
+ "minimatch": "^3.0.4",
+ "once": "^1.3.0",
+ "path-is-absolute": "^1.0.0"
+ },
+ "engines": {
+ "node": "*"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/glob-parent": {
+ "version": "6.0.2",
+ "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz",
+ "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==",
+ "dependencies": {
+ "is-glob": "^4.0.3"
+ },
+ "engines": {
+ "node": ">=10.13.0"
+ }
+ },
+ "node_modules/glob-to-regexp": {
+ "version": "0.4.1",
+ "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz",
+ "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw=="
+ },
+ "node_modules/globals": {
+ "version": "13.20.0",
+ "resolved": "https://registry.npmjs.org/globals/-/globals-13.20.0.tgz",
+ "integrity": "sha512-Qg5QtVkCy/kv3FUSlu4ukeZDVf9ee0iXLAUYX13gbR17bnejFTzr4iS9bY7kwCf1NztRNm1t91fjOiyx4CSwPQ==",
+ "dev": true,
+ "dependencies": {
+ "type-fest": "^0.20.2"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/globalthis": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.3.tgz",
+ "integrity": "sha512-sFdI5LyBiNTHjRd7cGPWapiHWMOXKyuBNX/cWJ3NfzrZQVa8GI/8cofCl74AOVqq9W5kNmguTIzJ/1s2gyI9wA==",
+ "dev": true,
+ "dependencies": {
+ "define-properties": "^1.1.3"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/globby": {
+ "version": "11.1.0",
+ "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz",
+ "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==",
+ "dev": true,
+ "dependencies": {
+ "array-union": "^2.1.0",
+ "dir-glob": "^3.0.1",
+ "fast-glob": "^3.2.9",
+ "ignore": "^5.2.0",
+ "merge2": "^1.4.1",
+ "slash": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/goober": {
+ "version": "2.1.13",
+ "resolved": "https://registry.npmjs.org/goober/-/goober-2.1.13.tgz",
+ "integrity": "sha512-jFj3BQeleOoy7t93E9rZ2de+ScC4lQICLwiAQmKMg9F6roKGaLSHoCDYKkWlSafg138jejvq/mTdvmnwDQgqoQ==",
+ "peerDependencies": {
+ "csstype": "^3.0.10"
+ }
+ },
+ "node_modules/gopd": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz",
+ "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==",
+ "dev": true,
+ "dependencies": {
+ "get-intrinsic": "^1.1.3"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/graceful-fs": {
+ "version": "4.2.11",
+ "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
+ "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="
+ },
+ "node_modules/graphemer": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz",
+ "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==",
+ "dev": true
+ },
+ "node_modules/has": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz",
+ "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==",
+ "dependencies": {
+ "function-bind": "^1.1.1"
+ },
+ "engines": {
+ "node": ">= 0.4.0"
+ }
+ },
+ "node_modules/has-bigints": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.2.tgz",
+ "integrity": "sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ==",
+ "dev": true,
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/has-flag": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
+ "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/has-property-descriptors": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.0.tgz",
+ "integrity": "sha512-62DVLZGoiEBDHQyqG4w9xCuZ7eJEwNmJRWw2VY84Oedb7WFcA27fiEVe8oUQx9hAUJ4ekurquucTGwsyO1XGdQ==",
+ "dev": true,
+ "dependencies": {
+ "get-intrinsic": "^1.1.1"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/has-proto": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.1.tgz",
+ "integrity": "sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==",
+ "dev": true,
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/has-symbols": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz",
+ "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==",
+ "dev": true,
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/has-tostringtag": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.0.tgz",
+ "integrity": "sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ==",
+ "dev": true,
+ "dependencies": {
+ "has-symbols": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/hoist-non-react-statics": {
+ "version": "3.3.2",
+ "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz",
+ "integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==",
+ "dependencies": {
+ "react-is": "^16.7.0"
+ }
+ },
+ "node_modules/human-signals": {
+ "version": "4.3.1",
+ "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-4.3.1.tgz",
+ "integrity": "sha512-nZXjEF2nbo7lIw3mgYjItAfgQXog3OjJogSbKa2CQIIvSGWcKgeJnQlNXip6NglNzYH45nSRiEVimMvYL8DDqQ==",
+ "dev": true,
+ "engines": {
+ "node": ">=14.18.0"
+ }
+ },
+ "node_modules/husky": {
+ "version": "8.0.3",
+ "resolved": "https://registry.npmjs.org/husky/-/husky-8.0.3.tgz",
+ "integrity": "sha512-+dQSyqPh4x1hlO1swXBiNb2HzTDN1I2IGLQx1GrBuiqFJfoMrnZWwVmatvSiO+Iz8fBUnf+lekwNo4c2LlXItg==",
+ "dev": true,
+ "bin": {
+ "husky": "lib/bin.js"
+ },
+ "engines": {
+ "node": ">=14"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/typicode"
+ }
+ },
+ "node_modules/ignore": {
+ "version": "5.2.4",
+ "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.4.tgz",
+ "integrity": "sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ==",
+ "dev": true,
+ "engines": {
+ "node": ">= 4"
+ }
+ },
+ "node_modules/import-fresh": {
+ "version": "3.3.0",
+ "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz",
+ "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==",
+ "dependencies": {
+ "parent-module": "^1.0.0",
+ "resolve-from": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/imurmurhash": {
+ "version": "0.1.4",
+ "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz",
+ "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.8.19"
+ }
+ },
+ "node_modules/indent-string": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz",
+ "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/inflight": {
+ "version": "1.0.6",
+ "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
+ "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==",
+ "dependencies": {
+ "once": "^1.3.0",
+ "wrappy": "1"
+ }
+ },
+ "node_modules/inherits": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
+ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="
+ },
+ "node_modules/internal-slot": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.5.tgz",
+ "integrity": "sha512-Y+R5hJrzs52QCG2laLn4udYVnxsfny9CpOhNhUvk/SSSVyF6T27FzRbF0sroPidSu3X8oEAkOn2K804mjpt6UQ==",
+ "dev": true,
+ "dependencies": {
+ "get-intrinsic": "^1.2.0",
+ "has": "^1.0.3",
+ "side-channel": "^1.0.4"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/is-array-buffer": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.2.tgz",
+ "integrity": "sha512-y+FyyR/w8vfIRq4eQcM1EYgSTnmHXPqaF+IgzgraytCFq5Xh8lllDVmAZolPJiZttZLeFSINPYMaEJ7/vWUa1w==",
+ "dev": true,
+ "dependencies": {
+ "call-bind": "^1.0.2",
+ "get-intrinsic": "^1.2.0",
+ "is-typed-array": "^1.1.10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-arrayish": {
+ "version": "0.2.1",
+ "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz",
+ "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg=="
+ },
+ "node_modules/is-bigint": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.0.4.tgz",
+ "integrity": "sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg==",
+ "dev": true,
+ "dependencies": {
+ "has-bigints": "^1.0.1"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-binary-path": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
+ "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==",
+ "dependencies": {
+ "binary-extensions": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/is-boolean-object": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.1.2.tgz",
+ "integrity": "sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA==",
+ "dev": true,
+ "dependencies": {
+ "call-bind": "^1.0.2",
+ "has-tostringtag": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-callable": {
+ "version": "1.2.7",
+ "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz",
+ "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==",
+ "dev": true,
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-core-module": {
+ "version": "2.13.0",
+ "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.13.0.tgz",
+ "integrity": "sha512-Z7dk6Qo8pOCp3l4tsX2C5ZVas4V+UxwQodwZhLopL91TX8UyyHEXafPcyoeeWuLrwzHcr3igO78wNLwHJHsMCQ==",
+ "dependencies": {
+ "has": "^1.0.3"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-date-object": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.5.tgz",
+ "integrity": "sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==",
+ "dev": true,
+ "dependencies": {
+ "has-tostringtag": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-docker": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-3.0.0.tgz",
+ "integrity": "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==",
+ "dev": true,
+ "bin": {
+ "is-docker": "cli.js"
+ },
+ "engines": {
+ "node": "^12.20.0 || ^14.13.1 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/is-extglob": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
+ "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/is-fullwidth-code-point": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-4.0.0.tgz",
+ "integrity": "sha512-O4L094N2/dZ7xqVdrXhh9r1KODPJpFms8B5sGdJLPy664AgvXsreZUyCQQNItZRDlYug4xStLjNp/sz3HvBowQ==",
+ "dev": true,
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/is-glob": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
+ "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
+ "dependencies": {
+ "is-extglob": "^2.1.1"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/is-inside-container": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/is-inside-container/-/is-inside-container-1.0.0.tgz",
+ "integrity": "sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==",
+ "dev": true,
+ "dependencies": {
+ "is-docker": "^3.0.0"
+ },
+ "bin": {
+ "is-inside-container": "cli.js"
+ },
+ "engines": {
+ "node": ">=14.16"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/is-negative-zero": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.2.tgz",
+ "integrity": "sha512-dqJvarLawXsFbNDeJW7zAz8ItJ9cd28YufuuFzh0G8pNHjJMnY08Dv7sYX2uF5UpQOwieAeOExEYAWWfu7ZZUA==",
+ "dev": true,
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-number": {
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
+ "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
+ "engines": {
+ "node": ">=0.12.0"
+ }
+ },
+ "node_modules/is-number-object": {
+ "version": "1.0.7",
+ "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.0.7.tgz",
+ "integrity": "sha512-k1U0IRzLMo7ZlYIfzRu23Oh6MiIFasgpb9X76eqfFZAqwH44UI4KTBvBYIZ1dSL9ZzChTB9ShHfLkR4pdW5krQ==",
+ "dev": true,
+ "dependencies": {
+ "has-tostringtag": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-path-inside": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz",
+ "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/is-regex": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz",
+ "integrity": "sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==",
+ "dev": true,
+ "dependencies": {
+ "call-bind": "^1.0.2",
+ "has-tostringtag": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-shared-array-buffer": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.2.tgz",
+ "integrity": "sha512-sqN2UDu1/0y6uvXyStCOzyhAjCSlHceFoMKJW8W9EU9cvic/QdsZ0kEU93HEy3IUEFZIiH/3w+AH/UQbPHNdhA==",
+ "dev": true,
+ "dependencies": {
+ "call-bind": "^1.0.2"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-stream": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-3.0.0.tgz",
+ "integrity": "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==",
+ "dev": true,
+ "engines": {
+ "node": "^12.20.0 || ^14.13.1 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/is-string": {
+ "version": "1.0.7",
+ "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.7.tgz",
+ "integrity": "sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==",
+ "dev": true,
+ "dependencies": {
+ "has-tostringtag": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-symbol": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.4.tgz",
+ "integrity": "sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg==",
+ "dev": true,
+ "dependencies": {
+ "has-symbols": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-typed-array": {
+ "version": "1.1.12",
+ "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.12.tgz",
+ "integrity": "sha512-Z14TF2JNG8Lss5/HMqt0//T9JeHXttXy5pH/DBU4vi98ozO2btxzq9MwYDZYnKwU8nRsz/+GVFVRDq3DkVuSPg==",
+ "dev": true,
+ "dependencies": {
+ "which-typed-array": "^1.1.11"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-weakref": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.0.2.tgz",
+ "integrity": "sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ==",
+ "dev": true,
+ "dependencies": {
+ "call-bind": "^1.0.2"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-wsl": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz",
+ "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==",
+ "dev": true,
+ "dependencies": {
+ "is-docker": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/is-wsl/node_modules/is-docker": {
+ "version": "2.2.1",
+ "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz",
+ "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==",
+ "dev": true,
+ "bin": {
+ "is-docker": "cli.js"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/isarray": {
+ "version": "2.0.5",
+ "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz",
+ "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==",
+ "dev": true
+ },
+ "node_modules/isexe": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
+ "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
+ "dev": true
+ },
+ "node_modules/jiti": {
+ "version": "1.19.1",
+ "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.19.1.tgz",
+ "integrity": "sha512-oVhqoRDaBXf7sjkll95LHVS6Myyyb1zaunVwk4Z0+WPSW4gjS0pl01zYKHScTuyEhQsFxV5L4DR5r+YqSyqyyg==",
+ "bin": {
+ "jiti": "bin/jiti.js"
+ }
+ },
+ "node_modules/js-tokens": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
+ "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="
+ },
+ "node_modules/js-yaml": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz",
+ "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==",
+ "dev": true,
+ "dependencies": {
+ "argparse": "^2.0.1"
+ },
+ "bin": {
+ "js-yaml": "bin/js-yaml.js"
+ }
+ },
+ "node_modules/json-parse-even-better-errors": {
+ "version": "2.3.1",
+ "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz",
+ "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w=="
+ },
+ "node_modules/json-schema-traverse": {
+ "version": "0.4.1",
+ "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
+ "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==",
+ "dev": true
+ },
+ "node_modules/json-stable-stringify-without-jsonify": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz",
+ "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==",
+ "dev": true
+ },
+ "node_modules/json5": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz",
+ "integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==",
+ "dev": true,
+ "dependencies": {
+ "minimist": "^1.2.0"
+ },
+ "bin": {
+ "json5": "lib/cli.js"
+ }
+ },
+ "node_modules/jsx-ast-utils": {
+ "version": "3.3.5",
+ "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.5.tgz",
+ "integrity": "sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==",
+ "dev": true,
+ "dependencies": {
+ "array-includes": "^3.1.6",
+ "array.prototype.flat": "^1.3.1",
+ "object.assign": "^4.1.4",
+ "object.values": "^1.1.6"
+ },
+ "engines": {
+ "node": ">=4.0"
+ }
+ },
+ "node_modules/jwt-decode": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/jwt-decode/-/jwt-decode-3.1.2.tgz",
+ "integrity": "sha512-UfpWE/VZn0iP50d8cz9NrZLM9lSWhcJ+0Gt/nm4by88UL+J1SiKN8/5dkjMmbEzwL2CAe+67GsegCbIKtbp75A=="
+ },
+ "node_modules/language-subtag-registry": {
+ "version": "0.3.22",
+ "resolved": "https://registry.npmjs.org/language-subtag-registry/-/language-subtag-registry-0.3.22.tgz",
+ "integrity": "sha512-tN0MCzyWnoz/4nHS6uxdlFWoUZT7ABptwKPQ52Ea7URk6vll88bWBVhodtnlfEuCcKWNGoc+uGbw1cwa9IKh/w==",
+ "dev": true
+ },
+ "node_modules/language-tags": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/language-tags/-/language-tags-1.0.5.tgz",
+ "integrity": "sha512-qJhlO9cGXi6hBGKoxEG/sKZDAHD5Hnu9Hs4WbOY3pCWXDhw0N8x1NenNzm2EnNLkLkk7J2SdxAkDSbb6ftT+UQ==",
+ "dev": true,
+ "dependencies": {
+ "language-subtag-registry": "~0.3.2"
+ }
+ },
+ "node_modules/levn": {
+ "version": "0.4.1",
+ "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz",
+ "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==",
+ "dev": true,
+ "dependencies": {
+ "prelude-ls": "^1.2.1",
+ "type-check": "~0.4.0"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/lilconfig": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.1.0.tgz",
+ "integrity": "sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==",
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/lines-and-columns": {
+ "version": "1.2.4",
+ "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz",
+ "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg=="
+ },
+ "node_modules/lint-staged": {
+ "version": "13.2.3",
+ "resolved": "https://registry.npmjs.org/lint-staged/-/lint-staged-13.2.3.tgz",
+ "integrity": "sha512-zVVEXLuQIhr1Y7R7YAWx4TZLdvuzk7DnmrsTNL0fax6Z3jrpFcas+vKbzxhhvp6TA55m1SQuWkpzI1qbfDZbAg==",
+ "dev": true,
+ "dependencies": {
+ "chalk": "5.2.0",
+ "cli-truncate": "^3.1.0",
+ "commander": "^10.0.0",
+ "debug": "^4.3.4",
+ "execa": "^7.0.0",
+ "lilconfig": "2.1.0",
+ "listr2": "^5.0.7",
+ "micromatch": "^4.0.5",
+ "normalize-path": "^3.0.0",
+ "object-inspect": "^1.12.3",
+ "pidtree": "^0.6.0",
+ "string-argv": "^0.3.1",
+ "yaml": "^2.2.2"
+ },
+ "bin": {
+ "lint-staged": "bin/lint-staged.js"
+ },
+ "engines": {
+ "node": "^14.13.1 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/lint-staged"
+ }
+ },
+ "node_modules/lint-staged/node_modules/chalk": {
+ "version": "5.2.0",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.2.0.tgz",
+ "integrity": "sha512-ree3Gqw/nazQAPuJJEy+avdl7QfZMcUvmHIKgEZkGL+xOBzRvup5Hxo6LHuMceSxOabuJLJm5Yp/92R9eMmMvA==",
+ "dev": true,
+ "engines": {
+ "node": "^12.17.0 || ^14.13 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/chalk?sponsor=1"
+ }
+ },
+ "node_modules/listr2": {
+ "version": "5.0.8",
+ "resolved": "https://registry.npmjs.org/listr2/-/listr2-5.0.8.tgz",
+ "integrity": "sha512-mC73LitKHj9w6v30nLNGPetZIlfpUniNSsxxrbaPcWOjDb92SHPzJPi/t+v1YC/lxKz/AJ9egOjww0qUuFxBpA==",
+ "dev": true,
+ "dependencies": {
+ "cli-truncate": "^2.1.0",
+ "colorette": "^2.0.19",
+ "log-update": "^4.0.0",
+ "p-map": "^4.0.0",
+ "rfdc": "^1.3.0",
+ "rxjs": "^7.8.0",
+ "through": "^2.3.8",
+ "wrap-ansi": "^7.0.0"
+ },
+ "engines": {
+ "node": "^14.13.1 || >=16.0.0"
+ },
+ "peerDependencies": {
+ "enquirer": ">= 2.3.0 < 3"
+ },
+ "peerDependenciesMeta": {
+ "enquirer": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/listr2/node_modules/cli-truncate": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-2.1.0.tgz",
+ "integrity": "sha512-n8fOixwDD6b/ObinzTrp1ZKFzbgvKZvuz/TvejnLn1aQfC6r52XEx85FmuC+3HI+JM7coBRXUvNqEU2PHVrHpg==",
+ "dev": true,
+ "dependencies": {
+ "slice-ansi": "^3.0.0",
+ "string-width": "^4.2.0"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/listr2/node_modules/emoji-regex": {
+ "version": "8.0.0",
+ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
+ "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
+ "dev": true
+ },
+ "node_modules/listr2/node_modules/is-fullwidth-code-point": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
+ "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/listr2/node_modules/slice-ansi": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-3.0.0.tgz",
+ "integrity": "sha512-pSyv7bSTC7ig9Dcgbw9AuRNUb5k5V6oDudjZoMBSr13qpLBG7tB+zgCkARjq7xIUgdz5P1Qe8u+rSGdouOOIyQ==",
+ "dev": true,
+ "dependencies": {
+ "ansi-styles": "^4.0.0",
+ "astral-regex": "^2.0.0",
+ "is-fullwidth-code-point": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/listr2/node_modules/string-width": {
+ "version": "4.2.3",
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
+ "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
+ "dev": true,
+ "dependencies": {
+ "emoji-regex": "^8.0.0",
+ "is-fullwidth-code-point": "^3.0.0",
+ "strip-ansi": "^6.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/locate-path": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz",
+ "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==",
+ "dev": true,
+ "dependencies": {
+ "p-locate": "^5.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/lodash.merge": {
+ "version": "4.6.2",
+ "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",
+ "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==",
+ "dev": true
+ },
+ "node_modules/log-update": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/log-update/-/log-update-4.0.0.tgz",
+ "integrity": "sha512-9fkkDevMefjg0mmzWFBW8YkFP91OrizzkW3diF7CpG+S2EYdy4+TVfGwz1zeF8x7hCx1ovSPTOE9Ngib74qqUg==",
+ "dev": true,
+ "dependencies": {
+ "ansi-escapes": "^4.3.0",
+ "cli-cursor": "^3.1.0",
+ "slice-ansi": "^4.0.0",
+ "wrap-ansi": "^6.2.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/log-update/node_modules/emoji-regex": {
+ "version": "8.0.0",
+ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
+ "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
+ "dev": true
+ },
+ "node_modules/log-update/node_modules/is-fullwidth-code-point": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
+ "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/log-update/node_modules/slice-ansi": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-4.0.0.tgz",
+ "integrity": "sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==",
+ "dev": true,
+ "dependencies": {
+ "ansi-styles": "^4.0.0",
+ "astral-regex": "^2.0.0",
+ "is-fullwidth-code-point": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/slice-ansi?sponsor=1"
+ }
+ },
+ "node_modules/log-update/node_modules/string-width": {
+ "version": "4.2.3",
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
+ "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
+ "dev": true,
+ "dependencies": {
+ "emoji-regex": "^8.0.0",
+ "is-fullwidth-code-point": "^3.0.0",
+ "strip-ansi": "^6.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/log-update/node_modules/wrap-ansi": {
+ "version": "6.2.0",
+ "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz",
+ "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==",
+ "dev": true,
+ "dependencies": {
+ "ansi-styles": "^4.0.0",
+ "string-width": "^4.1.0",
+ "strip-ansi": "^6.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/loose-envify": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
+ "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
+ "dependencies": {
+ "js-tokens": "^3.0.0 || ^4.0.0"
+ },
+ "bin": {
+ "loose-envify": "cli.js"
+ }
+ },
+ "node_modules/lru-cache": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
+ "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==",
+ "dev": true,
+ "dependencies": {
+ "yallist": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/merge-stream": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz",
+ "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==",
+ "dev": true
+ },
+ "node_modules/merge2": {
+ "version": "1.4.1",
+ "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz",
+ "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==",
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/micromatch": {
+ "version": "4.0.5",
+ "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz",
+ "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==",
+ "dependencies": {
+ "braces": "^3.0.2",
+ "picomatch": "^2.3.1"
+ },
+ "engines": {
+ "node": ">=8.6"
+ }
+ },
+ "node_modules/mimic-fn": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-4.0.0.tgz",
+ "integrity": "sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==",
+ "dev": true,
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/minimatch": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
+ "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
+ "dependencies": {
+ "brace-expansion": "^1.1.7"
+ },
+ "engines": {
+ "node": "*"
+ }
+ },
+ "node_modules/minimist": {
+ "version": "1.2.8",
+ "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz",
+ "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==",
+ "dev": true,
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/ms": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
+ "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==",
+ "dev": true
+ },
+ "node_modules/mz": {
+ "version": "2.7.0",
+ "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz",
+ "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==",
+ "dependencies": {
+ "any-promise": "^1.0.0",
+ "object-assign": "^4.0.1",
+ "thenify-all": "^1.0.0"
+ }
+ },
+ "node_modules/nanoid": {
+ "version": "3.3.6",
+ "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.6.tgz",
+ "integrity": "sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "bin": {
+ "nanoid": "bin/nanoid.cjs"
+ },
+ "engines": {
+ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
+ }
+ },
+ "node_modules/natural-compare": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz",
+ "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==",
+ "dev": true
+ },
+ "node_modules/natural-compare-lite": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/natural-compare-lite/-/natural-compare-lite-1.4.0.tgz",
+ "integrity": "sha512-Tj+HTDSJJKaZnfiuw+iaF9skdPpTo2GtEly5JHnWV/hfv2Qj/9RKsGISQtLh2ox3l5EAGw487hnBee0sIJ6v2g==",
+ "dev": true
+ },
+ "node_modules/next": {
+ "version": "13.4.13",
+ "resolved": "https://registry.npmjs.org/next/-/next-13.4.13.tgz",
+ "integrity": "sha512-A3YVbVDNeXLhWsZ8Nf6IkxmNlmTNz0yVg186NJ97tGZqPDdPzTrHotJ+A1cuJm2XfuWPrKOUZILl5iBQkIf8Jw==",
+ "dependencies": {
+ "@next/env": "13.4.13",
+ "@swc/helpers": "0.5.1",
+ "busboy": "1.6.0",
+ "caniuse-lite": "^1.0.30001406",
+ "postcss": "8.4.14",
+ "styled-jsx": "5.1.1",
+ "watchpack": "2.4.0",
+ "zod": "3.21.4"
+ },
+ "bin": {
+ "next": "dist/bin/next"
+ },
+ "engines": {
+ "node": ">=16.8.0"
+ },
+ "optionalDependencies": {
+ "@next/swc-darwin-arm64": "13.4.13",
+ "@next/swc-darwin-x64": "13.4.13",
+ "@next/swc-linux-arm64-gnu": "13.4.13",
+ "@next/swc-linux-arm64-musl": "13.4.13",
+ "@next/swc-linux-x64-gnu": "13.4.13",
+ "@next/swc-linux-x64-musl": "13.4.13",
+ "@next/swc-win32-arm64-msvc": "13.4.13",
+ "@next/swc-win32-ia32-msvc": "13.4.13",
+ "@next/swc-win32-x64-msvc": "13.4.13"
+ },
+ "peerDependencies": {
+ "@opentelemetry/api": "^1.1.0",
+ "react": "^18.2.0",
+ "react-dom": "^18.2.0",
+ "sass": "^1.3.0"
+ },
+ "peerDependenciesMeta": {
+ "@opentelemetry/api": {
+ "optional": true
+ },
+ "sass": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/next/node_modules/postcss": {
+ "version": "8.4.14",
+ "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.14.tgz",
+ "integrity": "sha512-E398TUmfAYFPBSdzgeieK2Y1+1cpdxJx8yXbK/m57nRhKSmk1GB2tO4lbLBtlkfPQTDKfe4Xqv1ASWPpayPEig==",
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/postcss/"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/postcss"
+ }
+ ],
+ "dependencies": {
+ "nanoid": "^3.3.4",
+ "picocolors": "^1.0.0",
+ "source-map-js": "^1.0.2"
+ },
+ "engines": {
+ "node": "^10 || ^12 || >=14"
+ }
+ },
+ "node_modules/next/node_modules/zod": {
+ "version": "3.21.4",
+ "resolved": "https://registry.npmjs.org/zod/-/zod-3.21.4.tgz",
+ "integrity": "sha512-m46AKbrzKVzOzs/DZgVnG5H55N1sv1M8qZU3A8RIKbs3mrACDNeIOeilDymVb2HdmP8uwshOCF4uJ8uM9rCqJw==",
+ "funding": {
+ "url": "https://github.com/sponsors/colinhacks"
+ }
+ },
+ "node_modules/node-releases": {
+ "version": "2.0.13",
+ "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.13.tgz",
+ "integrity": "sha512-uYr7J37ae/ORWdZeQ1xxMJe3NtdmqMC/JZK+geofDrkLUApKRHPd18/TxtBOJ4A0/+uUIliorNrfYV6s1b02eQ=="
+ },
+ "node_modules/normalize-path": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
+ "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/normalize-range": {
+ "version": "0.1.2",
+ "resolved": "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz",
+ "integrity": "sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/npm-run-path": {
+ "version": "5.1.0",
+ "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-5.1.0.tgz",
+ "integrity": "sha512-sJOdmRGrY2sjNTRMbSvluQqg+8X7ZK61yvzBEIDhz4f8z1TZFYABsqjjCBd/0PUNE9M6QDgHJXQkGUEm7Q+l9Q==",
+ "dev": true,
+ "dependencies": {
+ "path-key": "^4.0.0"
+ },
+ "engines": {
+ "node": "^12.20.0 || ^14.13.1 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/npm-run-path/node_modules/path-key": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/path-key/-/path-key-4.0.0.tgz",
+ "integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==",
+ "dev": true,
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/object-assign": {
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
+ "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/object-hash": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz",
+ "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==",
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/object-inspect": {
+ "version": "1.12.3",
+ "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.3.tgz",
+ "integrity": "sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g==",
+ "dev": true,
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/object-keys": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz",
+ "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==",
+ "dev": true,
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/object.assign": {
+ "version": "4.1.4",
+ "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.4.tgz",
+ "integrity": "sha512-1mxKf0e58bvyjSCtKYY4sRe9itRk3PJpquJOjeIkz885CczcI4IvJJDLPS72oowuSh+pBxUFROpX+TU++hxhZQ==",
+ "dev": true,
+ "dependencies": {
+ "call-bind": "^1.0.2",
+ "define-properties": "^1.1.4",
+ "has-symbols": "^1.0.3",
+ "object-keys": "^1.1.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/object.entries": {
+ "version": "1.1.6",
+ "resolved": "https://registry.npmjs.org/object.entries/-/object.entries-1.1.6.tgz",
+ "integrity": "sha512-leTPzo4Zvg3pmbQ3rDK69Rl8GQvIqMWubrkxONG9/ojtFE2rD9fjMKfSI5BxW3osRH1m6VdzmqK8oAY9aT4x5w==",
+ "dev": true,
+ "dependencies": {
+ "call-bind": "^1.0.2",
+ "define-properties": "^1.1.4",
+ "es-abstract": "^1.20.4"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/object.fromentries": {
+ "version": "2.0.6",
+ "resolved": "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.6.tgz",
+ "integrity": "sha512-VciD13dswC4j1Xt5394WR4MzmAQmlgN72phd/riNp9vtD7tp4QQWJ0R4wvclXcafgcYK8veHRed2W6XeGBvcfg==",
+ "dev": true,
+ "dependencies": {
+ "call-bind": "^1.0.2",
+ "define-properties": "^1.1.4",
+ "es-abstract": "^1.20.4"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/object.groupby": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/object.groupby/-/object.groupby-1.0.0.tgz",
+ "integrity": "sha512-70MWG6NfRH9GnbZOikuhPPYzpUpof9iW2J9E4dW7FXTqPNb6rllE6u39SKwwiNh8lCwX3DDb5OgcKGiEBrTTyw==",
+ "dev": true,
+ "dependencies": {
+ "call-bind": "^1.0.2",
+ "define-properties": "^1.2.0",
+ "es-abstract": "^1.21.2",
+ "get-intrinsic": "^1.2.1"
+ }
+ },
+ "node_modules/object.hasown": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/object.hasown/-/object.hasown-1.1.2.tgz",
+ "integrity": "sha512-B5UIT3J1W+WuWIU55h0mjlwaqxiE5vYENJXIXZ4VFe05pNYrkKuK0U/6aFcb0pKywYJh7IhfoqUfKVmrJJHZHw==",
+ "dev": true,
+ "dependencies": {
+ "define-properties": "^1.1.4",
+ "es-abstract": "^1.20.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/object.values": {
+ "version": "1.1.6",
+ "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.1.6.tgz",
+ "integrity": "sha512-FVVTkD1vENCsAcwNs9k6jea2uHC/X0+JcjG8YA60FN5CMaJmG95wT9jek/xX9nornqGRrBkKtzuAu2wuHpKqvw==",
+ "dev": true,
+ "dependencies": {
+ "call-bind": "^1.0.2",
+ "define-properties": "^1.1.4",
+ "es-abstract": "^1.20.4"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/once": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
+ "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
+ "dependencies": {
+ "wrappy": "1"
+ }
+ },
+ "node_modules/onetime": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/onetime/-/onetime-6.0.0.tgz",
+ "integrity": "sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==",
+ "dev": true,
+ "dependencies": {
+ "mimic-fn": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/open": {
+ "version": "9.1.0",
+ "resolved": "https://registry.npmjs.org/open/-/open-9.1.0.tgz",
+ "integrity": "sha512-OS+QTnw1/4vrf+9hh1jc1jnYjzSG4ttTBB8UxOwAnInG3Uo4ssetzC1ihqaIHjLJnA5GGlRl6QlZXOTQhRBUvg==",
+ "dev": true,
+ "dependencies": {
+ "default-browser": "^4.0.0",
+ "define-lazy-prop": "^3.0.0",
+ "is-inside-container": "^1.0.0",
+ "is-wsl": "^2.2.0"
+ },
+ "engines": {
+ "node": ">=14.16"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/optionator": {
+ "version": "0.9.3",
+ "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.3.tgz",
+ "integrity": "sha512-JjCoypp+jKn1ttEFExxhetCKeJt9zhAgAve5FXHixTvFDW/5aEktX9bufBKLRRMdU7bNtpLfcGu94B3cdEJgjg==",
+ "dev": true,
+ "dependencies": {
+ "@aashutoshrathi/word-wrap": "^1.2.3",
+ "deep-is": "^0.1.3",
+ "fast-levenshtein": "^2.0.6",
+ "levn": "^0.4.1",
+ "prelude-ls": "^1.2.1",
+ "type-check": "^0.4.0"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/ordinal": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/ordinal/-/ordinal-1.0.3.tgz",
+ "integrity": "sha512-cMddMgb2QElm8G7vdaa02jhUNbTSrhsgAGUz1OokD83uJTwSUn+nKoNoKVVaRa08yF6sgfO7Maou1+bgLd9rdQ=="
+ },
+ "node_modules/p-limit": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz",
+ "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==",
+ "dev": true,
+ "dependencies": {
+ "yocto-queue": "^0.1.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/p-locate": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz",
+ "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==",
+ "dev": true,
+ "dependencies": {
+ "p-limit": "^3.0.2"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/p-map": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/p-map/-/p-map-4.0.0.tgz",
+ "integrity": "sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==",
+ "dev": true,
+ "dependencies": {
+ "aggregate-error": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/parent-module": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
+ "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==",
+ "dependencies": {
+ "callsites": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/parse-json": {
+ "version": "5.2.0",
+ "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz",
+ "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==",
+ "dependencies": {
+ "@babel/code-frame": "^7.0.0",
+ "error-ex": "^1.3.1",
+ "json-parse-even-better-errors": "^2.3.0",
+ "lines-and-columns": "^1.1.6"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/path-exists": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
+ "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/path-is-absolute": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
+ "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/path-key": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
+ "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/path-parse": {
+ "version": "1.0.7",
+ "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz",
+ "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw=="
+ },
+ "node_modules/path-type": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz",
+ "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/picocolors": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz",
+ "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ=="
+ },
+ "node_modules/picomatch": {
+ "version": "2.3.1",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
+ "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
+ "engines": {
+ "node": ">=8.6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/jonschlinkert"
+ }
+ },
+ "node_modules/pidtree": {
+ "version": "0.6.0",
+ "resolved": "https://registry.npmjs.org/pidtree/-/pidtree-0.6.0.tgz",
+ "integrity": "sha512-eG2dWTVw5bzqGRztnHExczNxt5VGsE6OwTeCG3fdUf9KBsZzO3R5OIIIzWR+iZA0NtZ+RDVdaoE2dK1cn6jH4g==",
+ "dev": true,
+ "bin": {
+ "pidtree": "bin/pidtree.js"
+ },
+ "engines": {
+ "node": ">=0.10"
+ }
+ },
+ "node_modules/pify": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz",
+ "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/pirates": {
+ "version": "4.0.6",
+ "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.6.tgz",
+ "integrity": "sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==",
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/postcss": {
+ "version": "8.4.27",
+ "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.27.tgz",
+ "integrity": "sha512-gY/ACJtJPSmUFPDCHtX78+01fHa64FaU4zaaWfuh1MhGJISufJAH4cun6k/8fwsHYeK4UQmENQK+tRLCFJE8JQ==",
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/postcss/"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/postcss"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "dependencies": {
+ "nanoid": "^3.3.6",
+ "picocolors": "^1.0.0",
+ "source-map-js": "^1.0.2"
+ },
+ "engines": {
+ "node": "^10 || ^12 || >=14"
+ }
+ },
+ "node_modules/postcss-import": {
+ "version": "15.1.0",
+ "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-15.1.0.tgz",
+ "integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==",
+ "dependencies": {
+ "postcss-value-parser": "^4.0.0",
+ "read-cache": "^1.0.0",
+ "resolve": "^1.1.7"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ },
+ "peerDependencies": {
+ "postcss": "^8.0.0"
+ }
+ },
+ "node_modules/postcss-js": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.0.1.tgz",
+ "integrity": "sha512-dDLF8pEO191hJMtlHFPRa8xsizHaM82MLfNkUHdUtVEV3tgTp5oj+8qbEqYM57SLfc74KSbw//4SeJma2LRVIw==",
+ "dependencies": {
+ "camelcase-css": "^2.0.1"
+ },
+ "engines": {
+ "node": "^12 || ^14 || >= 16"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/postcss/"
+ },
+ "peerDependencies": {
+ "postcss": "^8.4.21"
+ }
+ },
+ "node_modules/postcss-load-config": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-4.0.1.tgz",
+ "integrity": "sha512-vEJIc8RdiBRu3oRAI0ymerOn+7rPuMvRXslTvZUKZonDHFIczxztIyJ1urxM1x9JXEikvpWWTUUqal5j/8QgvA==",
+ "dependencies": {
+ "lilconfig": "^2.0.5",
+ "yaml": "^2.1.1"
+ },
+ "engines": {
+ "node": ">= 14"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/postcss/"
+ },
+ "peerDependencies": {
+ "postcss": ">=8.0.9",
+ "ts-node": ">=9.0.0"
+ },
+ "peerDependenciesMeta": {
+ "postcss": {
+ "optional": true
+ },
+ "ts-node": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/postcss-nested": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.0.1.tgz",
+ "integrity": "sha512-mEp4xPMi5bSWiMbsgoPfcP74lsWLHkQbZc3sY+jWYd65CUwXrUaTp0fmNpa01ZcETKlIgUdFN/MpS2xZtqL9dQ==",
+ "dependencies": {
+ "postcss-selector-parser": "^6.0.11"
+ },
+ "engines": {
+ "node": ">=12.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/postcss/"
+ },
+ "peerDependencies": {
+ "postcss": "^8.2.14"
+ }
+ },
+ "node_modules/postcss-selector-parser": {
+ "version": "6.0.13",
+ "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.13.tgz",
+ "integrity": "sha512-EaV1Gl4mUEV4ddhDnv/xtj7sxwrwxdetHdWUGnT4VJQf+4d05v6lHYZr8N573k5Z0BViss7BDhfWtKS3+sfAqQ==",
+ "dependencies": {
+ "cssesc": "^3.0.0",
+ "util-deprecate": "^1.0.2"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/postcss-value-parser": {
+ "version": "4.2.0",
+ "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz",
+ "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ=="
+ },
+ "node_modules/prelude-ls": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
+ "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==",
+ "dev": true,
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/prettier": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.0.1.tgz",
+ "integrity": "sha512-fcOWSnnpCrovBsmFZIGIy9UqK2FaI7Hqax+DIO0A9UxeVoY4iweyaFjS5TavZN97Hfehph0nhsZnjlVKzEQSrQ==",
+ "dev": true,
+ "bin": {
+ "prettier": "bin/prettier.cjs"
+ },
+ "engines": {
+ "node": ">=14"
+ },
+ "funding": {
+ "url": "https://github.com/prettier/prettier?sponsor=1"
+ }
+ },
+ "node_modules/prettier-linter-helpers": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/prettier-linter-helpers/-/prettier-linter-helpers-1.0.0.tgz",
+ "integrity": "sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w==",
+ "dev": true,
+ "dependencies": {
+ "fast-diff": "^1.1.2"
+ },
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/prettier-plugin-tailwindcss": {
+ "version": "0.4.1",
+ "resolved": "https://registry.npmjs.org/prettier-plugin-tailwindcss/-/prettier-plugin-tailwindcss-0.4.1.tgz",
+ "integrity": "sha512-hwn2EiJmv8M+AW4YDkbjJ6HlZCTzLyz1QlySn9sMuKV/Px0fjwldlB7tol8GzdgqtkdPtzT3iJ4UzdnYXP25Ag==",
+ "dev": true,
+ "engines": {
+ "node": ">=12.17.0"
+ },
+ "peerDependencies": {
+ "@ianvs/prettier-plugin-sort-imports": "*",
+ "@prettier/plugin-pug": "*",
+ "@shopify/prettier-plugin-liquid": "*",
+ "@shufo/prettier-plugin-blade": "*",
+ "@trivago/prettier-plugin-sort-imports": "*",
+ "prettier": "^2.2 || ^3.0",
+ "prettier-plugin-astro": "*",
+ "prettier-plugin-css-order": "*",
+ "prettier-plugin-import-sort": "*",
+ "prettier-plugin-jsdoc": "*",
+ "prettier-plugin-marko": "*",
+ "prettier-plugin-organize-attributes": "*",
+ "prettier-plugin-organize-imports": "*",
+ "prettier-plugin-style-order": "*",
+ "prettier-plugin-svelte": "*",
+ "prettier-plugin-twig-melody": "*"
+ },
+ "peerDependenciesMeta": {
+ "@ianvs/prettier-plugin-sort-imports": {
+ "optional": true
+ },
+ "@prettier/plugin-pug": {
+ "optional": true
+ },
+ "@shopify/prettier-plugin-liquid": {
+ "optional": true
+ },
+ "@shufo/prettier-plugin-blade": {
+ "optional": true
+ },
+ "@trivago/prettier-plugin-sort-imports": {
+ "optional": true
+ },
+ "prettier-plugin-astro": {
+ "optional": true
+ },
+ "prettier-plugin-css-order": {
+ "optional": true
+ },
+ "prettier-plugin-import-sort": {
+ "optional": true
+ },
+ "prettier-plugin-jsdoc": {
+ "optional": true
+ },
+ "prettier-plugin-marko": {
+ "optional": true
+ },
+ "prettier-plugin-organize-attributes": {
+ "optional": true
+ },
+ "prettier-plugin-organize-imports": {
+ "optional": true
+ },
+ "prettier-plugin-style-order": {
+ "optional": true
+ },
+ "prettier-plugin-svelte": {
+ "optional": true
+ },
+ "prettier-plugin-twig-melody": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/prop-types": {
+ "version": "15.8.1",
+ "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
+ "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==",
+ "dependencies": {
+ "loose-envify": "^1.4.0",
+ "object-assign": "^4.1.1",
+ "react-is": "^16.13.1"
+ }
+ },
+ "node_modules/punycode": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.0.tgz",
+ "integrity": "sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==",
+ "dev": true,
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/queue-microtask": {
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
+ "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ]
+ },
+ "node_modules/react": {
+ "version": "18.2.0",
+ "resolved": "https://registry.npmjs.org/react/-/react-18.2.0.tgz",
+ "integrity": "sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==",
+ "dependencies": {
+ "loose-envify": "^1.1.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/react-datepicker": {
+ "version": "4.16.0",
+ "resolved": "https://registry.npmjs.org/react-datepicker/-/react-datepicker-4.16.0.tgz",
+ "integrity": "sha512-hNQ0PAg/LQoVbDUO/RWAdm/RYmPhN3cz7LuQ3hqbs24OSp69QCiKOJRrQ4jk1gv1jNR5oYu8SjjgfDh8q6Q1yw==",
+ "dependencies": {
+ "@popperjs/core": "^2.11.8",
+ "classnames": "^2.2.6",
+ "date-fns": "^2.30.0",
+ "prop-types": "^15.7.2",
+ "react-onclickoutside": "^6.12.2",
+ "react-popper": "^2.3.0"
+ },
+ "peerDependencies": {
+ "react": "^16.9.0 || ^17 || ^18",
+ "react-dom": "^16.9.0 || ^17 || ^18"
+ }
+ },
+ "node_modules/react-dom": {
+ "version": "18.2.0",
+ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.2.0.tgz",
+ "integrity": "sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==",
+ "dependencies": {
+ "loose-envify": "^1.1.0",
+ "scheduler": "^0.23.0"
+ },
+ "peerDependencies": {
+ "react": "^18.2.0"
+ }
+ },
+ "node_modules/react-fast-compare": {
+ "version": "3.2.2",
+ "resolved": "https://registry.npmjs.org/react-fast-compare/-/react-fast-compare-3.2.2.tgz",
+ "integrity": "sha512-nsO+KSNgo1SbJqJEYRE9ERzo7YtYbou/OqjSQKxV7jcKox7+usiUVZOAC+XnDOABXggQTno0Y1CpVnuWEc1boQ=="
+ },
+ "node_modules/react-hook-form": {
+ "version": "7.45.4",
+ "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.45.4.tgz",
+ "integrity": "sha512-HGDV1JOOBPZj10LB3+OZgfDBTn+IeEsNOKiq/cxbQAIbKaiJUe/KV8DBUzsx0Gx/7IG/orWqRRm736JwOfUSWQ==",
+ "engines": {
+ "node": ">=12.22.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/react-hook-form"
+ },
+ "peerDependencies": {
+ "react": "^16.8.0 || ^17 || ^18"
+ }
+ },
+ "node_modules/react-hot-toast": {
+ "version": "2.4.1",
+ "resolved": "https://registry.npmjs.org/react-hot-toast/-/react-hot-toast-2.4.1.tgz",
+ "integrity": "sha512-j8z+cQbWIM5LY37pR6uZR6D4LfseplqnuAO4co4u8917hBUvXlEqyP1ZzqVLcqoyUesZZv/ImreoCeHVDpE5pQ==",
+ "dependencies": {
+ "goober": "^2.1.10"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "peerDependencies": {
+ "react": ">=16",
+ "react-dom": ">=16"
+ }
+ },
+ "node_modules/react-is": {
+ "version": "16.13.1",
+ "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
+ "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="
+ },
+ "node_modules/react-onclickoutside": {
+ "version": "6.13.0",
+ "resolved": "https://registry.npmjs.org/react-onclickoutside/-/react-onclickoutside-6.13.0.tgz",
+ "integrity": "sha512-ty8So6tcUpIb+ZE+1HAhbLROvAIJYyJe/1vRrrcmW+jLsaM+/powDRqxzo6hSh9CuRZGSL1Q8mvcF5WRD93a0A==",
+ "funding": {
+ "type": "individual",
+ "url": "https://github.com/Pomax/react-onclickoutside/blob/master/FUNDING.md"
+ },
+ "peerDependencies": {
+ "react": "^15.5.x || ^16.x || ^17.x || ^18.x",
+ "react-dom": "^15.5.x || ^16.x || ^17.x || ^18.x"
+ }
+ },
+ "node_modules/react-popper": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/react-popper/-/react-popper-2.3.0.tgz",
+ "integrity": "sha512-e1hj8lL3uM+sgSR4Lxzn5h1GxBlpa4CQz0XLF8kx4MDrDRWY0Ena4c97PUeSX9i5W3UAfDP0z0FXCTQkoXUl3Q==",
+ "dependencies": {
+ "react-fast-compare": "^3.0.1",
+ "warning": "^4.0.2"
+ },
+ "peerDependencies": {
+ "@popperjs/core": "^2.0.0",
+ "react": "^16.8.0 || ^17 || ^18",
+ "react-dom": "^16.8.0 || ^17 || ^18"
+ }
+ },
+ "node_modules/react-transition-group": {
+ "version": "4.4.5",
+ "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz",
+ "integrity": "sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==",
+ "dependencies": {
+ "@babel/runtime": "^7.5.5",
+ "dom-helpers": "^5.0.1",
+ "loose-envify": "^1.4.0",
+ "prop-types": "^15.6.2"
+ },
+ "peerDependencies": {
+ "react": ">=16.6.0",
+ "react-dom": ">=16.6.0"
+ }
+ },
+ "node_modules/read-cache": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz",
+ "integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==",
+ "dependencies": {
+ "pify": "^2.3.0"
+ }
+ },
+ "node_modules/readdirp": {
+ "version": "3.6.0",
+ "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
+ "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==",
+ "dependencies": {
+ "picomatch": "^2.2.1"
+ },
+ "engines": {
+ "node": ">=8.10.0"
+ }
+ },
+ "node_modules/regenerator-runtime": {
+ "version": "0.14.0",
+ "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.0.tgz",
+ "integrity": "sha512-srw17NI0TUWHuGa5CFGGmhfNIeja30WMBfbslPNhf6JrqQlLN5gcrvig1oqPxiVaXb0oW0XRKtH6Nngs5lKCIA=="
+ },
+ "node_modules/regexp.prototype.flags": {
+ "version": "1.5.0",
+ "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.0.tgz",
+ "integrity": "sha512-0SutC3pNudRKgquxGoRGIz946MZVHqbNfPjBdxeOhBrdgDKlRoXmYLQN9xRbrR09ZXWeGAdPuif7egofn6v5LA==",
+ "dev": true,
+ "dependencies": {
+ "call-bind": "^1.0.2",
+ "define-properties": "^1.2.0",
+ "functions-have-names": "^1.2.3"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/resolve": {
+ "version": "1.22.4",
+ "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.4.tgz",
+ "integrity": "sha512-PXNdCiPqDqeUou+w1C2eTQbNfxKSuMxqTCuvlmmMsk1NWHL5fRrhY6Pl0qEYYc6+QqGClco1Qj8XnjPego4wfg==",
+ "dependencies": {
+ "is-core-module": "^2.13.0",
+ "path-parse": "^1.0.7",
+ "supports-preserve-symlinks-flag": "^1.0.0"
+ },
+ "bin": {
+ "resolve": "bin/resolve"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/resolve-from": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz",
+ "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==",
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/resolve-pkg-maps": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz",
+ "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==",
+ "dev": true,
+ "funding": {
+ "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1"
+ }
+ },
+ "node_modules/restore-cursor": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz",
+ "integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==",
+ "dev": true,
+ "dependencies": {
+ "onetime": "^5.1.0",
+ "signal-exit": "^3.0.2"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/restore-cursor/node_modules/mimic-fn": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz",
+ "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==",
+ "dev": true,
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/restore-cursor/node_modules/onetime": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz",
+ "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==",
+ "dev": true,
+ "dependencies": {
+ "mimic-fn": "^2.1.0"
+ },
+ "engines": {
+ "node": ">=6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/reusify": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz",
+ "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==",
+ "engines": {
+ "iojs": ">=1.0.0",
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/rfdc": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.3.0.tgz",
+ "integrity": "sha512-V2hovdzFbOi77/WajaSMXk2OLm+xNIeQdMMuB7icj7bk6zi2F8GGAxigcnDFpJHbNyNcgyJDiP+8nOrY5cZGrA==",
+ "dev": true
+ },
+ "node_modules/rimraf": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz",
+ "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==",
+ "dev": true,
+ "dependencies": {
+ "glob": "^7.1.3"
+ },
+ "bin": {
+ "rimraf": "bin.js"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/run-applescript": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/run-applescript/-/run-applescript-5.0.0.tgz",
+ "integrity": "sha512-XcT5rBksx1QdIhlFOCtgZkB99ZEouFZ1E2Kc2LHqNW13U3/74YGdkQRmThTwxy4QIyookibDKYZOPqX//6BlAg==",
+ "dev": true,
+ "dependencies": {
+ "execa": "^5.0.0"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/run-applescript/node_modules/execa": {
+ "version": "5.1.1",
+ "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz",
+ "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==",
+ "dev": true,
+ "dependencies": {
+ "cross-spawn": "^7.0.3",
+ "get-stream": "^6.0.0",
+ "human-signals": "^2.1.0",
+ "is-stream": "^2.0.0",
+ "merge-stream": "^2.0.0",
+ "npm-run-path": "^4.0.1",
+ "onetime": "^5.1.2",
+ "signal-exit": "^3.0.3",
+ "strip-final-newline": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sindresorhus/execa?sponsor=1"
+ }
+ },
+ "node_modules/run-applescript/node_modules/human-signals": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz",
+ "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==",
+ "dev": true,
+ "engines": {
+ "node": ">=10.17.0"
+ }
+ },
+ "node_modules/run-applescript/node_modules/is-stream": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz",
+ "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/run-applescript/node_modules/mimic-fn": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz",
+ "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==",
+ "dev": true,
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/run-applescript/node_modules/npm-run-path": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz",
+ "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==",
+ "dev": true,
+ "dependencies": {
+ "path-key": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/run-applescript/node_modules/onetime": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz",
+ "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==",
+ "dev": true,
+ "dependencies": {
+ "mimic-fn": "^2.1.0"
+ },
+ "engines": {
+ "node": ">=6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/run-applescript/node_modules/strip-final-newline": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz",
+ "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==",
+ "dev": true,
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/run-parallel": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz",
+ "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "dependencies": {
+ "queue-microtask": "^1.2.2"
+ }
+ },
+ "node_modules/rxjs": {
+ "version": "7.8.1",
+ "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz",
+ "integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==",
+ "dev": true,
+ "dependencies": {
+ "tslib": "^2.1.0"
+ }
+ },
+ "node_modules/safe-array-concat": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.0.0.tgz",
+ "integrity": "sha512-9dVEFruWIsnie89yym+xWTAYASdpw3CJV7Li/6zBewGf9z2i1j31rP6jnY0pHEO4QZh6N0K11bFjWmdR8UGdPQ==",
+ "dev": true,
+ "dependencies": {
+ "call-bind": "^1.0.2",
+ "get-intrinsic": "^1.2.0",
+ "has-symbols": "^1.0.3",
+ "isarray": "^2.0.5"
+ },
+ "engines": {
+ "node": ">=0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/safe-regex-test": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.0.0.tgz",
+ "integrity": "sha512-JBUUzyOgEwXQY1NuPtvcj/qcBDbDmEvWufhlnXZIm75DEHp+afM1r1ujJpJsV/gSM4t59tpDyPi1sd6ZaPFfsA==",
+ "dev": true,
+ "dependencies": {
+ "call-bind": "^1.0.2",
+ "get-intrinsic": "^1.1.3",
+ "is-regex": "^1.1.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/scheduler": {
+ "version": "0.23.0",
+ "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.0.tgz",
+ "integrity": "sha512-CtuThmgHNg7zIZWAXi3AsyIzA3n4xx7aNyjwC2VJldO2LMVDhFK+63xGqq6CsJH4rTAt6/M+N4GhZiDYPx9eUw==",
+ "dependencies": {
+ "loose-envify": "^1.1.0"
+ }
+ },
+ "node_modules/semver": {
+ "version": "7.5.4",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz",
+ "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==",
+ "dev": true,
+ "dependencies": {
+ "lru-cache": "^6.0.0"
+ },
+ "bin": {
+ "semver": "bin/semver.js"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/shebang-command": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
+ "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
+ "dev": true,
+ "dependencies": {
+ "shebang-regex": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/shebang-regex": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
+ "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/side-channel": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz",
+ "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==",
+ "dev": true,
+ "dependencies": {
+ "call-bind": "^1.0.0",
+ "get-intrinsic": "^1.0.2",
+ "object-inspect": "^1.9.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/signal-exit": {
+ "version": "3.0.7",
+ "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz",
+ "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==",
+ "dev": true
+ },
+ "node_modules/slash": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz",
+ "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/slice-ansi": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-5.0.0.tgz",
+ "integrity": "sha512-FC+lgizVPfie0kkhqUScwRu1O/lF6NOgJmlCgK+/LYxDCTk8sGelYaHDhFcDN+Sn3Cv+3VSa4Byeo+IMCzpMgQ==",
+ "dev": true,
+ "dependencies": {
+ "ansi-styles": "^6.0.0",
+ "is-fullwidth-code-point": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/slice-ansi?sponsor=1"
+ }
+ },
+ "node_modules/slice-ansi/node_modules/ansi-styles": {
+ "version": "6.2.1",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz",
+ "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==",
+ "dev": true,
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+ }
+ },
+ "node_modules/source-map": {
+ "version": "0.5.7",
+ "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz",
+ "integrity": "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/source-map-js": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz",
+ "integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/streamsearch": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz",
+ "integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==",
+ "engines": {
+ "node": ">=10.0.0"
+ }
+ },
+ "node_modules/string-argv": {
+ "version": "0.3.2",
+ "resolved": "https://registry.npmjs.org/string-argv/-/string-argv-0.3.2.tgz",
+ "integrity": "sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.6.19"
+ }
+ },
+ "node_modules/string-width": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz",
+ "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==",
+ "dev": true,
+ "dependencies": {
+ "eastasianwidth": "^0.2.0",
+ "emoji-regex": "^9.2.2",
+ "strip-ansi": "^7.0.1"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/string-width/node_modules/ansi-regex": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz",
+ "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==",
+ "dev": true,
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-regex?sponsor=1"
+ }
+ },
+ "node_modules/string-width/node_modules/strip-ansi": {
+ "version": "7.1.0",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz",
+ "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==",
+ "dev": true,
+ "dependencies": {
+ "ansi-regex": "^6.0.1"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/strip-ansi?sponsor=1"
+ }
+ },
+ "node_modules/string.prototype.matchall": {
+ "version": "4.0.8",
+ "resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.8.tgz",
+ "integrity": "sha512-6zOCOcJ+RJAQshcTvXPHoxoQGONa3e/Lqx90wUA+wEzX78sg5Bo+1tQo4N0pohS0erG9qtCqJDjNCQBjeWVxyg==",
+ "dev": true,
+ "dependencies": {
+ "call-bind": "^1.0.2",
+ "define-properties": "^1.1.4",
+ "es-abstract": "^1.20.4",
+ "get-intrinsic": "^1.1.3",
+ "has-symbols": "^1.0.3",
+ "internal-slot": "^1.0.3",
+ "regexp.prototype.flags": "^1.4.3",
+ "side-channel": "^1.0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/string.prototype.trim": {
+ "version": "1.2.7",
+ "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.7.tgz",
+ "integrity": "sha512-p6TmeT1T3411M8Cgg9wBTMRtY2q9+PNy9EV1i2lIXUN/btt763oIfxwN3RR8VU6wHX8j/1CFy0L+YuThm6bgOg==",
+ "dev": true,
+ "dependencies": {
+ "call-bind": "^1.0.2",
+ "define-properties": "^1.1.4",
+ "es-abstract": "^1.20.4"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/string.prototype.trimend": {
+ "version": "1.0.6",
+ "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.6.tgz",
+ "integrity": "sha512-JySq+4mrPf9EsDBEDYMOb/lM7XQLulwg5R/m1r0PXEFqrV0qHvl58sdTilSXtKOflCsK2E8jxf+GKC0T07RWwQ==",
+ "dev": true,
+ "dependencies": {
+ "call-bind": "^1.0.2",
+ "define-properties": "^1.1.4",
+ "es-abstract": "^1.20.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/string.prototype.trimstart": {
+ "version": "1.0.6",
+ "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.6.tgz",
+ "integrity": "sha512-omqjMDaY92pbn5HOX7f9IccLA+U1tA9GvtU4JrodiXFfYB7jPzzHpRzpglLAjtUV6bB557zwClJezTqnAiYnQA==",
+ "dev": true,
+ "dependencies": {
+ "call-bind": "^1.0.2",
+ "define-properties": "^1.1.4",
+ "es-abstract": "^1.20.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/strip-ansi": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
+ "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
+ "dev": true,
+ "dependencies": {
+ "ansi-regex": "^5.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/strip-bom": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz",
+ "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==",
+ "dev": true,
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/strip-final-newline": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-3.0.0.tgz",
+ "integrity": "sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==",
+ "dev": true,
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/strip-json-comments": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz",
+ "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/styled-jsx": {
+ "version": "5.1.1",
+ "resolved": "https://registry.npmjs.org/styled-jsx/-/styled-jsx-5.1.1.tgz",
+ "integrity": "sha512-pW7uC1l4mBZ8ugbiZrcIsiIvVx1UmTfw7UkC3Um2tmfUq9Bhk8IiyEIPl6F8agHgjzku6j0xQEZbfA5uSgSaCw==",
+ "dependencies": {
+ "client-only": "0.0.1"
+ },
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "peerDependencies": {
+ "react": ">= 16.8.0 || 17.x.x || ^18.0.0-0"
+ },
+ "peerDependenciesMeta": {
+ "@babel/core": {
+ "optional": true
+ },
+ "babel-plugin-macros": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/stylis": {
+ "version": "4.2.0",
+ "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.2.0.tgz",
+ "integrity": "sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw=="
+ },
+ "node_modules/sucrase": {
+ "version": "3.34.0",
+ "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.34.0.tgz",
+ "integrity": "sha512-70/LQEZ07TEcxiU2dz51FKaE6hCTWC6vr7FOk3Gr0U60C3shtAN+H+BFr9XlYe5xqf3RA8nrc+VIwzCfnxuXJw==",
+ "dependencies": {
+ "@jridgewell/gen-mapping": "^0.3.2",
+ "commander": "^4.0.0",
+ "glob": "7.1.6",
+ "lines-and-columns": "^1.1.6",
+ "mz": "^2.7.0",
+ "pirates": "^4.0.1",
+ "ts-interface-checker": "^0.1.9"
+ },
+ "bin": {
+ "sucrase": "bin/sucrase",
+ "sucrase-node": "bin/sucrase-node"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/sucrase/node_modules/commander": {
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz",
+ "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==",
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/sucrase/node_modules/glob": {
+ "version": "7.1.6",
+ "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz",
+ "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==",
+ "dependencies": {
+ "fs.realpath": "^1.0.0",
+ "inflight": "^1.0.4",
+ "inherits": "2",
+ "minimatch": "^3.0.4",
+ "once": "^1.3.0",
+ "path-is-absolute": "^1.0.0"
+ },
+ "engines": {
+ "node": "*"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/supports-color": {
+ "version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
+ "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
+ "dev": true,
+ "dependencies": {
+ "has-flag": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/supports-preserve-symlinks-flag": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz",
+ "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/swr": {
+ "version": "2.2.1",
+ "resolved": "https://registry.npmjs.org/swr/-/swr-2.2.1.tgz",
+ "integrity": "sha512-KJVA7dGtOBeZ+2sycEuzUfVIP5lZ/cd0xjevv85n2YG0x1uHJQicjAtahVZL6xG3+TjqhbBqimwYzVo3saeVXQ==",
+ "dependencies": {
+ "client-only": "^0.0.1",
+ "use-sync-external-store": "^1.2.0"
+ },
+ "peerDependencies": {
+ "react": "^16.11.0 || ^17.0.0 || ^18.0.0"
+ }
+ },
+ "node_modules/synckit": {
+ "version": "0.8.5",
+ "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.8.5.tgz",
+ "integrity": "sha512-L1dapNV6vu2s/4Sputv8xGsCdAVlb5nRDMFU/E27D44l5U6cw1g0dGd45uLc+OXjNMmF4ntiMdCimzcjFKQI8Q==",
+ "dev": true,
+ "dependencies": {
+ "@pkgr/utils": "^2.3.1",
+ "tslib": "^2.5.0"
+ },
+ "engines": {
+ "node": "^14.18.0 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/unts"
+ }
+ },
+ "node_modules/tailwindcss": {
+ "version": "3.3.3",
+ "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.3.3.tgz",
+ "integrity": "sha512-A0KgSkef7eE4Mf+nKJ83i75TMyq8HqY3qmFIJSWy8bNt0v1lG7jUcpGpoTFxAwYcWOphcTBLPPJg+bDfhDf52w==",
+ "dependencies": {
+ "@alloc/quick-lru": "^5.2.0",
+ "arg": "^5.0.2",
+ "chokidar": "^3.5.3",
+ "didyoumean": "^1.2.2",
+ "dlv": "^1.1.3",
+ "fast-glob": "^3.2.12",
+ "glob-parent": "^6.0.2",
+ "is-glob": "^4.0.3",
+ "jiti": "^1.18.2",
+ "lilconfig": "^2.1.0",
+ "micromatch": "^4.0.5",
+ "normalize-path": "^3.0.0",
+ "object-hash": "^3.0.0",
+ "picocolors": "^1.0.0",
+ "postcss": "^8.4.23",
+ "postcss-import": "^15.1.0",
+ "postcss-js": "^4.0.1",
+ "postcss-load-config": "^4.0.1",
+ "postcss-nested": "^6.0.1",
+ "postcss-selector-parser": "^6.0.11",
+ "resolve": "^1.22.2",
+ "sucrase": "^3.32.0"
+ },
+ "bin": {
+ "tailwind": "lib/cli.js",
+ "tailwindcss": "lib/cli.js"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
+ "node_modules/tapable": {
+ "version": "2.2.1",
+ "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz",
+ "integrity": "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==",
+ "dev": true,
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/text-table": {
+ "version": "0.2.0",
+ "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz",
+ "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==",
+ "dev": true
+ },
+ "node_modules/thenify": {
+ "version": "3.3.1",
+ "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz",
+ "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==",
+ "dependencies": {
+ "any-promise": "^1.0.0"
+ }
+ },
+ "node_modules/thenify-all": {
+ "version": "1.6.0",
+ "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz",
+ "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==",
+ "dependencies": {
+ "thenify": ">= 3.1.0 < 4"
+ },
+ "engines": {
+ "node": ">=0.8"
+ }
+ },
+ "node_modules/through": {
+ "version": "2.3.8",
+ "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz",
+ "integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==",
+ "dev": true
+ },
+ "node_modules/titleize": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/titleize/-/titleize-3.0.0.tgz",
+ "integrity": "sha512-KxVu8EYHDPBdUYdKZdKtU2aj2XfEx9AfjXxE/Aj0vT06w2icA09Vus1rh6eSu1y01akYg6BjIK/hxyLJINoMLQ==",
+ "dev": true,
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/to-fast-properties": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz",
+ "integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==",
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/to-regex-range": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
+ "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
+ "dependencies": {
+ "is-number": "^7.0.0"
+ },
+ "engines": {
+ "node": ">=8.0"
+ }
+ },
+ "node_modules/ts-interface-checker": {
+ "version": "0.1.13",
+ "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz",
+ "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA=="
+ },
+ "node_modules/tsconfig-paths": {
+ "version": "3.14.2",
+ "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.14.2.tgz",
+ "integrity": "sha512-o/9iXgCYc5L/JxCHPe3Hvh8Q/2xm5Z+p18PESBU6Ff33695QnCHBEjcytY2q19ua7Mbl/DavtBOLq+oG0RCL+g==",
+ "dev": true,
+ "dependencies": {
+ "@types/json5": "^0.0.29",
+ "json5": "^1.0.2",
+ "minimist": "^1.2.6",
+ "strip-bom": "^3.0.0"
+ }
+ },
+ "node_modules/tslib": {
+ "version": "2.6.1",
+ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.1.tgz",
+ "integrity": "sha512-t0hLfiEKfMUoqhG+U1oid7Pva4bbDPHYfJNiB7BiIjRkj1pyC++4N3huJfqY6aRH6VTB0rvtzQwjM4K6qpfOig=="
+ },
+ "node_modules/tsutils": {
+ "version": "3.21.0",
+ "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-3.21.0.tgz",
+ "integrity": "sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==",
+ "dev": true,
+ "dependencies": {
+ "tslib": "^1.8.1"
+ },
+ "engines": {
+ "node": ">= 6"
+ },
+ "peerDependencies": {
+ "typescript": ">=2.8.0 || >= 3.2.0-dev || >= 3.3.0-dev || >= 3.4.0-dev || >= 3.5.0-dev || >= 3.6.0-dev || >= 3.6.0-beta || >= 3.7.0-dev || >= 3.7.0-beta"
+ }
+ },
+ "node_modules/tsutils/node_modules/tslib": {
+ "version": "1.14.1",
+ "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz",
+ "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==",
+ "dev": true
+ },
+ "node_modules/type-check": {
+ "version": "0.4.0",
+ "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",
+ "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==",
+ "dev": true,
+ "dependencies": {
+ "prelude-ls": "^1.2.1"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/type-fest": {
+ "version": "0.20.2",
+ "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz",
+ "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==",
+ "dev": true,
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/typed-array-buffer": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.0.tgz",
+ "integrity": "sha512-Y8KTSIglk9OZEr8zywiIHG/kmQ7KWyjseXs1CbSo8vC42w7hg2HgYTxSWwP0+is7bWDc1H+Fo026CpHFwm8tkw==",
+ "dev": true,
+ "dependencies": {
+ "call-bind": "^1.0.2",
+ "get-intrinsic": "^1.2.1",
+ "is-typed-array": "^1.1.10"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/typed-array-byte-length": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.0.tgz",
+ "integrity": "sha512-Or/+kvLxNpeQ9DtSydonMxCx+9ZXOswtwJn17SNLvhptaXYDJvkFFP5zbfU/uLmvnBJlI4yrnXRxpdWH/M5tNA==",
+ "dev": true,
+ "dependencies": {
+ "call-bind": "^1.0.2",
+ "for-each": "^0.3.3",
+ "has-proto": "^1.0.1",
+ "is-typed-array": "^1.1.10"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/typed-array-byte-offset": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.0.tgz",
+ "integrity": "sha512-RD97prjEt9EL8YgAgpOkf3O4IF9lhJFr9g0htQkm0rchFp/Vx7LW5Q8fSXXub7BXAODyUQohRMyOc3faCPd0hg==",
+ "dev": true,
+ "dependencies": {
+ "available-typed-arrays": "^1.0.5",
+ "call-bind": "^1.0.2",
+ "for-each": "^0.3.3",
+ "has-proto": "^1.0.1",
+ "is-typed-array": "^1.1.10"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/typed-array-length": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.4.tgz",
+ "integrity": "sha512-KjZypGq+I/H7HI5HlOoGHkWUUGq+Q0TPhQurLbyrVrvnKTBgzLhIJ7j6J/XTQOi0d1RjyZ0wdas8bKs2p0x3Ng==",
+ "dev": true,
+ "dependencies": {
+ "call-bind": "^1.0.2",
+ "for-each": "^0.3.3",
+ "is-typed-array": "^1.1.9"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/typescript": {
+ "version": "5.1.6",
+ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.1.6.tgz",
+ "integrity": "sha512-zaWCozRZ6DLEWAWFrVDz1H6FVXzUSfTy5FUMWsQlU8Ym5JP9eO4xkTIROFCQvhQf61z6O/G6ugw3SgAnvvm+HA==",
+ "dev": true,
+ "bin": {
+ "tsc": "bin/tsc",
+ "tsserver": "bin/tsserver"
+ },
+ "engines": {
+ "node": ">=14.17"
+ }
+ },
+ "node_modules/unbox-primitive": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.2.tgz",
+ "integrity": "sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw==",
+ "dev": true,
+ "dependencies": {
+ "call-bind": "^1.0.2",
+ "has-bigints": "^1.0.2",
+ "has-symbols": "^1.0.3",
+ "which-boxed-primitive": "^1.0.2"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/untildify": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/untildify/-/untildify-4.0.0.tgz",
+ "integrity": "sha512-KK8xQ1mkzZeg9inewmFVDNkg3l5LUhoq9kN6iWYB/CC9YMG8HA+c1Q8HwDe6dEX7kErrEVNVBO3fWsVq5iDgtw==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/update-browserslist-db": {
+ "version": "1.0.11",
+ "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.11.tgz",
+ "integrity": "sha512-dCwEFf0/oT85M1fHBg4F0jtLwJrutGoHSQXCh7u4o2t1drG+c0a9Flnqww6XUKSfQMPpJBRjU8d4RXB09qtvaA==",
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/browserslist"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/browserslist"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "dependencies": {
+ "escalade": "^3.1.1",
+ "picocolors": "^1.0.0"
+ },
+ "bin": {
+ "update-browserslist-db": "cli.js"
+ },
+ "peerDependencies": {
+ "browserslist": ">= 4.21.0"
+ }
+ },
+ "node_modules/uri-js": {
+ "version": "4.4.1",
+ "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz",
+ "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==",
+ "dev": true,
+ "dependencies": {
+ "punycode": "^2.1.0"
+ }
+ },
+ "node_modules/use-sync-external-store": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz",
+ "integrity": "sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==",
+ "peerDependencies": {
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0"
+ }
+ },
+ "node_modules/util-deprecate": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
+ "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="
+ },
+ "node_modules/warning": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/warning/-/warning-4.0.3.tgz",
+ "integrity": "sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w==",
+ "dependencies": {
+ "loose-envify": "^1.0.0"
+ }
+ },
+ "node_modules/watchpack": {
+ "version": "2.4.0",
+ "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.0.tgz",
+ "integrity": "sha512-Lcvm7MGST/4fup+ifyKi2hjyIAwcdI4HRgtvTpIUxBRhB+RFtUh8XtDOxUfctVCnhVi+QQj49i91OyvzkJl6cg==",
+ "dependencies": {
+ "glob-to-regexp": "^0.4.1",
+ "graceful-fs": "^4.1.2"
+ },
+ "engines": {
+ "node": ">=10.13.0"
+ }
+ },
+ "node_modules/which": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
+ "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
+ "dev": true,
+ "dependencies": {
+ "isexe": "^2.0.0"
+ },
+ "bin": {
+ "node-which": "bin/node-which"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/which-boxed-primitive": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz",
+ "integrity": "sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==",
+ "dev": true,
+ "dependencies": {
+ "is-bigint": "^1.0.1",
+ "is-boolean-object": "^1.1.0",
+ "is-number-object": "^1.0.4",
+ "is-string": "^1.0.5",
+ "is-symbol": "^1.0.3"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/which-typed-array": {
+ "version": "1.1.11",
+ "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.11.tgz",
+ "integrity": "sha512-qe9UWWpkeG5yzZ0tNYxDmd7vo58HDBc39mZ0xWWpolAGADdFOzkfamWLDxkOWcvHQKVmdTyQdLD4NOfjLWTKew==",
+ "dev": true,
+ "dependencies": {
+ "available-typed-arrays": "^1.0.5",
+ "call-bind": "^1.0.2",
+ "for-each": "^0.3.3",
+ "gopd": "^1.0.1",
+ "has-tostringtag": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/wrap-ansi": {
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
+ "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
+ "dev": true,
+ "dependencies": {
+ "ansi-styles": "^4.0.0",
+ "string-width": "^4.1.0",
+ "strip-ansi": "^6.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/wrap-ansi?sponsor=1"
+ }
+ },
+ "node_modules/wrap-ansi/node_modules/emoji-regex": {
+ "version": "8.0.0",
+ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
+ "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
+ "dev": true
+ },
+ "node_modules/wrap-ansi/node_modules/is-fullwidth-code-point": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
+ "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/wrap-ansi/node_modules/string-width": {
+ "version": "4.2.3",
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
+ "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
+ "dev": true,
+ "dependencies": {
+ "emoji-regex": "^8.0.0",
+ "is-fullwidth-code-point": "^3.0.0",
+ "strip-ansi": "^6.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/wrappy": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
+ "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="
+ },
+ "node_modules/yallist": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
+ "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
+ "dev": true
+ },
+ "node_modules/yaml": {
+ "version": "2.3.1",
+ "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.3.1.tgz",
+ "integrity": "sha512-2eHWfjaoXgTBC2jNM1LRef62VQa0umtvRiDSk6HSzW7RvS5YtkabJrwYLLEKWBc8a5U2PTSCs+dJjUTJdlHsWQ==",
+ "engines": {
+ "node": ">= 14"
+ }
+ },
+ "node_modules/yocto-queue": {
+ "version": "0.1.0",
+ "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
+ "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==",
+ "dev": true,
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/zod": {
+ "version": "3.22.2",
+ "resolved": "https://registry.npmjs.org/zod/-/zod-3.22.2.tgz",
+ "integrity": "sha512-wvWkphh5WQsJbVk1tbx1l1Ly4yg+XecD+Mq280uBGt9wa5BKSWf4Mhp6GmrkPixhMxmabYY7RbzlwVP32pbGCg==",
+ "funding": {
+ "url": "https://github.com/sponsors/colinhacks"
+ }
+ }
+ }
+}
diff --git a/application/next-frontend/package.json b/application/next-frontend/package.json
new file mode 100644
index 00000000..10f60498
--- /dev/null
+++ b/application/next-frontend/package.json
@@ -0,0 +1,59 @@
+{
+ "name": "next-frontend",
+ "version": "0.1.0",
+ "private": true,
+ "engines": {
+ "node": ">=20.x"
+ },
+ "scripts": {
+ "dev": "next dev -p 4000",
+ "build": "next build",
+ "start": "next start -p $PORT",
+ "lint": "next lint --max-warnings 0",
+ "fix": "next lint --fix",
+ "typecheck": "tsc --noEmit --incremental false --pretty",
+ "prepare": "cd ../../ && husky install ./application/next-frontend/.husky"
+ },
+ "lint-staged": {
+ "*.{js,jsx,ts,tsx}": [
+ "prettier --write",
+ "eslint --fix --max-warnings 0"
+ ]
+ },
+ "dependencies": {
+ "@emotion/react": "^11.11.1",
+ "@emotion/styled": "^11.11.0",
+ "@hookform/resolvers": "^3.2.0",
+ "@mui/material": "^5.14.5",
+ "autoprefixer": "10.4.14",
+ "jwt-decode": "^3.1.2",
+ "next": "13.4.13",
+ "ordinal": "^1.0.3",
+ "postcss": "8.4.27",
+ "react": "18.2.0",
+ "react-datepicker": "^4.16.0",
+ "react-dom": "18.2.0",
+ "react-hook-form": "^7.45.4",
+ "react-hot-toast": "^2.4.1",
+ "swr": "^2.2.1",
+ "tailwindcss": "3.3.3",
+ "zod": "^3.22.2"
+ },
+ "devDependencies": {
+ "@types/node": "20.4.9",
+ "@types/react": "18.2.20",
+ "@types/react-datepicker": "^4.15.0",
+ "@types/react-dom": "18.2.7",
+ "@typescript-eslint/eslint-plugin": "^5.33.1",
+ "eslint": "8.46.0",
+ "eslint-config-next": "13.4.13",
+ "eslint-config-prettier": "9.0.0",
+ "eslint-plugin-prettier": "5.0.0",
+ "eslint-plugin-tailwindcss": "^3.13.0",
+ "husky": "^8.0.1",
+ "lint-staged": "^13.0.3",
+ "prettier": "3.0.1",
+ "prettier-plugin-tailwindcss": "^0.4.1",
+ "typescript": "5.1.6"
+ }
+}
diff --git a/application/next-frontend/pages/_app.tsx b/application/next-frontend/pages/_app.tsx
new file mode 100644
index 00000000..9a06f395
--- /dev/null
+++ b/application/next-frontend/pages/_app.tsx
@@ -0,0 +1,16 @@
+import '@/styles/globals.css'
+import '@/styles/reset.css'
+import '@/styles/fonts.css'
+import '@/styles/tailwindComposables.css'
+import type { AppProps } from 'next/app'
+import { ModalProvider } from 'Shared/context/ModalContext'
+import { Toaster } from 'react-hot-toast'
+
+export default function App({ Component, pageProps }: AppProps) {
+ return (
+
+
+
+
+ )
+}
diff --git a/application/next-frontend/pages/_document.tsx b/application/next-frontend/pages/_document.tsx
new file mode 100644
index 00000000..138cd8d1
--- /dev/null
+++ b/application/next-frontend/pages/_document.tsx
@@ -0,0 +1,13 @@
+import { Html, Head, Main, NextScript } from 'next/document'
+
+export default function Document() {
+ return (
+
+
+
+
+
+
+
+ )
+}
diff --git a/application/next-frontend/pages/admin/index.tsx b/application/next-frontend/pages/admin/index.tsx
new file mode 100644
index 00000000..938c1a54
--- /dev/null
+++ b/application/next-frontend/pages/admin/index.tsx
@@ -0,0 +1,25 @@
+import { GetServerSideProps } from 'next'
+import jwtDecode from 'jwt-decode'
+import type { JwtToken } from '@/Admin/types/User'
+import { AdminMainPage } from '@/Admin/scenarios/AdminMainPage'
+import { Navbar } from '@/Admin/scenarios/Navbar'
+
+export const getServerSideProps: GetServerSideProps = async ({ req }) => {
+ const jwt = req.cookies['access_token_cookie']
+ if (!jwt || !jwtDecode(jwt)) return { redirect: { destination: '/login', permanent: false }, props: {} }
+
+ return {
+ props: {},
+ }
+}
+
+const AdminHome = () => {
+ return (
+
+ )
+}
+
+export default AdminHome
diff --git a/application/next-frontend/pages/admin/onboarding/index.tsx b/application/next-frontend/pages/admin/onboarding/index.tsx
new file mode 100644
index 00000000..8a59bada
--- /dev/null
+++ b/application/next-frontend/pages/admin/onboarding/index.tsx
@@ -0,0 +1,21 @@
+import { GetServerSideProps } from 'next'
+import { Onboarding as OnboardAdmin } from '@/Admin/scenarios/Onboarding'
+
+export const getServerSideProps: GetServerSideProps = async ({ req }) => {
+ const jwt = req.cookies['access_token_cookie']
+ if (!jwt) return { redirect: { destination: '/login', permanent: false }, props: {} }
+
+ return {
+ props: {},
+ }
+}
+
+const Onboarding = () => {
+ return (
+
+
+
+ )
+}
+
+export default Onboarding
diff --git a/application/next-frontend/pages/api/hello.ts b/application/next-frontend/pages/api/hello.ts
new file mode 100644
index 00000000..d3979fec
--- /dev/null
+++ b/application/next-frontend/pages/api/hello.ts
@@ -0,0 +1,10 @@
+// Next.js API route support: https://nextjs.org/docs/api-routes/introduction
+import type { NextApiRequest, NextApiResponse } from 'next'
+
+type Data = {
+ name: string
+}
+
+export default function handler(req: NextApiRequest, res: NextApiResponse) {
+ res.status(200).json({ name: 'John Doe' })
+}
diff --git a/application/next-frontend/pages/index.tsx b/application/next-frontend/pages/index.tsx
new file mode 100644
index 00000000..4ffa949a
--- /dev/null
+++ b/application/next-frontend/pages/index.tsx
@@ -0,0 +1,11 @@
+import { Landing } from '@/Landing'
+import { Header } from '@/Landing/scenarios/Header'
+
+export default function Home() {
+ return (
+
+
+
+
+ )
+}
diff --git a/application/next-frontend/pages/login/callback.tsx b/application/next-frontend/pages/login/callback.tsx
new file mode 100644
index 00000000..13cc6b28
--- /dev/null
+++ b/application/next-frontend/pages/login/callback.tsx
@@ -0,0 +1,37 @@
+import { serversideApiUri } from '@/api/endpoints'
+import { GetServerSideProps } from 'next'
+import Link from 'next/link'
+
+export const getServerSideProps: GetServerSideProps = async ({ query, res }) => {
+ const code = query.code
+ if (!query.code || typeof query.code !== 'string')
+ return { props: { errorMsg: 'Could not get login information from Slack.' } }
+
+ const response = await fetch(`${serversideApiUri}/auth/login/callback?code=${code}`).catch((err) => {
+ console.error(err)
+ })
+
+ if (!response?.ok) {
+ return { props: { errorMsg: 'Something went wrong when logging inn through Slack.' } }
+ }
+
+ const cookie = response.headers.get('set-cookie')
+ if (!cookie) return { props: { errorMsg: 'Login credentials were not set correctly.' } }
+
+ res.setHeader('set-cookie', cookie)
+ return { redirect: { destination: '/admin', permanent: false }, props: {} }
+}
+
+const LoginCallback = ({ errorMsg }: { errorMsg?: string }) => {
+ return (
+
+
Failed to log in!
+ {`${errorMsg ?? ''} Try logging in again.`}
+
+ Go to login
+
+
+ )
+}
+
+export default LoginCallback
diff --git a/application/next-frontend/pages/login/index.tsx b/application/next-frontend/pages/login/index.tsx
new file mode 100644
index 00000000..98ed1e53
--- /dev/null
+++ b/application/next-frontend/pages/login/index.tsx
@@ -0,0 +1,13 @@
+import { Header } from '@/Landing/scenarios/Header'
+import { LoginPage } from 'Landing/scenarios/LoginPage'
+
+const Login = () => {
+ return (
+
+
+
+
+ )
+}
+
+export default Login
diff --git a/application/next-frontend/pages/slack/callback.tsx b/application/next-frontend/pages/slack/callback.tsx
new file mode 100644
index 00000000..0eeaea66
--- /dev/null
+++ b/application/next-frontend/pages/slack/callback.tsx
@@ -0,0 +1,44 @@
+import { serversideApiUri } from '@/api/endpoints'
+import { GetServerSideProps } from 'next'
+import { SlackCallbackPageSuccess, SlackCallbackPageError } from 'Landing/scenarios/SlackCallbackPage'
+import { Header } from '@/Landing/scenarios/Header'
+
+export const getServerSideProps: GetServerSideProps = async ({ query }) => {
+ const code = query.code
+ if (!query.code || typeof query.code !== 'string') return { props: { success: false } }
+
+ const res = await fetch(`${serversideApiUri}/slack/callback`, {
+ method: 'POST',
+ body: JSON.stringify({ code: code }),
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ }).catch((err) => {
+ console.error(err)
+ })
+
+ const success = res?.ok
+ let message: string | undefined
+ try {
+ message = (await res?.json()).message
+ } catch (err) {
+ console.error(err)
+ }
+
+ return { props: { success, message: message ?? null } }
+}
+
+const SlackInstallCallback = ({ success, message }: { success?: boolean; message: string | null }) => {
+ return (
+
+
+ {success ? (
+
+ ) : (
+
+ )}
+
+ )
+}
+
+export default SlackInstallCallback
diff --git a/application/next-frontend/postcss.config.js b/application/next-frontend/postcss.config.js
new file mode 100644
index 00000000..33ad091d
--- /dev/null
+++ b/application/next-frontend/postcss.config.js
@@ -0,0 +1,6 @@
+module.exports = {
+ plugins: {
+ tailwindcss: {},
+ autoprefixer: {},
+ },
+}
diff --git a/application/next-frontend/public/favicon.ico b/application/next-frontend/public/favicon.ico
new file mode 100644
index 00000000..718d6fea
Binary files /dev/null and b/application/next-frontend/public/favicon.ico differ
diff --git a/application/next-frontend/public/fonts/QueensCondensedTrial-Medium.ttf b/application/next-frontend/public/fonts/QueensCondensedTrial-Medium.ttf
new file mode 100644
index 00000000..3283e033
Binary files /dev/null and b/application/next-frontend/public/fonts/QueensCondensedTrial-Medium.ttf differ
diff --git a/application/next-frontend/public/fonts/QueensCondensedTrial-Regular.ttf b/application/next-frontend/public/fonts/QueensCondensedTrial-Regular.ttf
new file mode 100644
index 00000000..de344ccc
Binary files /dev/null and b/application/next-frontend/public/fonts/QueensCondensedTrial-Regular.ttf differ
diff --git a/application/next-frontend/src/Admin/assets/AdminSidebarBackground.svg b/application/next-frontend/src/Admin/assets/AdminSidebarBackground.svg
new file mode 100644
index 00000000..59edfeb1
--- /dev/null
+++ b/application/next-frontend/src/Admin/assets/AdminSidebarBackground.svg
@@ -0,0 +1,3 @@
+
+
+
diff --git a/application/next-frontend/src/Admin/assets/Line.svg b/application/next-frontend/src/Admin/assets/Line.svg
new file mode 100644
index 00000000..dd75512b
--- /dev/null
+++ b/application/next-frontend/src/Admin/assets/Line.svg
@@ -0,0 +1,3 @@
+
+
+
diff --git a/application/next-frontend/src/Admin/assets/MascotChilling.svg b/application/next-frontend/src/Admin/assets/MascotChilling.svg
new file mode 100644
index 00000000..4fcd7402
--- /dev/null
+++ b/application/next-frontend/src/Admin/assets/MascotChilling.svg
@@ -0,0 +1,10 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/application/next-frontend/src/Admin/assets/MascotHappy.svg b/application/next-frontend/src/Admin/assets/MascotHappy.svg
new file mode 100644
index 00000000..fea647ec
--- /dev/null
+++ b/application/next-frontend/src/Admin/assets/MascotHappy.svg
@@ -0,0 +1,10 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/application/next-frontend/src/Admin/assets/MascotWithFourArms.svg b/application/next-frontend/src/Admin/assets/MascotWithFourArms.svg
new file mode 100644
index 00000000..c679969d
--- /dev/null
+++ b/application/next-frontend/src/Admin/assets/MascotWithFourArms.svg
@@ -0,0 +1,10 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/application/next-frontend/src/Admin/assets/Pencil.svg b/application/next-frontend/src/Admin/assets/Pencil.svg
new file mode 100644
index 00000000..cfb152a4
--- /dev/null
+++ b/application/next-frontend/src/Admin/assets/Pencil.svg
@@ -0,0 +1,3 @@
+
+
+
diff --git a/application/next-frontend/src/Admin/assets/PizzaBotLogo.svg b/application/next-frontend/src/Admin/assets/PizzaBotLogo.svg
new file mode 100644
index 00000000..d1a76690
--- /dev/null
+++ b/application/next-frontend/src/Admin/assets/PizzaBotLogo.svg
@@ -0,0 +1,3 @@
+
+
+
diff --git a/application/next-frontend/src/Admin/assets/Triangle.svg b/application/next-frontend/src/Admin/assets/Triangle.svg
new file mode 100644
index 00000000..1e9dcaf9
--- /dev/null
+++ b/application/next-frontend/src/Admin/assets/Triangle.svg
@@ -0,0 +1,3 @@
+
+
+
diff --git a/application/next-frontend/src/Admin/assets/TriangleGreen.svg b/application/next-frontend/src/Admin/assets/TriangleGreen.svg
new file mode 100644
index 00000000..6cd38899
--- /dev/null
+++ b/application/next-frontend/src/Admin/assets/TriangleGreen.svg
@@ -0,0 +1,3 @@
+
+
+
diff --git a/application/next-frontend/src/Admin/assets/TriangleGrey.svg b/application/next-frontend/src/Admin/assets/TriangleGrey.svg
new file mode 100644
index 00000000..475fd3c7
--- /dev/null
+++ b/application/next-frontend/src/Admin/assets/TriangleGrey.svg
@@ -0,0 +1,3 @@
+
+
+
diff --git a/application/next-frontend/src/Admin/assets/pizza/PizzaEaten.svg b/application/next-frontend/src/Admin/assets/pizza/PizzaEaten.svg
new file mode 100644
index 00000000..4624e14a
--- /dev/null
+++ b/application/next-frontend/src/Admin/assets/pizza/PizzaEaten.svg
@@ -0,0 +1,12 @@
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/application/next-frontend/src/Admin/assets/pizza/PizzaRound.svg b/application/next-frontend/src/Admin/assets/pizza/PizzaRound.svg
new file mode 100644
index 00000000..4371add1
--- /dev/null
+++ b/application/next-frontend/src/Admin/assets/pizza/PizzaRound.svg
@@ -0,0 +1,20 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/application/next-frontend/src/Admin/assets/pizza/PizzaSlice.svg b/application/next-frontend/src/Admin/assets/pizza/PizzaSlice.svg
new file mode 100644
index 00000000..3e727195
--- /dev/null
+++ b/application/next-frontend/src/Admin/assets/pizza/PizzaSlice.svg
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
+
diff --git a/application/next-frontend/src/Admin/components/AdminSidebar.tsx b/application/next-frontend/src/Admin/components/AdminSidebar.tsx
new file mode 100644
index 00000000..aa19160f
--- /dev/null
+++ b/application/next-frontend/src/Admin/components/AdminSidebar.tsx
@@ -0,0 +1,29 @@
+import Image from 'next/image'
+import Mascot from 'Admin/assets/MascotChilling.svg'
+import TextBackground from 'Admin/assets/AdminSidebarBackground.svg'
+
+const AdminSidebar = () => {
+ return (
+
+
+
+
+
+ Pizza control in progress..
+
+
+
+
+
Welcome to the Admin Page!
+
+ As the administrator, you can edit who is invited, where you are going and when the events take
+ place.
+
+
+
+
+
+ )
+}
+
+export { AdminSidebar }
diff --git a/application/next-frontend/src/Admin/components/Button.tsx b/application/next-frontend/src/Admin/components/Button.tsx
new file mode 100644
index 00000000..8e671e16
--- /dev/null
+++ b/application/next-frontend/src/Admin/components/Button.tsx
@@ -0,0 +1,28 @@
+type ButtonProps = {
+ onClick?: () => void
+ className?: string
+ text: string
+ buttonStyle: 'primary' | 'secondary'
+ type?: 'submit' | 'reset' | 'button'
+ value?: string
+}
+
+const Button = ({ onClick, className, text, buttonStyle, type, value }: ButtonProps) => {
+ const primaryStyle = 'bg-white font-black hover:bg-green-secondary border-b-8'
+ const secondaryStyle = 'bg-green-light font-semibold hover:bg-yellow'
+
+ return (
+
+ {text}
+
+ )
+}
+
+export default Button
diff --git a/application/next-frontend/src/Admin/components/CardComponentWrapper.tsx b/application/next-frontend/src/Admin/components/CardComponentWrapper.tsx
new file mode 100644
index 00000000..e72fd77b
--- /dev/null
+++ b/application/next-frontend/src/Admin/components/CardComponentWrapper.tsx
@@ -0,0 +1,43 @@
+import React from 'react'
+import { useHover } from '@/Shared/context/HoverContext'
+
+const CardComponentWrapper = ({
+ title,
+ children,
+ addIcon,
+ onClickCard,
+ className,
+}: {
+ title: string
+ children?: React.ReactNode
+ addIcon?: boolean
+ onClickCard?: () => void
+ className?: string
+}) => {
+ const { setHovered, isHovered } = useHover()
+ return (
+ setHovered(true)}
+ onMouseLeave={() => setHovered(false)}
+ onClick={onClickCard}
+ >
+
+
{title}
+
+
+
{children}
+ {addIcon && (
+
+
+ Add +
+
+
+ )}
+
+ )
+}
+
+export { CardComponentWrapper }
diff --git a/application/next-frontend/src/Admin/scenarios/AdminMainPage.tsx b/application/next-frontend/src/Admin/scenarios/AdminMainPage.tsx
new file mode 100644
index 00000000..585a8cf3
--- /dev/null
+++ b/application/next-frontend/src/Admin/scenarios/AdminMainPage.tsx
@@ -0,0 +1,39 @@
+import { Events } from 'Admin/scenarios/Events'
+import { Restaurants } from 'Admin/scenarios/Restaurants'
+import { SlackChannel } from 'Admin/scenarios/SlackChannel'
+import Image from 'next/image'
+import MascotHappy from 'Admin/assets/MascotHappy.svg'
+
+import { HoverProvider } from 'Shared/context/HoverContext'
+
+const AdminMainPage = () => {
+ return (
+
+
+
+
Pizza Admin
+
+
+
+
+
+
+
+
*]:mb-14`}>
+
+
+
+
+
+
+
+ )
+}
+
+export { AdminMainPage }
diff --git a/application/next-frontend/src/Admin/scenarios/Events/components/CreatePizzaEventCard.tsx b/application/next-frontend/src/Admin/scenarios/Events/components/CreatePizzaEventCard.tsx
new file mode 100644
index 00000000..ad243e4c
--- /dev/null
+++ b/application/next-frontend/src/Admin/scenarios/Events/components/CreatePizzaEventCard.tsx
@@ -0,0 +1,203 @@
+import { useModal } from 'Shared/context/ModalContext'
+import { ApiEventPost, useEvents } from '@/api/useEvents'
+import { useRestaurants } from '@/api/useRestaurants'
+
+import { useForm, FormProvider, Controller } from 'react-hook-form'
+import { zodResolver } from '@hookform/resolvers/zod'
+import { z } from 'zod'
+import Button from 'Admin/components/Button'
+
+const formatTimeInput = (inp: string) => {
+ // Remove non digit chars
+ const formatted = inp.replace(/[^\d]/g, '')
+
+ return formatted.slice(0, 2)
+}
+
+const formatTimeInpOnBlur = (inp: string, min: number, max: number) => {
+ // Remove non digit chars
+ let formatted = inp.replace(/[^\d]/g, '')
+
+ if (formatted == '') formatted = '00'
+ let timeNumber = parseInt(formatted)
+
+ if (timeNumber < min) timeNumber = min
+ else if (timeNumber > max) timeNumber = max
+
+ if (timeNumber < 10) return `0${timeNumber}`
+ return `${timeNumber}`
+}
+
+const validationSchema = z.object({
+ eventDate: z.number(),
+ eventMonth: z.number(),
+ eventHour: z.string(),
+ eventMinute: z.string(),
+ participants: z.nullable(z.number().int()),
+})
+
+type FormData = {
+ eventDate: number
+ eventMonth: number
+ eventHour: string
+ eventMinute: string
+ participants?: number
+}
+
+const CreatePizzaEventCard = ({ selectedDate }: { selectedDate: Date }) => {
+ const months = Array.from({ length: 12 }, (_, i) => {
+ return new Date(0, i).toLocaleString('en-UK', { month: 'long' })
+ })
+
+ const date = selectedDate.getDate()
+ const currentMonth = selectedDate.getMonth()
+ const currentYear = selectedDate.getFullYear()
+
+ const methods = useForm({
+ resolver: zodResolver(validationSchema),
+ defaultValues: {
+ eventDate: selectedDate.getDate(),
+ eventMonth: selectedDate.getMonth(),
+ eventHour: '18',
+ eventMinute: '00',
+ participants: 5,
+ },
+ })
+ const { handleSubmit, control } = methods
+ const { closeModal } = useModal()
+ const { addEvent } = useEvents()
+ const { data: restaurantData } = useRestaurants()
+
+ const findRestaurant = () => {
+ if (!restaurantData || !restaurantData.length) {
+ return
+ }
+
+ const randomIndex = Math.floor(Math.random() * restaurantData.length)
+ return restaurantData[randomIndex]
+ }
+
+ const onSubmit = (event: FormData) => {
+ const restaurant = findRestaurant()
+ const newEventDate = new Date(
+ currentYear,
+ event.eventMonth,
+ event.eventDate,
+ parseInt(event.eventHour),
+ parseInt(event.eventMinute),
+ )
+
+ if (restaurant) {
+ const event: ApiEventPost = {
+ time: newEventDate.toISOString(),
+ restaurant_id: restaurant.id,
+ people_per_event: 5,
+ }
+ addEvent(event)
+ closeModal()
+ }
+ }
+
+ return (
+
+
+ Create Pizza Event?
+ closeModal()}
+ >
+ ×
+
+
+
+
+
+
+
+ )
+}
+
+export { CreatePizzaEventCard }
diff --git a/application/next-frontend/src/Admin/scenarios/Events/components/DeletePizzaEventCard.tsx b/application/next-frontend/src/Admin/scenarios/Events/components/DeletePizzaEventCard.tsx
new file mode 100644
index 00000000..ed31160a
--- /dev/null
+++ b/application/next-frontend/src/Admin/scenarios/Events/components/DeletePizzaEventCard.tsx
@@ -0,0 +1,65 @@
+import Button from '@/Admin/components/Button'
+import { useModal } from 'Shared/context/ModalContext'
+import { useEvents, ApiEvent } from '@/api/useEvents'
+import ordinal from 'ordinal'
+
+// Should pass ApiEvent instead and use those values
+
+type Props = {
+ event: ApiEvent
+}
+
+const DeletePizzaEventCard = ({ event }: Props) => {
+ const [eventDate, eventId] = [new Date(event.time), event.id]
+ const months = Array.from({ length: 12 }, (_, i) => {
+ return new Date(0, i).toLocaleString('en-UK', { month: 'long' })
+ })
+ const [date, month, time] = [
+ eventDate.getDate(),
+ eventDate.getMonth(),
+ `${eventDate.getHours()}:${String(eventDate.getMinutes()).padStart(2, '0')}`,
+ ]
+ const { delEvent } = useEvents()
+ const { closeModal } = useModal()
+
+ return (
+
+
+ Delete Pizza Event?
+ closeModal()}
+ >
+ ×
+
+
+
+
+ Date:
+ Time
+
+
+
+
+ {ordinal(date)} of {months[month]}
+
+ {time}
+
+
+
+ closeModal()} text="Cancel" buttonStyle="secondary" className="w-[270px]" />
+ {
+ typeof eventId === 'string' ? delEvent(eventId) : ''
+ closeModal()
+ }}
+ className="w-[270px] hover:bg-red-light"
+ text="Delete Event"
+ buttonStyle="primary"
+ />
+
+
+ )
+}
+
+export { DeletePizzaEventCard }
diff --git a/application/next-frontend/src/Admin/scenarios/Events/components/EventCalendar.tsx b/application/next-frontend/src/Admin/scenarios/Events/components/EventCalendar.tsx
new file mode 100644
index 00000000..3e55cf1c
--- /dev/null
+++ b/application/next-frontend/src/Admin/scenarios/Events/components/EventCalendar.tsx
@@ -0,0 +1,221 @@
+import { useMemo, useState } from 'react'
+import Image from 'next/image'
+import Triangle from 'Admin/assets/Triangle.svg'
+import Line from 'Admin/assets/Line.svg'
+import PizzaEaten from 'Admin/assets/pizza/PizzaEaten.svg'
+import PizzaRound from 'Admin/assets/pizza/PizzaRound.svg'
+import PizzaSlice from 'Admin/assets/pizza/PizzaSlice.svg'
+import { ApiEvent, useEvents } from '@/api/useEvents'
+import { useModal } from 'Shared/context/ModalContext'
+import { CreatePizzaEventCard } from './CreatePizzaEventCard'
+import { DeletePizzaEventCard } from './DeletePizzaEventCard'
+import { useRestaurants } from '@/api/useRestaurants'
+import { sendToast } from '@/Shared/toast/defaultToast'
+import { useCurrentChannel } from '@/api/useCurrentChannel'
+
+type ModalData = {
+ eventId: string | number
+ selectedDate: Date
+ event: ApiEvent | undefined
+}
+
+// Goes into the next month - 1 day, which is the last day of current month, and returns this day
+// e.g. if we are in October, and our last day is 31st, we return 31
+const daysInMonth = (date: Date) => new Date(date.getFullYear(), date.getMonth() + 1, 0).getDate()
+
+// Default is monday 1 ... saturday 6, sunday 0
+// This changes to monday 0 .. saturday 5, sunday 6
+const firstDayOfMonth = (date: Date) => {
+ const firstDay = new Date(date.getFullYear(), date.getMonth(), 1).getDay() - 1
+ return firstDay === -1 ? 6 : firstDay
+}
+
+const EventCalendar = () => {
+ // This will always be the first of the month, but is used for Year + Month purposes
+ const [currentDate, setCurrentDate] = useState(new Date())
+ const pizzaImages = [PizzaEaten, PizzaRound, PizzaSlice]
+ const daysInCurrentMonth = Array(daysInMonth(currentDate))
+ .fill(null)
+ .map((_, i) => i + 1)
+ const weekdays = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']
+ const months = Array.from({ length: 12 }, (_, i) => {
+ return new Date(0, i).toLocaleString('en-US', { month: 'long' })
+ })
+ const fullDay = new Date()
+ const today = new Date(fullDay.getFullYear(), fullDay.getMonth(), fullDay.getDate())
+ const { data } = useEvents()
+
+ const { openModal } = useModal()
+ const { data: restaurantData } = useRestaurants()
+ const { data: currentChannelData } = useCurrentChannel()
+
+ const handleOnClick = (modalData: ModalData) => {
+ if ((!restaurantData || !restaurantData.length) && !modalData.event) {
+ sendToast('You need to have added restaurants in order to create events.')
+ } else if (!currentChannelData || !currentChannelData.channel_id) {
+ sendToast('You need to have set a channel for the bot in order to create events.')
+ } else {
+ openModal(
+ modalData.event ? (
+
+ ) : (
+
+ ),
+ )
+ }
+ }
+
+ const eventsForCurrentMonth = useMemo(() => {
+ const getEventsForCurrentMonth = () => {
+ if (!data || data.length == 0) return []
+
+ return data
+ .filter((event) => {
+ const eventDate = new Date(event.time)
+ return (
+ eventDate.getMonth() === currentDate.getMonth() &&
+ eventDate.getFullYear() === currentDate.getFullYear()
+ )
+ })
+ .map((event) => [event.id, new Date(event.time).getDate()])
+ }
+ return getEventsForCurrentMonth()
+ }, [data, currentDate])
+
+ const getEventId = (selectedDate: Date) => {
+ if (selectedDate < today) return -1
+
+ const selectedDay = selectedDate.getDate()
+ const eventIndex = eventsForCurrentMonth.findIndex(([, m]) => m === selectedDay)
+ if (eventIndex == -1) return -1
+
+ const eventId = eventsForCurrentMonth[eventIndex][0]
+ return eventId
+ }
+
+ const renderMonth = () => {
+ // Reverse the list so that we can pop instead of shift when rendering rows
+ const remainingDays = [...daysInCurrentMonth.reverse()]
+ const weekRow = []
+ const start = firstDayOfMonth(currentDate)
+ weekRow.push(renderRow(remainingDays, start, 6))
+
+ while (remainingDays.length > 0) {
+ weekRow.push(renderRow(remainingDays, 0, Math.min(6, remainingDays.length - 1)))
+ }
+ return weekRow
+ }
+
+ const renderRow = (remainingDays: number[], startIndex: number, endIndex: number) => {
+ return (
+
+ {Array(7)
+ .fill(null)
+ .map((_, dayOfWeek) => {
+ if (startIndex <= dayOfWeek && dayOfWeek <= endIndex) {
+ const day = remainingDays.pop()
+ if (day) {
+ // currentToday is at 00:00, so therefore comparisons with today will always yield LT
+ // "current" as in, the date picked in the calendar
+ const currentToday = new Date(currentDate.getFullYear(), currentDate.getMonth(), day)
+ const currentTomorrow = new Date(
+ currentDate.getFullYear(),
+ currentDate.getMonth(),
+ day + 1,
+ )
+
+ // Event data. eventId is possibly -1, eventObject is possibly undefined
+ const eventId = getEventId(currentToday)
+ const eventObject = data?.find((event) => event.id == eventId)
+
+ // Styling
+ const image = pizzaImages[Math.floor(Math.random() * pizzaImages.length)]
+ const styling = `h-[3.75rem] w-[4.15rem] border border-green-primary text-dark
+ ${
+ today >= currentTomorrow
+ ? 'opacity-50'
+ : eventsForCurrentMonth.some(([, d]) => d == day)
+ ? 'cursor-pointer bg-green-primary text-white hover:bg-red-highlight'
+ : 'cursor-pointer bg-white hover:bg-green-secondary'
+ }`
+
+ return today >= currentTomorrow ? (
+
+ {day}
+
+ ) : (
+ {
+ handleOnClick({
+ eventId: eventId,
+ selectedDate: currentToday,
+ event: eventObject,
+ })
+ }}
+ >
+ {day}
+ {eventId != -1 && (
+
+ )}
+
+ )
+ }
+ }
+ return
+ })}
+
+ )
+ }
+
+ const setPreviousMonth = () => {
+ setCurrentDate(new Date(currentDate.getFullYear(), currentDate.getMonth() - 1, 1))
+ }
+
+ const setNextMonth = () => {
+ setCurrentDate(new Date(currentDate.getFullYear(), currentDate.getMonth() + 1, 1))
+ }
+
+ return (
+
+
+ setPreviousMonth()}
+ width={20}
+ src={Triangle}
+ className="rotate-180 cursor-pointer"
+ alt="show previous month"
+ />
+ {months[currentDate.getMonth()]}
+ setNextMonth()}
+ width={20}
+ src={Triangle}
+ alt="show next month"
+ className="cursor-pointer"
+ />
+
+
+
+
+
+ {weekdays.map((day) => (
+
+ {day}
+
+ ))}
+
+
+ {renderMonth()}
+
+
+ )
+}
+
+export { EventCalendar }
diff --git a/application/next-frontend/src/Admin/scenarios/Events/components/EventCalendarModal.tsx b/application/next-frontend/src/Admin/scenarios/Events/components/EventCalendarModal.tsx
new file mode 100644
index 00000000..fd9e6916
--- /dev/null
+++ b/application/next-frontend/src/Admin/scenarios/Events/components/EventCalendarModal.tsx
@@ -0,0 +1,18 @@
+import { HoverProvider } from 'Shared/context/HoverContext'
+import { Events } from '@/Admin/scenarios/Events/'
+import { EventCalendar } from '@/Admin/scenarios/Events/components/EventCalendar'
+
+const EventModal = () => {
+ return (
+
+ )
+}
+
+export { EventModal }
diff --git a/application/next-frontend/src/Admin/scenarios/Events/components/NextEventInfo.tsx b/application/next-frontend/src/Admin/scenarios/Events/components/NextEventInfo.tsx
new file mode 100644
index 00000000..ca8f3143
--- /dev/null
+++ b/application/next-frontend/src/Admin/scenarios/Events/components/NextEventInfo.tsx
@@ -0,0 +1,71 @@
+import { useHover } from '@/Shared/context/HoverContext'
+import { ApiInvitation, useInvitations } from '@/api/useInvitations'
+
+const InvitationsStatus = ({ invitations }: { invitations: ApiInvitation[] }) => {
+ const responses = invitations.reduce(
+ (prev, next) => {
+ if (next.rsvp === 'unanswered') prev.waiting++
+ else if (next.rsvp === 'attending') prev.accepted++
+ else if (next.rsvp === 'not attending') prev.declined++
+ return prev
+ },
+ { waiting: 0, accepted: 0, declined: 0 },
+ )
+
+ return (
+
+ {responses.accepted} Accepted
+ {responses.waiting} Waiting
+ {responses.declined} Declined
+
+ )
+}
+
+const NextEventInfo = ({
+ event_id,
+ resturantName,
+ date,
+ time,
+ meridiem,
+}: {
+ event_id: string
+ resturantName: string
+ date: string
+ time: string
+ meridiem: string
+}) => {
+ const { data, error, isLoading } = useInvitations(event_id)
+
+ const { isHovered } = useHover()
+
+ return (
+
+
Next event:
+
+ {resturantName}
+
+
+ {date}
+
+
+ {time} {meridiem}
+
+
+
+
Invitation status:
+
+ {isLoading ? (
+ 'Loading...'
+ ) : error ? (
+ `Failed to load invitation status. ${error?.info.msg}`
+ ) : !data || !data.length ? (
+ 'Invitations have not been sent yet.'
+ ) : (
+
+ )}
+
+
+ )
+}
+
+export { NextEventInfo }
diff --git a/application/next-frontend/src/Admin/scenarios/Events/index.tsx b/application/next-frontend/src/Admin/scenarios/Events/index.tsx
new file mode 100644
index 00000000..27913cc9
--- /dev/null
+++ b/application/next-frontend/src/Admin/scenarios/Events/index.tsx
@@ -0,0 +1,72 @@
+import { CardComponentWrapper } from '@/Admin/components/CardComponentWrapper'
+import { useEvents } from '@/api/useEvents'
+import { ApiEvent } from '@/api/useEvents'
+import { format } from 'date-fns'
+import { NextEventInfo } from './components/NextEventInfo'
+import { useEffect, useState } from 'react'
+import { useModal } from 'Shared/context/ModalContext'
+import { EventModal } from 'Admin/scenarios/Events/components/EventCalendarModal'
+
+const futureDate = (date: ApiEvent) => new Date(date.time) >= new Date()
+
+const differenceBetweenTwoDates = (date1: ApiEvent, date2: ApiEvent) =>
+ new Date(date1.time).getTime() - new Date(date2.time).getTime()
+
+const eventDateFormatted = (date: string) => format(new Date(date), 'EEEE, dd. MMMM')
+
+const eventTimeFormatted = (date: string) =>
+ format(new Date(date), 'h:mm a').toLowerCase().replace('am', 'a.m.').replace('pm', 'p.m.').split(' ')
+
+const upcomingEventsMessage = (eventsNumber: number) =>
+ eventsNumber === 1 ? 'You have 1 upcoming event.' : `You have ${eventsNumber} upcoming events.`
+
+const Events = ({ clickable = true }: { clickable?: boolean }) => {
+ const { data, isLoading, error } = useEvents()
+ const futureEvents = data?.filter(futureDate).sort(differenceBetweenTwoDates) ?? []
+ const [time, meridiem] = futureEvents.length > 0 ? eventTimeFormatted(futureEvents[0].time) : ['0', '0']
+ const [eventModalShowing, setEventModalShowing] = useState(false)
+ const { openModal, modalStack } = useModal()
+
+ const handleOnEventClick = () => {
+ setEventModalShowing(true)
+ openModal( )
+ }
+
+ useEffect(() => {
+ if (modalStack.length == 0) {
+ setEventModalShowing(false)
+ }
+ }, [modalStack])
+
+ return (
+
+
+ {isLoading ? (
+ 'Loading...'
+ ) : error ? (
+ `Failed to load events. ${error?.info.msg}`
+ ) : !data || !data.length || !futureEvents.length ? (
+ ''
+ ) : (
+
+ )}
+
+ {upcomingEventsMessage(futureEvents.length)}
+
+
+
+ )
+}
+
+export { Events }
diff --git a/application/next-frontend/src/Admin/scenarios/Navbar.tsx b/application/next-frontend/src/Admin/scenarios/Navbar.tsx
new file mode 100644
index 00000000..dbc97c89
--- /dev/null
+++ b/application/next-frontend/src/Admin/scenarios/Navbar.tsx
@@ -0,0 +1,58 @@
+import { useRouter } from 'next/router'
+import { apiRequestHelper } from '@/api/utils'
+import { useState } from 'react'
+import Image from 'next/image'
+import PizzaBotLogo from 'Admin/assets/PizzaBotLogo.svg'
+
+const Navbar = () => {
+ const router = useRouter()
+ const [activeIndex, setActiveIndex] = useState(2)
+
+ const logout = async () => {
+ try {
+ const res = await apiRequestHelper.del<{ msg: string }>('/auth/logout')
+ if (res) router.push('/login')
+ } catch (err) {
+ console.error(err)
+ }
+ }
+
+ const navbarEntries = [
+ {
+ text: 'Help',
+ key: 0,
+ },
+ {
+ text: 'Policy',
+ key: 1,
+ },
+ {
+ text: 'Admin',
+ key: 2,
+ },
+ ]
+
+ return (
+
+
+
+
+ {navbarEntries.map((entry) => (
+
setActiveIndex(entry.key)}
+ >
+ {entry.text}
+
+ ))}
+
logout()}>
+ Log out
+
+
+ )
+}
+
+export { Navbar }
diff --git a/application/next-frontend/src/Admin/scenarios/Onboarding/components/EventConfig.tsx b/application/next-frontend/src/Admin/scenarios/Onboarding/components/EventConfig.tsx
new file mode 100644
index 00000000..d73fcd47
--- /dev/null
+++ b/application/next-frontend/src/Admin/scenarios/Onboarding/components/EventConfig.tsx
@@ -0,0 +1,109 @@
+import { Controller, FormProvider, useForm } from 'react-hook-form'
+import { zodResolver } from '@hookform/resolvers/zod'
+import z from 'zod'
+import { TextField } from '@mui/material'
+import { ApiEventPost, useEvents } from '@/api/useEvents'
+import DatePicker from 'react-datepicker'
+import { useRestaurants } from '@/api/useRestaurants'
+import 'react-datepicker/dist/react-datepicker.css'
+import { useRouter } from 'next/router'
+import Button from '@/Admin/components/Button'
+
+const EventConfig = () => {
+ const router = useRouter()
+ const validationSchema = z.object({
+ dateTime: z.date().refine((value) => value !== null, { message: 'Date is required' }),
+ })
+
+ interface FormData {
+ dateTime: Date
+ }
+
+ const { data: restaurantData } = useRestaurants()
+
+ const findRestauarant = () => {
+ if (restaurantData === undefined || restaurantData.length === 0) {
+ return
+ }
+
+ const randomIndex = Math.floor(Math.random() * restaurantData.length)
+ return restaurantData[randomIndex]
+ }
+
+ const today = new Date()
+
+ const methods = useForm({
+ resolver: zodResolver(validationSchema),
+ defaultValues: {
+ dateTime: today,
+ },
+ })
+ const { handleSubmit } = methods
+
+ const { addEvent } = useEvents()
+
+ const onSubmit = (data: FormData) => {
+ const restaurant = findRestauarant()
+
+ if (restaurant) {
+ const event: ApiEventPost = {
+ time: data.dateTime.toISOString(),
+ people_per_event: 5,
+ restaurant_id: restaurant.id,
+ }
+ addEvent(event)
+ }
+ }
+
+ return (
+
+
2/2
+
+ Set up your first event!
+
+
+
+
+
+ )
+}
+
+export { EventConfig }
diff --git a/application/next-frontend/src/Admin/scenarios/Onboarding/components/RestaurantsConfig.tsx b/application/next-frontend/src/Admin/scenarios/Onboarding/components/RestaurantsConfig.tsx
new file mode 100644
index 00000000..48fd7058
--- /dev/null
+++ b/application/next-frontend/src/Admin/scenarios/Onboarding/components/RestaurantsConfig.tsx
@@ -0,0 +1,98 @@
+import Button from '@/Admin/components/Button'
+import { Restaurant, useRestaurants } from '@/api/useRestaurants'
+import { zodResolver } from '@hookform/resolvers/zod'
+import { useState } from 'react'
+import { useForm } from 'react-hook-form'
+import { z } from 'zod'
+
+interface RestaurantsConfigProps {
+ onNext: () => void
+}
+
+const RestaurantsConfig = (props: RestaurantsConfigProps) => {
+ const { addRestaurant } = useRestaurants()
+ const [restaurants, setRestaurants] = useState([])
+
+ const validationSchema = z.object({
+ name: z.string().min(1, { message: 'Name required' }),
+ })
+
+ const { register, handleSubmit, reset, getValues } = useForm({
+ resolver: zodResolver(validationSchema),
+ })
+
+ const validateForm = (data: Restaurant) => {
+ setRestaurants((prevRestaurants) => [data, ...prevRestaurants])
+ reset()
+ }
+
+ const addAllRestaurants = () => {
+ if (restaurants.length !== 0) {
+ restaurants.forEach(addRestaurant)
+ setRestaurants([])
+ props.onNext()
+ }
+ }
+
+ const deleteRestaurant = (index: number) => {
+ setRestaurants((prevRestaurants) => prevRestaurants.filter((_, i) => i !== index))
+ }
+
+ const handleAddClick = () => {
+ const { name } = getValues()
+ if (name) {
+ validateForm({ name })
+ }
+ }
+
+ return (
+
+
1/2
+
+ What Pizza Places would you like to visit?
+
+
+
+
+
+ {restaurants.map((restaurant, index) => (
+
+
deleteRestaurant(index)}>×
+
{restaurant.name}
+
+ ))}
+
+
+
+
+
+
+
+
+ )
+}
+
+export { RestaurantsConfig }
diff --git a/application/next-frontend/src/Admin/scenarios/Onboarding/index.tsx b/application/next-frontend/src/Admin/scenarios/Onboarding/index.tsx
new file mode 100644
index 00000000..83611623
--- /dev/null
+++ b/application/next-frontend/src/Admin/scenarios/Onboarding/index.tsx
@@ -0,0 +1,23 @@
+import { AdminSidebar } from 'Admin/components/AdminSidebar'
+import { RestaurantsConfig } from './components/RestaurantsConfig'
+import { EventConfig } from './components/EventConfig'
+import { useState } from 'react'
+
+const Onboarding = () => {
+ const [step, setStep] = useState(0)
+
+ return (
+
+
+
+ {step === 0 ? setStep(1)} /> : }
+
+
+ )
+}
+
+export { Onboarding }
diff --git a/application/next-frontend/src/Admin/scenarios/Restaurants/components/NewRestaurantModal.tsx b/application/next-frontend/src/Admin/scenarios/Restaurants/components/NewRestaurantModal.tsx
new file mode 100644
index 00000000..28cdea7f
--- /dev/null
+++ b/application/next-frontend/src/Admin/scenarios/Restaurants/components/NewRestaurantModal.tsx
@@ -0,0 +1,85 @@
+import { useForm } from 'react-hook-form'
+import { z } from 'zod'
+import { zodResolver } from '@hookform/resolvers/zod'
+import React from 'react'
+import { useModal } from 'Shared/context/ModalContext'
+import { useRestaurants, Restaurant } from '@/api/useRestaurants'
+
+const validationSchema = z.object({
+ name: z.string().min(1, { message: 'Name required' }),
+ link: z.string().optional(),
+ tlf: z.string().optional(),
+ address: z.string().optional(),
+})
+
+const NewRestaurantModal = () => {
+ const {
+ register,
+ handleSubmit,
+ setValue,
+ formState: { errors },
+ } = useForm({ resolver: zodResolver(validationSchema) })
+
+ const { closeModal } = useModal()
+ const { data, addRestaurant, delRestaurant } = useRestaurants()
+
+ const validateForm = (data: Restaurant) => {
+ const newRestaurant: Restaurant = { name: data.name }
+ addRestaurant(newRestaurant)
+ setValue('name', '')
+ }
+
+ return (
+
+
+
Places
+ closeModal()}>
+ ×
+
+
+
+
+
+
You have added...
+
+ {data?.length || 0} Pizza Places
+
+
+
+
+
+
+
+
+ {data &&
+ data.map((restaurant) => (
+
delRestaurant(restaurant.id)}
+ key={restaurant.id}
+ >
+
×
+
{restaurant.name}
+
+ ))}
+
+
+
+
+
+ )
+}
+
+export { NewRestaurantModal }
diff --git a/application/next-frontend/src/Admin/scenarios/Restaurants/index.tsx b/application/next-frontend/src/Admin/scenarios/Restaurants/index.tsx
new file mode 100644
index 00000000..c49afe17
--- /dev/null
+++ b/application/next-frontend/src/Admin/scenarios/Restaurants/index.tsx
@@ -0,0 +1,42 @@
+import { NewRestaurantModal } from './components/NewRestaurantModal'
+import { CardComponentWrapper } from 'Admin/components/CardComponentWrapper'
+import { useRestaurants } from '@/api/useRestaurants'
+import { useModal } from 'Shared/context/ModalContext'
+import { useHover } from 'Shared/context/HoverContext'
+
+const Restaurants = () => {
+ const { data, isLoading, error } = useRestaurants()
+ const { openModal } = useModal()
+ const { isHovered } = useHover()
+
+ const handleOnClickCard = () => {
+ openModal( )
+ }
+
+ return (
+
+
+ {isLoading ? (
+ 'Loading...'
+ ) : error ? (
+ `Failed to load users due to the following error: ${error?.info.msg}`
+ ) : (
+
+
+ {!data || data.length == 0 ? 'No restaurants found...' : 'You have added...'}
+
+
+
+ {!data || data.length == 0
+ ? 'Try adding one!'
+ : `${data.length} ${data.length == 1 ? 'Pizza Place' : 'Pizza Places'}`}
+
+
+
+ )}
+
+
+ )
+}
+
+export { Restaurants }
diff --git a/application/next-frontend/src/Admin/scenarios/SlackChannel.tsx b/application/next-frontend/src/Admin/scenarios/SlackChannel.tsx
new file mode 100644
index 00000000..4a2203c8
--- /dev/null
+++ b/application/next-frontend/src/Admin/scenarios/SlackChannel.tsx
@@ -0,0 +1,42 @@
+import { useCurrentChannel } from '@/api/useCurrentChannel'
+import { SlackUser } from '@/api/useSlackUsers'
+import { CardComponentWrapper } from '../components/CardComponentWrapper'
+import { useHover } from '@/Shared/context/HoverContext'
+
+const numUsersInChannel = (users: SlackUser[]) => {
+ if (users.length === 0) return 'No users in channel'
+ if (users.length === 1) return '1 user in channel'
+ return `${users.length} users in channel`
+}
+
+const SlackChannel = () => {
+ const { data, isLoading, error } = useCurrentChannel()
+
+ const channelName = data?.channel_name
+ const channelMembers = data?.users.filter((user) => user.active) ?? []
+ const { isHovered } = useHover()
+
+ return (
+
+ {isLoading ? (
+ 'Loading...'
+ ) : error ? (
+ `Failed to load channel info due to the following error: ${error?.info.msg}`
+ ) : !data || !data.channel_id ? (
+
+ No slack channel has been set for the PizzaBot. Please use the command{' '}
+ /set‑pizza‑channel in your preferred slack-channel to set.
+
+ ) : (
+
+
+ #{channelName}
+
+ {numUsersInChannel(channelMembers)}
+
+ )}
+
+ )
+}
+
+export { SlackChannel }
diff --git a/application/next-frontend/src/Admin/types/User.ts b/application/next-frontend/src/Admin/types/User.ts
new file mode 100644
index 00000000..8361e73c
--- /dev/null
+++ b/application/next-frontend/src/Admin/types/User.ts
@@ -0,0 +1,18 @@
+export type User = {
+ email: string
+ picture: string
+ id: string
+ name: string
+ roles: Array
+}
+
+export type JwtToken = {
+ fresh: string
+ iat: string
+ jti: string
+ type: string
+ sub: string
+ nbf: string
+ exp: string
+ user: User
+}
diff --git a/application/next-frontend/src/Landing/assets/Amalie.svg b/application/next-frontend/src/Landing/assets/Amalie.svg
new file mode 100644
index 00000000..d6265c93
--- /dev/null
+++ b/application/next-frontend/src/Landing/assets/Amalie.svg
@@ -0,0 +1,32 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/application/next-frontend/src/Landing/assets/Line.svg b/application/next-frontend/src/Landing/assets/Line.svg
new file mode 100644
index 00000000..a8ed263c
--- /dev/null
+++ b/application/next-frontend/src/Landing/assets/Line.svg
@@ -0,0 +1,3 @@
+
+
+
diff --git a/application/next-frontend/src/Landing/assets/Mathilde.svg b/application/next-frontend/src/Landing/assets/Mathilde.svg
new file mode 100644
index 00000000..b14bded8
--- /dev/null
+++ b/application/next-frontend/src/Landing/assets/Mathilde.svg
@@ -0,0 +1,31 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/application/next-frontend/src/Landing/assets/PizzaAnnouncement.png b/application/next-frontend/src/Landing/assets/PizzaAnnouncement.png
new file mode 100644
index 00000000..df533262
Binary files /dev/null and b/application/next-frontend/src/Landing/assets/PizzaAnnouncement.png differ
diff --git a/application/next-frontend/src/Landing/assets/PizzaBotWithFourArms.png b/application/next-frontend/src/Landing/assets/PizzaBotWithFourArms.png
new file mode 100644
index 00000000..449da951
Binary files /dev/null and b/application/next-frontend/src/Landing/assets/PizzaBotWithFourArms.png differ
diff --git a/application/next-frontend/src/Landing/assets/PizzaInvitation.png b/application/next-frontend/src/Landing/assets/PizzaInvitation.png
new file mode 100644
index 00000000..25e0b2fd
Binary files /dev/null and b/application/next-frontend/src/Landing/assets/PizzaInvitation.png differ
diff --git a/application/next-frontend/src/Landing/assets/Vilde.svg b/application/next-frontend/src/Landing/assets/Vilde.svg
new file mode 100644
index 00000000..d30f2b40
--- /dev/null
+++ b/application/next-frontend/src/Landing/assets/Vilde.svg
@@ -0,0 +1,31 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/application/next-frontend/src/Landing/assets/chooseChannel.svg b/application/next-frontend/src/Landing/assets/chooseChannel.svg
new file mode 100644
index 00000000..8edb3cd0
--- /dev/null
+++ b/application/next-frontend/src/Landing/assets/chooseChannel.svg
@@ -0,0 +1,21 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/application/next-frontend/src/Landing/assets/chooseDate.svg b/application/next-frontend/src/Landing/assets/chooseDate.svg
new file mode 100644
index 00000000..e2d0522e
--- /dev/null
+++ b/application/next-frontend/src/Landing/assets/chooseDate.svg
@@ -0,0 +1,73 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/application/next-frontend/src/Landing/assets/chooseRestaurants.svg b/application/next-frontend/src/Landing/assets/chooseRestaurants.svg
new file mode 100644
index 00000000..fd0de7f6
--- /dev/null
+++ b/application/next-frontend/src/Landing/assets/chooseRestaurants.svg
@@ -0,0 +1,21 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/application/next-frontend/src/Landing/assets/illustrations/EngagedTeams.svg b/application/next-frontend/src/Landing/assets/illustrations/EngagedTeams.svg
new file mode 100644
index 00000000..0eafd164
--- /dev/null
+++ b/application/next-frontend/src/Landing/assets/illustrations/EngagedTeams.svg
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
+
+
diff --git a/application/next-frontend/src/Landing/assets/illustrations/Friendships.svg b/application/next-frontend/src/Landing/assets/illustrations/Friendships.svg
new file mode 100644
index 00000000..066e9848
--- /dev/null
+++ b/application/next-frontend/src/Landing/assets/illustrations/Friendships.svg
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
+
+
diff --git a/application/next-frontend/src/Landing/assets/illustrations/GoodTraditions.svg b/application/next-frontend/src/Landing/assets/illustrations/GoodTraditions.svg
new file mode 100644
index 00000000..a2b3334e
--- /dev/null
+++ b/application/next-frontend/src/Landing/assets/illustrations/GoodTraditions.svg
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
+
+
diff --git a/application/next-frontend/src/Landing/assets/illustrations/HandWithTopping.svg b/application/next-frontend/src/Landing/assets/illustrations/HandWithTopping.svg
new file mode 100644
index 00000000..1599db21
--- /dev/null
+++ b/application/next-frontend/src/Landing/assets/illustrations/HandWithTopping.svg
@@ -0,0 +1,3 @@
+
+
+
diff --git a/application/next-frontend/src/Landing/assets/illustrations/HappyMascotLanding.svg b/application/next-frontend/src/Landing/assets/illustrations/HappyMascotLanding.svg
new file mode 100644
index 00000000..5b5e57b4
--- /dev/null
+++ b/application/next-frontend/src/Landing/assets/illustrations/HappyMascotLanding.svg
@@ -0,0 +1,19 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/application/next-frontend/src/Landing/assets/illustrations/Leaves1.svg b/application/next-frontend/src/Landing/assets/illustrations/Leaves1.svg
new file mode 100644
index 00000000..d4019273
--- /dev/null
+++ b/application/next-frontend/src/Landing/assets/illustrations/Leaves1.svg
@@ -0,0 +1,42 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/application/next-frontend/src/Landing/assets/illustrations/Leaves2.svg b/application/next-frontend/src/Landing/assets/illustrations/Leaves2.svg
new file mode 100644
index 00000000..0030c7c5
--- /dev/null
+++ b/application/next-frontend/src/Landing/assets/illustrations/Leaves2.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/application/next-frontend/src/Landing/assets/illustrations/MascotSad.svg b/application/next-frontend/src/Landing/assets/illustrations/MascotSad.svg
new file mode 100644
index 00000000..76f9399c
--- /dev/null
+++ b/application/next-frontend/src/Landing/assets/illustrations/MascotSad.svg
@@ -0,0 +1,10 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/application/next-frontend/src/Landing/assets/illustrations/MascotWithFourArms.svg b/application/next-frontend/src/Landing/assets/illustrations/MascotWithFourArms.svg
new file mode 100644
index 00000000..c679969d
--- /dev/null
+++ b/application/next-frontend/src/Landing/assets/illustrations/MascotWithFourArms.svg
@@ -0,0 +1,10 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/application/next-frontend/src/Landing/assets/illustrations/PizzaBotLogo.svg b/application/next-frontend/src/Landing/assets/illustrations/PizzaBotLogo.svg
new file mode 100644
index 00000000..d1a76690
--- /dev/null
+++ b/application/next-frontend/src/Landing/assets/illustrations/PizzaBotLogo.svg
@@ -0,0 +1,3 @@
+
+
+
diff --git a/application/next-frontend/src/Landing/assets/illustrations/PizzaBotLogoFooter.svg b/application/next-frontend/src/Landing/assets/illustrations/PizzaBotLogoFooter.svg
new file mode 100644
index 00000000..4a101c7c
--- /dev/null
+++ b/application/next-frontend/src/Landing/assets/illustrations/PizzaBotLogoFooter.svg
@@ -0,0 +1,3 @@
+
+
+
diff --git a/application/next-frontend/src/Landing/assets/illustrations/PizzaBotYellow.svg b/application/next-frontend/src/Landing/assets/illustrations/PizzaBotYellow.svg
new file mode 100644
index 00000000..f4ffd54a
--- /dev/null
+++ b/application/next-frontend/src/Landing/assets/illustrations/PizzaBotYellow.svg
@@ -0,0 +1,10 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/application/next-frontend/src/Landing/assets/illustrations/SafeEnvironments.svg b/application/next-frontend/src/Landing/assets/illustrations/SafeEnvironments.svg
new file mode 100644
index 00000000..492527f6
--- /dev/null
+++ b/application/next-frontend/src/Landing/assets/illustrations/SafeEnvironments.svg
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
+
+
diff --git a/application/next-frontend/src/Landing/assets/illustrations/SlackLogo.svg b/application/next-frontend/src/Landing/assets/illustrations/SlackLogo.svg
new file mode 100644
index 00000000..52a69777
--- /dev/null
+++ b/application/next-frontend/src/Landing/assets/illustrations/SlackLogo.svg
@@ -0,0 +1,17 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/application/next-frontend/src/Landing/assets/illustrations/SlackLogoWhite.svg b/application/next-frontend/src/Landing/assets/illustrations/SlackLogoWhite.svg
new file mode 100644
index 00000000..7e229dd4
--- /dev/null
+++ b/application/next-frontend/src/Landing/assets/illustrations/SlackLogoWhite.svg
@@ -0,0 +1,13 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/application/next-frontend/src/Landing/assets/illustrations/Tape.svg b/application/next-frontend/src/Landing/assets/illustrations/Tape.svg
new file mode 100644
index 00000000..d4464565
--- /dev/null
+++ b/application/next-frontend/src/Landing/assets/illustrations/Tape.svg
@@ -0,0 +1,3 @@
+
+
+
diff --git a/application/next-frontend/src/Landing/components/AddToSlackButton.tsx b/application/next-frontend/src/Landing/components/AddToSlackButton.tsx
new file mode 100644
index 00000000..ad49f484
--- /dev/null
+++ b/application/next-frontend/src/Landing/components/AddToSlackButton.tsx
@@ -0,0 +1,39 @@
+import Image from 'next/image'
+import SlackLogo from '@/Landing/assets/illustrations/SlackLogo.svg'
+import SlackLogoWhite from '@/Landing/assets/illustrations/SlackLogoWhite.svg'
+import { clientsideApiUri } from '@/api/endpoints'
+
+const addToSlack = async () => {
+ const res = await fetch(clientsideApiUri + '/slack/install', { method: 'GET' }).then((res) => res.json())
+ const redirectURL = res.redirect_url
+
+ if (redirectURL) window.location.assign(redirectURL)
+}
+
+const AddToSlackButton = () => {
+ return (
+
+
+ Add to Slack
+
+ )
+}
+
+const AddToSlackButtonWhite = () => {
+ return (
+
+
+ Add to Slack
+
+ )
+}
+
+export { AddToSlackButton, AddToSlackButtonWhite }
diff --git a/application/next-frontend/src/Landing/components/ImageWithPostit.tsx b/application/next-frontend/src/Landing/components/ImageWithPostit.tsx
new file mode 100644
index 00000000..e8ed6fda
--- /dev/null
+++ b/application/next-frontend/src/Landing/components/ImageWithPostit.tsx
@@ -0,0 +1,34 @@
+import Image from 'next/image'
+import Tape from 'Landing/assets/illustrations/Tape.svg'
+import { useState } from 'react'
+
+type Props = {
+ src: string
+ text: string
+ styles: {
+ tape: string
+ postit: string
+ postitHover: string
+ postitHoverAlternative: string
+ }
+}
+
+const ImageWithPostit = ({ src, text, styles }: Props) => {
+ const [isHovered, setIsHovered] = useState(false)
+
+ return (
+ setIsHovered(true)} onMouseLeave={() => setIsHovered(false)}>
+
+
+
+ {text}
+
+
+ )
+}
+
+export { ImageWithPostit }
diff --git a/application/next-frontend/src/Landing/components/secondSection/ImageContainer.tsx b/application/next-frontend/src/Landing/components/secondSection/ImageContainer.tsx
new file mode 100644
index 00000000..dff74cc8
--- /dev/null
+++ b/application/next-frontend/src/Landing/components/secondSection/ImageContainer.tsx
@@ -0,0 +1,15 @@
+import { StaticImport } from 'next/dist/shared/lib/get-img-props'
+import Image from 'next/image'
+
+const ImageContainer = ({ img, text }: { img: string | StaticImport; text: string }) => {
+ return (
+
+ )
+}
+
+export { ImageContainer }
diff --git a/application/next-frontend/src/Landing/index.tsx b/application/next-frontend/src/Landing/index.tsx
new file mode 100644
index 00000000..9e2524d9
--- /dev/null
+++ b/application/next-frontend/src/Landing/index.tsx
@@ -0,0 +1,23 @@
+import { FirstSection } from './scenarios/FirstSection'
+import { SecondSection } from './scenarios/SecondSection'
+import { ThirdSection } from './scenarios/ThirdSection'
+import { FourthSection } from './scenarios/FourthSection'
+import { FifthSection } from './scenarios/FifthSection'
+import { SixthSection } from './scenarios/SixthSection'
+import { Footer } from './scenarios/Footer'
+
+const Landing = () => {
+ return (
+ <>
+
+
+
+
+
+
+
+ >
+ )
+}
+
+export { Landing }
diff --git a/application/next-frontend/src/Landing/scenarios/FifthSection.tsx b/application/next-frontend/src/Landing/scenarios/FifthSection.tsx
new file mode 100644
index 00000000..b5bc8e05
--- /dev/null
+++ b/application/next-frontend/src/Landing/scenarios/FifthSection.tsx
@@ -0,0 +1,79 @@
+import Mathilde from 'Landing/assets/Mathilde.svg'
+import Amalie from 'Landing/assets/Amalie.svg'
+import Vilde from 'Landing/assets/Vilde.svg'
+import { ImageWithPostit } from 'Landing/components/ImageWithPostit'
+import { AddToSlackButton } from '../components/AddToSlackButton'
+
+const FifthSection = () => {
+ const postitTexts = [
+ "PizzaBot revolutionised our team lunches, effortlessly connecting us over random pizza outings. I even met my boyfriend during one! It's the perfect blend of fun and team building.",
+ "PizzaBot revolutionised our team lunches, effortlessly connecting us over random pizza outings. I even met my boyfriend during one! It's the perfect blend of fun and team building.",
+ "PizzaBot revolutionised our team lunches, effortlessly connecting us over random pizza outings. I even met my boyfriend during one! It's the perfect blend of fun and team building.",
+ ]
+
+ return (
+
+
+ {/* grid def */}
+
+
+
+ Create
+
+ fun memories
+
+
+
+
+
+
+
+
+
+
+
+
+ Explore PizzaBot premium to gather pictures from pizza events, along with many
+ other fun features{' '}
+
+
+
+
+
+
+
+
+
+
+
+ )
+}
+
+export { FifthSection }
diff --git a/application/next-frontend/src/Landing/scenarios/FirstSection.tsx b/application/next-frontend/src/Landing/scenarios/FirstSection.tsx
new file mode 100644
index 00000000..32f3d48d
--- /dev/null
+++ b/application/next-frontend/src/Landing/scenarios/FirstSection.tsx
@@ -0,0 +1,20 @@
+import Image from 'next/image'
+import Line from 'Landing/assets/Line.svg'
+import { AddToSlackButton } from '@/Landing/components/AddToSlackButton'
+
+const FirstSection = () => {
+ return (
+
+
+ A slice to socialise
+
+
+
+ Download the PizzaBot to build better work environments.
+
+
+
+ )
+}
+
+export { FirstSection }
diff --git a/application/next-frontend/src/Landing/scenarios/Footer.tsx b/application/next-frontend/src/Landing/scenarios/Footer.tsx
new file mode 100644
index 00000000..4ff302d6
--- /dev/null
+++ b/application/next-frontend/src/Landing/scenarios/Footer.tsx
@@ -0,0 +1,35 @@
+import PizzaBotLogoFooter from 'Landing/assets/illustrations/PizzaBotLogoFooter.svg'
+import Image from 'next/image'
+import Link from 'next/link'
+
+const Footer = () => {
+ return (
+
+
+
+
+
+ Links Coming Soon
+ Our Terms
+ Privacy Policy
+ Data Protection
+
+
+ feedback@blank.no
+
+ Have more questions?
+ Reach us at pizzabot@blank.no
+
+
+
+ Made with ❤️ from
+
+ Blank AS Oslo
+
+
+
+
+ )
+}
+
+export { Footer }
diff --git a/application/next-frontend/src/Landing/scenarios/FourthSection.tsx b/application/next-frontend/src/Landing/scenarios/FourthSection.tsx
new file mode 100644
index 00000000..f313aeef
--- /dev/null
+++ b/application/next-frontend/src/Landing/scenarios/FourthSection.tsx
@@ -0,0 +1,63 @@
+import Image from 'next/image'
+import PizzaAnnouncement from 'Landing/assets/PizzaAnnouncement.png'
+import PizzaInvitation from 'Landing/assets/PizzaInvitation.png'
+import PizzaBotWithFourArms from 'Landing/assets/PizzaBotWithFourArms.png'
+import { AddToSlackButtonWhite } from '@/Landing/components/AddToSlackButton'
+
+const FourthSection = () => {
+ return (
+
+
+
+
+ The PizzaBot picks 5 people from the slack channel randomly..
+
+
+ and invites them for an event
+
+
+
+
+
+
+
+
+
+
+ Invites are sendt privatly on slack. So you can always
+ decline!
+
+
+
+
+
+
+
+
+ After everyone accepts, events gets posted on the choses
+ channel
+
+
+
+
+
+
+
+
+
+ )
+}
+
+export { FourthSection }
diff --git a/application/next-frontend/src/Landing/scenarios/Header/index.tsx b/application/next-frontend/src/Landing/scenarios/Header/index.tsx
new file mode 100644
index 00000000..dbe50ad9
--- /dev/null
+++ b/application/next-frontend/src/Landing/scenarios/Header/index.tsx
@@ -0,0 +1,34 @@
+import Image from 'next/image'
+import PizzaBotLogo from '@/Landing/assets/illustrations/PizzaBotLogo.svg'
+import { LoginButton } from '@/Shared/components/LoginButton'
+import { useRouter } from 'next/router'
+
+const Header = () => {
+ const router = useRouter()
+
+ const routeToRoot = () => {
+ router.push('/')
+ }
+
+ return (
+ <>
+
+ {/* This is a placeholder for the header so that the content doesn't get hidden behind the header */}
+
+ >
+ )
+}
+
+export { Header }
diff --git a/application/next-frontend/src/Landing/scenarios/LoginPage/index.tsx b/application/next-frontend/src/Landing/scenarios/LoginPage/index.tsx
new file mode 100644
index 00000000..37233b28
--- /dev/null
+++ b/application/next-frontend/src/Landing/scenarios/LoginPage/index.tsx
@@ -0,0 +1,18 @@
+import { LoginButton } from '@/Shared/components/LoginButton'
+import Image from 'next/image'
+import MascotWithFourArms from 'Landing/assets/illustrations/MascotWithFourArms.svg'
+
+const LoginPage = () => {
+ return (
+
+
+
Welcome to Pizzabot!
+
Please login to continue
+
+
+
+
+ )
+}
+
+export { LoginPage }
diff --git a/application/next-frontend/src/Landing/scenarios/SecondSection.tsx b/application/next-frontend/src/Landing/scenarios/SecondSection.tsx
new file mode 100644
index 00000000..df45c50a
--- /dev/null
+++ b/application/next-frontend/src/Landing/scenarios/SecondSection.tsx
@@ -0,0 +1,49 @@
+import SafeEnvironments from 'Landing/assets/illustrations/SafeEnvironments.svg'
+import EngagedTeams from 'Landing/assets/illustrations/EngagedTeams.svg'
+import Friendships from 'Landing/assets/illustrations/Friendships.svg'
+import GoodTraditions from 'Landing/assets/illustrations/GoodTraditions.svg'
+import HappyMascot from 'Landing/assets/illustrations/HappyMascotLanding.svg'
+import { ImageContainer } from 'Landing/components/secondSection/ImageContainer'
+import Image from 'next/image'
+import { AddToSlackButtonWhite } from '@/Landing/components/AddToSlackButton'
+
+const SecondSection = () => {
+ return (
+
+
+ {/* Title */}
+
+
+ Build a work culture with the PizzaBot and create...
+
+
+
+ {/* Icon grid */}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {/* Bot icon */}
+
+
+
+
+
+
+ )
+}
+
+export { SecondSection }
diff --git a/application/next-frontend/src/Landing/scenarios/SixthSection.tsx b/application/next-frontend/src/Landing/scenarios/SixthSection.tsx
new file mode 100644
index 00000000..583c5abc
--- /dev/null
+++ b/application/next-frontend/src/Landing/scenarios/SixthSection.tsx
@@ -0,0 +1,29 @@
+import Image from 'next/image'
+import HandWithTopping from 'Landing/assets/illustrations/HandWithTopping.svg'
+import PizzaBotYellow from 'Landing/assets/illustrations/PizzaBotYellow.svg'
+import { AddToSlackButtonWhite } from '@/Landing/components/AddToSlackButton'
+
+const SixthSection = () => {
+ return (
+
+
+
+
+ Add the PizzaBot to slack now
+
+
+
+
+
+
+
+
+
+
+
+ )
+}
+
+export { SixthSection }
diff --git a/application/next-frontend/src/Landing/scenarios/SlackCallbackPage/index.tsx b/application/next-frontend/src/Landing/scenarios/SlackCallbackPage/index.tsx
new file mode 100644
index 00000000..7c726d73
--- /dev/null
+++ b/application/next-frontend/src/Landing/scenarios/SlackCallbackPage/index.tsx
@@ -0,0 +1,58 @@
+import Image from 'next/image'
+import MascotWithFourArms from 'Landing/assets/illustrations/MascotWithFourArms.svg'
+import MascotSad from 'Landing/assets/illustrations/MascotSad.svg'
+import Link from 'next/link'
+import { StaticImport } from 'next/dist/shared/lib/get-img-props'
+
+const SlackCallbackPage = ({
+ header,
+ message,
+ actionMessage,
+ icon,
+}: {
+ header: string
+ message?: string
+ actionMessage: string
+ icon: StaticImport
+}) => {
+ return (
+
+
+
{header}
+
{message}
+
{actionMessage}
+
+ Go back
+
+
+
+
+ )
+}
+
+const SlackCallbackPageSuccess = ({ message }: { message?: string }) => {
+ return (
+
+ )
+}
+
+const SlackCallbackPageError = ({ message }: { message?: string }) => {
+ return (
+
+ )
+}
+
+export { SlackCallbackPageSuccess, SlackCallbackPageError }
diff --git a/application/next-frontend/src/Landing/scenarios/ThirdSection.tsx b/application/next-frontend/src/Landing/scenarios/ThirdSection.tsx
new file mode 100644
index 00000000..85a15bef
--- /dev/null
+++ b/application/next-frontend/src/Landing/scenarios/ThirdSection.tsx
@@ -0,0 +1,91 @@
+import ChooseChannel from 'Landing/assets/chooseChannel.svg'
+import ChooseRestaurants from 'Landing/assets/chooseRestaurants.svg'
+import ChooseDate from 'Landing/assets/chooseDate.svg'
+import Image from 'next/image'
+import { AddToSlackButton } from '@/Landing/components/AddToSlackButton'
+
+const ThirdSection = () => {
+ return (
+
+
+
+
+ Fun to use, Easy to setup
+
+
+
+ After you add this to Slack..
+
+
+
+ {/* Part 1 */}
+
+
+
+
+ 1.
+
+
+ Choose
+ Channel
+
+
+
+ Choose the slack channel you want the PizzaBot to send invites on.
+
+
+
+ {/* Part 2 */}
+
+
+
+ 2.
+
+
+ Add
+ Restaurants
+
+
+
+ The PizzaBot chooses from one of the restaurants added.
+
+
+ {/* Part 3 */}
+
+
+
+
+ 3.
+
+
+ Set
+ Events
+
+
+
+ The PizzaBot sets up the event after you pick date & time
+
+
+
+
+ {/* Images */}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ )
+}
+
+export { ThirdSection }
diff --git a/application/next-frontend/src/Shared/components/LoginButton.tsx b/application/next-frontend/src/Shared/components/LoginButton.tsx
new file mode 100644
index 00000000..c4b31272
--- /dev/null
+++ b/application/next-frontend/src/Shared/components/LoginButton.tsx
@@ -0,0 +1,28 @@
+import { clientsideApiUri } from '@/api/endpoints'
+
+const LoginButton = ({ className }: { className?: string }) => {
+ const loginRedirect = async () => {
+ try {
+ const res = await fetch(clientsideApiUri + '/auth/login')
+ if (!res.ok) throw new Error()
+
+ const redirectURL = (await res.json()).auth_url
+ if (redirectURL) window.location.assign(redirectURL)
+ else throw new Error()
+ } catch (err) {
+ // TODO: Notify user of error
+ console.error(err)
+ }
+ }
+
+ return (
+
+ Login
+
+ )
+}
+
+export { LoginButton }
diff --git a/application/next-frontend/src/Shared/context/HoverContext.tsx b/application/next-frontend/src/Shared/context/HoverContext.tsx
new file mode 100644
index 00000000..eb13a804
--- /dev/null
+++ b/application/next-frontend/src/Shared/context/HoverContext.tsx
@@ -0,0 +1,36 @@
+import { createContext, useContext, useState } from 'react'
+
+interface HoverContextType {
+ isHovered: boolean
+ setHovered: (hovered: boolean) => void
+}
+
+const HoverContext = createContext(undefined)
+
+export const useHover = () => {
+ const context = useContext(HoverContext)
+ if (!context) {
+ throw new Error('useHover must be used within a HoverProvider')
+ }
+ return context
+}
+
+export const HoverProvider = ({
+ children,
+ isInsideModal = false,
+}: {
+ children: React.ReactNode
+ isInsideModal?: boolean
+}) => {
+ const [isHovered, setLocalHovered] = useState(false)
+
+ const setHovered = (hover: boolean) => {
+ if (isInsideModal) {
+ setLocalHovered(false)
+ } else {
+ setLocalHovered(hover)
+ }
+ }
+
+ return {children}
+}
diff --git a/application/next-frontend/src/Shared/context/ModalContext.tsx b/application/next-frontend/src/Shared/context/ModalContext.tsx
new file mode 100644
index 00000000..855186a2
--- /dev/null
+++ b/application/next-frontend/src/Shared/context/ModalContext.tsx
@@ -0,0 +1,39 @@
+import React, { createContext, useContext, useState } from 'react'
+import { ModalWrapper } from 'Shared/modal/ModalWrapper'
+
+interface ModalContextType {
+ modalStack: React.ReactElement[]
+ openModal: (modalContent: React.ReactElement) => void
+ closeModal: () => void
+}
+
+const ModalContext = createContext(undefined)
+
+export const useModal = () => {
+ const context = useContext(ModalContext)
+ if (!context) {
+ throw new Error('useModal must be used within a ModalProvider')
+ }
+ return context
+}
+
+export const ModalProvider = ({ children }: { children: React.ReactNode }) => {
+ const [modalStack, setModalStack] = useState([])
+
+ const openModal = (modalContent: React.ReactElement) => {
+ setModalStack((prevStack) => [...prevStack, modalContent])
+ }
+
+ const closeModal = () => {
+ setModalStack((prevStack) => prevStack.slice(0, -1))
+ }
+
+ return (
+
+ {children}
+ {modalStack.map((modal, index) => (
+ {modal}
+ ))}
+
+ )
+}
diff --git a/application/next-frontend/src/Shared/modal/ModalWrapper.tsx b/application/next-frontend/src/Shared/modal/ModalWrapper.tsx
new file mode 100644
index 00000000..75763a8b
--- /dev/null
+++ b/application/next-frontend/src/Shared/modal/ModalWrapper.tsx
@@ -0,0 +1,23 @@
+import { useModal } from 'Shared/context/ModalContext'
+
+interface ModalWrapperProps {
+ children: React.ReactNode
+}
+
+const ModalWrapper = ({ children }: ModalWrapperProps) => {
+ const { closeModal } = useModal()
+
+ const handleOverlayClick = (e: React.MouseEvent) => {
+ if (e.target === e.currentTarget) {
+ closeModal()
+ }
+ }
+ return (
+
+ )
+}
+
+export { ModalWrapper }
diff --git a/application/next-frontend/src/Shared/toast/defaultToast.tsx b/application/next-frontend/src/Shared/toast/defaultToast.tsx
new file mode 100644
index 00000000..8caf86cf
--- /dev/null
+++ b/application/next-frontend/src/Shared/toast/defaultToast.tsx
@@ -0,0 +1,15 @@
+import { toast } from 'react-hot-toast'
+
+const ToastComponent = (text: string) => {
+ return (
+
+ {text}
+
+ )
+}
+
+export const sendToast = (text: string) => {
+ const component = ToastComponent(text)
+
+ toast.custom(component, { position: 'top-center' })
+}
diff --git a/application/next-frontend/src/api/endpoints.ts b/application/next-frontend/src/api/endpoints.ts
new file mode 100644
index 00000000..c91ca2b3
--- /dev/null
+++ b/application/next-frontend/src/api/endpoints.ts
@@ -0,0 +1,15 @@
+if (!process.env.NODE_ENV) throw new Error('Missing enviroment variable NODE_ENV')
+if (!process.env.NEXT_PUBLIC_BACKEND_URI) throw new Error('Missing enviroment variable NEXT_PUBLIC_BACKEND_URI')
+
+/**
+ * For use in clientside functionality.
+ * In dev mode, api is routed through nginx with path /api
+ */
+export const clientsideApiUri =
+ process.env.NODE_ENV === 'development' ? '/api' : process.env.NEXT_PUBLIC_BACKEND_URI.replace(/\/+$/, '') + '/api'
+
+/**
+ * For use in serverside functionality.
+ * In dev mode, next serverside fetch calls is not routed with nginx and goes directly to backend
+ */
+export const serversideApiUri = process.env.NEXT_PUBLIC_BACKEND_URI.replace(/\/+$/, '') + '/api'
diff --git a/application/next-frontend/src/api/useAuthedSWR.ts b/application/next-frontend/src/api/useAuthedSWR.ts
new file mode 100644
index 00000000..8ebbde15
--- /dev/null
+++ b/application/next-frontend/src/api/useAuthedSWR.ts
@@ -0,0 +1,21 @@
+import { useRouter } from 'next/router'
+import useSWR from 'swr'
+import { apiRequestHelper } from './utils'
+
+export type FetcherError = {
+ statusCode: number
+ info: {
+ msg: string
+ }
+}
+
+export const useAuthedSWR = (endpoint: string) => {
+ const { error, ...rest } = useSWR(endpoint, apiRequestHelper.get)
+ const router = useRouter()
+
+ if (error?.statusCode === 401 || error?.statusCode === 403) {
+ router.push('/login')
+ }
+
+ return { error, ...rest }
+}
diff --git a/application/next-frontend/src/api/useCurrentChannel.ts b/application/next-frontend/src/api/useCurrentChannel.ts
new file mode 100644
index 00000000..607e5f94
--- /dev/null
+++ b/application/next-frontend/src/api/useCurrentChannel.ts
@@ -0,0 +1,18 @@
+import { useAuthedSWR } from './useAuthedSWR'
+import type { SlackUser } from './useSlackUsers'
+
+export type ApiCurrentChannelInfo = {
+ channel_name: string
+ channel_id: string
+ users: SlackUser[]
+}
+
+const useCurrentChannel = () => {
+ const endpoint = '/users/current-channel'
+
+ const { data, isLoading, error } = useAuthedSWR(endpoint)
+
+ return { data, isLoading, error }
+}
+
+export { useCurrentChannel }
diff --git a/application/next-frontend/src/api/useEvents.ts b/application/next-frontend/src/api/useEvents.ts
new file mode 100644
index 00000000..d8cd76a0
--- /dev/null
+++ b/application/next-frontend/src/api/useEvents.ts
@@ -0,0 +1,71 @@
+import { useAuthedSWR } from './useAuthedSWR'
+import { ApiRestaurant } from './useRestaurants'
+import { apiRequestHelper } from './utils'
+
+export interface Event {
+ time: string
+}
+
+export interface ApiEvent extends Event {
+ id: string
+ finalized: boolean
+ restaurant?: ApiRestaurant
+ people_per_event: number
+}
+
+export interface ApiEventPost extends Event {
+ restaurant_id: string
+ people_per_event: number
+ group_id?: string
+}
+
+const useEvents = () => {
+ const endpoint = '/events'
+ const { data, isLoading, error, mutate } = useAuthedSWR(endpoint)
+ const { post, del } = apiRequestHelper
+
+ const addEvent = async (newEvent: ApiEventPost) => {
+ try {
+ mutate(
+ async () => {
+ const createdEvent = await post(endpoint, newEvent)
+
+ if (data) {
+ return [...data, createdEvent]
+ }
+ },
+ {
+ rollbackOnError: true,
+ revalidate: false,
+ },
+ )
+ } catch (e) {
+ console.error(e)
+ }
+ }
+
+ const delEvent = (eventId: string) => {
+ try {
+ mutate(
+ async () => {
+ await del(`${endpoint}/${eventId}`)
+
+ if (data) {
+ return data.filter((event) => event.id !== eventId)
+ }
+ },
+ {
+ populateCache: true,
+ rollbackOnError: true,
+ revalidate: false,
+ },
+ )
+ } catch (e) {
+ console.error(e)
+ }
+ }
+
+ return { data, isLoading, error, addEvent, delEvent }
+}
+
+export { useEvents }
diff --git a/application/next-frontend/src/api/useInvitations.ts b/application/next-frontend/src/api/useInvitations.ts
new file mode 100644
index 00000000..db39ab26
--- /dev/null
+++ b/application/next-frontend/src/api/useInvitations.ts
@@ -0,0 +1,20 @@
+import { useAuthedSWR } from './useAuthedSWR'
+import { SlackUser } from './useSlackUsers'
+
+export type ApiInvitation = {
+ event_id: string
+ invited_at: string
+ reminded_at: string
+ rsvp: 'unanswered' | 'attending' | 'not attending'
+ slack_id: string
+ slack_user: SlackUser
+}
+
+const useInvitations = (eventId: string) => {
+ const endpoint = `/invitations/${eventId}`
+ const { data, isLoading, error } = useAuthedSWR(endpoint)
+
+ return { data, isLoading, error }
+}
+
+export { useInvitations }
diff --git a/application/next-frontend/src/api/useRestaurants.ts b/application/next-frontend/src/api/useRestaurants.ts
new file mode 100644
index 00000000..f739069b
--- /dev/null
+++ b/application/next-frontend/src/api/useRestaurants.ts
@@ -0,0 +1,66 @@
+import { apiRequestHelper } from './utils'
+import { useAuthedSWR } from './useAuthedSWR'
+
+export interface Restaurant {
+ name: string
+ link?: string
+ tlf?: string
+ address?: string
+}
+
+export interface ApiRestaurant extends Restaurant {
+ id: string
+ rating?: string
+}
+
+const useRestaurants = () => {
+ const endpoint = '/restaurants'
+ const { data, isLoading, error, mutate } = useAuthedSWR(endpoint)
+ const { post, del } = apiRequestHelper
+
+ const addRestaurant = (newRestaurant: Restaurant) => {
+ try {
+ mutate(
+ async () => {
+ await post(endpoint, newRestaurant)
+
+ if (data) {
+ return [...data, newRestaurant]
+ }
+ },
+ {
+ populateCache: true,
+ rollbackOnError: true,
+ revalidate: true,
+ },
+ )
+ } catch (e) {
+ console.error(e)
+ }
+ }
+
+ const delRestaurant = (restaurantId: string) => {
+ try {
+ mutate(
+ async () => {
+ await del(`${endpoint}/${restaurantId}`)
+
+ if (data) {
+ return data.filter((restaurant) => restaurant.id !== restaurantId)
+ }
+ },
+ {
+ populateCache: true,
+ rollbackOnError: true,
+ revalidate: false,
+ },
+ )
+ } catch (e) {
+ console.error(e)
+ }
+ }
+
+ return { data, isLoading, error, addRestaurant, delRestaurant }
+}
+
+export { useRestaurants }
diff --git a/application/next-frontend/src/api/useSlackUsers.ts b/application/next-frontend/src/api/useSlackUsers.ts
new file mode 100644
index 00000000..6fe0020c
--- /dev/null
+++ b/application/next-frontend/src/api/useSlackUsers.ts
@@ -0,0 +1,54 @@
+import { apiRequestHelper } from './utils'
+import { useAuthedSWR } from './useAuthedSWR'
+
+export interface BaseSlackUser {
+ active: boolean
+ priority: number
+}
+
+export interface SlackUser extends BaseSlackUser {
+ slack_id: string
+ current_username: string
+ first_seen: string
+ email?: string
+}
+
+const useSlackUsers = () => {
+ const endpoint = '/users'
+
+ const { data, isLoading, error, mutate } = useAuthedSWR(endpoint)
+
+ const { put } = apiRequestHelper
+
+ const updateUser = (userToUpdate: SlackUser) => {
+ const updatedBaseUser: BaseSlackUser = { active: !userToUpdate.active, priority: userToUpdate.priority }
+
+ try {
+ mutate(
+ async () => {
+ const user = await put(endpoint + '/' + userToUpdate.slack_id, updatedBaseUser)
+
+ if (data) {
+ // Update cache
+ const updatedData = data.map((oldUser) => {
+ if (user.slack_id === oldUser.slack_id) return user
+ return oldUser
+ })
+ return updatedData
+ }
+ },
+ {
+ populateCache: true,
+ rollbackOnError: true,
+ revalidate: false, //dont revalidate since cache is updated
+ },
+ )
+ } catch (e) {
+ console.error(e)
+ }
+ }
+
+ return { data, isLoading, error, updateUser }
+}
+
+export { useSlackUsers }
diff --git a/application/next-frontend/src/api/utils.ts b/application/next-frontend/src/api/utils.ts
new file mode 100644
index 00000000..0d1c2550
--- /dev/null
+++ b/application/next-frontend/src/api/utils.ts
@@ -0,0 +1,67 @@
+import { clientsideApiUri } from './endpoints'
+
+const getCookie = (name: string) => {
+ if (typeof window !== 'undefined') {
+ const value = `; ${document.cookie}`
+ const parts = value.split(`; ${name}=`)
+ if (parts && parts.length === 2) return parts.pop()?.split(';')?.shift()
+ }
+}
+
+const configureHeaders = () => {
+ const headers = new Headers()
+ const cookie = getCookie('csrf_access_token')
+
+ if (cookie) {
+ headers.set('X-CSRF-TOKEN', cookie)
+ }
+
+ headers.set('Content-Type', 'application/json')
+ headers.set('Accept', 'application/json')
+
+ return headers
+}
+
+const fetchData = async (endpoint: string, method: 'GET' | 'POST' | 'PUT' | 'DELETE', body?: object) => {
+ const options: RequestInit = {
+ method: method,
+ headers: configureHeaders(),
+ credentials: 'include',
+ }
+ if (body) options.body = JSON.stringify(body)
+
+ const res = await fetch(clientsideApiUri + endpoint, options)
+
+ if (!res.ok) {
+ const info = await res.json()
+ const err = { statusCode: res.status, info }
+ throw err
+ }
+
+ if (res.status == 204) {
+ return {} as Data
+ }
+
+ return res.json() as Data
+}
+
+export const apiRequestHelper = {
+ /**
+ * Should not be used directly, but through the useAuthedSWR hook
+ */
+ get: async (endpoint: string) => {
+ return await fetchData(endpoint, 'GET')
+ },
+
+ put: async (endpoint: string, body: object) => {
+ return await fetchData(endpoint, 'PUT', body)
+ },
+
+ post: async (endpoint: string, body: object) => {
+ return await fetchData(endpoint, 'POST', body)
+ },
+
+ del: async (endpoint: string) => {
+ return await fetchData(endpoint, 'DELETE')
+ },
+}
diff --git a/application/next-frontend/src/styles/fonts.css b/application/next-frontend/src/styles/fonts.css
new file mode 100644
index 00000000..1c19c91c
--- /dev/null
+++ b/application/next-frontend/src/styles/fonts.css
@@ -0,0 +1,13 @@
+@font-face {
+ font-family: 'Queens Condensed Trial Medium';
+ src: url('../../public/fonts/QueensCondensedTrial-Medium.ttf') format('truetype');
+ font-weight: normal;
+ font-style: normal;
+}
+
+@font-face {
+ font-family: 'Queens Condensed Trial Regular';
+ src: url('../../public/fonts/QueensCondensedTrial-Regular.ttf') format('truetype');
+ font-weight: normal;
+ font-style: normal;
+}
diff --git a/application/next-frontend/src/styles/globals.css b/application/next-frontend/src/styles/globals.css
new file mode 100644
index 00000000..ca370168
--- /dev/null
+++ b/application/next-frontend/src/styles/globals.css
@@ -0,0 +1,19 @@
+@import url('https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@700&display=swap');
+@import url('https://fonts.googleapis.com/css2?family=Inter&family=Space+Grotesk:wght@700&display=swap');
+@import url('https://fonts.googleapis.com/css2?family=Work+Sans:wght@300;400;500;600;700&family=Work+Sans:ital,wght@0,600;1,300&display=swap');
+
+@tailwind base;
+@tailwind components;
+@tailwind utilities;
+
+:root {
+ --foreground-rgb: 0, 0, 0;
+ --background-start-rgb: 214, 219, 220;
+ --background-end-rgb: 255, 255, 255;
+}
+
+body {
+ color: rgb(var(--foreground-rgb));
+ background: linear-gradient(to bottom, transparent, rgb(var(--background-end-rgb))) rgb(var(--background-start-rgb));
+ min-height: 100vh;
+}
diff --git a/application/next-frontend/src/styles/reset.css b/application/next-frontend/src/styles/reset.css
new file mode 100644
index 00000000..75709592
--- /dev/null
+++ b/application/next-frontend/src/styles/reset.css
@@ -0,0 +1,3 @@
+#__next {
+ min-height: inherit;
+}
diff --git a/application/next-frontend/src/styles/tailwindComposables.css b/application/next-frontend/src/styles/tailwindComposables.css
new file mode 100644
index 00000000..7c583ee9
--- /dev/null
+++ b/application/next-frontend/src/styles/tailwindComposables.css
@@ -0,0 +1,81 @@
+.header {
+ @apply fixed h-32 bg-green-secondary w-full px-12 top-0;
+}
+
+.scrollable-wrapper {
+ @apply relative max-h-[11rem];
+}
+
+.scrollable-list {
+ @apply max-h-[11rem] overflow-y-auto;
+}
+
+.scrollable-list::after {
+ content: '';
+ @apply absolute bottom-0 left-0 right-0 h-8 bg-gradient-to-b from-transparent to-yellow pointer-events-none;
+}
+
+.scrollable-list::-webkit-scrollbar {
+ width: 7px;
+}
+
+.scrollable-list::-webkit-scrollbar-thumb {
+ background-color: #05793c;
+ border-radius: 3px;
+}
+
+.scrollable-list::-webkit-scrollbar-track {
+ background-color: white;
+}
+
+.scrollable-list::-webkit-scrollbar-button {
+ display: none;
+}
+
+.grid-background {
+ background: #f1f1f1;
+ background-image: linear-gradient(0deg, #c1c1c1 1px, transparent 1px),
+ linear-gradient(90deg, #c1c1c1 1px, transparent 1px);
+ background-size: 145px 80px;
+}
+
+.marked-entry:hover {
+ border-radius: 0.8em 0.3em;
+ background: transparent;
+ background-image: linear-gradient(
+ to right,
+ rgba(95, 224, 157, 0.1),
+ rgb(95, 224, 157, 0.7) 4%,
+ rgb(95, 224, 157, 0.3)
+ );
+ -webkit-box-decoration-break: clone;
+ box-decoration-break: clone;
+}
+
+.marked-entry-card {
+ border-radius: 0.8em 0.3em;
+ background: transparent;
+ background-image: linear-gradient(
+ to right,
+ rgba(95, 224, 157, 0.1),
+ rgb(95, 224, 157, 0.7) 4%,
+ rgb(95, 224, 157, 0.3)
+ );
+ -webkit-box-decoration-break: clone;
+ box-decoration-break: clone;
+
+ clip-path: polygon(
+ 10px 0, /* top-left point shifted down by 20% of the height */
+ 100% 0, /* top-right point */
+ 100% 100%, /* bottom-right point */
+ 0 100% /* bottom-left point */
+ );
+}
+
+.text-border {
+ text-shadow:
+ -1px -1px 0 #05793C,
+ 1px -1px 0 #05793C,
+ -1px 1px 0 #05793C,
+ 1px 1px 0 #05793C;
+ }
\ No newline at end of file
diff --git a/application/next-frontend/tailwind.config.ts b/application/next-frontend/tailwind.config.ts
new file mode 100644
index 00000000..65e36e4f
--- /dev/null
+++ b/application/next-frontend/tailwind.config.ts
@@ -0,0 +1,56 @@
+import type { Config } from 'tailwindcss'
+
+const config: Config = {
+ content: [
+ './pages/**/*.{js,ts,jsx,tsx,mdx}',
+ './src/**/*.{js,ts,jsx,tsx,mdx}',
+ './src/app/**/*.{js,ts,jsx,tsx,mdx}',
+ ],
+ theme: {
+ extend: {
+ boxShadow: {
+ custom: '0px 32px 64px -14px rgba(106, 84, 18, 0.20), 12px 12px 0px 0px #117537',
+ },
+ backgroundImage: {
+ 'gradient-radial': 'radial-gradient(var(--tw-gradient-stops))',
+ 'gradient-conic': 'conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))',
+ },
+ fontFamily: {
+ spaceGrotesk: ['"Space Grotesk"', 'sans-serif'],
+ inter: ['Inter', 'sans-serif'],
+ workSans: ['"Work Sans"', 'sans-serif'],
+ queensMedium: ['"Queens Condensed Trial Medium"', 'sans-serif'],
+ queensRegular: ['"Queens Condensed Trial Regular"', 'sans-serif'],
+ },
+ colors: {
+ light: '#F1EFE9',
+ fuger: '#C1C1C1',
+ dark: '#303030',
+ green: {
+ primary: '#05793C',
+ secondary: '#5FE09D',
+ tertiary: '#003F1E',
+ quaternary: '#004B24',
+ light: '#CFF6E2',
+ calendar: '#EDFFF6',
+ },
+ yellow: '#FFF8C1',
+ shade: '#6A5412',
+ neutral: '#4E5445',
+ red: {
+ highlight: '#FF9494',
+ light: '#FFB9B9',
+ DEFAULT: '#CB4A4A',
+ },
+ postit: {
+ green: '#9AE59D',
+ yellow: '#F0E36F',
+ pink: '#F8B6B6',
+ },
+ },
+ },
+ plugins: [],
+ },
+}
+
+export default config
diff --git a/application/next-frontend/tsconfig.json b/application/next-frontend/tsconfig.json
new file mode 100644
index 00000000..1d73ccf8
--- /dev/null
+++ b/application/next-frontend/tsconfig.json
@@ -0,0 +1,26 @@
+{
+ "compilerOptions": {
+ "target": "es5",
+ "lib": ["dom", "dom.iterable", "esnext"],
+ "allowJs": true,
+ "skipLibCheck": true,
+ "strict": true,
+ "forceConsistentCasingInFileNames": true,
+ "noEmit": true,
+ "esModuleInterop": true,
+ "module": "esnext",
+ "moduleResolution": "bundler",
+ "resolveJsonModule": true,
+ "isolatedModules": true,
+ "jsx": "preserve",
+ "incremental": true,
+ "paths": {
+ "@/*": ["./src/*"],
+ "Admin/*": ["./src/Admin/*"],
+ "Landing/*": ["./src/Landing/*"],
+ "Shared/*": ["./src/Shared/*"],
+ }
+ },
+ "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],
+ "exclude": ["node_modules"]
+}
diff --git a/infrastructure/.terraformignore b/infrastructure/.terraformignore
deleted file mode 100644
index 43c1e44c..00000000
--- a/infrastructure/.terraformignore
+++ /dev/null
@@ -1,2 +0,0 @@
-../application/backend/venv/
-../application/frontend/node_modules/
\ No newline at end of file
diff --git a/infrastructure/main.tf b/infrastructure/main.tf
deleted file mode 100644
index ea7820f1..00000000
--- a/infrastructure/main.tf
+++ /dev/null
@@ -1,123 +0,0 @@
-resource "heroku_pipeline" "pizzabot" {
- name = var.prefix
-
- owner {
- id = var.heroku_team_id
- type = "team"
- }
-}
-
-
-module "production" {
- source = "./system"
-
- heroku_team_name = var.heroku_team_name
- hostname = "bot.blank.pizza"
- prefix = var.prefix
- environment = "prod"
- CLOUDAMQP_PLAN = "cloudamqp:tiger"
- PAPERTRAIL_PLAN = "papertrail:choklad"
- POSTGRES_PLAN = "heroku-postgresql:standard-0"
- FORMATION_SIZE_FRONTEND = "Basic"
- FORMATION_SIZE_BACKEND = "Basic"
- FORMATION_SIZE_BOT_WORKER = "Basic"
- FORMATION_QUANTITY_FRONTEND = 1
- FORMATION_QUANTITY_BACKEND = 1
- FORMATION_QUANTITY_BOT_WORKER = 1
- SLACK_APP_TOKEN = var.PRODUCTION_SLACK_APP_TOKEN
- SLACK_CLIENT_ID = var.PRODUCTION_SLACK_CLIENT_ID
- SLACK_CLIENT_SECRET = var.PRODUCTION_SLACK_CLIENT_SECRET
- SLACK_SIGNING_SECRET = var.PRODUCTION_SLACK_SIGNING_SECRET
- SECRET_KEY_BACKEND = var.PRODUCTION_SECRET_KEY_BACKEND
- CLOUDINARY_CLOUD_NAME = var.PRODUCTION_CLOUDINARY_CLOUD_NAME
- CLOUDINARY_API_KEY = var.PRODUCTION_CLOUDINARY_API_KEY
- CLOUDINARY_API_SECRET = var.PRODUCTION_CLOUDINARY_API_SECRET
- MQ_EVENT_KEY = "pizza"
- MQ_EVENT_QUEUE = "Pizza_Queue"
- MQ_EXCHANGE = "Pizza_Exchange"
- MQ_RPC_KEY = "rpc"
- DAYS_IN_ADVANCE_TO_INVITE = 10
- HOURS_BETWEEN_REMINDERS = 4
- REPLY_DEADLINE_IN_HOURS = 24
- FLASK_ENV = "production"
- BACKEND_URI = "api.bot.blank.pizza"
- FRONTEND_URI = "bot.blank.pizza"
-}
-
-# Add production apps to pipeline under production stage
-resource "heroku_pipeline_coupling" "production-backend" {
- app_id = module.production.app_backend_id
- pipeline = heroku_pipeline.pizzabot.id
- stage = "production"
-}
-
-# Add production apps to pipeline under production stage
-resource "heroku_pipeline_coupling" "production-bot" {
- app_id = module.production.app_bot_id
- pipeline = heroku_pipeline.pizzabot.id
- stage = "production"
-}
-
-# Add production apps to pipeline under production stage
-resource "heroku_pipeline_coupling" "production-frontend" {
- app_id = module.production.app_frontend_id
- pipeline = heroku_pipeline.pizzabot.id
- stage = "production"
-}
-
-/*
-module "staging" {
- source = "./system"
-
- heroku_team_name = var.heroku_team_name
- hostname = "staging.bot.blank.pizza"
- prefix = var.prefix
- environment = "stag"
- CLOUDAMQP_PLAN = "cloudamqp:lemur"
- PAPERTRAIL_PLAN = "papertrail:choklad"
- POSTGRES_PLAN = "heroku-postgresql:mini"
- FORMATION_SIZE_FRONTEND = "Basic"
- FORMATION_SIZE_BACKEND = "Basic"
- FORMATION_SIZE_BOT_WORKER = "Basic"
- FORMATION_QUANTITY_FRONTEND = 1
- FORMATION_QUANTITY_BACKEND = 1
- FORMATION_QUANTITY_BOT_WORKER = 1
- SLACK_APP_TOKEN = var.STAGING_SLACK_APP_TOKEN
- SLACK_CLIENT_ID: var.STAGING_SLACK_CLIENT_ID
- SLACK_CLIENT_SECRET: var.STAGING_SLACK_CLIENT_SECRET
- SLACK_SIGNING_SECRET: var.STAGING_SLACK_SIGNING_SECRET
- SECRET_KEY_BACKEND = var.STAGING_SECRET_KEY_BACKEND
- CLOUDINARY_CLOUD_NAME = var.STAGING_CLOUDINARY_CLOUD_NAME
- CLOUDINARY_API_KEY = var.STAGING_CLOUDINARY_API_KEY
- CLOUDINARY_API_SECRET = var.STAGING_CLOUDINARY_API_SECRET
- MQ_EVENT_KEY = "pizza"
- MQ_EVENT_QUEUE = "Pizza_Queue"
- MQ_EXCHANGE = "Pizza_Exchange"
- MQ_RPC_KEY = "rpc"
- DAYS_IN_ADVANCE_TO_INVITE = 10
- HOURS_BETWEEN_REMINDERS = 4
- REPLY_DEADLINE_IN_HOURS = 24
- FLASK_ENV = "production"
- BACKEND_URI = "staging.api.bot.blank.pizza"
- FRONTEND_URI = "staging.bot.blank.pizza"
-}
-
-# Add staging apps to pipeline under staging stage
-resource "heroku_pipeline_coupling" "staging-backend" {
- app_id = module.staging.app_backend_id
- pipeline = heroku_pipeline.pizzabot.id
- stage = "staging"
-}
-
-resource "heroku_pipeline_coupling" "staging-bot" {
- app_id = module.staging.app_bot_id
- pipeline = heroku_pipeline.pizzabot.id
- stage = "staging"
-}
-
-resource "heroku_pipeline_coupling" "staging-frontend" {
- app_id = module.staging.app_frontend_id
- pipeline = heroku_pipeline.pizzabot.id
- stage = "staging"
-}
-*/
diff --git a/infrastructure/.terraform.lock.hcl b/infrastructure/prod/.terraform.lock.hcl
similarity index 100%
rename from infrastructure/.terraform.lock.hcl
rename to infrastructure/prod/.terraform.lock.hcl
diff --git a/infrastructure/prod/.terraformignore b/infrastructure/prod/.terraformignore
new file mode 100644
index 00000000..d555fd4b
--- /dev/null
+++ b/infrastructure/prod/.terraformignore
@@ -0,0 +1,3 @@
+../../application/backend/venv/
+../../application/frontend/node_modules/
+../../application/next-frontend/node_modules/
\ No newline at end of file
diff --git a/infrastructure/prod/main.tf b/infrastructure/prod/main.tf
new file mode 100644
index 00000000..c72b3dd8
--- /dev/null
+++ b/infrastructure/prod/main.tf
@@ -0,0 +1,125 @@
+# resource "heroku_pipeline" "pizzabot" {
+# name = var.prefix
+
+# owner {
+# id = var.heroku_team_id
+# type = "team"
+# }
+# }
+
+
+module "production" {
+ source = "./system"
+
+ heroku_team_name = var.heroku_team_name
+ hostname = "www.pizzabot.app"
+ prefix = var.prefix
+ environment = "prod"
+ CLOUDAMQP_PLAN = "cloudamqp:tiger"
+ PAPERTRAIL_PLAN = "papertrail:choklad"
+ POSTGRES_PLAN = "heroku-postgresql:standard-0"
+ FORMATION_SIZE_FRONTEND = "Basic"
+ FORMATION_SIZE_BACKEND = "Basic"
+ FORMATION_SIZE_BOT_WORKER = "Basic"
+ FORMATION_QUANTITY_FRONTEND = 1
+ FORMATION_QUANTITY_BACKEND = 1
+ FORMATION_QUANTITY_BOT_WORKER = 1
+ SLACK_APP_TOKEN = var.PRODUCTION_SLACK_APP_TOKEN
+ SLACK_CLIENT_ID = var.PRODUCTION_SLACK_CLIENT_ID
+ SLACK_CLIENT_SECRET = var.PRODUCTION_SLACK_CLIENT_SECRET
+ SLACK_SIGNING_SECRET = var.PRODUCTION_SLACK_SIGNING_SECRET
+ SECRET_KEY_BACKEND = var.PRODUCTION_SECRET_KEY_BACKEND
+ CLOUDINARY_CLOUD_NAME = var.PRODUCTION_CLOUDINARY_CLOUD_NAME
+ CLOUDINARY_API_KEY = var.PRODUCTION_CLOUDINARY_API_KEY
+ CLOUDINARY_API_SECRET = var.PRODUCTION_CLOUDINARY_API_SECRET
+ MQ_EVENT_KEY = "pizza"
+ MQ_EVENT_QUEUE = "Pizza_Queue"
+ MQ_EXCHANGE = "Pizza_Exchange"
+ MQ_RPC_KEY = "rpc"
+ DAYS_IN_ADVANCE_TO_INVITE = 10
+ HOURS_BETWEEN_REMINDERS = 4
+ REPLY_DEADLINE_IN_HOURS = 24
+ FLASK_ENV = "production"
+ BACKEND_URI = "api.www.pizzabot.app"
+ NEXT_PUBLIC_BACKEND_URI = "https://api.www.pizzabot.app"
+ FRONTEND_URI = "www.pizzabot.app"
+}
+
+# Add production apps to pipeline under production stage
+resource "heroku_pipeline_coupling" "production-backend" {
+ app_id = module.production.app_backend_id
+ pipeline = var.EXISTING_PIPELINE_ID
+ stage = "production"
+}
+
+# Add production apps to pipeline under production stage
+resource "heroku_pipeline_coupling" "production-bot" {
+ app_id = module.production.app_bot_id
+ pipeline = var.EXISTING_PIPELINE_ID
+ stage = "production"
+}
+
+# Add production apps to pipeline under production stage
+resource "heroku_pipeline_coupling" "production-frontend" {
+ app_id = module.production.app_frontend_id
+ pipeline = var.EXISTING_PIPELINE_ID
+ stage = "production"
+}
+
+
+# module "staging" {
+# source = "./system"
+
+# heroku_team_name = var.heroku_team_name
+# hostname = "staging.pizzabot.app"
+# prefix = var.prefix
+# environment = "stag"
+# CLOUDAMQP_PLAN = "cloudamqp:lemur"
+# PAPERTRAIL_PLAN = "papertrail:choklad"
+# POSTGRES_PLAN = "heroku-postgresql:mini"
+# FORMATION_SIZE_FRONTEND = "Basic"
+# FORMATION_SIZE_BACKEND = "Basic"
+# FORMATION_SIZE_BOT_WORKER = "Basic"
+# FORMATION_QUANTITY_FRONTEND = 1
+# FORMATION_QUANTITY_BACKEND = 1
+# FORMATION_QUANTITY_BOT_WORKER = 1
+# SLACK_APP_TOKEN = var.STAGING_SLACK_APP_TOKEN
+# SLACK_CLIENT_ID: var.STAGING_SLACK_CLIENT_ID
+# SLACK_CLIENT_SECRET: var.STAGING_SLACK_CLIENT_SECRET
+# SLACK_SIGNING_SECRET: var.STAGING_SLACK_SIGNING_SECRET
+# SECRET_KEY_BACKEND = var.STAGING_SECRET_KEY_BACKEND
+# CLOUDINARY_CLOUD_NAME = var.STAGING_CLOUDINARY_CLOUD_NAME
+# CLOUDINARY_API_KEY = var.STAGING_CLOUDINARY_API_KEY
+# CLOUDINARY_API_SECRET = var.STAGING_CLOUDINARY_API_SECRET
+# MQ_EVENT_KEY = "pizza"
+# MQ_EVENT_QUEUE = "Pizza_Queue"
+# MQ_EXCHANGE = "Pizza_Exchange"
+# MQ_RPC_KEY = "rpc"
+# DAYS_IN_ADVANCE_TO_INVITE = 10
+# HOURS_BETWEEN_REMINDERS = 4
+# REPLY_DEADLINE_IN_HOURS = 24
+# FLASK_ENV = "production"
+# BACKEND_URI = "api.staging.pizzabot.app"
+# NEXT_PUBLIC_BACKEND_URI = "https://api.staging.pizzabot.app"
+# FRONTEND_URI = "staging.pizzabot.app"
+# }
+
+# #Add staging apps to pipeline under staging stage
+# resource "heroku_pipeline_coupling" "staging-backend" {
+# app_id = module.staging.app_backend_id
+# pipeline = heroku_pipeline.pizzabot.id
+# stage = "staging"
+# }
+
+# resource "heroku_pipeline_coupling" "staging-bot" {
+# app_id = module.staging.app_bot_id
+# pipeline = heroku_pipeline.pizzabot.id
+# stage = "staging"
+# }
+
+# resource "heroku_pipeline_coupling" "staging-frontend" {
+# app_id = module.staging.app_frontend_id
+# pipeline = heroku_pipeline.pizzabot.id
+# stage = "staging"
+# }
+
diff --git a/infrastructure/provider.tf b/infrastructure/prod/provider.tf
similarity index 100%
rename from infrastructure/provider.tf
rename to infrastructure/prod/provider.tf
diff --git a/infrastructure/system/main.tf b/infrastructure/prod/system/main.tf
similarity index 94%
rename from infrastructure/system/main.tf
rename to infrastructure/prod/system/main.tf
index 8e9daf28..30eb7578 100644
--- a/infrastructure/system/main.tf
+++ b/infrastructure/prod/system/main.tf
@@ -52,6 +52,7 @@ resource "heroku_app" "bot" {
"MQ_EVENT_KEY" = var.MQ_EVENT_KEY
"REPLY_DEADLINE_IN_HOURS" = var.REPLY_DEADLINE_IN_HOURS
"HOURS_BETWEEN_REMINDERS" = var.HOURS_BETWEEN_REMINDERS
+ "FRONTEND_URI" = "https://${var.FRONTEND_URI}"
}
sensitive_config_vars = {
@@ -67,6 +68,7 @@ resource "heroku_config" "endpoints" {
vars = {
FRONTEND_URI = "https://${var.FRONTEND_URI}"
BACKEND_URI = var.BACKEND_URI
+ DOMAIN = var.FRONTEND_URI
}
}
@@ -89,7 +91,7 @@ resource "heroku_app" "frontend" {
}
config_vars = {
- "NGINX_DEFAULT_REQUEST" = "index.html"
+ "NEXT_PUBLIC_BACKEND_URI" = var.NEXT_PUBLIC_BACKEND_URI
}
}
@@ -101,7 +103,7 @@ resource "heroku_build" "backend" {
]
source {
- path = "../application/backend"
+ path = "../../application/backend"
}
}
@@ -113,16 +115,15 @@ resource "heroku_build" "bot" {
]
source {
- path = "../application/bot"
+ path = "../../application/bot"
}
}
resource "heroku_build" "frontend" {
app_id = heroku_app.frontend.id
- buildpacks = ["https://github.com/dokku/heroku-buildpack-nginx"]
source {
- path = "../application/frontend/public"
+ path = "../../application/next-frontend"
}
}
diff --git a/infrastructure/system/outputs.tf b/infrastructure/prod/system/outputs.tf
similarity index 100%
rename from infrastructure/system/outputs.tf
rename to infrastructure/prod/system/outputs.tf
diff --git a/infrastructure/system/variables.tf b/infrastructure/prod/system/variables.tf
similarity index 96%
rename from infrastructure/system/variables.tf
rename to infrastructure/prod/system/variables.tf
index 72274340..e332f58a 100644
--- a/infrastructure/system/variables.tf
+++ b/infrastructure/prod/system/variables.tf
@@ -98,6 +98,10 @@ variable "BACKEND_URI" {
type = string
}
+variable "NEXT_PUBLIC_BACKEND_URI" {
+ type = string
+}
+
variable "FORMATION_SIZE_FRONTEND" {
type = string
}
diff --git a/infrastructure/system/versions.tf b/infrastructure/prod/system/versions.tf
similarity index 100%
rename from infrastructure/system/versions.tf
rename to infrastructure/prod/system/versions.tf
diff --git a/infrastructure/variables.tf b/infrastructure/prod/variables.tf
similarity index 55%
rename from infrastructure/variables.tf
rename to infrastructure/prod/variables.tf
index ecb2cee7..e934d7fd 100644
--- a/infrastructure/variables.tf
+++ b/infrastructure/prod/variables.tf
@@ -1,7 +1,7 @@
variable "prefix" {
description = "High-level name of this configuration, used as a resource name prefix"
type = string
- default = "pizzabot-v2"
+ default = "pizzabot-v3"
}
variable "heroku_api_key" {
@@ -21,6 +21,7 @@ variable "heroku_team_name" {
}
# ************* PRODUCTION ************* #
+
variable "PRODUCTION_SLACK_APP_TOKEN" {
type = string
}
@@ -53,37 +54,44 @@ variable "PRODUCTION_CLOUDINARY_API_SECRET" {
type = string
}
-# ************* STAGING ************* #
-/*
-variable "STAGING_SLACK_APP_TOKEN" {
+variable "EXISTING_PIPELINE_ID" {
type = string
+ description = "As we have two different folders thats connected to the same pipeline we add the pipeline Id manualy"
+ default = "ca5f3b56-618c-4e63-9284-39029d096782"
}
-variable "STAGING_SLACK_CLIENT_ID" {
- type = string
-}
-variable "STAGING_SLACK_CLIENT_SECRET" {
- type = string
-}
+# ************* STAGING ************* #
-variable "STAGING_SLACK_SIGNING_SECRET" {
- type = string
-}
+# variable "STAGING_SLACK_APP_TOKEN" {
+# type = string
+# }
-variable "STAGING_SECRET_KEY_BACKEND" {
- type = string
-}
+# variable "STAGING_SLACK_CLIENT_ID" {
+# type = string
+# }
-variable "STAGING_CLOUDINARY_CLOUD_NAME" {
- type = string
-}
+# variable "STAGING_SLACK_CLIENT_SECRET" {
+# type = string
+# }
-variable "STAGING_CLOUDINARY_API_KEY" {
- type = string
-}
+# variable "STAGING_SLACK_SIGNING_SECRET" {
+# type = string
+# }
+
+# variable "STAGING_SECRET_KEY_BACKEND" {
+# type = string
+# }
+
+# variable "STAGING_CLOUDINARY_CLOUD_NAME" {
+# type = string
+# }
+
+# variable "STAGING_CLOUDINARY_API_KEY" {
+# type = string
+# }
+
+# variable "STAGING_CLOUDINARY_API_SECRET" {
+# type = string
+# }
-variable "STAGING_CLOUDINARY_API_SECRET" {
- type = string
-}
-*/
diff --git a/infrastructure/versions.tf b/infrastructure/prod/versions.tf
similarity index 100%
rename from infrastructure/versions.tf
rename to infrastructure/prod/versions.tf
diff --git a/infrastructure/staging/.terraform.lock.hcl b/infrastructure/staging/.terraform.lock.hcl
new file mode 100644
index 00000000..12c8348f
--- /dev/null
+++ b/infrastructure/staging/.terraform.lock.hcl
@@ -0,0 +1,70 @@
+# This file is maintained automatically by "terraform init".
+# Manual edits may be lost in future updates.
+
+provider "registry.terraform.io/davidji99/herokux" {
+ version = "1.0.0"
+ hashes = [
+ "h1:ld/UkVsNNoND4vljbgf5dUIGklUvd0o0oUwrQfDv+Do=",
+ "zh:03a553b7e92de95a0507c838d067338680e97d14fda415ec721a4407040c24a2",
+ "zh:0979ce32e575a31c88d37435ef573c4a03017bd151fc327617feee2a33fdffdd",
+ "zh:09abc9a3a16d1eb5731e7dfd2705f832ebcb0732edbddfe4b82efee8ab030880",
+ "zh:241a3eca7a811f9c68fabf62709b9061829b2fa2e7854fea8f32c4d58087afc1",
+ "zh:26da170a4329773768d238a5e7fd54521454b99eb66f72bda611365a1d747cd8",
+ "zh:284379ad26b5c0628af66a4721696b7c10205c6427db7fe2b30a8779f088f037",
+ "zh:2929e30d6cf5296c55a452dd15249ec37e00a478da0c1abb800388d0f13107f8",
+ "zh:295820c66e999321eb779903b3ada055b85ed96931e93a9bf5308c6d6c9fc2a7",
+ "zh:2fb7872f03a6038a65c7bc509b48654bfa7a901d108415e5ab3901c29debeeb3",
+ "zh:3c40811bd00ab0474bdde1ba5744bad851deca6e638f6e301c14610077f58448",
+ "zh:525bfd2d19ba4bbf103a8f527126e264b9f5cb701c777d98834599bdb118916d",
+ "zh:6324dbdeaa57f21c916ea96b3386f02687e0ba5a152c8f2223e1c5640576c8dc",
+ "zh:6c74be9bcdb567faffaed2d17d575e983850eccb2c4cc16470f5e10588287948",
+ "zh:80fca4b571b6ffba0658b6f6d0e7b6b779691b9e4079d82de452c2a9654e99ae",
+ "zh:a3fca1a86e9b519a34786b70fca56970604bea8141c10e988b94576952470ff8",
+ "zh:b6739600bd9d83daa32f9c4f80ba68262cf4e622ecaec181d70347c5b7d5eeeb",
+ "zh:bbb17b666cf8aa1466ea2c40ef89fd452b7d20112c0640d0f04b83ce830134cc",
+ "zh:c4f42923ac80b47be41fd5bc265a7894518a3f8e14f2b24554d3b6f218cada05",
+ "zh:f329bdde33d41eb28b32a0fa17826740c06f998fb2f1dcfa1c8f2ce5de5438b3",
+ ]
+}
+
+provider "registry.terraform.io/hashicorp/external" {
+ version = "2.2.2"
+ hashes = [
+ "h1:BKQ5f5ijzeyBSnUr+j0wUi+bYv6KBQVQNDXNRVEcfJE=",
+ "zh:0b84ab0af2e28606e9c0c1289343949339221c3ab126616b831ddb5aaef5f5ca",
+ "zh:10cf5c9b9524ca2e4302bf02368dc6aac29fb50aeaa6f7758cce9aa36ae87a28",
+ "zh:56a016ee871c8501acb3f2ee3b51592ad7c3871a1757b098838349b17762ba6b",
+ "zh:719d6ef39c50e4cffc67aa67d74d195adaf42afcf62beab132dafdb500347d39",
+ "zh:78d5eefdd9e494defcb3c68d282b8f96630502cac21d1ea161f53cfe9bb483b3",
+ "zh:7fbfc4d37435ac2f717b0316f872f558f608596b389b895fcb549f118462d327",
+ "zh:8ac71408204db606ce63fe8f9aeaf1ddc7751d57d586ec421e62d440c402e955",
+ "zh:a4cacdb06f114454b6ed0033add28006afa3f65a0ea7a43befe45fc82e6809fb",
+ "zh:bb5ce3132b52ae32b6cc005bc9f7627b95259b9ffe556de4dad60d47d47f21f0",
+ "zh:bb60d2976f125ffd232a7ccb4b3f81e7109578b23c9c6179f13a11d125dca82a",
+ "zh:f9540ecd2e056d6e71b9ea5f5a5cf8f63dd5c25394b9db831083a9d4ea99b372",
+ "zh:ffd998b55b8a64d4335a090b6956b4bf8855b290f7554dd38db3302de9c41809",
+ ]
+}
+
+provider "registry.terraform.io/heroku/heroku" {
+ version = "5.1.2"
+ hashes = [
+ "h1:pjf0KADkGu7Nea5xPwKqKCGim+WxX9uQMXnwEhj18ZI=",
+ "zh:0a1dfe80eda8bd1621ebbbcf33cb475cfd20c685eaa0d23f1652d7d3c0f69b81",
+ "zh:1cadc7aa73579f50bd32e222f93a486b9fdc8e2fab7be7fcb92fb31ff217358d",
+ "zh:2cd692fd2a563b685fa91e29435351f6c8fe3f389a8482a1755c3d4f7a09809c",
+ "zh:41d8fc28e59e36870fc66767cbc271b2209e85714ad934ac271402ef26a30791",
+ "zh:67775fdc050dea3b191fd74a3be7a2857de70ef4690bf75b960e7c2bbb0ffe8d",
+ "zh:67f204d5c845e4fe5f94b51998f7be72bdb85cbb8e40b7d5bd799d6e74826a81",
+ "zh:7ec05e2eb1aed844b8c9c5151bc8012f546de14c16579f42735a12efe1db7e7d",
+ "zh:8d70482573d44fa553594d4262df96e6565b31ff5eae8267d93a7ee9c9052fab",
+ "zh:a8d445c9204b08f60682a134b5b76b3641634ed50c06eb4f6b38a7eb7c4026dc",
+ "zh:c00c0f220bfc1112a27fd028119249acacd814b6c964ed6ed14bbfe24e004ee7",
+ "zh:d0327c73e46fdfea9ae626cf057940af0ffa49bf16a492a3acced60aa9789526",
+ "zh:d540a68e1bed9bcb8ab1d56de43ff3afc60f9ea186d37bdcd2a60eb272d246e2",
+ "zh:df2065ca1a7b42f69080f3db8b4bbfaaacda7e4765a001ab2636e8883f72c535",
+ "zh:df8433d56b1944b63e0285943d54e3fbed5bfa52fd8314cd6cf4856f8f5de038",
+ "zh:fd50c1fd24007462ffeb75fdc04e046935f2b6dff35b678ebda14f16e9a7bc87",
+ "zh:fd9d4360276cda61f6d059ef66f8615280100f6e9601f5c5ff269b41c85d8440",
+ ]
+}
diff --git a/infrastructure/staging/.terraformignore b/infrastructure/staging/.terraformignore
new file mode 100644
index 00000000..d555fd4b
--- /dev/null
+++ b/infrastructure/staging/.terraformignore
@@ -0,0 +1,3 @@
+../../application/backend/venv/
+../../application/frontend/node_modules/
+../../application/next-frontend/node_modules/
\ No newline at end of file
diff --git a/infrastructure/staging/main.tf b/infrastructure/staging/main.tf
new file mode 100644
index 00000000..8a50c502
--- /dev/null
+++ b/infrastructure/staging/main.tf
@@ -0,0 +1,125 @@
+# resource "heroku_pipeline" "pizzabot" {
+# name = var.prefix
+
+# owner {
+# id = var.heroku_team_id
+# type = "team"
+# }
+# }
+
+
+# module "production" {
+# source = "./system"
+
+# heroku_team_name = var.heroku_team_name
+# hostname = "www.pizzabot.app"
+# prefix = var.prefix
+# environment = "prod"
+# CLOUDAMQP_PLAN = "cloudamqp:tiger"
+# PAPERTRAIL_PLAN = "papertrail:choklad"
+# POSTGRES_PLAN = "heroku-postgresql:standard-0"
+# FORMATION_SIZE_FRONTEND = "Basic"
+# FORMATION_SIZE_BACKEND = "Basic"
+# FORMATION_SIZE_BOT_WORKER = "Basic"
+# FORMATION_QUANTITY_FRONTEND = 1
+# FORMATION_QUANTITY_BACKEND = 1
+# FORMATION_QUANTITY_BOT_WORKER = 1
+# SLACK_APP_TOKEN = var.PRODUCTION_SLACK_APP_TOKEN
+# SLACK_CLIENT_ID = var.PRODUCTION_SLACK_CLIENT_ID
+# SLACK_CLIENT_SECRET = var.PRODUCTION_SLACK_CLIENT_SECRET
+# SLACK_SIGNING_SECRET = var.PRODUCTION_SLACK_SIGNING_SECRET
+# SECRET_KEY_BACKEND = var.PRODUCTION_SECRET_KEY_BACKEND
+# CLOUDINARY_CLOUD_NAME = var.PRODUCTION_CLOUDINARY_CLOUD_NAME
+# CLOUDINARY_API_KEY = var.PRODUCTION_CLOUDINARY_API_KEY
+# CLOUDINARY_API_SECRET = var.PRODUCTION_CLOUDINARY_API_SECRET
+# MQ_EVENT_KEY = "pizza"
+# MQ_EVENT_QUEUE = "Pizza_Queue"
+# MQ_EXCHANGE = "Pizza_Exchange"
+# MQ_RPC_KEY = "rpc"
+# DAYS_IN_ADVANCE_TO_INVITE = 10
+# HOURS_BETWEEN_REMINDERS = 4
+# REPLY_DEADLINE_IN_HOURS = 24
+# FLASK_ENV = "production"
+# BACKEND_URI = "api.www.pizzabot.app"
+# NEXT_PUBLIC_BACKEND_URI = "https://api.www.pizzabot.app"
+# FRONTEND_URI = "www.pizzabot.app"
+# }
+
+# # Add production apps to pipeline under production stage
+# resource "heroku_pipeline_coupling" "production-backend" {
+# app_id = module.production.app_backend_id
+# pipeline = heroku_pipeline.pizzabot.id
+# stage = "production"
+# }
+
+# # Add production apps to pipeline under production stage
+# resource "heroku_pipeline_coupling" "production-bot" {
+# app_id = module.production.app_bot_id
+# pipeline = heroku_pipeline.pizzabot.id
+# stage = "production"
+# }
+
+# # Add production apps to pipeline under production stage
+# resource "heroku_pipeline_coupling" "production-frontend" {
+# app_id = module.production.app_frontend_id
+# pipeline = heroku_pipeline.pizzabot.id
+# stage = "production"
+# }
+
+
+module "staging" {
+ source = "./system"
+
+ heroku_team_name = var.heroku_team_name
+ hostname = "staging.pizzabot.app"
+ prefix = var.prefix
+ environment = "stag"
+ CLOUDAMQP_PLAN = "cloudamqp:lemur"
+ PAPERTRAIL_PLAN = "papertrail:choklad"
+ POSTGRES_PLAN = "heroku-postgresql:mini"
+ FORMATION_SIZE_FRONTEND = "Basic"
+ FORMATION_SIZE_BACKEND = "Basic"
+ FORMATION_SIZE_BOT_WORKER = "Basic"
+ FORMATION_QUANTITY_FRONTEND = 1
+ FORMATION_QUANTITY_BACKEND = 1
+ FORMATION_QUANTITY_BOT_WORKER = 1
+ SLACK_APP_TOKEN = var.STAGING_SLACK_APP_TOKEN
+ SLACK_CLIENT_ID = var.STAGING_SLACK_CLIENT_ID
+ SLACK_CLIENT_SECRET = var.STAGING_SLACK_CLIENT_SECRET
+ SLACK_SIGNING_SECRET = var.STAGING_SLACK_SIGNING_SECRET
+ SECRET_KEY_BACKEND = var.STAGING_SECRET_KEY_BACKEND
+ CLOUDINARY_CLOUD_NAME = var.STAGING_CLOUDINARY_CLOUD_NAME
+ CLOUDINARY_API_KEY = var.STAGING_CLOUDINARY_API_KEY
+ CLOUDINARY_API_SECRET = var.STAGING_CLOUDINARY_API_SECRET
+ MQ_EVENT_KEY = "pizza"
+ MQ_EVENT_QUEUE = "Pizza_Queue"
+ MQ_EXCHANGE = "Pizza_Exchange"
+ MQ_RPC_KEY = "rpc"
+ DAYS_IN_ADVANCE_TO_INVITE = 10
+ HOURS_BETWEEN_REMINDERS = 4
+ REPLY_DEADLINE_IN_HOURS = 24
+ FLASK_ENV = "production"
+ BACKEND_URI = "api.staging.pizzabot.app"
+ NEXT_PUBLIC_BACKEND_URI = "https://api.staging.pizzabot.app"
+ FRONTEND_URI = "staging.pizzabot.app"
+}
+
+#Add staging apps to pipeline under staging stage
+resource "heroku_pipeline_coupling" "staging-backend" {
+ app_id = module.staging.app_backend_id
+ pipeline = var.EXISTING_PIPELINE_ID
+ stage = "staging"
+}
+
+resource "heroku_pipeline_coupling" "staging-bot" {
+ app_id = module.staging.app_bot_id
+ pipeline = var.EXISTING_PIPELINE_ID
+ stage = "staging"
+}
+
+resource "heroku_pipeline_coupling" "staging-frontend" {
+ app_id = module.staging.app_frontend_id
+ pipeline = var.EXISTING_PIPELINE_ID
+ stage = "staging"
+}
+
diff --git a/infrastructure/staging/provider.tf b/infrastructure/staging/provider.tf
new file mode 100644
index 00000000..363154e2
--- /dev/null
+++ b/infrastructure/staging/provider.tf
@@ -0,0 +1,8 @@
+provider "heroku" {
+ email = var.heroku_api_email
+ api_key = var.heroku_api_key
+}
+
+provider "herokux" {
+ api_key = var.heroku_api_key
+}
diff --git a/infrastructure/staging/system/main.tf b/infrastructure/staging/system/main.tf
new file mode 100644
index 00000000..30eb7578
--- /dev/null
+++ b/infrastructure/staging/system/main.tf
@@ -0,0 +1,193 @@
+resource "heroku_domain" "blank" {
+ app_id = heroku_app.frontend.id
+ hostname = var.hostname
+}
+
+resource "heroku_domain" "blank_backend" {
+ app_id = heroku_app.backend.id
+ hostname = var.BACKEND_URI
+}
+
+resource "heroku_app" "backend" {
+ name = "${var.prefix}-${var.environment}-backend"
+ region = "eu"
+ stack = "heroku-22"
+
+ organization {
+ name = var.heroku_team_name
+ }
+
+ config_vars = {
+ "MQ_EXCHANGE" = var.MQ_EXCHANGE
+ "MQ_EVENT_QUEUE" = var.MQ_EVENT_QUEUE
+ "MQ_RPC_KEY" = var.MQ_RPC_KEY
+ "MQ_EVENT_KEY" = var.MQ_EVENT_KEY
+ "DAYS_IN_ADVANCE_TO_INVITE" = var.DAYS_IN_ADVANCE_TO_INVITE
+ "FLASK_ENV" = var.FLASK_ENV
+ }
+
+ sensitive_config_vars = {
+ "SECRET_KEY" = var.SECRET_KEY_BACKEND
+ "SLACK_CLIENT_ID" = var.SLACK_CLIENT_ID
+ "SLACK_CLIENT_SECRET" = var.SLACK_CLIENT_SECRET
+ "CLOUDINARY_CLOUD_NAME" = var.CLOUDINARY_CLOUD_NAME
+ "CLOUDINARY_API_KEY" = var.CLOUDINARY_API_KEY
+ "CLOUDINARY_API_SECRET" = var.CLOUDINARY_API_SECRET
+ }
+}
+
+resource "heroku_app" "bot" {
+ name = "${var.prefix}-${var.environment}-bot"
+ region = "eu"
+ stack = "heroku-22"
+
+ organization {
+ name = var.heroku_team_name
+ }
+
+ config_vars = {
+ "MQ_EXCHANGE" = var.MQ_EXCHANGE
+ "MQ_EVENT_QUEUE" = var.MQ_EVENT_QUEUE
+ "MQ_RPC_KEY" = var.MQ_RPC_KEY
+ "MQ_EVENT_KEY" = var.MQ_EVENT_KEY
+ "REPLY_DEADLINE_IN_HOURS" = var.REPLY_DEADLINE_IN_HOURS
+ "HOURS_BETWEEN_REMINDERS" = var.HOURS_BETWEEN_REMINDERS
+ "FRONTEND_URI" = "https://${var.FRONTEND_URI}"
+ }
+
+ sensitive_config_vars = {
+ "SLACK_APP_TOKEN" = var.SLACK_APP_TOKEN
+ "SLACK_SIGNING_SECRET": var.SLACK_SIGNING_SECRET,
+ "SLACK_CLIENT_ID": var.SLACK_CLIENT_ID
+ "SLACK_CLIENT_SECRET": var.SLACK_CLIENT_SECRET
+ }
+}
+
+# NB: FRONTEND_URI must be set to the ssl custom domain "https://${var.hostname}" for the OAuth to work
+resource "heroku_config" "endpoints" {
+ vars = {
+ FRONTEND_URI = "https://${var.FRONTEND_URI}"
+ BACKEND_URI = var.BACKEND_URI
+ DOMAIN = var.FRONTEND_URI
+ }
+}
+
+resource "heroku_app_config_association" "config_backend_association" {
+ app_id = heroku_app.backend.id
+
+ vars = heroku_config.endpoints.vars
+ sensitive_vars = heroku_config.endpoints.sensitive_vars
+}
+
+resource "heroku_app" "frontend" {
+ name = "${var.prefix}-${var.environment}-frontend"
+ region = "eu"
+ stack = "heroku-22"
+
+ acm = true
+
+ organization {
+ name = var.heroku_team_name
+ }
+
+ config_vars = {
+ "NEXT_PUBLIC_BACKEND_URI" = var.NEXT_PUBLIC_BACKEND_URI
+ }
+}
+
+resource "heroku_build" "backend" {
+ app_id = heroku_app.backend.id
+ buildpacks = [
+ "https://github.com/heroku/heroku-buildpack-locale",
+ "https://github.com/heroku/heroku-buildpack-python"
+ ]
+
+ source {
+ path = "../../application/backend"
+ }
+}
+
+resource "heroku_build" "bot" {
+ app_id = heroku_app.bot.id
+ buildpacks = [
+ "https://github.com/heroku/heroku-buildpack-locale",
+ "https://github.com/heroku/heroku-buildpack-python"
+ ]
+
+ source {
+ path = "../../application/bot"
+ }
+}
+
+resource "heroku_build" "frontend" {
+ app_id = heroku_app.frontend.id
+
+ source {
+ path = "../../application/next-frontend"
+ }
+}
+
+resource "heroku_addon" "cloudamqp-backend" {
+ name = "${var.prefix}-${var.environment}-cloudamqp"
+ app_id = heroku_app.backend.id
+ plan = var.CLOUDAMQP_PLAN
+}
+
+resource "heroku_addon_attachment" "cloudamqp-bot" {
+ app_id = heroku_app.bot.id
+ addon_id = heroku_addon.cloudamqp-backend.id
+}
+
+resource "heroku_addon" "papertrail-backend" {
+ name = "${var.prefix}-${var.environment}-papertrail"
+ app_id = heroku_app.backend.id
+ plan = var.PAPERTRAIL_PLAN
+}
+
+resource "heroku_addon_attachment" "papertrail-bot" {
+ app_id = heroku_app.bot.id
+ addon_id = heroku_addon.papertrail-backend.id
+}
+
+# This creates the environment variable DATABASE_URL that we can use in our heroku_app
+resource "heroku_addon" "database" {
+ name = "${var.prefix}-${var.environment}-database"
+ app_id = heroku_app.backend.id
+ plan = var.POSTGRES_PLAN
+}
+
+# Needed to attach the database to a second app
+resource "heroku_addon_attachment" "database-attachment" {
+ app_id = heroku_app.bot.id
+ addon_id = heroku_addon.database.id
+}
+
+#resource "herokux_postgres_backup_schedule" "database_backup" {
+# postgres_id = heroku_addon.database.id
+# hour = 23
+# timezone = "Europe/Oslo"
+#}
+
+resource "heroku_formation" "formation-backend" {
+ app_id = heroku_app.backend.id
+ type = "web"
+ quantity = var.FORMATION_QUANTITY_BACKEND
+ size = var.FORMATION_SIZE_BACKEND
+ depends_on = [heroku_build.backend]
+}
+
+resource "heroku_formation" "formation-bot-worker" {
+ app_id = heroku_app.bot.id
+ type = "worker"
+ quantity = var.FORMATION_QUANTITY_BOT_WORKER
+ size = var.FORMATION_SIZE_BOT_WORKER
+ depends_on = [heroku_build.bot]
+}
+
+resource "heroku_formation" "formation-frontend" {
+ app_id = heroku_app.frontend.id
+ type = "web"
+ quantity = var.FORMATION_QUANTITY_FRONTEND
+ size = var.FORMATION_SIZE_FRONTEND
+ depends_on = [heroku_build.frontend]
+}
diff --git a/infrastructure/staging/system/outputs.tf b/infrastructure/staging/system/outputs.tf
new file mode 100644
index 00000000..fa9ecc2b
--- /dev/null
+++ b/infrastructure/staging/system/outputs.tf
@@ -0,0 +1,24 @@
+output "backend_app_url" {
+ value = heroku_app.backend.web_url
+ description = "Backend application URL"
+}
+
+output "bot_app_url" {
+ value = heroku_app.bot.web_url
+ description = "Backend application URL"
+}
+
+output "app_backend_id" {
+ value = heroku_app.backend.id
+ description = "Backend app id"
+}
+
+output "app_bot_id" {
+ value = heroku_app.bot.id
+ description = "Bot app id"
+}
+
+output "app_frontend_id" {
+ value = heroku_app.frontend.id
+ description = "Frontend app id"
+}
\ No newline at end of file
diff --git a/infrastructure/staging/system/variables.tf b/infrastructure/staging/system/variables.tf
new file mode 100644
index 00000000..e332f58a
--- /dev/null
+++ b/infrastructure/staging/system/variables.tf
@@ -0,0 +1,128 @@
+variable "heroku_team_name" {
+ type = string
+}
+
+variable "hostname" {
+ type = string
+}
+
+variable "prefix" {
+ type = string
+}
+
+variable "environment" {
+ type = string
+}
+
+variable "MQ_EXCHANGE" {
+ type = string
+}
+
+variable "MQ_EVENT_QUEUE" {
+ type = string
+}
+
+variable "MQ_RPC_KEY" {
+ type = string
+}
+
+variable "MQ_EVENT_KEY" {
+ type = string
+}
+
+variable "DAYS_IN_ADVANCE_TO_INVITE" {
+ type = number
+}
+
+variable "REPLY_DEADLINE_IN_HOURS" {
+ type = number
+}
+
+variable "HOURS_BETWEEN_REMINDERS" {
+ type = number
+}
+
+variable "SLACK_APP_TOKEN" {
+ type = string
+}
+
+variable "SLACK_CLIENT_ID" {
+ type = string
+}
+
+variable "SLACK_CLIENT_SECRET" {
+ type = string
+}
+
+variable "SLACK_SIGNING_SECRET" {
+ type = string
+}
+
+variable "SECRET_KEY_BACKEND" {
+ type = string
+}
+
+variable "CLOUDINARY_CLOUD_NAME" {
+ type = string
+}
+
+variable "CLOUDINARY_API_KEY" {
+ type = string
+}
+
+variable "CLOUDINARY_API_SECRET" {
+ type = string
+}
+
+variable "PAPERTRAIL_PLAN" {
+ type = string
+}
+
+variable "CLOUDAMQP_PLAN" {
+ type = string
+}
+
+variable "POSTGRES_PLAN" {
+ type = string
+}
+
+variable "FLASK_ENV" {
+ type = string
+}
+
+variable "FRONTEND_URI" {
+ type = string
+}
+
+variable "BACKEND_URI" {
+ type = string
+}
+
+variable "NEXT_PUBLIC_BACKEND_URI" {
+ type = string
+}
+
+variable "FORMATION_SIZE_FRONTEND" {
+ type = string
+}
+
+variable "FORMATION_SIZE_BACKEND" {
+ type = string
+}
+
+variable "FORMATION_SIZE_BOT_WORKER" {
+ type = string
+}
+
+variable "FORMATION_QUANTITY_FRONTEND" {
+ type = string
+}
+
+variable "FORMATION_QUANTITY_BACKEND" {
+ type = string
+}
+
+variable "FORMATION_QUANTITY_BOT_WORKER" {
+ type = string
+}
+
diff --git a/infrastructure/staging/system/versions.tf b/infrastructure/staging/system/versions.tf
new file mode 100644
index 00000000..9a8886ba
--- /dev/null
+++ b/infrastructure/staging/system/versions.tf
@@ -0,0 +1,11 @@
+terraform {
+ required_providers {
+ heroku = {
+ source = "heroku/heroku"
+ }
+ herokux = {
+ source = "davidji99/herokux"
+ }
+ }
+ required_version = ">= 0.13"
+}
diff --git a/infrastructure/staging/variables.tf b/infrastructure/staging/variables.tf
new file mode 100644
index 00000000..5f3b37fd
--- /dev/null
+++ b/infrastructure/staging/variables.tf
@@ -0,0 +1,94 @@
+variable "prefix" {
+ description = "High-level name of this configuration, used as a resource name prefix"
+ type = string
+ default = "pizzabot-v3"
+}
+
+variable "heroku_api_key" {
+ type = string
+}
+
+variable "heroku_api_email" {
+ type = string
+}
+
+variable "heroku_team_id" {
+ type = string
+}
+
+variable "heroku_team_name" {
+ type = string
+}
+
+# ************* PRODUCTION ************* #
+# variable "PRODUCTION_SLACK_APP_TOKEN" {
+# type = string
+# }
+
+# variable "PRODUCTION_SLACK_CLIENT_ID" {
+# type = string
+# }
+
+# variable "PRODUCTION_SLACK_CLIENT_SECRET" {
+# type = string
+# }
+
+# variable "PRODUCTION_SLACK_SIGNING_SECRET" {
+# type = string
+# }
+
+# variable "PRODUCTION_SECRET_KEY_BACKEND" {
+# type = string
+# }
+
+# variable "PRODUCTION_CLOUDINARY_CLOUD_NAME" {
+# type = string
+# }
+
+# variable "PRODUCTION_CLOUDINARY_API_KEY" {
+# type = string
+# }
+
+# variable "PRODUCTION_CLOUDINARY_API_SECRET" {
+# type = string
+# }
+
+# ************* STAGING ************* #
+
+variable "STAGING_SLACK_APP_TOKEN" {
+ type = string
+}
+
+variable "STAGING_SLACK_CLIENT_ID" {
+ type = string
+}
+
+variable "STAGING_SLACK_CLIENT_SECRET" {
+ type = string
+}
+
+variable "STAGING_SLACK_SIGNING_SECRET" {
+ type = string
+}
+
+variable "STAGING_SECRET_KEY_BACKEND" {
+ type = string
+}
+
+variable "STAGING_CLOUDINARY_CLOUD_NAME" {
+ type = string
+}
+
+variable "STAGING_CLOUDINARY_API_KEY" {
+ type = string
+}
+
+variable "STAGING_CLOUDINARY_API_SECRET" {
+ type = string
+}
+
+variable "EXISTING_PIPELINE_ID" {
+ type = string
+ description = "As the apps pushed to production is already created under an existing pipeline, we have to reference this pipeline so we dont have to create another"
+ default = "ca5f3b56-618c-4e63-9284-39029d096782"
+}
diff --git a/infrastructure/staging/versions.tf b/infrastructure/staging/versions.tf
new file mode 100644
index 00000000..9a8886ba
--- /dev/null
+++ b/infrastructure/staging/versions.tf
@@ -0,0 +1,11 @@
+terraform {
+ required_providers {
+ heroku = {
+ source = "heroku/heroku"
+ }
+ herokux = {
+ source = "davidji99/herokux"
+ }
+ }
+ required_version = ">= 0.13"
+}