diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 000000000..03a268b8b --- /dev/null +++ b/.dockerignore @@ -0,0 +1,34 @@ +# Include any files or directories that you don't want to be copied to your +# container here (e.g., local build artifacts, temporary files, etc.). +# +# For more help, visit the .dockerignore file reference guide at +# https://docs.docker.com/go/build-context-dockerignore/ + +**/.DS_Store +**/__pycache__ +**/.venv +**/.classpath +**/.dockerignore +**/.env +**/.git +**/.gitignore +**/.project +**/.settings +**/.toolstarget +**/.vs +**/.vscode +**/*.*proj.user +**/*.dbmdl +**/*.jfm +**/bin +**/charts +**/docker-compose* +**/compose.y*ml +**/Dockerfile* +**/node_modules +**/npm-debug.log +**/obj +**/secrets.dev.yaml +**/values.dev.yaml +LICENSE +README.md diff --git a/.gitignore b/.gitignore index 1f257dfa4..710a83067 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,6 @@ e2e/npm-debug.log +**/.tmp/** +**/.temp/** + diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 000000000..bb164391e --- /dev/null +++ b/Dockerfile @@ -0,0 +1,63 @@ +# syntax=docker/dockerfile:1 + +# Comments are provided throughout this file to help you get started. +# If you need more help, visit the Dockerfile reference guide at +# https://docs.docker.com/go/dockerfile-reference/ + +# Want to help us make this template better? Share your feedback here: https://forms.gle/ybq9Krt8jtBL3iCk7 + +################################################################################ +# Pick a base image to serve as the foundation for the other build stages in +# this file. +# +# For illustrative purposes, the following FROM command +# is using the alpine image (see https://hub.docker.com/_/alpine). +# By specifying the "latest" tag, it will also use whatever happens to be the +# most recent version of that image when you build your Dockerfile. +# If reproducibility is important, consider using a versioned tag +# (e.g., alpine:3.17.2) or SHA (e.g., alpine@sha256:c41ab5c992deb4fe7e5da09f67a8804a46bd0592bfdf0b1847dde0e0889d2bff). +FROM alpine:latest as base + +################################################################################ +# Create a stage for building/compiling the application. +# +# The following commands will leverage the "base" stage above to generate +# a "hello world" script and make it executable, but for a real application, you +# would issue a RUN command for your application's build process to generate the +# executable. For language-specific examples, take a look at the Dockerfiles in +# the Awesome Compose repository: https://github.com/docker/awesome-compose +FROM base as build +RUN echo -e '#!/bin/sh\n\ +echo Hello world from $(whoami)! In order to get your application running in a container, take a look at the comments in the Dockerfile to get started.'\ +> /bin/hello.sh +RUN chmod +x /bin/hello.sh + +################################################################################ +# Create a final stage for running your application. +# +# The following commands copy the output from the "build" stage above and tell +# the container runtime to execute it when the image is run. Ideally this stage +# contains the minimal runtime dependencies for the application as to produce +# the smallest image possible. This often means using a different and smaller +# image than the one used for building the application, but for illustrative +# purposes the "base" image is used here. +FROM base AS final + +# Create a non-privileged user that the app will run under. +# See https://docs.docker.com/go/dockerfile-user-best-practices/ +ARG UID=10001 +RUN adduser \ + --disabled-password \ + --gecos "" \ + --home "/nonexistent" \ + --shell "/sbin/nologin" \ + --no-create-home \ + --uid "${UID}" \ + appuser +USER appuser + +# Copy the executable from the "build" stage. +COPY --from=build /bin/hello.sh /bin/ + +# What the container should run when it is started. +ENTRYPOINT [ "/bin/hello.sh" ] diff --git a/README.Docker.md b/README.Docker.md new file mode 100644 index 000000000..ffca34086 --- /dev/null +++ b/README.Docker.md @@ -0,0 +1,17 @@ +### Building and running your application + +When you're ready, start your application by running: +`docker compose up --build`. + +### Deploying your application to the cloud + +First, build your image, e.g.: `docker build -t myapp .`. +If your cloud uses a different CPU architecture than your development +machine (e.g., you are on a Mac M1 and your cloud provider is amd64), +you'll want to build the image for that platform, e.g.: +`docker build --platform=linux/amd64 -t myapp .`. + +Then, push it to your registry, e.g. `docker push myregistry.com/myapp`. + +Consult Docker's [getting started](https://docs.docker.com/go/get-started-sharing/) +docs for more detail on building and pushing. \ No newline at end of file diff --git a/compose.yaml b/compose.yaml new file mode 100644 index 000000000..6a19e9a59 --- /dev/null +++ b/compose.yaml @@ -0,0 +1,52 @@ +# Comments are provided throughout this file to help you get started. +# If you need more help, visit the Docker Compose reference guide at +# https://docs.docker.com/go/compose-spec-reference/ + +# Here the instructions define your application as a service called "app". +# This service is built from the Dockerfile in the current directory. +# You can add other services your application may depend on here, such as a +# database or a cache. For examples, see the Awesome Compose repository: +# https://github.com/docker/awesome-compose +services: + app: + build: + context: . + target: final + # If your application exposes a port, uncomment the following lines and change + # the port numbers as needed. The first number is the host port and the second + # is the port inside the container. + # ports: + # - 8080:8080 + + # The commented out section below is an example of how to define a PostgreSQL + # database that your application can use. `depends_on` tells Docker Compose to + # start the database before your application. The `db-data` volume persists the + # database data between container restarts. The `db-password` secret is used + # to set the database password. You must create `db/password.txt` and add + # a password of your choosing to it before running `docker compose up`. + # depends_on: + # db: + # condition: service_healthy + # db: + # image: postgres + # restart: always + # user: postgres + # secrets: + # - db-password + # volumes: + # - db-data:/var/lib/postgresql/data + # environment: + # - POSTGRES_DB=example + # - POSTGRES_PASSWORD_FILE=/run/secrets/db-password + # expose: + # - 5432 + # healthcheck: + # test: [ "CMD", "pg_isready" ] + # interval: 10s + # timeout: 5s + # retries: 5 + # volumes: + # db-data: + # secrets: + # db-password: + # file: db/password.txt diff --git a/src/API/Dockerfile b/src/API/Dockerfile index 98b03d8a7..7f1239c02 100644 --- a/src/API/Dockerfile +++ b/src/API/Dockerfile @@ -63,3 +63,4 @@ COPY --from=builder /usr/src/app/colormap.txt . ENTRYPOINT ["/entrypoint.sh"] # Start the server using the production build CMD [ "node", "dist/src/main.js" ] + \ No newline at end of file diff --git a/src/API/package-lock.json b/src/API/package-lock.json index a1ed29cef..13ea06b6e 100644 --- a/src/API/package-lock.json +++ b/src/API/package-lock.json @@ -57,6 +57,7 @@ "reflect-metadata": "^0.1.13", "rimraf": "^3.0.2", "rxjs": "^7.2.0", + "sharp": "^0.35.3", "typeorm": "^0.3.7", "uuid": "^8.3.2", "winston": "^3.17.0", @@ -1015,7 +1016,6 @@ "version": "7.24.9", "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.24.9.tgz", "integrity": "sha512-5e3FI4Q3M3Pbr21+5xJwCv6ZT6KmGkI0vw3Tozy5ODAQFTIWe37iT8Cr7Ice2Ntb+M3iSKCEWMB1MBgKrW3whg==", - "peer": true, "dependencies": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.24.7", @@ -1684,6 +1684,16 @@ "kuler": "^2.0.0" } }, + "node_modules/@emnapi/runtime": { + "version": "1.11.3", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.3.tgz", + "integrity": "sha512-Xz4Tpyki7XyrpbUK1jR1AhdAdaXyhhY4lZ3neLodmhpuWfy2PAQN5B46sAiU4liOXGLkHypn/qU+jvfWSCYYLA==", + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, "node_modules/@esbuild/aix-ppc64": { "version": "0.19.11", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.19.11.tgz", @@ -2276,6 +2286,554 @@ "deprecated": "Use @eslint/object-schema instead", "dev": true }, + "node_modules/@img/colour": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.1.0.tgz", + "integrity": "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@img/sharp-darwin-arm64": { + "version": "0.35.3", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.35.3.tgz", + "integrity": "sha512-RMnFX7YQsMoh7lWfcM4NEHHymBX/rLuKNPVM84XE9ONPcaSCDgE7CHIHpSgPcO2xcRthgBy1HfNO319mwhIAkg==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-arm64": "1.3.2" + } + }, + "node_modules/@img/sharp-darwin-x64": { + "version": "0.35.3", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.35.3.tgz", + "integrity": "sha512-Xo+5uFBtLN0BKqieTxiFzFPQAUlBbbH5iBKyRX/z1JrbnYsHTfKJnUfL8+p2TPXr1pXqao4eeL4Rl144uDpK9w==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-x64": "1.3.2" + } + }, + "node_modules/@img/sharp-freebsd-wasm32": { + "version": "0.35.3", + "resolved": "https://registry.npmjs.org/@img/sharp-freebsd-wasm32/-/sharp-freebsd-wasm32-0.35.3.tgz", + "integrity": "sha512-lUxcqWIj2wMQ9BrwNjngcr1gWUr5xgaGThBRqPPalIC2n67Cqj1uPh8NnA/ZhAg8hUbKl+kVHKwgUIwe6ZYPrg==", + "license": "Apache-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "dependencies": { + "@img/sharp-wasm32": "0.35.3" + }, + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-darwin-arm64": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.3.2.tgz", + "integrity": "sha512-9J6ypZFpQBj4YnePGoq/S38w6nz+vqg5WZLrLGY4YuSemdMq47GMLBPO42MzwdGwpg/agZ7xzZcFHa48xlywfg==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-darwin-x64": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.3.2.tgz", + "integrity": "sha512-m2pW1n6cns9VaubNwsZ+c3CRYjxNQWgJ5gPlnL1nbBcpkBvFm6SCFN5o0psFHI8w9n11NKhFkeEDns98tiqbEw==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.3.2.tgz", + "integrity": "sha512-1eMLzy92I4J6rmi4mAT8yC3HxOtniyGELlzGbNMLLeqe052ahFQ0h6LFq+lh5DsDIdYViIDst08abvSbcEdLXQ==", + "cpu": [ + "arm" + ], + "libc": [ + "glibc" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm64": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.3.2.tgz", + "integrity": "sha512-dqVSFynCox4C/J8kT16V7SIFAns0IjgLwkvYT7p8LQVmJ5OS5b6tI9IGflxTeuBS//zXeFIUbwt5dwxyZ17cnA==", + "cpu": [ + "arm64" + ], + "libc": [ + "glibc" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-ppc64": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.3.2.tgz", + "integrity": "sha512-3z0NHDxD6n5I9gc05U1eW1AyRm+Gznzq3naMrthPNqE6oYykcogW0l/jfpJdjYnuNl8R7yI9pNbE1XiUeyq0Aw==", + "cpu": [ + "ppc64" + ], + "libc": [ + "glibc" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-riscv64": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.3.2.tgz", + "integrity": "sha512-bsb4rI+NldGOsXuej2r8OdSS8+zXDVaCWxyWrcv6kneTOlgAHtZABRzBBCwdsPiD90J4myNJuHpg6kA20ImW/w==", + "cpu": [ + "riscv64" + ], + "libc": [ + "glibc" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-s390x": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.3.2.tgz", + "integrity": "sha512-/ABshyj8gCpyIrNXnHn4LorDJ0HHm1VhXPBlxZ8zAtfVPAaSafXPGn+sUSIRiwaSBy0mmFjSjiXI5mkcwdChKQ==", + "cpu": [ + "s390x" + ], + "libc": [ + "glibc" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-x64": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.3.2.tgz", + "integrity": "sha512-ITPEtgffGJ0S6G9dRyw/366tJQqFRcHWPHhC+Stpg3Z8AEMrDrTr2lhdz4f/Y/HMbRh//7Z5mBzEpVdi62Oc3w==", + "cpu": [ + "x64" + ], + "libc": [ + "glibc" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-arm64": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.3.2.tgz", + "integrity": "sha512-zE9EdiUzUmg5mDT5a1rk5fYJ6GWPloTwWBYDS14naqHsL+EaMpDj1AWnpLgh3u0YCORv2Tt50wrcrpYqkP97Kw==", + "cpu": [ + "arm64" + ], + "libc": [ + "musl" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-x64": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.3.2.tgz", + "integrity": "sha512-m0lrLiUt+lBYnCFr8qV/65yMR4E/c7/wf78I5eKTdkEakFAlZ9QlzEM3QIhhAwVeUhLAHLcCq7a7Vszq/oFNZQ==", + "cpu": [ + "x64" + ], + "libc": [ + "musl" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-linux-arm": { + "version": "0.35.3", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.35.3.tgz", + "integrity": "sha512-affVWCTLooy8TSxbDx2qkzuDeaWLNVBA+P//FNBirHsXpP2fuBhk5AuboYUnrDnzoXes8GFjpTx0SBFOCRg+FA==", + "cpu": [ + "arm" + ], + "libc": [ + "glibc" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm": "1.3.2" + } + }, + "node_modules/@img/sharp-linux-arm64": { + "version": "0.35.3", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.35.3.tgz", + "integrity": "sha512-QgKDspHPnrU+GQ55XPhGwyhC8acLVOOSyAvo1oVfFmrIXLkDNmGWzAfDZ4xK8oSA1qBQrALcHX0G5UZni/SuFQ==", + "cpu": [ + "arm64" + ], + "libc": [ + "glibc" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm64": "1.3.2" + } + }, + "node_modules/@img/sharp-linux-ppc64": { + "version": "0.35.3", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.35.3.tgz", + "integrity": "sha512-sMd8rDxmpLOwv/7N44klFjOD5DUO7FLdjiXDI0hoxYaf7Ar262dQIEkosE98bps+5HPLtp/EvNqeqQtOycP/IA==", + "cpu": [ + "ppc64" + ], + "libc": [ + "glibc" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-ppc64": "1.3.2" + } + }, + "node_modules/@img/sharp-linux-riscv64": { + "version": "0.35.3", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.35.3.tgz", + "integrity": "sha512-0Eob78yjlYPfL5vMNWAW55l3R9Y6BQS/gOfe0ZcP9mEz9ohhKSt4im1hayiknXgf8AWrFqMvJcKIdmLmEe7yeQ==", + "cpu": [ + "riscv64" + ], + "libc": [ + "glibc" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-riscv64": "1.3.2" + } + }, + "node_modules/@img/sharp-linux-s390x": { + "version": "0.35.3", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.35.3.tgz", + "integrity": "sha512-KgAxQ0DxpNOq1rG2t5cgTgShJFGSuU7XO45cqC+1NVOuZnP6tlgZRuSYOfNupGkHID0o3cJOsw4DVeJpMovcGw==", + "cpu": [ + "s390x" + ], + "libc": [ + "glibc" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-s390x": "1.3.2" + } + }, + "node_modules/@img/sharp-linux-x64": { + "version": "0.35.3", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.35.3.tgz", + "integrity": "sha512-8pqvxubL2PGdhlPy6GLqzDYMUjyRmKAwKHYKixpdJYBUK7PJ0C029XdsnpFIdgRZG68fZiGdHVWcKPvtiPB4cA==", + "cpu": [ + "x64" + ], + "libc": [ + "glibc" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-x64": "1.3.2" + } + }, + "node_modules/@img/sharp-linuxmusl-arm64": { + "version": "0.35.3", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.35.3.tgz", + "integrity": "sha512-Vz0iQjzzcSX3HCbfwFfCSG/9SCIqyO0mH2sXyiHaAYfBk0cRsCWXRyQYX0ovCK/PAQBbTzQ0dsPQHh5MAFL59w==", + "cpu": [ + "arm64" + ], + "libc": [ + "musl" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-arm64": "1.3.2" + } + }, + "node_modules/@img/sharp-linuxmusl-x64": { + "version": "0.35.3", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.35.3.tgz", + "integrity": "sha512-6O1NPKcDVj9QEdg7Hx549EX8U0rp6yXQERqru6yRN7fGBn32UvIRJUlWnk+8xDCiG76hXVBbX82NZ/ZKr0euIg==", + "cpu": [ + "x64" + ], + "libc": [ + "musl" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-x64": "1.3.2" + } + }, + "node_modules/@img/sharp-wasm32": { + "version": "0.35.3", + "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.35.3.tgz", + "integrity": "sha512-cZ0XkcYGpHZkqW6iCkqTcmUC0CD9DhD5d/qeZlZkfRBn6GnHniZXLUo5+9xw8Iv76YE6LQFN9YNBlKREcCG76w==", + "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", + "optional": true, + "dependencies": { + "@emnapi/runtime": "^1.11.1" + }, + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-webcontainers-wasm32": { + "version": "0.35.3", + "resolved": "https://registry.npmjs.org/@img/sharp-webcontainers-wasm32/-/sharp-webcontainers-wasm32-0.35.3.tgz", + "integrity": "sha512-2rnq7bX3NzeR2T4YWgz8qiG4h3TSdMe+vN1iQXpJleSJ3SM5zQ8Fy2SyyXAWlbxpEZ2Y+Z4u1BePgJEYbSy80Q==", + "cpu": [ + "wasm32" + ], + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "@img/sharp-wasm32": "0.35.3" + }, + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-arm64": { + "version": "0.35.3", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.35.3.tgz", + "integrity": "sha512-4bPwFdMbeC4JQ8L8LOyWp6nsHcboP5fxkp6iPOXz2Vg49R42TuMs2whkJ5OAP4/Ul035qOzy0AecOF9VOscn4w==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-ia32": { + "version": "0.35.3", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.35.3.tgz", + "integrity": "sha512-r53mXsBN6lFUDiST764SvgwUdHAqM4rPAiDzAmf4fLoB6X/rkfyTrLCg6+g17wJJiCmB3JYgHuUldCWUIRFSXw==", + "cpu": [ + "ia32" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-x64": { + "version": "0.35.3", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.35.3.tgz", + "integrity": "sha512-D4y1vNeZrIIJCN+uHaWVtH86B+aCrdMYYjicy9pXHvbGZeGYLLSd3wdVuC37FxVXlU1ARsk84eKWfWMXGYEqvA==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, "node_modules/@ioredis/commands": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/@ioredis/commands/-/commands-1.5.1.tgz", @@ -3220,7 +3778,6 @@ "version": "10.4.1", "resolved": "https://registry.npmjs.org/@nestjs/common/-/common-10.4.1.tgz", "integrity": "sha512-4CkrDx0s4XuWqFjX8WvOFV7Y6RGJd0P2OBblkhZS7nwoctoSuW5pyEa8SWak6YHNGrHRpFb6ymm5Ai4LncwRVA==", - "peer": true, "dependencies": { "iterare": "1.2.1", "tslib": "2.6.3", @@ -3264,7 +3821,6 @@ "resolved": "https://registry.npmjs.org/@nestjs/core/-/core-10.3.10.tgz", "integrity": "sha512-ZbQ4jovQyzHtCGCrzK5NdtW1SYO2fHSsgSY1+/9WdruYCUra+JDkWEXgZ4M3Hv480Dl3OXehAmY1wCOojeMyMQ==", "hasInstallScript": true, - "peer": true, "dependencies": { "@nuxtjs/opencollective": "0.3.2", "fast-safe-stringify": "2.1.1", @@ -3301,7 +3857,6 @@ "version": "12.2.0", "resolved": "https://registry.npmjs.org/@nestjs/graphql/-/graphql-12.2.0.tgz", "integrity": "sha512-du/aI+EXADxtJrHF1mAXR6RYRHuEWPNnJyHTmIOPW2Wx5qN32P7lQoHGD7TySATMl5aa47w05lPzxcasdUmpMQ==", - "peer": true, "dependencies": { "@graphql-tools/merge": "9.0.4", "@graphql-tools/schema": "10.0.4", @@ -3410,7 +3965,6 @@ "version": "10.3.10", "resolved": "https://registry.npmjs.org/@nestjs/platform-express/-/platform-express-10.3.10.tgz", "integrity": "sha512-wK2ow3CZI2KFqWeEpPmoR300OB6BcBLxARV1EiClJLCj4S1mZsoCmS0YWgpk3j1j6mo0SI8vNLi/cC2iZPEPQA==", - "peer": true, "dependencies": { "body-parser": "1.20.2", "cors": "2.8.5", @@ -4307,7 +4861,6 @@ "resolved": "https://registry.npmjs.org/@types/jest/-/jest-27.5.2.tgz", "integrity": "sha512-mpT8LJJ4CMeeahobofYWIjFo0xonRS/HfxnVEPMPFSQdGUt1uHCnoPT7Zhb+sjDU2wz0oKV0OLUR0WzrHNgfeA==", "dev": true, - "peer": true, "dependencies": { "jest-matcher-utils": "^27.0.0", "pretty-format": "^27.0.0" @@ -4362,7 +4915,6 @@ "version": "18.19.42", "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.42.tgz", "integrity": "sha512-d2ZFc/3lnK2YCYhos8iaNIYu9Vfhr92nHiyJHRltXWjXUBjEE+A4I58Tdbnw4VhggSW+2j5y5gTrLs4biNnubg==", - "peer": true, "dependencies": { "undici-types": "~5.26.4" } @@ -4529,7 +5081,6 @@ "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.62.0.tgz", "integrity": "sha512-VlJEV0fOQ7BExOsHYAGrgbEiZoi8D+Bl2+f6V2RrXerRSylnp+ZBHmPvaIa8cz0Ajx7WO7Z5RqfgYg7ED1nRhA==", "dev": true, - "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "5.62.0", "@typescript-eslint/types": "5.62.0", @@ -4918,7 +5469,6 @@ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.12.1.tgz", "integrity": "sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg==", "devOptional": true, - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -4970,7 +5520,6 @@ "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", "dev": true, - "peer": true, "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", @@ -5601,7 +6150,6 @@ "version": "1.7.9", "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.9.tgz", "integrity": "sha512-LhLcE7Hbiryz8oMDdDptSrWowmB4Bl6RCt6sIJKpRB4XtVf0iEgewX3au/pJqm+Py1kCASkb/FFKjxQaLtxJvw==", - "peer": true, "dependencies": { "follow-redirects": "^1.15.6", "form-data": "^4.0.0", @@ -5892,7 +6440,6 @@ "url": "https://github.com/sponsors/ai" } ], - "peer": true, "dependencies": { "caniuse-lite": "^1.0.30001640", "electron-to-chromium": "^1.4.820", @@ -5991,7 +6538,6 @@ "resolved": "https://registry.npmjs.org/bullmq/-/bullmq-5.71.1.tgz", "integrity": "sha512-kOBfdcsHmO6wwmIjpersoVdYQ7jkjTgky4Yop0loc7QwSdgxliSzD69U9ijZuRrkyCJwz5p5eqxeGeQkJ0YGZQ==", "license": "MIT", - "peer": true, "dependencies": { "cron-parser": "4.9.0", "ioredis": "5.10.1", @@ -6275,14 +6821,12 @@ "node_modules/class-transformer": { "version": "0.5.1", "resolved": "https://registry.npmjs.org/class-transformer/-/class-transformer-0.5.1.tgz", - "integrity": "sha512-SQa1Ws6hUbfC98vKGxZH3KFY0Y1lm5Zm0SY8XX9zbK7FJCyVEac3ATW0RIpwzW+oOfmHE5PMPufDG9hCfoEOMw==", - "peer": true + "integrity": "sha512-SQa1Ws6hUbfC98vKGxZH3KFY0Y1lm5Zm0SY8XX9zbK7FJCyVEac3ATW0RIpwzW+oOfmHE5PMPufDG9hCfoEOMw==" }, "node_modules/class-validator": { "version": "0.14.2", "resolved": "https://registry.npmjs.org/class-validator/-/class-validator-0.14.2.tgz", "integrity": "sha512-3kMVRF2io8N8pY1IFIXlho9r8IPUUIfHe2hYVtiebvAzU2XeQFXTv+XI4WX+TnXmtwXMDcjngcpkiPM0O9PvLw==", - "peer": true, "dependencies": { "@types/validator": "^13.11.8", "libphonenumber-js": "^1.11.1", @@ -7050,7 +7594,6 @@ "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", "license": "Apache-2.0", - "optional": true, "engines": { "node": ">=8" } @@ -7535,7 +8078,6 @@ "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.0.tgz", "integrity": "sha512-dZ6+mexnaTIbSBZWgou51U6OmzIhYM2VcNdtiTtI7qPNZm35Akpr0f6vtw3w1Kmn5PYo+tZVfh13WrhpS6oLqQ==", "dev": true, - "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", @@ -7964,7 +8506,6 @@ "version": "4.19.2", "resolved": "https://registry.npmjs.org/express/-/express-4.19.2.tgz", "integrity": "sha512-5T6nhjsT+EOMzuck8JjBHARTHfMht0POzlA60WV2pMD3gyXw2LZnZ+ueGdNxG+0calOJcWKbpFcuzLZ91YWq9Q==", - "peer": true, "dependencies": { "accepts": "~1.3.8", "array-flatten": "1.1.1", @@ -8739,7 +9280,6 @@ "version": "16.9.0", "resolved": "https://registry.npmjs.org/graphql/-/graphql-16.9.0.tgz", "integrity": "sha512-GGTKBX4SD7Wdb8mqeDLni2oaRGYQWjWHGKPQ24ZMnUtKfcsVoiv4uX8+LJr1K6U5VW2Lu1BwJnj7uiori0YtRw==", - "peer": true, "engines": { "node": "^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0" } @@ -9668,7 +10208,6 @@ "resolved": "https://registry.npmjs.org/jest/-/jest-28.0.3.tgz", "integrity": "sha512-uS+T5J3w5xyzd1KSJCGKhCo8WTJXbNl86f5SW11wgssbandJOVLRKKUxmhdFfmKxhPeksl1hHZ0HaA8VBzp7xA==", "dev": true, - "peer": true, "dependencies": { "@jest/core": "^28.0.3", "import-local": "^3.0.2", @@ -12307,7 +12846,6 @@ "version": "6.9.16", "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-6.9.16.tgz", "integrity": "sha512-psAuZdTIRN08HKVd/E8ObdV6NO7NTBY3KsC30F7M4H1OnmLCUNaS56FpYxyb26zWLSyYF9Ozch9KYHhHegsiOQ==", - "peer": true, "engines": { "node": ">=6.0.0" } @@ -12710,7 +13248,6 @@ "version": "0.6.0", "resolved": "https://registry.npmjs.org/passport/-/passport-0.6.0.tgz", "integrity": "sha512-0fe+p3ZnrWRW74fe8+SvCyf4a3Pb2/h7gFkQ8yTJpAO50gDzlfjZUZTO1k5Eg9kUct22OxHLqDZoKUWRHOh9ug==", - "peer": true, "dependencies": { "passport-strategy": "1.x.x", "pause": "0.0.1", @@ -12822,7 +13359,6 @@ "version": "8.12.0", "resolved": "https://registry.npmjs.org/pg/-/pg-8.12.0.tgz", "integrity": "sha512-A+LHUSnwnxrnL/tZ+OLfqR1SxLN3c/pgDztZ47Rpbsd4jUytsTtwQo/TLPRzPJMp/1pbhYVhH9cuSZLAajNfjQ==", - "peer": true, "dependencies": { "pg-connection-string": "^2.6.4", "pg-pool": "^3.6.2", @@ -13108,7 +13644,6 @@ "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.8.tgz", "integrity": "sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==", "dev": true, - "peer": true, "bin": { "prettier": "bin-prettier.js" }, @@ -13503,7 +14038,6 @@ "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", "license": "MIT", - "peer": true, "dependencies": { "loose-envify": "^1.1.0" }, @@ -14126,7 +14660,6 @@ "version": "7.8.1", "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz", "integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==", - "peer": true, "dependencies": { "tslib": "^2.1.0" } @@ -14180,6 +14713,7 @@ "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", "license": "MIT", + "peer": true, "dependencies": { "loose-envify": "^1.1.0" } @@ -14327,6 +14861,67 @@ "sha.js": "bin.js" } }, + "node_modules/sharp": { + "version": "0.35.3", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.35.3.tgz", + "integrity": "sha512-ej0zVHuZGHCiABXcNxeYhpRnPNPAcvbG8RMdBAhDAxLKkCRVSpK3Iyu7qbqw3JMzoj0REeM6f3tJLtVwl0023Q==", + "license": "Apache-2.0", + "dependencies": { + "@img/colour": "^1.1.0", + "detect-libc": "^2.1.2", + "semver": "^7.8.5" + }, + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-darwin-arm64": "0.35.3", + "@img/sharp-darwin-x64": "0.35.3", + "@img/sharp-freebsd-wasm32": "0.35.3", + "@img/sharp-libvips-darwin-arm64": "1.3.2", + "@img/sharp-libvips-darwin-x64": "1.3.2", + "@img/sharp-libvips-linux-arm": "1.3.2", + "@img/sharp-libvips-linux-arm64": "1.3.2", + "@img/sharp-libvips-linux-ppc64": "1.3.2", + "@img/sharp-libvips-linux-riscv64": "1.3.2", + "@img/sharp-libvips-linux-s390x": "1.3.2", + "@img/sharp-libvips-linux-x64": "1.3.2", + "@img/sharp-libvips-linuxmusl-arm64": "1.3.2", + "@img/sharp-libvips-linuxmusl-x64": "1.3.2", + "@img/sharp-linux-arm": "0.35.3", + "@img/sharp-linux-arm64": "0.35.3", + "@img/sharp-linux-ppc64": "0.35.3", + "@img/sharp-linux-riscv64": "0.35.3", + "@img/sharp-linux-s390x": "0.35.3", + "@img/sharp-linux-x64": "0.35.3", + "@img/sharp-linuxmusl-arm64": "0.35.3", + "@img/sharp-linuxmusl-x64": "0.35.3", + "@img/sharp-webcontainers-wasm32": "0.35.3", + "@img/sharp-win32-arm64": "0.35.3", + "@img/sharp-win32-ia32": "0.35.3", + "@img/sharp-win32-x64": "0.35.3" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/sharp/node_modules/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -15294,7 +15889,6 @@ "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", "devOptional": true, - "peer": true, "dependencies": { "@cspotcode/source-map-support": "^0.8.0", "@tsconfig/node10": "^1.0.7", @@ -15470,7 +16064,6 @@ "version": "0.3.20", "resolved": "https://registry.npmjs.org/typeorm/-/typeorm-0.3.20.tgz", "integrity": "sha512-sJ0T08dV5eoZroaq9uPKBoNcGslHBR4E4y+EBHs//SiGbblGe7IeduP/IH4ddCcj0qp3PHwDwGnuvqEAnKlq/Q==", - "peer": true, "dependencies": { "@sqltools/formatter": "^1.2.5", "app-root-path": "^3.1.0", @@ -15631,7 +16224,6 @@ "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz", "integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==", "devOptional": true, - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -16005,7 +16597,6 @@ "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.82.1.tgz", "integrity": "sha512-C6uiGQJ+Gt4RyHXXYt+v9f+SN1v83x68URwgxNQ98cvH8kxiuywWGP4XeNZ1paOzZ63aY3cTciCEQJNFUljlLw==", "dev": true, - "peer": true, "dependencies": { "@types/eslint-scope": "^3.7.3", "@types/estree": "^1.0.0", @@ -16169,7 +16760,6 @@ "version": "3.17.0", "resolved": "https://registry.npmjs.org/winston/-/winston-3.17.0.tgz", "integrity": "sha512-DLiFIXYC5fMPxaRg832S6F5mJYvePtmO5G9v9IgUFPhXm9/GkXarH/TUrBAVzhTCzAj9anE/+GjrgXp/54nOgw==", - "peer": true, "dependencies": { "@colors/colors": "^1.6.0", "@dabh/diagnostics": "^2.0.2", diff --git a/src/API/package.json b/src/API/package.json index cc4acffc3..2255eb700 100644 --- a/src/API/package.json +++ b/src/API/package.json @@ -74,6 +74,7 @@ "reflect-metadata": "^0.1.13", "rimraf": "^3.0.2", "rxjs": "^7.2.0", + "sharp": "^0.35.3", "typeorm": "^0.3.7", "uuid": "^8.3.2", "winston": "^3.17.0", @@ -149,5 +150,12 @@ "directories": { "test": "test" }, - "keywords": [] + "keywords": [], + "allowScripts": { + "@apollo/protobufjs@1.2.7": true, + "@apollo/protobufjs@1.2.6": true, + "@nestjs/core@10.3.10": true, + "esbuild@0.19.11": true, + "msgpackr-extract@3.0.3": true + } } diff --git a/src/API/public/species-images/species_maps-Species_Distribution_Maps__Anopheles_arabiensis.jpeg b/src/API/public/species-images/species_maps-Species_Distribution_Maps__Anopheles_arabiensis.jpeg new file mode 100644 index 000000000..ad725b648 Binary files /dev/null and b/src/API/public/species-images/species_maps-Species_Distribution_Maps__Anopheles_arabiensis.jpeg differ diff --git a/src/API/public/species-images/species_maps-Species_Distribution_Maps__Anopheles_funestus.jpeg b/src/API/public/species-images/species_maps-Species_Distribution_Maps__Anopheles_funestus.jpeg new file mode 100644 index 000000000..9d1725d5f Binary files /dev/null and b/src/API/public/species-images/species_maps-Species_Distribution_Maps__Anopheles_funestus.jpeg differ diff --git a/src/API/public/species-images/species_maps-Species_Distribution_Maps__Anopheles_gambiae.jpeg b/src/API/public/species-images/species_maps-Species_Distribution_Maps__Anopheles_gambiae.jpeg new file mode 100644 index 000000000..18cbd2156 Binary files /dev/null and b/src/API/public/species-images/species_maps-Species_Distribution_Maps__Anopheles_gambiae.jpeg differ diff --git a/src/API/public/species-images/species_maps-Species_Distribution_Maps__Anopheles_melas.jpeg b/src/API/public/species-images/species_maps-Species_Distribution_Maps__Anopheles_melas.jpeg new file mode 100644 index 000000000..75ba5f46b Binary files /dev/null and b/src/API/public/species-images/species_maps-Species_Distribution_Maps__Anopheles_melas.jpeg differ diff --git a/src/API/public/species-images/species_maps-Species_Distribution_Maps__Anopheles_merus.jpeg b/src/API/public/species-images/species_maps-Species_Distribution_Maps__Anopheles_merus.jpeg new file mode 100644 index 000000000..175114450 Binary files /dev/null and b/src/API/public/species-images/species_maps-Species_Distribution_Maps__Anopheles_merus.jpeg differ diff --git a/src/API/public/species-images/species_maps-Species_Distribution_Maps__Anopheles_moucheti.jpeg b/src/API/public/species-images/species_maps-Species_Distribution_Maps__Anopheles_moucheti.jpeg new file mode 100644 index 000000000..56719fd15 Binary files /dev/null and b/src/API/public/species-images/species_maps-Species_Distribution_Maps__Anopheles_moucheti.jpeg differ diff --git a/src/API/public/species-images/species_maps-Species_Distribution_Maps__Anopheles_nili.jpeg b/src/API/public/species-images/species_maps-Species_Distribution_Maps__Anopheles_nili.jpeg new file mode 100644 index 000000000..6e2bc6a62 Binary files /dev/null and b/src/API/public/species-images/species_maps-Species_Distribution_Maps__Anopheles_nili.jpeg differ diff --git a/src/API/public/species-images/species_maps-Species_Distribution_Maps___Anopheles_arabiensis-20260707154635708.webp b/src/API/public/species-images/species_maps-Species_Distribution_Maps___Anopheles_arabiensis-20260707154635708.webp new file mode 100644 index 000000000..1140188ec Binary files /dev/null and b/src/API/public/species-images/species_maps-Species_Distribution_Maps___Anopheles_arabiensis-20260707154635708.webp differ diff --git a/src/API/public/species-images/species_maps-Species_Distribution_Maps___Anopheles_funestus-20260707155125461.webp b/src/API/public/species-images/species_maps-Species_Distribution_Maps___Anopheles_funestus-20260707155125461.webp new file mode 100644 index 000000000..47fac192b Binary files /dev/null and b/src/API/public/species-images/species_maps-Species_Distribution_Maps___Anopheles_funestus-20260707155125461.webp differ diff --git a/src/API/public/species-images/species_maps-Species_Distribution_Maps___Anopheles_gambiae-20260707155404385.webp b/src/API/public/species-images/species_maps-Species_Distribution_Maps___Anopheles_gambiae-20260707155404385.webp new file mode 100644 index 000000000..de1874f56 Binary files /dev/null and b/src/API/public/species-images/species_maps-Species_Distribution_Maps___Anopheles_gambiae-20260707155404385.webp differ diff --git a/src/API/public/species-images/species_maps-Species_Distribution_Maps___Anopheles_melas-20260707155456516.webp b/src/API/public/species-images/species_maps-Species_Distribution_Maps___Anopheles_melas-20260707155456516.webp new file mode 100644 index 000000000..7be27085d Binary files /dev/null and b/src/API/public/species-images/species_maps-Species_Distribution_Maps___Anopheles_melas-20260707155456516.webp differ diff --git a/src/API/public/species-images/species_maps-Species_Distribution_Maps___Anopheles_merus-20260707155537808.webp b/src/API/public/species-images/species_maps-Species_Distribution_Maps___Anopheles_merus-20260707155537808.webp new file mode 100644 index 000000000..5948cae5a Binary files /dev/null and b/src/API/public/species-images/species_maps-Species_Distribution_Maps___Anopheles_merus-20260707155537808.webp differ diff --git a/src/API/public/species-images/species_maps-Species_Distribution_Maps___Anopheles_moucheti-20260707155615966.webp b/src/API/public/species-images/species_maps-Species_Distribution_Maps___Anopheles_moucheti-20260707155615966.webp new file mode 100644 index 000000000..4d3ecc0a6 Binary files /dev/null and b/src/API/public/species-images/species_maps-Species_Distribution_Maps___Anopheles_moucheti-20260707155615966.webp differ diff --git a/src/API/public/species-images/species_maps-Species_Distribution_Maps___Anopheles_nili_complex-20260707155658885.webp b/src/API/public/species-images/species_maps-Species_Distribution_Maps___Anopheles_nili_complex-20260707155658885.webp new file mode 100644 index 000000000..01b9f1010 Binary files /dev/null and b/src/API/public/species-images/species_maps-Species_Distribution_Maps___Anopheles_nili_complex-20260707155658885.webp differ diff --git a/src/API/src/db/migrations/1785228239230-ConvertSpeciesImagesToBytea.ts b/src/API/src/db/migrations/1785228239230-ConvertSpeciesImagesToBytea.ts new file mode 100644 index 000000000..758b3afe2 --- /dev/null +++ b/src/API/src/db/migrations/1785228239230-ConvertSpeciesImagesToBytea.ts @@ -0,0 +1,20 @@ +import { MigrationInterface, QueryRunner } from "typeorm"; + +export class ConvertSpeciesImagesToBytea1785228239230 implements MigrationInterface { + name = 'ConvertSpeciesImagesToBytea1785228239230' + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "species_information" DROP COLUMN "speciesImage"`); + await queryRunner.query(`ALTER TABLE "species_information" ADD "speciesImage" bytea`); + await queryRunner.query(`ALTER TABLE "species_information" DROP COLUMN "previewImage"`); + await queryRunner.query(`ALTER TABLE "species_information" ADD "previewImage" bytea`); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "species_information" DROP COLUMN "previewImage"`); + await queryRunner.query(`ALTER TABLE "species_information" ADD "previewImage" character varying`); + await queryRunner.query(`ALTER TABLE "species_information" DROP COLUMN "speciesImage"`); + await queryRunner.query(`ALTER TABLE "species_information" ADD "speciesImage" character varying`); + } + +} \ No newline at end of file diff --git a/src/API/src/db/shared/reference.resolver.ts b/src/API/src/db/shared/reference.resolver.ts index 0747e2a03..decf230bd 100644 --- a/src/API/src/db/shared/reference.resolver.ts +++ b/src/API/src/db/shared/reference.resolver.ts @@ -8,6 +8,7 @@ import { Resolver, ObjectType, ArgsType, + Int, } from '@nestjs/graphql'; import { ReferenceService } from './reference.service'; import { UseGuards } from '@nestjs/common'; @@ -48,6 +49,11 @@ export class CreateReferenceInput { v_data: boolean; } +// Same shape as create — an update always resends the full form, +// matching how the frontend's updateSourceQuery builds its payload. +@InputType() +export class UpdateReferenceInput extends CreateReferenceInput {} + @ObjectType() class PaginatedReferenceData extends PaginatedResponse(Reference) {} @@ -81,6 +87,14 @@ export class GetReferenceDataArgs { @Field(stringTypeResolver, { nullable: true, defaultValue: '' }) @Min(0) textFilter: string; + + // Which column textFilter searches against — matches whichever option + // the user picked in the field dropdown on the frontend. Defaults to + // the previous hardcoded behavior so any existing caller that doesn't + // pass this still works unchanged. + @Field(stringTypeResolver, { nullable: true, defaultValue: 'article_title' }) + @Min(0) + filterField: string; } @Resolver(() => Reference) @@ -103,6 +117,7 @@ export class ReferenceResolver { startId, endId, textFilter, + filterField, }: GetReferenceDataArgs, ) { const { items, total } = await this.referenceService.findReferences( @@ -113,6 +128,7 @@ export class ReferenceResolver { startId, endId, textFilter, + filterField, ); return Object.assign(new PaginatedReferenceData(), { items, @@ -141,4 +157,25 @@ export class ReferenceResolver { }; return this.referenceService.save(newRef); } -} + + @UseGuards(GqlAuthGuard, RolesGuard) + @Roles(Role.Uploader, Role.Editor) + @Mutation(() => Reference) + async updateReference( + @Args('num_id', { type: () => Int }) num_id: number, + @Args({ name: 'input', type: () => UpdateReferenceInput, nullable: false }) + input: UpdateReferenceInput, + ) { + const updates: Partial = { + author: decodeURIComponent(input.author), + article_title: decodeURIComponent(input.article_title), + journal_title: decodeURIComponent(input.journal_title), + citation: decodeURIComponent(input.citation), + year: input.year, + published: input.published, + report_type: decodeURIComponent(input.report_type), + v_data: input.v_data, + }; + return this.referenceService.update(num_id, updates); + } +} \ No newline at end of file diff --git a/src/API/src/db/shared/reference.service.ts b/src/API/src/db/shared/reference.service.ts index 0fa4d2d6a..5cbb924ed 100644 --- a/src/API/src/db/shared/reference.service.ts +++ b/src/API/src/db/shared/reference.service.ts @@ -1,10 +1,16 @@ -import { Injectable } from '@nestjs/common'; +import { Injectable, NotFoundException } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { Reference } from './entities/reference.entity'; import { Occurrence } from '../occurrence/entities/occurrence.entity'; import { Dataset } from './entities/dataset.entity'; import { Repository } from 'typeorm'; +// Only these columns are ever allowed as a dynamic filter target — this +// whitelist exists specifically to prevent filterField (which ultimately +// comes from user input via the frontend dropdown) from being used to +// inject an arbitrary column/expression into the raw SQL string below. +const ALLOWED_FILTER_FIELDS = ['article_title', 'author', 'journal_title']; + @Injectable() export class ReferenceService { constructor( @@ -16,6 +22,10 @@ export class ReferenceService { return this.referenceRepository.findOne({ where: { id: id } }); } + findOneByNumId(num_id: number): Promise { + return this.referenceRepository.findOne({ where: { num_id } }); + } + findAll(): Promise { return this.referenceRepository.find(); } @@ -27,6 +37,20 @@ export class ReferenceService { return this.referenceRepository.save(reference); } + async update( + num_id: number, + updates: Partial, + ): Promise { + const existing = await this.findOneByNumId(num_id); + if (!existing) { + throw new NotFoundException( + `Reference with num_id ${num_id} not found`, + ); + } + const merged = this.referenceRepository.merge(existing, updates); + return this.referenceRepository.save(merged); + } + async findReferences( take: number, skip: number, @@ -35,11 +59,21 @@ export class ReferenceService { startId: number, endId: number, textFilter: string, + filterField: string = 'article_title', ): Promise<{ items: Reference[]; total: number }> { const nonStringCols = ['num_id', 'year', 'published', 'v_data']; + const orderByString = nonStringCols.includes(orderBy) + ? `reference.${orderBy}` + : `LOWER(reference.${orderBy})`; - // Build filter conditions that will be applied to both queries - const filterParams: Record = { status: 'Approved' }; + // Guard against an unexpected/invalid filterField value reaching the + // raw query string below — falls back to the original hardcoded + // column if the requested one isn't in the allowed list. + const safeFilterField = ALLOWED_FILTER_FIELDS.includes(filterField) + ? filterField + : 'article_title'; + + let query = this.referenceRepository.createQueryBuilder('reference'); if (startId && !isNaN(startId)) { filterParams.startId = startId; @@ -115,8 +149,8 @@ export class ReferenceService { countQuery = countQuery.andWhere('reference.num_id <= :endId', { endId }); } if (textFilter) { - countQuery = countQuery.andWhere( - 'LOWER(reference.article_title) LIKE :textFilter', + query = query.andWhere( + `LOWER("reference"."${safeFilterField}") LIKE :textFilter`, { textFilter: `%${textFilter.toLocaleLowerCase()}%`, }, @@ -128,4 +162,4 @@ export class ReferenceService { return { items, total }; } -} +} \ No newline at end of file diff --git a/src/API/src/db/speciesInformation/entities/speciesInformation.entity.ts b/src/API/src/db/speciesInformation/entities/speciesInformation.entity.ts index b815a903e..d5ad64185 100644 --- a/src/API/src/db/speciesInformation/entities/speciesInformation.entity.ts +++ b/src/API/src/db/speciesInformation/entities/speciesInformation.entity.ts @@ -17,9 +17,20 @@ export class SpeciesInformation extends BaseEntity { @Field({ nullable: false }) description: string; - @Column('varchar', { nullable: false }) - @Field({ nullable: false }) - speciesImage: string; + // Original, full-size JPEG, stored directly as raw bytes in Postgres. + // No @Field here — exposed to GraphQL as a base64 string via a + // @ResolveField in speciesInformation.resolver.ts, since GraphQL has + // no native binary type. Only ever needed for downloading — + // still never fetched by the list query (see allSpeciesInformation's + // select list in the service, unchanged). + @Column('bytea', { nullable: true }) + speciesImage: Buffer; + + // Small WebP version, generated automatically whenever a new + // speciesImage is uploaded. This is what gets displayed everywhere. + // Same base64-via-resolver treatment as speciesImage above. + @Column('bytea', { nullable: true }) + previewImage: Buffer; @Column('varchar', { nullable: false }) @Field({ nullable: false }) @@ -32,4 +43,4 @@ export class SpeciesInformation extends BaseEntity { @Column('varchar', { nullable: true }) @Field({ nullable: true }) link: string; -} +} \ No newline at end of file diff --git a/src/API/src/db/speciesInformation/speciesInformation.controller.ts b/src/API/src/db/speciesInformation/speciesInformation.controller.ts new file mode 100644 index 000000000..3e863850e --- /dev/null +++ b/src/API/src/db/speciesInformation/speciesInformation.controller.ts @@ -0,0 +1,84 @@ +import { + Controller, + Post, + Get, + Param, + Res, + UploadedFile, + UseInterceptors, + BadRequestException, + NotFoundException, +} from '@nestjs/common'; +import { FileInterceptor } from '@nestjs/platform-express'; +import { Response } from 'express'; +import { SpeciesInformationService } from './speciesInformation.service'; + +// CHANGED: TypeScript kept resolving sharp's type declarations as +// non-callable in this project's config, regardless of import style +// (`import sharp from`, `import * as sharp from`, and +// `import sharp = require(...)` all failed the same way). Plain +// require() sidesteps that entirely — sharp becomes typed as `any` +// and isn't type-checked at all, only used at runtime. +// eslint-disable-next-line @typescript-eslint/no-var-requires +const sharp = require('sharp'); + +@Controller('species-information') +export class SpeciesInformationController { + constructor( + private readonly speciesInformationService: SpeciesInformationService, + ) {} + + // Processes a newly-selected species image. No external storage upload + // happens here anymore — this validates the file, generates a smaller + // WebP preview, and returns both as base64 strings. The frontend holds + // these in local state and sends them along with the rest of the form + // in the createEditSpeciesInformation mutation, where they get decoded + // to raw bytes and saved directly into the species_information table's + // speciesImage / previewImage columns. + @Post('upload-image') + @UseInterceptors(FileInterceptor('file')) + async uploadImage(@UploadedFile() file: Express.Multer.File) { + if (!file) { + throw new BadRequestException('No file provided'); + } + + // Only JPEG is accepted, since that's the documented format and + // the only one we're set up to convert to WebP here. + if (file.mimetype !== 'image/jpeg') { + throw new BadRequestException('Only JPEG images are supported'); + } + + // Build a smaller WebP version in memory. + // - resize() caps the width so the preview is genuinely lighter + // - withoutEnlargement stops small images being blown up + // - webp({ quality: 80 }) is a solid size/quality balance + const previewBuffer = await sharp(file.buffer) + .resize({ width: 800, withoutEnlargement: true }) + .webp({ quality: 80 }) + .toBuffer(); + + return { + imageBase64: file.buffer.toString('base64'), + previewBase64: previewBuffer.toString('base64'), + }; + } + + // Download route for the list page's "Download" button. Reads the raw + // bytes straight from Postgres and sends them back as a file attachment + // — no redirect anywhere, since there's no external file to redirect to. + @Get(':id/download-image') + async downloadImage(@Param('id') id: string, @Res() res: Response) { + const species = + await this.speciesInformationService.getSpeciesImageForDownload(id); + + if (!species || !species.speciesImage) { + throw new NotFoundException('Image not found'); + } + + res.set({ + 'Content-Type': 'image/jpeg', + 'Content-Disposition': `attachment; filename="${species.name || 'species'}.jpeg"`, + }); + return res.send(species.speciesImage); + } +} \ No newline at end of file diff --git a/src/API/src/db/speciesInformation/speciesInformation.module.ts b/src/API/src/db/speciesInformation/speciesInformation.module.ts index d9eda15d0..b130b99ed 100644 --- a/src/API/src/db/speciesInformation/speciesInformation.module.ts +++ b/src/API/src/db/speciesInformation/speciesInformation.module.ts @@ -3,10 +3,20 @@ import { Module } from '@nestjs/common'; import { SpeciesInformation } from './entities/speciesInformation.entity'; import { SpeciesInformationService } from './speciesInformation.service'; import { SpeciesInformationResolver } from './speciesInformation.resolver'; +import { SpeciesInformationController } from './speciesInformation.controller'; +import { AzureBlobService } from 'src/db/azure-blob/azure-blob.service'; @Module({ imports: [TypeOrmModule.forFeature([SpeciesInformation])], - providers: [SpeciesInformationService, SpeciesInformationResolver], + // This was previously an empty array — meaning your REST endpoints + // (upload-image, download-image, images/:filename) were never + // actually reachable. Registering the controller here fixes that. + controllers: [SpeciesInformationController], + providers: [ + SpeciesInformationService, + SpeciesInformationResolver, + AzureBlobService, + ], exports: [SpeciesInformationService, SpeciesInformationResolver], }) export class SpeciesInformationModule {} diff --git a/src/API/src/db/speciesInformation/speciesInformation.resolver.ts b/src/API/src/db/speciesInformation/speciesInformation.resolver.ts index 0f8beed9b..3528c14b8 100644 --- a/src/API/src/db/speciesInformation/speciesInformation.resolver.ts +++ b/src/API/src/db/speciesInformation/speciesInformation.resolver.ts @@ -3,7 +3,9 @@ import { Field, InputType, Mutation, + Parent, Query, + ResolveField, Resolver, } from '@nestjs/graphql'; import { SpeciesInformationService } from './speciesInformation.service'; @@ -34,9 +36,15 @@ export class CreateSpeciesInformationInput { @Field() description: string; - @Field() + // Base64-encoded image bytes (from uploadImage's imageBase64 response), + // decoded to a Buffer in createEditSpeciesInformation before saving. + @Field({ nullable: true }) speciesImage: string; + // Base64-encoded WebP preview bytes (from uploadImage's previewBase64). + @Field({ nullable: true }) + previewImage: string; + @Field(() => [String]) citations?: string[]; @@ -58,6 +66,24 @@ export class SpeciesInformationResolver { return await this.speciesInformationService.allSpeciesInformation(); } + // Converts the stored bytea Buffer to a base64 string whenever a query + // actually asks for speciesImage. List queries that don't request this + // field never trigger it — allSpeciesInformation's service method + // already excludes speciesImage from its select for exactly this reason. + @ResolveField('speciesImage', () => String, { nullable: true }) + resolveSpeciesImage(@Parent() species: SpeciesInformation): string | null { + return species.speciesImage + ? species.speciesImage.toString('base64') + : null; + } + + @ResolveField('previewImage', () => String, { nullable: true }) + resolvePreviewImage(@Parent() species: SpeciesInformation): string | null { + return species.previewImage + ? species.previewImage.toString('base64') + : null; + } + @UseGuards(GqlAuthGuard, RolesGuard) @Roles(Role.Editor) @Mutation(() => SpeciesInformation) @@ -72,6 +98,14 @@ export class SpeciesInformationResolver { const newSpeciesInformation: SpeciesInformation = { id: input.id ?? uuidv4(), ...input, + // Decode base64 strings from the frontend back into raw bytes + // for storage in the bytea columns. + speciesImage: input.speciesImage + ? Buffer.from(input.speciesImage, 'base64') + : null, + previewImage: input.previewImage + ? Buffer.from(input.previewImage, 'base64') + : null, distributionMapUrl: '', citations: input.citations ?? [], }; @@ -83,10 +117,10 @@ export class SpeciesInformationResolver { @UseGuards(GqlAuthGuard, RolesGuard) @Roles(Role.Editor) - @Mutation(() => Boolean) // Return Boolean to indicate success or failure + @Mutation(() => Boolean) async deleteSpeciesInformation( @Args('id', { type: () => String }) id: string, ): Promise { return this.speciesInformationService.deleteSpeciesInformation(id); } -} +} \ No newline at end of file diff --git a/src/API/src/db/speciesInformation/speciesInformation.service.ts b/src/API/src/db/speciesInformation/speciesInformation.service.ts index a1fcddb11..8f74bceb2 100644 --- a/src/API/src/db/speciesInformation/speciesInformation.service.ts +++ b/src/API/src/db/speciesInformation/speciesInformation.service.ts @@ -10,27 +10,55 @@ export class SpeciesInformationService { private speciesInformationRepository: Repository, ) {} + // Used by the EDIT page. Returns every field, including speciesImage, + // because the edit page needs the original to display + download. async speciesInformationById(id: string): Promise { return await this.speciesInformationRepository.findOne({ where: { id: id }, }); } + // Used by the LIST page. Deliberately does NOT select speciesImage — + // that's the large original JPEG, and pulling it for every row in the + // list would make the list slow to load for no benefit, since the + // list only ever displays previewImage. async allSpeciesInformation(): Promise { return await this.speciesInformationRepository.find({ + select: [ + 'id', + 'name', + 'shortDescription', + 'description', + 'previewImage', + 'distributionMapUrl', + 'citations', + 'link', + // speciesImage intentionally left out + ], order: { id: 'ASC', }, }); } + // Used ONLY by the download endpoint below. When someone on the list + // page clicks "Download," we don't have speciesImage in memory yet — + // this does a fast, narrow lookup for just that one field. + async getSpeciesImageForDownload( + id: string, + ): Promise> { + return await this.speciesInformationRepository.findOne({ + where: { id }, + select: ['id', 'name', 'speciesImage'], + }); + } + async upsertSpeciesInformation(info: SpeciesInformation) { return await this.speciesInformationRepository.save(info); } async deleteSpeciesInformation(id: string): Promise { - // Perform the deletion logic, for example using TypeORM or another method const result = await this.speciesInformationRepository.delete(id); - return result.affected > 0; // Returns true if deletion was successful + return result.affected > 0; } } diff --git a/src/API/src/schema.gql b/src/API/src/schema.gql index 527ccee42..ed7dacb9c 100644 --- a/src/API/src/schema.gql +++ b/src/API/src/schema.gql @@ -88,8 +88,9 @@ input CreateSpeciesInformationInput { id: String link: String! name: String! + previewImage: String shortDescription: String! - speciesImage: String! + speciesImage: String } """doi""" @@ -430,8 +431,9 @@ type SpeciesInformation { id: String! link: String name: String! + previewImage: String shortDescription: String! - speciesImage: String! + speciesImage: String } """uploaded dataset""" diff --git a/src/Docker/docker-compose.dev.yml b/src/Docker/docker-compose.dev.yml index d7e96a0b4..418562a6d 100644 --- a/src/Docker/docker-compose.dev.yml +++ b/src/Docker/docker-compose.dev.yml @@ -32,12 +32,8 @@ services: environment: - POSTGRES_USER=postgres - POSTGRES_PASSWORD=postgrespass - - POSTGRES_HOST=db + - POSTGRES_HOST=host.docker.internal - POSTGRES_DB=mva - depends_on: - - db - links: - - db:db networks: - va-network @@ -70,14 +66,10 @@ services: - POSTGRES_DB=mva - POSTGRES_USER=postgres - POSTGRES_PASSWORD=postgrespass - - POSTGRES_HOST=db - - POSTGRES_PORT=5432 + - POSTGRES_HOST=host.docker.internal + - POSTGRES_PORT=5433 ports: - '8000:8000' - depends_on: - - db - links: - - db:db networks: - va-network diff --git a/src/TileServer/generateTiles.sh b/src/TileServer/generateTiles.sh old mode 100644 new mode 100755 diff --git a/src/TileServer/installTools.sh b/src/TileServer/installTools.sh old mode 100644 new mode 100755 diff --git a/src/UI/api/api.ts b/src/UI/api/api.ts index b1d4445ec..da40b1ce3 100644 --- a/src/UI/api/api.ts +++ b/src/UI/api/api.ts @@ -2,7 +2,7 @@ import axios, { AxiosError } from 'axios'; import https from 'https'; import download from 'js-file-download'; import { marked } from 'marked'; -import { DatasetFileType } from '../state/state.types'; +import { DatasetFileType, SpeciesInformation } from '../state/state.types'; import { toast } from 'react-toastify'; import { useAppDispatch } from '../state/hooks'; import { @@ -1420,3 +1420,27 @@ export const rejectReviewedDatasets = async ( ); return res.data; }; + +export const uploadSpeciesImageAuthenticated = async ( + file: File, + token: string +) => { + const formData = new window.FormData(); // Uses the browser's native FormData + formData.append('file', file); + + const config = { + headers: { + Authorization: `Bearer ${token}`, + 'Content-Type': 'multipart/form-data', + }, + }; + + // Points directly to your active NestJS controller route! + const res = await axios.post( + `${apiUrl}species-information/upload-image`, + formData, + config + ); + + return res.data; +}; diff --git a/src/UI/api/queries.ts b/src/UI/api/queries.ts index f3a98dcbd..fceafaa3f 100644 --- a/src/UI/api/queries.ts +++ b/src/UI/api/queries.ts @@ -120,11 +120,12 @@ export const referenceQuery = ( order: string, startId: number | null, endId: number | null, - textFilter: string + textFilter: string, + filterField: string = 'article_title' ) => { return ` query Reference{ - allReferenceData(skip:${skip}, take:${take}, orderBy:"${orderBy}", order:"${order}", startId: ${startId}, endId: ${endId}, textFilter: "${textFilter}") { + allReferenceData(skip:${skip}, take:${take}, orderBy:"${orderBy}", order:"${order}", startId: ${startId}, endId: ${endId}, textFilter: "${textFilter}", filterField: "${filterField}") { items{author article_title journal_title @@ -155,25 +156,62 @@ export const newSourceQuery = (source: NewSource) => { `; }; +export const updateSourceQuery = (source: NewSource) => { + const validatedSourceString = sourceStringValidation(source); + const year = new Date(source.year).getFullYear(); + return ` + mutation UpdateReference { + updateReference(num_id: ${source.num_id}, input: {author: "${validatedSourceString.author}", article_title: "${validatedSourceString.article_title}", journal_title: "${validatedSourceString.journal_title}", citation: "${validatedSourceString.citation}", year: ${year}, published: ${validatedSourceString.published}, report_type: "${validatedSourceString.report_type}", v_data: ${validatedSourceString.v_data}}) + {num_id} + } + `; +}; + +export const deleteSourceQuery = (num_id: number) => { + return ` + mutation { + deleteReference(num_id: ${num_id}) + }`; +}; export const upsertSpeciesInformationMutation = ( speciesInformation: SpeciesInformation ) => { + // FIX 1: Safely formats citations into a genuine GraphQL array literal format, e.g., [1, 2, 3] or [] + // We strip out outer double quotes by mapping array contents cleanly + const citationIds = Array.isArray(speciesInformation.citations) + ? speciesInformation.citations + .map((c) => Number(c)) + .filter((n) => !isNaN(n)) + : []; + const formattedCitations = `[${citationIds.join(',')}]`; + + const safeName = (speciesInformation.name || '').replace(/"/g, '\\"'); + const safeShortDesc = (speciesInformation.shortDescription || '').replace( + /"/g, + '\\"' + ); + const safeImg = speciesInformation.speciesImage || ''; + const safePreview = speciesInformation.previewImage || ''; + const safeLink = speciesInformation.link || ''; + return ` mutation { createEditSpeciesInformation(input: { - ${speciesInformation.id ? 'id: "' + speciesInformation.id + '"' : ''} - name: "${speciesInformation.name}" - shortDescription: "${speciesInformation.shortDescription}" - description: """${speciesInformation.description}""" - speciesImage: "${speciesInformation.speciesImage}" - citations: "${speciesInformation.citations}" - link:"${speciesInformation.link}" + ${speciesInformation.id ? `id: "${speciesInformation.id}"` : ''} + name: "${safeName}" + shortDescription: "${safeShortDesc}" + description: """${speciesInformation.description || '[]'}""" + speciesImage: "${safeImg}" + previewImage: "${safePreview}" + citations: ${formattedCitations} + link: "${safeLink}" }) { - name id + name description shortDescription speciesImage + previewImage citations link } @@ -220,6 +258,7 @@ export const speciesInformationById = (id: string) => { speciesImage citations link + previewImage } } `; @@ -233,7 +272,7 @@ export const allSpecies = () => { name shortDescription description - speciesImage + previewImage citations link } diff --git a/src/UI/components/map/layers/irOverlayModal.tsx b/src/UI/components/map/layers/irOverlayModal.tsx index 9aba5ffcc..e60b867da 100644 --- a/src/UI/components/map/layers/irOverlayModal.tsx +++ b/src/UI/components/map/layers/irOverlayModal.tsx @@ -154,9 +154,7 @@ const groupLayers = (layers: WMTSLayer[]): Record => { const yearStr = match[2]; const year = parseInt(yearStr, 10); const startTime = new Date(Date.UTC(year, 0, 1)).getTime(); - const endTime = new Date( - Date.UTC(year, 11, 31, 23, 59, 59, 999) - ).getTime(); + const endTime = new Date(Date.UTC(year, 11, 31, 23, 59, 59, 999)).getTime(); if (!groups[lookupKey].timeSeriesGroup) { groups[lookupKey].timeSeriesGroup = { @@ -171,9 +169,7 @@ const groupLayers = (layers: WMTSLayer[]): Record => { }; } - const isAlreadyInTimeSeries = groups[ - lookupKey - ].timeSeriesGroup!.temporalLayers.some( + const isAlreadyInTimeSeries = groups[lookupKey].timeSeriesGroup!.temporalLayers.some( (tl) => tl.layerName === layer.name ); @@ -204,9 +200,7 @@ const groupLayers = (layers: WMTSLayer[]): Record => { g.regularLayers = []; } if (g.timeSeriesGroup) { - g.timeSeriesGroup.temporalLayers.sort( - (a, b) => a.startTime - b.startTime - ); + g.timeSeriesGroup.temporalLayers.sort((a, b) => a.startTime - b.startTime); } }); @@ -1161,4 +1155,4 @@ export const IROverlaysPanel: React.FC = () => { /> ); -}; +}; \ No newline at end of file diff --git a/src/UI/components/shared/navbar.tsx b/src/UI/components/shared/navbar.tsx index 6cae12591..ad9ccec59 100644 --- a/src/UI/components/shared/navbar.tsx +++ b/src/UI/components/shared/navbar.tsx @@ -26,6 +26,9 @@ export default function NavBar() { const feature_flags = useAppSelector((state) => state.config.feature_flags); const { user } = useUser(); + { + /*const dev ='true'; //process.env.NODE_ENV === 'development';*/ + } const theme = useTheme(); const isMobile = useMediaQuery(theme.breakpoints.down('sm')); const auth = useAppSelector((state) => state.auth); @@ -53,7 +56,10 @@ export default function NavBar() { user && (roles.includes(RolesEnum.REVIEWER_MANAGER) || roles.includes(RolesEnum.REVIEWER) || - roles.includes(RolesEnum.ADMIN)) + roles.includes(RolesEnum.ADMIN) || + { + /*dev==='true'*/ + }) ) { moreOptions.push({ text: t('doi'), url: '/doi' }); } diff --git a/src/UI/components/shared/textEditor/RichTextEditor.tsx b/src/UI/components/shared/textEditor/RichTextEditor.tsx index e79b8efca..0ddbe564c 100644 --- a/src/UI/components/shared/textEditor/RichTextEditor.tsx +++ b/src/UI/components/shared/textEditor/RichTextEditor.tsx @@ -56,11 +56,13 @@ const ReactStatePlugin = ({ description }: { description: string }) => { }; export const TextEditor = (props: { + label?: string; description: string; initialDescription: string; setDescription: (d: string) => void; error?: boolean; helperText?: string; + hideBlockTypeSelector?: boolean; }) => { const editorConfig = { namespace: 'speciesInformation', @@ -69,6 +71,15 @@ export const TextEditor = (props: { onError(error: Error) { throw error; }, + // Underline (unlike bold/italic) has no native HTML tag Lexical applies + // automatically — it needs a theme class name to attach the formatting to. + // The matching CSS rule (.editor-text-underline) lives in the global stylesheet + // alongside the other .editor-* classes. + theme: { + text: { + underline: 'editor-text-underline', + }, + }, nodes: [ HeadingNode, ListNode, @@ -87,7 +98,12 @@ export const TextEditor = (props: { return (
- + {props.label ? ( + + {props.label} + + ) : null} +
{ +const EditorToolbar = ({ + hideBlockTypeSelector, +}: { + hideBlockTypeSelector?: boolean; +}) => { const t = useTranslations('RichTextEditor'); const [editor] = useLexicalComposerContext(); const [blockType, setBlockType] = useState('paragraph'); @@ -139,19 +143,21 @@ const EditorToolbar = () => { return ( - - - + {!hideBlockTypeSelector && ( + + + + )} { diff --git a/src/UI/components/sources/source_filters.tsx b/src/UI/components/sources/source_filters.tsx index 8e2245d3f..363114410 100644 --- a/src/UI/components/sources/source_filters.tsx +++ b/src/UI/components/sources/source_filters.tsx @@ -1,4 +1,4 @@ -import { Box, TextField } from '@mui/material'; +import { Box, MenuItem, Select, SelectChangeEvent, TextField } from '@mui/material'; import { debounce } from 'lodash'; import { useCallback, useState } from 'react'; import { useAppDispatch } from '../../state/hooks'; @@ -6,6 +6,7 @@ import { getSourceInfo } from '../../state/source/actions/getSourceInfo'; import { changeFilterId, changeFilterText, + changeFilterField, } from '../../state/source/sourceSlice'; import { useTranslations } from 'next-intl'; import { useRouter } from 'next/router'; @@ -17,6 +18,14 @@ const getEndId = (range: string) => { return parseInt(range.substring(range.indexOf('-') + 1, range.length)); }; +// The value each option maps to must match a real column name that +// reference.service.ts's findReferences can filter against. +const FILTER_FIELD_OPTIONS = [ + { value: 'article_title', labelKey: 'filters.fieldTitle' }, + { value: 'author', labelKey: 'filters.fieldAuthor' }, + { value: 'journal_title', labelKey: 'filters.fieldJournalTitle' }, +]; + export default function SourceFilters(): JSX.Element { const t = useTranslations('SourcesPage'); const dispatch = useAppDispatch(); @@ -25,6 +34,7 @@ export default function SourceFilters(): JSX.Element { const hasNumIds = typeof router.query.num_ids === 'string'; const [idError, setIdError] = useState(false); + const [filterField, setFilterField] = useState('article_title'); const idHandler = useCallback( debounce((value: string) => { @@ -70,25 +80,60 @@ export default function SourceFilters(): JSX.Element { textHandler(event.target.value); }; + const handleFieldChange = (event: SelectChangeEvent) => { + const value = event.target.value; + setFilterField(value); + dispatch(changeFilterField(value)); + dispatch(getSourceInfo()); + }; + return ( + ); -} +} \ No newline at end of file diff --git a/src/UI/components/sources/source_form.tsx b/src/UI/components/sources/source_form.tsx index 84091872b..93c20bdd9 100644 --- a/src/UI/components/sources/source_form.tsx +++ b/src/UI/components/sources/source_form.tsx @@ -2,14 +2,16 @@ import { Paper, Box, Button, Typography, Switch } from '@mui/material'; import { useForm, Controller } from 'react-hook-form'; import { yupResolver } from '@hookform/resolvers/yup'; import { FormControlLabel, TextField } from '@mui/material'; -import React, { useState } from 'react'; +import React, { useEffect, useState } from 'react'; import * as yup from 'yup'; import { useDispatch } from 'react-redux'; import { AppDispatch } from '../../state/store'; import { DatePicker, LocalizationProvider } from '@mui/x-date-pickers'; import { AdapterDateFns } from '@mui/x-date-pickers/AdapterDateFns'; import { postNewSource } from '../../state/source/actions/postNewSource'; +import { updateSource } from '../../state/source/actions/updateSource'; import { useTranslations } from 'next-intl'; +import { TextEditor } from '../shared/textEditor/RichTextEditor'; export interface NewSource { author: string; @@ -31,35 +33,82 @@ const schema = yup journal_title: yup.string().required(), year: yup.string().required(), published: yup.boolean().required(), - report_type: yup.string().required(), + report_type: yup.string().notRequired(), v_data: yup.boolean().required(), + num_id: yup.number().notRequired(), }) .required(); -export default function SourceForm() { +interface SourceFormProps { + existingSource?: NewSource | null; +} + +const DEFAULT_REPORT_TYPE = 'Journal Article'; + +export default function SourceForm({ existingSource }: SourceFormProps) { const t = useTranslations('NewSourcePage'); + const isEditMode = !!existingSource; - const { register, reset, control, handleSubmit } = useForm({ + const { reset, control, handleSubmit } = useForm({ resolver: yupResolver(schema), defaultValues: { v_data: false, published: false, + report_type: DEFAULT_REPORT_TYPE, }, }); const [year, setYear] = useState(null); + + // Tracks whether the form's values are safe to render into the editors. + // If there's no existingSource (new-source mode), we're ready immediately. + // If there IS an existingSource, we must wait until reset(existingSource) + // has actually run — otherwise TextEditor mounts once with empty content + // and never picks up the real data (Lexical only reads initial content once). + const [formReady, setFormReady] = useState(!existingSource); + const onKeyDown = (e: { preventDefault: () => void }) => { e.preventDefault(); }; const dispatch = useDispatch(); + + useEffect(() => { + if (existingSource) { + reset(existingSource); + setYear(new Date(existingSource.year, 0, 1)); + setFormReady(true); + } + }, [existingSource, reset]); + const onSubmit = async (data: NewSource) => { - console.log(data); - const success = await dispatch(postNewSource(data)); - if (success) { - reset(); + if (isEditMode) { + // updateSource already shows a success/error toast internally + await dispatch(updateSource(data)); + } else { + const resultAction = await dispatch(postNewSource(data)); + // postNewSource already shows a success/error toast internally; + // we just check the real return value to decide whether to clear the form + if ( + postNewSource.fulfilled.match(resultAction) && + resultAction.payload === true + ) { + reset({ + v_data: false, + published: false, + report_type: DEFAULT_REPORT_TYPE, + }); + setYear(null); + } } }; + // Don't render the form (and its TextEditors) until we know Controller's + // values actually reflect existingSource. Briefly returning null avoids + // a flash of empty editors that never get corrected. + if (existingSource && !formReady) { + return null; + } + return ( onSubmit(d))}>
- {t('title')} + {isEditMode ? t('editTitle') : t('title')}
+
( - + helperText={error ? t('authorHelperText') : undefined} + hideBlockTypeSelector + /> )} rules={{ required: 'Author required' }} />
-
+
( - + helperText={error ? t('articleTitleHelperText') : undefined} + hideBlockTypeSelector + /> )} rules={{ required: 'Article Title required' }} /> @@ -129,18 +179,18 @@ export default function SourceForm() { name="journal_title" control={control} render={({ - field: { onChange, value }, + field: { value, onChange }, fieldState: { error }, }) => ( - + helperText={error ? t('journalTitleHelperText') : undefined} + hideBlockTypeSelector + /> )} rules={{ required: 'Journal Title required' }} /> @@ -157,11 +207,11 @@ export default function SourceForm() { }) => ( + /> )} rules={{ required: 'Citation required' }} /> @@ -217,6 +267,7 @@ export default function SourceForm() { }) => ( + /> )} - rules={{ required: 'Report Type required' }} />

