diff --git a/MPCAutofill/cardpicker/admin.py b/MPCAutofill/cardpicker/admin.py index b88213180..5f3a97a8b 100644 --- a/MPCAutofill/cardpicker/admin.py +++ b/MPCAutofill/cardpicker/admin.py @@ -7,21 +7,25 @@ @admin.register(Tag) class AdminTag(admin.ModelAdmin[Tag]): list_display = ("name",) + search_fields = ("name",) @admin.register(Card) class AdminCard(admin.ModelAdmin[Card]): list_display = ("identifier", "name", "source", "dpi", "date", "tags") + search_fields = ("identifier", "name") @admin.register(DFCPair) class AdminDFCPair(admin.ModelAdmin[DFCPair]): list_display = ("front", "back") + search_fields = ("front",) @admin.register(Source) class AdminSource(admin.ModelAdmin[Source]): list_display = ("name", "identifier", "contribution", "description") + search_fields = ("name", "identifier") def contribution(self, obj: Source) -> str: qty_all, qty_cards, qty_cardbacks, qty_tokens, avgdpi = obj.count() diff --git a/MPCAutofill/cardpicker/search/search_functions.py b/MPCAutofill/cardpicker/search/search_functions.py index 07fd7ca1e..12269acaa 100644 --- a/MPCAutofill/cardpicker/search/search_functions.py +++ b/MPCAutofill/cardpicker/search/search_functions.py @@ -268,7 +268,7 @@ def retrieve_cardback_identifiers(self) -> list[str]: """ cardbacks: list[str] - order_by = ["-priority", "source__name", "name"] + order_by = ["-priority", "source__ordinal", "source__name", "name"] if self.filter_cardbacks: # afaik, `~Q(pk__in=[])` is the best way to have an always-true filter language_filter = ( diff --git a/MPCAutofill/cardpicker/sources/update_database.py b/MPCAutofill/cardpicker/sources/update_database.py index 03a16aadf..d5df33422 100644 --- a/MPCAutofill/cardpicker/sources/update_database.py +++ b/MPCAutofill/cardpicker/sources/update_database.py @@ -20,7 +20,10 @@ def add_images_in_folder_to_list(source_type: Type[SourceType], folder: Folder, images: deque[Image]) -> None: - images.extend(source_type.get_all_images_inside_folder(folder)) + try: + images.extend(source_type.get_all_images_inside_folder(folder)) + except Exception as e: + print(f"Uncaught exception while adding images in folder to list: **{e}**") def explore_folder(source: Source, source_type: Type[SourceType], root_folder: Folder) -> list[Image]: @@ -59,6 +62,7 @@ def transform_images_into_objects(source: Source, images: list[Image], tags: Tag card_count = 0 cardback_count = 0 token_count = 0 + errors: list[str] = [] # report on all exceptions at the end for image in images: try: @@ -115,12 +119,23 @@ def transform_images_into_objects(source: Source, images: list[Image], tags: Tag ) ) except AssertionError as e: - print(f"Skipping image **{image.name}** (identifier **{image.id}**) for the following reason: **{e}**") + errors.append( + f"Assertion error while processing **{image.name}** (identifier **{image.id}**) will not be indexed " + f"for the following reason: **{e}**" + ) + except Exception as e: + errors.append( + f"Uncaught exception while processing image **{image.name}** (identifier **{image.id}**): **{e}**" + ) print( f" and done! Generated {TEXT_BOLD}{card_count:,}{TEXT_END} card/s, {TEXT_BOLD}{cardback_count:,}{TEXT_END} " f"cardback/s, and {TEXT_BOLD}{token_count:,}{TEXT_END} token/s in " f"{TEXT_BOLD}{(time.time() - t0):.2f}{TEXT_END} seconds." ) + if errors: + print("The following cards failed to process:", flush=True) + for error in errors: + print(f"* {error}", flush=True) return cards diff --git a/desktop-tool/requirements.txt b/desktop-tool/requirements.txt index 6535d4f72..81cd79a98 100644 --- a/desktop-tool/requirements.txt +++ b/desktop-tool/requirements.txt @@ -6,7 +6,7 @@ defusedxml~=0.7.1 enlighten~=1.11.2 fpdf2~=2.7.4 InquirerPy~=0.3.4 -pillow==10.0.1 +pillow==10.2.0 pre-commit pyinstaller~=5.13.0 pytest~=7.3 diff --git a/docker/django/Dockerfile b/docker/django/Dockerfile index 296f3822e..68224e97f 100644 --- a/docker/django/Dockerfile +++ b/docker/django/Dockerfile @@ -19,12 +19,6 @@ RUN apt-get update && \ RUN rm -rf /etc/cron.*/* && \ chmod u+s /usr/sbin/cron -# Configure apt with node 18 -RUN curl -sL https://deb.nodesource.com/setup_18.x | bash - - -# Install node and npm -RUN apt-get install -y nodejs - # Copy requirements.txt COPY MPCAutofill/requirements.txt /MPCAutofill/ WORKDIR /MPCAutofill @@ -33,15 +27,20 @@ WORKDIR /MPCAutofill RUN pip3 install gunicorn wheel RUN pip3 install -r requirements.txt -# Copy frontend files from repository -COPY MPCAutofill/cardpicker/frontend /MPCAutofill/cardpicker/frontend -COPY MPCAutofill/package.json /MPCAutofill/package.json -COPY MPCAutofill/package-lock.json /MPCAutofill/package-lock.json - # Copy relevant files from repository COPY docker /MPCAutofill/docker +COPY common /MPCAutofill/common COPY MPCAutofill /MPCAutofill/MPCAutofill +# Handle environment variables - write out `.env` with the variables passed from the +# compose file (and overwrite `.env` as copied from the host machine in the process) +RUN touch /MPCAutofill/MPCAutofill/MPCAutofill/.env +RUN echo "DATABASE_HOST=$DATABASE_HOST" > /MPCAutofill/MPCAutofill/MPCAutofill/.env # using > to overwrite the file +RUN echo "ELASTICSEARCH_HOST=$ELASTICSEARCH_HOST" >> /MPCAutofill/MPCAutofill/MPCAutofill/.env +RUN echo "DEBUG=$DEBUG" >> /MPCAutofill/MPCAutofill/MPCAutofill/.env +RUN echo "ALLOWED_HOSTS=$ALLOWED_HOSTS" >> /MPCAutofill/MPCAutofill/MPCAutofill/.env +RUN echo "GAME=$GAME" >> /MPCAutofill/MPCAutofill/MPCAutofill/.env + # Make sure that all scripts are executable, and in case we # checked out under Windows with CRLF, convert line endings RUN chmod +x docker/django/*.sh && \ diff --git a/docker/django/entrypoint.sh b/docker/django/entrypoint.sh index 8f339542e..dbf4c59c1 100644 --- a/docker/django/entrypoint.sh +++ b/docker/django/entrypoint.sh @@ -18,7 +18,6 @@ until curl --silent --output /dev/null http://elasticsearch:9200/_cat/health?h=s done # Gather static files -npm install && npm run build python3 manage.py collectstatic --noinput # Check if we are running for the first time diff --git a/docker/docker-compose.prod.yml b/docker/docker-compose.prod.yml index e331b2e71..f7dd31159 100644 --- a/docker/docker-compose.prod.yml +++ b/docker/docker-compose.prod.yml @@ -7,6 +7,8 @@ # Many code templates and inspirations are taken from blog post: # https://testdriven.io/blog/dockerizing-django-with-postgres-gunicorn-and-nginx/ +# Run this file with compose by specifying this file and the base compose file: +# docker compose -f docker-compose.prod.yml -f docker-compose.yml up version: "3.4" services: @@ -22,6 +24,14 @@ services: depends_on: - postgres - elasticsearch + environment: + # Do not change these variables + - DATABASE_HOST=postgres + - ELASTICSEARCH_HOST=elasticsearch + - DEBUG=False + - ALLOWED_HOSTS=django-api + # These variables may be customised + - GAME=MTG # nginx serving the frontend nginx: @@ -31,7 +41,7 @@ services: context: .. dockerfile: ./docker/nginx/Dockerfile ports: - - "${FRONTEND_PORT}:80" + - "80:80" # TODO: support ports other than 80 postgres: expose: diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml index 3b7da6886..148b2e82c 100644 --- a/docker/docker-compose.yml +++ b/docker/docker-compose.yml @@ -28,9 +28,9 @@ services: - discovery.type=single-node - logger.level=WARN volumes: - - elaticsearch_data:/usr/share/elasticsearch/data + - elasticsearch_data:/usr/share/elasticsearch/data # Persistent storage for containers volumes: postgres_data: - elaticsearch_data: + elasticsearch_data: diff --git a/docker/nginx/Dockerfile b/docker/nginx/Dockerfile index 01d6a457a..04fa04dca 100644 --- a/docker/nginx/Dockerfile +++ b/docker/nginx/Dockerfile @@ -1,16 +1,15 @@ # Docker to serve the static Next.js frontend via nginx FROM nginx:1.21-alpine -# Copy nginx config -RUN rm /etc/nginx/conf.d/default.conf -COPY docker/nginx/nginx.conf /etc/nginx/conf.d - # Copy common and frontend files COPY frontend /frontend RUN rm -rf /frontend/out COPY common /common WORKDIR /frontend +# Point frontend at the correct URL to communicate with the backend +RUN echo "NEXT_PUBLIC_BACKEND_URL=http://localhost:80" > /frontend/.env.local + # install npm RUN apk add --update npm @@ -21,3 +20,7 @@ RUN npx next build # Copy our static site files into the directory that the nginx docker container consumes files from RUN cp -r out/* /usr/share/nginx/html + +# Copy nginx config +RUN rm /etc/nginx/conf.d/default.conf +COPY docker/nginx/nginx.conf /etc/nginx/conf.d diff --git a/docker/nginx/nginx.conf b/docker/nginx/nginx.conf index 8e111cc44..2237bd955 100644 --- a/docker/nginx/nginx.conf +++ b/docker/nginx/nginx.conf @@ -1,8 +1,15 @@ +upstream django-api { + server django:8000; +} + server { listen 80 default_server; listen [::]:80 default_server; root /usr/share/nginx/html; index index.html; + location /2/ { + proxy_pass http://django-api; + } location / { # remove .html extension. retrieved from https://stackoverflow.com/a/38238001 if ($request_uri ~ ^/(.*)\.html(\?|$)) { diff --git a/frontend/package-lock.json b/frontend/package-lock.json index a8a82e5ed..c104eddad 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -34,6 +34,7 @@ "react-dropzone": "^14.2.3", "react-redux": "^8.1.1", "react-render-if-visible": "^2.1.1", + "react-use": "^17.5.0", "styled-components": "^5.3.9", "typescript": "^4.9.4", "ua-parser-js": "^1.0.35", @@ -2904,6 +2905,11 @@ "node": ">=10.0.0" } }, + "node_modules/@xobotyi/scrollbar-width": { + "version": "1.9.5", + "resolved": "https://registry.npmjs.org/@xobotyi/scrollbar-width/-/scrollbar-width-1.9.5.tgz", + "integrity": "sha512-N8tkAACJx2ww8vFMneJmaAgmjAG1tnVBZJRLRcx061tmsLRZHSEZSLuGWnwPtunsSLvSqXQ2wfp7Mgqg1I+2dQ==" + }, "node_modules/@xtuc/ieee754": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", @@ -3924,6 +3930,14 @@ "node": ">= 0.6" } }, + "node_modules/copy-to-clipboard": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/copy-to-clipboard/-/copy-to-clipboard-3.3.3.tgz", + "integrity": "sha512-2KV8NhB5JqC3ky0r9PMCAZKbUHSwtEo4CwCs0KXgruG43gX5PMqDEBbVU4OUzw2MuAWUfsuFmWvEKG5QRfSnJA==", + "dependencies": { + "toggle-selection": "^1.0.6" + } + }, "node_modules/cosmiconfig": { "version": "8.1.3", "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-8.1.3.tgz", @@ -3972,6 +3986,14 @@ "node": ">=4" } }, + "node_modules/css-in-js-utils": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/css-in-js-utils/-/css-in-js-utils-3.1.0.tgz", + "integrity": "sha512-fJAcud6B3rRu+KHYk+Bwf+WFL2MDCJJ1XG9x137tJQ0xYxor7XziQtuGFbWNdqrvF4Tk26O3H73nfVqXt/fW1A==", + "dependencies": { + "hyphenate-style-name": "^1.0.3" + } + }, "node_modules/css-to-react-native": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/css-to-react-native/-/css-to-react-native-3.2.0.tgz", @@ -3982,6 +4004,18 @@ "postcss-value-parser": "^4.0.2" } }, + "node_modules/css-tree": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-1.1.3.tgz", + "integrity": "sha512-tRpdppF7TRazZrjJ6v3stzv93qxRcSsFmW6cX0Zm2NVKpxE1WV1HblnghVv9TreireHkqI/VDEsfolRF1p6y7Q==", + "dependencies": { + "mdn-data": "2.0.14", + "source-map": "^0.6.1" + }, + "engines": { + "node": ">=8.0.0" + } + }, "node_modules/css.escape": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/css.escape/-/css.escape-1.5.1.tgz", @@ -4007,9 +4041,9 @@ } }, "node_modules/csstype": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.1.tgz", - "integrity": "sha512-DJR/VvkAvSZW9bTouZue2sSxDwdTN92uHjqeKVm+0dAqdfNykRzQ95tay8aXMBAAPpUiq4Qcug2L7neoRh2Egw==" + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", + "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==" }, "node_modules/d": { "version": "1.0.1", @@ -4301,6 +4335,14 @@ "is-arrayish": "^0.2.1" } }, + "node_modules/error-stack-parser": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/error-stack-parser/-/error-stack-parser-2.1.4.tgz", + "integrity": "sha512-Sk5V6wVazPhq5MhpO+AUxJn5x7XSXGl1R93Vn7i+zS15KDVxQijejNCrz8340/2bgLBjR9GtEG8ZVKONDjcqGQ==", + "dependencies": { + "stackframe": "^1.3.4" + } + }, "node_modules/es-abstract": { "version": "1.21.2", "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.21.2.tgz", @@ -5381,6 +5423,21 @@ "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", "dev": true }, + "node_modules/fast-loops": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/fast-loops/-/fast-loops-1.1.3.tgz", + "integrity": "sha512-8EZzEP0eKkEEVX+drtd9mtuQ+/QrlfW/5MlwcwK5Nds6EkZ/tRzEexkzUY2mIssnAyVLT+TKHuRXmFNNXYUd6g==" + }, + "node_modules/fast-shallow-equal": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fast-shallow-equal/-/fast-shallow-equal-1.0.0.tgz", + "integrity": "sha512-HPtaa38cPgWvaCFmRNhlc6NG7pv6NUHqjPgVAkWGoB9mQMwYB27/K0CvOM5Czy+qpT3e8XJ6Q4aPAnzpNpzNaw==" + }, + "node_modules/fastest-stable-stringify": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/fastest-stable-stringify/-/fastest-stable-stringify-2.0.2.tgz", + "integrity": "sha512-bijHueCGd0LqqNK9b5oCMHc0MluJAx0cwqASgbWMvkO01lCYgIhacVRLcaDz3QnyYIRNJRDwMb41VuT6pHJ91Q==" + }, "node_modules/fastq": { "version": "1.15.0", "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.15.0.tgz", @@ -5991,6 +6048,11 @@ "node": ">=10.17.0" } }, + "node_modules/hyphenate-style-name": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/hyphenate-style-name/-/hyphenate-style-name-1.0.4.tgz", + "integrity": "sha512-ygGZLjmXfPHj+ZWh6LwbC37l43MhfztxetbFCoYTM2VjkIUpeHgSNn7QIyVFj7YQ1Wl9Cbw5sholVJPzWvC2MQ==" + }, "node_modules/iconv-lite": { "version": "0.4.24", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", @@ -6114,6 +6176,15 @@ "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" }, + "node_modules/inline-style-prefixer": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/inline-style-prefixer/-/inline-style-prefixer-7.0.0.tgz", + "integrity": "sha512-I7GEdScunP1dQ6IM2mQWh6v0mOYdYmH3Bp31UecKdrcUgcURTcctSe1IECdUznSHKSmsHtjrT3CwCPI1pyxfUQ==", + "dependencies": { + "css-in-js-utils": "^3.1.0", + "fast-loops": "^1.1.3" + } + }, "node_modules/inquirer": { "version": "8.2.5", "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-8.2.5.tgz", @@ -7826,6 +7897,11 @@ "tmpl": "1.0.5" } }, + "node_modules/mdn-data": { + "version": "2.0.14", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.14.tgz", + "integrity": "sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow==" + }, "node_modules/memoize-one": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-6.0.0.tgz", @@ -8082,6 +8158,30 @@ "thenify-all": "^1.0.0" } }, + "node_modules/nano-css": { + "version": "5.6.1", + "resolved": "https://registry.npmjs.org/nano-css/-/nano-css-5.6.1.tgz", + "integrity": "sha512-T2Mhc//CepkTa3X4pUhKgbEheJHYAxD0VptuqFhDbGMUWVV2m+lkNiW/Ieuj35wrfC8Zm0l7HvssQh7zcEttSw==", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.4.15", + "css-tree": "^1.1.2", + "csstype": "^3.1.2", + "fastest-stable-stringify": "^2.0.2", + "inline-style-prefixer": "^7.0.0", + "rtl-css-js": "^1.16.1", + "stacktrace-js": "^2.0.2", + "stylis": "^4.3.0" + }, + "peerDependencies": { + "react": "*", + "react-dom": "*" + } + }, + "node_modules/nano-css/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/nanoid": { "version": "3.3.6", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.6.tgz", @@ -9211,6 +9311,50 @@ "react-dom": ">=16.6.0" } }, + "node_modules/react-universal-interface": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/react-universal-interface/-/react-universal-interface-0.6.2.tgz", + "integrity": "sha512-dg8yXdcQmvgR13RIlZbTRQOoUrDciFVoSBZILwjE2LFISxZZ8loVJKAkuzswl5js8BHda79bIb2b84ehU8IjXw==", + "peerDependencies": { + "react": "*", + "tslib": "*" + } + }, + "node_modules/react-use": { + "version": "17.5.0", + "resolved": "https://registry.npmjs.org/react-use/-/react-use-17.5.0.tgz", + "integrity": "sha512-PbfwSPMwp/hoL847rLnm/qkjg3sTRCvn6YhUZiHaUa3FA6/aNoFX79ul5Xt70O1rK+9GxSVqkY0eTwMdsR/bWg==", + "dependencies": { + "@types/js-cookie": "^2.2.6", + "@xobotyi/scrollbar-width": "^1.9.5", + "copy-to-clipboard": "^3.3.1", + "fast-deep-equal": "^3.1.3", + "fast-shallow-equal": "^1.0.0", + "js-cookie": "^2.2.1", + "nano-css": "^5.6.1", + "react-universal-interface": "^0.6.2", + "resize-observer-polyfill": "^1.5.1", + "screenfull": "^5.1.0", + "set-harmonic-interval": "^1.0.1", + "throttle-debounce": "^3.0.1", + "ts-easing": "^0.2.0", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "react": "*", + "react-dom": "*" + } + }, + "node_modules/react-use/node_modules/@types/js-cookie": { + "version": "2.2.7", + "resolved": "https://registry.npmjs.org/@types/js-cookie/-/js-cookie-2.2.7.tgz", + "integrity": "sha512-aLkWa0C0vO5b4Sr798E26QgOkss68Un0bLjs7u9qxzPT5CG+8DuNTffWES58YzJs3hrVAOs1wonycqEBqNJubA==" + }, + "node_modules/react-use/node_modules/js-cookie": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/js-cookie/-/js-cookie-2.2.1.tgz", + "integrity": "sha512-HvdH2LzI/EAZcUwA8+0nKNtWHqS+ZmijLA30RwZA0bo7ToCckjK5MkGhjED9KoRcXO6BaGI3I9UIzSA1FKFPOQ==" + }, "node_modules/readable-stream": { "version": "3.6.2", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", @@ -9433,6 +9577,14 @@ "integrity": "sha512-APM0Gt1KoXBz0iIkkdB/kfvGOwC4UuJFeG/c+yV7wSc7q96cG/kJ0HiYCnzivD9SB53cLV1MlHFNfOuPaadYSw==", "dev": true }, + "node_modules/rtl-css-js": { + "version": "1.16.1", + "resolved": "https://registry.npmjs.org/rtl-css-js/-/rtl-css-js-1.16.1.tgz", + "integrity": "sha512-lRQgou1mu19e+Ya0LsTvKrVJ5TYUbqCVPAiImX3UfLTenarvPUl1QFdvu5Z3PYmHT9RCcwIfbjRQBntExyj3Zg==", + "dependencies": { + "@babel/runtime": "^7.1.2" + } + }, "node_modules/run-async": { "version": "2.4.1", "resolved": "https://registry.npmjs.org/run-async/-/run-async-2.4.1.tgz", @@ -9638,6 +9790,17 @@ "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", "dev": true }, + "node_modules/screenfull": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/screenfull/-/screenfull-5.2.0.tgz", + "integrity": "sha512-9BakfsO2aUQN2K9Fdbj87RJIEZ82Q9IGim7FqM5OsebfoFC6ZHXgDq/KvniuLTPdeM8wY2o6Dj3WQ7KeQCj3cA==", + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/semver": { "version": "7.5.4", "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", @@ -9669,6 +9832,14 @@ "integrity": "sha512-RVnVQxTXuerk653XfuliOxBP81Sf0+qfQE73LIYKcyMYHG94AuH0kgrQpRDuTZnSmjpysHmzxJXKNfa6PjFhyQ==", "dev": true }, + "node_modules/set-harmonic-interval": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/set-harmonic-interval/-/set-harmonic-interval-1.0.1.tgz", + "integrity": "sha512-AhICkFV84tBP1aWqPwLZqFvAwqEoVA9kxNMniGEUvzOlm4vLmOFLiTT3UZ6bziJTy4bOVpzWGTfSCbmaayGx8g==", + "engines": { + "node": ">=6.9" + } + }, "node_modules/shallowequal": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/shallowequal/-/shallowequal-1.1.0.tgz", @@ -9734,7 +9905,6 @@ "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true, "engines": { "node": ">=0.10.0" } @@ -9763,6 +9933,14 @@ "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", "dev": true }, + "node_modules/stack-generator": { + "version": "2.0.10", + "resolved": "https://registry.npmjs.org/stack-generator/-/stack-generator-2.0.10.tgz", + "integrity": "sha512-mwnua/hkqM6pF4k8SnmZ2zfETsRUpWXREfA/goT8SLCV4iOFa4bzOX2nDipWAZFPTjLvQB82f5yaodMVhK0yJQ==", + "dependencies": { + "stackframe": "^1.3.4" + } + }, "node_modules/stack-utils": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz", @@ -9784,6 +9962,38 @@ "node": ">=8" } }, + "node_modules/stackframe": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/stackframe/-/stackframe-1.3.4.tgz", + "integrity": "sha512-oeVtt7eWQS+Na6F//S4kJ2K2VbRlS9D43mAlMyVpVWovy9o+jfgH8O9agzANzaiLjclA0oYzUXEM4PurhSUChw==" + }, + "node_modules/stacktrace-gps": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/stacktrace-gps/-/stacktrace-gps-3.1.2.tgz", + "integrity": "sha512-GcUgbO4Jsqqg6RxfyTHFiPxdPqF+3LFmQhm7MgCuYQOYuWyqxo5pwRPz5d/u6/WYJdEnWfK4r+jGbyD8TSggXQ==", + "dependencies": { + "source-map": "0.5.6", + "stackframe": "^1.3.4" + } + }, + "node_modules/stacktrace-gps/node_modules/source-map": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.6.tgz", + "integrity": "sha512-MjZkVp0NHr5+TPihLcadqnlVoGIoWo4IBHptutGh9wI3ttUYvCG26HkSuDi+K6lsZ25syXJXcctwgyVCt//xqA==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/stacktrace-js": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/stacktrace-js/-/stacktrace-js-2.0.2.tgz", + "integrity": "sha512-Je5vBeY4S1r/RnLydLl0TBTi3F2qdfWmYsGvtfZgEI+SCprPppaIhQf5nGcal4gI4cGpCV/duLcAzT1np6sQqg==", + "dependencies": { + "error-stack-parser": "^2.0.6", + "stack-generator": "^2.0.5", + "stacktrace-gps": "^3.0.4" + } + }, "node_modules/standard": { "version": "17.0.0", "resolved": "https://registry.npmjs.org/standard/-/standard-17.0.0.tgz", @@ -10121,6 +10331,11 @@ } } }, + "node_modules/stylis": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.3.1.tgz", + "integrity": "sha512-EQepAV+wMsIaGVGX1RECzgrcqRRU/0sYOHkeLsZ3fzHaHXZy4DaOOX0vOlGQdlsjkh3mFHAIlVimpwAs4dslyQ==" + }, "node_modules/supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", @@ -10311,6 +10526,14 @@ "node": ">=0.8" } }, + "node_modules/throttle-debounce": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/throttle-debounce/-/throttle-debounce-3.0.1.tgz", + "integrity": "sha512-dTEWWNu6JmeVXY0ZYoPuH5cRIwc0MeGbJwah9KUNYSJwommQpCzTySTpEe8Gs1J23aeWEuAobe4Ag7EHVt/LOg==", + "engines": { + "node": ">=10" + } + }, "node_modules/through": { "version": "2.3.8", "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", @@ -10379,6 +10602,11 @@ "node": ">=8.0" } }, + "node_modules/toggle-selection": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/toggle-selection/-/toggle-selection-1.0.6.tgz", + "integrity": "sha512-BiZS+C1OS8g/q2RRbJmy59xpyghNBqrr6k5L/uKBGRsTfxmu3ffiRnd8mlGPUVayg8pvfi5urfnu8TU7DVOkLQ==" + }, "node_modules/tough-cookie": { "version": "4.1.3", "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.3.tgz", @@ -10406,6 +10634,11 @@ "node": ">=14" } }, + "node_modules/ts-easing": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/ts-easing/-/ts-easing-0.2.0.tgz", + "integrity": "sha512-Z86EW+fFFh/IFB1fqQ3/+7Zpf9t2ebOAxNI/V6Wo7r5gqiqtxmgTlQ1qbqQcjLKYeSHPTsEmvlJUDg/EuL0uHQ==" + }, "node_modules/ts-loader": { "version": "9.4.2", "resolved": "https://registry.npmjs.org/ts-loader/-/ts-loader-9.4.2.tgz", @@ -13268,6 +13501,11 @@ "integrity": "sha512-uRjjusqpoqfmRkTaNuLJ2VohVr67Q5YwDATW3VU7PfzTj6IRaihGrYI7zckGZjxQPBIp63nfvJbM+Yu5ICh0Bg==", "dev": true }, + "@xobotyi/scrollbar-width": { + "version": "1.9.5", + "resolved": "https://registry.npmjs.org/@xobotyi/scrollbar-width/-/scrollbar-width-1.9.5.tgz", + "integrity": "sha512-N8tkAACJx2ww8vFMneJmaAgmjAG1tnVBZJRLRcx061tmsLRZHSEZSLuGWnwPtunsSLvSqXQ2wfp7Mgqg1I+2dQ==" + }, "@xtuc/ieee754": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", @@ -13992,6 +14230,14 @@ "integrity": "sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA==", "dev": true }, + "copy-to-clipboard": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/copy-to-clipboard/-/copy-to-clipboard-3.3.3.tgz", + "integrity": "sha512-2KV8NhB5JqC3ky0r9PMCAZKbUHSwtEo4CwCs0KXgruG43gX5PMqDEBbVU4OUzw2MuAWUfsuFmWvEKG5QRfSnJA==", + "requires": { + "toggle-selection": "^1.0.6" + } + }, "cosmiconfig": { "version": "8.1.3", "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-8.1.3.tgz", @@ -14028,6 +14274,14 @@ "resolved": "https://registry.npmjs.org/css-color-keywords/-/css-color-keywords-1.0.0.tgz", "integrity": "sha512-FyyrDHZKEjXDpNJYvVsV960FiqQyXc/LlYmsxl2BcdMb2WPx0OGRVgTg55rPSyLSNMqP52R9r8geSp7apN3Ofg==" }, + "css-in-js-utils": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/css-in-js-utils/-/css-in-js-utils-3.1.0.tgz", + "integrity": "sha512-fJAcud6B3rRu+KHYk+Bwf+WFL2MDCJJ1XG9x137tJQ0xYxor7XziQtuGFbWNdqrvF4Tk26O3H73nfVqXt/fW1A==", + "requires": { + "hyphenate-style-name": "^1.0.3" + } + }, "css-to-react-native": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/css-to-react-native/-/css-to-react-native-3.2.0.tgz", @@ -14038,6 +14292,15 @@ "postcss-value-parser": "^4.0.2" } }, + "css-tree": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-1.1.3.tgz", + "integrity": "sha512-tRpdppF7TRazZrjJ6v3stzv93qxRcSsFmW6cX0Zm2NVKpxE1WV1HblnghVv9TreireHkqI/VDEsfolRF1p6y7Q==", + "requires": { + "mdn-data": "2.0.14", + "source-map": "^0.6.1" + } + }, "css.escape": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/css.escape/-/css.escape-1.5.1.tgz", @@ -14060,9 +14323,9 @@ } }, "csstype": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.1.tgz", - "integrity": "sha512-DJR/VvkAvSZW9bTouZue2sSxDwdTN92uHjqeKVm+0dAqdfNykRzQ95tay8aXMBAAPpUiq4Qcug2L7neoRh2Egw==" + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", + "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==" }, "d": { "version": "1.0.1", @@ -14286,6 +14549,14 @@ "is-arrayish": "^0.2.1" } }, + "error-stack-parser": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/error-stack-parser/-/error-stack-parser-2.1.4.tgz", + "integrity": "sha512-Sk5V6wVazPhq5MhpO+AUxJn5x7XSXGl1R93Vn7i+zS15KDVxQijejNCrz8340/2bgLBjR9GtEG8ZVKONDjcqGQ==", + "requires": { + "stackframe": "^1.3.4" + } + }, "es-abstract": { "version": "1.21.2", "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.21.2.tgz", @@ -15082,6 +15353,21 @@ "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", "dev": true }, + "fast-loops": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/fast-loops/-/fast-loops-1.1.3.tgz", + "integrity": "sha512-8EZzEP0eKkEEVX+drtd9mtuQ+/QrlfW/5MlwcwK5Nds6EkZ/tRzEexkzUY2mIssnAyVLT+TKHuRXmFNNXYUd6g==" + }, + "fast-shallow-equal": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fast-shallow-equal/-/fast-shallow-equal-1.0.0.tgz", + "integrity": "sha512-HPtaa38cPgWvaCFmRNhlc6NG7pv6NUHqjPgVAkWGoB9mQMwYB27/K0CvOM5Czy+qpT3e8XJ6Q4aPAnzpNpzNaw==" + }, + "fastest-stable-stringify": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/fastest-stable-stringify/-/fastest-stable-stringify-2.0.2.tgz", + "integrity": "sha512-bijHueCGd0LqqNK9b5oCMHc0MluJAx0cwqASgbWMvkO01lCYgIhacVRLcaDz3QnyYIRNJRDwMb41VuT6pHJ91Q==" + }, "fastq": { "version": "1.15.0", "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.15.0.tgz", @@ -15513,6 +15799,11 @@ "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", "dev": true }, + "hyphenate-style-name": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/hyphenate-style-name/-/hyphenate-style-name-1.0.4.tgz", + "integrity": "sha512-ygGZLjmXfPHj+ZWh6LwbC37l43MhfztxetbFCoYTM2VjkIUpeHgSNn7QIyVFj7YQ1Wl9Cbw5sholVJPzWvC2MQ==" + }, "iconv-lite": { "version": "0.4.24", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", @@ -15591,6 +15882,15 @@ "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" }, + "inline-style-prefixer": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/inline-style-prefixer/-/inline-style-prefixer-7.0.0.tgz", + "integrity": "sha512-I7GEdScunP1dQ6IM2mQWh6v0mOYdYmH3Bp31UecKdrcUgcURTcctSe1IECdUznSHKSmsHtjrT3CwCPI1pyxfUQ==", + "requires": { + "css-in-js-utils": "^3.1.0", + "fast-loops": "^1.1.3" + } + }, "inquirer": { "version": "8.2.5", "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-8.2.5.tgz", @@ -16872,6 +17172,11 @@ "tmpl": "1.0.5" } }, + "mdn-data": { + "version": "2.0.14", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.14.tgz", + "integrity": "sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow==" + }, "memoize-one": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-6.0.0.tgz", @@ -17054,6 +17359,28 @@ "thenify-all": "^1.0.0" } }, + "nano-css": { + "version": "5.6.1", + "resolved": "https://registry.npmjs.org/nano-css/-/nano-css-5.6.1.tgz", + "integrity": "sha512-T2Mhc//CepkTa3X4pUhKgbEheJHYAxD0VptuqFhDbGMUWVV2m+lkNiW/Ieuj35wrfC8Zm0l7HvssQh7zcEttSw==", + "requires": { + "@jridgewell/sourcemap-codec": "^1.4.15", + "css-tree": "^1.1.2", + "csstype": "^3.1.2", + "fastest-stable-stringify": "^2.0.2", + "inline-style-prefixer": "^7.0.0", + "rtl-css-js": "^1.16.1", + "stacktrace-js": "^2.0.2", + "stylis": "^4.3.0" + }, + "dependencies": { + "@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==" + } + } + }, "nanoid": { "version": "3.3.6", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.6.tgz", @@ -17826,6 +18153,45 @@ "prop-types": "^15.6.2" } }, + "react-universal-interface": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/react-universal-interface/-/react-universal-interface-0.6.2.tgz", + "integrity": "sha512-dg8yXdcQmvgR13RIlZbTRQOoUrDciFVoSBZILwjE2LFISxZZ8loVJKAkuzswl5js8BHda79bIb2b84ehU8IjXw==", + "requires": {} + }, + "react-use": { + "version": "17.5.0", + "resolved": "https://registry.npmjs.org/react-use/-/react-use-17.5.0.tgz", + "integrity": "sha512-PbfwSPMwp/hoL847rLnm/qkjg3sTRCvn6YhUZiHaUa3FA6/aNoFX79ul5Xt70O1rK+9GxSVqkY0eTwMdsR/bWg==", + "requires": { + "@types/js-cookie": "^2.2.6", + "@xobotyi/scrollbar-width": "^1.9.5", + "copy-to-clipboard": "^3.3.1", + "fast-deep-equal": "^3.1.3", + "fast-shallow-equal": "^1.0.0", + "js-cookie": "^2.2.1", + "nano-css": "^5.6.1", + "react-universal-interface": "^0.6.2", + "resize-observer-polyfill": "^1.5.1", + "screenfull": "^5.1.0", + "set-harmonic-interval": "^1.0.1", + "throttle-debounce": "^3.0.1", + "ts-easing": "^0.2.0", + "tslib": "^2.1.0" + }, + "dependencies": { + "@types/js-cookie": { + "version": "2.2.7", + "resolved": "https://registry.npmjs.org/@types/js-cookie/-/js-cookie-2.2.7.tgz", + "integrity": "sha512-aLkWa0C0vO5b4Sr798E26QgOkss68Un0bLjs7u9qxzPT5CG+8DuNTffWES58YzJs3hrVAOs1wonycqEBqNJubA==" + }, + "js-cookie": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/js-cookie/-/js-cookie-2.2.1.tgz", + "integrity": "sha512-HvdH2LzI/EAZcUwA8+0nKNtWHqS+ZmijLA30RwZA0bo7ToCckjK5MkGhjED9KoRcXO6BaGI3I9UIzSA1FKFPOQ==" + } + } + }, "readable-stream": { "version": "3.6.2", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", @@ -17990,6 +18356,14 @@ "integrity": "sha512-APM0Gt1KoXBz0iIkkdB/kfvGOwC4UuJFeG/c+yV7wSc7q96cG/kJ0HiYCnzivD9SB53cLV1MlHFNfOuPaadYSw==", "dev": true }, + "rtl-css-js": { + "version": "1.16.1", + "resolved": "https://registry.npmjs.org/rtl-css-js/-/rtl-css-js-1.16.1.tgz", + "integrity": "sha512-lRQgou1mu19e+Ya0LsTvKrVJ5TYUbqCVPAiImX3UfLTenarvPUl1QFdvu5Z3PYmHT9RCcwIfbjRQBntExyj3Zg==", + "requires": { + "@babel/runtime": "^7.1.2" + } + }, "run-async": { "version": "2.4.1", "resolved": "https://registry.npmjs.org/run-async/-/run-async-2.4.1.tgz", @@ -18113,6 +18487,11 @@ } } }, + "screenfull": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/screenfull/-/screenfull-5.2.0.tgz", + "integrity": "sha512-9BakfsO2aUQN2K9Fdbj87RJIEZ82Q9IGim7FqM5OsebfoFC6ZHXgDq/KvniuLTPdeM8wY2o6Dj3WQ7KeQCj3cA==" + }, "semver": { "version": "7.5.4", "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", @@ -18138,6 +18517,11 @@ "integrity": "sha512-RVnVQxTXuerk653XfuliOxBP81Sf0+qfQE73LIYKcyMYHG94AuH0kgrQpRDuTZnSmjpysHmzxJXKNfa6PjFhyQ==", "dev": true }, + "set-harmonic-interval": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/set-harmonic-interval/-/set-harmonic-interval-1.0.1.tgz", + "integrity": "sha512-AhICkFV84tBP1aWqPwLZqFvAwqEoVA9kxNMniGEUvzOlm4vLmOFLiTT3UZ6bziJTy4bOVpzWGTfSCbmaayGx8g==" + }, "shallowequal": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/shallowequal/-/shallowequal-1.1.0.tgz", @@ -18190,8 +18574,7 @@ "source-map": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==" }, "source-map-js": { "version": "1.0.2", @@ -18214,6 +18597,14 @@ "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", "dev": true }, + "stack-generator": { + "version": "2.0.10", + "resolved": "https://registry.npmjs.org/stack-generator/-/stack-generator-2.0.10.tgz", + "integrity": "sha512-mwnua/hkqM6pF4k8SnmZ2zfETsRUpWXREfA/goT8SLCV4iOFa4bzOX2nDipWAZFPTjLvQB82f5yaodMVhK0yJQ==", + "requires": { + "stackframe": "^1.3.4" + } + }, "stack-utils": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz", @@ -18231,6 +18622,37 @@ } } }, + "stackframe": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/stackframe/-/stackframe-1.3.4.tgz", + "integrity": "sha512-oeVtt7eWQS+Na6F//S4kJ2K2VbRlS9D43mAlMyVpVWovy9o+jfgH8O9agzANzaiLjclA0oYzUXEM4PurhSUChw==" + }, + "stacktrace-gps": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/stacktrace-gps/-/stacktrace-gps-3.1.2.tgz", + "integrity": "sha512-GcUgbO4Jsqqg6RxfyTHFiPxdPqF+3LFmQhm7MgCuYQOYuWyqxo5pwRPz5d/u6/WYJdEnWfK4r+jGbyD8TSggXQ==", + "requires": { + "source-map": "0.5.6", + "stackframe": "^1.3.4" + }, + "dependencies": { + "source-map": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.6.tgz", + "integrity": "sha512-MjZkVp0NHr5+TPihLcadqnlVoGIoWo4IBHptutGh9wI3ttUYvCG26HkSuDi+K6lsZ25syXJXcctwgyVCt//xqA==" + } + } + }, + "stacktrace-js": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/stacktrace-js/-/stacktrace-js-2.0.2.tgz", + "integrity": "sha512-Je5vBeY4S1r/RnLydLl0TBTi3F2qdfWmYsGvtfZgEI+SCprPppaIhQf5nGcal4gI4cGpCV/duLcAzT1np6sQqg==", + "requires": { + "error-stack-parser": "^2.0.6", + "stack-generator": "^2.0.5", + "stacktrace-gps": "^3.0.4" + } + }, "standard": { "version": "17.0.0", "resolved": "https://registry.npmjs.org/standard/-/standard-17.0.0.tgz", @@ -18449,6 +18871,11 @@ "client-only": "0.0.1" } }, + "stylis": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.3.1.tgz", + "integrity": "sha512-EQepAV+wMsIaGVGX1RECzgrcqRRU/0sYOHkeLsZ3fzHaHXZy4DaOOX0vOlGQdlsjkh3mFHAIlVimpwAs4dslyQ==" + }, "supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", @@ -18583,6 +19010,11 @@ "thenify": ">= 3.1.0 < 4" } }, + "throttle-debounce": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/throttle-debounce/-/throttle-debounce-3.0.1.tgz", + "integrity": "sha512-dTEWWNu6JmeVXY0ZYoPuH5cRIwc0MeGbJwah9KUNYSJwommQpCzTySTpEe8Gs1J23aeWEuAobe4Ag7EHVt/LOg==" + }, "through": { "version": "2.3.8", "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", @@ -18642,6 +19074,11 @@ "is-number": "^7.0.0" } }, + "toggle-selection": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/toggle-selection/-/toggle-selection-1.0.6.tgz", + "integrity": "sha512-BiZS+C1OS8g/q2RRbJmy59xpyghNBqrr6k5L/uKBGRsTfxmu3ffiRnd8mlGPUVayg8pvfi5urfnu8TU7DVOkLQ==" + }, "tough-cookie": { "version": "4.1.3", "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.3.tgz", @@ -18663,6 +19100,11 @@ "punycode": "^2.3.0" } }, + "ts-easing": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/ts-easing/-/ts-easing-0.2.0.tgz", + "integrity": "sha512-Z86EW+fFFh/IFB1fqQ3/+7Zpf9t2ebOAxNI/V6Wo7r5gqiqtxmgTlQ1qbqQcjLKYeSHPTsEmvlJUDg/EuL0uHQ==" + }, "ts-loader": { "version": "9.4.2", "resolved": "https://registry.npmjs.org/ts-loader/-/ts-loader-9.4.2.tgz", diff --git a/frontend/package.json b/frontend/package.json index 00f545019..389ac8f12 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -38,6 +38,7 @@ "react-dropzone": "^14.2.3", "react-redux": "^8.1.1", "react-render-if-visible": "^2.1.1", + "react-use": "^17.5.0", "styled-components": "^5.3.9", "typescript": "^4.9.4", "ua-parser-js": "^1.0.35", diff --git a/frontend/src/app/app.tsx b/frontend/src/app/app.tsx index 8efd6e953..54963cb30 100644 --- a/frontend/src/app/app.tsx +++ b/frontend/src/app/app.tsx @@ -8,13 +8,11 @@ import Col from "react-bootstrap/Col"; import Row from "react-bootstrap/Row"; import styled from "styled-components"; -import { NavbarHeight } from "@/common/constants"; -import { useAppDispatch, useAppSelector } from "@/common/types"; +import { NavbarHeight, RibbonHeight } from "@/common/constants"; +import { useAppSelector } from "@/common/types"; import { NoBackendDefault } from "@/components/noBackendDefault"; -import { - selectBackendURL, - useBackendConfigured, -} from "@/features/backend/backendSlice"; +import { useBackendConfigured } from "@/features/backend/backendSlice"; +import { SelectedImagesRibbon } from "@/features/bulkManagement/bulkManagementRibbon"; import { CardGrid } from "@/features/card/cardGrid"; import { CommonCardback } from "@/features/card/commonCardback"; import { Export } from "@/features/export/export"; @@ -24,16 +22,20 @@ import { selectIsProjectEmpty, selectProjectCardback, } from "@/features/project/projectSlice"; -import { fetchSourceDocumentsAndReportError } from "@/features/search/sourceDocumentsSlice"; import { SearchSettings } from "@/features/searchSettings/searchSettings"; import { Status } from "@/features/status/status"; +const FixedHeightRow = styled(Row)` + height: ${RibbonHeight}px; + box-shadow: 0 -1px 0 rgb(255, 255, 255, 50%) inset; +`; + const OverflowCol = styled(Col)` position: relative; - height: calc( - 100vh - ${NavbarHeight}px - ); // for compatibility with older browsers - height: calc(100dvh - ${NavbarHeight}px); // handles the ios address bar + // define height twice - first as a fallback for older browser compatibility, + // then using dvh to account for the ios address bar + height: calc(100vh - ${NavbarHeight}px - ${RibbonHeight}px); + height: calc(100dvh - ${NavbarHeight}px - ${RibbonHeight}px); overflow-y: scroll; overscroll-behavior: none; scrollbar-width: thin; @@ -43,9 +45,7 @@ function App() { // TODO: should we periodically ping the backend to make sure it's still alive? //# region queries and hooks - const dispatch = useAppDispatch(); const backendConfigured = useBackendConfigured(); - const backendURL = useAppSelector(selectBackendURL); const cardback = useAppSelector(selectProjectCardback); const isProjectEmpty = useAppSelector(selectIsProjectEmpty); @@ -76,6 +76,9 @@ function App() { <> {backendConfigured ? ( + + + @@ -89,13 +92,13 @@ function App() { className="px-2" > - + - + - + diff --git a/frontend/src/common/constants.ts b/frontend/src/common/constants.ts index a4c68aac1..c92f3ed27 100644 --- a/frontend/src/common/constants.ts +++ b/frontend/src/common/constants.ts @@ -30,6 +30,7 @@ export const Back: Faces = "back"; export const ToggleButtonHeight = 38; // pixels export const NavbarHeight = 50; // pixels - aligns with the natural height of the navbar +export const RibbonHeight = 54; // pixels export const NavbarLogoHeight = 40; // pixels export const ContentMaxWidth = 1200; // pixels - aligns with bootstrap's large breakpoint diff --git a/frontend/src/common/test-utils.tsx b/frontend/src/common/test-utils.tsx index 19de8b940..95909d1ab 100644 --- a/frontend/src/common/test-utils.tsx +++ b/frontend/src/common/test-utils.tsx @@ -364,13 +364,17 @@ export async function deselectSlot(slot: number, face: Faces) { ); } +export function clickMoreSelectOptionsDropdown() { + screen.getByTestId("more-select-options").click(); +} + export async function selectSimilar() { - screen.getByText("Modify").click(); + clickMoreSelectOptionsDropdown(); await waitFor(() => screen.getByText("Select Similar").click()); } export async function selectAll() { - screen.getByText("Modify").click(); + clickMoreSelectOptionsDropdown(); await waitFor(() => screen.getByText("Select All").click()); } @@ -383,13 +387,13 @@ export async function changeQueries(query: string) { } export async function changeQueryForSelectedImages(query: string) { - screen.getByText("Modify").click(); + clickMoreSelectOptionsDropdown(); await waitFor(() => screen.getByText("Change Query").click()); await changeQueries(query); } export async function changeImageForSelectedImages(cardName: string) { - screen.getByText("Modify").click(); + clickMoreSelectOptionsDropdown(); await waitFor(() => screen.getByText("Change Version").click()); await waitFor(() => expect(screen.getByText("Option 1"))); await waitFor(() => @@ -400,13 +404,13 @@ export async function changeImageForSelectedImages(cardName: string) { } export async function clearQueriesForSelectedImages() { - screen.getByText("Modify").click(); + clickMoreSelectOptionsDropdown(); await waitFor(() => screen.getByText("Clear Query").click()); } export async function deleteSelectedImages() { - screen.getByText("Modify").click(); - await waitFor(() => screen.getByText("Delete Slots").click()); + clickMoreSelectOptionsDropdown(); + await waitFor(() => screen.getByText("Delete Cards").click()); } export async function openSearchSettingsModal() { diff --git a/frontend/src/components/OverflowList.tsx b/frontend/src/components/OverflowList.tsx new file mode 100644 index 000000000..bf35a12d8 --- /dev/null +++ b/frontend/src/components/OverflowList.tsx @@ -0,0 +1,162 @@ +/** + * Vendored in from https://github.com/mattrothenberg/react-overflow-list + * with some minor tweaks to reduce flickering. + */ + +import React, { useCallback, useEffect } from "react"; +import { + useMeasure, + useMount, + usePrevious, + useShallowCompareEffect, + useUpdateEffect, +} from "react-use"; + +type CollapseDirection = "start" | "end"; +type OverflowDirection = "none" | "grow" | "shrink"; + +export interface OverflowListProps { + items: T[]; + itemRenderer: (item: T, index: number) => React.ReactNode; + overflowRenderer: (items: T[]) => React.ReactNode; + minVisibleItems?: number; + onOverflow?: (items: T[]) => void; + collapseFrom?: CollapseDirection; + className?: string; + tagName?: keyof JSX.IntrinsicElements; + alwaysRenderOverflow?: boolean; +} + +interface OverflowListState { + visible: T[]; + overflow: T[]; + lastOverflowCount: number; + overflowDirection: OverflowDirection; + opacity: 1 | 0; +} + +export function OverflowList(props: OverflowListProps) { + const { + items, + collapseFrom = "end", + minVisibleItems = 0, + tagName = "div", + className = "", + alwaysRenderOverflow = false, + overflowRenderer, + itemRenderer, + } = props; + const [state, setState] = React.useState>({ + visible: items, + overflow: [], + lastOverflowCount: 0, + overflowDirection: "none", + opacity: 0, + }); + + const spacer = React.useRef(null); + + useShallowCompareEffect(() => { + repartition(false); + }, [state]); + + useMount(() => { + repartition(false); + }); + + useUpdateEffect(() => { + setState(() => ({ + overflowDirection: "none", + lastOverflowCount: 0, + overflow: [], + visible: items, + opacity: 0, + })); + }, [items]); + + const WrapperComponent = tagName; + + const maybeOverflow = + state.overflow.length === 0 && !alwaysRenderOverflow + ? null + : overflowRenderer(state.overflow); + + const repartition = useCallback( + (growing: boolean) => { + if (!spacer.current) { + return; + } + + if (growing) { + setState((state) => ({ + overflowDirection: "grow", + lastOverflowCount: + state.overflowDirection === "none" + ? state.overflow.length + : state.lastOverflowCount, + overflow: [], + visible: props.items, + opacity: 0, + })); + } else if (spacer.current.getBoundingClientRect().width < 0.9) { + setState((state) => { + if (state.visible.length <= minVisibleItems!) { + return state; + } + const collapseFromStart = collapseFrom === "start"; + const visible = state.visible.slice(); + const next = collapseFromStart ? visible.shift() : visible.pop(); + if (!next) { + return state; + } + const overflow = collapseFromStart + ? [...state.overflow, next] + : [next, ...state.overflow]; + return { + ...state, + direction: + state.overflowDirection === "none" + ? "shrink" + : state.overflowDirection, + overflow, + visible, + opacity: 0, + }; + }); + } else { + setState((prevState) => { + return { ...prevState, overflowDirection: "none", opacity: 1 }; + }); + } + }, + [collapseFrom, minVisibleItems, props.items] + ); + + const [ref, { width }] = useMeasure(); + const previousWidth = usePrevious(width); + + useEffect(() => { + if (!previousWidth) return; + + repartition(width > previousWidth); + }, [width, previousWidth]); + + return ( + + {collapseFrom === "start" ? maybeOverflow : null} + {state.visible.map(itemRenderer)} + {collapseFrom === "end" ? maybeOverflow : null} +
+ + ); +} diff --git a/frontend/src/components/jumbotron.tsx b/frontend/src/components/jumbotron.tsx new file mode 100644 index 000000000..9a457f4f0 --- /dev/null +++ b/frontend/src/components/jumbotron.tsx @@ -0,0 +1,18 @@ +import { DOMAttributes, HTMLAttributes, PropsWithChildren } from "react"; +import { Variant } from "react-bootstrap/types"; + +interface Props extends HTMLAttributes { + variant: Variant; +} + +export function Jumbotron({ + children, + variant, + ...rest +}: PropsWithChildren) { + return ( +
+
{children}
+
+ ); +} diff --git a/frontend/src/features/bulkManagement/bulkManagementStatus.test.tsx b/frontend/src/features/bulkManagement/bulkManagementRibbon.test.tsx similarity index 99% rename from frontend/src/features/bulkManagement/bulkManagementStatus.test.tsx rename to frontend/src/features/bulkManagement/bulkManagementRibbon.test.tsx index 53c1655c3..c7da0657e 100644 --- a/frontend/src/features/bulkManagement/bulkManagementStatus.test.tsx +++ b/frontend/src/features/bulkManagement/bulkManagementRibbon.test.tsx @@ -16,6 +16,7 @@ import { changeImageForSelectedImages, changeQueryForSelectedImages, clearQueriesForSelectedImages, + clickMoreSelectOptionsDropdown, deleteSelectedImages, deselectSlot, expectCardbackSlotState, @@ -203,7 +204,7 @@ test("cannot change the images of multiple selected images when they don't share await selectSlot(1, Front); await selectSlot(2, Front); - screen.getByText("Modify").click(); + clickMoreSelectOptionsDropdown(); await waitFor(() => expect(screen.queryByText("Change Version")).not.toBeInTheDocument() ); diff --git a/frontend/src/features/bulkManagement/bulkManagementRibbon.tsx b/frontend/src/features/bulkManagement/bulkManagementRibbon.tsx new file mode 100644 index 000000000..29faeece0 --- /dev/null +++ b/frontend/src/features/bulkManagement/bulkManagementRibbon.tsx @@ -0,0 +1,342 @@ +/** + * This component exposes a ribbon which displays the number of selected images + * and facilitates operating on the selected images in bulk - updating their queries, + * setting their selected versions, or deleting them from the project. + */ + +import React, { + ButtonHTMLAttributes, + PropsWithChildren, + ReactElement, + useState, +} from "react"; +import Dropdown from "react-bootstrap/Dropdown"; +import Stack from "react-bootstrap/Stack"; +import styled from "styled-components"; + +import { Faces, Slots, useAppDispatch, useAppSelector } from "@/common/types"; +import { RightPaddedIcon } from "@/components/icon"; +import { OverflowList } from "@/components/OverflowList"; +import { GridSelectorModal } from "@/features/gridSelector/gridSelectorModal"; +import { setSelectedSlotsAndShowModal } from "@/features/modals/modalsSlice"; +import { + bulkAlignMemberSelection, + bulkSetMemberSelection, + clearQueries, + deleteSlots, + selectAllSelectedProjectMembersHaveTheSameQuery, + selectAllSlotsForFace, + selectIsProjectEmpty, + selectSelectedSlots, + setSelectedImages, +} from "@/features/project/projectSlice"; +import { selectSearchResultsForQueryOrDefault } from "@/features/search/searchResultsSlice"; +import { selectActiveFace } from "@/features/viewSettings/viewSettingsSlice"; + +const RibbonText = styled.p` + font-size: 0.9em; + user-select: none; + -webkit-user-select: none; + white-space: nowrap; +`; + +const HoverableRibbonText = styled(RibbonText)` + &:hover { + background-color: rgba(100, 100, 100, 30%); + } + border-radius: 4px; + transition: background-color 0.1s ease-in-out; + cursor: pointer; +`; + +interface RibbonButtonProps + extends PropsWithChildren> { + inDropdown: boolean; +} + +function RibbonButton({ children, onClick, inDropdown }: RibbonButtonProps) { + return inDropdown ? ( + {children} + ) : ( + + {children} + + ); +} + +function SelectSimilar({ + slot, + inDropdown, +}: { + slot: [Faces, number]; + inDropdown: boolean; +}) { + /** + * Clicking this is equivalent to double-clicking a CardSlot's checkbox. + * If other slots have the same query as `slot`, clicking this will select them all. + */ + + const dispatch = useAppDispatch(); + const onClick = () => + dispatch(bulkAlignMemberSelection({ slot: slot[1], face: slot[0] })); + return ( + + Select Similar + + ); +} + +function SelectAll({ inDropdown }: { inDropdown: boolean }) { + /** + * Clicking this selects all slots in the active face. + */ + + const dispatch = useAppDispatch(); + + const face = useAppSelector(selectActiveFace); + const slots = useAppSelector((state) => selectAllSlotsForFace(state, face)); + const onClick = () => + dispatch(bulkSetMemberSelection({ selectedStatus: true, slots: slots })); + + return ( + + Select All + + ); +} + +function ChangeSelectedImageSelectedImages({ + slots, + inDropdown, +}: { + slots: Slots; + inDropdown: boolean; +}) { + /** + * Clicking this brings up the grid selector modal for changing the selected images for multiple slots at once. + * sorry for the stupid naming convention here 🗿 + */ + + const dispatch = useAppDispatch(); + + const [showModal, setShowModal] = useState(false); + const handleShowModal = () => setShowModal(true); + const handleHideModal = () => setShowModal(false); + + const handleChangeImages = (selectedImage: string): void => { + dispatch(setSelectedImages({ selectedImage, slots, deselect: true })); + handleHideModal(); + }; + + const query = useAppSelector((state) => + selectAllSelectedProjectMembersHaveTheSameQuery(state, slots) + ); + + const cardbacks = useAppSelector((state) => state.cardbacks.cardbacks) ?? []; + const searchResultsForQueryOrDefault = useAppSelector((state) => + slots.length > 0 + ? selectSearchResultsForQueryOrDefault( + state, + query, + slots[0][0], + cardbacks + ) + : undefined + ); + + return slots.length > 0 ? ( + <> + {searchResultsForQueryOrDefault != null && + searchResultsForQueryOrDefault.length > 1 && ( + + Change Version + + )} + {searchResultsForQueryOrDefault != null && ( + + )} + + ) : null; +} + +function ChangeSelectedImageQueries({ + slots, + inDropdown, +}: { + slots: Slots; + inDropdown: boolean; +}) { + /** + * Clicking this brings up the modal for changing the queries for multiple slots at once. + */ + + const dispatch = useAppDispatch(); + + const handleShowModal = () => { + dispatch(setSelectedSlotsAndShowModal([slots, "changeQuery"])); + }; + + return ( + + Change Query + + ); +} + +function ClearSelectedImageQueries({ + slots, + inDropdown, +}: { + slots: Slots; + inDropdown: boolean; +}) { + /** + * Clicking this clears the queries for multiple slots at once. + */ + + const dispatch = useAppDispatch(); + const onClick = () => dispatch(clearQueries({ slots })); + return ( + + Clear Query + + ); +} + +function DeleteSelectedImages({ + slots, + inDropdown, +}: { + slots: Slots; + inDropdown: boolean; +}) { + /** + * Clicking this deletes multiple slots at once. + */ + + const dispatch = useAppDispatch(); + + const slotNumbers = slots.map(([face, slot]) => slot); + const onClick = () => dispatch(deleteSlots({ slots: slotNumbers })); + + return ( + + Delete Cards + + ); +} + +const VerticallyCentredStack = styled(Stack)` + position: relative; + top: calc( + 50% - 20px + ); // can't use transform: translate(0, -50%) because it messes with the dropdown +`; + +type OptionKey = + | "selectSimilar" + | "selectAll" + | "changeSelectedImageSelectedImages" + | "changeSelectedImageQueries" + | "clearSelectedImageQueries" + | "deleteSelectedImages"; + +export function SelectedImagesRibbon() { + const slots = useAppSelector(selectSelectedSlots); + const isProjectEmpty = useAppSelector(selectIsProjectEmpty); + + const dispatch = useAppDispatch(); + const onClick = () => + dispatch(bulkSetMemberSelection({ selectedStatus: false, slots })); + + const renderOption = (key: OptionKey, inDropdown: boolean): ReactElement => { + switch (key) { + case "selectSimilar": + return ; + case "selectAll": + return ; + case "changeSelectedImageSelectedImages": + return ( + + ); + case "changeSelectedImageQueries": + return ( + + ); + case "clearSelectedImageQueries": + return ( + + ); + case "deleteSelectedImages": + return ; + } + }; + const enabledOptions: Array = [ + ...((slots.length === 1 ? ["selectSimilar"] : []) as Array), + ...((!isProjectEmpty ? ["selectAll"] : []) as Array), + ...((slots.length > 0 + ? [ + "changeSelectedImageSelectedImages", + "changeSelectedImageQueries", + "clearSelectedImageQueries", + "deleteSelectedImages", + ] + : []) as Array), + ]; + + const itemRenderer = (item: OptionKey, index: number) => + renderOption(item, false); + const overflowRenderer = (items: Array) => { + return ( + + + + + + {items.map((item) => renderOption(item, true))} + + + ); + }; + + return ( + + + {slots.length} card + {slots.length != 1 && "s"} selected. + + {slots.length > 0 && ( + <> + + + + │ + + )} +
+ + + ); +} diff --git a/frontend/src/features/bulkManagement/bulkManagementStatus.tsx b/frontend/src/features/bulkManagement/bulkManagementStatus.tsx deleted file mode 100644 index 5798850ca..000000000 --- a/frontend/src/features/bulkManagement/bulkManagementStatus.tsx +++ /dev/null @@ -1,209 +0,0 @@ -/** - * This component exposes a bootstrap Alert to display the number of selected images - * and facilitates operating on the selected images in bulk - updating their queries, - * setting their selected versions, or deleting them from the project. - */ - -import React, { useState } from "react"; -import Alert from "react-bootstrap/Alert"; -import Button from "react-bootstrap/Button"; -import Dropdown from "react-bootstrap/Dropdown"; -import Stack from "react-bootstrap/Stack"; - -import { Faces, Slots, useAppDispatch, useAppSelector } from "@/common/types"; -import { RightPaddedIcon } from "@/components/icon"; -import { GridSelectorModal } from "@/features/gridSelector/gridSelectorModal"; -import { setSelectedSlotsAndShowModal } from "@/features/modals/modalsSlice"; -import { - bulkAlignMemberSelection, - bulkSetMemberSelection, - clearQueries, - deleteSlots, - selectAllSelectedProjectMembersHaveTheSameQuery, - selectAllSlotsForFace, - selectSelectedSlots, - setSelectedImages, -} from "@/features/project/projectSlice"; -import { selectSearchResultsForQueryOrDefault } from "@/features/search/searchResultsSlice"; -import { selectActiveFace } from "@/features/viewSettings/viewSettingsSlice"; - -function SelectSimilar({ slot }: { slot: [Faces, number] }) { - /** - * Clicking this is equivalent to double-clicking a CardSlot's checkbox. - * If other slots have the same query as `slot`, clicking this will select them all. - */ - - const dispatch = useAppDispatch(); - const onClick = () => - dispatch(bulkAlignMemberSelection({ slot: slot[1], face: slot[0] })); - return ( - - Select Similar - - ); -} - -function SelectAll() { - /** - * Clicking this selects all slots in the active face. - */ - - const dispatch = useAppDispatch(); - - const face = useAppSelector(selectActiveFace); - const slots = useAppSelector((state) => selectAllSlotsForFace(state, face)); - const onClick = () => - dispatch(bulkSetMemberSelection({ selectedStatus: true, slots: slots })); - - return ( - - Select All - - ); -} - -function ChangeSelectedImageSelectedImages({ slots }: { slots: Slots }) { - /** - * Clicking this brings up the grid selector modal for changing the selected images for multiple slots at once. - * sorry for the stupid naming convention here 🗿 - */ - - const dispatch = useAppDispatch(); - - const [showModal, setShowModal] = useState(false); - const handleShowModal = () => setShowModal(true); - const handleHideModal = () => setShowModal(false); - - const handleChangeImages = (selectedImage: string): void => { - dispatch(setSelectedImages({ selectedImage, slots, deselect: true })); - handleHideModal(); - }; - - const query = useAppSelector((state) => - selectAllSelectedProjectMembersHaveTheSameQuery(state, slots) - ); - - const cardbacks = useAppSelector((state) => state.cardbacks.cardbacks) ?? []; - // calling slots[0] is safe because this component will only be rendered with > 0 slots selected - const searchResultsForQueryOrDefault = useAppSelector((state) => - selectSearchResultsForQueryOrDefault(state, query, slots[0][0], cardbacks) - ); - - return ( - <> - {searchResultsForQueryOrDefault != null && - searchResultsForQueryOrDefault.length > 1 && ( - - Change Version - - )} - {searchResultsForQueryOrDefault != null && ( - - )} - - ); -} - -function ChangeSelectedImageQueries({ slots }: { slots: Slots }) { - /** - * Clicking this brings up the modal for changing the queries for multiple slots at once. - */ - - const dispatch = useAppDispatch(); - - const handleShowModal = () => { - dispatch(setSelectedSlotsAndShowModal([slots, "changeQuery"])); - }; - - return ( - <> - - Change Query - - - ); -} - -function ClearSelectedImageQueries({ slots }: { slots: Slots }) { - /** - * Clicking this clears the queries for multiple slots at once. - */ - - const dispatch = useAppDispatch(); - const onClick = () => dispatch(clearQueries({ slots })); - return ( - - Clear Query - - ); -} - -function DeleteSelectedImages({ slots }: { slots: Slots }) { - /** - * Clicking this deletes multiple slots at once. - */ - - const dispatch = useAppDispatch(); - - const slotNumbers = slots.map(([face, slot]) => slot); - const onClick = () => dispatch(deleteSlots({ slots: slotNumbers })); - - return ( - - Delete Slots - - ); -} - -export function SelectedImagesStatus() { - const slots = useAppSelector(selectSelectedSlots); - - const dispatch = useAppDispatch(); - const onClick = () => - dispatch(bulkSetMemberSelection({ selectedStatus: false, slots })); - - return ( - <> - 0 ? "" : "none" }} - > - - {slots.length} image - {slots.length != 1 && "s"} selected. - - {slots.length > 0 && ( - - Modify - - {slots.length === 1 && } - - - - - - - - - - )} - - - - ); -} diff --git a/frontend/src/features/card/__snapshots__/cardSlot.test.tsx.snap b/frontend/src/features/card/__snapshots__/cardSlot.test.tsx.snap index ea22e9176..298726755 100644 --- a/frontend/src/features/card/__snapshots__/cardSlot.test.tsx.snap +++ b/frontend/src/features/card/__snapshots__/cardSlot.test.tsx.snap @@ -468,6 +468,10 @@ exports[`the html structure of a CardSlot with multiple search results, image se `; exports[`the html structure of a CardSlot's grid selector, cards faceted by source 1`] = ` +.c0 { + padding-right: 0.5em; +} + .c2 { z-index: 1; opacity: 1; @@ -483,10 +487,6 @@ exports[`the html structure of a CardSlot's grid selector, cards faceted by sour border: solid 0px black; } -.c0 { - padding-right: 0.5em; -} - - - ) : ( - <> - ); + + ) : null; } diff --git a/frontend/src/features/mobile/mobileStatus.tsx b/frontend/src/features/mobile/mobileStatus.tsx index 669e7575a..4ea766b39 100644 --- a/frontend/src/features/mobile/mobileStatus.tsx +++ b/frontend/src/features/mobile/mobileStatus.tsx @@ -1,17 +1,15 @@ import React from "react"; -import Alert from "react-bootstrap/Alert"; import { UAParser } from "ua-parser-js"; import { ProjectName } from "@/common/constants"; +import { Jumbotron } from "@/components/jumbotron"; export function MobileStatus() { const ua = UAParser(); return ua.device.type === "mobile" ? ( - + It seems like you're on a mobile device! The {ProjectName} executable that auto-fills your order requires a desktop computer. - - ) : ( - <> - ); + + ) : null; } diff --git a/frontend/src/features/project/projectStatus.tsx b/frontend/src/features/project/projectStatus.tsx index 499ce34a0..e40147791 100644 --- a/frontend/src/features/project/projectStatus.tsx +++ b/frontend/src/features/project/projectStatus.tsx @@ -4,6 +4,7 @@ import Alert from "react-bootstrap/Alert"; import { ProjectMaxSize } from "@/common/constants"; import { useAppSelector } from "@/common/types"; import { bracket, imageSizeToMBString } from "@/common/utils"; +import { Jumbotron } from "@/components/jumbotron"; import { selectProjectFileSize, selectProjectSize, @@ -15,7 +16,7 @@ export function ProjectStatus() { const projectFileSize = useAppSelector(selectProjectFileSize); return projectSize > 0 ? ( - +

Your project contains {projectSize} card {projectSize !== 1 && "s"}, belongs in the bracket of up to{" "} @@ -28,8 +29,6 @@ export function ProjectStatus() { )} - - ) : ( - <> - ); + + ) : null; } diff --git a/frontend/src/features/search/searchStatus.tsx b/frontend/src/features/search/searchStatus.tsx index f3b3eadf2..fa6cd48a0 100644 --- a/frontend/src/features/search/searchStatus.tsx +++ b/frontend/src/features/search/searchStatus.tsx @@ -1,8 +1,8 @@ import React from "react"; -import Alert from "react-bootstrap/Alert"; import Stack from "react-bootstrap/Stack"; import { useAppSelector } from "@/common/types"; +import { Jumbotron } from "@/components/jumbotron"; import { Spinner } from "@/components/spinner"; export function SearchStatus() { @@ -10,16 +10,14 @@ export function SearchStatus() { (state) => state.cardDocuments.status === "loading" ); return fetchingCardData ? ( - - + +

Loading Card Data...
Some search results may appear incomplete until this is finished.
-
- ) : ( - <> - ); + + ) : null; } diff --git a/frontend/src/features/status/status.tsx b/frontend/src/features/status/status.tsx index 929c53a04..2e1bc3d90 100644 --- a/frontend/src/features/status/status.tsx +++ b/frontend/src/features/status/status.tsx @@ -1,6 +1,5 @@ import React from "react"; -import { SelectedImagesStatus } from "@/features/bulkManagement/bulkManagementStatus"; import { InvalidIdentifiersStatus } from "@/features/invalidIdentifiers/invalidIdentifiersStatus"; import { MobileStatus } from "@/features/mobile/mobileStatus"; import { ProjectStatus } from "@/features/project/projectStatus"; @@ -9,10 +8,8 @@ import { SearchStatus } from "@/features/search/searchStatus"; export function Status() { return ( <> -

Edit Project

- diff --git a/frontend/src/features/ui/__snapshots__/dynamicLogo.test.tsx.snap b/frontend/src/features/ui/__snapshots__/dynamicLogo.test.tsx.snap index 6b7597a63..ca0e63a22 100644 --- a/frontend/src/features/ui/__snapshots__/dynamicLogo.test.tsx.snap +++ b/frontend/src/features/ui/__snapshots__/dynamicLogo.test.tsx.snap @@ -42,6 +42,7 @@ exports[`the html structure of the dynamic logo, backend configured 1`] = ` -moz-user-select: none; -ms-user-select: none; user-select: none; + color: white; } .c2 { @@ -274,6 +275,7 @@ exports[`the html structure of the dynamic logo, no backend configured 1`] = ` -moz-user-select: none; -ms-user-select: none; user-select: none; + color: white; } .c2 { diff --git a/frontend/src/features/ui/dynamicLogo.tsx b/frontend/src/features/ui/dynamicLogo.tsx index 06c58314d..272305709 100644 --- a/frontend/src/features/ui/dynamicLogo.tsx +++ b/frontend/src/features/ui/dynamicLogo.tsx @@ -47,6 +47,7 @@ const DynamicLogoLabel = styled.p` text-shadow: 0 4px 15px #000000; white-space: nowrap; user-select: none; + color: white; `; const DynamicLogoArrowKeyframes = keyframes` @@ -262,7 +263,10 @@ export function DynamicLogo() { ] > = [ [ - sampleCardsQuery.isSuccess ? sampleCardsQuery.data["TOKEN"][0] : null, + sampleCardsQuery.isSuccess + ? sampleCardsQuery.data["TOKEN"][0] ?? + sampleCardsQuery.data["CARDBACK"][0] + : null, FirstImageTransformWrapper, ], [ diff --git a/frontend/src/features/viewSettings/viewSettings.tsx b/frontend/src/features/viewSettings/viewSettings.tsx index ffe5c24c6..6e878917d 100644 --- a/frontend/src/features/viewSettings/viewSettings.tsx +++ b/frontend/src/features/viewSettings/viewSettings.tsx @@ -21,9 +21,9 @@ export function ViewSettings() { return ( dispatch(toggleFaces())} - on="Show Fronts" + on="Switch to Backs" onClassName="flex-centre" - off="Show Backs" + off="Switch to Fronts" offClassName="flex-centre" onstyle="info" offstyle="info"