From 3b2feb0a312e964641e90a6e47c5cb25928d38ea Mon Sep 17 00:00:00 2001 From: Indiana Eunice Date: Fri, 5 Jun 2026 12:49:12 +0300 Subject: [PATCH 1/8] modified files on initial setup --- .dockerignore | 34 +++++++++++++++++++++++++ Dockerfile | 63 ++++++++++++++++++++++++++++++++++++++++++++++ README.Docker.md | 17 +++++++++++++ compose.yaml | 52 ++++++++++++++++++++++++++++++++++++++ src/API/Dockerfile | 5 ++-- 5 files changed, 169 insertions(+), 2 deletions(-) create mode 100644 .dockerignore create mode 100644 Dockerfile create mode 100644 README.Docker.md create mode 100644 compose.yaml 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/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 395add2fb..8fe68d90b 100644 --- a/src/API/Dockerfile +++ b/src/API/Dockerfile @@ -1,5 +1,5 @@ # Use Node.js 18 as the base image -FROM node:20.5.0 AS builder +FROM node:20.19.0 AS builder # Create app directory WORKDIR /usr/src/app @@ -13,7 +13,7 @@ COPY . . # Creates a "dist" folder with the production build RUN npm run build -FROM node:20.5.0 AS runner +FROM node:20.19.0 AS runner # Set up the working directory WORKDIR /usr/src/app @@ -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 From 5c4cb6b3ac8ab9785e935156ef104791fc020d19 Mon Sep 17 00:00:00 2001 From: IndianaEunice Date: Mon, 22 Jun 2026 10:08:29 +0300 Subject: [PATCH 2/8] changes made --- .gitignore | 3 ++ ...oaded-dataset-ingestion-tracking-fields.ts | 8 ++--- src/Docker/docker-compose.dev.yml | 14 ++------ src/TileServer/generateTiles.sh | 0 src/TileServer/installTools.sh | 0 src/UI/.envrc | 35 +++++++++++++++++++ src/UI/components/species/speciesList.tsx | 14 ++++---- .../speciesInformationEditor.tsx | 2 +- src/UI/pages/species/details.tsx | 3 ++ 9 files changed, 57 insertions(+), 22 deletions(-) mode change 100644 => 100755 src/TileServer/generateTiles.sh mode change 100644 => 100755 src/TileServer/installTools.sh create mode 100644 src/UI/.envrc 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/src/API/src/db/migrations/1778735811063-uploaded-dataset-ingestion-tracking-fields.ts b/src/API/src/db/migrations/1778735811063-uploaded-dataset-ingestion-tracking-fields.ts index 8abfd1799..aa9bc47f3 100644 --- a/src/API/src/db/migrations/1778735811063-uploaded-dataset-ingestion-tracking-fields.ts +++ b/src/API/src/db/migrations/1778735811063-uploaded-dataset-ingestion-tracking-fields.ts @@ -8,22 +8,22 @@ export class UploadedDatasetIngestionTrackingFields1778735811063 public async up(queryRunner: QueryRunner): Promise { await queryRunner.query(` ALTER TABLE "uploaded_dataset" - ADD "ingestion_status" character varying + ADD COLUMN IF NOT EXISTS "ingestion_status" character varying `); await queryRunner.query(` ALTER TABLE "uploaded_dataset" - ADD "ingestion_errors" text + ADD COLUMN IF NOT EXISTS "ingestion_errors" text `); await queryRunner.query(` ALTER TABLE "uploaded_dataset" - ADD "total_ingested_rows" integer DEFAULT '0' + ADD COLUMN IF NOT EXISTS "total_ingested_rows" integer DEFAULT '0' `); await queryRunner.query(` ALTER TABLE "uploaded_dataset" - ADD "ingestion_progress" double precision DEFAULT '0' + ADD COLUMN IF NOT EXISTS "ingestion_progress" double precision DEFAULT '0' `); // DO NOT TOUCH reference PRIMARY KEY 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/.envrc b/src/UI/.envrc new file mode 100644 index 000000000..e68f795bb --- /dev/null +++ b/src/UI/.envrc @@ -0,0 +1,35 @@ +export PGPORT=5432 +export POSTGRES_USER=postgres +export POSTGRES_PASSWORD=postgrespass +export POSTGRES_DB=mva +export POSTGRES_HOST="127.0.0.1" +export PORT=7000 +export AUTH0_ISSUER_URL='https://dev-326tk4zu.us.auth0.com/' +export AUTH0_AUDIENCE='https://www.vectoratlas.org' +export TOKEN_KEY='' +export AZURE_STORAGE_CONNECTION_STRING='DefaultEndpointsProtocol=http;AccountName=devstoreaccount1;AccountKey=Eby8vdM02xNOcqFlqUwJPLlmEtlCDXJ1OUzFT50uSRZ6IFsuFq2UVErCz4I6tq/K1SZFPTOtr/KBHBeksoGMGw==;BlobEndpoint=http://127.0.0.1:10000/devstoreaccount1;' +export EMAIL_PASSWORD='' +export AUTH0_CLIENT_SECRET = $AUTH0_CLIENT_SECRET +export AUTH0_CLIENT_ID = 'sQPoZzmH4QaAHVEJrDaK3pPeHG0SmCtr' + +export MAILERPASSWORD='' +export BASE_URL='http://localhost:3002' +export DATA_VALIDATION_URL='/validate/data/' +export DATA_INGESTION_URL='/upload/data/' +export TEMP_DIR='temp/' +export EMAIL_HOST='' +export EMAIL_FROM='' +export EMAIL_PASSWORD= +export EMAIL_PORT= e.g 587 for Gmail +export EMAIL_SECURE=0 +export SENT_EMAIL_FOLDER=' e.g for Gmail it is [Gmail]/Sent Mail. For outlook it may be something else' +export IMAP_SERVER='' +export IMAP_PORT= e.g 993 +export DOI_RESOLVER_BASE_URL='/map?doi=' +export DATACITE_USER='' +export DATACITE_PASSWORD='' +export DATACITE_URL='https://api.test.datacite.org/dois' +export DATACITE_PREFIX='10.82746' +export DOI_PUBLISHER='International Centre of Insect Physiology and Ecology' +export AZURE_CONTAINER_NAME=vectoratlas +export MAX_UPLOAD_SIZE=100000 #onekb=1000 100MB diff --git a/src/UI/components/species/speciesList.tsx b/src/UI/components/species/speciesList.tsx index 445b33809..5a82b7e42 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 Image from 'next/image'; export default function SpeciesList(): JSX.Element { const t = useTranslations('SpeciesPage'); @@ -69,7 +70,7 @@ export default function SpeciesList(): JSX.Element { setOpenDialog(false); // Close the dialog without deleting }; - console.log('species', speciesList.items); + // console.log('species', speciesList.items); const panelStyle = { boxShadow: 3, @@ -136,10 +137,11 @@ export default function SpeciesList(): JSX.Element { justifyContent: 'center', }} > - - Mosquito Species #1 - + diff --git a/src/UI/components/speciesInformation/speciesInformationEditor.tsx b/src/UI/components/speciesInformation/speciesInformationEditor.tsx index 0f8bdaea1..377bfc068 100644 --- a/src/UI/components/speciesInformation/speciesInformationEditor.tsx +++ b/src/UI/components/speciesInformation/speciesInformationEditor.tsx @@ -170,7 +170,7 @@ const SpeciesInformationEditor = () => { }; const handleImageUpload = async (e: React.ChangeEvent) => { - if (e.target.files && e.target.files[0].size < UPLOAD_LIMIT_IN_KB * 1024) { + if (e.target.files && e.target.files[0].size < UPLOAD_LIMIT_IN_KB * 512) { const speciesImage = await toBase64(e.target.files[0]); setSpeciesImage(speciesImage); } else { diff --git a/src/UI/pages/species/details.tsx b/src/UI/pages/species/details.tsx index 82ed04d9f..e3018c4b2 100644 --- a/src/UI/pages/species/details.tsx +++ b/src/UI/pages/species/details.tsx @@ -1,3 +1,6 @@ + + + import { Box, Button, From 1607d751d9cc3ef1f03912915d1dc0cd19fd85f3 Mon Sep 17 00:00:00 2001 From: IndianaEunice Date: Mon, 13 Jul 2026 10:49:13 +0300 Subject: [PATCH 3/8] Add species image viewer and others --- .../speciesInformation.controller.ts | 48 ++++ .../speciesInformation.module.ts | 9 +- .../speciesInformation.resolver.ts | 2 +- .../speciesInformation.service.ts | 22 +- .../components/species/SpeciesImageViewer.tsx | 220 ++++++++++++++++++ src/UI/components/species/speciesList.tsx | 23 +- .../speciesInformationEditor.tsx | 67 ++++-- src/UI/pages/species/details.tsx | 19 +- .../actions/upsertSpeciesInfo.action.ts | 9 + src/UI/utils/speciesImageUtils.ts | 90 +++++++ 10 files changed, 459 insertions(+), 50 deletions(-) create mode 100644 src/API/src/db/speciesInformation/speciesInformation.controller.ts create mode 100644 src/UI/components/species/SpeciesImageViewer.tsx create mode 100644 src/UI/utils/speciesImageUtils.ts 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..762bd6105 --- /dev/null +++ b/src/API/src/db/speciesInformation/speciesInformation.controller.ts @@ -0,0 +1,48 @@ +import { + Controller, + Post, + Get, + Param, + Res, + UploadedFile, + UseInterceptors, + BadRequestException, + NotFoundException, +} from '@nestjs/common'; +import { FileInterceptor } from '@nestjs/platform-express'; +import { Response } from 'express'; +import { join } from 'path'; +import * as fs from 'fs'; +import { AzureBlobService } from '../azure-blob/azure-blob.service'; + +@Controller('species-information') +export class SpeciesInformationController { + constructor(private readonly azureBlobService: AzureBlobService) {} + + @Post('upload-image') + @UseInterceptors(FileInterceptor('file')) + async uploadImage(@UploadedFile() file: Express.Multer.File) { + if (!file) { + throw new BadRequestException('No file provided'); + } + + const result = await this.azureBlobService.upload(file, 'species-images'); + + return { + imageUrl: result.uploadedFileUrl, + fileName: result.filePath, + }; + } + + @Get('images/:filename') + async getImage(@Param('filename') filename: string, @Res() res: Response) { + const filePath = join(process.cwd(), 'public', 'species-images', filename); + console.log('Looking for image at:', filePath); // TEMP DEBUG LINE + + if (!fs.existsSync(filePath)) { + throw new NotFoundException('Image not found'); + } + + return res.sendFile(filePath); + } +} \ 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..2416cdc2a 100644 --- a/src/API/src/db/speciesInformation/speciesInformation.module.ts +++ b/src/API/src/db/speciesInformation/speciesInformation.module.ts @@ -4,9 +4,16 @@ import { SpeciesInformation } from './entities/speciesInformation.entity'; import { SpeciesInformationService } from './speciesInformation.service'; import { SpeciesInformationResolver } from './speciesInformation.resolver'; +import { AzureBlobService } from 'src/db/azure-blob/azure-blob.service'; + @Module({ imports: [TypeOrmModule.forFeature([SpeciesInformation])], - providers: [SpeciesInformationService, SpeciesInformationResolver], + controllers: [], + 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..7286e685a 100644 --- a/src/API/src/db/speciesInformation/speciesInformation.resolver.ts +++ b/src/API/src/db/speciesInformation/speciesInformation.resolver.ts @@ -89,4 +89,4 @@ export class SpeciesInformationResolver { ): 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..ce81978fd 100644 --- a/src/API/src/db/speciesInformation/speciesInformation.service.ts +++ b/src/API/src/db/speciesInformation/speciesInformation.service.ts @@ -24,13 +24,29 @@ export class SpeciesInformationService { }); } + async allSpeciesInformationPaginated( + page: number, + pageSize: number, + ): Promise<{ items: SpeciesInformation[]; total: number; hasMore: boolean }> { + const [items, total] = await this.speciesInformationRepository.findAndCount({ + order: { id: 'ASC' }, + skip: page * pageSize, + take: pageSize, + }); + + return { + items, + total, + hasMore: (page + 1) * pageSize < total, + }; + } + 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; } -} +} \ No newline at end of file diff --git a/src/UI/components/species/SpeciesImageViewer.tsx b/src/UI/components/species/SpeciesImageViewer.tsx new file mode 100644 index 000000000..d6743e9b9 --- /dev/null +++ b/src/UI/components/species/SpeciesImageViewer.tsx @@ -0,0 +1,220 @@ +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, + resolveSpeciesImageUrl, +} from '../../utils/speciesImageUtils'; + +const MIN_SCALE = 1; +const MAX_SCALE = 3; +const SCALE_STEP = 0.5; + +type SpeciesImageViewerProps = { + imageRef?: string; + alt: string; + speciesName?: string; + thumbnailWidth?: number | string; + showActions?: boolean; +}; + +export default function SpeciesImageViewer({ + imageRef, + alt, + speciesName, + thumbnailWidth = 300, + showActions = true, +}: SpeciesImageViewerProps) { + 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 }); + + const imageUrl = resolveSpeciesImageUrl(imageRef); + if (!imageUrl) { + return null; + } + + const resetView = () => { + setScale(1); + setPosition({ x: 0, y: 0 }); + }; + + const handleClose = () => { + resetView(); + setOpen(false); + }; + + const handleDownload = async () => { + await downloadSpeciesImage(imageRef, 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 5a82b7e42..35d2df775 100644 --- a/src/UI/components/species/speciesList.tsx +++ b/src/UI/components/species/speciesList.tsx @@ -20,7 +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 Image from 'next/image'; +import SpeciesImageViewer from './SpeciesImageViewer'; export default function SpeciesList(): JSX.Element { const t = useTranslations('SpeciesPage'); @@ -137,19 +137,12 @@ export default function SpeciesList(): JSX.Element { justifyContent: 'center', }} > - - Mosquito Species #1 - + @@ -230,4 +223,4 @@ export default function SpeciesList(): JSX.Element { ); -} +} \ No newline at end of file diff --git a/src/UI/components/speciesInformation/speciesInformationEditor.tsx b/src/UI/components/speciesInformation/speciesInformationEditor.tsx index 377bfc068..90eadc237 100644 --- a/src/UI/components/speciesInformation/speciesInformationEditor.tsx +++ b/src/UI/components/speciesInformation/speciesInformationEditor.tsx @@ -9,6 +9,7 @@ import { Box, Autocomplete, } from '@mui/material'; +import ArrowBackIcon from '@mui/icons-material/ArrowBack'; import { ShortTextEditor } from '../shared/textEditor/shortTextEditor'; import { useAppDispatch, useAppSelector } from '../../state/hooks'; import { @@ -19,13 +20,14 @@ import { SpeciesInformation } from '../../state/state.types'; import { toast } from 'react-toastify'; import UploadIcon from '@mui/icons-material/Upload'; import DeleteIcon from '@mui/icons-material/Delete'; -import { toBase64 } from '../shared/imageTools'; import { useTranslations } from 'next-intl'; import { getSourceInfo } from '../../state/source/actions/getSourceInfo'; import { speciesList } from '../../state/map/utils/countrySpeciesLists'; import { TextEditor } from '../shared/textEditor/RichTextEditor'; +import { uploadSpeciesImageAuthenticated } from '../../api/api'; +import SpeciesImageViewer from '../species/SpeciesImageViewer'; -const UPLOAD_LIMIT_IN_KB = 512; +const UPLOAD_LIMIT_IN_KB = 1024; type Subsection = { title: string; @@ -44,9 +46,11 @@ const SpeciesInformationEditor = () => { const [species, setSpecies] = useState(''); const [link, setLink] = useState(''); const [subsections, setSubsections] = useState([]); + const [uploadingImage, setUploadingImage] = 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 @@ -145,12 +149,16 @@ 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: '' }]); }; @@ -170,10 +178,12 @@ const SpeciesInformationEditor = () => { }; const handleImageUpload = async (e: React.ChangeEvent) => { - if (e.target.files && e.target.files[0].size < UPLOAD_LIMIT_IN_KB * 512) { - const speciesImage = await toBase64(e.target.files[0]); - setSpeciesImage(speciesImage); - } else { + const file = e.target.files?.[0]; + if (!file) { + return; + } + + if (file.size >= UPLOAD_LIMIT_IN_KB * 1024) { const error = t('speciesInformationEditor.uploadImageFileHelperText', { maxSize: UPLOAD_LIMIT_IN_KB, }); @@ -181,6 +191,19 @@ const SpeciesInformationEditor = () => { toast.error(error, { autoClose: 5000, }); + return; + } + + try { + setUploadingImage(true); + const result = await uploadSpeciesImageAuthenticated(file, token); + setSpeciesImage(result.imageUrl); + 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 +216,16 @@ const SpeciesInformationEditor = () => { return (
+ + {id ? t('speciesInformationEditor.edit') @@ -283,13 +316,15 @@ const SpeciesInformationEditor = () => { sx={{ display: 'flex', flexDirection: 'column', alignItems: 'center' }} > + + ))} @@ -155,6 +210,31 @@ export default function SourceTable(): JSX.Element { onRowsPerPageChange={handleChangeRowsPerPage} /> )} + + + {t('deleteConfirm.title')} + + {t('deleteConfirm.message')} + + + + + + ); } diff --git a/src/UI/components/species/SpeciesImageViewer.tsx b/src/UI/components/species/SpeciesImageViewer.tsx index d6743e9b9..8497a68b7 100644 --- a/src/UI/components/species/SpeciesImageViewer.tsx +++ b/src/UI/components/species/SpeciesImageViewer.tsx @@ -16,15 +16,28 @@ 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 = { - imageRef?: string; + // 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; @@ -32,19 +45,23 @@ type SpeciesImageViewerProps = { }; export default function SpeciesImageViewer({ - imageRef, + 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 }); - const imageUrl = resolveSpeciesImageUrl(imageRef); + const imageUrl = resolveSpeciesImageUrl(previewRef); if (!imageUrl) { return null; } @@ -60,7 +77,13 @@ export default function SpeciesImageViewer({ }; const handleDownload = async () => { - await downloadSpeciesImage(imageRef, speciesName); + // Prefer the direct ref when we have it — it's a single request. + // Otherwise fall back to the id-based lookup. + if (downloadRef) { + await downloadSpeciesImage(downloadRef, speciesName); + } else if (speciesId) { + await downloadSpeciesImageById(speciesId, speciesName); + } }; const zoomIn = () => setScale((s) => Math.min(MAX_SCALE, s + SCALE_STEP)); @@ -113,22 +136,24 @@ export default function SpeciesImageViewer({ }} /> {showActions && ( - + )} @@ -145,26 +170,29 @@ export default function SpeciesImageViewer({ > {speciesName || alt} - + - + = MAX_SCALE}> - + - + @@ -198,23 +226,25 @@ export default function SpeciesImageViewer({ maxHeight: '100%', objectFit: 'contain', transform: `translate(${position.x}px, ${position.y}px) scale(${scale})`, - transition: isDragging.current ? 'none' : 'transform 0.15s ease-out', + 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 35d2df775..8b462790a 100644 --- a/src/UI/components/species/speciesList.tsx +++ b/src/UI/components/species/speciesList.tsx @@ -1,4 +1,4 @@ -import { useEffect, useState ,useCallback} from 'react'; +import { useEffect, useState, useCallback } from 'react'; import { Button, Grid, @@ -27,9 +27,7 @@ export default function SpeciesList(): JSX.Element { const router = useRouter(); const speciesList = useAppSelector((state) => state.speciesInfo.speciesDict); - const isEditor = useAppSelector((state) => - state.auth.roles.includes('editor') - ); + const isEditor = 'true'; const dispatch = useDispatch(); const loadingSpeciesInformation = useAppSelector( (s) => s.speciesInfo.loading @@ -39,10 +37,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)); @@ -55,23 +53,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, @@ -138,7 +134,11 @@ export default function SpeciesList(): JSX.Element { }} > handleDeleteClick(row.id as string)} // Handle delete click + onClick={() => handleDeleteClick(row.id as string)} className="DeleteButton" > {t('buttons.deleteItem')} @@ -197,7 +197,6 @@ export default function SpeciesList(): JSX.Element { ))} - {/* Dialog Box */}
); -} \ No newline at end of file +} diff --git a/src/UI/components/speciesInformation/speciesInformationEditor.tsx b/src/UI/components/speciesInformation/speciesInformationEditor.tsx index 90eadc237..14170be87 100644 --- a/src/UI/components/speciesInformation/speciesInformationEditor.tsx +++ b/src/UI/components/speciesInformation/speciesInformationEditor.tsx @@ -24,8 +24,8 @@ import { useTranslations } from 'next-intl'; import { getSourceInfo } from '../../state/source/actions/getSourceInfo'; import { speciesList } from '../../state/map/utils/countrySpeciesLists'; import { TextEditor } from '../shared/textEditor/RichTextEditor'; -import { uploadSpeciesImageAuthenticated } from '../../api/api'; import SpeciesImageViewer from '../species/SpeciesImageViewer'; +import { uploadSpeciesImageAuthenticated } from '../../api/api'; const UPLOAD_LIMIT_IN_KB = 1024; @@ -37,9 +37,9 @@ type Subsection = { const SpeciesInformationEditor = () => { 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([]); @@ -47,8 +47,8 @@ const SpeciesInformationEditor = () => { 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(); @@ -60,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)); @@ -104,12 +111,12 @@ 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), which + // was previously blocking the whole form from populating if that + // request hadn't resolved yet. useEffect(() => { - console.log('All citations:', allCitations); - }, [allCitations]); - - useEffect(() => { - if (currentSpeciesInformation && sources.items?.length > 0) { + if (currentSpeciesInformation) { setName(currentSpeciesInformation.name); setShortDescription(currentSpeciesInformation.shortDescription); try { @@ -120,8 +127,15 @@ const SpeciesInformationEditor = () => { setSubsections([]); } setSpeciesImage(currentSpeciesInformation.speciesImage); + setPreviewImage(currentSpeciesInformation.previewImage); setLink(currentSpeciesInformation.link); + } + }, [currentSpeciesInformation]); + // Citation matching genuinely does depend 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 = @@ -140,7 +154,6 @@ const SpeciesInformationEditor = () => { } }, [currentSpeciesInformation, sources.items]); - // Early return AFTER all hooks if (loadingSpeciesInformation) { return (
@@ -149,12 +162,8 @@ const SpeciesInformationEditor = () => { ); } - const citationIds = selectedCitations.map((c) => c.num_id); - console.log('Selected citations:', selectedCitations); - console.log('Mapped citation IDs:', citationIds); - const handleBack = () => { router.push('/species'); }; @@ -183,6 +192,11 @@ const SpeciesInformationEditor = () => { return; } + 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, @@ -196,8 +210,12 @@ const SpeciesInformationEditor = () => { try { setUploadingImage(true); - const result = await uploadSpeciesImageAuthenticated(file, token); + const result = await uploadSpeciesImageAuthenticated( + file, + token?.toString() + ); setSpeciesImage(result.imageUrl); + setPreviewImage(result.previewImageUrl); toast.success('Image uploaded successfully!'); } catch (error) { toast.error('Failed to upload image. Please try again.'); @@ -223,7 +241,7 @@ const SpeciesInformationEditor = () => { sx={{ mb: 2 }} > - Back to Species List + {t('buttons.back')} @@ -328,7 +346,7 @@ const SpeciesInformationEditor = () => { @@ -337,9 +355,10 @@ const SpeciesInformationEditor = () => { maxSize: UPLOAD_LIMIT_IN_KB, })} - {speciesImage && ( + {previewImage && ( @@ -347,12 +366,12 @@ const SpeciesInformationEditor = () => { - Citation + {t('speciesInformationEditor.citation')} setCitationSearch(e.target.value)} sx={{ mb: 2 }} @@ -466,12 +485,17 @@ const SpeciesInformationEditor = () => { @@ -480,4 +504,4 @@ const SpeciesInformationEditor = () => { ); }; -export default SpeciesInformationEditor; \ No newline at end of file +export default SpeciesInformationEditor; 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..f630d6477 --- /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/species/details.tsx b/src/UI/pages/species/details.tsx index 22cd5d558..c0ff01c47 100644 --- a/src/UI/pages/species/details.tsx +++ b/src/UI/pages/species/details.tsx @@ -1,230 +1,229 @@ - +import { useState, useRef, WheelEvent, MouseEvent } from 'react'; import { Box, Button, - Container, - Grid, - TextField, - Typography, + Dialog, + DialogActions, + DialogContent, + DialogTitle, + IconButton, + Tooltip, } from '@mui/material'; -import { useMediaQuery, useTheme } from '@mui/material'; -import { useRouter } from 'next/router'; -import { useEffect, useState } from 'react'; -import { useAppDispatch, useAppSelector } from '../../state/hooks'; -import { getSpeciesInformation } from '../../state/speciesInformation/actions/upsertSpeciesInfo.action'; -import ArrowBackIcon from '@mui/icons-material/ArrowBack'; -import ReactMarkdown from 'react-markdown'; -import SectionPanel from '../../components/layout/sectionPanel'; -import SpeciesImageViewer from '../../components/species/SpeciesImageViewer'; -import { getMessages } from '../../utils/localization'; -import { GetServerSidePropsContext } from 'next'; -import { getSourceInfo } from '../../state/source/actions/getSourceInfo'; -import { getOccurrenceData } from '../../state/map/actions/getOccurrenceData'; -export default function SpeciesDetails() { - const router = useRouter(); - const dispatch = useAppDispatch(); - const urlId = router.query.id as string | undefined; - const speciesDetails: any = useAppSelector( - (state) => state.speciesInfo.currentInfoDetails - ); - const loadingSpeciesInformation = useAppSelector( - (s) => s.speciesInfo.loading - ); - useEffect(() => { - if (urlId) { - dispatch(getSpeciesInformation(urlId)); - } - }, [urlId, dispatch]); - const theme = useTheme(); - const isMatch = useMediaQuery(theme.breakpoints.down('sm')); - if (loadingSpeciesInformation) { - return
loading
; +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, + resolveSpeciesImageUrl, +} from '../../utils/speciesImageUtils'; + +const MIN_SCALE = 1; +const MAX_SCALE = 3; +const SCALE_STEP = 0.5; + +type SpeciesImageViewerProps = { + // CHANGED: split the old single `imageRef` into two props — + // previewRef drives what's displayed, downloadRef drives what's downloaded + previewRef?: string; + downloadRef?: string; + alt: string; + speciesName?: string; + thumbnailWidth?: number | string; + showActions?: boolean; +}; + +export default function SpeciesImageViewer({ + previewRef, + downloadRef, + alt, + speciesName, + thumbnailWidth = 300, + showActions = true, +}: SpeciesImageViewerProps) { + 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 }); + + // CHANGED: image shown on screen now resolves from previewRef (resized version) + const imageUrl = resolveSpeciesImageUrl(previewRef); + if (!imageUrl) { + return null; } - const handleBack = () => { - router.push('/species'); + + const resetView = () => { + setScale(1); + setPosition({ x: 0, y: 0 }); + }; + + const handleClose = () => { + resetView(); + setOpen(false); + }; + + const handleDownload = async () => { + // CHANGED: download now uses downloadRef (original full-size speciesImage) + // instead of the same ref used for preview + await downloadSpeciesImage(downloadRef, 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 speciesDetailsSectionHeader = { - backgroundColor: 'rgba(0,0,0,0.05)', - borderRadius: 2, - padding: 2, + + const handleMouseDown = (e: MouseEvent) => { + if (scale === 1) return; + isDragging.current = true; + lastPos.current = { x: e.clientX, y: e.clientY }; }; - const speciesDetailsSection = { - display: 'flex', - padding: 5, - justifyContent: 'space-around', + + 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 speciesDescriptionSection = { - padding: 5, - justifyContent: 'space-around', + + const stopDragging = () => { + isDragging.current = false; }; + return ( -
- - - - - - {speciesDetails?.link ? ( + <> + + {alt} { + (e.target as HTMLImageElement).style.display = 'none'; + }} + /> + {showActions && ( + + - ) : ( - - - - No species data on map - - - )} - - -
- + )} + + + + - - - - Details - - - - - - - {speciesDetails?.shortDescription} - - - - - - Description - - - {(() => { - let sections = []; - try { - sections = JSON.parse(speciesDetails?.description || '[]'); - } catch (e) { - console.error( - 'Invalid JSON in speciesDetails.description:', - e - ); - return ( - - Invalid description format - - ); - } - return Array.isArray(sections) ? ( - sections.map((section, index) => ( - - - {section.title} - - - {section.content} - - - )) - ) : ( - - Description is not a valid array - - ); - })()} - - {/* - Distribution Map - - */} - - - -
-
+ {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', + }} + /> + + + + + + + ); } -export async function getServerSideProps(context: GetServerSidePropsContext) { - return await getMessages(context); -} 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 8498efb5e..d0cee6690 100644 --- a/src/UI/public/messages/en.json +++ b/src/UI/public/messages/en.json @@ -512,7 +512,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." }, @@ -523,59 +523,81 @@ "email": "vectoratlas@icipe.org" } }, + "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", + "create": "Create new species", + "edit": "Edit item", + "deleteItem": "Delete item", + "more": "View more details", "cancel": "Cancel", - "delete": "Delete" - }, - "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" - } + "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" + } } }, + + "SourcesPage": { "title": "Source List", + "filters": { + "titleFilter": "Filter by Title" + }, "grid": { - "id": "ID", "author": "Author", "title": "Title", "journalTitle": "Journal Title", "year": "Year", - "published": "Published", - "vectorData": "Vector Data" + "actions": "Actions" }, - "filters": { - "errorMsg": "Please enter range (e.g. 100-200)", - "idFilter": "Filter by id (e.g. 100-200)", - "titleFilter": "Filter by Title" + "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" } }, "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", + "journalTitle": "Journal Title", + "journalTitleHelperText": "Journal Title is a required field", "citation": "Citation", "citationHelperText": "Article Title is a required field", "year": "Year", @@ -585,10 +607,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", @@ -801,7 +826,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": { @@ -941,4 +968,4 @@ "path": "Path" } } -} +} \ No newline at end of file diff --git a/src/UI/public/messages/fr.json b/src/UI/public/messages/fr.json index a0f88c9d4..d47955f68 100644 --- a/src/UI/public/messages/fr.json +++ b/src/UI/public/messages/fr.json @@ -2,25 +2,25 @@ "PageTitles": { "home": "Créer une application suivante" }, - "MenuItems": { - "map": "Carte", - "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" - }, + "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" @@ -517,59 +517,80 @@ "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" - }, - "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" - } + "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" + }, + "tooltips": { + "zoomOut": "Zoom arrière", + "zoomIn": "Zoom avant", + "resetZoom": "Réinitialiser le zoom", + "closePreview": "Fermer l'aperçu" + }, + "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 de sources", - "grid": { - "id": "IDENTIFIANT", - "author": "Auteur", - "title": "Titre", - "journalTitle": "Titre de la revue", - "year": "Année", - "published": "Publié", - "vectorData": "Données vectorielles" - }, - "filters": { - "errorMsg": "Veuillez saisir la gamme (par exemple 100-200)", - "idFilter": "Filtre par ID (par exemple 100-200)", - "titleFilter": "Filtre par titre" - } + +"SourcesPage": { + "title": "Liste des sources", + "filters": { + "titleFilter": "Filtrer par titre" }, + "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", + "journalTitle": "Titre du journal", + "journalTitleHelperText": "Le titre du journal est un champ requis", "citation": "Citation", "citationHelperText": "Le titre de l'article est un champ requis", "year": "Année", @@ -580,7 +601,9 @@ "vectorData": "Données vectorielles", "buttons": { "submit": "Soumettre", - "reset": "Réinitialiser" + "reset": "Réinitialiser", + "update": "Mettre à jour", + "back": "Retour à la liste des sources" } }, "AdminPage": { @@ -788,7 +811,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,4 +953,4 @@ "path": "Chemin" } } -} +} \ No newline at end of file diff --git a/src/UI/public/messages/pt.json b/src/UI/public/messages/pt.json index c82e738d7..c3a8e80f4 100644 --- a/src/UI/public/messages/pt.json +++ b/src/UI/public/messages/pt.json @@ -3,24 +3,24 @@ "home": "Crie o próximo aplicativo" }, "MenuItems": { - "map": "Mapa", - "upload": "Carregar", - "news": "Notícias", - "about": "Sobre", - "more": "Mais", - "help": "Ajuda", - "login": "Conecte-se", - "species": "Lista de espécies", - "source": "Lista de origem", - "addSource": "Adicione fonte", - "datasets": "Conjuntos de dados", - "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" @@ -519,57 +519,76 @@ }, "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" - }, - "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" - } + "delete": "Excluir", + "preview": "Visualizar", + "download": "Baixar", + "downloadFull": "Baixar imagem completa", + "close": "Fechar", + "back": "Voltar à lista de espécies" + }, + "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" }, + "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", + "journalTitle": "Título do periódico", + "journalTitleHelperText": "Título do periódico é um campo necessário", "citation": "Citação", "citationHelperText": "O título do artigo é um campo necessário", "year": "Ano", @@ -580,7 +599,10 @@ "vectorData": "Dados vetoriais", "buttons": { "submit": "Enviar", - "reset": "Reiniciar" + "reset": "Reiniciar", + "update": "Atualizar", + "back": "Voltar à lista de fontes" + } }, "AdminPage": { @@ -788,7 +810,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": { @@ -928,4 +952,4 @@ "path": "Caminho" } } -} +} \ 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/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..39e997548 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; @@ -21,6 +23,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 +35,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, @@ -68,18 +76,41 @@ export const sourceSlice = createSlice({ changeFilterText(state, action: PayloadAction) { state.source_table_options.textFilter = 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 +121,6 @@ export const { changeSort, changeFilterId, changeFilterText, + 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 e22ddd0dc..90578e7c1 100644 --- a/src/UI/state/speciesInformation/actions/upsertSpeciesInfo.action.ts +++ b/src/UI/state/speciesInformation/actions/upsertSpeciesInfo.action.ts @@ -36,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) ), @@ -51,6 +53,7 @@ export const unsanitiseSpeciesInformation = ( shortDescription: decodeURIComponent(speciesInformation.shortDescription), description: decodeURIComponent(speciesInformation.description), speciesImage: safeDecodeURIComponent(speciesInformation.speciesImage), + previewImage: safeDecodeURIComponent(speciesInformation.previewImage || ''), citations: speciesInformation.citations.map((citation) => decodeURIComponent(citation) ), @@ -59,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; @@ -69,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( @@ -84,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( @@ -97,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)); } ); @@ -126,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( @@ -136,7 +156,6 @@ export const deleteSpeciesInformation = createAsyncThunk( 'ReduxActions.SpeciesInformation.errors.deleteError', { id: id } ) - //`Failed to delete species information with id ${id}` ); } } catch (e) { @@ -144,7 +163,6 @@ export const deleteSpeciesInformation = createAsyncThunk( await getTranslation( 'ReduxActions.SpeciesInformation.errors.deleteGeneralError' ) - //'Unable to delete species information' ); } dispatch(speciesInfoLoading(false)); @@ -162,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 5f07f61ee..cfa8e3140 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; }; diff --git a/src/UI/utils/speciesImageUtils.ts b/src/UI/utils/speciesImageUtils.ts index 900852796..11c749516 100644 --- a/src/UI/utils/speciesImageUtils.ts +++ b/src/UI/utils/speciesImageUtils.ts @@ -1,9 +1,10 @@ +// Turns whatever is stored (a full URL, a bare filename, a data URL, +// etc.) into something an can use directly. export function resolveSpeciesImageUrl(imageRef?: string): string { if (!imageRef) { return ''; } - if ( imageRef.startsWith('data:') || imageRef.startsWith('http://') || @@ -13,12 +14,10 @@ export function resolveSpeciesImageUrl(imageRef?: string): string { return imageRef; } - if (imageRef.startsWith('/')) { return imageRef; } - return `/vector-api/species-information/images/${imageRef}`; } @@ -43,6 +42,9 @@ export function getSpeciesImageDownloadUrl( 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 @@ -52,9 +54,11 @@ export async function downloadSpeciesImage( return; } - const fileName = `${(speciesName || 'species').replace(/\s+/g, '_')}_image.png`; + const fileName = `${(speciesName || 'species').replace( + /\s+/g, + '_' + )}_image.png`; - // Handle data URLs (base64 images) if (resolvedUrl.startsWith('data:')) { const link = document.createElement('a'); link.href = resolvedUrl; @@ -87,4 +91,42 @@ export async function downloadSpeciesImage( console.error('Download failed:', error); alert(`Failed to download image: ${message}`); } -} \ No newline at end of file +} + +// 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 redirect to the real file; fetch() follows that +// redirect automatically and hands us the actual image bytes. +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}`); + } +} 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", From 0b3725c40851aa86a7fb28497cda442c560cd4b0 Mon Sep 17 00:00:00 2001 From: IndianaEunice Date: Thu, 30 Jul 2026 14:43:07 +0300 Subject: [PATCH 5/8] feat"species and sourcelis fix" --- src/API/src/db/shared/reference.resolver.ts | 39 +- src/API/src/db/shared/reference.service.ts | 39 +- .../entities/speciesInformation.entity.ts | 21 +- .../speciesInformation.controller.ts | 86 +--- .../speciesInformation.resolver.ts | 36 +- src/UI/api/queries.ts | 5 +- .../shared/textEditor/RichTextEditor.tsx | 100 +++-- .../shared/textEditor/ToolbarPlugin.tsx | 131 ++++-- src/UI/components/sources/source_filters.tsx | 59 ++- src/UI/components/sources/source_form.tsx | 66 ++- src/UI/components/sources/source_table.tsx | 107 ++--- .../components/species/SpeciesImageViewer.tsx | 11 +- src/UI/components/species/speciesList.tsx | 4 +- .../speciesInformationEditor.tsx | 31 +- src/UI/pages/edit_source.tsx | 1 + src/UI/pages/sources.tsx | 1 + src/UI/pages/species/details.tsx | 401 +++++++++--------- src/UI/public/messages/en.json | 62 +-- src/UI/public/messages/fr.json | 24 +- src/UI/public/messages/pt.json | 22 +- src/UI/state/source/actions/getSourceInfo.ts | 18 +- src/UI/state/source/sourceSlice.ts | 13 +- src/UI/state/state.types.ts | 1 + src/UI/utils/speciesImageUtils.ts | 35 +- 24 files changed, 756 insertions(+), 557 deletions(-) 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 6bd1bde34..ac8aaa7a4 100644 --- a/src/API/src/db/shared/reference.service.ts +++ b/src/API/src/db/shared/reference.service.ts @@ -1,8 +1,14 @@ -import { Injectable } from '@nestjs/common'; +import { Injectable, NotFoundException } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { Reference } from './entities/reference.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( @@ -14,6 +20,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(); } @@ -25,6 +35,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, @@ -33,11 +57,20 @@ 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})`; + + // 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)) { @@ -52,7 +85,7 @@ export class ReferenceService { } if (textFilter) { query = query.andWhere( - 'LOWER("reference"."article_title") LIKE :textFilter', + `LOWER("reference"."${safeFilterField}") LIKE :textFilter`, { textFilter: `%${textFilter.toLocaleLowerCase()}%`, }, @@ -67,4 +100,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 f32727217..d5ad64185 100644 --- a/src/API/src/db/speciesInformation/entities/speciesInformation.entity.ts +++ b/src/API/src/db/speciesInformation/entities/speciesInformation.entity.ts @@ -17,17 +17,20 @@ export class SpeciesInformation extends BaseEntity { @Field({ nullable: false }) description: string; - // Original, large JPEG. Only ever needed for downloading — - // never fetched by the list query. - @Column('varchar', { nullable: true }) - @Field({ nullable: true }) - 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. - @Column('varchar', { nullable: true }) - @Field({ nullable: true }) - previewImage: string; + // Same base64-via-resolver treatment as speciesImage above. + @Column('bytea', { nullable: true }) + previewImage: Buffer; @Column('varchar', { nullable: false }) @Field({ nullable: false }) @@ -40,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 index 94129cdc7..3e863850e 100644 --- a/src/API/src/db/speciesInformation/speciesInformation.controller.ts +++ b/src/API/src/db/speciesInformation/speciesInformation.controller.ts @@ -11,9 +11,6 @@ import { } from '@nestjs/common'; import { FileInterceptor } from '@nestjs/platform-express'; import { Response } from 'express'; -import { join } from 'path'; -import * as fs from 'fs'; -import { AzureBlobService } from '../azure-blob/azure-blob.service'; import { SpeciesInformationService } from './speciesInformation.service'; // CHANGED: TypeScript kept resolving sharp's type declarations as @@ -28,15 +25,16 @@ const sharp = require('sharp'); @Controller('species-information') export class SpeciesInformationController { constructor( - private readonly azureBlobService: AzureBlobService, private readonly speciesInformationService: SpeciesInformationService, ) {} - // Uploads a new species image. Two things happen here: - // 1. The original JPEG is uploaded as-is (this becomes speciesImage). - // 2. A smaller WebP copy is generated on the server and uploaded too - // (this becomes previewImage). The frontend never has to do any - // image conversion itself. + // 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) { @@ -50,13 +48,7 @@ export class SpeciesInformationController { throw new BadRequestException('Only JPEG images are supported'); } - // Step 1: upload the original JPEG, unchanged - const originalResult = await this.azureBlobService.upload( - file, - 'species-images', - ); - - // Step 2: build a smaller WebP version in memory. + // 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 @@ -65,41 +57,15 @@ export class SpeciesInformationController { .webp({ quality: 80 }) .toBuffer(); - // Step 3: upload that WebP buffer as its own file, in its own - // container, so it's a completely separate object from the original - const previewFile: Express.Multer.File = { - ...file, - buffer: previewBuffer, - size: previewBuffer.length, - mimetype: 'image/webp', - originalname: file.originalname.replace(/\.jpe?g$/i, '.webp'), - }; - const previewResult = await this.azureBlobService.upload( - previewFile, - 'species-images-preview', - ); - - // Store only the bare filename (no directory prefix, no host/URL) so - // it matches the format the rest of the species_information table - // uses, and what the /images/:filename route expects when it looks - // the file up under public/species-images/. - const stripDirectory = (filePath: string) => filePath.split('/').pop(); - return { - imageUrl: stripDirectory(originalResult.filePath), - fileName: stripDirectory(originalResult.filePath), - previewImageUrl: stripDirectory(previewResult.filePath), - previewFileName: stripDirectory(previewResult.filePath), + imageBase64: file.buffer.toString('base64'), + previewBase64: previewBuffer.toString('base64'), }; } - // On-demand download route. The list page never loads speciesImage, - // so when someone clicks "Download" there, the frontend calls this - // route with just the species id. We look up speciesImage here and - // redirect the browser to the real file — either straight to Azure - // (if it's already a full URL) or through our own local image route - // (if it's just a bare filename sitting in the flat species-images - // folder, as with the manually-populated legacy rows). + // 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 = @@ -109,24 +75,10 @@ export class SpeciesInformationController { throw new NotFoundException('Image not found'); } - const isFullUrl = - species.speciesImage.startsWith('http://') || - species.speciesImage.startsWith('https://'); - - const redirectTarget = isFullUrl - ? species.speciesImage - : `/vector-api/species-information/images/${species.speciesImage}`; - - return res.redirect(redirectTarget); - } - - @Get('images/:filename') - async getImage(@Param('filename') filename: string, @Res() res: Response) { - const filePath = join(process.cwd(), 'public', 'species-images', filename); - - if (!fs.existsSync(filePath)) { - throw new NotFoundException('Image not found'); - } - return res.sendFile(filePath); + 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.resolver.ts b/src/API/src/db/speciesInformation/speciesInformation.resolver.ts index 4e4455333..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,10 +36,12 @@ 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; - // Lets the frontend send the WebP preview URL when creating/editing + // Base64-encoded WebP preview bytes (from uploadImage's previewBase64). @Field({ nullable: true }) previewImage: string; @@ -62,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) @@ -76,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 ?? [], }; @@ -93,4 +123,4 @@ export class SpeciesInformationResolver { ): Promise { return this.speciesInformationService.deleteSpeciesInformation(id); } -} +} \ No newline at end of file diff --git a/src/UI/api/queries.ts b/src/UI/api/queries.ts index 2564d97c2..9e0248be5 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 diff --git a/src/UI/components/shared/textEditor/RichTextEditor.tsx b/src/UI/components/shared/textEditor/RichTextEditor.tsx index 1c9d4fc95..01b1e67d0 100644 --- a/src/UI/components/shared/textEditor/RichTextEditor.tsx +++ b/src/UI/components/shared/textEditor/RichTextEditor.tsx @@ -19,7 +19,7 @@ import { $convertToMarkdownString, TRANSFORMERS, } from '@lexical/markdown'; -import { Divider, Typography } from '@mui/material'; +import { Typography } from '@mui/material'; import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'; import { mergeRegister } from '@lexical/utils'; @@ -35,10 +35,6 @@ const DescriptionWatcherPlugin = ({ useEffect(() => { return mergeRegister( editor.registerUpdateListener(({ editorState, tags }) => { - // Skip updates that were caused by our own programmatic sync - // (i.e. loading existingSource into the editor), so we don't - // immediately overwrite the freshly-loaded value with itself - // via a stale/empty read. if (tags.has(SYNC_TAG)) { return; } @@ -73,6 +69,8 @@ export const TextEditor = (props: { setDescription: (d: string) => void; error?: boolean; helperText?: string; + hideBlockTypeSelector?: boolean; + label?: string; }) => { const editorConfig = { namespace: 'speciesInformation', @@ -98,44 +96,68 @@ export const TextEditor = (props: { return ( -
- -
- - } - placeholder={''} - ErrorBoundary={LexicalErrorBoundary} - /> - - - - - - -
- {props.helperText ? ( - + {props.label ? ( + - {props.helperText} - + + {props.label} + + + ) : null} -
+
+
+ + } + placeholder={''} + ErrorBoundary={LexicalErrorBoundary} + /> + + + + + + +
+ {props.helperText ? ( + + {props.helperText} + + ) : null} +
- + +
); -}; +}; \ No newline at end of file diff --git a/src/UI/components/shared/textEditor/ToolbarPlugin.tsx b/src/UI/components/shared/textEditor/ToolbarPlugin.tsx index 1eab01292..da0e837d8 100644 --- a/src/UI/components/shared/textEditor/ToolbarPlugin.tsx +++ b/src/UI/components/shared/textEditor/ToolbarPlugin.tsx @@ -11,7 +11,6 @@ import { $getSelection, $isRangeSelection, FORMAT_TEXT_COMMAND, - LexicalCommand, SELECTION_CHANGE_COMMAND, TextFormatType, } from 'lexical'; @@ -37,7 +36,17 @@ import { useTranslations } from 'next-intl'; const LowPriority = 1; -const EditorToolbar = () => { +// showBlockTypeSelector controls whether the "Normal/Heading/List" dropdown +// renders. compact drops the MUI Toolbar wrapper and shrinks the B/I/U +// buttons so the whole toolbar can sit inline inside a , next to +// the field label, instead of as its own row inside the box. +const EditorToolbar = ({ + showBlockTypeSelector = true, + compact = false, +}: { + showBlockTypeSelector?: boolean; + compact?: boolean; +}) => { const t = useTranslations('RichTextEditor'); const [editor] = useLexicalComposerContext(); const [blockType, setBlockType] = useState('paragraph'); @@ -69,7 +78,6 @@ const EditorToolbar = () => { } } - // Update text format const formats = []; if (selection.hasFormat('bold')) { formats.push('bold'); @@ -137,51 +145,84 @@ const EditorToolbar = () => { editor.dispatchCommand(FORMAT_TEXT_COMMAND, format); }; + // Added: stops the browser's default mousedown behavior on each format + // button. Without this, clicking a button moves focus off the Lexical + // contentEditable *before* onClick fires, which collapses/clears the + // text selection. By the time toggleFormat() runs, there's nothing left + // to format, so the button toggles visually but bold/italic/underline + // never actually gets applied to the text. + const preventFocusLoss = (e: React.MouseEvent) => { + e.preventDefault(); + }; + + const blockTypeSelector = showBlockTypeSelector ? ( + + + + ) : null; + + const formatButtons = ( + + + + + + + + + + + + ); + + if (compact) { + return ( + + {blockTypeSelector} + {formatButtons} + + ); + } + return ( - - - - - - - - - - - - - - + {blockTypeSelector} + {formatButtons} ); }; -export default EditorToolbar; +export default EditorToolbar; \ No newline at end of file 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 a0450cbf9..214707ffb 100644 --- a/src/UI/components/sources/source_form.tsx +++ b/src/UI/components/sources/source_form.tsx @@ -33,7 +33,7 @@ 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(), }) @@ -43,16 +43,13 @@ interface SourceFormProps { existingSource?: NewSource | null; } -// Default applied to brand-new sources so `report_type` is never saved as an -// empty string. Adjust this value to whatever your team's standard/most -// common report type actually is. 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, @@ -69,22 +66,23 @@ export default function SourceForm({ existingSource }: SourceFormProps) { useEffect(() => { if (existingSource) { - console.log('SourceForm received existingSource:', existingSource); reset(existingSource); setYear(new Date(existingSource.year, 0, 1)); } }, [existingSource, reset]); const onSubmit = async (data: NewSource) => { - console.log(data); if (isEditMode) { - const success = await dispatch(updateSource(data)); - if (success) { - // stays on the page with updated values - } + // updateSource already shows a success/error toast internally + await dispatch(updateSource(data)); } else { - const success = await dispatch(postNewSource(data)); - if (success) { + 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, @@ -115,13 +113,6 @@ export default function SourceForm({ existingSource }: SourceFormProps) {
- - {t('author')} - ( )} rules={{ required: 'Author required' }} @@ -143,13 +136,6 @@ export default function SourceForm({ existingSource }: SourceFormProps) {
- - {t('articleTitle')} - ( )} rules={{ required: 'Article Title required' }} @@ -171,13 +159,6 @@ export default function SourceForm({ existingSource }: SourceFormProps) {
- - {t('journalTitle')} - ( )} rules={{ required: 'Journal Title required' }} @@ -208,11 +191,11 @@ export default function SourceForm({ existingSource }: SourceFormProps) { }) => ( + /> )} rules={{ required: 'Citation required' }} /> @@ -268,6 +251,7 @@ export default function SourceForm({ existingSource }: SourceFormProps) { }) => ( + /> )} - rules={{ required: 'Report Type required' }} />

@@ -292,9 +274,9 @@ export default function SourceForm({ existingSource }: SourceFormProps) { control={ onChange(e.target.checked)} color="primary" size="medium" - {...register('published')} /> } label={t('published')} @@ -314,9 +296,9 @@ export default function SourceForm({ existingSource }: SourceFormProps) { control={ onChange(e.target.checked)} color="primary" size="medium" - {...register('v_data')} /> } label={t('vectorData')} @@ -342,4 +324,4 @@ export default function SourceForm({ existingSource }: SourceFormProps) { ); -} +} \ No newline at end of file diff --git a/src/UI/components/sources/source_table.tsx b/src/UI/components/sources/source_table.tsx index 41b99d49c..1ec7968ff 100644 --- a/src/UI/components/sources/source_table.tsx +++ b/src/UI/components/sources/source_table.tsx @@ -40,6 +40,11 @@ export default function SourceTable(): JSX.Element { const table_options = useAppSelector( (state) => state.source.source_table_options ); + // Same roles this page's edit route already requires (see edit_source.tsx: + // ) + const canEdit = useAppSelector((state) => + ['uploader', 'editor'].some((role) => state.auth.roles.includes(role)) + ); const dispatch = useDispatch(); @@ -141,9 +146,11 @@ export default function SourceTable(): JSX.Element { ))} - - {t('grid.actions')} - + {canEdit && ( + + {t('grid.actions')} + + )} @@ -159,26 +166,28 @@ export default function SourceTable(): JSX.Element { {row[header.id as keyof typeof row]} ))} - - - - + {canEdit && ( + + + + + )} ))} @@ -211,30 +220,32 @@ export default function SourceTable(): JSX.Element { /> )} - - {t('deleteConfirm.title')} - - {t('deleteConfirm.message')} - - - - - - + {canEdit && ( + + {t('deleteConfirm.title')} + + {t('deleteConfirm.message')} + + + + + + + )} ); -} +} \ No newline at end of file diff --git a/src/UI/components/species/SpeciesImageViewer.tsx b/src/UI/components/species/SpeciesImageViewer.tsx index 8497a68b7..6363a5a54 100644 --- a/src/UI/components/species/SpeciesImageViewer.tsx +++ b/src/UI/components/species/SpeciesImageViewer.tsx @@ -61,7 +61,10 @@ export default function SpeciesImageViewer({ const isDragging = useRef(false); const lastPos = useRef({ x: 0, y: 0 }); - const imageUrl = resolveSpeciesImageUrl(previewRef); + // 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; } @@ -78,9 +81,9 @@ export default function SpeciesImageViewer({ const handleDownload = async () => { // Prefer the direct ref when we have it — it's a single request. - // Otherwise fall back to the id-based lookup. + // downloadRef is the original full-size JPEG, not the WebP preview. if (downloadRef) { - await downloadSpeciesImage(downloadRef, speciesName); + await downloadSpeciesImage(downloadRef, speciesName, 'image/jpeg'); } else if (speciesId) { await downloadSpeciesImageById(speciesId, speciesName); } @@ -247,4 +250,4 @@ export default function SpeciesImageViewer({ ); -} +} \ No newline at end of file diff --git a/src/UI/components/species/speciesList.tsx b/src/UI/components/species/speciesList.tsx index 8b462790a..5bb5f7590 100644 --- a/src/UI/components/species/speciesList.tsx +++ b/src/UI/components/species/speciesList.tsx @@ -27,7 +27,9 @@ export default function SpeciesList(): JSX.Element { const router = useRouter(); const speciesList = useAppSelector((state) => state.speciesInfo.speciesDict); - const isEditor = 'true'; + const isEditor = useAppSelector((state) => + state.auth.roles.includes('editor') + ); const dispatch = useDispatch(); const loadingSpeciesInformation = useAppSelector( (s) => s.speciesInfo.loading diff --git a/src/UI/components/speciesInformation/speciesInformationEditor.tsx b/src/UI/components/speciesInformation/speciesInformationEditor.tsx index 14170be87..fb3adcc83 100644 --- a/src/UI/components/speciesInformation/speciesInformationEditor.tsx +++ b/src/UI/components/speciesInformation/speciesInformationEditor.tsx @@ -112,9 +112,7 @@ const SpeciesInformationEditor = () => { }, [dispatch]); // Populate the core species fields as soon as the record loads — - // this no longer waits on the citations list (sources.items), which - // was previously blocking the whole form from populating if that - // request hadn't resolved yet. + // this no longer waits on the citations list (sources.items). useEffect(() => { if (currentSpeciesInformation) { setName(currentSpeciesInformation.name); @@ -132,8 +130,8 @@ const SpeciesInformationEditor = () => { } }, [currentSpeciesInformation]); - // Citation matching genuinely does depend on the sources list being - // loaded, so it stays in its own effect, separate from the fields above. + // 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]; @@ -186,12 +184,20 @@ 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) => { 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; @@ -210,12 +216,9 @@ const SpeciesInformationEditor = () => { try { setUploadingImage(true); - const result = await uploadSpeciesImageAuthenticated( - file, - token?.toString() - ); - setSpeciesImage(result.imageUrl); - setPreviewImage(result.previewImageUrl); + 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.'); @@ -496,12 +499,12 @@ const SpeciesInformationEditor = () => { {saving ? '...' : id - ? t('speciesInformationEditor.buttons.update') - : t('speciesInformationEditor.buttons.create')} + ? t('speciesInformationEditor.buttons.update') + : t('speciesInformationEditor.buttons.create')}
); }; -export default SpeciesInformationEditor; +export default SpeciesInformationEditor; \ No newline at end of file diff --git a/src/UI/pages/edit_source.tsx b/src/UI/pages/edit_source.tsx index f630d6477..6d7639d2a 100644 --- a/src/UI/pages/edit_source.tsx +++ b/src/UI/pages/edit_source.tsx @@ -17,6 +17,7 @@ function EditSource(): JSX.Element { const router = useRouter(); const dispatch = useDispatch(); const t = useTranslations('NewSourcePage'); + const idParam = router.query.id as string | undefined; 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 c0ff01c47..8e32c324d 100644 --- a/src/UI/pages/species/details.tsx +++ b/src/UI/pages/species/details.tsx @@ -1,229 +1,212 @@ -import { useState, useRef, WheelEvent, MouseEvent } from 'react'; import { Box, Button, - Dialog, - DialogActions, - DialogContent, - DialogTitle, - IconButton, - Tooltip, + Container, + Grid, + TextField, + Typography, } 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, - resolveSpeciesImageUrl, -} from '../../utils/speciesImageUtils'; - -const MIN_SCALE = 1; -const MAX_SCALE = 3; -const SCALE_STEP = 0.5; - -type SpeciesImageViewerProps = { - // CHANGED: split the old single `imageRef` into two props — - // previewRef drives what's displayed, downloadRef drives what's downloaded - previewRef?: string; - downloadRef?: string; - alt: string; - speciesName?: string; - thumbnailWidth?: number | string; - showActions?: boolean; -}; +import { useMediaQuery, useTheme } from '@mui/material'; +import { useRouter } from 'next/router'; +import { useEffect, useState } from 'react'; +import { useAppDispatch, useAppSelector } from '../../state/hooks'; +import { getSpeciesInformation } from '../../state/speciesInformation/actions/upsertSpeciesInfo.action'; +import ArrowBackIcon from '@mui/icons-material/ArrowBack'; +import ReactMarkdown from 'react-markdown'; +import SectionPanel from '../../components/layout/sectionPanel'; +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 SpeciesImageViewer({ - previewRef, - downloadRef, - alt, - speciesName, - thumbnailWidth = 300, - showActions = true, -}: SpeciesImageViewerProps) { - 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 }); - - // CHANGED: image shown on screen now resolves from previewRef (resized version) - const imageUrl = resolveSpeciesImageUrl(previewRef); - if (!imageUrl) { - return null; +export default function SpeciesDetails() { + const router = useRouter(); + const dispatch = useAppDispatch(); + const urlId = router.query.id as string | undefined; + const speciesDetails: any = useAppSelector( + (state) => state.speciesInfo.currentInfoDetails + ); + const loadingSpeciesInformation = useAppSelector( + (s) => s.speciesInfo.loading + ); + useEffect(() => { + if (urlId) { + dispatch(getSpeciesInformation(urlId)); + } + }, [urlId, dispatch]); + const theme = useTheme(); + const isMatch = useMediaQuery(theme.breakpoints.down('sm')); + if (loadingSpeciesInformation) { + return
loading
; } - - const resetView = () => { - setScale(1); - setPosition({ x: 0, y: 0 }); - }; - - const handleClose = () => { - resetView(); - setOpen(false); + const handleBack = () => { + router.push('/species'); }; - - const handleDownload = async () => { - // CHANGED: download now uses downloadRef (original full-size speciesImage) - // instead of the same ref used for preview - await downloadSpeciesImage(downloadRef, speciesName); + const speciesDetailsSectionHeader = { + backgroundColor: 'rgba(0,0,0,0.05)', + borderRadius: 2, + padding: 2, }; - - 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 speciesDetailsSection = { + display: 'flex', + padding: 5, + justifyContent: 'space-around', }; - - const handleMouseDown = (e: MouseEvent) => { - if (scale === 1) return; - isDragging.current = true; - lastPos.current = { x: e.clientX, y: e.clientY }; + const speciesDescriptionSection = { + padding: 5, + justifyContent: 'space-around', }; - - 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 && ( - - +
+ + + + + + {speciesDetails?.link ? ( - - )} - - - - + + + No species data on map + + + )} + + +
+ - {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', - }} - /> - - - - - - - + + + + Details + + +
+ +
+ + + + {speciesDetails?.shortDescription} + + + +
+ + Description + + + {(() => { + let sections = []; + try { + sections = JSON.parse(speciesDetails?.description || '[]'); + } catch (e) { + console.error( + 'Invalid JSON in speciesDetails.description:', + e + ); + return ( + + Invalid description format + + ); + } + return Array.isArray(sections) ? ( + sections.map((section, index) => ( + + + {section.title} + + + {section.content} + + + )) + ) : ( + + Description is not a valid array + + ); + })()} + +
+
+
+
+
); } +export async function getServerSideProps(context: GetServerSidePropsContext) { + return await getMessages(context); +} \ No newline at end of file diff --git a/src/UI/public/messages/en.json b/src/UI/public/messages/en.json index d0cee6690..88a6cbcb0 100644 --- a/src/UI/public/messages/en.json +++ b/src/UI/public/messages/en.json @@ -568,27 +568,31 @@ }, - "SourcesPage": { - "title": "Source List", - "filters": { - "titleFilter": "Filter by Title" - }, - "grid": { - "author": "Author", - "title": "Title", - "journalTitle": "Journal Title", - "year": "Year", - "actions": "Actions" - }, - "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" - } - }, +"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", @@ -597,8 +601,9 @@ "articleTitle": "Article Title", "articleTitleHelperText": "Article Title is a required field", "journalTitle": "Journal Title", + "journalTitleHelperText": "Journal Title is a required field", - "citation": "Citation", + "citation": " DOI Citation", "citationHelperText": "Article Title is a required field", "year": "Year", "yearHelperText": "Year is a required field", @@ -920,11 +925,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 d47955f68..8c4da172e 100644 --- a/src/UI/public/messages/fr.json +++ b/src/UI/public/messages/fr.json @@ -564,7 +564,11 @@ "SourcesPage": { "title": "Liste des sources", "filters": { - "titleFilter": "Filtrer par titre" + "titleFilter": "Filtrer par titre", + "fieldTitle": "Titre", + "fieldAuthor": "Auteur", + "fieldJournalTitle": "Titre du journal", + "searchPlaceholder": "Rechercher..." }, "grid": { "author": "Auteur", @@ -591,7 +595,7 @@ "articleTitleHelperText": "Le titre de l'article est un champ requis", "journalTitle": "Titre du journal", "journalTitleHelperText": "Le titre du journal est un champ requis", - "citation": "Citation", + "citation": " DOI Citation", "citationHelperText": "Le titre de l'article est un champ requis", "year": "Année", "yearHelperText": "L'année est un champ obligatoire", @@ -904,13 +908,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}", diff --git a/src/UI/public/messages/pt.json b/src/UI/public/messages/pt.json index c3a8e80f4..972610ed0 100644 --- a/src/UI/public/messages/pt.json +++ b/src/UI/public/messages/pt.json @@ -562,7 +562,11 @@ "SourcesPage": { "title": "Lista de fontes", "filters": { - "titleFilter": "Filtrar por título" + "titleFilter": "Filtrar por título", + "fieldTitle": "Título", + "fieldAuthor": "Autor", + "fieldJournalTitle": "Título do periódico", + "searchPlaceholder": "Pesquisar..." }, "grid": { "author": "Autor", @@ -589,7 +593,7 @@ "articleTitleHelperText": "O título do artigo é um campo necessário", "journalTitle": "Título do periódico", "journalTitleHelperText": "Título do periódico é um campo necessário", - "citation": "Citação", + "citation": " DOI Citação", "citationHelperText": "O título do artigo é um campo necessário", "year": "Ano", "yearHelperText": "Ano é um campo necessário", @@ -904,12 +908,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}", 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/sourceSlice.ts b/src/UI/state/source/sourceSlice.ts index 39e997548..f7cd15e5c 100644 --- a/src/UI/state/source/sourceSlice.ts +++ b/src/UI/state/source/sourceSlice.ts @@ -11,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 { @@ -46,6 +43,10 @@ export const initialState: SourceState = { startId: 0, endId: null, textFilter: '', + // Which column the text filter searches against. Defaults to the + // previous hardcoded behavior (article_title) so nothing breaks for + // anyone not yet using the new field-picker dropdown. + filterField: 'article_title', }, }; @@ -76,6 +77,9 @@ 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 = ''; @@ -121,6 +125,7 @@ export const { changeSort, changeFilterId, changeFilterText, + changeFilterField, clearSourceEdit, } = sourceSlice.actions; -export default sourceSlice.reducer; +export default sourceSlice.reducer; \ No newline at end of file diff --git a/src/UI/state/state.types.ts b/src/UI/state/state.types.ts index cfa8e3140..bb8e762af 100644 --- a/src/UI/state/state.types.ts +++ b/src/UI/state/state.types.ts @@ -53,6 +53,7 @@ export type FilterSort = { startId: number | null; endId: number | null; textFilter: string; + filterField: string; }; export type News = { diff --git a/src/UI/utils/speciesImageUtils.ts b/src/UI/utils/speciesImageUtils.ts index 11c749516..944e7a4f6 100644 --- a/src/UI/utils/speciesImageUtils.ts +++ b/src/UI/utils/speciesImageUtils.ts @@ -1,6 +1,10 @@ // Turns whatever is stored (a full URL, a bare filename, a data URL, -// etc.) into something an can use directly. -export function resolveSpeciesImageUrl(imageRef?: string): string { +// raw base64 image data, etc.) into something an can use +// directly. +export function resolveSpeciesImageUrl( + imageRef?: string, + mimeType: string = 'image/jpeg' +): string { if (!imageRef) { return ''; } @@ -18,14 +22,25 @@ export function resolveSpeciesImageUrl(imageRef?: string): string { 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 + speciesName?: string, + mimeType: string = 'image/jpeg' ): string { - const resolvedUrl = resolveSpeciesImageUrl(imageRef); + const resolvedUrl = resolveSpeciesImageUrl(imageRef, mimeType); if (!resolvedUrl) { return ''; } @@ -47,9 +62,10 @@ export function getSpeciesImageDownloadUrl( // it directly — no extra network round trip needed to find the file. export async function downloadSpeciesImage( imageRef?: string, - speciesName?: string + speciesName?: string, + mimeType: string = 'image/jpeg' ): Promise { - const resolvedUrl = resolveSpeciesImageUrl(imageRef); + const resolvedUrl = resolveSpeciesImageUrl(imageRef, mimeType); if (!resolvedUrl) { return; } @@ -70,7 +86,7 @@ export async function downloadSpeciesImage( } try { - const downloadUrl = getSpeciesImageDownloadUrl(imageRef, speciesName); + const downloadUrl = getSpeciesImageDownloadUrl(imageRef, speciesName, mimeType); const response = await fetch(downloadUrl); if (!response.ok) { @@ -95,8 +111,7 @@ export async function downloadSpeciesImage( // 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 redirect to the real file; fetch() follows that -// redirect automatically and hands us the actual image bytes. +// to look it up and streams back the actual image bytes directly. export async function downloadSpeciesImageById( speciesId: string, speciesName?: string @@ -129,4 +144,4 @@ export async function downloadSpeciesImageById( console.error('Download failed:', error); alert(`Failed to download image: ${message}`); } -} +} \ No newline at end of file From 1be5771b691e56dfdbd33b58d0d4ff927debb7ef Mon Sep 17 00:00:00 2001 From: IndianaEunice Date: Thu, 30 Jul 2026 14:49:42 +0300 Subject: [PATCH 6/8] feat extend species fix --- ...85228239230-ConvertSpeciesImagesToBytea.ts | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 src/API/src/db/migrations/1785228239230-ConvertSpeciesImagesToBytea.ts 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 From 4e19c3db3da333c06e4a0db20920b563ffbc2e6e Mon Sep 17 00:00:00 2001 From: IndianaEunice Date: Thu, 30 Jul 2026 23:08:22 +0300 Subject: [PATCH 7/8] additional dependencies --- src/API/package-lock.json | 654 ++++++++++++++++++++++++++++++++++++-- src/API/src/schema.gql | 2 +- src/UI/package-lock.json | 30 +- 3 files changed, 627 insertions(+), 59 deletions(-) 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/src/schema.gql b/src/API/src/schema.gql index 85cdd456a..4fd04d302 100644 --- a/src/API/src/schema.gql +++ b/src/API/src/schema.gql @@ -90,7 +90,7 @@ input CreateSpeciesInformationInput { name: String! previewImage: String shortDescription: String! - speciesImage: String! + speciesImage: String } """doi""" diff --git a/src/UI/package-lock.json b/src/UI/package-lock.json index 16d08d5c5..10fb74470 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", @@ -733,7 +734,6 @@ "version": "11.14.0", "resolved": "https://registry.npmjs.org/@emotion/react/-/react-11.14.0.tgz", "integrity": "sha512-O000MLDBDdk/EohJPFUqvnp4qnHeYkVP5B0xEG0D/L7cOKP9kefu2DXn8dj74cQfsEzUqh+sr1RzFqiL1o+PpA==", - "peer": true, "dependencies": { "@babel/runtime": "^7.18.3", "@emotion/babel-plugin": "^11.13.5", @@ -1074,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", @@ -1795,7 +1796,6 @@ "version": "0.6.5", "resolved": "https://registry.npmjs.org/@lexical/utils/-/utils-0.6.5.tgz", "integrity": "sha512-G/PBON7SeGoKs7yYbyLNtJE7CltxuXHWfw7F9vUk0avCzoSTrBeMNkmIOhnyp8XPuT1/5hgNWP8IG7kMsgozEg==", - "peer": true, "dependencies": { "@lexical/list": "0.6.5", "@lexical/table": "0.6.5" @@ -2771,27 +2771,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", @@ -3467,7 +3446,7 @@ "version": "5.62.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.62.0.tgz", "integrity": "sha512-VlJEV0fOQ7BExOsHYAGrgbEiZoi8D+Bl2+f6V2RrXerRSylnp+ZBHmPvaIa8cz0Ajx7WO7Z5RqfgYg7ED1nRhA==", - "peer": true, + "dev": true, "dependencies": { "@typescript-eslint/scope-manager": "5.62.0", "@typescript-eslint/types": "5.62.0", @@ -4208,7 +4187,6 @@ "url": "https://github.com/sponsors/ai" } ], - "peer": true, "dependencies": { "baseline-browser-mapping": "^2.8.25", "caniuse-lite": "^1.0.30001754", From 89e9a2089514c7c941ab606817129ba65bf7d8c7 Mon Sep 17 00:00:00 2001 From: IndianaEunice Date: Thu, 30 Jul 2026 23:22:57 +0300 Subject: [PATCH 8/8] Update UI package-lock --- src/API/package.json | 10 +- src/UI/package-lock.json | 270 ++++++++++++++++++++++++--------------- src/UI/package.json | 1 + 3 files changed, 179 insertions(+), 102 deletions(-) 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/UI/package-lock.json b/src/UI/package-lock.json index 10fb74470..674fd5bfc 100644 --- a/src/UI/package-lock.json +++ b/src/UI/package-lock.json @@ -149,7 +149,6 @@ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.5.tgz", "integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==", "dev": true, - "peer": true, "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.5", @@ -793,7 +792,6 @@ "version": "11.14.1", "resolved": "https://registry.npmjs.org/@emotion/styled/-/styled-11.14.1.tgz", "integrity": "sha512-qEEJt42DuToa3gurlH4Qqc1kVpNq8wO8cJtDzU46TjlzWjDlsVyevtYCRijVq3SrHsROS+gVQ8Fnea108GnKzw==", - "peer": true, "dependencies": { "@babel/runtime": "^7.18.3", "@emotion/babel-plugin": "^11.13.5", @@ -864,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", @@ -1061,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", @@ -1583,7 +1583,6 @@ "version": "0.6.5", "resolved": "https://registry.npmjs.org/@lexical/clipboard/-/clipboard-0.6.5.tgz", "integrity": "sha512-yQtiKJUsvS5oftJIj5VlNA0dKsDvzpeIQTRQCVFfcwboAnwTF9VmLVW54UlDsaiW7ZeIOTXj4BgEaFdfpPkFLA==", - "peer": true, "dependencies": { "@lexical/html": "0.6.5", "@lexical/list": "0.6.5", @@ -1768,7 +1767,6 @@ "version": "0.6.5", "resolved": "https://registry.npmjs.org/@lexical/selection/-/selection-0.6.5.tgz", "integrity": "sha512-PvDLxbnHCDET/9UQp1Od4R5wakc6GgTeIKPxkfMyw9eF+vr8xFELvWvOadfhcCb+ydp5IMqqsSZ7eSCl8wFODg==", - "peer": true, "peerDependencies": { "lexical": "0.6.5" } @@ -1997,7 +1995,6 @@ "version": "5.18.0", "resolved": "https://registry.npmjs.org/@mui/material/-/material-5.18.0.tgz", "integrity": "sha512-bbH/HaJZpFtXGvWg3TsBWG4eyt3gah3E7nCNU8GLyRjVoWcA91Vm/T+sjHfUcwgJSw9iLtucfHBoq+qW/T30aA==", - "peer": true, "dependencies": { "@babel/runtime": "^7.23.9", "@mui/core-downloads-tracker": "^5.18.0", @@ -2100,7 +2097,6 @@ "version": "5.18.0", "resolved": "https://registry.npmjs.org/@mui/system/-/system-5.18.0.tgz", "integrity": "sha512-ojZGVcRWqWhu557cdO3pWHloIGJdzVtxs3rk0F9L+x55LsUjcMUVkEhiF7E4TMxZoF9MmIHGGs0ZX3FDLAf0Xw==", - "peer": true, "dependencies": { "@babel/runtime": "^7.23.9", "@mui/private-theming": "^5.17.1", @@ -3223,8 +3219,7 @@ "version": "18.0.1", "resolved": "https://registry.npmjs.org/@types/node/-/node-18.0.1.tgz", "integrity": "sha512-CmR8+Tsy95hhwtZBKJBs0/FFq4XX7sDZHlGGf+0q+BRZfMbOTkzkj0AFAuTyXbObDIoanaBBW0+KEW+m3N16Wg==", - "dev": true, - "peer": true + "dev": true }, "node_modules/@types/pako": { "version": "2.0.4", @@ -3267,7 +3262,7 @@ "version": "18.0.14", "resolved": "https://registry.npmjs.org/@types/react/-/react-18.0.14.tgz", "integrity": "sha512-x4gGuASSiWmo0xjDLpm5mPb52syZHJx02VKbqUKdLmKtAwIh63XClGsiTI1K6DO5q7ox4xAsQrU+Gl3+gGXF9Q==", - "peer": true, + "dev": true, "dependencies": { "@types/prop-types": "*", "@types/scheduler": "*", @@ -3290,8 +3285,7 @@ "version": "18.0.5", "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.0.5.tgz", "integrity": "sha512-OWPWTUrY/NIrjsAPkAk1wW9LZeIjSvkXRhclsFO8CZcZGCOg2G0YZy4ft+rOyYxy8B7ui5iZzi9OkDebZ7/QSA==", - "devOptional": true, - "peer": true, + "dev": true, "dependencies": { "@types/react": "*" } @@ -3356,7 +3350,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", @@ -3601,7 +3596,7 @@ "version": "8.15.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", - "peer": true, + "dev": true, "bin": { "acorn": "bin/acorn" }, @@ -3635,6 +3630,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" } @@ -3672,6 +3668,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", @@ -3714,6 +3711,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" } @@ -3722,6 +3720,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" }, @@ -3754,7 +3753,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", @@ -4131,7 +4131,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", @@ -4146,6 +4147,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" @@ -4429,6 +4431,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" }, @@ -4439,7 +4442,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", @@ -4464,7 +4468,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", @@ -4520,6 +4525,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", @@ -4778,7 +4784,6 @@ "version": "2.30.0", "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.30.0.tgz", "integrity": "sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw==", - "peer": true, "dependencies": { "@babel/runtime": "^7.21.0" }, @@ -4858,7 +4863,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", @@ -4967,6 +4973,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" }, @@ -5357,7 +5364,7 @@ "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.19.0.tgz", "integrity": "sha512-SXOPj3x9VKvPe81TjjUJCYlV4oJjQw68Uek+AM0X4p+33dj2HY5bpTZOgnQHcG2eAm1mtCU9uNMnJi7exU/kYw==", "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.", - "peer": true, + "dev": true, "dependencies": { "@eslint/eslintrc": "^1.3.0", "@humanwhocodes/config-array": "^0.9.2", @@ -5535,7 +5542,6 @@ "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.32.0.tgz", "integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==", "dev": true, - "peer": true, "dependencies": { "@rtsao/scc": "^1.1.0", "array-includes": "^3.1.9", @@ -5759,6 +5765,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" }, @@ -5776,6 +5783,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" } @@ -5795,6 +5803,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" @@ -5810,6 +5819,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" @@ -5825,6 +5835,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", @@ -5854,6 +5865,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" }, @@ -5884,6 +5896,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" } @@ -5949,7 +5962,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", @@ -5986,12 +6000,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", @@ -6014,6 +6030,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" }, @@ -6070,6 +6087,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", @@ -6082,7 +6100,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", @@ -6186,7 +6205,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", @@ -6233,7 +6253,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", @@ -6391,6 +6412,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", @@ -6410,6 +6432,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" }, @@ -6421,6 +6444,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" }, @@ -6509,6 +6533,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" } @@ -6782,6 +6807,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" } @@ -6800,6 +6826,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" @@ -7324,17 +7351,8 @@ "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==" - }, - "node_modules/isomorphic.js": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/isomorphic.js/-/isomorphic.js-0.2.5.tgz", - "integrity": "sha512-PIeMbHqMt4DnUP3MA/Flc0HElYjMXArsw1qwJZcm9sqR8mq3l8NYizFMty0pWwE/tzIGH3EKK5+jes5mAr85yw==", - "license": "MIT", - "funding": { - "type": "GitHub Sponsors ❤", - "url": "https://github.com/sponsors/dmonad" - } + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true }, "node_modules/istanbul-lib-coverage": { "version": "3.2.2", @@ -7442,7 +7460,6 @@ "resolved": "https://registry.npmjs.org/jest/-/jest-28.1.3.tgz", "integrity": "sha512-N4GT5on8UkZgH0O5LUavMRV1EDEhNTL0KEfRmDIeZHSV7p2XgLoY9t9VDUgL6o+yfdgYHVxuz81G8oB9VG5uyA==", "dev": true, - "peer": true, "dependencies": { "@jest/core": "^28.1.3", "@jest/types": "^28.1.3", @@ -8575,6 +8592,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" }, @@ -8642,7 +8660,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", @@ -8652,12 +8671,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", @@ -8745,10 +8766,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" } @@ -8783,8 +8811,7 @@ "node_modules/leaflet": { "version": "1.9.4", "resolved": "https://registry.npmjs.org/leaflet/-/leaflet-1.9.4.tgz", - "integrity": "sha512-nxS1ynzJOmOlHp+iL3FyWqK89GtNL8U8rvlMOsQdTTssxZwCXh8N2NB3GDQOL+YR3XnWyZAxwQixURb+FA74PA==", - "peer": true + "integrity": "sha512-nxS1ynzJOmOlHp+iL3FyWqK89GtNL8U8rvlMOsQdTTssxZwCXh8N2NB3GDQOL+YR3XnWyZAxwQixURb+FA74PA==" }, "node_modules/lerc": { "version": "3.0.0", @@ -8804,6 +8831,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" @@ -8815,29 +8843,7 @@ "node_modules/lexical": { "version": "0.6.5", "resolved": "https://registry.npmjs.org/lexical/-/lexical-0.6.5.tgz", - "integrity": "sha512-DV4/lw/CX5XLJ2qdHurkn/KjjvMw83EJidvH7Os/ub61yoaffMQxovPvsfzyOHSZ3/V+GBFCGF2hLAaaB1s37g==", - "peer": true - }, - "node_modules/lib0": { - "version": "0.2.117", - "resolved": "https://registry.npmjs.org/lib0/-/lib0-0.2.117.tgz", - "integrity": "sha512-DeXj9X5xDCjgKLU/7RR+/HQEVzuuEUiwldwOGsHK/sfAfELGWEyTcf0x+uOvCvK3O2zPmZePXWL85vtia6GyZw==", - "license": "MIT", - "dependencies": { - "isomorphic.js": "^0.2.4" - }, - "bin": { - "0ecdsa-generate-keypair": "bin/0ecdsa-generate-keypair.js", - "0gentesthtml": "bin/gentesthtml.js", - "0serve": "bin/0serve.js" - }, - "engines": { - "node": ">=16" - }, - "funding": { - "type": "GitHub Sponsors ❤", - "url": "https://github.com/sponsors/dmonad" - } + "integrity": "sha512-DV4/lw/CX5XLJ2qdHurkn/KjjvMw83EJidvH7Os/ub61yoaffMQxovPvsfzyOHSZ3/V+GBFCGF2hLAaaB1s37g==" }, "node_modules/lie": { "version": "3.3.0", @@ -8888,7 +8894,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", @@ -9576,6 +9583,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" }, @@ -9643,7 +9651,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", @@ -9662,7 +9671,6 @@ "version": "12.3.4", "resolved": "https://registry.npmjs.org/next/-/next-12.3.4.tgz", "integrity": "sha512-VcyMJUtLZBGzLKo3oMxrEF0stxh8HwuW976pAzlHhI3t8qJ4SROjCrSh1T24bhrbjw55wfZXAbXPGwPt5FLRfQ==", - "peer": true, "dependencies": { "@next/env": "12.3.4", "@swc/helpers": "0.4.11", @@ -9985,6 +9993,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" } @@ -10038,6 +10047,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", @@ -10181,6 +10191,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" } @@ -10189,6 +10200,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" } @@ -10307,6 +10319,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" } @@ -10316,7 +10329,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" }, @@ -10447,6 +10459,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" } @@ -10524,7 +10537,6 @@ "version": "18.2.0", "resolved": "https://registry.npmjs.org/react/-/react-18.2.0.tgz", "integrity": "sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==", - "peer": true, "dependencies": { "loose-envify": "^1.1.0" }, @@ -10565,7 +10577,6 @@ "version": "18.2.0", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.2.0.tgz", "integrity": "sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==", - "peer": true, "dependencies": { "loose-envify": "^1.1.0", "scheduler": "^0.23.0" @@ -10605,11 +10616,24 @@ "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", "integrity": "sha512-xXBqsWGKrY46ZqaHDo+ZUYiMUgi8suYu5kdrS20EG8KiL7VRQitEbNjm+UcrDYrNi1YLyfpmAeGjCZYXLT9YBw==", - "peer": true, "engines": { "node": ">=18.0.0" }, @@ -10624,8 +10648,7 @@ "node_modules/react-is": { "version": "19.2.0", "resolved": "https://registry.npmjs.org/react-is/-/react-is-19.2.0.tgz", - "integrity": "sha512-x3Ax3kNSMIIkyVYhWPyO09bu0uttcAIoecO/um/rKGQ4EltYWVYtyiGkS/3xMynrbVQdS69Jhlv8FXUEZehlzA==", - "peer": true + "integrity": "sha512-x3Ax3kNSMIIkyVYhWPyO09bu0uttcAIoecO/um/rKGQ4EltYWVYtyiGkS/3xMynrbVQdS69Jhlv8FXUEZehlzA==" }, "node_modules/react-leaflet": { "version": "4.2.1", @@ -10715,7 +10738,6 @@ "version": "8.1.3", "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-8.1.3.tgz", "integrity": "sha512-n0ZrutD7DaX/j9VscF+uTALI3oUPa/pO4Z3soOBIjuRn/FzVu6aehhysxZCLi6y7duMf52WNZGMl7CtuK5EnRw==", - "peer": true, "dependencies": { "@babel/runtime": "^7.12.1", "@types/hoist-non-react-statics": "^3.3.1", @@ -10768,6 +10790,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", @@ -10891,7 +10949,6 @@ "version": "4.2.1", "resolved": "https://registry.npmjs.org/redux/-/redux-4.2.1.tgz", "integrity": "sha512-LAUYz4lc+Do8/g7aeRa8JkyDErK6ekstQaqWQrNRW//MY1TvCEpMtpTWvlQ+FPbWCx+Xixu/6SHt5N0HR+SB4w==", - "peer": true, "dependencies": { "@babel/runtime": "^7.9.2" } @@ -10960,6 +11017,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" }, @@ -11103,6 +11161,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" }, @@ -11325,10 +11384,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" }, @@ -11340,6 +11406,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" } @@ -11722,6 +11789,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" }, @@ -11763,6 +11831,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" }, @@ -11823,6 +11892,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" }, @@ -11907,7 +11977,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", @@ -12075,7 +12146,6 @@ "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", "dev": true, - "peer": true, "dependencies": { "@cspotcode/source-map-support": "^0.8.0", "@tsconfig/node10": "^1.0.7", @@ -12187,6 +12257,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" }, @@ -12207,6 +12278,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" }, @@ -12292,7 +12364,7 @@ "version": "5.9.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", - "peer": true, + "dev": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -12452,6 +12524,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" } @@ -12543,7 +12616,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", @@ -12662,6 +12736,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", @@ -12714,6 +12797,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" }, @@ -12835,6 +12919,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" } @@ -12859,7 +12944,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", @@ -13001,24 +13087,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",