From 836c33aab5142383805f99f5aa4c77fb11c57748 Mon Sep 17 00:00:00 2001 From: Melanie de Leeuw Date: Tue, 7 Apr 2026 11:26:23 +0200 Subject: [PATCH 01/13] Initial set up of the Digital Specimen overview --- .../DigitalSpecimenCard.scss | 11 +++++ .../LabelValuePair/LabelValuePair.scss | 12 +++++ .../LabelValuePair/LabelValuePair.tsx | 32 ++++++++++++++ src/components/digitalSpecimen/Routes.tsx | 6 ++- .../DigitalSpecimenOverview.scss | 23 ++++++++++ .../DigitalSpecimenOverview.tsx | 44 +++++++++++++++++++ .../DataMappers/digitalSpecimenDataMapper.ts | 2 +- 7 files changed, 127 insertions(+), 3 deletions(-) create mode 100644 src/components/Cards/DigitalSpecimenCard/DigitalSpecimenCard.scss create mode 100644 src/components/LabelValuePair/LabelValuePair.scss create mode 100644 src/components/LabelValuePair/LabelValuePair.tsx create mode 100644 src/pages/DigitalSpecimenOverview/DigitalSpecimenOverview.scss create mode 100644 src/pages/DigitalSpecimenOverview/DigitalSpecimenOverview.tsx diff --git a/src/components/Cards/DigitalSpecimenCard/DigitalSpecimenCard.scss b/src/components/Cards/DigitalSpecimenCard/DigitalSpecimenCard.scss new file mode 100644 index 000000000..5c9db8dc3 --- /dev/null +++ b/src/components/Cards/DigitalSpecimenCard/DigitalSpecimenCard.scss @@ -0,0 +1,11 @@ +.digital-specimen-card { + .ds-card-header { + margin-bottom: 16px; + } + .ds-card-body { + display: grid; + grid-template-columns: 200px 1fr; /* Fixed width for labels, flexible for values */ + gap: 8px 16px; /* Row gap and Column gap */ + align-items: start; + } +} \ No newline at end of file diff --git a/src/components/LabelValuePair/LabelValuePair.scss b/src/components/LabelValuePair/LabelValuePair.scss new file mode 100644 index 000000000..d3e4c6ec9 --- /dev/null +++ b/src/components/LabelValuePair/LabelValuePair.scss @@ -0,0 +1,12 @@ +.property-row-fragment { + display: contents; + + .property-label { + font-size: 14px; + font-weight: 500; + } + .property-value { + font-size: 14px; + font-weight: 400; + } +} \ No newline at end of file diff --git a/src/components/LabelValuePair/LabelValuePair.tsx b/src/components/LabelValuePair/LabelValuePair.tsx new file mode 100644 index 000000000..2f6950f3a --- /dev/null +++ b/src/components/LabelValuePair/LabelValuePair.tsx @@ -0,0 +1,32 @@ +/* Import styles */ +import './LabelValuePair.scss'; + +type Props = { + item: { + label: string; + value: string; + isHtml: boolean; + type: string; + } +} + +/** + * Renders a single row of data. + * If the value is missing/null, it returns null to hide the row. + */ +export const LabelValuePair = ({ item }: Props) => { + if (!item.value) return null; + + return ( +
+ {item.label} + + {item.isHtml ? ( + + ) : ( + item.value + )} + +
+ ); + }; \ No newline at end of file diff --git a/src/components/digitalSpecimen/Routes.tsx b/src/components/digitalSpecimen/Routes.tsx index d7b12ee4a..b0137f927 100644 --- a/src/components/digitalSpecimen/Routes.tsx +++ b/src/components/digitalSpecimen/Routes.tsx @@ -2,12 +2,14 @@ import { Route } from "react-router-dom"; /* Import Components */ -import DigitalSpecimen from "./DigitalSpecimen"; +// import DigitalSpecimen from "./DigitalSpecimen"; +import DigitalSpecimenDetails from "pages/DigitalSpecimenOverview/DigitalSpecimenOverview"; /* Routes associated with the Digital Specimen page */ const routes = [ - } /> + // } /> + } /> ]; export default routes; \ No newline at end of file diff --git a/src/pages/DigitalSpecimenOverview/DigitalSpecimenOverview.scss b/src/pages/DigitalSpecimenOverview/DigitalSpecimenOverview.scss new file mode 100644 index 000000000..35b090a6d --- /dev/null +++ b/src/pages/DigitalSpecimenOverview/DigitalSpecimenOverview.scss @@ -0,0 +1,23 @@ +.digital-specimen-container { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 24px; + + #ds-content-column { + .digital-specimen-card { + margin-block-end: 24px; + padding: 16px; + + .ds-card-header { + display: flex; + justify-content: space-between; + align-items: center; + + h2 { + font-size: 16px; + margin: 0; + } + } + } + } +} \ No newline at end of file diff --git a/src/pages/DigitalSpecimenOverview/DigitalSpecimenOverview.tsx b/src/pages/DigitalSpecimenOverview/DigitalSpecimenOverview.tsx new file mode 100644 index 000000000..5f2c37380 --- /dev/null +++ b/src/pages/DigitalSpecimenOverview/DigitalSpecimenOverview.tsx @@ -0,0 +1,44 @@ +/* Import components */ +import { DigitalSpecimenCard } from 'components/Cards/DigitalSpecimenCard/DigitalSpecimenCard'; +import { Hero } from 'components/Hero/Hero'; + +/* Import hooks */ +import { useDigitalSpecimenComplete } from 'hooks/useDigitalSpecimen'; + +/* Import styling */ +import './DigitalSpecimenOverview.scss'; + +const DigitalSpecimenDetails = () => { + /* Base variables */ + const url = new URL(globalThis.location.href); + const segments = url.pathname.split('/'); + const identifier = segments.slice(2).join("/"); + const { data: specimen, isLoading, isError } = useDigitalSpecimenComplete({ handle: identifier}); + + if (isLoading) return

Retrieving the Digital Specimen Details...

; + if (!specimen) return

No data found

+ if (isError) return

Something went wrong with fetching the Digital Specimen. Please try again later.

