diff --git a/.dockerignore b/.dockerignore index b457b49..e2ce317 100644 --- a/.dockerignore +++ b/.dockerignore @@ -9,6 +9,7 @@ env/ exports/ .editorconfig +.env .env.example .gitignore .markdownlint.json diff --git a/.editorconfig b/.editorconfig index 124e6bb..3ae43fb 100644 --- a/.editorconfig +++ b/.editorconfig @@ -19,3 +19,6 @@ indent_size = 2 [*.{toml,yml,yaml,json,ini}] indent_size = 2 + +[site.webmanifest] +indent_size = 2 diff --git a/.env.example b/.env.example index 5ca6e68..b54820a 100644 --- a/.env.example +++ b/.env.example @@ -1,12 +1,4 @@ -HOST=0.0.0.0 -PORT=5000 - -FLASK_APP=bereal/server.py -FLASK_ENV=development - -SECRET_KEY=hello - -HOST=redis +HOST=localhost PORT=5000 REDIS_HOST=redis diff --git a/.gitignore b/.gitignore index 792fe70..ba9d2c4 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,7 @@ *.db *.rdb +*.log* content/**/** !content/.gitkeep diff --git a/.vscode/ltex.dictionary.en-US.txt b/.vscode/ltex.dictionary.en-US.txt index 8947502..ebba31e 100644 --- a/.vscode/ltex.dictionary.en-US.txt +++ b/.vscode/ltex.dictionary.en-US.txt @@ -27,3 +27,4 @@ processForm otp localStorage getItem +ubc diff --git a/.vscode/settings.json b/.vscode/settings.json index 42cc7ca..c893cf4 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -10,14 +10,15 @@ "*.mako": "python", "*.conf": "ini", "*.rdb": "sql", - "*.local": "plaintext" + "*.local": "plaintext", + "log.log*": "plaintext", + "site.webmanifest": "json", }, "files.insertFinalNewline": true, "editor.formatOnSave": true, "editor.codeActionsOnSave": { "source.fixAll": "explicit", - // TODO(michaelfromyeg): make this happen for everything except CSS - "source.organizeImports": "never" + "source.organizeImports": "explicit" }, "python.terminal.activateEnvironment": true, "python.terminal.activateEnvInCurrentTerminal": true, diff --git a/Makefile b/Makefile index c2edf8c..90afcd4 100644 --- a/Makefile +++ b/Makefile @@ -5,24 +5,21 @@ client: @cd client && npm start build: - @echo "Building the server..." - @docker-compose -f docker-compose.local.yml build + @echo "Building the server image..." + @docker build -t michaelfromyeg/bereal_wrapped -f docker/Dockerfile.server . + @docker push michaelfromyeg/bereal_wrapped up: @echo "Booting up the server..." - @docker-compose -f docker-compose.local.yml up -d + @docker stack deploy -c docker-stack.local.yml bereal-wrapped logs: @echo "Showing the server logs..." - @docker-compose -f docker-compose.local.yml logs -f + @docker service logs -f bereal-wrapped_web down: @echo "Shutting down the server..." - @docker-compose -f docker-compose.local.yml down - -kill: - @echo "Killing the server..." - @docker-compose -f docker-compose.local.yml kill + @docker stack rm bereal-wrapped start-redis: @echo "Booting up Redis..." @@ -38,14 +35,20 @@ stop-redis: celery: @echo "Booting up Celery..." + @export FLASK_APP=bereal.server + @export FLASK_ENV=development--non-docker @celery -A bereal.celery worker --loglevel=DEBUG --logfile=celery.log -E flower: @echo "Booting up Flower..." + @export FLASK_APP=bereal.server + @export FLASK_ENV=development--non-docker @celery -A bereal.celery flower --address=0.0.0.0 --inspect --enable-events --loglevel=DEBUG --logfile=flower.log server: @echo "Booting up the server..." + @export FLASK_APP=bereal.server + @export FLASK_ENV=development--non-docker @python -m bereal.server cli: @@ -59,3 +62,10 @@ typecheck: format: @echo "Formatting the code..." @ruff check bereal && ruff format bereal + +clean: + rm *.log* + rm *.rdb + rm bereal/*.db + rm exports/*.mp4 + rm -rf content/*/ diff --git a/bereal/bereal.py b/bereal/bereal.py index 133b268..df65b85 100644 --- a/bereal/bereal.py +++ b/bereal/bereal.py @@ -4,15 +4,13 @@ import os from datetime import datetime -from typing import Any +from typing import Any, Literal, TypedDict import requests as r from .logger import logger from .utils import BASE_URL, CONTENT_PATH, TIMEOUT, str2datetime -from typing import TypedDict, Literal - class MediaInfo(TypedDict): """ @@ -139,8 +137,14 @@ def download_image(date_str: str, url: str, base_path: str) -> None: logger.debug("Invalid date: %s", date_str) return None - # Extracting the image name from the URL - image_name = date_str + "_" + url.split("/")[-1] + # Extracting the image name from the URL; will be the last "thing" in this/that/other.xyz + image_path = url.split("/")[-1] + + # if image_path.lower().endswith(".webp"): + # logger.warning("Skipping webp image: %s, currently not supported", image_path) + # return None + + image_name = date_str + "_" + image_path with open(os.path.join(base_path, image_name), "wb") as img_file: img_response = r.get(url, timeout=10) diff --git a/bereal/celery.py b/bereal/celery.py index bc90f35..dc25f80 100644 --- a/bereal/celery.py +++ b/bereal/celery.py @@ -25,7 +25,9 @@ def make_celery(app_name=__name__, broker=f"redis://{REDIS_HOST}:{REDIS_PORT}/0" @bcelery.task(time_limit=1200) -def make_video(token: str, bereal_token: str, phone: str, year: str, song_path: str, mode: Mode) -> str: +def make_video( + token: str, bereal_token: str, phone: str, year: str, display_date: bool, song_path: str, mode: Mode +) -> str: """ Creating a video takes about ~15 min. This is a work-in-progress! @@ -44,7 +46,7 @@ def make_video(token: str, bereal_token: str, phone: str, year: str, song_path: logger.info("Creating images for %s...", video_file) try: - image_folder = create_images(phone, year) + image_folder = create_images(phone, year, display_date) except Exception as e: logger.error("Failed to create images: %s", e) gc.collect() diff --git a/bereal/images.py b/bereal/images.py index ab2b14d..5861da6 100644 --- a/bereal/images.py +++ b/bereal/images.py @@ -16,6 +16,7 @@ def process_image( primary_folder: str, secondary_folder: str, output_folder: str, + display_date: bool = False, ) -> None: """ Combine the primary image with the secondary image, and save the result in the output folder. @@ -24,82 +25,70 @@ def process_image( offset = 50 text_opacity = 150 - # Extract prefix from primary filename primary_prefix = primary_filename.split("_")[0] - - # Check if there's a corresponding file in the secondary folder with the same prefix secondary_files = [file for file in os.listdir(secondary_folder) if file.startswith(primary_prefix)] if not secondary_files: return None - # Use the first matching file in the secondary folder secondary_filename = secondary_files[0] primary_path = os.path.join(primary_folder, primary_filename) secondary_path = os.path.join(secondary_folder, secondary_filename) - # Load primary and secondary images primary_image = Image.open(primary_path) secondary_image = Image.open(secondary_path) - source = Image.open(os.path.join(os.getcwd(), OUTLINE_PATH)) + outline_image = Image.open(OUTLINE_PATH) + # TODO(michaelfromyeg): hack to fix alpha bug; remove after properly addressed primary_image = primary_image.convert("RGBA") secondary_image = secondary_image.convert("RGBA") - source = source.convert("RGBA") + outline_image = outline_image.convert("RGBA") - # Create border around secondary image - secondary_image = ImageChops.multiply(source, secondary_image) + secondary_image = ImageChops.multiply(outline_image, secondary_image) - # Resize secondary image to fraction the size of the primary image width, height = primary_image.size new_size = (width // 3, height // 3) secondary_image = secondary_image.resize(new_size) - # Overlay secondary image on top-left corner of primary image primary_image.paste(secondary_image, (10, 10), secondary_image) width, height = primary_image.size draw = ImageDraw.Draw(primary_image) - # font file is assumed to exist under static/ - font_path = os.path.join(FONT_BASE_PATH, "Inter-Bold.ttf") - font = ImageFont.truetype(font_path, font_size) - - text_bbox = draw.textbbox((0, 0), primary_prefix, font=font) + if display_date: + font_path = os.path.join(FONT_BASE_PATH, "Inter-Bold.ttf") + font = ImageFont.truetype(font_path, font_size) - # Calculate the position to center the text - x = (width - text_bbox[2]) // 2 - y = (height - text_bbox[3]) - offset + text_bbox = draw.textbbox((0, 0), primary_prefix, font=font) - # Calculate the size of the rectangle to fill the text_bbox - rect_width = text_bbox[2] + 20 # Add some padding - rect_height = text_bbox[3] + 20 # Add some padding + x = (width - text_bbox[2]) // 2 + y = (height - text_bbox[3]) - offset - # Draw a semi-transparent filled rectangle as the background - draw.rectangle( - ((x - 30, y - 15), (x + rect_width + 10, y + rect_height + 10)), - fill=(0, 0, 0, text_opacity), - ) + rect_width = text_bbox[2] + 20 + rect_height = text_bbox[3] + 20 - # Draw the text on the image - draw.text((x, y), primary_prefix, font=font, fill="white") - # Save the modified image + draw.text((x, y), primary_prefix, font=font, fill="white") + draw.rectangle( + ((x - 30, y - 15), (x + rect_width + 10, y + rect_height + 10)), + fill=(0, 0, 0, text_opacity), + ) - # Save the result in the output folder output_path = os.path.join(output_folder, f"combined_{primary_filename}") - # ensure the photo is jpg ready + # TODO(michaelfromyeg): this is a hack; I had some issues with the alpha channel showing up occasionally primary_image = primary_image.convert("RGB") - primary_image.save(output_path, quality=IMAGE_QUALITY) logger.debug("Combined image saved at %s", output_path) + return None + def create_images( phone: str, year: str, + display_date: bool, ) -> str: """ Put secondary images on top of primary images. @@ -124,7 +113,7 @@ def create_images( # specifically, "AssertionError: daemonic processes are not allowed to have children" for primary_filename in primary_filenames: - process_image(primary_filename, primary_folder, secondary_folder, output_folder) + process_image(primary_filename, primary_folder, secondary_folder, output_folder, display_date) # Use multiprocessing to process images in parallel # processes = max(1, multiprocessing.cpu_count() - 2) diff --git a/bereal/send.py b/bereal/send.py index 0198af1..e80814a 100644 --- a/bereal/send.py +++ b/bereal/send.py @@ -4,11 +4,11 @@ For now, only phone. Eventually, consider e-mail. """ -from .utils import FLASK_ENV, TWILIO_ACCOUNT_SID, TWILIO_AUTH_TOKEN, TWILIO_PHONE_NUMBER -from .logger import logger - from twilio.rest import Client +from .logger import logger +from .utils import FLASK_ENV, TWILIO_ACCOUNT_SID, TWILIO_AUTH_TOKEN, TWILIO_PHONE_NUMBER + def sms(phone: str, link: str) -> None: """ @@ -17,14 +17,12 @@ def sms(phone: str, link: str) -> None: try: client = Client(TWILIO_ACCOUNT_SID, TWILIO_AUTH_TOKEN) - if FLASK_ENV == "development": - logger.info("Skipping SMS in development mode") - return - message_body = f"Here is the link to your BeReal Wrapped!\n\n{link}" - message = client.messages.create(body=message_body, from_=TWILIO_PHONE_NUMBER, to=phone) - - logger.info("Sent message to %s: %s", phone, message.sid) + if FLASK_ENV == "development" or FLASK_ENV == "development--non-docker": + logger.debug("Skipping SMS in development mode; would send %s", message_body) + else: + message = client.messages.create(body=message_body, from_=TWILIO_PHONE_NUMBER, to=phone) + logger.info("Sent message %s to %s", message.sid, phone) except Exception as e: logger.error("Failed to send SMS: %s", e) pass diff --git a/bereal/server.py b/bereal/server.py index 2265797..ec8bdeb 100644 --- a/bereal/server.py +++ b/bereal/server.py @@ -21,15 +21,14 @@ from flask_limiter.util import get_remote_address # noqa: E402 from flask_migrate import Migrate # noqa: E402 from flask_sqlalchemy import SQLAlchemy # noqa: E402 -from itsdangerous import URLSafeTimedSerializer # noqa: E402 from .bereal import send_code, verify_code # noqa: E402 from .celery import bcelery, make_video # noqa: E402 from .logger import logger # noqa: E402 from .utils import ( # noqa: E402 CONTENT_PATH, - DEFAULT_SONG_PATH, DEFAULT_SHORT_SONG_PATH, + DEFAULT_SONG_PATH, EXPORTS_PATH, FLASK_ENV, GIT_COMMIT_HASH, @@ -37,7 +36,6 @@ PORT, REDIS_HOST, REDIS_PORT, - SECRET_KEY, Mode, str2mode, ) @@ -56,11 +54,9 @@ default_limits=["500 per day", "200 per hour", "20 per minute", "5 per second"], ) -serializer = URLSafeTimedSerializer(SECRET_KEY) - logger.info("Running in %s mode", FLASK_ENV) -if FLASK_ENV == "development": +if FLASK_ENV != "production": logger.info("Enabling CORS for development") CORS(app) else: @@ -152,7 +148,7 @@ def validate_otp() -> tuple[Response, int]: return jsonify({"error": "Bad Request", "message": "Invalid verification code"}), 400 # generate a custom app token; this we can safely save in our DB - bereal_token = secrets.token_urlsafe(20) + bereal_token = secrets.token_hex(10) insert_bereal_token(phone, bereal_token) @@ -172,31 +168,42 @@ def create_video() -> tuple[Response, int]: token = request.form["token"] year = request.form["year"] + display_date = True if request.form["displayDate"] == "true" else False wav_file = request.files.get("file", None) mode_str = request.form.get("mode") mode = str2mode(mode_str) + # TODO(michaelfromyeg): as a temporary hack, validate phone and year to not get weird paths + if not phone.isnumeric() or len(phone) > 20: + return jsonify({"error": "Bad Request", "message": "Invalid phone number"}), 400 + if not year.isnumeric() or len(year) > 4: + return jsonify({"error": "Bad Request", "message": "Invalid year"}), 400 + song_folder = os.path.join(CONTENT_PATH, phone, year) os.makedirs(song_folder, exist_ok=True) song_path = os.path.join(song_folder, "song.wav") + disable_music = True if request.form["disableMusic"] == "true" else False - if wav_file: + if not disable_music and wav_file: logger.info("Downloading music file %s...", wav_file.filename) try: wav_file.save(song_path) - except Exception as error: - logger.warning("Could not save music file, received: %s", error) + except Exception as exception: + logger.warning("Could not save music file, received: %s", exception) song_path = DEFAULT_SHORT_SONG_PATH if mode == Mode.CLASSIC else DEFAULT_SONG_PATH - else: + elif not disable_music: logger.info("No music file provided; using default...") song_path = DEFAULT_SHORT_SONG_PATH if mode == Mode.CLASSIC else DEFAULT_SONG_PATH + else: + logger.info("Music disabled; setting path to empty string") + song_path = "" logger.info("Queueing video task...") # TODO(michaelfromyeg): replace token with bereal_token - task = make_video.delay(token, bereal_token, phone, year, song_path, mode) + task = make_video.delay(token, bereal_token, phone, year, display_date, song_path, mode) return jsonify({"taskId": task.id}), 202 @@ -227,7 +234,7 @@ def task_status(task_id) -> tuple[Response, int]: "message": "An error occurred processing your task. Try again later.", "error": str(task.info), } - return jsonify(response), 500 + return jsonify(response), 200 response = {"status": task.status, "result": task.result if task.state == "SUCCESS" else None} return jsonify(response), 200 @@ -318,8 +325,11 @@ def delete_expired_tokens() -> None: """ Delete all expired tokens from the database. """ - expiration_time = datetime.utcnow() - timedelta(hours=24) - BerealToken.query.filter(BerealToken.timestamp < expiration_time).delete() + expiration_time = datetime.utcnow() - timedelta(days=1) + + n = BerealToken.query.filter(BerealToken.timestamp < expiration_time).delete() + logger.info("Deleted %d expired tokens", n) + db.session.commit() return None @@ -329,7 +339,7 @@ def delete_old_videos() -> None: """ Delete videos that are more than a day old. """ - time_limit = datetime.now() - timedelta(days=1) + expiration_time = datetime.utcnow() - timedelta(days=1) for filename in os.listdir(EXPORTS_PATH): file_path = os.path.join(EXPORTS_PATH, filename) @@ -337,7 +347,7 @@ def delete_old_videos() -> None: if os.path.isfile(file_path): file_mod_time = datetime.fromtimestamp(os.path.getmtime(file_path)) - if file_mod_time < time_limit: + if file_mod_time < expiration_time: try: os.remove(file_path) logger.info("Deleted video file %s", file_path) @@ -368,4 +378,4 @@ def scheduled_video_task() -> None: if __name__ == "__main__": logger.info("Starting BeReal server on %s:%d...", HOST, PORT) - app.run(host=HOST, port=PORT, debug=FLASK_ENV == "development") + app.run(host=HOST, port=PORT, debug=FLASK_ENV != "production") diff --git a/bereal/static/songs/seven-nation-army.wav b/bereal/static/songs/midnight-city.wav similarity index 76% rename from bereal/static/songs/seven-nation-army.wav rename to bereal/static/songs/midnight-city.wav index b3f2e83..2f9e361 100644 Binary files a/bereal/static/songs/seven-nation-army.wav and b/bereal/static/songs/midnight-city.wav differ diff --git a/bereal/utils.py b/bereal/utils.py index 563a5c7..e017549 100644 --- a/bereal/utils.py +++ b/bereal/utils.py @@ -8,22 +8,35 @@ from datetime import datetime from enum import StrEnum -from dotenv import load_dotenv - from .logger import logger +FLASK_ENV = os.environ.get("FLASK_ENV", "production") + + # Environment variables -load_dotenv() +def get_secret(secret_name: str) -> str | None: + """ + Get a Docker Swarm secret. + """ + print(FLASK_ENV) -SECRET_KEY = os.getenv("SECRET_KEY") or "SECRET_KEY" -FLASK_ENV = os.getenv("FLASK_ENV") or "production" + if FLASK_ENV == "development--non-docker": + import dotenv + + dotenv.load_dotenv() + + return os.getenv(secret_name.upper()) + + try: + with open(f"/run/secrets/{secret_name}", "r") as secret_file: + return secret_file.read().strip() + except IOError: + return None -if SECRET_KEY == "SECRET_KEY": - raise ValueError("SECRET_KEY environment variable not set or non-unique") -TWILIO_PHONE_NUMBER = os.getenv("TWILIO_PHONE_NUMBER") -TWILIO_AUTH_TOKEN = os.getenv("TWILIO_AUTH_TOKEN") -TWILIO_ACCOUNT_SID = os.getenv("TWILIO_ACCOUNT_SID") +TWILIO_PHONE_NUMBER = get_secret("twilio_phone_number") +TWILIO_AUTH_TOKEN = get_secret("twilio_auth_token") +TWILIO_ACCOUNT_SID = get_secret("twilio_account_sid") if TWILIO_PHONE_NUMBER is None or TWILIO_AUTH_TOKEN is None or TWILIO_ACCOUNT_SID is None: raise ValueError("TWILIO environment variables not set") @@ -73,7 +86,7 @@ def str2mode(s: str | None) -> Mode: SONGS_PATH = os.path.join(STATIC_PATH, "songs") -DEFAULT_SONG_PATH = os.path.join(SONGS_PATH, "seven-nation-army.wav") +DEFAULT_SONG_PATH = os.path.join(SONGS_PATH, "midnight-city.wav") DEFAULT_SHORT_SONG_PATH = os.path.join(SONGS_PATH, "midnight-city-short.wav") ENDCARD_TEMPLATE_IMAGE_PATH = os.path.join(IMAGES_PATH, "endCard_template.jpg") @@ -91,14 +104,14 @@ def str2mode(s: str | None) -> Mode: config.read("config.ini") -HOST: str | None = os.getenv("HOST") or "localhost" -PORT: str | None = os.getenv("PORT") or "5000" +HOST: str | None = get_secret("host") or "localhost" +PORT: str | None = get_secret("port") or "5000" PORT = int(PORT) if PORT is not None else None -TRUE_HOST = f"http://{HOST}:{PORT}" if FLASK_ENV == "development" else "https://api.bereal.michaeldemar.co" +TRUE_HOST = "https://api.bereal.michaeldemar.co" if FLASK_ENV == "production" else f"http://localhost:{PORT}" -REDIS_HOST: str | None = os.getenv("REDIS_HOST") or "redis" -REDIS_PORT: str | None = os.getenv("REDIS_PORT") or "6379" +REDIS_HOST: str | None = get_secret("redis_host") or "redis" +REDIS_PORT: str | None = get_secret("redis_port") or "6379" REDIS_PORT = int(REDIS_PORT) if REDIS_PORT is not None else None logger.info("REDIS_HOST: %s, REDIS_PORT: %s", REDIS_HOST, REDIS_PORT) diff --git a/bereal/videos.py b/bereal/videos.py index 4318d18..21dd213 100644 --- a/bereal/videos.py +++ b/bereal/videos.py @@ -5,16 +5,15 @@ import os import librosa - from moviepy.audio.io.AudioFileClip import AudioFileClip from moviepy.video.fx import all as vfx from moviepy.video.io.ImageSequenceClip import ImageSequenceClip - from PIL import Image, ImageDraw, ImageFont from .logger import logger from .utils import ( CONTENT_PATH, + DEFAULT_SONG_PATH, ENDCARD_TEMPLATE_IMAGE_PATH, EXPORTS_PATH, FONT_BASE_PATH, @@ -69,13 +68,13 @@ def create_slideshow3( """ Create a video slideshow from a target set of images. """ - logger.debug("Creating slideshow for %s, %s", phone, year) + logger.info("Creating slideshow for %s, %s", phone, year) if not os.path.isdir(input_folder): raise ValueError("Input folder does not exist!") - if music_file is not None and not os.path.isfile(music_file): - raise ValueError("Music file does not exist!") + if music_file is not None and len(music_file) > 0 and not os.path.isfile(music_file): + raise ValueError("Music file specfied but does not exist!") n_images = len(os.listdir(input_folder)) if n_images == 0: @@ -104,18 +103,18 @@ def create_slideshow3( if mode == Mode.CLASSIC: main_clip = main_clip.fx(vfx.accel_decel, new_duration=30) - music = AudioFileClip(music_file) - - if music.duration < main_clip.duration: - logger.warning("Music is shorter than final clip; looping music") + if music_file is not None and len(music_file) > 0: + logger.info("Adding music to slideshow") + music = AudioFileClip(music_file) - music = music.fx(vfx.loop, duration=main_clip.duration) - else: - logger.info("Music is longer than final clip; clipping appropriately") + if music.duration < main_clip.duration: + logger.warning("Music is shorter than final clip; looping music") + music = music.fx(vfx.loop, duration=main_clip.duration) + else: + logger.info("Music is longer than final clip; clipping appropriately") + music.set_duration(main_clip.duration) - music = music.subclip(0, main_clip.duration) - - main_clip = main_clip.set_audio(music) + main_clip = main_clip.set_audio(music) main_clip.write_videofile(output_file, codec="libx264", audio_codec="aac", threads=4, fps=24) @@ -142,6 +141,9 @@ def build_slideshow( """ Create the actual slideshow. """ + if not os.path.isfile(song_path): + song_path = DEFAULT_SONG_PATH + audio_file = librosa.load(song_path) y, sr = audio_file _, beat_frames = librosa.beat.beat_track(y=y, sr=sr) diff --git a/client/package-lock.json b/client/package-lock.json index a0972eb..56923a1 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -8,13 +8,6 @@ "name": "client", "version": "0.1.0", "dependencies": { - "@testing-library/jest-dom": "^5.17.0", - "@testing-library/react": "^13.4.0", - "@testing-library/user-event": "^13.5.0", - "@types/jest": "^27.5.2", - "@types/node": "^16.18.68", - "@types/react": "^18.2.46", - "@types/react-dom": "^18.2.18", "autoprefixer": "^10.4.16", "axios": "^1.7.3", "country-list": "^2.3.0", @@ -22,23 +15,32 @@ "postcss": "^8.4.35", "react": "^18.2.0", "react-dom": "^18.2.0", + "react-router-dom": "^6.22.0", "react-scripts": "5.0.1", "react-select": "^5.8.0", "react-toastify": "^9.1.3", "tailwindcss": "^3.4.0", - "typescript": "^4.9.5", "web-vitals": "^2.1.4" }, "devDependencies": { "@babel/plugin-proposal-private-property-in-object": "^7.21.11", + "@testing-library/jest-dom": "^6.4.8", + "@testing-library/react": "^16.0.0", + "@testing-library/user-event": "^14.5.2", + "@types/jest": "^29.5.12", + "@types/node": "^22.3.0", + "@types/react": "^18.3.3", + "@types/react-dom": "^18.3.0", "@types/react-select": "^5.0.1", - "@types/webappsec-credential-management": "^0.6.8" + "@types/webappsec-credential-management": "^0.6.8", + "typescript": "^5.5.4" } }, "node_modules/@adobe/css-tools": { "version": "4.4.0", "resolved": "https://registry.npmjs.org/@adobe/css-tools/-/css-tools-4.4.0.tgz", - "integrity": "sha512-Ff9+ksdQQB3rMncgqDK78uLznstjyfIf2Arnh22pW8kBpLs6rpKDwgnZT46hin5Hl1WzazzK64DOrhSwYpS7bQ==" + "integrity": "sha512-Ff9+ksdQQB3rMncgqDK78uLznstjyfIf2Arnh22pW8kBpLs6rpKDwgnZT46hin5Hl1WzazzK64DOrhSwYpS7bQ==", + "dev": true }, "node_modules/@alloc/quick-lru": { "version": "5.2.0", @@ -2854,6 +2856,27 @@ "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" } }, + "node_modules/@jest/expect-utils": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-29.7.0.tgz", + "integrity": "sha512-GlsNBWiFQFCVi9QVSx7f5AgMeLxe9YCCs5PuP2O2LdjDAA8Jh9eX7lA1Jq/xdXw3Wb3hyvlFNfZIfcRetSzYcA==", + "dev": true, + "dependencies": { + "jest-get-type": "^29.6.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/expect-utils/node_modules/jest-get-type": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.6.3.tgz", + "integrity": "sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==", + "dev": true, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, "node_modules/@jest/fake-timers": { "version": "27.5.1", "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-27.5.1.tgz", @@ -3412,6 +3435,14 @@ } } }, + "node_modules/@remix-run/router": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.15.0.tgz", + "integrity": "sha512-HOil5aFtme37dVQTB6M34G95kPM3MMuqSmIRVCC52eKV+Y/tGSqw9P3rWhlAx6A+mz+MoX+XxsGsNJbaI5qCgQ==", + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/@rollup/plugin-babel": { "version": "5.3.1", "resolved": "https://registry.npmjs.org/@rollup/plugin-babel/-/plugin-babel-5.3.1.tgz", @@ -3734,6 +3765,7 @@ "version": "10.4.0", "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.0.tgz", "integrity": "sha512-pemlzrSESWbdAloYml3bAJMEfNh1Z7EduzqPKprCH5S341frlpYnUEW0H72dLxa6IsYr+mPno20GiSm+h9dEdQ==", + "dev": true, "peer": true, "dependencies": { "@babel/code-frame": "^7.10.4", @@ -3753,6 +3785,7 @@ "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, "peer": true, "dependencies": { "color-convert": "^2.0.1" @@ -3768,6 +3801,7 @@ "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, "peer": true, "dependencies": { "ansi-styles": "^4.1.0", @@ -3784,6 +3818,7 @@ "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, "peer": true, "dependencies": { "color-name": "~1.1.4" @@ -3796,12 +3831,14 @@ "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, "peer": true }, "node_modules/@testing-library/dom/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, "peer": true, "engines": { "node": ">=8" @@ -3811,6 +3848,7 @@ "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, "peer": true, "dependencies": { "has-flag": "^4.0.0" @@ -3820,22 +3858,22 @@ } }, "node_modules/@testing-library/jest-dom": { - "version": "5.17.0", - "resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-5.17.0.tgz", - "integrity": "sha512-ynmNeT7asXyH3aSVv4vvX4Rb+0qjOhdNHnO/3vuZNqPmhDpV/+rCSGwQ7bLcmU2cJ4dvoheIO85LQj0IbJHEtg==", + "version": "6.4.8", + "resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-6.4.8.tgz", + "integrity": "sha512-JD0G+Zc38f5MBHA4NgxQMR5XtO5Jx9g86jqturNTt2WUfRmLDIY7iKkWHDCCTiDuFMre6nxAD5wHw9W5kI4rGw==", + "dev": true, "dependencies": { - "@adobe/css-tools": "^4.0.1", + "@adobe/css-tools": "^4.4.0", "@babel/runtime": "^7.9.2", - "@types/testing-library__jest-dom": "^5.9.1", "aria-query": "^5.0.0", "chalk": "^3.0.0", "css.escape": "^1.5.1", - "dom-accessibility-api": "^0.5.6", - "lodash": "^4.17.15", + "dom-accessibility-api": "^0.6.3", + "lodash": "^4.17.21", "redent": "^3.0.0" }, "engines": { - "node": ">=8", + "node": ">=14", "npm": ">=6", "yarn": ">=1" } @@ -3844,6 +3882,7 @@ "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" }, @@ -3858,6 +3897,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", + "dev": true, "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" @@ -3870,6 +3910,7 @@ "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" }, @@ -3880,12 +3921,20 @@ "node_modules/@testing-library/jest-dom/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==" + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/@testing-library/jest-dom/node_modules/dom-accessibility-api": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.6.3.tgz", + "integrity": "sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==", + "dev": true }, "node_modules/@testing-library/jest-dom/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" } @@ -3894,6 +3943,7 @@ "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" }, @@ -3902,121 +3952,39 @@ } }, "node_modules/@testing-library/react": { - "version": "13.4.0", - "resolved": "https://registry.npmjs.org/@testing-library/react/-/react-13.4.0.tgz", - "integrity": "sha512-sXOGON+WNTh3MLE9rve97ftaZukN3oNf2KjDy7YTx6hcTO2uuLHuCGynMDhFwGw/jYf4OJ2Qk0i4i79qMNNkyw==", + "version": "16.0.0", + "resolved": "https://registry.npmjs.org/@testing-library/react/-/react-16.0.0.tgz", + "integrity": "sha512-guuxUKRWQ+FgNX0h0NS0FIq3Q3uLtWVpBzcLOggmfMoUpgBnzBzvLLd4fbm6yS8ydJd94cIfY4yP9qUQjM2KwQ==", + "dev": true, "dependencies": { - "@babel/runtime": "^7.12.5", - "@testing-library/dom": "^8.5.0", - "@types/react-dom": "^18.0.0" + "@babel/runtime": "^7.12.5" }, "engines": { - "node": ">=12" + "node": ">=18" }, "peerDependencies": { + "@testing-library/dom": "^10.0.0", + "@types/react": "^18.0.0", + "@types/react-dom": "^18.0.0", "react": "^18.0.0", "react-dom": "^18.0.0" - } - }, - "node_modules/@testing-library/react/node_modules/@testing-library/dom": { - "version": "8.20.1", - "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-8.20.1.tgz", - "integrity": "sha512-/DiOQ5xBxgdYRC8LNk7U+RWat0S3qRLeIw3ZIkMQ9kkVlRmwD/Eg8k8CqIpD6GW7u20JIUOfMKbxtiLutpjQ4g==", - "dependencies": { - "@babel/code-frame": "^7.10.4", - "@babel/runtime": "^7.12.5", - "@types/aria-query": "^5.0.1", - "aria-query": "5.1.3", - "chalk": "^4.1.0", - "dom-accessibility-api": "^0.5.9", - "lz-string": "^1.5.0", - "pretty-format": "^27.0.2" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/@testing-library/react/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==", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/@testing-library/react/node_modules/aria-query": { - "version": "5.1.3", - "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.1.3.tgz", - "integrity": "sha512-R5iJ5lkuHybztUfuOAznmboyjWq8O6sqNqtK7CLOqdydi54VNbORp49mb14KbWgG1QD3JFO9hJdZ+y4KutfdOQ==", - "dependencies": { - "deep-equal": "^2.0.5" - } - }, - "node_modules/@testing-library/react/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "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/@testing-library/react/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==", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/@testing-library/react/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==" - }, - "node_modules/@testing-library/react/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==", - "engines": { - "node": ">=8" - } - }, - "node_modules/@testing-library/react/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==", - "dependencies": { - "has-flag": "^4.0.0" }, - "engines": { - "node": ">=8" + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } } }, "node_modules/@testing-library/user-event": { - "version": "13.5.0", - "resolved": "https://registry.npmjs.org/@testing-library/user-event/-/user-event-13.5.0.tgz", - "integrity": "sha512-5Kwtbo3Y/NowpkbRuSepbyMFkZmHgD+vPzYB/RJ4oxt5Gj/avFFBYjhw27cqSVPVw/3a67NK1PbiIr9k4Gwmdg==", - "dependencies": { - "@babel/runtime": "^7.12.5" - }, + "version": "14.5.2", + "resolved": "https://registry.npmjs.org/@testing-library/user-event/-/user-event-14.5.2.tgz", + "integrity": "sha512-YAh82Wh4TIrxYLmfGcixwD18oIjyC1pFQC2Y01F2lzV2HTMiYrI0nze0FD0ocB//CKS/7jIUgae+adPqxK5yCQ==", + "dev": true, "engines": { - "node": ">=10", + "node": ">=12", "npm": ">=6" }, "peerDependencies": { @@ -4042,7 +4010,9 @@ "node_modules/@types/aria-query": { "version": "5.0.4", "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz", - "integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==" + "integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==", + "dev": true, + "peer": true }, "node_modules/@types/babel__core": { "version": "7.20.5", @@ -4208,12 +4178,260 @@ } }, "node_modules/@types/jest": { - "version": "27.5.2", - "resolved": "https://registry.npmjs.org/@types/jest/-/jest-27.5.2.tgz", - "integrity": "sha512-mpT8LJJ4CMeeahobofYWIjFo0xonRS/HfxnVEPMPFSQdGUt1uHCnoPT7Zhb+sjDU2wz0oKV0OLUR0WzrHNgfeA==", + "version": "29.5.12", + "resolved": "https://registry.npmjs.org/@types/jest/-/jest-29.5.12.tgz", + "integrity": "sha512-eDC8bTvT/QhYdxJAulQikueigY5AsdBRH2yDKW3yveW7svY3+DzN84/2NUgkw10RTiJbWqZrTtoGVdYlvFJdLw==", + "dev": true, + "dependencies": { + "expect": "^29.0.0", + "pretty-format": "^29.0.0" + } + }, + "node_modules/@types/jest/node_modules/@jest/schemas": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", + "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", + "dev": true, + "dependencies": { + "@sinclair/typebox": "^0.27.8" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@types/jest/node_modules/@jest/types": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz", + "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==", + "dev": true, + "dependencies": { + "@jest/schemas": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^17.0.8", + "chalk": "^4.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@types/jest/node_modules/@sinclair/typebox": { + "version": "0.27.8", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", + "integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==", + "dev": true + }, + "node_modules/@types/jest/node_modules/@types/yargs": { + "version": "17.0.33", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.33.tgz", + "integrity": "sha512-WpxBCKWPLr4xSsHgz511rFJAM+wS28w2zEO1QDNY5zM/S8ok70NNfztH0xwhqKyaK0OHCbN98LDAZuy1ctxDkA==", + "dev": true, + "dependencies": { + "@types/yargs-parser": "*" + } + }, + "node_modules/@types/jest/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": { - "jest-matcher-utils": "^27.0.0", - "pretty-format": "^27.0.0" + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@types/jest/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/@types/jest/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/@types/jest/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/@types/jest/node_modules/diff-sequences": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", + "integrity": "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==", + "dev": true, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@types/jest/node_modules/expect": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/expect/-/expect-29.7.0.tgz", + "integrity": "sha512-2Zks0hf1VLFYI1kbh0I5jP3KHHyCHpkfyHBzsSXRFgl/Bg9mWYfMW8oD+PdMPlEwy5HNsR9JutYy6pMeOh61nw==", + "dev": true, + "dependencies": { + "@jest/expect-utils": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@types/jest/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/@types/jest/node_modules/jest-diff": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-29.7.0.tgz", + "integrity": "sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw==", + "dev": true, + "dependencies": { + "chalk": "^4.0.0", + "diff-sequences": "^29.6.3", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@types/jest/node_modules/jest-get-type": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.6.3.tgz", + "integrity": "sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==", + "dev": true, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@types/jest/node_modules/jest-matcher-utils": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-29.7.0.tgz", + "integrity": "sha512-sBkD+Xi9DtcChsI3L3u0+N0opgPYnCRPtGcQYrgXmR+hmt/fYfWAL0xRXYU8eWOdfuLgBe0YCW3AFtnRLagq/g==", + "dev": true, + "dependencies": { + "chalk": "^4.0.0", + "jest-diff": "^29.7.0", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@types/jest/node_modules/jest-message-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-29.7.0.tgz", + "integrity": "sha512-GBEV4GRADeP+qtB2+6u61stea8mGcOT4mCtrYISZwfu9/ISHFJ/5zOMXYbpBE9RsS5+Gb63DW4FgmnKJ79Kf6w==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.12.13", + "@jest/types": "^29.6.3", + "@types/stack-utils": "^2.0.0", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "micromatch": "^4.0.4", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@types/jest/node_modules/jest-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.7.0.tgz", + "integrity": "sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==", + "dev": true, + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "graceful-fs": "^4.2.9", + "picomatch": "^2.2.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@types/jest/node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@types/jest/node_modules/pretty-format/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@types/jest/node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "dev": true + }, + "node_modules/@types/jest/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/@types/json-schema": { @@ -4232,9 +4450,12 @@ "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==" }, "node_modules/@types/node": { - "version": "16.18.105", - "resolved": "https://registry.npmjs.org/@types/node/-/node-16.18.105.tgz", - "integrity": "sha512-w2d0Z9yMk07uH3+Cx0N8lqFyi3yjXZxlbYappPj+AsOlT02OyxyiuNoNHdGt6EuiSm8Wtgp2YV7vWg+GMFrvFA==" + "version": "22.3.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.3.0.tgz", + "integrity": "sha512-nrWpWVaDZuaVc5X84xJ0vNrLvomM205oQyLsRt7OHNZbSHslcWsvgFR7O7hire2ZonjLrWBbedmotmIlJDVd6g==", + "dependencies": { + "undici-types": "~6.18.2" + } }, "node_modules/@types/node-forge": { "version": "1.3.11", @@ -4287,6 +4508,7 @@ "version": "18.3.0", "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.0.tgz", "integrity": "sha512-EhwApuTmMBmXuFOikhQLIBUn6uFg81SwLMOAUgodJF14SOBOCMdU04gDoYi0WOJJHD144TL32z4yDqCW3dnkQg==", + "dev": true, "dependencies": { "@types/react": "*" } @@ -4367,14 +4589,6 @@ "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz", "integrity": "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==" }, - "node_modules/@types/testing-library__jest-dom": { - "version": "5.14.9", - "resolved": "https://registry.npmjs.org/@types/testing-library__jest-dom/-/testing-library__jest-dom-5.14.9.tgz", - "integrity": "sha512-FSYhIjFlfOpGSRyVoMBMuS3ws5ehFQODymf3vlI7U1K8c7PHwWwFY7VREfmsuzHSOnoKs/9/Y983ayOs7eRzqw==", - "dependencies": { - "@types/jest": "*" - } - }, "node_modules/@types/trusted-types": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", @@ -5056,6 +5270,7 @@ "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" } @@ -6497,7 +6712,8 @@ "node_modules/css.escape": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/css.escape/-/css.escape-1.5.1.tgz", - "integrity": "sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==" + "integrity": "sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==", + "dev": true }, "node_modules/cssdb": { "version": "7.11.2", @@ -6867,6 +7083,7 @@ "version": "2.0.3", "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "dev": true, "engines": { "node": ">=6" } @@ -6976,7 +7193,9 @@ "node_modules/dom-accessibility-api": { "version": "0.5.16", "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz", - "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==" + "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==", + "dev": true, + "peer": true }, "node_modules/dom-converter": { "version": "0.2.0", @@ -9456,6 +9675,7 @@ "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" } @@ -12448,6 +12668,8 @@ "version": "1.5.0", "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz", "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==", + "dev": true, + "peer": true, "bin": { "lz-string": "bin/bin.js" } @@ -12591,6 +12813,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", "integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==", + "dev": true, "engines": { "node": ">=4" } @@ -14996,6 +15219,36 @@ "node": ">=0.10.0" } }, + "node_modules/react-router": { + "version": "6.22.0", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.22.0.tgz", + "integrity": "sha512-q2yemJeg6gw/YixRlRnVx6IRJWZD6fonnfZhN1JIOhV2iJCPeRNSH3V1ISwHf+JWcESzLC3BOLD1T07tmO5dmg==", + "dependencies": { + "@remix-run/router": "1.15.0" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "react": ">=16.8" + } + }, + "node_modules/react-router-dom": { + "version": "6.22.0", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.22.0.tgz", + "integrity": "sha512-z2w+M4tH5wlcLmH3BMMOMdrtrJ9T3oJJNsAlBJbwk+8Syxd5WFJ7J5dxMEW0/GEXD1BBis4uXRrNIz3mORr0ag==", + "dependencies": { + "@remix-run/router": "1.15.0", + "react-router": "6.22.0" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "react": ">=16.8", + "react-dom": ">=16.8" + } + }, "node_modules/react-scripts": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/react-scripts/-/react-scripts-5.0.1.tgz", @@ -15173,6 +15426,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz", "integrity": "sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==", + "dev": true, "dependencies": { "indent-string": "^4.0.0", "strip-indent": "^3.0.0" @@ -16446,6 +16700,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz", "integrity": "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==", + "dev": true, "dependencies": { "min-indent": "^1.0.0" }, @@ -17175,15 +17430,15 @@ } }, "node_modules/typescript": { - "version": "4.9.5", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz", - "integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==", + "version": "5.5.4", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.5.4.tgz", + "integrity": "sha512-Mtq29sKDAEYP7aljRgtPOpTvOfbwRWlS6dPRzwjdE+C0R4brX/GUyhHSecbHMFLNBLcJIPt9nl9yG5TZ1weH+Q==", "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" }, "engines": { - "node": ">=4.2.0" + "node": ">=14.17" } }, "node_modules/unbox-primitive": { @@ -17205,6 +17460,11 @@ "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.12.1.tgz", "integrity": "sha512-hEQt0+ZLDVUMhebKxL4x1BTtDY7bavVofhZ9KZ4aI26X9SRaE+Y3m83XUL1UP2jn8ynjndwCCpEHdUG+9pP1Tw==" }, + "node_modules/undici-types": { + "version": "6.18.2", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.18.2.tgz", + "integrity": "sha512-5ruQbENj95yDYJNS3TvcaxPMshV7aizdv/hWYjGIKoANWKjhWNBsr2YEuYZKodQulB1b8l7ILOuDQep3afowQQ==" + }, "node_modules/unicode-canonical-property-names-ecmascript": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.0.tgz", diff --git a/client/package.json b/client/package.json index 837a498..04589d2 100644 --- a/client/package.json +++ b/client/package.json @@ -3,13 +3,6 @@ "version": "0.1.0", "private": true, "dependencies": { - "@testing-library/jest-dom": "^5.17.0", - "@testing-library/react": "^13.4.0", - "@testing-library/user-event": "^13.5.0", - "@types/jest": "^27.5.2", - "@types/node": "^16.18.68", - "@types/react": "^18.2.46", - "@types/react-dom": "^18.2.18", "autoprefixer": "^10.4.16", "axios": "^1.7.3", "country-list": "^2.3.0", @@ -17,11 +10,11 @@ "postcss": "^8.4.35", "react": "^18.2.0", "react-dom": "^18.2.0", + "react-router-dom": "^6.22.0", "react-scripts": "5.0.1", "react-select": "^5.8.0", "react-toastify": "^9.1.3", "tailwindcss": "^3.4.0", - "typescript": "^4.9.5", "web-vitals": "^2.1.4" }, "scripts": { @@ -50,7 +43,20 @@ }, "devDependencies": { "@babel/plugin-proposal-private-property-in-object": "^7.21.11", + "@testing-library/jest-dom": "^6.4.8", + "@testing-library/react": "^16.0.0", + "@testing-library/user-event": "^14.5.2", + "@types/jest": "^29.5.12", + "@types/node": "^22.3.0", + "@types/react": "^18.3.3", + "@types/react-dom": "^18.3.0", "@types/react-select": "^5.0.1", - "@types/webappsec-credential-management": "^0.6.8" + "@types/webappsec-credential-management": "^0.6.8", + "typescript": "^5.5.4" + }, + "jest": { + "transformIgnorePatterns": [ + "node_modules/(?!axios)" + ] } } diff --git a/client/postcss.config.js b/client/postcss.config.js index 33ad091..12a703d 100644 --- a/client/postcss.config.js +++ b/client/postcss.config.js @@ -3,4 +3,4 @@ module.exports = { tailwindcss: {}, autoprefixer: {}, }, -} +}; diff --git a/client/public/index.html b/client/public/index.html index 81a26f0..3d24f45 100644 --- a/client/public/index.html +++ b/client/public/index.html @@ -6,13 +6,12 @@ - -