@@ -241,9 +290,9 @@ export default function SourceForm() { control={ onChange(e.target.checked)} color="primary" size="medium" - {...register('published')} /> } label={t('published')} @@ -263,9 +312,9 @@ export default function SourceForm() { control={ onChange(e.target.checked)} color="primary" size="medium" - {...register('v_data')} /> } label={t('vectorData')} @@ -276,7 +325,7 @@ export default function SourceForm() {

+ - ))} + )} ))} @@ -155,6 +239,33 @@ export default function SourceTable(): JSX.Element { onRowsPerPageChange={handleChangeRowsPerPage} /> )} + + {canEdit && ( + + {t('deleteConfirm.title')} + + {t('deleteConfirm.message')} + + + + + + + )} ); } diff --git a/src/UI/components/species/SpeciesImageViewer.tsx b/src/UI/components/species/SpeciesImageViewer.tsx new file mode 100644 index 000000000..6363a5a54 --- /dev/null +++ b/src/UI/components/species/SpeciesImageViewer.tsx @@ -0,0 +1,253 @@ +import { useState, useRef, WheelEvent, MouseEvent } from 'react'; +import { + Box, + Button, + Dialog, + DialogActions, + DialogContent, + DialogTitle, + IconButton, + Tooltip, +} from '@mui/material'; +import ZoomInIcon from '@mui/icons-material/ZoomIn'; +import ZoomOutIcon from '@mui/icons-material/ZoomOut'; +import RestartAltIcon from '@mui/icons-material/RestartAlt'; +import DownloadIcon from '@mui/icons-material/Download'; +import CloseIcon from '@mui/icons-material/Close'; +import { + downloadSpeciesImage, + downloadSpeciesImageById, + resolveSpeciesImageUrl, +} from '../../utils/speciesImageUtils'; +import { useTranslations } from 'next-intl'; + +const MIN_SCALE = 1; +const MAX_SCALE = 3; +const SCALE_STEP = 0.5; + +type SpeciesImageViewerProps = { + // Controls what's DISPLAYED — always the small preview image + previewRef?: string; + + // Controls what's DOWNLOADED, direct case: pass this when the caller + // already has the full-size image ref/URL loaded (the edit page). + downloadRef?: string; + + // Controls what's DOWNLOADED, fallback case: pass this when the + // caller only has the species id (the list page). If both downloadRef + // and speciesId are given, downloadRef wins. + speciesId?: string; + + alt: string; + speciesName?: string; + thumbnailWidth?: number | string; + showActions?: boolean; +}; + +export default function SpeciesImageViewer({ + previewRef, + downloadRef, + speciesId, + alt, + speciesName, + thumbnailWidth = 300, + showActions = true, +}: SpeciesImageViewerProps) { + const t = useTranslations('SpeciesPage'); + + const [open, setOpen] = useState(false); + const [scale, setScale] = useState(1); + const [position, setPosition] = useState({ x: 0, y: 0 }); + const isDragging = useRef(false); + const lastPos = useRef({ x: 0, y: 0 }); + + // previewRef holds the WebP preview (generated server-side from the + // original JPEG), so it needs the correct MIME type when resolved to + // a data URI — otherwise the browser may fail to render it correctly. + const imageUrl = resolveSpeciesImageUrl(previewRef, 'image/webp'); + if (!imageUrl) { + return null; + } + + const resetView = () => { + setScale(1); + setPosition({ x: 0, y: 0 }); + }; + + const handleClose = () => { + resetView(); + setOpen(false); + }; + + const handleDownload = async () => { + // Prefer the direct ref when we have it — it's a single request. + // downloadRef is the original full-size JPEG, not the WebP preview. + if (downloadRef) { + await downloadSpeciesImage(downloadRef, speciesName, 'image/jpeg'); + } else if (speciesId) { + await downloadSpeciesImageById(speciesId, speciesName); + } + }; + + const zoomIn = () => setScale((s) => Math.min(MAX_SCALE, s + SCALE_STEP)); + const zoomOut = () => + setScale((s) => { + const next = Math.max(MIN_SCALE, s - SCALE_STEP); + if (next === MIN_SCALE) setPosition({ x: 0, y: 0 }); + return next; + }); + + const handleWheel = (e: WheelEvent) => { + e.preventDefault(); + if (e.deltaY < 0) zoomIn(); + else zoomOut(); + }; + + const handleMouseDown = (e: MouseEvent) => { + if (scale === 1) return; + isDragging.current = true; + lastPos.current = { x: e.clientX, y: e.clientY }; + }; + + const handleMouseMove = (e: MouseEvent) => { + if (!isDragging.current) return; + const dx = e.clientX - lastPos.current.x; + const dy = e.clientY - lastPos.current.y; + lastPos.current = { x: e.clientX, y: e.clientY }; + setPosition((p) => ({ x: p.x + dx, y: p.y + dy })); + }; + + const stopDragging = () => { + isDragging.current = false; + }; + + return ( + <> + + {alt} { + (e.target as HTMLImageElement).style.display = 'none'; + }} + /> + {showActions && ( + + + + + )} + + + + + {speciesName || alt} + + + + + + + + + + + = MAX_SCALE}> + + + + + + + + + + + + + + + + { + (e.target as HTMLImageElement).style.display = 'none'; + }} + draggable={false} + sx={{ + maxWidth: '100%', + maxHeight: '100%', + objectFit: 'contain', + transform: `translate(${position.x}px, ${position.y}px) scale(${scale})`, + transition: isDragging.current + ? 'none' + : 'transform 0.15s ease-out', + cursor: scale > 1 ? 'grab' : 'default', + userSelect: 'none', + }} + /> + + + + + + + + ); +} \ No newline at end of file diff --git a/src/UI/components/species/speciesList.tsx b/src/UI/components/species/speciesList.tsx index 445b33809..5bb5f7590 100644 --- a/src/UI/components/species/speciesList.tsx +++ b/src/UI/components/species/speciesList.tsx @@ -1,4 +1,4 @@ -import { useEffect, useState } from 'react'; +import { useEffect, useState, useCallback } from 'react'; import { Button, Grid, @@ -20,6 +20,7 @@ import ArrowForwardIcon from '@mui/icons-material/ArrowForward'; import ReactMarkdown from 'react-markdown'; import { useTranslations } from 'next-intl'; import { getAllSpecies } from '../../state/speciesInformation/actions/getAllSpecies'; +import SpeciesImageViewer from './SpeciesImageViewer'; export default function SpeciesList(): JSX.Element { const t = useTranslations('SpeciesPage'); @@ -38,10 +39,10 @@ export default function SpeciesList(): JSX.Element { dispatch(getAllSpecies()); }, [dispatch]); - const [openDialog, setOpenDialog] = useState(false); // State to control dialog visibility + const [openDialog, setOpenDialog] = useState(false); const [selectedSpeciesId, setSelectedSpeciesId] = useState( null - ); // State for selected species ID + ); const handleClick = (speciesId: string) => { dispatch(setCurrentInfoDetails(speciesId)); @@ -54,23 +55,21 @@ export default function SpeciesList(): JSX.Element { }; const handleDeleteClick = (speciesId: string) => { - setSelectedSpeciesId(speciesId); // Set selected species ID - setOpenDialog(true); // Open the confirmation dialog + setSelectedSpeciesId(speciesId); + setOpenDialog(true); }; const handleConfirmDelete = () => { if (selectedSpeciesId) { - dispatch(deleteSpeciesInformation(selectedSpeciesId)); // Dispatch the delete action + dispatch(deleteSpeciesInformation(selectedSpeciesId)); } - setOpenDialog(false); // Close the dialog after confirming delete + setOpenDialog(false); }; const handleCloseDialog = () => { - setOpenDialog(false); // Close the dialog without deleting + setOpenDialog(false); }; - console.log('species', speciesList.items); - const panelStyle = { boxShadow: 3, margin: 3, @@ -136,18 +135,16 @@ export default function SpeciesList(): JSX.Element { justifyContent: 'center', }} > - - Mosquito Species #1 - +
@@ -188,7 +185,7 @@ export default function SpeciesList(): JSX.Element { backgroundColor: 'red', }} variant="contained" - onClick={() => handleDeleteClick(row.id as string)} // Handle delete click + onClick={() => handleDeleteClick(row.id as string)} className="DeleteButton" > {t('buttons.deleteItem')} @@ -202,7 +199,6 @@ export default function SpeciesList(): JSX.Element { ))} - {/* Dialog Box */} { const t = useTranslations('SpeciesPage'); - // ALL useState hooks const [shortDescription, setShortDescription] = useState(''); const [speciesImage, setSpeciesImage] = useState(''); + const [previewImage, setPreviewImage] = useState(''); const [name, setName] = useState(''); const [citationSearch, setCitationSearch] = useState(''); const [selectedCitations, setSelectedCitations] = useState([]); const [species, setSpecies] = useState(''); const [link, setLink] = useState(''); const [subsections, setSubsections] = useState([]); + const [uploadingImage, setUploadingImage] = useState(false); + const [saving, setSaving] = useState(false); - // ALL useAppSelector and useAppDispatch hooks const sources = useAppSelector((state) => state.source.source_info); + const token = useAppSelector((state) => state.auth.token); const dispatch = useAppDispatch(); const currentSpeciesInformation = useAppSelector( (s) => s.speciesInfo.currentInfoForEditing @@ -56,40 +60,47 @@ const SpeciesInformationEditor = () => { ); const allCitations = useAppSelector((s) => s.source.source_info.items); - // useRouter hook const router = useRouter(); const id = router.query.id as string | undefined; - // ALL useCallback hooks - const saveSpeciesInformation = useCallback(() => { + const saveSpeciesInformation = useCallback(async () => { const speciesInformation: SpeciesInformation = { id, name, shortDescription, description: JSON.stringify(subsections), speciesImage, + previewImage, citations: selectedCitations.map((c) => c.num_id), link: species, }; - dispatch(upsertSpeciesInformation(speciesInformation)); - toast.success('Species information saved!'); - setSubsections([]); - setName(''); - setShortDescription(''); - setSpeciesImage(''); + setSaving(true); + const resultAction = await dispatch( + upsertSpeciesInformation(speciesInformation) + ); + setSaving(false); + + if (upsertSpeciesInformation.fulfilled.match(resultAction)) { + setSubsections([]); + setName(''); + setShortDescription(''); + setSpeciesImage(''); + setPreviewImage(''); + } }, [ dispatch, id, name, shortDescription, speciesImage, + previewImage, subsections, selectedCitations, species, + token, ]); - // ALL useEffect hooks useEffect(() => { if (id) { dispatch(getSpeciesInformation(id)); @@ -100,12 +111,10 @@ const SpeciesInformationEditor = () => { dispatch(getSourceInfo()); }, [dispatch]); + // Populate the core species fields as soon as the record loads — + // this no longer waits on the citations list (sources.items). useEffect(() => { - console.log('All citations:', allCitations); - }, [allCitations]); - - useEffect(() => { - if (currentSpeciesInformation && sources.items?.length > 0) { + if (currentSpeciesInformation) { setName(currentSpeciesInformation.name); setShortDescription(currentSpeciesInformation.shortDescription); try { @@ -116,8 +125,15 @@ const SpeciesInformationEditor = () => { setSubsections([]); } setSpeciesImage(currentSpeciesInformation.speciesImage); + setPreviewImage(currentSpeciesInformation.previewImage); setLink(currentSpeciesInformation.link); + } + }, [currentSpeciesInformation]); + // Citation matching still depends on the sources list being loaded, + // so it stays in its own effect, separate from the fields above. + useEffect(() => { + if (currentSpeciesInformation && sources.items?.length > 0) { const rawCitations: string = currentSpeciesInformation.citations[0]; const citationIds = @@ -136,7 +152,6 @@ const SpeciesInformationEditor = () => { } }, [currentSpeciesInformation, sources.items]); - // Early return AFTER all hooks if (loadingSpeciesInformation) { return (
@@ -145,11 +160,11 @@ const SpeciesInformationEditor = () => { ); } - // Regular variables and functions const citationIds = selectedCitations.map((c) => c.num_id); - console.log('Selected citations:', selectedCitations); - console.log('Mapped citation IDs:', citationIds); + const handleBack = () => { + router.push('/species'); + }; const handleAddSubsection = () => { setSubsections([...subsections, { title: '', content: '' }]); @@ -169,11 +184,26 @@ const SpeciesInformationEditor = () => { setSubsections(updated); }; + // Uploads a JPEG species image. The backend no longer stores this + // anywhere external (no Azure) — it just validates the file, generates + // a WebP preview, and hands back both as base64 strings. Those base64 + // strings are held here in local state and only actually persisted + // when the form is submitted via saveSpeciesInformation, which sends + // them to createEditSpeciesInformation to be decoded and saved + // directly into the species_information table's bytea columns. const handleImageUpload = async (e: React.ChangeEvent) => { - if (e.target.files && e.target.files[0].size < UPLOAD_LIMIT_IN_KB * 1024) { - const speciesImage = await toBase64(e.target.files[0]); - setSpeciesImage(speciesImage); - } else { + const file = e.target.files?.[0]; + if (!file) { + return; + } + + // Only JPEG is accepted here, matching what the backend expects. + if (file.type !== 'image/jpeg') { + toast.error('Please upload a JPEG image.'); + return; + } + + if (file.size >= UPLOAD_LIMIT_IN_KB * 1024) { const error = t('speciesInformationEditor.uploadImageFileHelperText', { maxSize: UPLOAD_LIMIT_IN_KB, }); @@ -181,6 +211,20 @@ const SpeciesInformationEditor = () => { toast.error(error, { autoClose: 5000, }); + return; + } + + try { + setUploadingImage(true); + const result = await uploadSpeciesImageAuthenticated(file, token?.toString()); + setSpeciesImage(result.imageBase64); + setPreviewImage(result.previewBase64); + toast.success('Image uploaded successfully!'); + } catch (error) { + toast.error('Failed to upload image. Please try again.'); + console.error('Image upload error:', error); + } finally { + setUploadingImage(false); } }; @@ -193,6 +237,16 @@ const SpeciesInformationEditor = () => { return (
+ + {id ? t('speciesInformationEditor.edit') @@ -283,17 +337,19 @@ const SpeciesInformationEditor = () => { sx={{ display: 'flex', flexDirection: 'column', alignItems: 'center' }} > @@ -302,24 +358,23 @@ const SpeciesInformationEditor = () => { maxSize: UPLOAD_LIMIT_IN_KB, })} - {speciesImage && ( - - Species image - + {previewImage && ( + )} - Citation + {t('speciesInformationEditor.citation')} setCitationSearch(e.target.value)} sx={{ mb: 2 }} @@ -433,18 +488,23 @@ const SpeciesInformationEditor = () => {
); }; -export default SpeciesInformationEditor; +export default SpeciesInformationEditor; \ No newline at end of file diff --git a/src/UI/package-lock.json b/src/UI/package-lock.json index 6220f1a7c..77d2835a5 100644 --- a/src/UI/package-lock.json +++ b/src/UI/package-lock.json @@ -55,6 +55,7 @@ "react-quill": "^2.0.0", "react-redux": "^8.0.2", "react-scroll": "^1.8.9", + "react-swipeable-views-utils": "^0.14.2", "react-toastify": "^9.1.1", "recharts": "^3.7.0", "redux-mock-store": "^1.5.4", @@ -861,6 +862,7 @@ "version": "1.4.1", "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-1.4.1.tgz", "integrity": "sha512-XXrH9Uarn0stsyldqDYq8r++mROmWRI1xKMXa640Bb//SY1+ECYX6VzT6Lcx5frD0V30XieqJ0oX9I2Xj5aoMA==", + "dev": true, "dependencies": { "ajv": "^6.12.4", "debug": "^4.3.2", @@ -1058,6 +1060,7 @@ "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.9.5.tgz", "integrity": "sha512-ObyMyWxZiCu/yTisA7uzx81s40xR2fD5Cg/2Kq7G02ajkNubJf6BopgDTmDyc3U7sXpNKM8cYOw7s7Tyr+DnCw==", "deprecated": "Use @eslint/config-array instead", + "dev": true, "dependencies": { "@humanwhocodes/object-schema": "^1.2.1", "debug": "^4.1.1", @@ -1071,7 +1074,8 @@ "version": "1.2.1", "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz", "integrity": "sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==", - "deprecated": "Use @eslint/object-schema instead" + "deprecated": "Use @eslint/object-schema instead", + "dev": true }, "node_modules/@icons/material": { "version": "0.2.4", @@ -2763,27 +2767,6 @@ "url": "https://github.com/sponsors/tannerlinsley" } }, - "node_modules/@testing-library/dom": { - "version": "10.4.1", - "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.1.tgz", - "integrity": "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@babel/code-frame": "^7.10.4", - "@babel/runtime": "^7.12.5", - "@types/aria-query": "^5.0.1", - "aria-query": "5.3.0", - "dom-accessibility-api": "^0.5.9", - "lz-string": "^1.5.0", - "picocolors": "1.1.1", - "pretty-format": "^27.0.2" - }, - "engines": { - "node": ">=18" - } - }, "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", @@ -3366,7 +3349,8 @@ "node_modules/@types/scheduler": { "version": "0.26.0", "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.26.0.tgz", - "integrity": "sha512-WFHp9YUJQ6CKshqoC37iOlHnQSmxNc795UhB26CyBBttrN9svdIrUjl/NjnNmfcwtncN0h/0PPAFWv9ovP8mLA==" + "integrity": "sha512-WFHp9YUJQ6CKshqoC37iOlHnQSmxNc795UhB26CyBBttrN9svdIrUjl/NjnNmfcwtncN0h/0PPAFWv9ovP8mLA==", + "dev": true }, "node_modules/@types/semver": { "version": "7.7.1", @@ -3643,6 +3627,7 @@ "version": "5.3.2", "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } @@ -3680,6 +3665,7 @@ "version": "6.12.6", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", @@ -3722,6 +3708,7 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, "engines": { "node": ">=8" } @@ -3730,6 +3717,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" }, @@ -3762,7 +3750,8 @@ "node_modules/argparse": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==" + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true }, "node_modules/aria-query": { "version": "5.3.0", @@ -4139,7 +4128,8 @@ "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true }, "node_modules/baseline-browser-mapping": { "version": "2.8.27", @@ -4154,6 +4144,7 @@ "version": "1.1.12", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -4437,6 +4428,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" }, @@ -4447,7 +4439,8 @@ "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/combined-stream": { "version": "1.0.8", @@ -4472,7 +4465,8 @@ "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==" + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true }, "node_modules/convert-source-map": { "version": "1.9.0", @@ -4528,6 +4522,7 @@ "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", @@ -4865,7 +4860,8 @@ "node_modules/deep-is": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", - "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==" + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true }, "node_modules/deepmerge": { "version": "4.3.1", @@ -4974,6 +4970,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", + "dev": true, "dependencies": { "esutils": "^2.0.2" }, @@ -5764,6 +5761,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-3.0.0.tgz", "integrity": "sha512-uuQC43IGctw68pJA1RgbQS8/NP7rch6Cwd4j3ZBtgo4/8Flj4eGE7ZYSZRN3iq5pVUv6GPdW5Z1RFleo84uLDA==", + "dev": true, "dependencies": { "eslint-visitor-keys": "^2.0.0" }, @@ -5781,6 +5779,7 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz", "integrity": "sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw==", + "dev": true, "engines": { "node": ">=10" } @@ -5800,6 +5799,7 @@ "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" @@ -5815,6 +5815,7 @@ "version": "7.2.2", "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", + "dev": true, "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^5.2.0" @@ -5830,6 +5831,7 @@ "version": "9.6.1", "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", + "dev": true, "dependencies": { "acorn": "^8.9.0", "acorn-jsx": "^5.3.2", @@ -5859,6 +5861,7 @@ "version": "1.6.0", "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==", + "dev": true, "dependencies": { "estraverse": "^5.1.0" }, @@ -5889,6 +5892,7 @@ "version": "2.0.3", "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, "engines": { "node": ">=0.10.0" } @@ -5954,7 +5958,8 @@ "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true }, "node_modules/fast-diff": { "version": "1.3.0", @@ -5991,12 +5996,14 @@ "node_modules/fast-json-stable-stringify": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", - "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==" + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true }, "node_modules/fast-levenshtein": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", - "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==" + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true }, "node_modules/fastq": { "version": "1.19.1", @@ -6019,6 +6026,7 @@ "version": "6.0.1", "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", + "dev": true, "dependencies": { "flat-cache": "^3.0.4" }, @@ -6075,6 +6083,7 @@ "version": "3.2.0", "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.2.0.tgz", "integrity": "sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==", + "dev": true, "dependencies": { "flatted": "^3.2.9", "keyv": "^4.5.3", @@ -6087,7 +6096,8 @@ "node_modules/flatted": { "version": "3.3.3", "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", - "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==" + "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", + "dev": true }, "node_modules/follow-redirects": { "version": "1.15.11", @@ -6191,7 +6201,8 @@ "node_modules/fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==" + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true }, "node_modules/fsevents": { "version": "2.3.3", @@ -6238,7 +6249,8 @@ "node_modules/functional-red-black-tree": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz", - "integrity": "sha512-dsKNQNdj6xA3T+QlADDA7mOSlX0qiMINjn0cgr+eGHGsbSHzTabcIogz2+p/iqP1Xs6EP/sS2SbqH+brGTbq0g==" + "integrity": "sha512-dsKNQNdj6xA3T+QlADDA7mOSlX0qiMINjn0cgr+eGHGsbSHzTabcIogz2+p/iqP1Xs6EP/sS2SbqH+brGTbq0g==", + "dev": true }, "node_modules/functions-have-names": { "version": "1.2.3", @@ -6396,6 +6408,7 @@ "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.7.tgz", "integrity": "sha512-OvD9ENzPLbegENnYP5UUfJIirTg4+XwMWGaQfQTY0JenxNvvIKP3U3/tAQSPIu/lHxXYSZmpXlUHeqAIdKzBLQ==", "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", @@ -6415,6 +6428,7 @@ "version": "6.0.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, "dependencies": { "is-glob": "^4.0.3" }, @@ -6426,6 +6440,7 @@ "version": "13.24.0", "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", + "dev": true, "dependencies": { "type-fest": "^0.20.2" }, @@ -6514,6 +6529,7 @@ "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" } @@ -6787,6 +6803,7 @@ "version": "0.1.4", "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, "engines": { "node": ">=0.8.19" } @@ -6805,6 +6822,7 @@ "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "dev": true, "dependencies": { "once": "^1.3.0", "wrappy": "1" @@ -8580,6 +8598,7 @@ "version": "4.1.1", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "dev": true, "dependencies": { "argparse": "^2.0.1" }, @@ -8647,7 +8666,8 @@ "node_modules/json-buffer": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", - "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==" + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true }, "node_modules/json-parse-even-better-errors": { "version": "2.3.1", @@ -8657,12 +8677,14 @@ "node_modules/json-schema-traverse": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==" + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true }, "node_modules/json-stable-stringify-without-jsonify": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", - "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==" + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true }, "node_modules/json-stringify-pretty-compact": { "version": "2.0.0", @@ -8750,10 +8772,17 @@ "resolved": "https://registry.npmjs.org/kdbush/-/kdbush-4.0.2.tgz", "integrity": "sha512-WbCVYJ27Sz8zi9Q7Q0xHC+05iwkm3Znipc2XTlrnJbsHMYktW4hPhXUE8Ys1engBrvffoSCqbil1JQAa7clRpA==" }, + "node_modules/keycode": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/keycode/-/keycode-2.2.1.tgz", + "integrity": "sha512-Rdgz9Hl9Iv4QKi8b0OlCRQEzp4AgVxyCtz5S/+VIHezDmrDhkp2N2TqBWOLz0/gbeREXOOiI9/4b8BY9uw2vFg==", + "license": "MIT" + }, "node_modules/keyv": { "version": "4.5.4", "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, "dependencies": { "json-buffer": "3.0.1" } @@ -8808,6 +8837,7 @@ "version": "0.4.1", "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, "dependencies": { "prelude-ls": "^1.2.1", "type-check": "~0.4.0" @@ -8892,7 +8922,8 @@ "node_modules/lodash.merge": { "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", - "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==" + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true }, "node_modules/lodash.throttle": { "version": "4.1.1", @@ -9580,6 +9611,7 @@ "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, "dependencies": { "brace-expansion": "^1.1.7" }, @@ -9647,7 +9679,8 @@ "node_modules/natural-compare": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", - "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==" + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true }, "node_modules/natural-compare-lite": { "version": "1.4.0", @@ -9988,6 +10021,7 @@ "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, "dependencies": { "wrappy": "1" } @@ -10041,6 +10075,7 @@ "version": "0.9.4", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, "dependencies": { "deep-is": "^0.1.3", "fast-levenshtein": "^2.0.6", @@ -10184,6 +10219,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, "engines": { "node": ">=0.10.0" } @@ -10192,6 +10228,7 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, "engines": { "node": ">=8" } @@ -10310,6 +10347,7 @@ "version": "1.2.1", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, "engines": { "node": ">= 0.8.0" } @@ -10449,6 +10487,7 @@ "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, "engines": { "node": ">=6" } @@ -10605,6 +10644,20 @@ "react": ">=16.13.1" } }, + "node_modules/react-event-listener": { + "version": "0.6.7", + "resolved": "https://registry.npmjs.org/react-event-listener/-/react-event-listener-0.6.7.tgz", + "integrity": "sha512-DLxxUSjZT5Qg8u+51V0KPKW7FI4zCkbP7fjKNGpaRItjaYgO4WycwoqJDumw+UGGQ0DmmPbQ3RUH8LsqQgHWpQ==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.2.0", + "prop-types": "^15.6.0", + "warning": "^4.0.1" + }, + "peerDependencies": { + "react": "^16.3.0" + } + }, "node_modules/react-hook-form": { "version": "7.66.0", "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.66.0.tgz", @@ -10765,6 +10818,42 @@ "react-dom": "^15.5.4 || ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, + "node_modules/react-swipeable-views-core": { + "version": "0.14.2", + "resolved": "https://registry.npmjs.org/react-swipeable-views-core/-/react-swipeable-views-core-0.14.2.tgz", + "integrity": "sha512-x61l9bPAuE+m7osUrli1RSqezNlo4pALZ4QRlZdY0esPv0PS6uqygQsmYnY3YEaKGzXt98kqgaMooSNGV1oFZQ==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.0.0", + "warning": "^4.0.1" + }, + "engines": { + "node": ">=6.0.0" + }, + "peerDependencies": { + "react": "^15.3.0 || ^16.0.0 || ^17.0.0" + } + }, + "node_modules/react-swipeable-views-utils": { + "version": "0.14.2", + "resolved": "https://registry.npmjs.org/react-swipeable-views-utils/-/react-swipeable-views-utils-0.14.2.tgz", + "integrity": "sha512-6DekO3vdxGQOPFlF9wllxOoeYtacFsY6/EgJ3UYPTNa7CiGwQzzSMcVaNVp/Fj/javytyt2CQjVdMLTecs9Ncw==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.0.0", + "keycode": "^2.1.7", + "prop-types": "^15.6.0", + "react-event-listener": "^0.6.0", + "react-swipeable-views-core": "^0.14.2", + "shallow-equal": "^1.2.1" + }, + "engines": { + "node": ">=6.0.0" + }, + "peerDependencies": { + "react": "^15.3.0 || ^16.0.0 || ^17.0.0" + } + }, "node_modules/react-toastify": { "version": "9.1.3", "resolved": "https://registry.npmjs.org/react-toastify/-/react-toastify-9.1.3.tgz", @@ -10956,6 +11045,7 @@ "version": "3.2.0", "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-3.2.0.tgz", "integrity": "sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg==", + "dev": true, "engines": { "node": ">=8" }, @@ -11099,6 +11189,7 @@ "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", "deprecated": "Rimraf versions prior to v4 are no longer supported", + "dev": true, "dependencies": { "glob": "^7.1.3" }, @@ -11321,10 +11412,17 @@ "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==" }, + "node_modules/shallow-equal": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/shallow-equal/-/shallow-equal-1.2.1.tgz", + "integrity": "sha512-S4vJDjHHMBaiZuT9NPb616CSmLf618jawtv3sufLl6ivK8WocjAo58cXwbRV1cgqxH0Qbv+iUt6m05eqEa2IRA==", + "license": "MIT" + }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, "dependencies": { "shebang-regex": "^3.0.0" }, @@ -11336,6 +11434,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, "engines": { "node": ">=8" } @@ -11718,6 +11817,7 @@ "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, "dependencies": { "ansi-regex": "^5.0.1" }, @@ -11759,6 +11859,7 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, "engines": { "node": ">=8" }, @@ -11819,6 +11920,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" }, @@ -11903,7 +12005,8 @@ "node_modules/text-table": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", - "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==" + "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", + "dev": true }, "node_modules/through": { "version": "2.3.8", @@ -12182,6 +12285,7 @@ "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, "dependencies": { "prelude-ls": "^1.2.1" }, @@ -12202,6 +12306,7 @@ "version": "0.20.2", "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "dev": true, "engines": { "node": ">=10" }, @@ -12446,6 +12551,7 @@ "version": "4.4.1", "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, "dependencies": { "punycode": "^2.1.0" } @@ -12537,7 +12643,8 @@ "node_modules/v8-compile-cache": { "version": "2.4.0", "resolved": "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-2.4.0.tgz", - "integrity": "sha512-ocyWc3bAHBB/guyqJQVI5o4BZkPhznPYUG2ea80Gond/BgNWpap8TOmLSeeQG7bnh2KMISxskdADG59j7zruhw==" + "integrity": "sha512-ocyWc3bAHBB/guyqJQVI5o4BZkPhznPYUG2ea80Gond/BgNWpap8TOmLSeeQG7bnh2KMISxskdADG59j7zruhw==", + "dev": true }, "node_modules/v8-compile-cache-lib": { "version": "3.0.1", @@ -12656,6 +12763,15 @@ "makeerror": "1.0.12" } }, + "node_modules/warning": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/warning/-/warning-4.0.3.tgz", + "integrity": "sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.0.0" + } + }, "node_modules/web-worker": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/web-worker/-/web-worker-1.5.0.tgz", @@ -12708,6 +12824,7 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, "dependencies": { "isexe": "^2.0.0" }, @@ -12829,6 +12946,7 @@ "version": "1.2.5", "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, "engines": { "node": ">=0.10.0" } @@ -12853,7 +12971,8 @@ "node_modules/wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true }, "node_modules/write-file-atomic": { "version": "4.0.2", @@ -12995,24 +13114,6 @@ "node": ">=12" } }, - "node_modules/yjs": { - "version": "13.6.29", - "resolved": "https://registry.npmjs.org/yjs/-/yjs-13.6.29.tgz", - "integrity": "sha512-kHqDPdltoXH+X4w1lVmMtddE3Oeqq48nM40FD5ojTd8xYhQpzIDcfE2keMSU5bAgRPJBe225WTUdyUgj1DtbiQ==", - "license": "MIT", - "peer": true, - "dependencies": { - "lib0": "^0.2.99" - }, - "engines": { - "node": ">=16.0.0", - "npm": ">=8.0.0" - }, - "funding": { - "type": "GitHub Sponsors ❤", - "url": "https://github.com/sponsors/dmonad" - } - }, "node_modules/yn": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", diff --git a/src/UI/package.json b/src/UI/package.json index e6873f741..e39e74f96 100644 --- a/src/UI/package.json +++ b/src/UI/package.json @@ -60,6 +60,7 @@ "react-quill": "^2.0.0", "react-redux": "^8.0.2", "react-scroll": "^1.8.9", + "react-swipeable-views-utils": "^0.14.2", "react-toastify": "^9.1.1", "recharts": "^3.7.0", "redux-mock-store": "^1.5.4", diff --git a/src/UI/pages/_app.tsx b/src/UI/pages/_app.tsx index 43b8abaae..f0502443f 100644 --- a/src/UI/pages/_app.tsx +++ b/src/UI/pages/_app.tsx @@ -1,3 +1,4 @@ +// @ts-ignore import '../styles/globals.css'; import type { AppProps } from 'next/app'; import Head from 'next/head'; @@ -10,6 +11,7 @@ import store from '../state/store'; import NavBar from '../components/shared/navbar'; // import Footer from '../components/shared/footer'; import { useEffect } from 'react'; +// @ts-ignore import 'react-toastify/dist/ReactToastify.css'; import { ToastContainer } from 'react-toastify'; import Script from 'next/script'; diff --git a/src/UI/pages/api/auth/verifyToken.ts b/src/UI/pages/api/auth/verifyToken.ts index 4abd7746f..34d609cec 100644 --- a/src/UI/pages/api/auth/verifyToken.ts +++ b/src/UI/pages/api/auth/verifyToken.ts @@ -26,6 +26,7 @@ export default async function handler( throw new Error('No valid token provided for verification.'); } + console.log('Received token for verification:', token, TOKEN_KEY); // 2. SECURELY VERIFY TOKEN: Using the server-only key const verifiedToken: any = njwt.verify(token, TOKEN_KEY); diff --git a/src/UI/pages/edit_source.tsx b/src/UI/pages/edit_source.tsx new file mode 100644 index 000000000..f8f23b200 --- /dev/null +++ b/src/UI/pages/edit_source.tsx @@ -0,0 +1,94 @@ +import { Container, Button, Box } from '@mui/material'; +import ArrowBackIcon from '@mui/icons-material/ArrowBack'; +import React, { useEffect } from 'react'; +import { useRouter } from 'next/router'; +import { useDispatch } from 'react-redux'; +import { useTranslations } from 'next-intl'; +import AuthWrapper from '../components/shared/AuthWrapper'; +import SourceForm, { NewSource } from '../components/sources/source_form'; +import { getMessages } from '../utils/localization'; +import { GetServerSidePropsContext } from 'next'; +import { AppDispatch } from '../state/store'; +import { useAppSelector } from '../state/hooks'; +import { getSourceById } from '../state/source/actions/getSourceById'; +import { clearSourceEdit } from '../state/source/sourceSlice'; + +function EditSource(): JSX.Element { + const router = useRouter(); + const dispatch = useDispatch(); + const t = useTranslations('NewSourcePage'); + + const idParam = router.query.id as string | undefined; + + const source_edit = useAppSelector((state) => state.source.source_edit); + const source_edit_status = useAppSelector( + (state) => state.source.source_edit_status + ); + + useEffect(() => { + if (idParam) { + const num_id = parseInt(idParam, 10); + if (!isNaN(num_id)) { + dispatch(getSourceById(num_id)); + } + } + + return () => { + dispatch(clearSourceEdit()); + }; + }, [idParam, dispatch]); + + const handleBack = () => { + if (window.history.length > 1) { + router.back(); + } else { + router.push('/sources'); + } + }; + + return ( + <> +
+
+ + + + +
+ + {source_edit_status === 'success' && source_edit ? ( + + ) : source_edit_status === 'error' ? ( +
+ Could not load this source. It may have been deleted, or the + link is invalid. +
+ ) : ( +
Loading....
+ )} +
+
+
+
+
+ + ); +} + +export async function getServerSideProps(context: GetServerSidePropsContext) { + return await getMessages(context); +} + +export default EditSource; diff --git a/src/UI/pages/sources.tsx b/src/UI/pages/sources.tsx index bcf84574e..fff2776e7 100644 --- a/src/UI/pages/sources.tsx +++ b/src/UI/pages/sources.tsx @@ -14,6 +14,7 @@ const SourceTableNoSsr = dynamic( { ssr: false } ); + export default function SourcesPage(): JSX.Element { const t = useTranslations('SourcesPage'); const dispatch = useDispatch(); diff --git a/src/UI/pages/species/details.tsx b/src/UI/pages/species/details.tsx index 82ed04d9f..8e32c324d 100644 --- a/src/UI/pages/species/details.tsx +++ b/src/UI/pages/species/details.tsx @@ -18,6 +18,8 @@ import { getMessages } from '../../utils/localization'; import { GetServerSidePropsContext } from 'next'; import { getSourceInfo } from '../../state/source/actions/getSourceInfo'; import { getOccurrenceData } from '../../state/map/actions/getOccurrenceData'; +import SpeciesImageViewer from '../../components/species/SpeciesImageViewer'; + export default function SpeciesDetails() { const router = useRouter(); const dispatch = useAppDispatch(); @@ -133,14 +135,12 @@ export default function SpeciesDetails() {
- Mosquito Species #1
- {/* - Distribution Map - - */} @@ -231,4 +209,4 @@ export default function SpeciesDetails() { } export async function getServerSideProps(context: GetServerSidePropsContext) { return await getMessages(context); -} +} \ No newline at end of file diff --git a/src/UI/pages/species/edit.tsx b/src/UI/pages/species/edit.tsx index d2cb8c81b..57900c87f 100644 --- a/src/UI/pages/species/edit.tsx +++ b/src/UI/pages/species/edit.tsx @@ -4,6 +4,7 @@ import AuthWrapper from '../../components/shared/AuthWrapper'; import SpeciesInformationEditor from '../../components/speciesInformation/speciesInformationEditor'; import { getMessages } from '../../utils/localization'; import { GetServerSidePropsContext } from 'next'; +import { RolesEnum } from '../../state/state.types'; const SpeciesInformationEditorPage = (): JSX.Element => { return ( @@ -18,7 +19,7 @@ const SpeciesInformationEditorPage = (): JSX.Element => { }} >
- +
diff --git a/src/UI/public/messages/en.json b/src/UI/public/messages/en.json index f5d3e9eb5..f71a2fae8 100644 --- a/src/UI/public/messages/en.json +++ b/src/UI/public/messages/en.json @@ -515,7 +515,7 @@ "paragraph1": "Maps are powerful tools. They can illustrate the distribution of mosquito vector species known to transmit some of the world's most debilitating diseases and highlight where these species are no longer susceptible to the insecticides used as their primary method of control.", "paragraph2": "All evidence-based maps rely on field data collected in a myriad of different ways by multiple data collectors for a wide variety of purposes. In isolation, these data are able to answer the questions they were collected to address, but when combined, their value multiplies.", "paragraph3": "The Vector Atlas is an international project dedicated to synthesising complex vector data into intuitive mapped surfaces to support vector control decision making. At its core is the Vector Atlas Data Base (VADB), built over the past three years and continuing to expand. The VADB combines African vector occurrence, bionomics and insecticide resistance data alongside human behaviour, local environment and community information.", - "paragraph4": "It provides a 'one stop shop' of relatable and cross-referenced data access and underpins a comprehensive suite of vector maps, including species suitability, phenotypic insecticide resistance and relative abundance. All curated data and modelled surfaces are available for download via the map page on this platform. Working closely with expert vector teams in Burkina Faso, Côte d’Ivoire, the Democratic Republic of the Congo, Nigeria, Senegal and Uganda, we are bringing spatial modelling to the core of malaria vector control decisions.", + "paragraph4": "It provides a 'one stop shop' of relatable and cross-referenced data access and underpins a comprehensive suite of vector maps, including species suitability, phenotypic insecticide resistance and relative abundance. All curated data and modelled surfaces are available for download via the map page on this platform. Working closely with expert vector teams in Burkina Faso, Côte d'Ivoire, the Democratic Republic of the Congo, Nigeria, Senegal and Uganda, we are bringing spatial modelling to the core of malaria vector control decisions.", "paragraph5": "The Vector Atlas is a University of Oxford, International Centre of Insect Physiology and Ecology (icipe) and The Kids (Australia) initiative, working alongside the Malaria Atlas Project and funded by the Gates Foundation.", "paragraph6": "We are always interested in receiving feedback to continue developing the Vector Atlas resources shared via this platform. If you have any comments, questions or suggestions, please contact us via email." }, @@ -526,138 +526,87 @@ "email": "vectoratlas@icipe.org" } }, - - - "cataloguePage": { - "title": "Species Catalogue", - "grid": { - "id": "ID", - "scientificName": "Scientific Name", - "displayName": "Display Name", - "category": "Category", - "color": "Color" - }, - "filters": { - "searchPlaceholder": "Search species...", - "categories": { - "all": "All", - "Primary": "Primary", - "Secondary": "Secondary", - "Uncategorized": "Uncategorized" - } - }, - "editPage": { - "backBtn": "← Back to Catalogue", - "header": "Edit Species Entry", - "coreIdLabel": "System Core ID Record Link", - "scientificNameLabel": "Scientific Name (Database Reference)", - "scientificNameHelper": "Standard classification label defined during initial data ingestion.", - "displayNamePlaceholder": "Enter a friendly layout name (e.g. Gambiae Variety)", - "colorLabel": "Custom Map Dot Color", - "colorHelperSecondary": "Secondary category species are locked to the standard green display color.", - "colorHelperPrimary": "Adjust this picker manually to modify the vector display layer color.", - "cancel": "Cancel", - "updateBtn": "Update Parameters", - "accessDenied": "Access Denied", - "returnBtn": "Return to Catalogue", - "alerts": { - "successTitle": "Species Record Saved", - "successText": "Successfully updated parameters.", - "errorTitle": "Submission Error", - "loadError": "Failed to load active species profile record from database" - } - } - }, "SpeciesPage": { - "title": "Species List", - "confirmDeleteTitle": "Confirm Delete", + "title": "Species list", + "confirmDeleteTitle": "Confirm deletion", "confirmDeleteMessage": "Are you sure you want to delete this species information? This action cannot be undone.", "buttons": { - "create": "Create New Species", - "edit": "Edit Item", - "deleteItem": "Delete Item", - "more": "See more details", - "cancel": "Cancel", - "delete": "Delete" - }, - "countryTable": { - "title": "Country Registry Catalog", - "grid": { - "name": "Country Name", - "alternativeNames": "Alternative Names (Legacy)" - }, - "filters": { - "searchPlaceholder": "Search countries..." - }, - "tableStates": { - "loading": "Loading countries...", - "empty": "No records exist matching current filters." - }, - "editPage": { - "header": "Edit Country Name", - "backBtn": "← Back to Catalogue", - "coreIdLabel": "System Core ID (Immutable)", - "alternativeNamesLabel": "Alternative Names (Legacy Backups)", - "alternativeNamesHelper": "Previous database names are stored here to prevent breaking legacy map data.", - "displayNameLabel": "Country Display Name", - "displayNamePlaceholder": "e.g. Côte d'Ivoire", - "displayNameHelper": "Update this field to fix capitalization or formatting.", + "create": "Create new species", + "edit": "Edit item", + "deleteItem": "Delete item", + "more": "View more details", "cancel": "Cancel", - "updateBtn": "Update Name", - "alerts": { - "loadError": "Failed to load active country profile record from database.", - "notFound": "Country record not found in the database.", - "successTitle": "Country Updated", - "successText": "Successfully updated the country display name.", - "errorTitle": "Submission Error", - "errorText": "Failed to execute database transmission." - } + "delete": "Delete", + "preview": "Preview", + "download": "Download", + "downloadFull": "Download full image", + "close": "Close", + "back": "Back to Species List" + }, + "tooltips": { + "zoomOut": "Zoom out", + "zoomIn": "Zoom in", + "resetZoom": "Reset zoom", + "closePreview": "Close preview" + }, + "speciesInformationEditor": { + "create": "Create species information", + "edit": "Update species information", + "name": "Name", + "nameHelperText": "Name cannot be empty", + "shortDescription": "Short description", + "shortDescriptionHelperText": "Short description cannot be empty", + "fullDescription": "Full description", + "image": "Species image", + "uploadImageFile": "Upload Species Image File", + "uploadImageFileHelperText": "Files must be smaller than {maxSize} KB", + "distributionMapImage": "Distribution map image", + "citation": "Citations", + "buttons": { + "create": "Create", + "update": "Update" + } } }, - "speciesInformationEditor": { - "create": "Create species information", - "edit": "Update species information", - "name": "Name", - "nameHelperText": "Name cannot be empty", - "shortDescription": "Short description", - "shortDescriptionHelperText": "Short description cannot be empty", - "fullDescription": "Full description", - "image": "Species image", - "uploadImageFile": "Upload Species Image File", - "uploadImageFileHelperText": "Files must be smaller than {maxSize} KB", - "distributionMapImage": "Distribution map image", - "buttons": { - "create": "Create", - "update": "Update" - } - } - }, - "SourcesPage": { - "title": "Source List", - "grid": { - "id": "ID", - "author": "Author", - "title": "Title", - "journalTitle": "Journal Title", - "year": "Year", - "published": "Published", - "vectorData": "Vector Data" - }, - "filters": { - "errorMsg": "Please enter range (e.g. 100-200)", - "idFilter": "Filter by id (e.g. 100-200)", - "titleFilter": "Filter by Title" - } - }, + +"SourcesPage": { + "title": "Source List", + "edit": "Edit", + "delete": "Delete", + "deleteConfirm": { + "title": "Delete this source?", + "message": "This action cannot be undone. Are you sure you want to delete this source?", + "confirm": "Delete", + "cancel": "Cancel" + }, + "filters": { + "titleFilter": "Filter by Title", + "fieldTitle": "Title", + "fieldAuthor": "Author", + "fieldJournalTitle": "Journal Title", + "searchPlaceholder": "Search..." + }, + "grid": { + "author": "Author", + "title": "Title", + "journalTitle": "Journal Title", + "year": "Year", + "actions": "Actions" + } +}, "NewSourcePage": { "title": "Add a new reference source", + "editTitle": "Edit reference source", "author": "Author", "authorHelperText": "Author is a required field", "articleTitle": "Article Title", "articleTitleHelperText": "Article Title is a required field", - "citation": "Citation", + "journalTitle": "Journal Title", + + "journalTitleHelperText": "Journal Title is a required field", + "citation": " DOI Citation", "citationHelperText": "Article Title is a required field", "year": "Year", "yearHelperText": "Year is a required field", @@ -666,10 +615,13 @@ "published": "Published", "vectorData": "Vector Data", "buttons": { - "submit": "Submit", - "reset": "Reset" - } - }, + "update": "Update", + "submit": "Submit", + "reset": "Reset", + "back": "Back to Source List" + } + }, + "AdminPage": { "title": "Administration", "datasets": "Datasets", @@ -884,7 +836,9 @@ "datasetLogsLoadError": "Something went wrong when retrieved dataset logs. Please try again", "reuploadRequestError": "Something went wrong with requesting dataset re-upload. Please try again", "reuploadError": "Something went wrong with dataset re-upload. Please try again", - "deleteError": "Error deleting dataset" + "deleteError": "Error deleting dataset", + "updateError": "Unknown error updating reference. Please try again.", + "updateSuccess": "Reference {id} updated successfully" } }, "UploadedModel": { @@ -976,11 +930,14 @@ } }, "Source": { - "createSuccess": "Reference created with id {id}", - "errors": { - "createError": "Unknown error in creating new reference. Please try again.", - "duplicateSource": "Reference with title {article_title} already exists" - } + "createSuccess": "Reference created with id {id}", + "updateSuccess": "Reference {id} updated successfully", + "errors": { + "createError": "Unknown error in creating new reference. Please try again.", + "duplicateSource": "Reference with title {article_title} already exists", + "updateError": "Unknown error updating reference. Please try again." + } + }, "SpeciesInformation": { "updateSuccess": "Updated species information with id {id}", diff --git a/src/UI/public/messages/fr.json b/src/UI/public/messages/fr.json index 23f9c7d73..24bff7ed3 100644 --- a/src/UI/public/messages/fr.json +++ b/src/UI/public/messages/fr.json @@ -2,28 +2,25 @@ "PageTitles": { "home": "Créer une application suivante" }, - "MenuItems": { - "map": "Carte", - "upload": "Télécharger", - "news": "Nouvelles", - "about": "À propos", - "more": "Plus", - "help": "Aide", - "catalogue":"Catalogue", - "login": "Se connecter", - "species": "Liste des espèces", - "source": "Liste de sources", - "countryCatalogue": "Catalogue des pays", - "addSource": "Ajouter la source", - "datasets": "Ensembles de données", - "editPointData": "Modifier les données de point", - "models": "Modèles", - "communication": "Communication", - "doi": "Doi", - "admin": "Administrer", - "translations": "Traductions" - }, - + "MenuItems": { + "Data": "Données", + "upload": "Télécharger", + "news": "Nouvelles", + "about": "À propos", + "more": "Plus", + "help": "Aide", + "login": "Se connecter", + "species": "Liste des espèces", + "source": "Liste de sources", + "addSource": "Ajouter la source", + "datasets": "Ensembles de données", + "editPointData": "Modifier les données de point", + "models": "Modèles", + "communication": "Communication", + "doi": "Doi", + "admin": "Administrer", + "translations": "Traductions" +}, "HomePage": { "list1": "Commencez par l'édition", "list2": "Enregistrez et voyez vos modifications instantanément" @@ -564,60 +561,85 @@ "email": "E-mail" } }, + "SpeciesPage": { "title": "Liste des espèces", "confirmDeleteTitle": "Confirmer la suppression", - "confirmDeleteMessage": "Êtes-vous sûr de vouloir supprimer les informations de cette espèce? ", + "confirmDeleteMessage": "Êtes-vous sûr de vouloir supprimer ces informations sur l'espèce ? Cette action est irréversible.", "buttons": { - "create": "Créer de nouvelles espèces", - "edit": "Modifier", - "deleteItem": "Supprimer", + "create": "Créer une nouvelle espèce", + "edit": "Modifier l'élément", + "deleteItem": "Supprimer l'élément", "more": "Voir plus de détails", "cancel": "Annuler", - "delete": "Supprimer" + "delete": "Supprimer", + "preview": "Aperçu", + "download": "Télécharger", + "downloadFull": "Télécharger l'image complète", + "close": "Fermer", + "back": "Retour à la liste des espèces" }, - "speciesInformationEditor": { - "create": "Créer des informations sur les espèces", - "edit": "Mettre à jour les informations sur les espèces", - "name": "Nom", - "nameHelperText": "Le nom ne peut pas être vide", - "shortDescription": "Brève description", - "shortDescriptionHelperText": "La description courte ne peut pas être vide", - "fullDescription": "Description complète", - "image": "Image de l'espèce", - "uploadImageFile": "Télécharger le fichier d'image des espèces", - "uploadImageFileHelperText": "Fichiers doivent être plus petites que {maxSize} kb", - "distributionMapImage": "Image de la carte de distribution", - "buttons": { - "create": "Créer", - "update": "Mise à jour" - } - } - }, - "SourcesPage": { - "title": "Liste de sources", - "grid": { - "id": "IDENTIFIANT", - "author": "Auteur", - "title": "Titre", - "journalTitle": "Titre de la revue", - "year": "Année", - "published": "Publié", - "vectorData": "Données vectorielles" + "tooltips": { + "zoomOut": "Zoom arrière", + "zoomIn": "Zoom avant", + "resetZoom": "Réinitialiser le zoom", + "closePreview": "Fermer l'aperçu" }, - "filters": { - "errorMsg": "Veuillez saisir la gamme (par exemple 100-200)", - "idFilter": "Filtre par ID (par exemple 100-200)", - "titleFilter": "Filtre par titre" + "speciesInformationEditor": { + "create": "Créer les informations de l'espèce", + "edit": "Modifier les informations de l'espèce", + "name": "Nom", + "nameHelperText": "Le nom ne peut pas être vide", + "shortDescription": "Description courte", + "shortDescriptionHelperText": "La description courte ne peut pas être vide", + "fullDescription": "Description complète", + "image": "Image de l'espèce", + "uploadImageFile": "Téléverser le fichier image de l'espèce", + "uploadImageFileHelperText": "Les fichiers doivent être inférieurs à {maxSize} Ko", + "distributionMapImage": "Carte de répartition", + "citation": "Citations", + "buttons": { + "create": "Créer", + "update": "Mettre à jour" + } } }, + +"SourcesPage": { + "title": "Liste des sources", + "filters": { + "titleFilter": "Filtrer par titre", + "fieldTitle": "Titre", + "fieldAuthor": "Auteur", + "fieldJournalTitle": "Titre du journal", + "searchPlaceholder": "Rechercher..." + }, + "grid": { + "author": "Auteur", + "title": "Titre", + "journalTitle": "Titre du journal", + "year": "Année", + "actions": "Actions" + }, + "edit": "Modifier", + "delete": "Supprimer", + "deleteConfirm": { + "title": "Supprimer cette source ?", + "message": "Cette action est irréversible. Êtes-vous sûr de vouloir supprimer cette source ?", + "confirm": "Supprimer", + "cancel": "Annuler" + } +}, "NewSourcePage": { "title": "Ajouter une nouvelle source de référence", + "editTitle": "Modifier la source de référence", "author": "Auteur", "authorHelperText": "L'auteur est un champ obligatoire", "articleTitle": "Titre d'article", "articleTitleHelperText": "Le titre de l'article est un champ requis", - "citation": "Citation", + "journalTitle": "Titre du journal", + "journalTitleHelperText": "Le titre du journal est un champ requis", + "citation": " DOI Citation", "citationHelperText": "Le titre de l'article est un champ requis", "year": "Année", "yearHelperText": "L'année est un champ obligatoire", @@ -627,7 +649,9 @@ "vectorData": "Données vectorielles", "buttons": { "submit": "Soumettre", - "reset": "Réinitialiser" + "reset": "Réinitialiser", + "update": "Mettre à jour", + "back": "Retour à la liste des sources" } }, "AdminPage": { @@ -837,7 +861,9 @@ "datasetLogsLoadError": "Quelque chose s'est mal passé lors des journaux de jeu de données récupérés. ", "reuploadRequestError": "Quelque chose a mal tourné avec la demande de re-téléchargement de l'ensemble de données. ", "reuploadError": "Quelque chose a mal tourné avec le re-téléchargement de l'ensemble de données. ", - "deleteError": "Erreur lors de la suppression de l'ensemble de données" + "deleteError": "Erreur lors de la suppression de l'ensemble de données", + "updateError": "Erreur inconnue lors de la mise à jour de la référence. Veuillez réessayer.", + "updateSuccess": "Référence {id} mise à jour avec succès" } }, "UploadedModel": { @@ -928,13 +954,15 @@ "approveError": "Quelque chose a mal tourné avec l'approbation de l'ensemble de données. " } }, - "Source": { - "createSuccess": "Référence créée avec id {id}", - "errors": { - "createError": "Erreur inconnue dans la création de nouvelles références. ", - "duplicateSource": "Référence avec le titre {article_title} existe déjà" - } - }, + "Source": { + "createSuccess": "Référence créée avec id {id}", + "updateSuccess": "Référence {id} mise à jour avec succès", + "errors": { + "createError": "Erreur inconnue dans la création de nouvelles références. ", + "duplicateSource": "Référence avec le titre {article_title} existe déjà", + "updateError": "Erreur inconnue lors de la mise à jour de la référence. Veuillez réessayer." + } +}, "SpeciesInformation": { "updateSuccess": "Informations sur les espèces mises à jour avec id {id}", "createSuccess": "Informations sur les nouvelles espèces créées avec id {id}", @@ -1012,4 +1040,4 @@ } } } -} +} \ No newline at end of file diff --git a/src/UI/public/messages/pt.json b/src/UI/public/messages/pt.json index bfb814def..8a27e6dd1 100644 --- a/src/UI/public/messages/pt.json +++ b/src/UI/public/messages/pt.json @@ -3,26 +3,24 @@ "home": "Crie o próximo aplicativo" }, "MenuItems": { - "map": "Mapa", - "upload": "Carregar", - "news": "Notícias", - "about": "Sobre", - "more": "Mais", - "catalogue":"catálogo", - "help": "Ajuda", - "login": "Conecte-se", - "species": "Lista de espécies", - "source": "Lista de origem", - "addSource": "Adicione fonte", - "datasets": "Conjuntos de dados", - "countryCatalogue": "Catálogo de Países", - "editPointData": "Editar dados do ponto", - "models": "Modelos", - "communication": "Comunicação", - "doi": "Doi", - "admin": "Admin", - "translations": "Traduções" - }, + "Data": "Dados", + "upload": "Carregar", + "news": "Notícias", + "about": "Sobre", + "more": "Mais", + "help": "Ajuda", + "login": "Entrar", + "species": "Lista de espécies", + "source": "Lista de fontes", + "addSource": "Adicionar fonte", + "datasets": "Conjuntos de dados", + "editPointData": "Editar dados de pontos", + "models": "Modelos", + "communication": "Comunicação", + "doi": "DOI", + "admin": "Administrador", + "translations": "Traduções" +}, "HomePage": { "list1": "Comece editando", "list2": "Salve e veja suas mudanças instantaneamente" @@ -564,58 +562,81 @@ }, "SpeciesPage": { "title": "Lista de espécies", - "confirmDeleteTitle": "Confirme excluir", - "confirmDeleteMessage": "Tem certeza de que deseja excluir informações sobre esta espécie? ", + "confirmDeleteTitle": "Confirmar exclusão", + "confirmDeleteMessage": "Tem certeza de que deseja excluir estas informações da espécie? Esta ação não pode ser desfeita.", "buttons": { - "create": "Crie novas espécies", - "edit": "Item de edição", + "create": "Criar nova espécie", + "edit": "Editar item", "deleteItem": "Excluir item", - "more": "Veja mais detalhes", + "more": "Ver mais detalhes", "cancel": "Cancelar", - "delete": "Excluir" + "delete": "Excluir", + "preview": "Visualizar", + "download": "Baixar", + "downloadFull": "Baixar imagem completa", + "close": "Fechar", + "back": "Voltar à lista de espécies" }, - "speciesInformationEditor": { - "create": "Crie informações sobre espécies", - "edit": "Atualize as informações das espécies", - "name": "Nome", - "nameHelperText": "Nome não pode estar vazio", - "shortDescription": "Breve descrição", - "shortDescriptionHelperText": "Breve descrição não pode estar vazia", - "fullDescription": "Descrição completa", - "image": "Imagem da espécie", - "uploadImageFile": "Faça o upload do arquivo de imagem da espécie", - "uploadImageFileHelperText": "Os arquivos devem ser menores que {maxSize} kb", - "distributionMapImage": "Imagem do mapa de distribuição", - "buttons": { - "create": "Criar", - "update": "Atualizar" - } + "tooltips": { + "zoomOut": "Diminuir zoom", + "zoomIn": "Aumentar zoom", + "resetZoom": "Redefinir zoom", + "closePreview": "Fechar visualização" + }, + "speciesInformationEditor": { + "create": "Criar informações da espécie", + "edit": "Editar informações da espécie", + "name": "Nome", + "nameHelperText": "O nome não pode estar vazio", + "shortDescription": "Descrição curta", + "shortDescriptionHelperText": "A descrição curta não pode estar vazia", + "fullDescription": "Descrição completa", + "image": "Imagem da espécie", + "uploadImageFile": "Carregar imagem da espécie", + "uploadImageFileHelperText": "Os arquivos devem ser menores que {maxSize} KB", + "distributionMapImage": "Mapa de distribuição", + "citation": "Citações", + "buttons": { + "create": "Criar", + "update": "Atualizar" + } } }, "SourcesPage": { - "title": "Lista de origem", - "grid": { - "id": "EU IA", - "author": "Autor", - "title": "Título", - "journalTitle": "Título do diário", - "year": "Ano", - "published": "Publicado", - "vectorData": "Dados vetoriais" - }, - "filters": { - "errorMsg": "Por favor, insira o intervalo (por exemplo, 100-200)", - "idFilter": "Filtro por id (por exemplo, 100-200)", - "titleFilter": "Filtro por título" - } + "title": "Lista de fontes", + "filters": { + "titleFilter": "Filtrar por título", + "fieldTitle": "Título", + "fieldAuthor": "Autor", + "fieldJournalTitle": "Título do periódico", + "searchPlaceholder": "Pesquisar..." }, + "grid": { + "author": "Autor", + "title": "Título", + "journalTitle": "Título do periódico", + "year": "Ano", + "actions": "Ações" + }, + "edit": "Editar", + "delete": "Excluir", + "deleteConfirm": { + "title": "Excluir esta fonte?", + "message": "Esta ação não pode ser desfeita. Tem certeza de que deseja excluir esta fonte?", + "confirm": "Excluir", + "cancel": "Cancelar" + } +}, "NewSourcePage": { "title": "Adicione uma nova fonte de referência", + "editTitle": "Editar fonte de referência", "author": "Autor", "authorHelperText": "Autor é um campo necessário", "articleTitle": "Título do artigo", "articleTitleHelperText": "O título do artigo é um campo necessário", - "citation": "Citação", + "journalTitle": "Título do periódico", + "journalTitleHelperText": "Título do periódico é um campo necessário", + "citation": " DOI Citação", "citationHelperText": "O título do artigo é um campo necessário", "year": "Ano", "yearHelperText": "Ano é um campo necessário", @@ -625,7 +646,10 @@ "vectorData": "Dados vetoriais", "buttons": { "submit": "Enviar", - "reset": "Reiniciar" + "reset": "Reiniciar", + "update": "Atualizar", + "back": "Voltar à lista de fontes" + } }, "AdminPage": { @@ -835,7 +859,9 @@ "datasetLogsLoadError": "Algo deu errado ao recuperar logs do conjunto de dados. ", "reuploadRequestError": "Algo deu errado em solicitar novamente o conjunto de dados. ", "reuploadError": "Algo deu errado com o conjunto de dados novamente. ", - "deleteError": "Erro excluindo o conjunto de dados" + "deleteError": "Erro excluindo o conjunto de dados", + "updateError": "Erro desconhecido ao atualizar a referência. Tente novamente.", + "updateSuccess": "Referência {id} atualizada com sucesso" } }, "UploadedModel": { @@ -927,12 +953,14 @@ } }, "Source": { - "createSuccess": "Referência criada com id {id}", - "errors": { - "createError": "Erro desconhecido na criação de uma nova referência. ", - "duplicateSource": "Referência com o título {artigo_title} já existe" - } - }, + "createSuccess": "Referência criada com id {id}", + "updateSuccess": "Referência {id} atualizada com sucesso", + "errors": { + "createError": "Erro desconhecido na criação de uma nova referência. ", + "duplicateSource": "Referência com o título {article_title} já existe", + "updateError": "Erro desconhecido ao atualizar a referência. Tente novamente." + } +}, "SpeciesInformation": { "updateSuccess": "Informações sobre espécies atualizadas com id {id}", "createSuccess": "Informações de novas espécies criadas com id {id}", @@ -1009,4 +1037,4 @@ } } } -} +} \ No newline at end of file diff --git a/src/UI/state/source/actions/deleteSource.ts b/src/UI/state/source/actions/deleteSource.ts new file mode 100644 index 000000000..32f755ef6 --- /dev/null +++ b/src/UI/state/source/actions/deleteSource.ts @@ -0,0 +1,31 @@ +import { createAsyncThunk } from '@reduxjs/toolkit'; +import { toast } from 'react-toastify'; +import { fetchGraphQlDataAuthenticated } from '../../../api/api'; +import { deleteSourceQuery } from '../../../api/queries'; +import { AppState } from '../../store'; +import { getTranslation } from '../../../utils/localization'; + +export const deleteSource = createAsyncThunk( + 'source/deleteSource', + async (num_id: number, { getState }) => { + const query = deleteSourceQuery(num_id); + const token = (getState() as AppState).auth.token; + const result = await fetchGraphQlDataAuthenticated(query, token); + + if (result.errors) { + toast.error( + await getTranslation('ReduxActions.Source.errors.deleteError') + // 'Unknown error in deleting reference. Please try again.' + ); + return false; + } else if (result.data) { + toast.success( + await getTranslation('ReduxActions.Source.deleteSuccess', { + id: num_id, + }) + // `Reference ${num_id} deleted successfully` + ); + return true; + } + } +); diff --git a/src/UI/state/source/actions/getSourceById.ts b/src/UI/state/source/actions/getSourceById.ts new file mode 100644 index 000000000..d519da764 --- /dev/null +++ b/src/UI/state/source/actions/getSourceById.ts @@ -0,0 +1,14 @@ +import { createAsyncThunk } from '@reduxjs/toolkit'; +import { fetchGraphQlData } from '../../../api/api'; +import { referenceQuery } from '../../../api/queries'; + +export const getSourceById = createAsyncThunk( + 'source/getSourceById', + async (num_id: number) => { + const result = await fetchGraphQlData( + referenceQuery(0, 1, 'num_id', 'ASC', num_id, num_id, '') + ); + const items = result.data.allReferenceData.items; + return items.length > 0 ? items[0] : null; + } +); diff --git a/src/UI/state/source/actions/getSourceInfo.ts b/src/UI/state/source/actions/getSourceInfo.ts index 8a7814c3c..8fb9499b3 100644 --- a/src/UI/state/source/actions/getSourceInfo.ts +++ b/src/UI/state/source/actions/getSourceInfo.ts @@ -6,9 +6,16 @@ import { AppState } from '../../store'; export const getSourceInfo = createAsyncThunk( 'source/getSourceInfo', async (_, { getState }) => { - const { page, rowsPerPage, orderBy, order, startId, endId, textFilter } = ( - getState() as AppState - ).source.source_table_options; + const { + page, + rowsPerPage, + orderBy, + order, + startId, + endId, + textFilter, + filterField, + } = (getState() as AppState).source.source_table_options; const skip = page * rowsPerPage; const sourceInfo = await fetchGraphQlData( referenceQuery( @@ -18,9 +25,10 @@ export const getSourceInfo = createAsyncThunk( order.toLocaleUpperCase(), startId, endId, - textFilter + textFilter, + filterField ) ); return sourceInfo.data.allReferenceData; } -); +); \ No newline at end of file diff --git a/src/UI/state/source/actions/updateSource.ts b/src/UI/state/source/actions/updateSource.ts new file mode 100644 index 000000000..788ba0714 --- /dev/null +++ b/src/UI/state/source/actions/updateSource.ts @@ -0,0 +1,31 @@ +import { createAsyncThunk } from '@reduxjs/toolkit'; +import { toast } from 'react-toastify'; +import { fetchGraphQlDataAuthenticated } from '../../../api/api'; +import { updateSourceQuery } from '../../../api/queries'; +import { NewSource } from '../../../components/sources/source_form'; +import { AppState } from '../../store'; +import { getTranslation } from '../../../utils/localization'; + +export const updateSource = createAsyncThunk( + 'source/updateSource', + async (source: NewSource, { getState }) => { + const query = updateSourceQuery(source); + const token = (getState() as AppState).auth.token; + const result = await fetchGraphQlDataAuthenticated(query, token); + if (result.errors) { + toast.error( + await getTranslation('ReduxActions.Source.errors.updateError') + // 'Unknown error updating reference. Please try again.' + ); + return false; + } else if (result.data) { + toast.success( + await getTranslation('ReduxActions.Source.updateSuccess', { + id: result.data.updateReference.num_id, + }) + // `Reference ${num_id} updated successfully` + ); + return true; + } + } +); diff --git a/src/UI/state/source/sourceSlice.ts b/src/UI/state/source/sourceSlice.ts index ddb4ef391..230811b41 100644 --- a/src/UI/state/source/sourceSlice.ts +++ b/src/UI/state/source/sourceSlice.ts @@ -1,6 +1,8 @@ import { createSlice, PayloadAction } from '@reduxjs/toolkit'; import { FilterSort } from '../state.types'; import { getSourceInfo } from './actions/getSourceInfo'; +import { deleteSource } from './actions/deleteSource'; +import { getSourceById } from './actions/getSourceById'; export interface Source { [index: string]: any; @@ -9,10 +11,7 @@ export interface Source { journal_title: string; citation: string; year: number; - //published: boolean; report_type: string; - //v_data: boolean; - //num_id: number; } export interface SourceState { @@ -21,6 +20,9 @@ export interface SourceState { total: number; }; source_info_status: string; + source_delete_status: string; + source_edit: Source | null; + source_edit_status: string; source_table_options: FilterSort; } @@ -30,6 +32,9 @@ export const initialState: SourceState = { total: 0, }, source_info_status: '', + source_delete_status: '', + source_edit: null, + source_edit_status: '', source_table_options: { page: 0, rowsPerPage: 10, @@ -38,6 +43,8 @@ export const initialState: SourceState = { startId: 0, endId: null, textFilter: '', + + filterField: 'article_title', }, }; @@ -68,18 +75,44 @@ export const sourceSlice = createSlice({ changeFilterText(state, action: PayloadAction) { state.source_table_options.textFilter = action.payload; }, + changeFilterField(state, action: PayloadAction) { + state.source_table_options.filterField = action.payload; + }, + clearSourceEdit(state) { + state.source_edit = null; + state.source_edit_status = ''; + }, }, extraReducers: (builder) => { builder .addCase(getSourceInfo.pending, (state) => { state.source_info_status = 'loading'; }) - .addCase(getSourceInfo.rejected, (state, action) => { + .addCase(getSourceInfo.rejected, (state) => { state.source_info_status = 'error'; }) .addCase(getSourceInfo.fulfilled, (state, action) => { state.source_info = action.payload; state.source_info_status = 'success'; + }) + .addCase(deleteSource.pending, (state) => { + state.source_delete_status = 'loading'; + }) + .addCase(deleteSource.rejected, (state) => { + state.source_delete_status = 'error'; + }) + .addCase(deleteSource.fulfilled, (state, action) => { + state.source_delete_status = action.payload ? 'success' : 'error'; + }) + .addCase(getSourceById.pending, (state) => { + state.source_edit_status = 'loading'; + }) + .addCase(getSourceById.rejected, (state) => { + state.source_edit_status = 'error'; + }) + .addCase(getSourceById.fulfilled, (state, action) => { + state.source_edit = action.payload; + state.source_edit_status = action.payload ? 'success' : 'error'; }); }, }); @@ -90,5 +123,7 @@ export const { changeSort, changeFilterId, changeFilterText, + changeFilterField, + clearSourceEdit, } = sourceSlice.actions; export default sourceSlice.reducer; diff --git a/src/UI/state/speciesInformation/actions/upsertSpeciesInfo.action.ts b/src/UI/state/speciesInformation/actions/upsertSpeciesInfo.action.ts index 87f2b3203..90578e7c1 100644 --- a/src/UI/state/speciesInformation/actions/upsertSpeciesInfo.action.ts +++ b/src/UI/state/speciesInformation/actions/upsertSpeciesInfo.action.ts @@ -20,6 +20,14 @@ import { toast } from 'react-toastify'; import { getAllSpecies } from './getAllSpecies'; import { getTranslation } from '../../../utils/localization'; +const safeDecodeURIComponent = (value: string): string => { + try { + return decodeURIComponent(value); + } catch { + return value; + } +}; + const sanitiseSpeciesInformation = ( speciesInformation: SpeciesInformation ): SpeciesInformation => { @@ -28,6 +36,8 @@ const sanitiseSpeciesInformation = ( name: encodeURIComponent(speciesInformation.name), shortDescription: encodeURIComponent(speciesInformation.shortDescription), description: encodeURIComponent(speciesInformation.description), + speciesImage: speciesInformation.speciesImage, + previewImage: speciesInformation.previewImage, citations: speciesInformation.citations.map((citation) => encodeURIComponent(citation) ), @@ -42,6 +52,8 @@ export const unsanitiseSpeciesInformation = ( name: decodeURIComponent(speciesInformation.name), shortDescription: decodeURIComponent(speciesInformation.shortDescription), description: decodeURIComponent(speciesInformation.description), + speciesImage: safeDecodeURIComponent(speciesInformation.speciesImage), + previewImage: safeDecodeURIComponent(speciesInformation.previewImage || ''), citations: speciesInformation.citations.map((citation) => decodeURIComponent(citation) ), @@ -50,7 +62,10 @@ export const unsanitiseSpeciesInformation = ( export const upsertSpeciesInformation = createAsyncThunk( 'speciesInformation/upsert', - async (speciesInformation: SpeciesInformation, { getState, dispatch }) => { + async ( + speciesInformation: SpeciesInformation, + { getState, dispatch, rejectWithValue } + ) => { dispatch(speciesInfoLoading(true)); try { const token = (getState() as AppState).auth.token; @@ -60,14 +75,27 @@ export const upsertSpeciesInformation = createAsyncThunk( ), token ); + + // 🔧 ADDED: GraphQL can return HTTP 200 with an `errors` array, or with + // `data` present but the specific field null. Axios won't throw for + // either case, so we have to check explicitly. + if (newSpecies.errors?.length) { + throw new Error( + newSpecies.errors[0]?.message || 'GraphQL mutation returned errors' + ); + } + if (!newSpecies.data?.createEditSpeciesInformation) { + throw new Error( + 'GraphQL mutation succeeded but returned no species information' + ); + } + if (speciesInformation.id) { toast.success( await getTranslation( 'ReduxActions.SpeciesInformation.updateSuccess', { id: newSpecies.data.createEditSpeciesInformation.id } ) - // 'Updated species information with id ' + - // newSpecies.data.createEditSpeciesInformation.id ); } else { toast.success( @@ -75,8 +103,6 @@ export const upsertSpeciesInformation = createAsyncThunk( 'ReduxActions.SpeciesInformation.createSuccess', { id: newSpecies.data.createEditSpeciesInformation.id } ) - // 'New species information created with id ' + - // newSpecies.data.createEditSpeciesInformation.id ); } dispatch( @@ -88,15 +114,20 @@ export const upsertSpeciesInformation = createAsyncThunk( citations: [], }) ); + dispatch(speciesInfoLoading(false)); + return newSpecies.data.createEditSpeciesInformation; // 🔧 ADDED: gives the component a fulfilled payload to check against } catch (e) { + // 🔧 CHANGED: log the real error so it shows up in the browser console + // instead of only ever seeing the generic toast. + console.error('upsertSpeciesInformation failed:', e); toast.error( await getTranslation( 'ReduxActions.SpeciesInformation.errors.updateError' ) - //'Unable to update species information' ); + dispatch(speciesInfoLoading(false)); + return rejectWithValue(e instanceof Error ? e.message : String(e)); // 🔧 ADDED } - dispatch(speciesInfoLoading(false)); } ); @@ -117,9 +148,7 @@ export const deleteSpeciesInformation = createAsyncThunk( 'ReduxActions.SpeciesInformation.deleteSuccess', { id: id } ) - //`Deleted species information with id ${id}` ); - // Optionally refresh the species list or handle state cleanup dispatch(getAllSpecies()); } else { toast.error( @@ -127,7 +156,6 @@ export const deleteSpeciesInformation = createAsyncThunk( 'ReduxActions.SpeciesInformation.errors.deleteError', { id: id } ) - //`Failed to delete species information with id ${id}` ); } } catch (e) { @@ -135,7 +163,6 @@ export const deleteSpeciesInformation = createAsyncThunk( await getTranslation( 'ReduxActions.SpeciesInformation.errors.deleteGeneralError' ) - //'Unable to delete species information' ); } dispatch(speciesInfoLoading(false)); @@ -153,6 +180,7 @@ export const getSpeciesInformation = createAsyncThunk( unsanitiseSpeciesInformation(res.data.speciesInformationById) ) ); + dispatch( setCurrentInfoDetails( unsanitiseSpeciesInformation(res.data.speciesInformationById) diff --git a/src/UI/state/state.types.ts b/src/UI/state/state.types.ts index 6fec8f3eb..817c95520 100644 --- a/src/UI/state/state.types.ts +++ b/src/UI/state/state.types.ts @@ -32,11 +32,15 @@ export type VectorAtlasFilters = { }; export type SpeciesInformation = { - id: string | undefined; + id?: string; name: string; shortDescription: string; description: string; + // On list-page records this will be undefined, since the list query + // doesn't fetch it — that's expected, not a bug. speciesImage: string; + previewImage: string; + distributionMapUrl?: string; citations: string[]; link: string; }; @@ -49,6 +53,7 @@ export type FilterSort = { startId: number | null; endId: number | null; textFilter: string; + filterField: string; }; export type News = { diff --git a/src/UI/styles/globals.css b/src/UI/styles/globals.css index 4f2580089..90d4c3278 100644 --- a/src/UI/styles/globals.css +++ b/src/UI/styles/globals.css @@ -21,6 +21,7 @@ a { input[type='range'] { -webkit-appearance: none; + appearance: none; height: 7px; background: #ebbd40; border: #038543; @@ -55,8 +56,12 @@ input[type='range']::-webkit-slider-thumb { border: 1px solid #ff1744; } +.editor-text-underline { + text-decoration: underline; +} + .ql-editor { height: 150px !important; max-height: 150px; overflow: auto; -} +} \ No newline at end of file diff --git a/src/UI/utils/speciesImageUtils.ts b/src/UI/utils/speciesImageUtils.ts new file mode 100644 index 000000000..944e7a4f6 --- /dev/null +++ b/src/UI/utils/speciesImageUtils.ts @@ -0,0 +1,147 @@ +// Turns whatever is stored (a full URL, a bare filename, a data URL, +// raw base64 image data, etc.) into something an can use +// directly. +export function resolveSpeciesImageUrl( + imageRef?: string, + mimeType: string = 'image/jpeg' +): string { + if (!imageRef) { + return ''; + } + + if ( + imageRef.startsWith('data:') || + imageRef.startsWith('http://') || + imageRef.startsWith('https://') || + imageRef.startsWith('/vector-api/') + ) { + return imageRef; + } + + if (imageRef.startsWith('/')) { + return imageRef; + } + + // Raw base64 image data (no prefix at all) — this is what + // speciesImage/previewImage now contain since images are stored + // directly in the database rather than referenced by filename/URL. + // Wrap it as a data URI rather than treating it as a path fragment, + // which previously produced an unusably long URL. + const isLikelyBase64 = /^[A-Za-z0-9+/]+={0,2}$/.test(imageRef.slice(0, 100)); + if (isLikelyBase64) { + return `data:${mimeType};base64,${imageRef}`; + } + + return `/vector-api/species-information/images/${imageRef}`; +} + +export function getSpeciesImageDownloadUrl( + imageRef?: string, + speciesName?: string, + mimeType: string = 'image/jpeg' +): string { + const resolvedUrl = resolveSpeciesImageUrl(imageRef, mimeType); + if (!resolvedUrl) { + return ''; + } + + if (resolvedUrl.startsWith('data:')) { + return resolvedUrl; + } + + if (resolvedUrl.includes('/species-information/images/')) { + const separator = resolvedUrl.includes('?') ? '&' : '?'; + return `${resolvedUrl}${separator}download=true`; + } + + return resolvedUrl; +} + +// Case A: we already have the image ref/URL in hand (the EDIT page, +// since it loads the full record including speciesImage). Downloads +// it directly — no extra network round trip needed to find the file. +export async function downloadSpeciesImage( + imageRef?: string, + speciesName?: string, + mimeType: string = 'image/jpeg' +): Promise { + const resolvedUrl = resolveSpeciesImageUrl(imageRef, mimeType); + if (!resolvedUrl) { + return; + } + + const fileName = `${(speciesName || 'species').replace( + /\s+/g, + '_' + )}_image.png`; + + if (resolvedUrl.startsWith('data:')) { + const link = document.createElement('a'); + link.href = resolvedUrl; + link.download = fileName; + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); + return; + } + + try { + const downloadUrl = getSpeciesImageDownloadUrl(imageRef, speciesName, mimeType); + const response = await fetch(downloadUrl); + + if (!response.ok) { + throw new Error(`Failed to fetch image: ${response.status}`); + } + + const blob = await response.blob(); + const objectUrl = URL.createObjectURL(blob); + const link = document.createElement('a'); + link.href = objectUrl; + link.download = fileName; + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); + URL.revokeObjectURL(objectUrl); + } catch (error: unknown) { + const message = error instanceof Error ? error.message : String(error); + console.error('Download failed:', error); + alert(`Failed to download image: ${message}`); + } +} + +// Case B: we only have the species id, not the image ref (the LIST +// page, since its query never fetches speciesImage). Asks the backend +// to look it up and streams back the actual image bytes directly. +export async function downloadSpeciesImageById( + speciesId: string, + speciesName?: string +): Promise { + const fileName = `${(speciesName || 'species').replace( + /\s+/g, + '_' + )}_image.jpg`; + + try { + const response = await fetch( + `/vector-api/species-information/${speciesId}/download-image` + ); + + if (!response.ok) { + throw new Error(`Failed to fetch image: ${response.status}`); + } + + const blob = await response.blob(); + const objectUrl = URL.createObjectURL(blob); + const link = document.createElement('a'); + link.href = objectUrl; + link.download = fileName; + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); + URL.revokeObjectURL(objectUrl); + } catch (error: unknown) { + const message = error instanceof Error ? error.message : String(error); + console.error('Download failed:', error); + alert(`Failed to download image: ${message}`); + } +} \ No newline at end of file diff --git a/src/package-lock.json b/src/package-lock.json index 2b6237cb8..0fcf2a8ef 100644 --- a/src/package-lock.json +++ b/src/package-lock.json @@ -6,7 +6,8 @@ "": { "dependencies": { "react-swipeable-views": "^0.14.0", - "react-swipeable-views-utils": "^0.14.0" + "react-swipeable-views-utils": "^0.14.0", + "sharp": "^0.35.3" }, "devDependencies": { "@types/react-swipeable-views": "^0.13.6", @@ -22,6 +23,564 @@ "regenerator-runtime": "^0.12.0" } }, + "node_modules/@emnapi/runtime": { + "version": "1.11.2", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.2.tgz", + "integrity": "sha512-kyOl3X0DuTiT1h2ft8r2fYO8JYtU9a9Xis/zBSiGArNaagCOWx90N1k2wxp18czFDH+OgcWGb5ZP/XMt3dcyPA==", + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@img/colour": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.1.0.tgz", + "integrity": "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@img/sharp-darwin-arm64": { + "version": "0.35.3", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.35.3.tgz", + "integrity": "sha512-RMnFX7YQsMoh7lWfcM4NEHHymBX/rLuKNPVM84XE9ONPcaSCDgE7CHIHpSgPcO2xcRthgBy1HfNO319mwhIAkg==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-arm64": "1.3.2" + } + }, + "node_modules/@img/sharp-darwin-x64": { + "version": "0.35.3", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.35.3.tgz", + "integrity": "sha512-Xo+5uFBtLN0BKqieTxiFzFPQAUlBbbH5iBKyRX/z1JrbnYsHTfKJnUfL8+p2TPXr1pXqao4eeL4Rl144uDpK9w==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-x64": "1.3.2" + } + }, + "node_modules/@img/sharp-freebsd-wasm32": { + "version": "0.35.3", + "resolved": "https://registry.npmjs.org/@img/sharp-freebsd-wasm32/-/sharp-freebsd-wasm32-0.35.3.tgz", + "integrity": "sha512-lUxcqWIj2wMQ9BrwNjngcr1gWUr5xgaGThBRqPPalIC2n67Cqj1uPh8NnA/ZhAg8hUbKl+kVHKwgUIwe6ZYPrg==", + "license": "Apache-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "dependencies": { + "@img/sharp-wasm32": "0.35.3" + }, + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-darwin-arm64": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.3.2.tgz", + "integrity": "sha512-9J6ypZFpQBj4YnePGoq/S38w6nz+vqg5WZLrLGY4YuSemdMq47GMLBPO42MzwdGwpg/agZ7xzZcFHa48xlywfg==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-darwin-x64": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.3.2.tgz", + "integrity": "sha512-m2pW1n6cns9VaubNwsZ+c3CRYjxNQWgJ5gPlnL1nbBcpkBvFm6SCFN5o0psFHI8w9n11NKhFkeEDns98tiqbEw==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.3.2.tgz", + "integrity": "sha512-1eMLzy92I4J6rmi4mAT8yC3HxOtniyGELlzGbNMLLeqe052ahFQ0h6LFq+lh5DsDIdYViIDst08abvSbcEdLXQ==", + "cpu": [ + "arm" + ], + "libc": [ + "glibc" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm64": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.3.2.tgz", + "integrity": "sha512-dqVSFynCox4C/J8kT16V7SIFAns0IjgLwkvYT7p8LQVmJ5OS5b6tI9IGflxTeuBS//zXeFIUbwt5dwxyZ17cnA==", + "cpu": [ + "arm64" + ], + "libc": [ + "glibc" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-ppc64": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.3.2.tgz", + "integrity": "sha512-3z0NHDxD6n5I9gc05U1eW1AyRm+Gznzq3naMrthPNqE6oYykcogW0l/jfpJdjYnuNl8R7yI9pNbE1XiUeyq0Aw==", + "cpu": [ + "ppc64" + ], + "libc": [ + "glibc" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-riscv64": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.3.2.tgz", + "integrity": "sha512-bsb4rI+NldGOsXuej2r8OdSS8+zXDVaCWxyWrcv6kneTOlgAHtZABRzBBCwdsPiD90J4myNJuHpg6kA20ImW/w==", + "cpu": [ + "riscv64" + ], + "libc": [ + "glibc" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-s390x": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.3.2.tgz", + "integrity": "sha512-/ABshyj8gCpyIrNXnHn4LorDJ0HHm1VhXPBlxZ8zAtfVPAaSafXPGn+sUSIRiwaSBy0mmFjSjiXI5mkcwdChKQ==", + "cpu": [ + "s390x" + ], + "libc": [ + "glibc" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-x64": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.3.2.tgz", + "integrity": "sha512-ITPEtgffGJ0S6G9dRyw/366tJQqFRcHWPHhC+Stpg3Z8AEMrDrTr2lhdz4f/Y/HMbRh//7Z5mBzEpVdi62Oc3w==", + "cpu": [ + "x64" + ], + "libc": [ + "glibc" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-arm64": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.3.2.tgz", + "integrity": "sha512-zE9EdiUzUmg5mDT5a1rk5fYJ6GWPloTwWBYDS14naqHsL+EaMpDj1AWnpLgh3u0YCORv2Tt50wrcrpYqkP97Kw==", + "cpu": [ + "arm64" + ], + "libc": [ + "musl" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-x64": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.3.2.tgz", + "integrity": "sha512-m0lrLiUt+lBYnCFr8qV/65yMR4E/c7/wf78I5eKTdkEakFAlZ9QlzEM3QIhhAwVeUhLAHLcCq7a7Vszq/oFNZQ==", + "cpu": [ + "x64" + ], + "libc": [ + "musl" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-linux-arm": { + "version": "0.35.3", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.35.3.tgz", + "integrity": "sha512-affVWCTLooy8TSxbDx2qkzuDeaWLNVBA+P//FNBirHsXpP2fuBhk5AuboYUnrDnzoXes8GFjpTx0SBFOCRg+FA==", + "cpu": [ + "arm" + ], + "libc": [ + "glibc" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm": "1.3.2" + } + }, + "node_modules/@img/sharp-linux-arm64": { + "version": "0.35.3", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.35.3.tgz", + "integrity": "sha512-QgKDspHPnrU+GQ55XPhGwyhC8acLVOOSyAvo1oVfFmrIXLkDNmGWzAfDZ4xK8oSA1qBQrALcHX0G5UZni/SuFQ==", + "cpu": [ + "arm64" + ], + "libc": [ + "glibc" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm64": "1.3.2" + } + }, + "node_modules/@img/sharp-linux-ppc64": { + "version": "0.35.3", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.35.3.tgz", + "integrity": "sha512-sMd8rDxmpLOwv/7N44klFjOD5DUO7FLdjiXDI0hoxYaf7Ar262dQIEkosE98bps+5HPLtp/EvNqeqQtOycP/IA==", + "cpu": [ + "ppc64" + ], + "libc": [ + "glibc" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-ppc64": "1.3.2" + } + }, + "node_modules/@img/sharp-linux-riscv64": { + "version": "0.35.3", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.35.3.tgz", + "integrity": "sha512-0Eob78yjlYPfL5vMNWAW55l3R9Y6BQS/gOfe0ZcP9mEz9ohhKSt4im1hayiknXgf8AWrFqMvJcKIdmLmEe7yeQ==", + "cpu": [ + "riscv64" + ], + "libc": [ + "glibc" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-riscv64": "1.3.2" + } + }, + "node_modules/@img/sharp-linux-s390x": { + "version": "0.35.3", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.35.3.tgz", + "integrity": "sha512-KgAxQ0DxpNOq1rG2t5cgTgShJFGSuU7XO45cqC+1NVOuZnP6tlgZRuSYOfNupGkHID0o3cJOsw4DVeJpMovcGw==", + "cpu": [ + "s390x" + ], + "libc": [ + "glibc" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-s390x": "1.3.2" + } + }, + "node_modules/@img/sharp-linux-x64": { + "version": "0.35.3", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.35.3.tgz", + "integrity": "sha512-8pqvxubL2PGdhlPy6GLqzDYMUjyRmKAwKHYKixpdJYBUK7PJ0C029XdsnpFIdgRZG68fZiGdHVWcKPvtiPB4cA==", + "cpu": [ + "x64" + ], + "libc": [ + "glibc" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-x64": "1.3.2" + } + }, + "node_modules/@img/sharp-linuxmusl-arm64": { + "version": "0.35.3", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.35.3.tgz", + "integrity": "sha512-Vz0iQjzzcSX3HCbfwFfCSG/9SCIqyO0mH2sXyiHaAYfBk0cRsCWXRyQYX0ovCK/PAQBbTzQ0dsPQHh5MAFL59w==", + "cpu": [ + "arm64" + ], + "libc": [ + "musl" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-arm64": "1.3.2" + } + }, + "node_modules/@img/sharp-linuxmusl-x64": { + "version": "0.35.3", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.35.3.tgz", + "integrity": "sha512-6O1NPKcDVj9QEdg7Hx549EX8U0rp6yXQERqru6yRN7fGBn32UvIRJUlWnk+8xDCiG76hXVBbX82NZ/ZKr0euIg==", + "cpu": [ + "x64" + ], + "libc": [ + "musl" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-x64": "1.3.2" + } + }, + "node_modules/@img/sharp-wasm32": { + "version": "0.35.3", + "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.35.3.tgz", + "integrity": "sha512-cZ0XkcYGpHZkqW6iCkqTcmUC0CD9DhD5d/qeZlZkfRBn6GnHniZXLUo5+9xw8Iv76YE6LQFN9YNBlKREcCG76w==", + "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", + "optional": true, + "dependencies": { + "@emnapi/runtime": "^1.11.1" + }, + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-webcontainers-wasm32": { + "version": "0.35.3", + "resolved": "https://registry.npmjs.org/@img/sharp-webcontainers-wasm32/-/sharp-webcontainers-wasm32-0.35.3.tgz", + "integrity": "sha512-2rnq7bX3NzeR2T4YWgz8qiG4h3TSdMe+vN1iQXpJleSJ3SM5zQ8Fy2SyyXAWlbxpEZ2Y+Z4u1BePgJEYbSy80Q==", + "cpu": [ + "wasm32" + ], + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "@img/sharp-wasm32": "0.35.3" + }, + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-arm64": { + "version": "0.35.3", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.35.3.tgz", + "integrity": "sha512-4bPwFdMbeC4JQ8L8LOyWp6nsHcboP5fxkp6iPOXz2Vg49R42TuMs2whkJ5OAP4/Ul035qOzy0AecOF9VOscn4w==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-ia32": { + "version": "0.35.3", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.35.3.tgz", + "integrity": "sha512-r53mXsBN6lFUDiST764SvgwUdHAqM4rPAiDzAmf4fLoB6X/rkfyTrLCg6+g17wJJiCmB3JYgHuUldCWUIRFSXw==", + "cpu": [ + "ia32" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-x64": { + "version": "0.35.3", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.35.3.tgz", + "integrity": "sha512-D4y1vNeZrIIJCN+uHaWVtH86B+aCrdMYYjicy9pXHvbGZeGYLLSd3wdVuC37FxVXlU1ARsk84eKWfWMXGYEqvA==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, "node_modules/@material-ui/types": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/@material-ui/types/-/types-4.1.1.tgz", @@ -71,6 +630,15 @@ "dev": true, "license": "MIT" }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -214,12 +782,80 @@ "integrity": "sha512-odxIc1/vDlo4iZcfXqRYFj0vpXFNoGdKMAUieAlFYO6m/nl5e9KR/beGf41z4a1FI+aQgtjhuaSlDxQ0hmkrHg==", "license": "MIT" }, + "node_modules/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/shallow-equal": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/shallow-equal/-/shallow-equal-1.2.1.tgz", "integrity": "sha512-S4vJDjHHMBaiZuT9NPb616CSmLf618jawtv3sufLl6ivK8WocjAo58cXwbRV1cgqxH0Qbv+iUt6m05eqEa2IRA==", "license": "MIT" }, + "node_modules/sharp": { + "version": "0.35.3", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.35.3.tgz", + "integrity": "sha512-ej0zVHuZGHCiABXcNxeYhpRnPNPAcvbG8RMdBAhDAxLKkCRVSpK3Iyu7qbqw3JMzoj0REeM6f3tJLtVwl0023Q==", + "license": "Apache-2.0", + "dependencies": { + "@img/colour": "^1.1.0", + "detect-libc": "^2.1.2", + "semver": "^7.8.5" + }, + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-darwin-arm64": "0.35.3", + "@img/sharp-darwin-x64": "0.35.3", + "@img/sharp-freebsd-wasm32": "0.35.3", + "@img/sharp-libvips-darwin-arm64": "1.3.2", + "@img/sharp-libvips-darwin-x64": "1.3.2", + "@img/sharp-libvips-linux-arm": "1.3.2", + "@img/sharp-libvips-linux-arm64": "1.3.2", + "@img/sharp-libvips-linux-ppc64": "1.3.2", + "@img/sharp-libvips-linux-riscv64": "1.3.2", + "@img/sharp-libvips-linux-s390x": "1.3.2", + "@img/sharp-libvips-linux-x64": "1.3.2", + "@img/sharp-libvips-linuxmusl-arm64": "1.3.2", + "@img/sharp-libvips-linuxmusl-x64": "1.3.2", + "@img/sharp-linux-arm": "0.35.3", + "@img/sharp-linux-arm64": "0.35.3", + "@img/sharp-linux-ppc64": "0.35.3", + "@img/sharp-linux-riscv64": "0.35.3", + "@img/sharp-linux-s390x": "0.35.3", + "@img/sharp-linux-x64": "0.35.3", + "@img/sharp-linuxmusl-arm64": "0.35.3", + "@img/sharp-linuxmusl-x64": "0.35.3", + "@img/sharp-webcontainers-wasm32": "0.35.3", + "@img/sharp-win32-arm64": "0.35.3", + "@img/sharp-win32-ia32": "0.35.3", + "@img/sharp-win32-x64": "0.35.3" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD", + "optional": true + }, "node_modules/warning": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/warning/-/warning-4.0.3.tgz", diff --git a/src/package.json b/src/package.json index 84d0b2cd8..eb2703b02 100644 --- a/src/package.json +++ b/src/package.json @@ -1,7 +1,8 @@ { "dependencies": { "react-swipeable-views": "^0.14.0", - "react-swipeable-views-utils": "^0.14.0" + "react-swipeable-views-utils": "^0.14.0", + "sharp": "^0.35.3" }, "devDependencies": { "@types/react-swipeable-views": "^0.13.6",