; + + return ( + <> + + +
+
+ + + + + +
+
+ + ); +}; + +export default DigitalSpecimenDetails; \ No newline at end of file diff --git a/src/utils/DataMappers/digitalSpecimenDataMapper.ts b/src/utils/DataMappers/digitalSpecimenDataMapper.ts index 54d854822..705328b88 100644 --- a/src/utils/DataMappers/digitalSpecimenDataMapper.ts +++ b/src/utils/DataMappers/digitalSpecimenDataMapper.ts @@ -35,7 +35,7 @@ export const mapDigitalSpecimen = (rawData: any): DigitalSpecimenUIModel | null Object.entries(fields).forEach(([fieldKey, config]) => { mappedFields[fieldKey] = { label: config.label, - value: config.resolve(ds, acceptedIdentification), + value: config.resolve(ds, {acceptedIdentification}), isHtml: !!config.isHtml, type: config.type || 'base', }; From 047e171c8cb2e152eea5ebbc4a6ad04a9859cfef Mon Sep 17 00:00:00 2001 From: Melanie de Leeuw Date: Tue, 14 Apr 2026 13:48:19 +0200 Subject: [PATCH 02/13] Added setup of digitalSpecimenCard --- .../DigitalSpecimenCard.tsx | 40 +++++++++++++++++++ 1 file changed, 40 insertions(+) create mode 100644 src/components/Cards/DigitalSpecimenCard/DigitalSpecimenCard.tsx diff --git a/src/components/Cards/DigitalSpecimenCard/DigitalSpecimenCard.tsx b/src/components/Cards/DigitalSpecimenCard/DigitalSpecimenCard.tsx new file mode 100644 index 000000000..70c26ca0e --- /dev/null +++ b/src/components/Cards/DigitalSpecimenCard/DigitalSpecimenCard.tsx @@ -0,0 +1,40 @@ +import { CopyIcon, Pencil2Icon } from "@radix-ui/react-icons"; +import { Button, Card } from "@radix-ui/themes"; +import { LabelValuePair } from "components/LabelValuePair/LabelValuePair"; + +/* Import styles */ +import './DigitalSpecimenCard.scss'; + +type Props = { + cardHeader: string, + annotate?: boolean, + copy?: boolean, + fragment: any +} + +export const DigitalSpecimenCard = ({ cardHeader, annotate, copy, fragment }: Props) => { + return ( + +
+

{cardHeader}

+ { annotate && + + } + { copy && + + } +
+
+ {Object.entries(fragment).map(([key, item]: [string, any]) => ( + + ))} +
+
+ ) +} \ No newline at end of file From 76ec7f69079c24b2c610aca73f6d4628d62f24d8 Mon Sep 17 00:00:00 2001 From: Melanie de Leeuw Date: Mon, 20 Apr 2026 13:33:37 +0200 Subject: [PATCH 03/13] Changed html --- src/components/Hero/Hero.tsx | 19 ++++++++++++------ .../LabelValuePair/LabelValuePair.tsx | 20 +++++++++---------- .../DigitalSpecimenOverview.tsx | 3 ++- .../VirtualCollectionDetails.tsx | 2 +- 4 files changed, 26 insertions(+), 18 deletions(-) diff --git a/src/components/Hero/Hero.tsx b/src/components/Hero/Hero.tsx index ffb863510..ffa7a3729 100644 --- a/src/components/Hero/Hero.tsx +++ b/src/components/Hero/Hero.tsx @@ -19,12 +19,13 @@ import { useClipboard } from "hooks/useClipboard"; type Props = { title: string; - description: string; - badge?: string[]; + description?: string; + badge?: [{ content: string, type: "soft" | "solid" | "outline" | "surface" }]; navigateTo?: { pathName: string; text: string }; showShareButton?: boolean; details?: any; showCreateButton?: boolean; + isHtml?: boolean; } /** @@ -38,7 +39,7 @@ type Props = { * @param create Boolean that indicates if the functionality for creating a VC should be working * @returns A JSX element that shows a Hero banner with information and possibly navigation */ -export const Hero = ( { title, description, badge, navigateTo, showShareButton, details, showCreateButton }: Props) => { +export const Hero = ( { title, description, badge, navigateTo, showShareButton, details, showCreateButton, isHtml = false }: Props) => { /* Hooks */ const navigate = useNavigate(); const isAllowedToCreateVC = useHasRole('dissco-virtual-collection'); @@ -91,13 +92,17 @@ export const Hero = ( { title, description, badge, navigateTo, showShareButton, - {badge?.map((badge: string) => { + {badge?.map(({ content, type }) => { return ( - {badge} + {content} ) })}
-

{title}

+ {isHtml ? ( +

+ ) : ( +

{ title }

+ )} {showCreateButton &&
+ {description &&

{description} @@ -136,6 +142,7 @@ export const Hero = ( { title, description, badge, navigateTo, showShareButton, )}

+ } {details && <> diff --git a/src/components/LabelValuePair/LabelValuePair.tsx b/src/components/LabelValuePair/LabelValuePair.tsx index 2f6950f3a..4a8baaaa8 100644 --- a/src/components/LabelValuePair/LabelValuePair.tsx +++ b/src/components/LabelValuePair/LabelValuePair.tsx @@ -18,15 +18,15 @@ export const LabelValuePair = ({ item }: Props) => { if (!item.value) return null; return ( -
- {item.label} - - {item.isHtml ? ( - - ) : ( - item.value - )} - -
+
+ {item.label} + + {item.isHtml ? ( + + ) : ( + item.value + )} + +
); }; \ No newline at end of file diff --git a/src/pages/DigitalSpecimenOverview/DigitalSpecimenOverview.tsx b/src/pages/DigitalSpecimenOverview/DigitalSpecimenOverview.tsx index 5f2c37380..31ea7f1eb 100644 --- a/src/pages/DigitalSpecimenOverview/DigitalSpecimenOverview.tsx +++ b/src/pages/DigitalSpecimenOverview/DigitalSpecimenOverview.tsx @@ -23,9 +23,10 @@ const DigitalSpecimenDetails = () => { <>
diff --git a/src/pages/VirtualCollectionDetails/VirtualCollectionDetails.tsx b/src/pages/VirtualCollectionDetails/VirtualCollectionDetails.tsx index 01e7c39ce..8260a5c14 100644 --- a/src/pages/VirtualCollectionDetails/VirtualCollectionDetails.tsx +++ b/src/pages/VirtualCollectionDetails/VirtualCollectionDetails.tsx @@ -66,7 +66,7 @@ const VirtualCollectionDetails = () => { Date: Tue, 21 Apr 2026 14:48:38 +0200 Subject: [PATCH 04/13] Added styling for different types of labelPair values --- src/components/Hero/Hero.scss | 11 ++++ src/components/Hero/Hero.tsx | 28 ++++++--- .../LabelValuePair/LabelValuePair.scss | 27 ++++++++ .../LabelValuePair/LabelValuePair.tsx | 61 +++++++++++++++++-- .../DigitalSpecimenOverview.tsx | 3 +- .../schemas/DigitalSpecimenSchema.ts | 36 +++++++++-- 6 files changed, 144 insertions(+), 22 deletions(-) diff --git a/src/components/Hero/Hero.scss b/src/components/Hero/Hero.scss index cd52d536a..5799fece2 100644 --- a/src/components/Hero/Hero.scss +++ b/src/components/Hero/Hero.scss @@ -5,6 +5,12 @@ header { display: flex; justify-content: space-between; margin-block-end: var(--spacing-l); + + div { + button { + margin-inline-end: var(--spacing-sm); + } + } } #hero-title { display: flex; @@ -15,6 +21,11 @@ header { margin-block-start: var(--spacing-sm); } } + #hero-badges { + span { + margin-inline-end: var(--spacing-sm); + } + } #hero-content { display: flex; flex-direction: column; diff --git a/src/components/Hero/Hero.tsx b/src/components/Hero/Hero.tsx index c937bebeb..f08930c71 100644 --- a/src/components/Hero/Hero.tsx +++ b/src/components/Hero/Hero.tsx @@ -3,7 +3,7 @@ import { useLayoutEffect, useRef, useState } from "react"; import { format } from "date-fns"; /* Import components */ -import { ArrowLeftIcon, ClipboardCopyIcon, CopyIcon, PlusIcon } from "@radix-ui/react-icons"; +import { ArrowLeftIcon, ClipboardCopyIcon, CopyIcon, Pencil2Icon, PlusIcon } from "@radix-ui/react-icons"; import { Badge, Button, Dialog, Flex } from "@radix-ui/themes"; import { useLocation, useNavigate } from "react-router-dom"; @@ -20,12 +20,13 @@ import { useClipboard } from "hooks/useClipboard"; type Props = { title: string; description?: string; - badge?: [{ content: string, type: "soft" | "solid" | "outline" | "surface", color: "sky" | "green" }]; + badge?: { content: string, type: "soft" | "solid" | "outline" | "surface", color: "sky" | "grass" }[]; navigateTo?: { pathName: string; text: string }; showShareButton?: boolean; details?: any; showCreateButton?: boolean; isHtml?: boolean; + annotate?: boolean; } /** @@ -39,7 +40,7 @@ type Props = { * @param create Boolean that indicates if the functionality for creating a VC should be working * @returns A JSX element that shows a Hero banner with information and possibly navigation */ -export const Hero = ( { title, description, badge, navigateTo, showShareButton, details, showCreateButton, isHtml = false }: Props) => { +export const Hero = ( { title, description, badge, navigateTo, showShareButton, details, showCreateButton, isHtml = false, annotate }: Props) => { /* Hooks */ const navigate = useNavigate(); const isAllowedToCreateVC = useHasRole('dissco-virtual-collection'); @@ -83,6 +84,12 @@ export const Hero = ( { title, description, badge, navigateTo, showShareButton, }
+ {annotate && + + } {showShareButton &&
- - {badge?.map(({ content, type, color }) => { - return ( - {content} - ) - })} + +
+ {badge?.map(({ content, type, color }) => { + if (!content) return null; + return ( + {content} + ) + })} +
{isHtml ? (

diff --git a/src/components/LabelValuePair/LabelValuePair.scss b/src/components/LabelValuePair/LabelValuePair.scss index d3e4c6ec9..37cfeb161 100644 --- a/src/components/LabelValuePair/LabelValuePair.scss +++ b/src/components/LabelValuePair/LabelValuePair.scss @@ -5,8 +5,35 @@ font-size: 14px; font-weight: 500; } + + .verbatim { + font-size: 12px; + color: grey; + } .property-value { font-size: 14px; font-weight: 400; + + a { + svg { + margin-inline-start: 4px; + } + } + + .verbatim { + font-size: 12px; + color: grey; + + span { + margin-inline-start: 8px; + } + } + .btn-as-link { + padding: 0; + + svg { + margin-inline-start: var(--spacing-xs); + } + } } } \ No newline at end of file diff --git a/src/components/LabelValuePair/LabelValuePair.tsx b/src/components/LabelValuePair/LabelValuePair.tsx index c413286ce..eff970569 100644 --- a/src/components/LabelValuePair/LabelValuePair.tsx +++ b/src/components/LabelValuePair/LabelValuePair.tsx @@ -1,6 +1,16 @@ /* Import styles */ +import { Badge } from '@radix-ui/themes'; import './LabelValuePair.scss'; +/* Import components */ +import { CopyIcon, ExternalLinkIcon } from '@radix-ui/react-icons'; + +/* Import hooks */ +import { useClipboard } from 'hooks/useClipboard'; + +/* Import utils */ +import { RetrieveEnvVariable } from 'app/Utilities'; + type Props = { item: { label: string; @@ -16,16 +26,55 @@ type Props = { */ export const LabelValuePair = ({ item }: Props) => { if (!item.value) return null; + + /* Base variables */ + const { copy } = useClipboard(); + + /* Return correct piece of HTML based on the type */ + const setCorrectItemType = () => { + switch (item.type) { + case 'url': + return ( + + Catalogue of Life + + + ); + case 'verbatim': + return ( + + {item.value} + Original + + ) + case 'copy': + return ( + + ) + default: + return ( + item?.isHtml ? ( + + ) : ( + {item.value} + ) + ) + } + + } return (
- {item.label} + {item.type === 'verbatim' ? + {item.label} + : {item.label} + } + - {item.isHtml ? ( - - ) : ( - item.value - )} + {setCorrectItemType()}
); diff --git a/src/pages/DigitalSpecimenOverview/DigitalSpecimenOverview.tsx b/src/pages/DigitalSpecimenOverview/DigitalSpecimenOverview.tsx index ecced6fcf..787ce5bf9 100644 --- a/src/pages/DigitalSpecimenOverview/DigitalSpecimenOverview.tsx +++ b/src/pages/DigitalSpecimenOverview/DigitalSpecimenOverview.tsx @@ -26,7 +26,8 @@ const DigitalSpecimenDetails = () => { navigateTo={{ pathName:"/search", text: "Specimens"}} showShareButton={true} isHtml={specimen?.IDENTIFICATION?.scientificName?.value?.isHtml} - badge={[{ content: specimen?.UI_COMPONENTS_DATA?.taxonRank.value, type: 'solid', color: 'green'}]} + badge={[{ content: specimen?.UI_COMPONENTS_DATA?.taxonRank.value.toLowerCase(), type: 'solid', color: 'grass'}, { content: specimen?.UI_COMPONENTS_DATA?.typeStatus.value, type: 'solid', color: 'sky'}]} + annotate={true} >
diff --git a/src/utils/DataMappers/schemas/DigitalSpecimenSchema.ts b/src/utils/DataMappers/schemas/DigitalSpecimenSchema.ts index 578c39ef9..d6532d755 100644 --- a/src/utils/DataMappers/schemas/DigitalSpecimenSchema.ts +++ b/src/utils/DataMappers/schemas/DigitalSpecimenSchema.ts @@ -44,16 +44,20 @@ const DIGITAL_SPECIMEN_SCHEMA_MAP: Record> = return htmlLabel || fallbackLabel; }, }, - taxonomicStatus: { - label: 'Taxonomic Status', - type: 'url', - resolve: (_, { acceptedIdentification }) => acceptedIdentification?.["@id"] - }, verbatimName: { label: 'Identification Verbatim', type: 'verbatim', resolve: (ds) => ds["ods:hasIdentifications"]?.[0]?.["dwc:verbatimIdentification"] }, + rank: { + label: 'Rank', + resolve: (_, { acceptedIdentification }) => acceptedIdentification?.["dwc:taxonRank"] + }, + taxonomicStatus: { + label: 'Taxonomic Status', + type: 'url', + resolve: (_, { acceptedIdentification }) => acceptedIdentification?.["@id"] + }, kingdom: { label: 'Kingdom', resolve: (_, { acceptedIdentification }) => acceptedIdentification?.["dwc:kingdom"] @@ -90,6 +94,14 @@ const DIGITAL_SPECIMEN_SCHEMA_MAP: Record> = label: 'Specific Epithet', resolve: (_, { acceptedIdentification }) => acceptedIdentification?.["dwc:specificEpithet"] }, + infragenericEpithet: { + label: 'Infrageneric Epithet', + resolve: (_, { acceptedIdentification }) => acceptedIdentification?.["dwc:infragenericEpithet"] + }, + infraspecificEpithet: { + label: 'Infraspecific Epithet', + resolve: (_, { acceptedIdentification }) => acceptedIdentification?.["dwc:infraspecificEpithet"] + }, nomenClaturalCode: { label: 'Nomenclatural Code', resolve: (_, { acceptedIdentification }) => acceptedIdentification?.["dwc:nomenclaturalCode"] @@ -98,6 +110,14 @@ const DIGITAL_SPECIMEN_SCHEMA_MAP: Record> = label: 'Scientific Name Authorship', resolve: (_, { acceptedIdentification }) => acceptedIdentification?.["dwc:scientificNameAuthorship"] }, + sex: { + label: 'Sex', + resolve: (_, { primaryEvent }) => primaryEvent?.["dwc:sex"] + }, + lifeStage: { + label: 'Life Stage', + resolve: (_, { primaryEvent }) => primaryEvent?.["dwc:lifeStage"] + } }, LOCATION: { @@ -116,7 +136,7 @@ const DIGITAL_SPECIMEN_SCHEMA_MAP: Record> = }, geodeticDatum: { label: 'Geodetic Datum', - resolve: (_, { primaryEvent }) => primaryEvent?.["ods:hasLocation"]?.["ods:hasGeoreference"]["dwc:geodeticDatum"] + resolve: (_, { primaryEvent }) => primaryEvent?.["ods:hasLocation"]?.["ods:hasGeoreference"]?.["dwc:geodeticDatum"] } }, @@ -146,6 +166,10 @@ const DIGITAL_SPECIMEN_SCHEMA_MAP: Record> = taxonRank: { label: 'Taxonomic Rank', resolve: (_, { acceptedIdentification }) => acceptedIdentification?.["dwc:taxonRank"] + }, + typeStatus: { + label: 'Type Status', + resolve: (ds) => ds["ods:hasIdentifications"]?.[0]?.["dwc:typeStatus"] } } }; From 56031fffd50734f2771ff9b8e9d0161be86c6a48 Mon Sep 17 00:00:00 2001 From: Melanie de Leeuw Date: Wed, 22 Apr 2026 14:42:47 +0200 Subject: [PATCH 05/13] Added georeference map --- .../DigitalSpecimenCard.scss | 5 +++++ .../DigitalSpecimenCard.tsx | 12 +++++++++-- .../components/DigitalSpecimenOverview.tsx | 5 ++++- .../customUI/openStreetMap/OpenStreetMap.tsx | 20 +++++++++---------- .../openStreetMap/OpenStreetMapMarker.tsx | 20 ++++++++----------- .../DigitalSpecimenOverview.tsx | 2 +- .../schemas/DigitalSpecimenSchema.ts | 20 ++++++++++++++++++- 7 files changed, 56 insertions(+), 28 deletions(-) diff --git a/src/components/Cards/DigitalSpecimenCard/DigitalSpecimenCard.scss b/src/components/Cards/DigitalSpecimenCard/DigitalSpecimenCard.scss index 5c9db8dc3..b236d2753 100644 --- a/src/components/Cards/DigitalSpecimenCard/DigitalSpecimenCard.scss +++ b/src/components/Cards/DigitalSpecimenCard/DigitalSpecimenCard.scss @@ -2,6 +2,11 @@ .ds-card-header { margin-bottom: 16px; } + .ds-card-georeference { + width: 100%; + height: 320px; + margin-block-end: 8px; + } .ds-card-body { display: grid; grid-template-columns: 200px 1fr; /* Fixed width for labels, flexible for values */ diff --git a/src/components/Cards/DigitalSpecimenCard/DigitalSpecimenCard.tsx b/src/components/Cards/DigitalSpecimenCard/DigitalSpecimenCard.tsx index 70c26ca0e..d2d6c9cf4 100644 --- a/src/components/Cards/DigitalSpecimenCard/DigitalSpecimenCard.tsx +++ b/src/components/Cards/DigitalSpecimenCard/DigitalSpecimenCard.tsx @@ -1,6 +1,8 @@ +/* Import components */ import { CopyIcon, Pencil2Icon } from "@radix-ui/react-icons"; import { Button, Card } from "@radix-ui/themes"; import { LabelValuePair } from "components/LabelValuePair/LabelValuePair"; +import { OpenStreetMap } from "components/elements/customUI/CustomUI"; /* Import styles */ import './DigitalSpecimenCard.scss'; @@ -9,10 +11,11 @@ type Props = { cardHeader: string, annotate?: boolean, copy?: boolean, - fragment: any + fragment: any, + georeference?: boolean } -export const DigitalSpecimenCard = ({ cardHeader, annotate, copy, fragment }: Props) => { +export const DigitalSpecimenCard = ({ cardHeader, annotate, copy, fragment, georeference = false }: Props) => { return (
@@ -30,6 +33,11 @@ export const DigitalSpecimenCard = ({ cardHeader, annotate, copy, fragment }: Pr }
+ {georeference && +
+ +
+ }
{Object.entries(fragment).map(([key, item]: [string, any]) => ( diff --git a/src/components/digitalSpecimen/components/contentBlock/components/DigitalSpecimenOverview.tsx b/src/components/digitalSpecimen/components/contentBlock/components/DigitalSpecimenOverview.tsx index 610d1515c..9c3e2a171 100644 --- a/src/components/digitalSpecimen/components/contentBlock/components/DigitalSpecimenOverview.tsx +++ b/src/components/digitalSpecimen/components/contentBlock/components/DigitalSpecimenOverview.tsx @@ -271,7 +271,10 @@ const DigitalSpecimenOverview = (props: Props) => { {/* Geological reference map */} - + diff --git a/src/components/elements/customUI/openStreetMap/OpenStreetMap.tsx b/src/components/elements/customUI/openStreetMap/OpenStreetMap.tsx index a18b72020..9b6d1bd3a 100644 --- a/src/components/elements/customUI/openStreetMap/OpenStreetMap.tsx +++ b/src/components/elements/customUI/openStreetMap/OpenStreetMap.tsx @@ -1,31 +1,29 @@ /* Import Dependencies */ import { MapContainer, TileLayer } from 'react-leaflet'; -/* Import Types */ -import { Dict } from 'app/Types'; - /* Import Components */ import OpenStreetMapMarker from './OpenStreetMapMarker'; /* Props Type */ type Props = { - georeference: Dict | undefined + longitude?: number, + latitude?: number, }; /** * Component that renders a Leaflet map based on the given geological reference, - * @param georeference A geological reference object holding the latitude and longitude among other properties + * @param longitude Longitude decimal number + * @param latitude Latitude decimal number * @returns JSX Component */ -const OpenStreetMap = (props: Props) => { - const { georeference } = props; - +const OpenStreetMap = ({longitude, latitude}: Props) => { + console.log(longitude, latitude) return (
- {(georeference && 'dwc:decimalLatitude' in georeference && 'dwc:decimalLongitude' in georeference) ? - { url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png" /> - + :

Geographical map could not be created due to a lack of coordinates

} diff --git a/src/components/elements/customUI/openStreetMap/OpenStreetMapMarker.tsx b/src/components/elements/customUI/openStreetMap/OpenStreetMapMarker.tsx index 27b791f3b..802217b03 100644 --- a/src/components/elements/customUI/openStreetMap/OpenStreetMapMarker.tsx +++ b/src/components/elements/customUI/openStreetMap/OpenStreetMapMarker.tsx @@ -5,38 +5,34 @@ import { Marker, Popup, useMap } from 'react-leaflet'; /* Import Hooks */ import { useTrigger } from 'app/Hooks'; -/* Import Types */ -import { Dict } from 'app/Types'; - /* Import Sources */ import markerIconPng from 'leaflet/dist/images/marker-icon.png'; /* Props Type */ type Props = { - georeference: Dict + longitude: number, + latitude: number, }; -const OpenStreetMapMarker = (props: Props) => { - const { georeference } = props; - +const OpenStreetMapMarker = ({longitude, latitude}: Props) => { /* Hooks */ const leafletMap = useMap(); const trigger = useTrigger(); trigger.SetTrigger(() => { - leafletMap.setView([georeference['dwc:decimalLatitude'], georeference['dwc:decimalLongitude']]); - }, [georeference]); + leafletMap.setView([latitude, longitude]); + }, [longitude, latitude]); return ( -

Coordinates

-

Latitude: {georeference['dwc:decimalLatitude']}

-

Longitude: {georeference['dwc:decimalLongitude']}

+

Latitude: {latitude}

+

Longitude: {longitude}

); diff --git a/src/pages/DigitalSpecimenOverview/DigitalSpecimenOverview.tsx b/src/pages/DigitalSpecimenOverview/DigitalSpecimenOverview.tsx index 787ce5bf9..29602f5de 100644 --- a/src/pages/DigitalSpecimenOverview/DigitalSpecimenOverview.tsx +++ b/src/pages/DigitalSpecimenOverview/DigitalSpecimenOverview.tsx @@ -34,7 +34,7 @@ const DigitalSpecimenDetails = () => {
- +
diff --git a/src/utils/DataMappers/schemas/DigitalSpecimenSchema.ts b/src/utils/DataMappers/schemas/DigitalSpecimenSchema.ts index d6532d755..9c6f133ca 100644 --- a/src/utils/DataMappers/schemas/DigitalSpecimenSchema.ts +++ b/src/utils/DataMappers/schemas/DigitalSpecimenSchema.ts @@ -137,7 +137,25 @@ const DIGITAL_SPECIMEN_SCHEMA_MAP: Record> = geodeticDatum: { label: 'Geodetic Datum', resolve: (_, { primaryEvent }) => primaryEvent?.["ods:hasLocation"]?.["ods:hasGeoreference"]?.["dwc:geodeticDatum"] - } + }, + decimalLongitude: { + label: 'Decimal Longitude', + resolve: (_, { primaryEvent }) => primaryEvent?.["ods:hasLocation"]?.["ods:hasGeoreference"]?.["dwc:decimalLongitude"] + }, + verbatimLongitude: { + label: 'Decimal Longitude', + resolve: (_, { primaryEvent }) => primaryEvent?.["ods:hasLocation"]?.["ods:hasGeoreference"]?.["dwc:verbatimLongitude"], + type: 'verbatim' + }, + decimalLatitude: { + label: 'Decimal Latitude', + resolve: (_, { primaryEvent }) => primaryEvent?.["ods:hasLocation"]?.["ods:hasGeoreference"]?.["dwc:decimalLatitude"] + }, + verbatimLatitude: { + label: 'Decimal Latitude', + resolve: (_, { primaryEvent }) => primaryEvent?.["ods:hasLocation"]?.["ods:hasGeoreference"]?.["dwc:verbatimLatitude"], + type: 'verbatim' + }, }, COLLECTING_EVENT: { From 28e36722ac6eba98838c9949c8a597b208071ecd Mon Sep 17 00:00:00 2001 From: Melanie de Leeuw Date: Wed, 22 Apr 2026 15:51:35 +0200 Subject: [PATCH 06/13] Added citation as well --- .../DigitalSpecimenCard.scss | 21 ++++++++- .../DigitalSpecimenCard.tsx | 45 ++++++++++++++++--- .../LabelValuePair/LabelValuePair.tsx | 3 +- .../DigitalSpecimenOverview.scss | 2 +- .../DigitalSpecimenOverview.tsx | 6 ++- .../DataMappers/digitalSpecimenDataMapper.ts | 1 + .../schemas/DigitalSpecimenSchema.ts | 25 +++++++++++ .../DataMappers/types/dataMapperTypes.ts | 2 + 8 files changed, 93 insertions(+), 12 deletions(-) diff --git a/src/components/Cards/DigitalSpecimenCard/DigitalSpecimenCard.scss b/src/components/Cards/DigitalSpecimenCard/DigitalSpecimenCard.scss index b236d2753..baafd7049 100644 --- a/src/components/Cards/DigitalSpecimenCard/DigitalSpecimenCard.scss +++ b/src/components/Cards/DigitalSpecimenCard/DigitalSpecimenCard.scss @@ -7,10 +7,27 @@ height: 320px; margin-block-end: 8px; } + .ds-card-citation { + max-height: 92px; + width: 100%; + background-color: #F2F3F8; + padding: 16px; + margin-block-end: 8px; + + p { + font-size: 14px; + + a { + color: black; + text-decoration: underline; + } + + } + } .ds-card-body { display: grid; - grid-template-columns: 200px 1fr; /* Fixed width for labels, flexible for values */ - gap: 8px 16px; /* Row gap and Column gap */ + grid-template-columns: 200px 1fr; + gap: 8px 16px; align-items: start; } } \ No newline at end of file diff --git a/src/components/Cards/DigitalSpecimenCard/DigitalSpecimenCard.tsx b/src/components/Cards/DigitalSpecimenCard/DigitalSpecimenCard.tsx index d2d6c9cf4..c3c7c7a3c 100644 --- a/src/components/Cards/DigitalSpecimenCard/DigitalSpecimenCard.tsx +++ b/src/components/Cards/DigitalSpecimenCard/DigitalSpecimenCard.tsx @@ -13,9 +13,37 @@ type Props = { copy?: boolean, fragment: any, georeference?: boolean + citation?: boolean } -export const DigitalSpecimenCard = ({ cardHeader, annotate, copy, fragment, georeference = false }: Props) => { +export const DigitalSpecimenCard = ({ cardHeader, annotate, copy, fragment, georeference = false, citation = false }: Props) => { + const craftCitation = (fragment: any) => { + return ( + <> + + {fragment['organisationName'].value ?? fragment['organisationId'].value} + + {` (${new Date().getFullYear()}). `} + + Distributed System of Scientific Collections + + {`. [Dataset]. `} + + {fragment['digitalSpecimenId'].value} + + + ) + + } return (
@@ -33,14 +61,19 @@ export const DigitalSpecimenCard = ({ cardHeader, annotate, copy, fragment, geor }
- {georeference && -
- -
+ { georeference && +
+ +
+ } + { citation && +
+

{craftCitation(fragment)}

+
}
{Object.entries(fragment).map(([key, item]: [string, any]) => ( - + ))}
diff --git a/src/components/LabelValuePair/LabelValuePair.tsx b/src/components/LabelValuePair/LabelValuePair.tsx index eff970569..5d8bc0fea 100644 --- a/src/components/LabelValuePair/LabelValuePair.tsx +++ b/src/components/LabelValuePair/LabelValuePair.tsx @@ -17,6 +17,7 @@ type Props = { value: string; isHtml: boolean; type: string; + hidden: boolean; } } @@ -25,7 +26,7 @@ type Props = { * If the value is missing/null, it returns null to hide the row. */ export const LabelValuePair = ({ item }: Props) => { - if (!item.value) return null; + if (!item.value || item.hidden) return null; /* Base variables */ const { copy } = useClipboard(); diff --git a/src/pages/DigitalSpecimenOverview/DigitalSpecimenOverview.scss b/src/pages/DigitalSpecimenOverview/DigitalSpecimenOverview.scss index 35b090a6d..ab2c6ea06 100644 --- a/src/pages/DigitalSpecimenOverview/DigitalSpecimenOverview.scss +++ b/src/pages/DigitalSpecimenOverview/DigitalSpecimenOverview.scss @@ -3,7 +3,7 @@ grid-template-columns: 1fr 1fr; gap: 24px; - #ds-content-column { + #ds-right-column, #ds-left-column { .digital-specimen-card { margin-block-end: 24px; padding: 16px; diff --git a/src/pages/DigitalSpecimenOverview/DigitalSpecimenOverview.tsx b/src/pages/DigitalSpecimenOverview/DigitalSpecimenOverview.tsx index 29602f5de..f11ec9966 100644 --- a/src/pages/DigitalSpecimenOverview/DigitalSpecimenOverview.tsx +++ b/src/pages/DigitalSpecimenOverview/DigitalSpecimenOverview.tsx @@ -31,12 +31,14 @@ const DigitalSpecimenDetails = () => { >
-
+
+
+
- +
diff --git a/src/utils/DataMappers/digitalSpecimenDataMapper.ts b/src/utils/DataMappers/digitalSpecimenDataMapper.ts index 8e47f62bc..4c0d55287 100644 --- a/src/utils/DataMappers/digitalSpecimenDataMapper.ts +++ b/src/utils/DataMappers/digitalSpecimenDataMapper.ts @@ -52,6 +52,7 @@ export const mapDigitalSpecimen = (rawData: any): DigitalSpecimenUIModel | null value: config.resolve(ds, {acceptedIdentification, primaryEvent}), isHtml: Boolean(config.isHtml), type: config.type || 'base', + hidden: config.hidden || false } ]) ); diff --git a/src/utils/DataMappers/schemas/DigitalSpecimenSchema.ts b/src/utils/DataMappers/schemas/DigitalSpecimenSchema.ts index 9c6f133ca..7946000d3 100644 --- a/src/utils/DataMappers/schemas/DigitalSpecimenSchema.ts +++ b/src/utils/DataMappers/schemas/DigitalSpecimenSchema.ts @@ -178,6 +178,31 @@ const DIGITAL_SPECIMEN_SCHEMA_MAP: Record> = license: { label: 'License Agreement', resolve: (ds) => ds["dcterms:license"] + }, + organisationId: { + label: 'Organisation ID', + resolve: (ds) => ds["ods:organisationID"], + hidden: true + }, + organisationName: { + label: 'Organisation Name', + resolve: (ds) => ds["ods:organisationName"], + hidden: true + }, + scientificName: { + label: 'Scientific Name', + resolve: (_, { acceptedIdentification }) => { + const htmlLabel = acceptedIdentification?.["ods:scientificNameHTMLLabel"]; + const fallbackLabel = acceptedIdentification?.["dwc:scientificName"]; + + return htmlLabel || fallbackLabel; + }, + hidden: true + }, + digitalSpecimenId: { + label: 'Digital Specimen ID', + resolve: (ds) => ds["@id"], + hidden: true } }, UI_COMPONENTS_DATA: { diff --git a/src/utils/DataMappers/types/dataMapperTypes.ts b/src/utils/DataMappers/types/dataMapperTypes.ts index d6b9cdb27..2967745a7 100644 --- a/src/utils/DataMappers/types/dataMapperTypes.ts +++ b/src/utils/DataMappers/types/dataMapperTypes.ts @@ -10,6 +10,7 @@ interface UIProperty { value: any; isHtml: boolean; type: string; + hidden: boolean; } /* Corresponding field config interface for DigitalSpecimen schema */ @@ -18,6 +19,7 @@ interface FieldConfig { resolve: (ds: any, context: MapperContext) => any; isHtml?: boolean; type?: string; + hidden?: boolean; } /* Result of the Digital Specimen data mapper */ From edd790ffb060c943bb082616f5e700c38eabf6bb Mon Sep 17 00:00:00 2001 From: Melanie de Leeuw Date: Wed, 29 Apr 2026 13:16:12 +0200 Subject: [PATCH 07/13] Releasing first version of the new digital specimen overview --- .../DigitalSpecimenCard.tsx | 40 +++++++++---------- .../LabelValuePair/LabelValuePair.tsx | 4 +- src/components/digitalSpecimen/Routes.tsx | 6 +-- 3 files changed, 24 insertions(+), 26 deletions(-) diff --git a/src/components/Cards/DigitalSpecimenCard/DigitalSpecimenCard.tsx b/src/components/Cards/DigitalSpecimenCard/DigitalSpecimenCard.tsx index c3c7c7a3c..31d65f427 100644 --- a/src/components/Cards/DigitalSpecimenCard/DigitalSpecimenCard.tsx +++ b/src/components/Cards/DigitalSpecimenCard/DigitalSpecimenCard.tsx @@ -20,26 +20,26 @@ export const DigitalSpecimenCard = ({ cardHeader, annotate, copy, fragment, geor const craftCitation = (fragment: any) => { return ( <> - - {fragment['organisationName'].value ?? fragment['organisationId'].value} - - {` (${new Date().getFullYear()}). `} - - Distributed System of Scientific Collections - - {`. [Dataset]. `} - - {fragment['digitalSpecimenId'].value} - + + {fragment['organisationName'].value ?? fragment['organisationId'].value} + + {` (${new Date().getFullYear()}). `} + + Distributed System of Scientific Collections + + {`. [Dataset]. `} + + {fragment['digitalSpecimenId'].value} + ) diff --git a/src/components/LabelValuePair/LabelValuePair.tsx b/src/components/LabelValuePair/LabelValuePair.tsx index 5d8bc0fea..f85ddead1 100644 --- a/src/components/LabelValuePair/LabelValuePair.tsx +++ b/src/components/LabelValuePair/LabelValuePair.tsx @@ -26,11 +26,11 @@ type Props = { * If the value is missing/null, it returns null to hide the row. */ export const LabelValuePair = ({ item }: Props) => { - if (!item.value || item.hidden) return null; - /* Base variables */ const { copy } = useClipboard(); + if (!item.value || item.hidden) return null; + /* Return correct piece of HTML based on the type */ const setCorrectItemType = () => { switch (item.type) { diff --git a/src/components/digitalSpecimen/Routes.tsx b/src/components/digitalSpecimen/Routes.tsx index b0137f927..d7b12ee4a 100644 --- a/src/components/digitalSpecimen/Routes.tsx +++ b/src/components/digitalSpecimen/Routes.tsx @@ -2,14 +2,12 @@ import { Route } from "react-router-dom"; /* Import Components */ -// import DigitalSpecimen from "./DigitalSpecimen"; -import DigitalSpecimenDetails from "pages/DigitalSpecimenOverview/DigitalSpecimenOverview"; +import DigitalSpecimen from "./DigitalSpecimen"; /* Routes associated with the Digital Specimen page */ const routes = [ - // } /> - } /> + } /> ]; export default routes; \ No newline at end of file From 6fa39176be384484deb0bb946da3effd07602950 Mon Sep 17 00:00:00 2001 From: Melanie de Leeuw Date: Thu, 30 Apr 2026 09:05:00 +0200 Subject: [PATCH 08/13] Changed props for OpenStreetMap in the IdCard --- src/components/search/components/idCard/IdCard.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/components/search/components/idCard/IdCard.tsx b/src/components/search/components/idCard/IdCard.tsx index 05ab553d8..37b10e974 100644 --- a/src/components/search/components/idCard/IdCard.tsx +++ b/src/components/search/components/idCard/IdCard.tsx @@ -178,7 +178,10 @@ const IdCard = () => { ) && - + } From 4dd60bb71e1713b9f1ab08ed7502151ddaf7dc46 Mon Sep 17 00:00:00 2001 From: Melanie de Leeuw Date: Thu, 30 Apr 2026 09:38:51 +0200 Subject: [PATCH 09/13] Replaced all spacings to variables --- .../DigitalSpecimenCard.scss | 20 +++++++++---------- .../LabelValuePair/LabelValuePair.scss | 20 +++++++++---------- .../customUI/openStreetMap/OpenStreetMap.tsx | 1 - .../DigitalSpecimenOverview.scss | 8 ++++---- src/styles/variables.scss | 2 ++ 5 files changed, 26 insertions(+), 25 deletions(-) diff --git a/src/components/Cards/DigitalSpecimenCard/DigitalSpecimenCard.scss b/src/components/Cards/DigitalSpecimenCard/DigitalSpecimenCard.scss index baafd7049..fa421a8d5 100644 --- a/src/components/Cards/DigitalSpecimenCard/DigitalSpecimenCard.scss +++ b/src/components/Cards/DigitalSpecimenCard/DigitalSpecimenCard.scss @@ -1,24 +1,24 @@ .digital-specimen-card { .ds-card-header { - margin-bottom: 16px; + margin-bottom: var(--spacing-m); } .ds-card-georeference { width: 100%; - height: 320px; - margin-block-end: 8px; + height: var(--general-card-width); + margin-block-end: var(--spacing-sm); } .ds-card-citation { - max-height: 92px; + max-height: var(--citation-content-spacing); width: 100%; background-color: #F2F3F8; - padding: 16px; - margin-block-end: 8px; + padding: var(--spacing-m); + margin-block-end: var(--spacing-sm); p { - font-size: 14px; + font-size: var(--sm-font-size); a { - color: black; + color: inherit; text-decoration: underline; } @@ -26,8 +26,8 @@ } .ds-card-body { display: grid; - grid-template-columns: 200px 1fr; - gap: 8px 16px; + grid-template-columns: var(--general-card-height) 1fr; + gap: var(--spacing-sm) var(--spacing-m); align-items: start; } } \ No newline at end of file diff --git a/src/components/LabelValuePair/LabelValuePair.scss b/src/components/LabelValuePair/LabelValuePair.scss index 37cfeb161..7761e9c8c 100644 --- a/src/components/LabelValuePair/LabelValuePair.scss +++ b/src/components/LabelValuePair/LabelValuePair.scss @@ -2,30 +2,30 @@ display: contents; .property-label { - font-size: 14px; - font-weight: 500; + font-size: var(--sm-font-size); + font-weight: var(--medium-font-weight); } .verbatim { - font-size: 12px; - color: grey; + font-size: var(--xs-font-size); + color: var(--gray-a9); } .property-value { - font-size: 14px; - font-weight: 400; + font-size: var(--sm-font-size); + font-weight: var(--regular-font-weight); a { svg { - margin-inline-start: 4px; + margin-inline-start: var(--spacing-xs); } } .verbatim { - font-size: 12px; - color: grey; + font-size: var(--xs-font-size); + color: var(--gray-a9); span { - margin-inline-start: 8px; + margin-inline-start: var(--spacing-sm); } } .btn-as-link { diff --git a/src/components/elements/customUI/openStreetMap/OpenStreetMap.tsx b/src/components/elements/customUI/openStreetMap/OpenStreetMap.tsx index 9b6d1bd3a..8abbeffff 100644 --- a/src/components/elements/customUI/openStreetMap/OpenStreetMap.tsx +++ b/src/components/elements/customUI/openStreetMap/OpenStreetMap.tsx @@ -19,7 +19,6 @@ type Props = { * @returns JSX Component */ const OpenStreetMap = ({longitude, latitude}: Props) => { - console.log(longitude, latitude) return (
{(longitude && latitude) ? diff --git a/src/pages/DigitalSpecimenOverview/DigitalSpecimenOverview.scss b/src/pages/DigitalSpecimenOverview/DigitalSpecimenOverview.scss index ab2c6ea06..8a629a1c4 100644 --- a/src/pages/DigitalSpecimenOverview/DigitalSpecimenOverview.scss +++ b/src/pages/DigitalSpecimenOverview/DigitalSpecimenOverview.scss @@ -1,12 +1,12 @@ .digital-specimen-container { display: grid; grid-template-columns: 1fr 1fr; - gap: 24px; + gap: var(--spacing-l); #ds-right-column, #ds-left-column { .digital-specimen-card { - margin-block-end: 24px; - padding: 16px; + margin-block-end: var(--spacing-l); + padding: var(--spacing-m); .ds-card-header { display: flex; @@ -14,7 +14,7 @@ align-items: center; h2 { - font-size: 16px; + font-size: var(--md-font-size); margin: 0; } } diff --git a/src/styles/variables.scss b/src/styles/variables.scss index 62d88ef44..a452c20bd 100644 --- a/src/styles/variables.scss +++ b/src/styles/variables.scss @@ -61,6 +61,8 @@ $media-queries: ( /* Container spacing */ --general-card-width: 320px; + --general-card-height: 200px; --header-content-spacing: 600px; + --citation-content-spacing: 92px; } From e065106664238bb5db893fec60d592518fb46152 Mon Sep 17 00:00:00 2001 From: Melanie de Leeuw Date: Wed, 6 May 2026 08:52:42 +0200 Subject: [PATCH 10/13] Added some specificity and css var --- .../Cards/DigitalSpecimenCard/DigitalSpecimenCard.scss | 2 +- src/components/Hero/Hero.scss | 2 +- src/components/Hero/Hero.tsx | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/components/Cards/DigitalSpecimenCard/DigitalSpecimenCard.scss b/src/components/Cards/DigitalSpecimenCard/DigitalSpecimenCard.scss index fa421a8d5..5157deba1 100644 --- a/src/components/Cards/DigitalSpecimenCard/DigitalSpecimenCard.scss +++ b/src/components/Cards/DigitalSpecimenCard/DigitalSpecimenCard.scss @@ -10,7 +10,7 @@ .ds-card-citation { max-height: var(--citation-content-spacing); width: 100%; - background-color: #F2F3F8; + background-color: var(--indigo-2); padding: var(--spacing-m); margin-block-end: var(--spacing-sm); diff --git a/src/components/Hero/Hero.scss b/src/components/Hero/Hero.scss index 5799fece2..c1b3732b8 100644 --- a/src/components/Hero/Hero.scss +++ b/src/components/Hero/Hero.scss @@ -6,7 +6,7 @@ header { justify-content: space-between; margin-block-end: var(--spacing-l); - div { + #hero-top-buttons-right, #hero-top-buttons-left { button { margin-inline-end: var(--spacing-sm); } diff --git a/src/components/Hero/Hero.tsx b/src/components/Hero/Hero.tsx index f08930c71..fcb109f23 100644 --- a/src/components/Hero/Hero.tsx +++ b/src/components/Hero/Hero.tsx @@ -75,7 +75,7 @@ export const Hero = ( { title, description, badge, navigateTo, showShareButton, return (
-
+
{navigateTo && }
-
+
{annotate &&
{isHtml ? ( -

+

) : (

{ title }

)} diff --git a/src/components/LabelValuePair/LabelValuePair.tsx b/src/components/LabelValuePair/LabelValuePair.tsx index f85ddead1..bfcb586ec 100644 --- a/src/components/LabelValuePair/LabelValuePair.tsx +++ b/src/components/LabelValuePair/LabelValuePair.tsx @@ -11,6 +11,9 @@ import { useClipboard } from 'hooks/useClipboard'; /* Import utils */ import { RetrieveEnvVariable } from 'app/Utilities'; +/* Import dependencies */ +import DOMPurify from 'dompurify'; + type Props = { item: { label: string; @@ -58,7 +61,7 @@ export const LabelValuePair = ({ item }: Props) => { default: return ( item?.isHtml ? ( - + ) : ( {item.value} ) From ee30cad7107ef999c6eb0f628cd0746a419eba74 Mon Sep 17 00:00:00 2001 From: Melanie de Leeuw Date: Wed, 6 May 2026 11:34:06 +0200 Subject: [PATCH 12/13] Added trivyignore vulnerability because of image vulnerability --- .github/workflows/.trivyignore | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/workflows/.trivyignore b/.github/workflows/.trivyignore index bd04c58e4..847b0e7b2 100644 --- a/.github/workflows/.trivyignore +++ b/.github/workflows/.trivyignore @@ -24,4 +24,7 @@ CVE-2026-33416 CVE-2026-33636 # lodash-es is pinned as a dependency on a vulnerable version in formik and cannot be upgraded by us Apr-3-2026 -CVE-2026-4800 \ No newline at end of file +CVE-2026-4800 + +# Vulnerability in image dependency nghttp2-libs that is included in the image and we cannot fix May-6-2026 +CVE-2026-27135 \ No newline at end of file From 54a051c75a43b4e4095909b3a92dd063dce723d0 Mon Sep 17 00:00:00 2001 From: Melanie de Leeuw Date: Thu, 7 May 2026 11:09:59 +0200 Subject: [PATCH 13/13] Added wrapper function for dompurify --- src/components/Hero/Hero.tsx | 4 ++-- src/components/LabelValuePair/LabelValuePair.tsx | 8 ++++---- .../VirtualCollectionDetails.tsx | 2 +- .../VirtualCollectionsOverview.tsx | 2 +- src/utils/Utils.test.tsx | 2 +- src/utils/{Pagination.ts => Utils.ts} | 14 +++++++++++++- 6 files changed, 22 insertions(+), 10 deletions(-) rename src/utils/{Pagination.ts => Utils.ts} (80%) diff --git a/src/components/Hero/Hero.tsx b/src/components/Hero/Hero.tsx index a8bd6b0e4..c93b6a52d 100644 --- a/src/components/Hero/Hero.tsx +++ b/src/components/Hero/Hero.tsx @@ -1,7 +1,6 @@ /* Import dependencies */ import { useLayoutEffect, useRef, useState } from "react"; import { format } from "date-fns"; -import DOMPurify from 'dompurify'; /* Import components */ import { ArrowLeftIcon, ClipboardCopyIcon, CopyIcon, Pencil2Icon, PlusIcon } from "@radix-ui/react-icons"; @@ -13,6 +12,7 @@ import './Hero.scss'; /* Import utilities */ import { RetrieveEnvVariable } from "app/Utilities"; +import { sanitizeHtmlWrapper } from "utils/Utils"; /* Import hooks */ import { useHasRole } from "hooks/roleChecker"; @@ -110,7 +110,7 @@ export const Hero = ( { title, description, badge, navigateTo, showShareButton,
{isHtml ? ( -

+

) : (

{ title }

)} diff --git a/src/components/LabelValuePair/LabelValuePair.tsx b/src/components/LabelValuePair/LabelValuePair.tsx index bfcb586ec..ade31ca93 100644 --- a/src/components/LabelValuePair/LabelValuePair.tsx +++ b/src/components/LabelValuePair/LabelValuePair.tsx @@ -1,9 +1,9 @@ /* Import styles */ -import { Badge } from '@radix-ui/themes'; import './LabelValuePair.scss'; /* Import components */ import { CopyIcon, ExternalLinkIcon } from '@radix-ui/react-icons'; +import { Badge } from '@radix-ui/themes'; /* Import hooks */ import { useClipboard } from 'hooks/useClipboard'; @@ -11,8 +11,8 @@ import { useClipboard } from 'hooks/useClipboard'; /* Import utils */ import { RetrieveEnvVariable } from 'app/Utilities'; -/* Import dependencies */ -import DOMPurify from 'dompurify'; +/* Import utils */ +import { sanitizeHtmlWrapper } from 'utils/Utils'; type Props = { item: { @@ -61,7 +61,7 @@ export const LabelValuePair = ({ item }: Props) => { default: return ( item?.isHtml ? ( - + ) : ( {item.value} ) diff --git a/src/pages/VirtualCollectionDetails/VirtualCollectionDetails.tsx b/src/pages/VirtualCollectionDetails/VirtualCollectionDetails.tsx index 903c92c52..3dfb8a813 100644 --- a/src/pages/VirtualCollectionDetails/VirtualCollectionDetails.tsx +++ b/src/pages/VirtualCollectionDetails/VirtualCollectionDetails.tsx @@ -12,7 +12,7 @@ import { Badge, Card } from "@radix-ui/themes"; import { useVirtualCollectionDetails, useSelectedVirtualCollection } from "hooks/useVirtualCollections"; /* Import utils */ -import { paginateItems } from "utils/Pagination"; +import { paginateItems } from "utils/Utils"; import { RetrieveEnvVariable } from "app/Utilities"; import { GetSpecimenNameHTMLLabel } from "app/utilities/NomenclaturalUtilities"; diff --git a/src/pages/VirtualCollectionOverview/VirtualCollectionsOverview.tsx b/src/pages/VirtualCollectionOverview/VirtualCollectionsOverview.tsx index 49850c8a5..584efcedb 100644 --- a/src/pages/VirtualCollectionOverview/VirtualCollectionsOverview.tsx +++ b/src/pages/VirtualCollectionOverview/VirtualCollectionsOverview.tsx @@ -16,7 +16,7 @@ import './VirtualCollectionsOverview.scss'; import { useVirtualCollections } from "hooks/useVirtualCollections"; /* Import utils */ -import { paginateItems } from "utils/Pagination"; +import { paginateItems } from "utils/Utils"; /** * Base component that renders the Virtual Collections page diff --git a/src/utils/Utils.test.tsx b/src/utils/Utils.test.tsx index cf854d8c5..1b391403e 100644 --- a/src/utils/Utils.test.tsx +++ b/src/utils/Utils.test.tsx @@ -1,5 +1,5 @@ import { describe, expect, it } from 'vitest'; -import { paginateItems } from 'utils/Pagination'; +import { paginateItems } from 'utils/Utils'; /** * Test suite for the pagination utilities diff --git a/src/utils/Pagination.ts b/src/utils/Utils.ts similarity index 80% rename from src/utils/Pagination.ts rename to src/utils/Utils.ts index b99bd73c3..289970a88 100644 --- a/src/utils/Pagination.ts +++ b/src/utils/Utils.ts @@ -1,3 +1,6 @@ +/* Import dependencies */ +import DOMPurify from 'dompurify'; + /** * Paginates an array of items based on the current page and limit. * This utility can do both server-side and client-side pagination. We need both. @@ -30,4 +33,13 @@ export const paginateItems = (items: T[] | undefined, currentPage: number, ma totalPages, totalAmount, }; -}; \ No newline at end of file +}; + +/** + * Wrapper function for the DOMPurify package to sanitize html when dangerously setting it + * @param htmlString html string that needs to be sanitized + * @returns a sanitized HTML string + */ +export const sanitizeHtmlWrapper = (htmlString: string) => { + return DOMPurify.sanitize(htmlString); +} \ No newline at end of file