diff --git a/packages/openneuro-app/src/scripts/components/accordion/accordion.scss b/packages/openneuro-app/src/scripts/components/accordion/accordion.scss index 0bf6ee8d1c..813cca6082 100644 --- a/packages/openneuro-app/src/scripts/components/accordion/accordion.scss +++ b/packages/openneuro-app/src/scripts/components/accordion/accordion.scss @@ -1,4 +1,4 @@ -@import '../scss/variables'; +@import '../../scss/variables'; /* Accordion styles */ .on-accordion-wrapper { diff --git a/packages/openneuro-app/src/scripts/components/activity-slider/ActivitySlider.tsx b/packages/openneuro-app/src/scripts/components/activity-slider/ActivitySlider.tsx index a806459108..66361e74b4 100644 --- a/packages/openneuro-app/src/scripts/components/activity-slider/ActivitySlider.tsx +++ b/packages/openneuro-app/src/scripts/components/activity-slider/ActivitySlider.tsx @@ -6,6 +6,7 @@ import Carousel from "react-multi-carousel/lib/Carousel" import type { ArrowProps, ResponsiveType } from "react-multi-carousel/lib/types" import "react-multi-carousel/lib/styles.css" import "./slider.scss" +import { ModalityHexagon } from "../../components/modality-cube/ModalityHexagon" export interface ActivitySliderProps { className?: string @@ -62,28 +63,9 @@ export const ActivitySlider = ({ {data.map(({ node }) => (
-
- {node.latestSnapshot.summary?.primaryModality - ? ( - <> -
-
-
- {node.latestSnapshot.summary?.primaryModality} -
- - ) - : ( - <> -
-
N/A
- - )} -
+

diff --git a/packages/openneuro-app/src/scripts/components/activity-slider/slider.scss b/packages/openneuro-app/src/scripts/components/activity-slider/slider.scss index f05b6ef40b..8bfde5d3fb 100644 --- a/packages/openneuro-app/src/scripts/components/activity-slider/slider.scss +++ b/packages/openneuro-app/src/scripts/components/activity-slider/slider.scss @@ -1,4 +1,4 @@ -@import '../scss/variables'; +@import '../../scss/variables'; .activity-slider { h3 { @@ -32,87 +32,6 @@ margin: 10px 0 0; } } - .activity-slider-node { - .hexagon-wrapper { - text-align: center; - margin: 20px auto 0; - position: relative; - display: inline-block; - width: 45px; - height: 45px; - div.label { - position: absolute; - top: 0; - text-align: center; - width: 100%; - left: 0; - right: 0; - bottom: 0; - display: flex; - justify-content: center; - align-items: center; - color: #fff; - font-weight: 600; - font-size: 13px; - } - } - - .hexagon { - height: 100%; - width: calc(100% * 0.57735); - display: inline-block; - } - - .hexagon:before { - position: absolute; - top: 0; - right: calc((100% / 2) - ((100% * 0.57735) / 2)); - background-color: inherit; - height: inherit; - width: inherit; - content: ''; - transform: rotateZ(60deg); - } - - .hexagon:after { - position: absolute; - top: 0; - right: calc((100% / 2) - ((100% * 0.57735) / 2)); - background-color: inherit; - height: inherit; - width: inherit; - content: ''; - transform: rotateZ(-60deg); - } - .hexagon { - transition: background-color 0.3s; - background-color: $on-dark-aqua; - - &.mri { - background-color: $mri-theme; - } - &.eeg { - background-color: $eeg-theme; - } - - &.pet { - background-color: $pet-theme; - } - &.ieeg { - background-color: $ieeg-theme; - } - - &.meg { - background-color: $meg-theme; - } - &.eeg { - background-color: $on-light-green; - } - &.nirs{ - background-color: $nirs-theme; - } - } - } } .react-multi-carousel-dot--active button { background-color: var(--current-theme-primary); diff --git a/packages/openneuro-app/src/scripts/components/button/button.scss b/packages/openneuro-app/src/scripts/components/button/button.scss index a988bfce27..c1154040a8 100644 --- a/packages/openneuro-app/src/scripts/components/button/button.scss +++ b/packages/openneuro-app/src/scripts/components/button/button.scss @@ -1,4 +1,4 @@ -@import '../scss/variables'; +@import '../../scss/variables'; .on-button { font-family: $font-sans; diff --git a/packages/openneuro-app/src/scripts/components/count-toggle/count-toggle.scss b/packages/openneuro-app/src/scripts/components/count-toggle/count-toggle.scss index ace9ab0596..893faeb198 100644 --- a/packages/openneuro-app/src/scripts/components/count-toggle/count-toggle.scss +++ b/packages/openneuro-app/src/scripts/components/count-toggle/count-toggle.scss @@ -1,4 +1,4 @@ -@import '../scss/variables'; +@import '../../scss/variables'; .toggle-btn { border: 1px solid #aaa; diff --git a/packages/openneuro-app/src/scripts/components/dropdown/dropdown.scss b/packages/openneuro-app/src/scripts/components/dropdown/dropdown.scss index 523d1c0266..b67fd63c6a 100644 --- a/packages/openneuro-app/src/scripts/components/dropdown/dropdown.scss +++ b/packages/openneuro-app/src/scripts/components/dropdown/dropdown.scss @@ -1,4 +1,4 @@ -@import '../scss/variables'; +@import '../../scss/variables'; .dropdown-wrapper { display: inline-block; diff --git a/packages/openneuro-app/src/scripts/components/facets/facet.scss b/packages/openneuro-app/src/scripts/components/facets/facet.scss index c472ff5cec..0cad909e1a 100644 --- a/packages/openneuro-app/src/scripts/components/facets/facet.scss +++ b/packages/openneuro-app/src/scripts/components/facets/facet.scss @@ -1,4 +1,4 @@ -@import '../scss/variables'; +@import '../../scss/variables'; .facet-accordion.on-accordion-wrapper { padding: 0 15px; @@ -45,7 +45,7 @@ .level-1 > li { border-top: 1px solid #dfdfdf; > span:hover { - background-color: var(--current-theme-primary-light); + background-color: var(--current-theme-primary-ultralight); } > .label { @@ -60,7 +60,7 @@ display: flex; justify-content: flex-start; &:hover { - background-color: var(--current-theme-primary-light); + background-color: var(--current-theme-primary-ultralight); } .label { @@ -165,7 +165,8 @@ .search-nav .facet-open.modality-facet { margin: 20px 0; border: 0; - background: #e5f4f7; + background: var(--current-theme-primary-ultralight); + border: 1px solid var(--current-theme-primary-light); border-radius: $border-radius-default; padding: 10px; diff --git a/packages/openneuro-app/src/scripts/components/footer/footer.scss b/packages/openneuro-app/src/scripts/components/footer/footer.scss index 3458ae8388..cce975a617 100644 --- a/packages/openneuro-app/src/scripts/components/footer/footer.scss +++ b/packages/openneuro-app/src/scripts/components/footer/footer.scss @@ -1,4 +1,4 @@ -@import '../scss/variables'; +@import '../../scss/variables'; footer { background: $charcoal; diff --git a/packages/openneuro-app/src/scripts/components/front-page/front-page.scss b/packages/openneuro-app/src/scripts/components/front-page/front-page.scss index 924417b780..9ee3ab8672 100644 --- a/packages/openneuro-app/src/scripts/components/front-page/front-page.scss +++ b/packages/openneuro-app/src/scripts/components/front-page/front-page.scss @@ -1,4 +1,4 @@ -@import '../scss/variables'; +@import '../../scss/variables'; body { font-family: $font-sans; diff --git a/packages/openneuro-app/src/scripts/components/header/header.scss b/packages/openneuro-app/src/scripts/components/header/header.scss index 5c8f6918bd..4a1c012ed5 100644 --- a/packages/openneuro-app/src/scripts/components/header/header.scss +++ b/packages/openneuro-app/src/scripts/components/header/header.scss @@ -1,4 +1,4 @@ -@import '../scss/variables'; +@import '../../scss/variables'; header { position: relative; } diff --git a/packages/openneuro-app/src/scripts/components/input/input.scss b/packages/openneuro-app/src/scripts/components/input/input.scss index c1713efd94..a53eef9239 100644 --- a/packages/openneuro-app/src/scripts/components/input/input.scss +++ b/packages/openneuro-app/src/scripts/components/input/input.scss @@ -1,4 +1,4 @@ -@import '../scss/variables'; +@import '../../scss/variables'; form, fieldset { diff --git a/packages/openneuro-app/src/scripts/components/input/term-search.scss b/packages/openneuro-app/src/scripts/components/input/term-search.scss index f47eaea8ed..00d043c940 100644 --- a/packages/openneuro-app/src/scripts/components/input/term-search.scss +++ b/packages/openneuro-app/src/scripts/components/input/term-search.scss @@ -1,4 +1,4 @@ -@import "../scss/variables"; +@import "../../scss/variables"; .term-input { display: flex; diff --git a/packages/openneuro-app/src/scripts/components/loading/loading.scss b/packages/openneuro-app/src/scripts/components/loading/loading.scss index e3c9eec7b4..e0e37de885 100644 --- a/packages/openneuro-app/src/scripts/components/loading/loading.scss +++ b/packages/openneuro-app/src/scripts/components/loading/loading.scss @@ -18,7 +18,7 @@ $spinner-hexagon-bg-color: var(--current-theme-primary); width: $spinner-width; height: $spinner-height; border-radius: 0; - background: var(--current-theme-primary-light); + background: var(--current-theme-primary-ultralight); opacity: 1; top: $spinner-base-value; } diff --git a/packages/openneuro-app/src/scripts/components/modal/modal.scss b/packages/openneuro-app/src/scripts/components/modal/modal.scss index 99bade3db1..b7d304e846 100644 --- a/packages/openneuro-app/src/scripts/components/modal/modal.scss +++ b/packages/openneuro-app/src/scripts/components/modal/modal.scss @@ -1,4 +1,4 @@ -@import '../scss/variables'; +@import '../../scss/variables'; .modal-wrapper { position: fixed; diff --git a/packages/openneuro-app/src/scripts/components/modality-cube/ModalityHexagon.tsx b/packages/openneuro-app/src/scripts/components/modality-cube/ModalityHexagon.tsx new file mode 100644 index 0000000000..7eaffcc974 --- /dev/null +++ b/packages/openneuro-app/src/scripts/components/modality-cube/ModalityHexagon.tsx @@ -0,0 +1,29 @@ +import React from "react" +import type { FC } from "react" +import { modalityShortMapping } from "../../components/formatting/modality-label" +import "./modality-hexagon.scss" +interface ModalityHexagonProps { + primaryModality: string | null | undefined +} + +//ModalityHexagon component displays a colored hexagon and label +// based on the provided primaryModality. + +export const ModalityHexagon: FC = ({ + primaryModality, +}) => { + const hexagonClass = primaryModality + ? primaryModality.toLowerCase() + : "no-modality" + + const labelText = primaryModality + ? modalityShortMapping(primaryModality) + : + + return ( +
+
+
{labelText}
+
+ ) +} diff --git a/packages/openneuro-app/src/scripts/components/modality-cube/modality-cube.scss b/packages/openneuro-app/src/scripts/components/modality-cube/modality-cube.scss index 494557d4e7..3f5fbc7056 100644 --- a/packages/openneuro-app/src/scripts/components/modality-cube/modality-cube.scss +++ b/packages/openneuro-app/src/scripts/components/modality-cube/modality-cube.scss @@ -1,4 +1,4 @@ -@import '../scss/variables'; +@import '../../scss/variables'; // Variables for hexagonal grid and cube sizes $hex-width: 240px; $hex-height: 450px; // Height for the hex grid diff --git a/packages/openneuro-app/src/scripts/components/modality-cube/modality-hexagon.scss b/packages/openneuro-app/src/scripts/components/modality-cube/modality-hexagon.scss new file mode 100644 index 0000000000..6cf85facde --- /dev/null +++ b/packages/openneuro-app/src/scripts/components/modality-cube/modality-hexagon.scss @@ -0,0 +1,93 @@ +@import '../../scss/variables'; + +.hexagon-wrapper { + text-align: center; + position: relative; + display: inline-block; + width: 45px; + height: 45px; + + div.label { + position: absolute; + top: 0; + text-align: center; + width: 100%; + left: 0; + right: 0; + bottom: 0; + display: flex; + justify-content: center; + align-items: center; + font-weight: 600; + font-size: 13px; + color: #fff; // Default label color + } + + .hexagon { + height: 100%; + width: calc(100% * 0.57735); + display: inline-block; + transition: background-color 0.3s; + background-color: $on-dark-aqua; // Default hexagon background + + &:before, + &:after { + position: absolute; + top: 0; + right: calc((100% / 2) - ((100% * 0.57735) / 2)); + background-color: inherit; + height: inherit; + width: inherit; + content: ''; + } + + &:before { + transform: rotateZ(60deg); + } + + &:after { + transform: rotateZ(-60deg); + } + + &.mri { background-color: $mri-theme; } + &.eeg { background-color: $on-light-green; } // EEG uses on-light-green + &.pet { background-color: $pet-theme; } + &.ieeg { background-color: $ieeg-theme; } + &.meg { background-color: $meg-theme; } + &.nirs { background-color: $nirs-theme; } + } +} + +a { + .hexagon-wrapper { + margin: 20px auto; + + div.label { + color: unset; + } + + .hexagon { + transition: color 0.3s; + background-color: #fff; + + // Modality specific colors (when inside 'a') + &.mri { background-color: #fff; color: $mri-theme; } + &.eeg { background-color: #fff; color: $eeg-theme; } + &.pet { background-color: #fff; color: $pet-theme; } + &.ieeg { background-color: #fff; color: $ieeg-theme; } + &.meg { background-color: #fff; color: $meg-theme; } + &.nirs { background-color: #fff; color: $nirs-theme; } + } + + &:hover { + .hexagon { + color: lighten($on-dark-aqua, 15%); + &.mri { color: lighten($mri-theme, 10%); } + &.eeg { color: lighten($eeg-theme, 10%); } + &.pet { color: lighten($pet-theme, 10%); } + &.ieeg { color: lighten($ieeg-theme, 10%); } + &.meg { color: lighten($meg-theme, 10%); } + } + } + } +} \ No newline at end of file diff --git a/packages/openneuro-app/src/scripts/components/page/page.scss b/packages/openneuro-app/src/scripts/components/page/page.scss index 6c0bc5f0f3..1dd4d5cfba 100644 --- a/packages/openneuro-app/src/scripts/components/page/page.scss +++ b/packages/openneuro-app/src/scripts/components/page/page.scss @@ -1,4 +1,4 @@ -@import '../scss/variables'; +@import '../../scss/variables'; @import '../scss//normalize'; @import '../scss//helper-classes'; @import '../scss//flex-grid'; diff --git a/packages/openneuro-app/src/scripts/components/progress-bar/progress-bar.scss b/packages/openneuro-app/src/scripts/components/progress-bar/progress-bar.scss index b3707eaf41..5ae890791d 100644 --- a/packages/openneuro-app/src/scripts/components/progress-bar/progress-bar.scss +++ b/packages/openneuro-app/src/scripts/components/progress-bar/progress-bar.scss @@ -1,4 +1,4 @@ -@import '../scss/variables'; +@import '../../scss/variables'; // Outer container .progress { overflow: hidden; diff --git a/packages/openneuro-app/src/scripts/components/radio/radio.scss b/packages/openneuro-app/src/scripts/components/radio/radio.scss index bc37ad5c87..21eabb555a 100644 --- a/packages/openneuro-app/src/scripts/components/radio/radio.scss +++ b/packages/openneuro-app/src/scripts/components/radio/radio.scss @@ -1,4 +1,4 @@ -@import '../scss/variables'; +@import '../../scss/variables'; .custom-radio { margin: 10px 0 20px; [type='radio']:checked, @@ -319,7 +319,7 @@ } } [type='radio']:not(:checked) + label:hover { - background-color: var(--current-theme-primary-light); + background-color: var(--current-theme-primary-ultralight); } [type='radio']:checked + label { color: #fff; diff --git a/packages/openneuro-app/src/scripts/components/range/TwoHandleRange.scss b/packages/openneuro-app/src/scripts/components/range/TwoHandleRange.scss index 2cd77cc5ff..32c0141e60 100644 --- a/packages/openneuro-app/src/scripts/components/range/TwoHandleRange.scss +++ b/packages/openneuro-app/src/scripts/components/range/TwoHandleRange.scss @@ -1,4 +1,4 @@ -@import '../scss/variables'; +@import '../../scss/variables'; $rangeWidth: 90%; .formRange-inner { @@ -82,7 +82,7 @@ $rangeWidth: 90%; /* For Chrome browsers */ .thumb::-webkit-slider-thumb { background-color: #fff; - border: 1px solid var(--current-theme-primary-light); + border: 1px solid var(--current-theme-primary-ultralight); border-radius: 50%; box-shadow: 0 0 5px -1px var(--current-theme-primary); cursor: pointer; @@ -96,7 +96,7 @@ $rangeWidth: 90%; /* For Firefox browsers */ .thumb::-moz-range-thumb { background-color: #fff; - border: 1px solid var(--current-theme-primary-light); + border: 1px solid var(--current-theme-primary-ultralight); border-radius: 50%; box-shadow: 0 0 5px -1px var(--current-theme-primary); cursor: pointer; diff --git a/packages/openneuro-app/src/scripts/components/read-more/read-more.scss b/packages/openneuro-app/src/scripts/components/read-more/read-more.scss index 37445f6854..7978a741bd 100644 --- a/packages/openneuro-app/src/scripts/components/read-more/read-more.scss +++ b/packages/openneuro-app/src/scripts/components/read-more/read-more.scss @@ -1,4 +1,4 @@ -@import '../scss/variables'; +@import '../../scss/variables'; .has-read-more { position: relative; diff --git a/packages/openneuro-app/src/scripts/components/scss/_variables.scss b/packages/openneuro-app/src/scripts/components/scss/_variables.scss deleted file mode 100644 index e34ec11a31..0000000000 --- a/packages/openneuro-app/src/scripts/components/scss/_variables.scss +++ /dev/null @@ -1,245 +0,0 @@ -$font-sans: 'Open Sans', sans-serif; - -//old vars -$white: #fff; -$black: #000; -$mine-shaft: #222; -$stanford-red: #8c1515; -$coral: #eb472c; -$salmon: #ec7764; - -$rock: #5f574f; -$stone: #928b81; -$driftwood: #b6b1a9; -$misty: #dad7cb; - -$fuchsia: #9169a7; - -$ocean: #00505c; -$algae: #175e54; -$sky: rgb(0, 124, 146); -$turquoise: #009b76; -$persian-green: #00b489; -$night: #012a30; -$jungle-mist: #b5cbd3; -$dodger-blue: #5890ff; - -$yellow: #ffd820; -$honey: #fffcee; - -$success: #24c85e; -$info: #24c8c8; -$warning: #f7ba4b; -$danger: #c82424; - -$notification-green: #77e6a3; - -$warning-orange-cream: #ffefdb; -$warning-orange: #df7600; - -//end old vars - -$charcoal: #333; -$newspaper: #dfdfdf; - -/* SCSS RGB */ -$on-light-aqua: rgb(55, 188, 210); -$on-dark-aqua: rgba(32, 78, 90, 1); -$on-secondary: #196583; -$on-dark-aqua-light: #e5f4f7; - -$mri-theme: rgba(79, 51, 130, 1); -$mri-hover: adjust-color($mri-theme, $lightness: 10%); -$mri-dark: darken($mri-theme, 25%); -$mri-secondary: adjust-color($mri-theme, $lightness: 15%); -$mri-light: adjust-color($mri-theme, $lightness: 45%); - -$eeg-theme: rgba(134, 31, 55, 1); -$eeg-hover: adjust-color($eeg-theme, $lightness: 10%); -$eeg-dark: darken($eeg-theme, 25%); -$eeg-secondary: adjust-color($eeg-theme, $lightness: 15%); -$eeg-light: adjust-color($eeg-theme, $lightness: 45%); - -$pet-theme: rgba(0, 105, 192, 1); -$pet-hover: adjust-color($pet-theme, $lightness: 10%); -$pet-dark: darken($pet-theme, 25%); -$pet-secondary: adjust-color($pet-theme, $lightness: 15%); -$pet-light: adjust-color($pet-theme, $lightness: 45%); - -$nih-theme: rgb(34, 133, 148); -$nih-hover: adjust-color($nih-theme, $lightness: 10%); -$nih-dark: rgba(32, 85, 138, 1); -$nih-secondary: adjust-color($nih-theme, $lightness: 15%); -$nih-light: rgba(155, 211, 221, 1); - -$ieeg-theme: rgba(18, 109, 62, 1); -$ieeg-hover: adjust-color($ieeg-theme, $lightness: 10%); -$ieeg-dark: darken($ieeg-theme, 25%); -$ieeg-secondary: adjust-color($ieeg-theme, $lightness: 15%); -$ieeg-light: rgb(200, 212, 153); - -$meg-theme: rgba(156, 57, 0, 1); -$meg-hover: adjust-color($meg-theme, $lightness: 10%); -$meg-dark: darken($meg-theme, 25%); -$meg-secondary: adjust-color($meg-theme, $lightness: 15%); -$meg-light: adjust-color($meg-theme, $lightness: 45%); - - -$nirs-theme: rgb(166 21 40); -$nirs-hover: adjust-color($nirs-theme, $lightness: 10%); -$nirs-dark: darken($nirs-theme, 25%); -$nirs-secondary: adjust-color($nirs-theme, $lightness: 15%); -$nirs-light: adjust-color($nirs-theme, $lightness: 45%); - -$on-light-green: #00eeb5; -$on-light-red: #c00342; -$on-light-orange: rgb(255, 110, 43); - -$primary: $on-dark-aqua; - -$chiclet-color: #a2059e; - -/* CSS HEX */ -:root { - --mri-theme: #{$mri-theme}; - --eeg-theme: #{$eeg-theme}; - --pet-theme: #{$pet-theme}; - --ieeg-theme: #{$ieeg-theme}; - --meg-theme: #{$meg-theme}; - --nirs-theme: #{$nirs-theme}; - - --on-light-aqua: #{$on-light-aqua}; - --on-dark-aqua: #{$on-dark-aqua}; - --on-light-green: #{on-light-green}; - - --current-theme-primary: #{$on-dark-aqua}; - --current-theme-primary-hover: #{$on-light-aqua}; - --current-theme-secondary: #{$on-secondary}; - --current-theme-primary-dark: #{$on-dark-aqua}; - --current-theme-primary-light: #{$on-dark-aqua-light}; - --current-theme-header: #333; - --current-theme-header-dark: #333; -} - -.mri-theme { - background-color: $mri-theme; -} -.eeg-theme { - background-color: $eeg-theme; -} - -.pet-theme { - background-color: $pet-theme; -} -.ieeg-theme { - background-color: $ieeg-theme; -} - -.on-light-aqua { - background-color: $on-light-aqua; -} -.on-light-green { - background-color: $on-light-green; -} -.on-dark-aqua { - background-color: $on-dark-aqua; -} - -.meg-theme { - background-color: $meg-theme; -} - -.nih-theme { - background: $nih-theme; -} - -.gray-bg { - background: #f3f3f3; -} - -//Base unit used for spacing gutters -$base-unit: 20px; - -//Screen Sizes -$screen-sm: 480px; -$screen-md: 767px; -$screen-lg: 989px; - -$border-radius-default: 4px; - -.search-page-mri, -.dataset-page-mri { - --current-theme-primary: #{$mri-theme}; - --current-theme-primary-hover: #{$mri-hover}; - --current-theme-primary-dark: #{$mri-dark}; - --current-theme-secondary: #{$mri-secondary}; - --current-theme-primary-light: #{$mri-light}; - --current-theme-header: #{$mri-theme}; - --current-theme-header-dark: #{$mri-dark}; -} - -.search-page-eeg, -.dataset-page-eeg { - --current-theme-primary: #{$eeg-theme}; - --current-theme-primary-hover: #{$eeg-hover}; - --current-theme-primary-dark: #{$eeg-dark}; - --current-theme-secondary: #{$eeg-secondary}; - --current-theme-primary-light: #{$eeg-light}; - --current-theme-header: #{$eeg-theme}; - --current-theme-header-dark: #{$eeg-dark}; -} - -.search-page-pet, -.dataset-page-pet { - --current-theme-primary: #{$pet-theme}; - --current-theme-primary-hover: #{$pet-hover}; - --current-theme-primary-dark: #{$pet-dark}; - --current-theme-secondary: #{$pet-secondary}; - --current-theme-primary-light: #{$pet-light}; - --current-theme-header: #{$pet-theme}; - --current-theme-header-dark: #{$pet-dark}; -} - -.search-page-nih, -.dataset-page-nih{ - --current-theme-primary: #{$nih-theme}; - --current-theme-primary-hover: #{$nih-hover}; - --current-theme-primary-dark: #{$nih-dark}; - --current-theme-secondary: #{$nih-secondary}; - --current-theme-primary-light: #{$nih-light}; - --current-theme-header: #{$nih-theme}; - --current-theme-header-dark: #{$nih-dark}; -} - -.search-page-ieeg, -.dataset-page-ieeg { - --current-theme-primary: #{$ieeg-theme}; - --current-theme-primary-hover: #{$ieeg-hover}; - --current-theme-primary-dark: #{$ieeg-dark}; - --current-theme-secondary: #{$ieeg-secondary}; - --current-theme-primary-light: #{$ieeg-light}; - --current-theme-header: #{$ieeg-theme}; - --current-theme-header-dark: #{$ieeg-dark}; -} - -.search-page-meg, -.dataset-page-meg { - --current-theme-primary: #{$meg-theme}; - --current-theme-primary-hover: #{$meg-hover}; - --current-theme-primary-dark: #{$meg-dark}; - --current-theme-secondary: #{$meg-secondary}; - --current-theme-primary-light: #{$meg-light}; - --current-theme-header: #{$meg-theme}; - --current-theme-header-dark: #{$meg-dark}; -} - -.search-page-nirs, -.dataset-page-nirs { - --current-theme-primary: #{$nirs-theme}; - --current-theme-primary-hover: #{$nirs-hover}; - --current-theme-primary-dark: #{$nirs-dark}; - --current-theme-secondary: #{$nirs-secondary}; - --current-theme-primary-light: #{$nirs-light}; - --current-theme-header: #{$nirs-theme}; - --current-theme-header-dark: #{$nirs-dark}; -} diff --git a/packages/openneuro-app/src/scripts/components/scss/upload-modal.scss b/packages/openneuro-app/src/scripts/components/scss/upload-modal.scss index c476c56a86..2ea96f10ba 100644 --- a/packages/openneuro-app/src/scripts/components/scss/upload-modal.scss +++ b/packages/openneuro-app/src/scripts/components/scss/upload-modal.scss @@ -1,4 +1,4 @@ -@import '../scss/variables'; +@import '../../scss/variables'; .upload-modal.modal-wrapper { // display: block; diff --git a/packages/openneuro-app/src/scripts/components/search-page/SearchResultsList.tsx b/packages/openneuro-app/src/scripts/components/search-page/SearchResultsList.tsx deleted file mode 100644 index be30b77007..0000000000 --- a/packages/openneuro-app/src/scripts/components/search-page/SearchResultsList.tsx +++ /dev/null @@ -1,29 +0,0 @@ -import React from "react" -import { SearchResultItem } from "./SearchResultItem" -import "./search-page.scss" - -export interface SearchResultsListProps { - items - datasetTypeSelected: string -} - -export const SearchResultsList = ({ - items, - datasetTypeSelected, -}: SearchResultsListProps) => { - return ( -
- {items.map((data) => { - if (data) { - return ( - - ) - } - })} -
- ) -} diff --git a/packages/openneuro-app/src/scripts/components/tooltip/tooltip.scss b/packages/openneuro-app/src/scripts/components/tooltip/tooltip.scss index fb1fb136ae..93532cca4f 100644 --- a/packages/openneuro-app/src/scripts/components/tooltip/tooltip.scss +++ b/packages/openneuro-app/src/scripts/components/tooltip/tooltip.scss @@ -1,4 +1,4 @@ -@import '../scss/variables'; +@import '../../scss/variables'; /* START TOOLTIP STYLES */ [data-tooltip] { diff --git a/packages/openneuro-app/src/scripts/dataset/components/DatasetHeader.tsx b/packages/openneuro-app/src/scripts/dataset/components/DatasetHeader.tsx index ee1a1dbb14..c1bb3c4db2 100644 --- a/packages/openneuro-app/src/scripts/dataset/components/DatasetHeader.tsx +++ b/packages/openneuro-app/src/scripts/dataset/components/DatasetHeader.tsx @@ -1,5 +1,6 @@ import React from "react" import { Link } from "react-router-dom" +import { ModalityHexagon } from "../../components/modality-cube/ModalityHexagon" export interface DatasetHeaderProps { modality: string | null | undefined @@ -16,8 +17,6 @@ export const DatasetHeader: React.FC = ({ datasetHeaderTools, datasetUserActions, }) => { - const hexagonClass = modality ? modality.toLowerCase() : "no-modality" - return (
@@ -25,16 +24,9 @@ export const DatasetHeader: React.FC = ({

-
-
-
- {modality - ? ( - modality.substr(0, 4) - ) - : } -
-
+ {renderEditor?.() || pageHeading}

diff --git a/packages/openneuro-app/src/scripts/scss/dataset/dataset-page.scss b/packages/openneuro-app/src/scripts/scss/dataset/dataset-page.scss index bb2e30d876..01b5a18042 100644 --- a/packages/openneuro-app/src/scripts/scss/dataset/dataset-page.scss +++ b/packages/openneuro-app/src/scripts/scss/dataset/dataset-page.scss @@ -1,8 +1,8 @@ @import '../variables.scss'; .dataset { .dataset-header { - .ds-header-inner{ - display: flex; + .ds-header-inner { + display: flex; align-items: center; flex-wrap: wrap; justify-content: space-between; @@ -11,45 +11,47 @@ justify-content: flex-start; align-items: center; } - .ds-inner-left{ + .ds-inner-left { max-width: 70%; @media screen and (max-width: 767px) { max-width: 100%; } + h1 a { + margin-right: 10px; + } } - .ds-inner-right{ + .ds-inner-right { max-width: 30%; @media screen and (max-width: 767px) { max-width: 100%; } } - h1 { - display: flex; - align-items: center; - color: #fff; - font-size: 24px; - margin:10px 0 ; - @media (max-width: 767px) { - font-size: 20px; - flex-wrap: wrap; - justify-content: center; + h1 { + display: flex; + align-items: center; + color: #fff; + font-size: 24px; + margin: 10px 0; + @media (max-width: 767px) { + font-size: 20px; + flex-wrap: wrap; + justify-content: center; + } } - } - .hexagon-wrapper { - - @media (max-width: 767px) { - width: 56px; - height: 56px; - div.label { - font-size: 17px; + .hexagon-wrapper { + @media (max-width: 767px) { + width: 56px; + height: 56px; + div.label { + font-size: 17px; + } } } - } - .dataset-tool-buttons{ - flex-basis: 100%; + .dataset-tool-buttons { + flex-basis: 100%; + } } } - } .dataset-header-alert { padding: 10px 20px; @@ -171,106 +173,6 @@ } } -.dataset { - .hexagon-wrapper { - text-align: center; - margin: 20px 10px; - position: relative; - display: inline-block; - width: 45px; - height: 45px; - div.label { - position: absolute; - top: 0; - text-align: center; - width: 100%; - left: 0; - right: 0; - bottom: 0; - display: flex; - justify-content: center; - align-items: center; - font-weight: 600; - font-size: 13px; - } - - .hexagon { - height: 100%; - width: calc(100% * 0.57735); - display: inline-block; - } - - .hexagon:before { - position: absolute; - top: 0; - right: calc((100% / 2) - ((100% * 0.57735) / 2)); - background-color: inherit; - height: inherit; - width: inherit; - content: ''; - transform: rotateZ(60deg); - } - - .hexagon:after { - position: absolute; - top: 0; - right: calc((100% / 2) - ((100% * 0.57735) / 2)); - background-color: inherit; - height: inherit; - width: inherit; - content: ''; - transform: rotateZ(-60deg); - } - } - - a { - .hexagon { - transition: color 0.3s; - background-color: #fff; - - &.mri { - color: $mri-theme; - } - &.eeg { - color: $eeg-theme; - } - - &.pet { - color: $pet-theme; - } - &.ieeg { - color: $ieeg-theme; - } - - &.meg { - color: $meg-theme; - } - } - &:hover { - .hexagon { - color: lighten($on-dark-aqua, 15%); - &.mri { - color: lighten($mri-theme, 10%); - } - &.eeg { - color: lighten($eeg-theme, 10%); - } - - &.pet { - color: lighten($pet-theme, 10%); - } - &.ieeg { - color: lighten($ieeg-theme, 10%); - } - - &.meg { - color: lighten($meg-theme, 10%); - } - } - } - } -} - .sidebar .dataset-meta-block { margin-bottom: 25px; font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Helvetica, Arial, diff --git a/packages/openneuro-app/src/scripts/scss/variables.scss b/packages/openneuro-app/src/scripts/scss/variables.scss index b1b79e95cb..e8f88168b6 100644 --- a/packages/openneuro-app/src/scripts/scss/variables.scss +++ b/packages/openneuro-app/src/scripts/scss/variables.scss @@ -7,14 +7,11 @@ $mine-shaft: #222; $stanford-red: #8c1515; $coral: #eb472c; $salmon: #ec7764; - $rock: #5f574f; $stone: #928b81; $driftwood: #b6b1a9; $misty: #dad7cb; - $fuchsia: #9169a7; - $ocean: #00505c; $algae: #175e54; $sky: rgb(0, 124, 146); @@ -23,64 +20,81 @@ $persian-green: #00b489; $night: #012a30; $jungle-mist: #b5cbd3; $dodger-blue: #5890ff; - $yellow: #ffd820; $honey: #fffcee; - $success: #24c85e; $info: #24c8c8; $warning: #f7ba4b; $danger: #c82424; - $notification-green: #77e6a3; - $warning-orange-cream: #ffefdb; $warning-orange: #df7600; - //end old vars +// gray colors $charcoal: #333; $newspaper: #dfdfdf; +$cloud: #F8F8F8; /* SCSS RGB */ $on-light-aqua: rgb(55, 188, 210); $on-dark-aqua: rgba(32, 78, 90, 1); $on-secondary: #196583; -$on-dark-aqua-light: #e5f4f7; +$on-light: #8bc8d4; +$on-ultralight: adjust-color($on-dark-aqua, $lightness: 72%); $mri-theme: rgba(79, 51, 130, 1); $mri-hover: adjust-color($mri-theme, $lightness: 10%); $mri-dark: darken($mri-theme, 25%); $mri-secondary: adjust-color($mri-theme, $lightness: 15%); $mri-light: adjust-color($mri-theme, $lightness: 45%); +$mri-ultralight: adjust-color($mri-theme, $lightness: 62%); $eeg-theme: rgba(134, 31, 55, 1); $eeg-hover: adjust-color($eeg-theme, $lightness: 10%); $eeg-dark: darken($eeg-theme, 25%); $eeg-secondary: adjust-color($eeg-theme, $lightness: 15%); $eeg-light: adjust-color($eeg-theme, $lightness: 45%); +$eeg-ultralight: adjust-color($eeg-theme, $lightness: 65%); $pet-theme: rgba(0, 105, 192, 1); $pet-hover: adjust-color($pet-theme, $lightness: 10%); $pet-dark: darken($pet-theme, 25%); $pet-secondary: adjust-color($pet-theme, $lightness: 15%); $pet-light: adjust-color($pet-theme, $lightness: 45%); +$pet-ultralight: adjust-color($pet-theme, $lightness: 60%); $ieeg-theme: rgba(18, 109, 62, 1); $ieeg-hover: adjust-color($ieeg-theme, $lightness: 10%); $ieeg-dark: darken($ieeg-theme, 25%); $ieeg-secondary: adjust-color($ieeg-theme, $lightness: 15%); $ieeg-light: rgb(200, 212, 153); +$ieeg-ultralight: adjust-color($ieeg-theme, $lightness: 72%); $meg-theme: rgba(156, 57, 0, 1); $meg-hover: adjust-color($meg-theme, $lightness: 10%); $meg-dark: darken($meg-theme, 25%); $meg-secondary: adjust-color($meg-theme, $lightness: 15%); $meg-light: adjust-color($meg-theme, $lightness: 45%); +$meg-ultralight: adjust-color($meg-theme, $lightness: 68%); + +$nih-theme: rgb(34, 133, 148); +$nih-hover: adjust-color($nih-theme, $lightness: 10%); +$nih-dark: rgba(32, 85, 138, 1); +$nih-secondary: adjust-color($nih-theme, $lightness: 15%); +$nih-light: rgba(155, 211, 221, 1); + +$nirs-theme: rgb(166 21 40); +$nirs-hover: adjust-color($nirs-theme, $lightness: 10%); +$nirs-dark: darken($nirs-theme, 25%); +$nirs-secondary: adjust-color($nirs-theme, $lightness: 15%); +$nirs-light: adjust-color($nirs-theme, $lightness: 45%); +$nirs-ultralight: adjust-color($nirs-theme, $lightness: 61%); $on-light-green: #00eeb5; $on-light-red: #c00342; $on-light-orange: rgb(255, 110, 43); +$text-cite: #509145; $primary: $on-dark-aqua; @@ -95,7 +109,6 @@ $screen-lg: 989px; $border-radius-default: 4px; - /* CSS HEX */ :root { --mri-theme: #{$mri-theme}; @@ -103,7 +116,8 @@ $border-radius-default: 4px; --pet-theme: #{$pet-theme}; --ieeg-theme: #{$ieeg-theme}; --meg-theme: #{$meg-theme}; - + --nirs-theme: #{$nirs-theme}; + --nih-theme: #{$nih-theme}; --on-light-aqua: #{$on-light-aqua}; --on-dark-aqua: #{$on-dark-aqua}; --on-light-green: #{on-light-green}; @@ -112,12 +126,15 @@ $border-radius-default: 4px; --current-theme-primary-hover: #{$on-light-aqua}; --current-theme-secondary: #{$on-secondary}; --current-theme-primary-dark: #{$on-dark-aqua}; - --current-theme-primary-light: #{$on-dark-aqua-light}; + --current-theme-primary-light: #{$on-light}; + --current-theme-primary-ultralight: #{$on-ultralight}; --current-theme-header: #333; --current-theme-header-dark: #333; --font-sans: #{$font-sans}; --border-radius-default: #{$border-radius-default}; --newspaper: #{$newspaper}; + --cloud: #{$cloud}; + --text-citation: #{$text-cite}; } .mri-theme { @@ -148,11 +165,18 @@ $border-radius-default: 4px; background-color: $meg-theme; } +.nih-theme { + background: $nih-theme; +} + +.nirs-theme { + background: $nirs-theme; +} + .gray-bg { background: #f3f3f3; } - .search-page-mri, .dataset-page-mri { --current-theme-primary: #{$mri-theme}; @@ -162,6 +186,7 @@ $border-radius-default: 4px; --current-theme-primary-light: #{$mri-light}; --current-theme-header: #{$mri-theme}; --current-theme-header-dark: #{$mri-dark}; + --current-theme-primary-ultralight: #{$mri-ultralight}; } .search-page-eeg, @@ -173,6 +198,7 @@ $border-radius-default: 4px; --current-theme-primary-light: #{$eeg-light}; --current-theme-header: #{$eeg-theme}; --current-theme-header-dark: #{$eeg-dark}; + --current-theme-primary-ultralight: #{$eeg-ultralight}; } .search-page-pet, @@ -184,6 +210,7 @@ $border-radius-default: 4px; --current-theme-primary-light: #{$pet-light}; --current-theme-header: #{$pet-theme}; --current-theme-header-dark: #{$pet-dark}; + --current-theme-primary-ultralight: #{$pet-ultralight}; } .search-page-ieeg, @@ -195,6 +222,7 @@ $border-radius-default: 4px; --current-theme-primary-light: #{$ieeg-light}; --current-theme-header: #{$ieeg-theme}; --current-theme-header-dark: #{$ieeg-dark}; + --current-theme-primary-ultralight: #{$ieeg-ultralight}; } .search-page-meg, @@ -206,4 +234,28 @@ $border-radius-default: 4px; --current-theme-primary-light: #{$meg-light}; --current-theme-header: #{$meg-theme}; --current-theme-header-dark: #{$meg-dark}; + --current-theme-primary-ultralight: #{$meg-ultralight}; +} + +.search-page-nirs, +.dataset-page-nirs { + --current-theme-primary: #{$nirs-theme}; + --current-theme-primary-hover: #{$nirs-hover}; + --current-theme-primary-dark: #{$nirs-dark}; + --current-theme-secondary: #{$nirs-secondary}; + --current-theme-primary-light: #{$nirs-light}; + --current-theme-header: #{$nirs-theme}; + --current-theme-header-dark: #{$nirs-dark}; + --current-theme-primary-ultralight: #{$nirs-ultralight}; +} + +.search-page-nih, +.dataset-page-nih { + --current-theme-primary: #{$nih-theme}; + --current-theme-primary-hover: #{$nih-hover}; + --current-theme-primary-dark: #{$nih-dark}; + --current-theme-secondary: #{$nih-secondary}; + --current-theme-primary-light: #{$nih-light}; + --current-theme-header: #{$nih-theme}; + --current-theme-header-dark: #{$nih-dark}; } diff --git a/packages/openneuro-app/src/scripts/components/search-page/__tests__/NuerobagelSearch.spec.tsx b/packages/openneuro-app/src/scripts/search/__tests__/NuerobagelSearch.spec.tsx similarity index 89% rename from packages/openneuro-app/src/scripts/components/search-page/__tests__/NuerobagelSearch.spec.tsx rename to packages/openneuro-app/src/scripts/search/__tests__/NuerobagelSearch.spec.tsx index 34bc393244..484fd2d9a1 100644 --- a/packages/openneuro-app/src/scripts/components/search-page/__tests__/NuerobagelSearch.spec.tsx +++ b/packages/openneuro-app/src/scripts/search/__tests__/NuerobagelSearch.spec.tsx @@ -1,6 +1,6 @@ import React from "react" import { fireEvent, render, screen } from "@testing-library/react" -import { NeurobagelSearch } from "../NeurobagelSearch" +import { NeurobagelSearch } from "../components/NeurobagelSearch" describe("NeurobagelSearch component", () => { it("? toggle can be clicked", async () => { diff --git a/packages/openneuro-app/src/scripts/components/search-page/CommunityHeader.tsx b/packages/openneuro-app/src/scripts/search/components/CommunityHeader.tsx similarity index 100% rename from packages/openneuro-app/src/scripts/components/search-page/CommunityHeader.tsx rename to packages/openneuro-app/src/scripts/search/components/CommunityHeader.tsx diff --git a/packages/openneuro-app/src/scripts/search/components/DatasetsRadioTabs.tsx b/packages/openneuro-app/src/scripts/search/components/DatasetsRadioTabs.tsx new file mode 100644 index 0000000000..10041187da --- /dev/null +++ b/packages/openneuro-app/src/scripts/search/components/DatasetsRadioTabs.tsx @@ -0,0 +1,103 @@ +import React, { useCallback, useContext } from "react" +import type { FC } from "react" +import { SearchParamsCtx } from "../search-params-ctx" +import { useCookies } from "react-cookie" +import { getUnexpiredProfile } from "../../authentication/profile" +import { useUser } from "../../queries/user" +import SlidingRadioGroup from "../inputs/sliding-radio-group" + +export const DatasetsRadioTabs: FC = () => { + const [cookies] = useCookies() + const loggedOut = !getUnexpiredProfile(cookies) + const { user } = useUser() + const isAdmin = user?.admin + const { searchParams, setSearchParams } = useContext(SearchParamsCtx) + const { + datasetType_available, + datasetType_selected, + datasetStatus_available, + datasetStatus_selected, + searchAllDatasets, + } = searchParams + + const updatedDatasetTypeAvailable = [...datasetType_available] + if (isAdmin) { + const adminDatasetValue = "Admin: All Datasets" + const adminDatasetLabel = "Admin" // Define the new label + + // Check if an item with the 'Admin: All Datasets' value already exists + const alreadyHasAdminDataset = updatedDatasetTypeAvailable.some((item) => + (typeof item === "object" ? item.value : item) === adminDatasetValue + ) + + if (!alreadyHasAdminDataset) { + // Push it as an object with both value and the desired label + updatedDatasetTypeAvailable.push({ + value: adminDatasetValue, + label: adminDatasetLabel, + }) + } + } + + const setShowSelected = useCallback( + (newDatasetTypeSelected: string) => { + setSearchParams((prevState) => { + const newSearchAllDatasets = + newDatasetTypeSelected === "Admin: All Datasets" + + return { + ...prevState, + datasetType_selected: newDatasetTypeSelected, + searchAllDatasets: newSearchAllDatasets, + + datasetStatus_selected: newSearchAllDatasets + ? undefined + : newDatasetTypeSelected === "My Datasets" + ? prevState.datasetStatus_selected + : undefined, + } + }) + }, + [setSearchParams], + ) + + const setShowMyUploadsSelected = useCallback( + (newDatasetStatusSelected: string) => { + setSearchParams((prevState) => ({ + ...prevState, + datasetStatus_selected: newDatasetStatusSelected, + })) + }, + [setSearchParams], + ) + + if (loggedOut) { + return null + } + + return ( + <> + + + {datasetType_selected === "My Datasets" && !searchAllDatasets && ( + + )} + + ) +} diff --git a/packages/openneuro-app/src/scripts/components/search-page/FacetBlockContainerExample.tsx b/packages/openneuro-app/src/scripts/search/components/FacetBlockContainerExample.tsx similarity index 100% rename from packages/openneuro-app/src/scripts/components/search-page/FacetBlockContainerExample.tsx rename to packages/openneuro-app/src/scripts/search/components/FacetBlockContainerExample.tsx diff --git a/packages/openneuro-app/src/scripts/components/search-page/FilterDateItem.tsx b/packages/openneuro-app/src/scripts/search/components/FilterDateItem.tsx similarity index 100% rename from packages/openneuro-app/src/scripts/components/search-page/FilterDateItem.tsx rename to packages/openneuro-app/src/scripts/search/components/FilterDateItem.tsx diff --git a/packages/openneuro-app/src/scripts/components/search-page/FilterListItem.tsx b/packages/openneuro-app/src/scripts/search/components/FilterListItem.tsx similarity index 95% rename from packages/openneuro-app/src/scripts/components/search-page/FilterListItem.tsx rename to packages/openneuro-app/src/scripts/search/components/FilterListItem.tsx index 9ba61f8b5f..9b212ee988 100644 --- a/packages/openneuro-app/src/scripts/components/search-page/FilterListItem.tsx +++ b/packages/openneuro-app/src/scripts/search/components/FilterListItem.tsx @@ -1,5 +1,5 @@ import React from "react" -import { ModalityLabel } from "../formatting/modality-label" +import { ModalityLabel } from "../../components/formatting/modality-label" type TextList = string[] type Text = string diff --git a/packages/openneuro-app/src/scripts/components/search-page/FiltersBlock.tsx b/packages/openneuro-app/src/scripts/search/components/FiltersBlock.tsx similarity index 96% rename from packages/openneuro-app/src/scripts/components/search-page/FiltersBlock.tsx rename to packages/openneuro-app/src/scripts/search/components/FiltersBlock.tsx index ef18a984eb..30df59fc90 100644 --- a/packages/openneuro-app/src/scripts/components/search-page/FiltersBlock.tsx +++ b/packages/openneuro-app/src/scripts/search/components/FiltersBlock.tsx @@ -3,7 +3,8 @@ import { Button } from "../../components/button/Button" import { FilterListItem } from "./FilterListItem" import { TermListItem } from "./TermListItem" import type { FacetSelectValueType } from "../../components/facets/FacetSelect" -import "./filters-block.scss" +import "../scss/filters-block.scss" +import { modalityShortMapping } from "../../components/formatting/modality-label" export interface FiltersBlockProps { keywords: string[] @@ -69,17 +70,13 @@ export const FiltersBlock = ({ const subjectCountRangeIsNull = JSON.stringify(subjectCountRange) === JSON.stringify([null, null]) + const labelText = modalityShortMapping(modality_selected) + return (

{noFilters - ? ( - - Showing all available {modality_selected ? modality_selected : ""} - {" "} - datasets - - ) + ? Showing all available {labelText ? labelText : ""} datasets : ( <> {loading diff --git a/packages/openneuro-app/src/scripts/search/components/MetaListItemList.tsx b/packages/openneuro-app/src/scripts/search/components/MetaListItemList.tsx new file mode 100644 index 0000000000..318e01b7f6 --- /dev/null +++ b/packages/openneuro-app/src/scripts/search/components/MetaListItemList.tsx @@ -0,0 +1,31 @@ +import React from "react" +import type { FC, ReactNode } from "react" + +interface MetaListItemListProps { + typeLabel: ReactNode + items: (string | ReactNode)[] +} + +/** + * A reusable component for rendering a list of meta items in the search results details. + */ +export const MetaListItemList: FC = ( + { typeLabel, items }, +) => { + if (!items || items.length === 0) { + return null + } + + return ( +
+ +
+ {items.map((item, index) => ( + + {item} + + ))} +
+
+ ) +} diff --git a/packages/openneuro-app/src/scripts/components/search-page/ModalityHeader.tsx b/packages/openneuro-app/src/scripts/search/components/ModalityHeader.tsx similarity index 100% rename from packages/openneuro-app/src/scripts/components/search-page/ModalityHeader.tsx rename to packages/openneuro-app/src/scripts/search/components/ModalityHeader.tsx diff --git a/packages/openneuro-app/src/scripts/components/search-page/NeurobagelSearch.tsx b/packages/openneuro-app/src/scripts/search/components/NeurobagelSearch.tsx similarity index 100% rename from packages/openneuro-app/src/scripts/components/search-page/NeurobagelSearch.tsx rename to packages/openneuro-app/src/scripts/search/components/NeurobagelSearch.tsx diff --git a/packages/openneuro-app/src/scripts/components/search-page/SearchPage.tsx b/packages/openneuro-app/src/scripts/search/components/SearchPage.tsx similarity index 87% rename from packages/openneuro-app/src/scripts/components/search-page/SearchPage.tsx rename to packages/openneuro-app/src/scripts/search/components/SearchPage.tsx index 5c94f60038..2ffe92f147 100644 --- a/packages/openneuro-app/src/scripts/components/search-page/SearchPage.tsx +++ b/packages/openneuro-app/src/scripts/search/components/SearchPage.tsx @@ -1,7 +1,7 @@ import React from "react" import { ModalityHeader } from "./ModalityHeader" import { CommunityHeader } from "./CommunityHeader" -import "./search-page.scss" +import "../scss/search-page.scss" export interface PortalContent { className?: string @@ -15,6 +15,7 @@ export interface PortalContent { } export interface SearchPageProps { + hasDetailsOpen?: boolean portalContent?: PortalContent renderSearchFacets: () => React.ReactNode renderSearchResultsList: () => React.ReactNode @@ -23,15 +24,18 @@ export interface SearchPageProps { renderSearchHeader: () => React.ReactNode renderLoading: () => React.ReactNode renderAggregateCounts: () => React.ReactNode + renderItemDetails: () => React.ReactNode } export const SearchPage = ({ + hasDetailsOpen, portalContent, renderSearchFacets, renderSearchResultsList, renderSortBy, renderFilterBlock, renderSearchHeader, + renderItemDetails, renderLoading, renderAggregateCounts, }: SearchPageProps) => { @@ -69,18 +73,14 @@ export const SearchPage = ({ ) : null} -
+
-
-

{renderSearchHeader()}

-
-
{renderSearchFacets()}
-
+
+
{renderSearchHeader()}
{renderLoading()}
{renderFilterBlock()}
{renderSortBy()}
+ {renderSearchResultsList()}
+ {renderItemDetails()}
diff --git a/packages/openneuro-app/src/scripts/search/components/SearchResultDetails.tsx b/packages/openneuro-app/src/scripts/search/components/SearchResultDetails.tsx new file mode 100644 index 0000000000..e4d538093e --- /dev/null +++ b/packages/openneuro-app/src/scripts/search/components/SearchResultDetails.tsx @@ -0,0 +1,167 @@ +import React, { useEffect, useRef } from "react" +import type { FC, ReactNode } from "react" +import bytes from "bytes" +import parseISO from "date-fns/parseISO" +import formatDistanceToNow from "date-fns/formatDistanceToNow" +import { Link } from "react-router-dom" +import type { SearchResultItemProps } from "./SearchResultItem" +import { ModalityLabel } from "../../components/formatting/modality-label" +import { MetaListItemList } from "./MetaListItemList" +import "../scss/search-result-details.scss" + +interface SearchResultDetailsProps { + itemData: SearchResultItemProps["node"] | null + onClose: () => void +} + +export const SearchResultDetails: FC = ( + { itemData, onClose }, +) => { + const closeButtonRef = useRef(null) + + useEffect(() => { + if (itemData && closeButtonRef.current) { + closeButtonRef.current.focus() + } + }, [itemData]) + + if (!itemData) { + return null + } + + const formatDate = (dateString: string): string => { + if (!dateString) return "N/A" + const date = new Date(dateString) + return date.toISOString().split("T")[0] + } + + const summary = itemData.latestSnapshot?.summary + const numSessions = summary?.sessions?.length > 0 + ? summary.sessions.length + : 1 + const numSubjects = summary?.subjects?.length > 0 + ? summary.subjects.length + : 1 + + // Header for more details + const moreDetailsHeader = ( +

+ + {itemData.latestSnapshot?.description?.Name || itemData.id} + +

+ ) + + // Lists + const modalityList = summary?.modalities?.length + ? ( +
+ {summary?.modalities.length === 1 ? "Modality" : "Modalities"} + } + items={summary?.modalities.map((modality) => ( + + ))} + /> +
+ ) + : null + + const taskList = summary?.tasks?.length + ? ( +
+ Tasks} items={summary?.tasks} /> +
+ ) + : null + + const tracers = summary?.pet?.TracerName?.length + ? ( +
+ + {summary?.pet?.TracerName.length === 1 + ? "Radiotracer" + : "Radiotracers"} + + } + items={summary?.pet?.TracerName} + /> +
+ ) + : null + + // function for consistent meta item rendering + const renderMetaItem = ( + label: string | ReactNode, + content: ReactNode, + ): JSX.Element => ( +
+ + {content} +
+ ) + + const sessions = renderMetaItem("Sessions", numSessions.toLocaleString()) + const subjects = renderMetaItem("Participants", numSubjects.toLocaleString()) + const size = renderMetaItem( + "Size", + bytes(itemData?.latestSnapshot?.size) || "unknown", + ) + const files = renderMetaItem("Files", summary?.totalFiles?.toLocaleString()) + const lastUpdatedDisplay = renderMetaItem( + "Last Updated", +
+ {formatDate( + itemData.snapshots?.[itemData.snapshots.length - 1]?.created || + itemData.created, + )} +
, + ) + const accessionNumberDisplay = renderMetaItem( + "Openneuro Accession Number", + + {itemData?.id} + , + ) + const authors = renderMetaItem( + "Authors", +
{itemData.latestSnapshot?.description?.Authors}
, + ) + const uploaderDisplay = renderMetaItem( + "Uploader by", +
+ {itemData.uploader?.name} on {formatDate(itemData?.created)} -{" "} + {formatDistanceToNow(parseISO(itemData?.created))} ago +
, + ) + + return ( +
+
+ + {moreDetailsHeader} + {authors} + {modalityList} + {taskList} + {accessionNumberDisplay} + {tracers} + {sessions} + {subjects} + {size} + {files} + {uploaderDisplay} + {lastUpdatedDisplay} +
+
+ ) +} diff --git a/packages/openneuro-app/src/scripts/components/search-page/SearchResultItem.tsx b/packages/openneuro-app/src/scripts/search/components/SearchResultItem.tsx similarity index 52% rename from packages/openneuro-app/src/scripts/components/search-page/SearchResultItem.tsx rename to packages/openneuro-app/src/scripts/search/components/SearchResultItem.tsx index 2dfcf8dc1e..e4de85fd00 100644 --- a/packages/openneuro-app/src/scripts/components/search-page/SearchResultItem.tsx +++ b/packages/openneuro-app/src/scripts/search/components/SearchResultItem.tsx @@ -1,21 +1,17 @@ import React from "react" -import bytes from "bytes" +import getYear from "date-fns/getYear" import parseISO from "date-fns/parseISO" -import formatDistanceToNow from "date-fns/formatDistanceToNow" import { Link } from "react-router-dom" import { Tooltip } from "../../components/tooltip/Tooltip" import { Icon } from "../../components/icon/Icon" import { useCookies } from "react-cookie" import { getProfile } from "../../authentication/profile" import { useUser } from "../../queries/user" -import "./search-result.scss" +import "../scss/search-result.scss" import activityPulseIcon from "../../../assets/activity-icon.png" -import { ModalityLabel } from "../../components/formatting/modality-label" import { hasEditPermissions } from "../../authentication/profile" -/** - * Return an equivalent to moment(date).format('L') without moment - * @param {*} dateObject - */ +import { ModalityHexagon } from "../../components/modality-cube/ModalityHexagon" + export const formatDate = (dateObject) => new Date(dateObject).toISOString().split("T")[0] @@ -50,6 +46,7 @@ export interface SearchResultItemProps { latestSnapshot: { id: string size: number + readme: string summary: { pet: { BodyPart: string @@ -58,6 +55,7 @@ export interface SearchResultItemProps { TracerName: string[] TracerRadionuclide: string } + primaryModality: string modalities: string[] sessions: [] subjects: string[] @@ -84,7 +82,9 @@ export interface SearchResultItemProps { warnings: number } description: { + Authors: string[] Name: string + DatasetDOI: string } } analytics: { @@ -112,11 +112,15 @@ export interface SearchResultItemProps { ] } datasetTypeSelected?: string + onClick: (itemId: string, event: React.MouseEvent) => void + isExpanded: boolean } export const SearchResultItem = ({ node, datasetTypeSelected, + onClick, + isExpanded, }: SearchResultItemProps) => { const { user } = useUser() const [cookies] = useCookies() @@ -125,96 +129,10 @@ export const SearchResultItem = ({ const isAdmin = user?.admin const hasEdit = hasEditPermissions(node.permissions, profileSub) || isAdmin - - const heading = node.latestSnapshot.description?.Name - ? node.latestSnapshot.description?.Name - : node.id - const summary = node.latestSnapshot?.summary const datasetId = node.id - const numSessions = summary?.sessions.length > 0 ? summary.sessions.length : 1 - const numSubjects = summary?.subjects.length > 0 ? summary.subjects.length : 1 - const accessionNumber = ( - - Openneuro Accession Number: - {node.id} - - ) - const sessions = ( - - Sessions: - {numSessions.toLocaleString()} - - ) - - const ages = (value) => { - if (value) { - const ages = value.filter((x) => x) - if (ages.length === 0) return "N/A" - else if (ages.length === 1) return ages[0] - else return `${Math.min(...ages)} - ${Math.max(...ages)}` - } else return "N/A" - } - const agesRange = ( - - - {node?.metadata?.ages?.length === 1 - ? "Participant's Age" - : "Participants' Ages"} - :{" "} - - - {ages(summary?.subjectMetadata?.map((subject) => subject.age))} - - - ) - const subjects = ( - - Participants: - {numSubjects.toLocaleString()} - - ) - const size = ( - - Size: - {bytes(node?.latestSnapshot?.size) || "unknown"} - - ) - const files = ( - - Files: - {summary?.totalFiles.toLocaleString()} - - ) + const heading = node.latestSnapshot.description?.Name?.trim() || datasetId - const dateAdded = formatDate(node.created) - const dateAddedDifference = formatDistanceToNow(parseISO(node.created)) - let lastUpdatedDate - if (node.snapshots.length) { - const dateUpdated = formatDate( - node.snapshots[node.snapshots.length - 1].created, - ) - const dateUpdatedDifference = formatDistanceToNow( - parseISO(node.snapshots[node.snapshots.length - 1].created), - ) - - lastUpdatedDate = ( - <> - | -
- Updated: - {dateUpdated} - {dateUpdatedDifference} ago -
- - ) - } - - const uploader = ( -
- Uploaded by: - {node.uploader?.name} on {dateAdded} - {dateAddedDifference} ago -
- ) const downloads = node.analytics.downloads ? node.analytics.downloads.toLocaleString() + " Downloads \n" : "" @@ -292,33 +210,12 @@ export const SearchResultItem = ({ ) - const _list = (type, items) => { - if (items && items.length > 0) { - return ( - <> - {type}: -
- {items.map((item, index) => ( - - {item} - - ))} -
- - ) - } else { - return null - } - } - let invalid = false - // Legacy issues still flagged if (node.latestSnapshot.issues) { - invalid = node.latestSnapshot.issues.some((issue) => - issue.severity === "error" + invalid = node.latestSnapshot.issues.some( + (issue) => issue.severity === "error", ) } else { - // Test if there's any schema validator errors invalid = node.latestSnapshot.validation?.errors > 0 } const shared = !node.public && node.uploader?.id !== profileSub @@ -346,74 +243,61 @@ export const SearchResultItem = ({
) - const modalityList = summary?.modalities.length - ? ( -
- {_list( - <>{summary?.modalities.length === 1 ? "Modality" : "Modalities"}, - summary?.modalities.map((modality) => ( - - )), - )} -
- ) - : null - const taskList = summary?.tasks.length - ?
{_list(<>Tasks, summary?.tasks)}
- : null - - const tracers = summary?.pet?.TracerName?.length - ? ( -
- {_list( - <> - {summary?.pet?.TracerName.length === 1 - ? "Radiotracer" - : "Radiotracers"} - , - summary?.pet?.TracerName, - )} -
- ) - : null + const year = getYear(parseISO(node.created)) + const authors = node.latestSnapshot.description?.Authors + ? node.latestSnapshot.description.Authors.join(" and ") + : "NO AUTHORS FOUND" + const datasetCite = + `${authors} (${year}). ${node.latestSnapshot.description.Name}. OpenNeuro. [Dataset] doi: ${node.latestSnapshot.description.DatasetDOI}` + const trimlength = 450 return ( <> -
+
-

- {heading} -

-
- {uploader} - {lastUpdatedDate} +
+

+ {heading} +

+

+ {node.latestSnapshot?.readme + ? (node.latestSnapshot.readme.length > trimlength + ? `${node.latestSnapshot.readme.substring(0, trimlength)}...` + : node.latestSnapshot.readme) + : ""} +

+ {datasetCite}
-
+
+
+ {datasetOwenerIcons} + {activityIcon} + +
{MyDatasetsPage && ( -
+
Access: {datasetPerms}
)} -
- {datasetOwenerIcons} - {activityIcon} +
+
-
- {modalityList} - {taskList} - {tracers} -
-
- {accessionNumber} - {sessions} - {subjects} - {agesRange} - {size} - {files} -
) diff --git a/packages/openneuro-app/src/scripts/search/components/SearchResultsList.tsx b/packages/openneuro-app/src/scripts/search/components/SearchResultsList.tsx new file mode 100644 index 0000000000..8b41925e2a --- /dev/null +++ b/packages/openneuro-app/src/scripts/search/components/SearchResultsList.tsx @@ -0,0 +1,45 @@ +import React from "react" +import { SearchResultItem } from "./SearchResultItem" +import "../scss/search-page.scss" +import type { SearchResultItemProps } from "./SearchResultItem" + +export interface SearchResultsListProps { + items: { node: SearchResultItemProps["node"] }[] + datasetTypeSelected: string + clickedItemData: SearchResultItemProps["node"] | null + handleItemClick: ( + itemId: string, + event: React.MouseEvent, + ) => void +} + +export const SearchResultsList = ({ + items, + datasetTypeSelected, + clickedItemData, + handleItemClick, +}: SearchResultsListProps) => { + return ( + <> +
+ {items.map((data) => { + if (data) { + const isActive = clickedItemData?.id === data.node.id + return ( + + ) + } + return null + })} +
+ + ) +} diff --git a/packages/openneuro-app/src/scripts/components/search-page/SearchSort.tsx b/packages/openneuro-app/src/scripts/search/components/SearchSort.tsx similarity index 92% rename from packages/openneuro-app/src/scripts/components/search-page/SearchSort.tsx rename to packages/openneuro-app/src/scripts/search/components/SearchSort.tsx index f72a259f7e..79d1054385 100644 --- a/packages/openneuro-app/src/scripts/components/search-page/SearchSort.tsx +++ b/packages/openneuro-app/src/scripts/search/components/SearchSort.tsx @@ -1,6 +1,6 @@ import React from "react" -import { Dropdown } from "../dropdown/Dropdown" -import "./search-sort.scss" +import { Dropdown } from "../../components/dropdown/Dropdown" +import "../scss/search-sort.scss" export interface SearchSortProps { items: { diff --git a/packages/openneuro-app/src/scripts/components/search-page/SearchSortContainerExample.tsx b/packages/openneuro-app/src/scripts/search/components/SearchSortContainerExample.tsx similarity index 100% rename from packages/openneuro-app/src/scripts/components/search-page/SearchSortContainerExample.tsx rename to packages/openneuro-app/src/scripts/search/components/SearchSortContainerExample.tsx diff --git a/packages/openneuro-app/src/scripts/components/search-page/TermListItem.tsx b/packages/openneuro-app/src/scripts/search/components/TermListItem.tsx similarity index 100% rename from packages/openneuro-app/src/scripts/components/search-page/TermListItem.tsx rename to packages/openneuro-app/src/scripts/search/components/TermListItem.tsx diff --git a/packages/openneuro-app/src/scripts/components/search-page/neurobagel_logo.svg b/packages/openneuro-app/src/scripts/search/components/neurobagel_logo.svg similarity index 100% rename from packages/openneuro-app/src/scripts/components/search-page/neurobagel_logo.svg rename to packages/openneuro-app/src/scripts/search/components/neurobagel_logo.svg diff --git a/packages/openneuro-app/src/scripts/search/filters-block-container.tsx b/packages/openneuro-app/src/scripts/search/filters-block-container.tsx index 32aed935d6..8496dc7dd7 100644 --- a/packages/openneuro-app/src/scripts/search/filters-block-container.tsx +++ b/packages/openneuro-app/src/scripts/search/filters-block-container.tsx @@ -13,7 +13,7 @@ import { SearchParamsCtx, useCheckIfParamsAreSelected, } from "./search-params-ctx" -import { FiltersBlock } from "../components/search-page/FiltersBlock" +import { FiltersBlock } from "./components/FiltersBlock" import initialSearchParams from "./initial-search-params" interface FiltersBlockContainerProps { diff --git a/packages/openneuro-app/src/scripts/search/inputs/admin-allDatasets-toggle.tsx b/packages/openneuro-app/src/scripts/search/inputs/admin-allDatasets-toggle.tsx deleted file mode 100644 index 5e55c08925..0000000000 --- a/packages/openneuro-app/src/scripts/search/inputs/admin-allDatasets-toggle.tsx +++ /dev/null @@ -1,47 +0,0 @@ -import React, { useContext } from "react" -import type { FC } from "react" -import { SearchParamsCtx } from "../search-params-ctx" -import { Button } from "../../components/button/Button" -import styled from "@emotion/styled" - -const StyledButton = styled(Button)` - width: auto; - padding: 6px 10px; - margin-bottom: 20px; - justify-content: flex-start; - &.active { - background: #fff5f3; - border-color: #e68383; - color: #710000; - i { - color: #710000; - } - } -` - -const TaskInput: FC = () => { - const { - searchParams: { searchAllDatasets }, - setSearchParams, - } = useContext(SearchParamsCtx) - - const toggleSearchAllDatasets = () => - setSearchParams((prevState) => ({ - ...prevState, - searchAllDatasets: !prevState.searchAllDatasets, - })) - - return ( - - - ) -} - -export default TaskInput diff --git a/packages/openneuro-app/src/scripts/search/inputs/index.ts b/packages/openneuro-app/src/scripts/search/inputs/index.ts index 645b4434f7..4cc2fcbb28 100644 --- a/packages/openneuro-app/src/scripts/search/inputs/index.ts +++ b/packages/openneuro-app/src/scripts/search/inputs/index.ts @@ -1,8 +1,6 @@ import KeywordInput from "./keyword-input" -import AllDatasetsToggle from "./admin-allDatasets-toggle" import ModalitySelect from "./modality-select" import InitiativeSelect from "./initiative-select" -import ShowDatasetRadios from "./show-datasets-radios" import AgeRangeInput from "./age-range-input" import SubjectCountRangeInput from "./subject-count-range-input" import DatasetTypeSelect from "./dataset-type-select" @@ -23,7 +21,6 @@ import SortBySelect from "./sort-by-select" export { AgeRangeInput, - AllDatasetsToggle, AuthorInput, BodyPartsInput, DatasetTypeSelect, @@ -36,7 +33,6 @@ export { ScannerManufacturersModelNames, SectionSelect, SexRadios, - ShowDatasetRadios, SortBySelect, SpeciesSelect, StudyDomainInput, diff --git a/packages/openneuro-app/src/scripts/search/inputs/show-datasets-radios.tsx b/packages/openneuro-app/src/scripts/search/inputs/show-datasets-radios.tsx deleted file mode 100644 index 0eb9ba6a69..0000000000 --- a/packages/openneuro-app/src/scripts/search/inputs/show-datasets-radios.tsx +++ /dev/null @@ -1,74 +0,0 @@ -import React, { useContext } from "react" -import type { FC } from "react" -import { SearchParamsCtx } from "../search-params-ctx" -import { RadioGroup } from "../../components/radio/RadioGroup" -import { FacetSelect } from "../../components/facets/FacetSelect" -import { useCookies } from "react-cookie" -import { getUnexpiredProfile } from "../../authentication/profile" -import { AccordionTab } from "../../components/accordion/AccordionTab" -import { AccordionWrap } from "../../components/accordion/AccordionWrap" - -const ShowDatasetsRadios: FC = () => { - const [cookies] = useCookies() - const loggedOut = !getUnexpiredProfile(cookies) - - const { searchParams, setSearchParams } = useContext(SearchParamsCtx) - - const { - datasetType_available, - datasetType_selected, - datasetStatus_available, - datasetStatus_selected, - } = searchParams - - const setShowSelected = (datasetType_selected) => { - setSearchParams((prevState) => ({ - ...prevState, - datasetType_selected, - datasetStatus_selected: datasetType_selected === "My Datasets" - ? prevState.datasetStatus_selected - : undefined, - })) - } - - const setShowMyUploadsSelected = (datasetStatus_selected) => { - setSearchParams((prevState) => ({ - ...prevState, - datasetStatus_selected, - })) - } - - return loggedOut ? null : ( - <> -
- -
- {datasetType_selected === "My Datasets" && ( - - - - - - )} - - ) -} - -export default ShowDatasetsRadios diff --git a/packages/openneuro-app/src/scripts/search/inputs/sliding-radio-group.tsx b/packages/openneuro-app/src/scripts/search/inputs/sliding-radio-group.tsx new file mode 100644 index 0000000000..8dc944960e --- /dev/null +++ b/packages/openneuro-app/src/scripts/search/inputs/sliding-radio-group.tsx @@ -0,0 +1,127 @@ +import React, { useCallback, useEffect, useRef, useState } from "react" +import type { FC } from "react" +import "../scss/sliding-radio-group.scss" + +interface RadioItem { + value: string + label: string +} + +interface SlidingRadioGroupProps { + items: (string | RadioItem)[] + selected: string | undefined + setSelected: (value: string) => void + groupName: string + className?: string + initialSelectedValueOverride?: string +} + +const SlidingRadioGroup: FC = ({ + items, + selected, + setSelected, + groupName, + className = "", + initialSelectedValueOverride, +}) => { + const containerRef = useRef(null) + const radioLabelRefs = useRef<{ [key: string]: HTMLLabelElement }>({}) + const [sliderStyles, setSliderStyles] = useState({ + left: 0, + width: 0, + height: 0, + top: 0, + }) + + const calculateSliderPosition = useCallback((targetValue: string) => { + if (!containerRef.current || !radioLabelRefs.current[targetValue]) { + return { left: 0, width: 0, height: 0, top: 0 } + } + + const targetLabel = radioLabelRefs.current[targetValue] + const containerRect = containerRef.current.getBoundingClientRect() + const labelRect = targetLabel.getBoundingClientRect() + + return { + left: labelRect.left - containerRect.left, + width: labelRect.width, + height: labelRect.height, + top: labelRect.top - containerRect.top, + } + }, []) + + useEffect(() => { + const updateSlider = () => { + const valueToCalculate = initialSelectedValueOverride || selected + + if (valueToCalculate) { + const newStyles = calculateSliderPosition(valueToCalculate) + setSliderStyles(newStyles) + } else { + // If nothing is selected, hide the slider + setSliderStyles({ left: 0, width: 0, height: 0, top: 0 }) + } + } + + const timeoutId = setTimeout(updateSlider, 50) + window.addEventListener("resize", updateSlider) + + return () => { + clearTimeout(timeoutId) + window.removeEventListener("resize", updateSlider) + } + }, [selected, calculateSliderPosition, initialSelectedValueOverride]) + + const isSliderVisible = sliderStyles.width > 0 && sliderStyles.height > 0 + + return ( +
+ {/* The sliding highlight element */} +
+ +
+ {items.map((item) => { + const value = typeof item === "object" ? item.value : item + const label = typeof item === "object" ? item.label : item + const isChecked = selected === value + + return ( +
+ setSelected(value)} + /> + +
+ ) + })} +
+
+ ) +} + +export default SlidingRadioGroup diff --git a/packages/openneuro-app/src/scripts/search/inputs/sort-by-select.tsx b/packages/openneuro-app/src/scripts/search/inputs/sort-by-select.tsx index 0c84bab378..7473f7cd6c 100644 --- a/packages/openneuro-app/src/scripts/search/inputs/sort-by-select.tsx +++ b/packages/openneuro-app/src/scripts/search/inputs/sort-by-select.tsx @@ -1,7 +1,7 @@ import React, { useContext } from "react" import type { FC } from "react" import { SearchParamsCtx } from "../search-params-ctx" -import { SearchSort } from "../../components/search-page/SearchSort" +import { SearchSort } from "../components/SearchSort" interface SortBySelectProps { /* eslint-disable-next-line @typescript-eslint/no-explicit-any */ diff --git a/packages/openneuro-app/src/scripts/components/search-page/filters-block.scss b/packages/openneuro-app/src/scripts/search/scss/filters-block.scss similarity index 98% rename from packages/openneuro-app/src/scripts/components/search-page/filters-block.scss rename to packages/openneuro-app/src/scripts/search/scss/filters-block.scss index 8a5ec1eb7f..70e79c8439 100644 --- a/packages/openneuro-app/src/scripts/components/search-page/filters-block.scss +++ b/packages/openneuro-app/src/scripts/search/scss/filters-block.scss @@ -1,4 +1,4 @@ -@import '../scss/variables'; +@import '../../scss/variables'; .filters-block { position: relative; diff --git a/packages/openneuro-app/src/scripts/components/search-page/search-page.scss b/packages/openneuro-app/src/scripts/search/scss/search-page.scss similarity index 74% rename from packages/openneuro-app/src/scripts/components/search-page/search-page.scss rename to packages/openneuro-app/src/scripts/search/scss/search-page.scss index c75ee308ca..5a551f469d 100644 --- a/packages/openneuro-app/src/scripts/components/search-page/search-page.scss +++ b/packages/openneuro-app/src/scripts/search/scss/search-page.scss @@ -1,9 +1,29 @@ -@import '../scss/variables'; +@import '../../scss/variables'; + +// Common styles for portal headers (can be extended) +.search-page-portal-header, +.search-page-coms { + .primary-content { + font-weight: 300; + } + .secondary-content { + font-weight: 300; + margin-top: 25px; + display: flex; + } +} .search-page-portal-header { position: relative; color: #fff; z-index: 1100; + background-color: var(--current-theme-primary); + background: linear-gradient( + 16deg, + var(--current-theme-primary-dark) 0%, + var(--current-theme-primary) 70% + ); + @media (max-width: 980px) { z-index: 3; } @@ -19,6 +39,7 @@ display: none; } } + @media (max-width: 767px) { .portal-primary { width: 100%; @@ -27,12 +48,8 @@ } } - .primary-content, - .secondary-content { - font-weight: 300; - font-size: 24px; - } .primary-content { + font-size: 24px; a { color: #fff; } @@ -41,8 +58,7 @@ } } .secondary-content { - margin: 25px 0 50px; - display: flex; + margin: 25px 0 50px; // This margin is specific to portal-header @media (max-width: 450px) { flex-direction: column; font-size: 17px; @@ -56,31 +72,19 @@ } } -.search-page-portal-header { - background-color: var(--current-theme-primary); - background: linear-gradient( - 16deg, - var(--current-theme-primary-dark) 0%, - var(--current-theme-primary) 70% - ); -} - .search-page-coms { - position: relative; background-color: #f3f7f6; padding: 20px 0 40px; + position: relative; // Only defined here for .search-page-coms + h2 { font-size: 19px; } .primary-content { - font-weight: 300; font-size: 19px; } .secondary-content { - font-weight: 300; font-size: 15px; - margin-top: 25px; - display: flex; } } @@ -89,7 +93,6 @@ width: 250px; height: 144.34px; margin: 72.17px 0; - background-size: auto 288.6751px; background-position: center; @@ -104,8 +107,6 @@ background: inherit; left: 36.61px; - /*counter transform the bg image on the caps*/ - &:after, &:after { content: ''; position: absolute; @@ -125,7 +126,6 @@ .hexBottom { bottom: -88.3883px; - &:after { background-position: center bottom; } @@ -163,13 +163,13 @@ width: 100%; color: #000; font-weight: bold; + &.front { transform: translateZ(69px); display: flex; flex-direction: column; justify-content: center; align-items: center; - background-color: rgba(0, 0, 0, 0.32); } &.top { @@ -184,9 +184,6 @@ } } -.search { - padding: 0 0 50px; -} .search-heading { margin: 20px 0; @media (max-width: 450px) { @@ -202,40 +199,48 @@ font-size: 14px; } } + .search-wrapper { + box-sizing: border-box; + flex: 0 0 auto; + flex-grow: 1; + flex-basis: 100%; + max-width: 100%; + min-width: 0; display: flex; + position: relative; + padding: 20px 35px; } .close-filters-btn, .show-filters-btn { - display: none; -} + display: none; // Hidden by default -@media (max-width: 980px) { - .close-filters-btn, - .show-filters-btn { - display: block; + @media (max-width: 980px) { + display: block; // Show on smaller screens position: absolute; - top: 40px; + top: 20px; right: 0; background: none; border: 0; font-size: 12px; margin: 10px; padding: 0; + z-index: 1100; + cursor: pointer; } -} -@media (max-width: 450px) { - .close-filters-btn, - .show-filters-btn { + @media (max-width: 450px) { top: 0px; left: 0; margin: 10px 0 20px; border: 1px solid #ccc; padding: 10px; - display: block; } +} + +// Specific placement for close button on smaller screens +@media (max-width: 450px) { .close-filters-btn { left: 20px; } @@ -243,8 +248,9 @@ .search-nav { flex: 0 0 auto; - padding: 0; - width: 430px; + padding: 20px 30px; + width: 40%; + max-width: 450px; @media (max-width: 980px) { opacity: 0; @@ -261,13 +267,15 @@ min-height: 100%; overflow: scroll; position: fixed; - z-index: 1205; + z-index: 1205; // Higher z-index for fixed overlay top: 0; padding: 80px 25px; + &.show-mobile-filters { opacity: 1; transform: translateX(0); transition: opacity 1s; + max-width: 100vw; } } @@ -288,21 +296,47 @@ .search-content { flex: 1; - padding: 0 0 0 30px; + padding: 20px 50px 20px 20px; position: relative; - max-width: 100%; + max-width: calc(100% - 450px - 380px); + @media screen and (max-width: 1410px) { + max-width: 100%; + } @media (max-width: 980px) { - padding: 0; + padding: 0 10px; } .search-results { - border: 1px solid $newspaper; - border-radius: $border-radius-default; max-width: 100%; + .search-result { - border-bottom: 1px solid $newspaper; + border: 1px solid transparent; + &:nth-child(odd) { + background-color: var(--cloud); + } + &.expanded { + border-top-color: var(--current-theme-primary-light); + border-bottom-color: var(--current-theme-primary-light); + background-color: var(--current-theme-primary-ultralight); + } } } +} +.search-details { + flex-grow: 1; + max-width: 380px; + @media screen and (max-width: 1410px) { + position: fixed; + top: 0; + right: 0; + bottom: 0; + left: 0; + width: 100%; + max-width: 100%; + z-index: 5000; + padding: 20px; + overflow-y: auto; + } } .search-sort { @@ -323,42 +357,42 @@ flex-basis: 50%; } -.on-accordion-wrapper .keyword-accordion { - .accordion-title { - background: #e9e9e9; - width: 18px; - border-radius: 50%; - line-height: 0; - display: flex; - justify-content: center; - align-items: center; - height: 18px; - font-size: 12px; - align-content: space-around; - flex-direction: row; - font-weight: 600; - color: #515151; - font-family: monospace; - &:after { - display: none; +.on-accordion-wrapper { + .keyword-accordion { + .accordion-title { + background: #e9e9e9; + width: 18px; + border-radius: 50%; + line-height: 0; + display: flex; + justify-content: center; + align-items: center; + height: 18px; + font-size: 12px; + align-content: space-around; + flex-direction: row; + font-weight: 600; + color: #515151; + font-family: monospace; + &:after { + display: none; + } } - } - .accordion-content { - font-size: 12px; - margin: 10px 0; - .on-icon { - font-size: 10px; - padding: 1px 4px; - border-radius: $border-radius-default; - line-height: 1; - background-color: var(--current-theme-primary); - color: #fff; + .accordion-content { + font-size: 12px; + margin: 10px 0; + .on-icon { + font-size: 10px; + padding: 1px 4px; + border-radius: $border-radius-default; + line-height: 1; + background-color: var(--current-theme-primary); + color: #fff; + } } } -} - -.on-accordion-wrapper .keyword-accordion { - .accordion-title { + // Specific placement for keyword-accordion title + .keyword-accordion .accordion-title { position: absolute; top: -20px; left: 70px; @@ -388,13 +422,11 @@ cursor: pointer; align-items: center; justify-content: flex-start; - font-size: 14px; border-top: 1px solid $newspaper; padding: 10px; display: block; font-weight: 500; - font-weight: 500; &.selected-item { color: var(--current-theme-primary); } @@ -406,10 +438,12 @@ .search-facet-wrapper { .modality-facet.facet-accordion.on-accordion-wrapper { margin: 20px 0; - border: 0; - background: #e5f4f7; + border: 0; // Explicitly override border for mobile + background: var(--current-theme-primary-ultralight); + border: 1px solid var(--current-theme-primary-light); border-radius: $border-radius-default; padding: 10px; + .accordion-item.collapsed { max-height: initial; } @@ -421,7 +455,7 @@ color: var(--current-theme-primary); } .accordion-title { - margin: 10px 0 15px 0; + margin: 10px 0 15px 0; // Explicitly override margin for accordion title padding: 0; &:after { display: none; @@ -442,7 +476,6 @@ display: flex; align-items: center; justify-content: flex-start; - span { margin-left: 10px; display: inline-block; @@ -452,7 +485,6 @@ .level-1 > li { border-top: 1px solid $newspaper; - > .label { padding: 10px; font-weight: 500; @@ -465,7 +497,6 @@ > li { display: flex; justify-content: flex-start; - .label { margin-left: 15px; } diff --git a/packages/openneuro-app/src/scripts/search/scss/search-result-details.scss b/packages/openneuro-app/src/scripts/search/scss/search-result-details.scss new file mode 100644 index 0000000000..ea51094d17 --- /dev/null +++ b/packages/openneuro-app/src/scripts/search/scss/search-result-details.scss @@ -0,0 +1,70 @@ +@import '../../scss/variables'; + +.search-details { + border-left: 1px solid var(--current-theme-primary-light); + background-color: var(--current-theme-primary-ultralight); + padding: 20px; + font-size: 14px; + min-height: calc(100vh - 106px); + h4 { + font-size: 16px; + } + + .result-summary-meta { + margin-bottom: 10px; + + label { + font-weight: 600; + color: #000; + margin-bottom: 5px; + display: inline-block; + } + } + + .search-details-scroll { + position: sticky; + top: 10px; + align-self: flex-start; + width: 100%; + padding: 15px; + z-index: 100; + max-height: calc(100vh - 20px); + overflow-y: auto; + overflow-x: hidden; + + } + + .task-list, + .modality-list { + .list-item { + display: inline-block; + font-weight: 600; + padding: 3px 6px; + margin: 0 10px 5px 0; + color: var(--current-theme-secondary); + border: 1px solid; + border-radius: $border-radius-default; + } + } + + + + .close-details-button { + cursor: pointer; + position: absolute; + top: 0; + right: 0; + border: 0; + background-color: transparent; + color: #000; + font-size: 20px; + padding: 2px 5px; + margin-right: 10px; + &:hover { + font-weight: bold; + } + &:focus { + outline: -webkit-focus-ring-color auto 1px; + } + } +} \ No newline at end of file diff --git a/packages/openneuro-app/src/scripts/components/search-page/search-result.scss b/packages/openneuro-app/src/scripts/search/scss/search-result.scss similarity index 50% rename from packages/openneuro-app/src/scripts/components/search-page/search-result.scss rename to packages/openneuro-app/src/scripts/search/scss/search-result.scss index 5853d367fb..84a58b4bb3 100644 --- a/packages/openneuro-app/src/scripts/components/search-page/search-result.scss +++ b/packages/openneuro-app/src/scripts/search/scss/search-result.scss @@ -1,8 +1,9 @@ -@import '../scss/variables'; +@import '../../scss/variables'; .search-result { - padding: 20px; + padding: 30px 20px; h3 { margin: 0 0 5px; + font-size: 19px; a { color: var(--current-theme-primary); &:hover { @@ -10,11 +11,26 @@ } } } + cite { + color: var(--text-citation); + } + .hexagon-wrapper { + margin-left: 10px; + width: 28px; + height: 28px; + div.label { + font-size: 10px; + font-weight: normal; + } + } + .dataset-permissions-tag, .result-icon-wrap, .owner-icon-wrap { display: flex; justify-content: flex-end; + align-items: center; + padding: 0; @media (max-width: 480px) { justify-content: flex-start; margin-bottom: 10px; @@ -26,7 +42,6 @@ .result-activity-icon { width: 18px; height: 19px; - &[data-tooltip]::after { width: 130px; padding: 5px; @@ -36,65 +51,23 @@ } } - .result-meta-body > div { - font-size: 14px; - margin-bottom: 10px; - display: flex; - flex-direction: column; - - strong { - text-transform: uppercase; - color: #666; - margin-bottom: 5px; - } - .list-item { - display: inline-block; - - border-right: 1px solid #cacaca; - font-weight: 600; - padding: 3px 6px; - margin: 0 10px 5px 0; - color: var(--current-theme-secondary); - border: 1px solid; - border-radius: $border-radius-default; - } - } - - .result-meta-footer { - display: flex; - flex-wrap: wrap; - .result-summary-meta { - margin: 0 10px 10px 0; - padding: 0 40px 0 0; - font-size: 14px; - font-weight: 600; - &:last-child { - padding-right: 0; - } - strong { - text-transform: uppercase; - color: #666; - } - } - } - .updated-divider { margin: 0 10px; @media (max-width: 767px) { display: none; } } - .result-upload-info { - text-align: left; - font-size: 12px; - margin: 0 0 20px; - display: flex; - justify-content: flex-start; - @media (max-width: 767px) { - flex-direction: column; - } - span { - color: #666; + + .result-actions { + text-align: right; + padding: 0; + .on-button { + border: 1px solid var(--current-theme-secondary); + background-color: #fff; + &.expanded { + color: #fff; + background-color: var(--current-theme-secondary); + } } } } diff --git a/packages/openneuro-app/src/scripts/components/search-page/search-sort.scss b/packages/openneuro-app/src/scripts/search/scss/search-sort.scss similarity index 96% rename from packages/openneuro-app/src/scripts/components/search-page/search-sort.scss rename to packages/openneuro-app/src/scripts/search/scss/search-sort.scss index 5aea9dca64..db8cf37cc9 100644 --- a/packages/openneuro-app/src/scripts/components/search-page/search-sort.scss +++ b/packages/openneuro-app/src/scripts/search/scss/search-sort.scss @@ -1,4 +1,4 @@ -@import '../scss/variables'; +@import '../../scss/variables'; .search-sort-list-label { padding: 0 0 5px; diff --git a/packages/openneuro-app/src/scripts/search/scss/sliding-radio-group.scss b/packages/openneuro-app/src/scripts/search/scss/sliding-radio-group.scss new file mode 100644 index 0000000000..f1fddc1455 --- /dev/null +++ b/packages/openneuro-app/src/scripts/search/scss/sliding-radio-group.scss @@ -0,0 +1,115 @@ +@import '../../scss/variables'; + + + +// Root container for the general SlidingRadioGroup component +.sliding-radio-group-root { + *, + *::before, + *::after { + box-sizing: border-box; + } + &.btn-group-wrapper.facet-radio.show-dataset-radios-container { + position: relative; + margin: 0 0 10px; + background-color: #f6f6f6; + border: 1px solid #eee; + border-radius: 4px; + overflow: hidden; + min-height: 40px; + + display: flex; + flex-direction: row; + align-items: stretch; + justify-content: flex-start; + + &:after { + content: none; + display: none; + background: none; + position: static; + width: 0; + height: 0; + } + } + + // Styles for .dataset-filter-radio (individual radio button wrapper) + .dataset-filter-radio { + margin: 0; + + [type='radio']:checked, + [type='radio']:not(:checked) { + position: absolute; + left: -9999px; + } + + > label { + font-size: 13px; + line-height: 1.7em; + position: relative; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + height: 100%; + white-space: nowrap; + padding: 6px 12px; + border-right: 1px solid #ddd; + + &::before, + &::after { + display: none; + } + } + + &:last-child > label, + label.is-active { + border-right: none; + } + + input[type='radio']:focus-within + label { + box-shadow: inset 0 0 3px 1px #00a3ff; + outline: 0; + } + } + + // Styles for the inner group of radio buttons container + + .show-dataset-radios-group { + display: flex; + flex-direction: row; + justify-content: flex-start; + align-items: stretch; + z-index: 1; + position: relative; + width: 100%; + } + + .sliding-highlight { + position: absolute; + background-color: var(--current-theme-primary); + border-radius: 4px; + + transition: left 0.3s ease, width 0.3s ease, top 0.3s ease, height 0.3s ease, + opacity 0.3s ease; + z-index: 0; + opacity: 0; + } + + //Specific button states within this component (text color changes) + + .dataset-filter-radio { + > label { + color: var(--current-theme-primary); + background-color: transparent; + } + + [type='radio']:checked + label { + color: #fff; + } + + [type='radio']:not(:checked) + label:hover { + color: var(--current-theme-primary-dark); + } + } +} diff --git a/packages/openneuro-app/src/scripts/search/search-container.tsx b/packages/openneuro-app/src/scripts/search/search-container.tsx index ca73c0d0f2..9bb6710f14 100644 --- a/packages/openneuro-app/src/scripts/search/search-container.tsx +++ b/packages/openneuro-app/src/scripts/search/search-container.tsx @@ -1,14 +1,16 @@ -import React, { useContext, useEffect } from "react" +import React, { useContext, useEffect, useRef, useState } from "react" import type { FC } from "react" +import * as Sentry from "@sentry/react" import { useLocation } from "react-router-dom" -import { SearchPage } from "../components/search-page/SearchPage" -import { SearchResultsList } from "../components/search-page/SearchResultsList" -import { NeurobagelSearch } from "../components/search-page/NeurobagelSearch" +import { SearchPage } from "./components/SearchPage" +import { SearchResultsList } from "./components/SearchResultsList" +import { NeurobagelSearch } from "./components/NeurobagelSearch" import { Button } from "../components/button/Button" import { Loading } from "../components/loading/Loading" +import { modalityShortMapping } from "../components/formatting/modality-label" + import { AgeRangeInput, - AllDatasetsToggle, AuthorInput, BodyPartsInput, DatasetTypeSelect, @@ -21,7 +23,6 @@ import { ScannerManufacturersModelNames, SectionSelect, SexRadios, - ShowDatasetRadios, SortBySelect, SpeciesSelect, StudyDomainInput, @@ -30,13 +31,15 @@ import { TracerNames, TracerRadionuclides, } from "./inputs" +import { DatasetsRadioTabs } from "./components/DatasetsRadioTabs" import FiltersBlockContainer from "./filters-block-container" import AggregateCountsContainer from "../pages/front-page/aggregate-queries/aggregate-counts-container" import { useSearchResults } from "./use-search-results" import { SearchParamsCtx } from "./search-params-ctx" import type { SearchParams } from "./initial-search-params" import Helmet from "react-helmet" -import AdminUser from "../authentication/admin-user.jsx" +import type { SearchResultItemProps } from "./components/SearchResultItem" +import { SearchResultDetails } from "./components/SearchResultDetails" export interface SearchContainerProps { /* eslint-disable-next-line @typescript-eslint/no-explicit-any */ @@ -97,7 +100,8 @@ export const setDefaultSearch = ( // Check for grant-related conditions if ( - is_grant_portal && grant === "nih" && + is_grant_portal && + grant === "nih" && searchParams.brain_initiative !== "true" ) { setSearchParams((prevState) => ({ @@ -153,12 +157,59 @@ const SearchContainer: FC = ({ portalContent }) => { hasNextPage = data.datasets.pageInfo.hasNextPage } + const [clickedItemData, setClickedItemData] = useState< + SearchResultItemProps["node"] | null + >(null) + + const lastOpenedButtonRef = useRef(null) + + useEffect(() => { + setClickedItemData(null) + }, [JSON.stringify(searchParams)]) + + // handleItemClick to accept itemId and event, and store the event.currentTarget + const handleItemClick = ( + itemId: string, + event: React.MouseEvent, + ) => { + const nodeData = + resultsList.find((item) => item.node.id === itemId)?.node || null + + if (!nodeData) { + Sentry.captureException(`Error: nodeData not found for ID: ${itemId}`) + return + } + + if (clickedItemData && clickedItemData.id === nodeData.id) { + // If the same item is clicked again, close details + setClickedItemData(null) + // Focus will be returned by handleCloseDetails + } else { + setClickedItemData(nodeData) + lastOpenedButtonRef.current = event.currentTarget + } + } + + // handleCloseDetails to use setTimeout for focus + const handleCloseDetails = () => { + setClickedItemData(null) + setTimeout(() => { + if (lastOpenedButtonRef.current) { + lastOpenedButtonRef.current.focus() + lastOpenedButtonRef.current = null + } + }, 0) + } + + const labelText = modality ? modalityShortMapping(modality) : null + return ( <> - OpenNeuro - {modality || selected_grant || ""} Search + OpenNeuro - {labelText || selected_grant || ""} Search portalContent.modality @@ -172,20 +223,27 @@ const SearchContainer: FC = ({ portalContent }) => { )} renderSortBy={() => } renderSearchHeader={() => ( - <> - {portalContent - ? "Search " + (modality || selected_grant || "") + " Portal" - : "Search All Datasets"} - +
+
+ {portalContent + ? ( +

+ {"Search " + (labelText || selected_grant || "") + + " Portal"} +

+ ) + :

{"Search All Datasets"}

} +
+
+ +
+
)} renderSearchFacets={() => ( <> - - - - {!searchParams.searchAllDatasets && } + {!portalContent ? : } @@ -233,18 +291,26 @@ const SearchContainer: FC = ({ portalContent }) => { {/* TODO: make div below into display component. */}
- {hasNextPage && resultsList.length > 0 && - ( -
-
- )} + {hasNextPage && resultsList.length > 0 && ( +
+
+ )}
)} + renderItemDetails={() => + clickedItemData && ( + + )} /> ) diff --git a/packages/openneuro-app/src/scripts/search/use-search-results.tsx b/packages/openneuro-app/src/scripts/search/use-search-results.tsx index 6716e9aa26..c134bffe1d 100644 --- a/packages/openneuro-app/src/scripts/search/use-search-results.tsx +++ b/packages/openneuro-app/src/scripts/search/use-search-results.tsx @@ -61,8 +61,10 @@ const searchQuery = gql` } latestSnapshot { size + readme summary { modalities + primaryModality secondaryModalities sessions subjects @@ -94,6 +96,7 @@ const searchQuery = gql` description { Name Authors + DatasetDOI } } analytics { diff --git a/packages/openneuro-app/src/scripts/users/github-auth-button.tsx b/packages/openneuro-app/src/scripts/users/github-auth-button.tsx index 31f5c22114..440b530d04 100644 --- a/packages/openneuro-app/src/scripts/users/github-auth-button.tsx +++ b/packages/openneuro-app/src/scripts/users/github-auth-button.tsx @@ -18,7 +18,7 @@ const GithubSyncDiv = styled.div` margin: 10px; &:hover { - background-color: var(--current-theme-primary-light); + background-color: var(--current-theme-primary-ultralight); color: var(--current-theme-primary); } &.active {