diff --git a/app/scripts/components/common/card/horizontal-info-card.tsx b/app/scripts/components/common/card/horizontal-info-card.tsx
index 51063dbcf..1ad714745 100644
--- a/app/scripts/components/common/card/horizontal-info-card.tsx
+++ b/app/scripts/components/common/card/horizontal-info-card.tsx
@@ -1,9 +1,6 @@
import React from 'react';
import styled, { css } from 'styled-components';
-import {
- glsp,
- themeVal,
-} from '@devseed-ui/theme-provider';
+import { glsp, themeVal } from '@devseed-ui/theme-provider';
import { CardTitle } from './styles';
import { variableBaseType } from '$styles/variable-utils';
import { Pill } from '$styles/pill';
@@ -17,6 +14,8 @@ const CardImage = styled.div`
min-width: 10rem;
width: 10rem;
height: 100%;
+ background: ${themeVal('color.primary-100')};
+
img {
width: 100%;
height: 100%;
@@ -48,8 +47,8 @@ export const HorizontalCardStyles = css`
/* stylelint-disable-next-line value-no-vendor-prefix */
display: -webkit-box;
-webkit-line-clamp: 2; /* number of lines to show */
- line-clamp: 2;
-
+ line-clamp: 2;
+
/* stylelint-disable-next-line property-no-vendor-prefix */
-webkit-box-orient: vertical;
}
@@ -70,36 +69,28 @@ interface Props {
}
export default function HorizontalInfoCard(props: Props) {
- const {
- title,
- description,
- imgSrc,
- imgAlt,
- tagLabels,
- } = props;
+ const { title, description, imgSrc, imgAlt, tagLabels } = props;
return (
-
+ {imgSrc && }
{title}
-
+ {tagLabels && tagLabels.length > 0 && (
+
+ )}
);
-}
\ No newline at end of file
+}
diff --git a/app/scripts/components/common/card/index.tsx b/app/scripts/components/common/card/index.tsx
index 08e32a3bb..ce98fac0b 100644
--- a/app/scripts/components/common/card/index.tsx
+++ b/app/scripts/components/common/card/index.tsx
@@ -7,12 +7,21 @@ import {
media,
multiply,
themeVal,
- listReset,
+ listReset
} from '@devseed-ui/theme-provider';
const SmartLink = lazy(() => import('../smart-link'));
-import { CardBody, CardBlank, CardHeader, CardHeadline, CardTitle, CardOverline } from './styles';
-import HorizontalInfoCard, { HorizontalCardStyles } from './horizontal-info-card';
+import {
+ CardBody,
+ CardBlank,
+ CardHeader,
+ CardHeadline,
+ CardTitle,
+ CardOverline
+} from './styles';
+import HorizontalInfoCard, {
+ HorizontalCardStyles
+} from './horizontal-info-card';
import { variableBaseType, variableGlsp } from '$styles/variable-utils';
import { ElementInteractive } from '$components/common/element-interactive';
import { Figure } from '$components/common/figure';
@@ -175,15 +184,17 @@ const CardLabel = styled.span`
}
`;
-
const CardFigure = styled(Figure)`
order: -1;
+ background: ${themeVal('color.primary-100')};
+ min-height: ${variableGlsp(12)};
img {
height: 100%;
width: 100%;
object-fit: cover;
mix-blend-mode: multiply;
+ display: ${(props) => (props.src ? 'inline-block' : 'none')};
}
`;
@@ -259,8 +270,10 @@ export interface CardComponentProps extends CardComponentBaseProps {
type CardComponentPropsType = CardComponentProps | CardComponentPropsDeprecated;
// Type guard to check if props has linkProperties
-function hasLinkProperties(props: CardComponentPropsType): props is CardComponentProps {
- return !!((props as CardComponentProps).linkProperties);
+function hasLinkProperties(
+ props: CardComponentPropsType
+): props is CardComponentProps {
+ return !!(props as CardComponentProps).linkProperties;
}
function CardComponent(props: CardComponentPropsType) {
@@ -280,8 +293,8 @@ function CardComponent(props: CardComponentPropsType) {
hideExternalLinkBadge,
onCardClickCapture
} = props;
-// @TODO: This process is not necessary once all the instances adapt the linkProperties syntax
-// Consolidate them to use LinkProperties only
+ // @TODO: This process is not necessary once all the instances adapt the linkProperties syntax
+ // Consolidate them to use LinkProperties only
let linkProperties: LinkWithPathProperties;
if (hasLinkProperties(props)) {
@@ -313,59 +326,60 @@ function CardComponent(props: CardComponentPropsType) {
linkLabel={linkLabel ?? 'View more'}
onClickCapture={onCardClickCapture}
>
- {
- cardType !== 'horizontal-info' && (
- <>
-
-
- {title}
-
- {(hideExternalLinkBadge !== true && isExternalLink) && }
- {!isExternalLink && tagLabels && parentTo && (
- tagLabels.map((label) => (
-
- {label}
-
- ))
- )}
- {date ? (
- <>
- published on{' '}
-
- {format(date, 'MMM d, yyyy')}
-
- >
- ) : (
- overline
- )}
-
-
-
- {description && (
-
- {description}
-
- )}
- {footerContent && {footerContent} }
- {imgSrc && (
-
-
-
- )}
- >
- )
- }
- {
- cardType === 'horizontal-info' && (
-
- )
- }
+ {cardType !== 'horizontal-info' && (
+ <>
+
+
+ {title}
+
+ {hideExternalLinkBadge !== true && isExternalLink && (
+
+ )}
+ {!isExternalLink &&
+ tagLabels &&
+ parentTo &&
+ tagLabels.map((label) => (
+
+ {label}
+
+ ))}
+ {date ? (
+ <>
+ published on{' '}
+
+ {format(date, 'MMM d, yyyy')}
+
+ >
+ ) : (
+ overline
+ )}
+
+
+
+ {description && (
+
+ {description}
+
+ )}
+ {footerContent && {footerContent} }
+
+
+
+ >
+ )}
+ {cardType === 'horizontal-info' && (
+
+ )}
);
}
@@ -373,4 +387,3 @@ function CardComponent(props: CardComponentPropsType) {
export const Card = styled(CardComponent)`
/* Convert to styled-component: https://styled-components.com/docs/advanced#caveat */
`;
-
diff --git a/app/scripts/components/common/catalog/catalog-card.tsx b/app/scripts/components/common/catalog/catalog-card.tsx
index 427ca9ba8..e9a85a751 100644
--- a/app/scripts/components/common/catalog/catalog-card.tsx
+++ b/app/scripts/components/common/catalog/catalog-card.tsx
@@ -1,17 +1,27 @@
-import React from "react";
-import styled, { css } from "styled-components";
-import { CollecticonPlus, CollecticonTickSmall, iconDataURI } from "@devseed-ui/collecticons";
-import { glsp, themeVal } from "@devseed-ui/theme-provider";
+import React from 'react';
+import styled, { css } from 'styled-components';
+import {
+ CollecticonPlus,
+ CollecticonTickSmall,
+ iconDataURI
+} from '@devseed-ui/collecticons';
+import { glsp, themeVal } from '@devseed-ui/theme-provider';
-import { Card, LinkProperties } from "../card";
-import { CardMeta, CardTopicsList } from "../card/styles";
-import { DatasetClassification } from "../dataset-classification";
-import { CardSourcesList } from "../card-sources";
-import TextHighlight from "../text-highlight";
-import { DatasetData, DatasetLayer } from "$types/veda";
-import { getDatasetPath } from "$utils/routes";
-import { TAXONOMY_SOURCE, TAXONOMY_TOPICS, getAllTaxonomyValues, getTaxonomy } from "$utils/veda-data/taxonomies";
-import { Pill } from "$styles/pill";
+import { Card, LinkProperties } from '../card';
+import { CardMeta, CardTopicsList } from '../card/styles';
+import { DatasetClassification } from '../dataset-classification';
+import { CardSourcesList } from '../card-sources';
+import TextHighlight from '../text-highlight';
+import { getDatasetDescription, getMediaProperty } from './utils';
+import { DatasetData, DatasetLayer } from '$types/veda';
+import { getDatasetPath } from '$utils/routes';
+import {
+ TAXONOMY_SOURCE,
+ TAXONOMY_TOPICS,
+ getAllTaxonomyValues,
+ getTaxonomy
+} from '$utils/veda-data/taxonomies';
+import { Pill } from '$styles/pill';
interface CatalogCardProps {
dataset: DatasetData;
@@ -24,50 +34,52 @@ interface CatalogCardProps {
linkProperties: LinkProperties;
}
-const CardSelectable = styled(Card)<{ checked?: boolean, selectable?: boolean }>`
+const CardSelectable = styled(Card)<{
+ checked?: boolean;
+ selectable?: boolean;
+}>`
outline: 4px solid transparent;
${({ checked }) =>
checked &&
css`
- outline-color: ${themeVal("color.primary")};
+ outline-color: ${themeVal('color.primary')};
`}
${({ selectable }) =>
selectable &&
css`
- &::before {
- content: '';
- position: absolute;
- top: 50%;
- left: 80px;
- height: 3rem;
- min-width: 3rem;
- transform: translate(-50%, -50%);
- padding: ${glsp(0.5, 1, 0.5, 1)};
- display: flex;
- align-items: center;
- justify-content: center;
- background: ${themeVal("color.primary")};
- border-radius: ${themeVal("shape.ellipsoid")};
- font-weight: ${themeVal("type.base.bold")};
- line-height: 1rem;
- background-image: url(${({ theme }) =>
- iconDataURI(CollecticonPlus, {
- color: theme.color?.surface,
- size: "large"
- })});
- background-repeat: no-repeat;
- background-position: 0.75rem center;
- pointer-events: none;
- transition: all 0.16s ease-in-out;
- opacity: 0;
- }
+ &::before {
+ content: '';
+ position: absolute;
+ top: 50%;
+ left: 80px;
+ height: 3rem;
+ min-width: 3rem;
+ transform: translate(-50%, -50%);
+ padding: ${glsp(0.5, 1, 0.5, 1)};
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ background: ${themeVal('color.primary')};
+ border-radius: ${themeVal('shape.ellipsoid')};
+ font-weight: ${themeVal('type.base.bold')};
+ line-height: 1rem;
+ background-image: url(${({ theme }) =>
+ iconDataURI(CollecticonPlus, {
+ color: theme.color?.surface,
+ size: 'large'
+ })});
+ background-repeat: no-repeat;
+ background-position: 0.75rem center;
+ pointer-events: none;
+ transition: all 0.16s ease-in-out;
+ opacity: 0;
+ }
- &:hover ::before {
- opacity: 1;
- }
- `
- }
+ &:hover ::before {
+ opacity: 1;
+ }
+ `}
${({ checked }) =>
checked &&
@@ -76,32 +88,41 @@ const CardSelectable = styled(Card)<{ checked?: boolean, selectable?: boolean }>
opacity: 1;
z-index: 10;
content: 'Selected';
- color: ${themeVal("color.surface")};
+ color: ${themeVal('color.surface')};
padding-left: 2.75rem;
background-image: url(${({ theme }) =>
iconDataURI(CollecticonTickSmall, {
color: theme.color?.surface,
- size: "large"
+ size: 'large'
})});
- background-color: ${themeVal("color.base")};
+ background-color: ${themeVal('color.base')};
}
&:hover ::before {
- background-color: ${themeVal("color.primary")};
+ background-color: ${themeVal('color.primary')};
}
`}
`;
export const CatalogCard = (props: CatalogCardProps) => {
- const { dataset, layer, searchTerm, selectable, selected, onDatasetClick, linkProperties, pathname} = props;
+ const {
+ dataset,
+ layer,
+ searchTerm,
+ selectable,
+ selected,
+ onDatasetClick,
+ linkProperties,
+ pathname
+ } = props;
const topics = getTaxonomy(dataset, TAXONOMY_TOPICS)?.values;
const sources = getTaxonomy(dataset, TAXONOMY_SOURCE)?.values;
const allTaxonomyValues = getAllTaxonomyValues(dataset).map((v) => v.name);
const title = layer ? layer.name : dataset.name;
- const description = layer ? layer.description : dataset.description;
- const imgSrc = layer?.media?.src ?? dataset.media?.src;
- const imgAlt = layer?.media?.alt ?? dataset.media?.alt;
+ const description = getDatasetDescription(layer, dataset);
+ const imgSrc = getMediaProperty(layer, dataset, 'src');
+ const imgAlt = getMediaProperty(layer, dataset, 'alt');
const handleClick = (e: React.MouseEvent) => {
if (onDatasetClick) {
@@ -145,7 +166,10 @@ export const CatalogCard = (props: CatalogCardProps) => {
{topics.map((t) => (
-
+
{t.name}
@@ -155,7 +179,11 @@ export const CatalogCard = (props: CatalogCardProps) => {
) : null}
>
}
- linkProperties={{...linkProperties, linkTo: linkTo, onLinkClick: handleClick}}
+ linkProperties={{
+ ...linkProperties,
+ linkTo: linkTo,
+ onLinkClick: handleClick
+ }}
/>
);
};
diff --git a/app/scripts/components/common/catalog/utils.test.ts b/app/scripts/components/common/catalog/utils.test.ts
index 60e8428b0..837cefcfe 100644
--- a/app/scripts/components/common/catalog/utils.test.ts
+++ b/app/scripts/components/common/catalog/utils.test.ts
@@ -1,5 +1,13 @@
import { omit, set } from 'lodash';
-import { FilterActions, onFilterAction } from './utils';
+import { FormatBlock } from '../types';
+import {
+ FilterActions,
+ getDatasetDescription,
+ getDescription,
+ getMediaProperty,
+ onFilterAction
+} from './utils';
+import { DatasetData, DatasetLayer, StoryData } from '$types/veda';
describe('onFilterAction', () => {
let setSearchMock;
@@ -142,3 +150,258 @@ describe('onFilterAction', () => {
);
});
});
+
+describe('getDescription', () => {
+ it('should return cardDescription when available', () => {
+ const data = {
+ description: 'Regular description',
+ cardDescription: 'Card description'
+ } as DatasetData;
+
+ expect(getDescription(data)).toBe('Card description');
+ });
+
+ it('should fall back to description when cardDescription is not available', () => {
+ const data = {
+ description: 'Regular description'
+ } as DatasetData;
+
+ expect(getDescription(data)).toBe('Regular description');
+ });
+
+ it('works with different data types', () => {
+ const dataset = {
+ description: 'Dataset description',
+ cardDescription: 'Dataset card description'
+ } as DatasetData;
+
+ const story = {
+ description: 'Story description',
+ cardDescription: 'Story card description'
+ } as StoryData;
+
+ const layer = {
+ description: 'Layer description',
+ cardDescription: 'Layer card description'
+ } as DatasetLayer;
+
+ expect(getDescription(dataset)).toBe('Dataset card description');
+ expect(getDescription(story)).toBe('Story card description');
+ expect(getDescription(layer)).toBe('Layer card description');
+ });
+
+ it('works with FormatBlock', () => {
+ const formatBlock = {
+ id: '1',
+ name: 'Test',
+ description: 'Format block description',
+ cardDescription: 'Format block card description',
+ date: '2024-01-01',
+ link: '/test',
+ parentLink: '/parent',
+ media: { src: 'src', alt: 'alt' },
+ parent: 'story'
+ } as FormatBlock;
+
+ expect(getDescription(formatBlock)).toBe('Format block card description');
+ });
+
+ it('works with FormatBlock without cardDescription', () => {
+ const formatBlock = {
+ id: '1',
+ name: 'Test',
+ description: 'Format block description',
+ date: '2024-01-01',
+ link: '/test',
+ parentLink: '/parent',
+ media: { src: 'src', alt: 'alt' },
+ parent: 'story'
+ } as FormatBlock;
+
+ expect(getDescription(formatBlock)).toBe('Format block description');
+ });
+
+ it('handles empty strings in description fields', () => {
+ const data = {
+ description: '',
+ cardDescription: ''
+ } as DatasetData;
+
+ expect(getDescription(data)).toBe('');
+ });
+
+ it('handles undefined cardDescription', () => {
+ const data = {
+ description: 'Regular description',
+ cardDescription: undefined
+ } as DatasetData;
+
+ expect(getDescription(data)).toBe('Regular description');
+ });
+});
+
+describe('getDatasetDescription', () => {
+ const dataset = {
+ description: 'Dataset description',
+ cardDescription: 'Dataset card description'
+ } as DatasetData;
+
+ const layer = {
+ description: 'Layer description',
+ cardDescription: 'Layer card description'
+ } as DatasetLayer;
+
+ it('should return layer description when layer is provided', () => {
+ expect(getDatasetDescription(layer, dataset)).toBe(
+ 'Layer card description'
+ );
+ });
+
+ it('should return dataset description when no layer is provided', () => {
+ expect(getDatasetDescription(undefined, dataset)).toBe(
+ 'Dataset card description'
+ );
+ });
+
+ it('should handle layer without cardDescription', () => {
+ const layerWithoutCard = {
+ description: 'Layer description'
+ } as DatasetLayer;
+
+ expect(getDatasetDescription(layerWithoutCard, dataset)).toBe(
+ 'Layer description'
+ );
+ });
+
+ it('should work with story data', () => {
+ const story = {
+ description: 'Story description',
+ cardDescription: 'Story card description'
+ } as StoryData;
+
+ expect(getDatasetDescription(undefined, story)).toBe(
+ 'Story card description'
+ );
+ });
+});
+
+describe('getMediaProperty', () => {
+ const dataset = {
+ media: {
+ src: 'dataset-src',
+ alt: 'dataset-alt'
+ },
+ cardMedia: {
+ src: 'dataset-card-src',
+ alt: 'dataset-card-alt'
+ }
+ } as DatasetData;
+
+ const layer = {
+ media: {
+ src: 'layer-src',
+ alt: 'layer-alt'
+ },
+ cardMedia: {
+ src: 'layer-card-src',
+ alt: 'layer-card-alt'
+ }
+ } as DatasetLayer;
+
+ it('should follow precedence order for src property', () => {
+ // Test full precedence chain
+ expect(getMediaProperty(layer, dataset, 'src')).toBe('layer-card-src');
+
+ // Test without layer cardMedia
+ const layerNoCard = { ...layer, cardMedia: undefined };
+ expect(getMediaProperty(layerNoCard, dataset, 'src')).toBe('layer-src');
+
+ // Test without layer
+ expect(getMediaProperty(undefined, dataset, 'src')).toBe(
+ 'dataset-card-src'
+ );
+
+ // Test with minimal data
+ const minimalDataset = {
+ media: { src: 'only-src', alt: 'only-alt' }
+ } as DatasetData;
+ expect(getMediaProperty(undefined, minimalDataset, 'src')).toBe('only-src');
+ });
+
+ it('should follow precedence order for alt property', () => {
+ expect(getMediaProperty(layer, dataset, 'alt')).toBe('layer-card-alt');
+
+ const layerNoCard = { ...layer, cardMedia: undefined };
+ expect(getMediaProperty(layerNoCard, dataset, 'alt')).toBe('layer-alt');
+
+ expect(getMediaProperty(undefined, dataset, 'alt')).toBe(
+ 'dataset-card-alt'
+ );
+ });
+
+ it('should work with FormatBlock', () => {
+ const formatBlock = {
+ id: '1',
+ name: 'Test',
+ description: 'description',
+ date: '2024-01-01',
+ link: '/test',
+ parentLink: '/parent',
+ media: { src: 'media-src', alt: 'media-alt' },
+ cardMedia: { src: 'card-src', alt: 'card-alt' },
+ parent: 'story'
+ } as FormatBlock;
+
+ expect(getMediaProperty(undefined, formatBlock, 'src')).toBe('card-src');
+ expect(getMediaProperty(undefined, formatBlock, 'alt')).toBe('card-alt');
+ });
+
+ it('should fall back to media in FormatBlock when cardMedia is not available', () => {
+ const formatBlock = {
+ id: '1',
+ name: 'Test',
+ description: 'description',
+ date: '2024-01-01',
+ link: '/test',
+ parentLink: '/parent',
+ media: { src: 'media-src', alt: 'media-alt' },
+ parent: 'story'
+ } as FormatBlock;
+
+ expect(getMediaProperty(undefined, formatBlock, 'src')).toBe('media-src');
+ expect(getMediaProperty(undefined, formatBlock, 'alt')).toBe('media-alt');
+ });
+
+ it('should work with layer and FormatBlock combination', () => {
+ const formatBlock = {
+ id: '1',
+ name: 'Test',
+ description: 'description',
+ date: '2024-01-01',
+ link: '/test',
+ parentLink: '/parent',
+ media: { src: 'media-src', alt: 'media-alt' },
+ cardMedia: { src: 'card-src', alt: 'card-alt' },
+ parent: 'story'
+ } as FormatBlock;
+
+ expect(getMediaProperty(layer, formatBlock, 'src')).toBe('layer-card-src');
+ expect(getMediaProperty(layer, formatBlock, 'alt')).toBe('layer-card-alt');
+ });
+
+ it('should return empty string when no media is available', () => {
+ const emptyDataset = {} as DatasetData;
+ expect(getMediaProperty(undefined, emptyDataset, 'src')).toBe('');
+ expect(getMediaProperty(undefined, emptyDataset, 'alt')).toBe('');
+ });
+
+ it('should work with story data', () => {
+ const story = {
+ media: { src: 'story-src', alt: 'story-alt' },
+ cardMedia: { src: 'story-card-src', alt: 'story-card-alt' }
+ } as StoryData;
+
+ expect(getMediaProperty(undefined, story, 'src')).toBe('story-card-src');
+ expect(getMediaProperty(undefined, story, 'alt')).toBe('story-card-alt');
+ });
+});
diff --git a/app/scripts/components/common/catalog/utils.ts b/app/scripts/components/common/catalog/utils.ts
index ded96f3b6..2fb9ad1a4 100644
--- a/app/scripts/components/common/catalog/utils.ts
+++ b/app/scripts/components/common/catalog/utils.ts
@@ -1,5 +1,7 @@
import { omit, set } from 'lodash';
+import { FormatBlock } from '../types';
import { optionAll } from '$components/common/browse-controls/constants';
+import { DatasetData, DatasetLayer, StoryData } from '$types/veda';
export enum FilterActions {
TAXONOMY_MULTISELECT = 'taxonomy_multiselect',
@@ -9,7 +11,7 @@ export enum FilterActions {
SORT_DIR = 'sdir',
TAXONOMY = 'taxonomy',
CLEAR_TAXONOMY = 'clear_taxonomy',
- CLEAR_SEARCH = 'clear_search',
+ CLEAR_SEARCH = 'clear_search'
}
export type FilterAction = (action: FilterActions, value?: any) => void;
@@ -77,3 +79,57 @@ export function onFilterAction(
break;
}
}
+
+type DataObject = DatasetData | DatasetLayer | StoryData | FormatBlock;
+
+/**
+ * Returns the description for a dataset, layer, or story, prioritizing cardDescription
+ * @param data - Dataset, layer, or story object
+ * @returns The appropriate description string
+ */
+export const getDescription = (data: DataObject): string => {
+ return data.cardDescription ?? data.description;
+};
+
+/**
+ * Returns the description for a dataset/story and its optional layer
+ * Layer description takes precedence over dataset/story description
+ * @param layer - Optional layer object
+ * @param data - Dataset or story object
+ * @returns The appropriate description string
+ */
+export const getDatasetDescription = (
+ layer: DatasetLayer | undefined,
+ data: DatasetData | StoryData
+): string => {
+ if (!layer) {
+ return getDescription(data);
+ }
+ return getDescription(layer);
+};
+
+/**
+ * Gets media property following strict precedence order:
+ * 1. Layer cardMedia
+ * 2. Layer media
+ * 3. Parent cardMedia
+ * 4. Parent media
+ *
+ * @param layer - Optional layer object
+ * @param data - Parent (dataset/story) object
+ * @param property - Media property to retrieve ('src' or 'alt')
+ * @returns The appropriate media property value
+ */
+export const getMediaProperty = (
+ layer: DatasetLayer | undefined,
+ data: DatasetData | StoryData | FormatBlock,
+ property: T
+): string => {
+ return (
+ layer?.cardMedia?.[property] ??
+ layer?.media?.[property] ??
+ data.cardMedia?.[property] ??
+ data.media?.[property] ??
+ ''
+ );
+};
diff --git a/app/scripts/components/common/featured-slider-section.tsx b/app/scripts/components/common/featured-slider-section.tsx
index 824d1e2ce..d4cc0a123 100644
--- a/app/scripts/components/common/featured-slider-section.tsx
+++ b/app/scripts/components/common/featured-slider-section.tsx
@@ -6,6 +6,7 @@ import SmartLink from './smart-link';
import PublishedDate from './pub-date';
import { CardSourcesList } from './card-sources';
import { DatasetClassification } from './dataset-classification';
+import { getDescription, getMediaProperty } from './catalog/utils';
import { Card } from '$components/common/card';
import { CardMeta, CardTopicsList } from '$components/common/card/styles';
import { FoldGrid, FoldHeader, FoldTitle } from '$components/common/fold';
@@ -70,12 +71,15 @@ function FeaturedSliderSection(props: FeaturedSliderSectionProps) {
// Disable no-mutating rule since the copy of the array is being mutated
// eslint-disable-next-line fp/no-mutating-methods
- const sortedFeaturedItems = dateProperty? [...featuredItems].sort((itemA: StoryData | DatasetData, itemB: StoryData | DatasetData) => {
- const pubDateOfItemA = new Date(itemA[dateProperty]);
- const pubDateOfItemB = new Date(itemB[dateProperty]);
- return pubDateOfItemB.getTime() - pubDateOfItemA.getTime();
- }) as StoryData[] | DatasetData[]: featuredItems;
-
+ const sortedFeaturedItems = dateProperty
+ ? ([...featuredItems].sort(
+ (itemA: StoryData | DatasetData, itemB: StoryData | DatasetData) => {
+ const pubDateOfItemA = new Date(itemA[dateProperty]);
+ const pubDateOfItemB = new Date(itemB[dateProperty]);
+ return pubDateOfItemB.getTime() - pubDateOfItemA.getTime();
+ }
+ ) as StoryData[] | DatasetData[])
+ : featuredItems;
return (
@@ -127,9 +131,9 @@ function FeaturedSliderSection(props: FeaturedSliderSectionProps) {
)}
}
- description={d.description}
- imgSrc={d.media?.src}
- imgAlt={d.media?.alt}
+ description={getDescription(d)}
+ imgSrc={getMediaProperty(undefined, d, 'src')}
+ imgAlt={getMediaProperty(undefined, d, 'alt')}
footerContent={
<>
{topics?.length ? (
diff --git a/app/scripts/components/common/related-content.tsx b/app/scripts/components/common/related-content.tsx
index 79fcfbc99..2ef23b869 100644
--- a/app/scripts/components/common/related-content.tsx
+++ b/app/scripts/components/common/related-content.tsx
@@ -2,15 +2,10 @@ import React from 'react';
import styled from 'styled-components';
import { media } from '@devseed-ui/theme-provider';
-import {
- stories,
- datasets,
- Media,
- RelatedContentData,
- LinkContentData,
- StoryData
-} from 'veda';
+import { stories, datasets, RelatedContentData, StoryData } from 'veda';
import SmartLink from './smart-link';
+import { getDescription, getMediaProperty } from './catalog/utils';
+import { FormatBlock } from './types';
import { utcString2userTzDate } from '$utils/date';
import {
getDatasetPath,
@@ -45,18 +40,6 @@ const RelatedContentInner = styled.div`
`}
`;
-interface FormatBlock {
- id: string;
- name: string;
- description: string;
- date: string;
- link: string;
- asLink?: LinkContentData;
- parentLink: string;
- media: Media;
- parent: RelatedContentData['type'];
-}
-
function formatUrl(id: string, parent: string) {
switch (parent) {
case datasetString:
@@ -78,8 +61,10 @@ function formatBlock({
id,
name,
description,
+ cardDescription,
date,
media,
+ cardMedia,
asLink,
type
}): FormatBlock {
@@ -87,8 +72,10 @@ function formatBlock({
id,
name,
description,
+ cardDescription,
date,
media,
+ cardMedia,
asLink,
...formatUrl(id, type),
parent: type
@@ -110,14 +97,17 @@ function formatContents(relatedData: RelatedContentData[]) {
);
}
- const { name, description, media } = matchingContent;
+ const { name, description, media, cardDescription, cardMedia } =
+ matchingContent;
return formatBlock({
id,
name,
description,
+ cardDescription,
asLink: (matchingContent as StoryData).asLink,
date: (matchingContent as StoryData).pubDate,
media,
+ cardMedia,
type
});
});
@@ -159,11 +149,11 @@ export default function RelatedContent(props: RelatedContentProps) {
? utcString2userTzDate(t.date)
: undefined
}
- description={t.description}
+ description={getDescription(t)}
tagLabels={[t.parent]}
parentTo={t.parentLink}
- imgSrc={t.media.src}
- imgAlt={t.media.alt}
+ imgSrc={getMediaProperty(undefined, t, 'src')}
+ imgAlt={getMediaProperty(undefined, t, 'alt')}
/>
))}
diff --git a/app/scripts/components/common/types.d.ts b/app/scripts/components/common/types.d.ts
new file mode 100644
index 000000000..ec06f7b92
--- /dev/null
+++ b/app/scripts/components/common/types.d.ts
@@ -0,0 +1,13 @@
+export interface FormatBlock {
+ id: string;
+ name: string;
+ description: string;
+ cardDescription?: string;
+ date: string;
+ link: string;
+ asLink?: LinkContentData;
+ parentLink: string;
+ media: Media;
+ cardMedia?: Media;
+ parent: RelatedContentData['type'];
+}
diff --git a/app/scripts/components/home/featured-stories.tsx b/app/scripts/components/home/featured-stories.tsx
index a8bb557a7..e043cbe35 100644
--- a/app/scripts/components/home/featured-stories.tsx
+++ b/app/scripts/components/home/featured-stories.tsx
@@ -8,6 +8,10 @@ import { Fold, FoldHeader, FoldTitle, FoldBody } from '$components/common/fold';
import { variableGlsp } from '$styles/variable-utils';
import { STORIES_PATH, getStoryPath } from '$utils/routes';
import SmartLink from '$components/common/smart-link';
+import {
+ getDescription,
+ getMediaProperty
+} from '$components/common/catalog/utils';
const FeaturedStoryList = styled.ol`
${listReset()}
@@ -87,10 +91,10 @@ function FeaturedStories() {
title={d.name}
tagLabels={[getString('stories').one]}
parentTo={STORIES_PATH}
- description={i === 0 ? d.description : undefined}
+ description={i === 0 ? getDescription(d) : undefined}
date={d.pubDate ? new Date(d.pubDate) : undefined}
- imgSrc={d.media?.src}
- imgAlt={d.media?.alt}
+ imgSrc={getMediaProperty(undefined, d, 'src')}
+ imgAlt={getMediaProperty(undefined, d, 'alt')}
/>
);
diff --git a/app/scripts/components/stories/hub/hub-content.tsx b/app/scripts/components/stories/hub/hub-content.tsx
index ae2c3ee5a..a459c2e8d 100644
--- a/app/scripts/components/stories/hub/hub-content.tsx
+++ b/app/scripts/components/stories/hub/hub-content.tsx
@@ -8,7 +8,11 @@ import { VerticalDivider } from '@devseed-ui/toolbar';
import { Subtitle } from '@devseed-ui/typography';
import PublishedDate from '$components/common/pub-date';
import BrowseControls from '$components/common/browse-controls';
-import { FilterActions } from '$components/common/catalog/utils';
+import {
+ FilterActions,
+ getDescription,
+ getMediaProperty
+} from '$components/common/catalog/utils';
import {
Fold,
FoldHeader,
@@ -17,7 +21,11 @@ import {
} from '$components/common/fold';
import { useSlidingStickyHeaderProps } from '$components/common/layout-root/useSlidingStickyHeaderProps';
import { Card, LinkProperties } from '$components/common/card';
-import { CardListGrid, CardMeta, CardTopicsList } from '$components/common/card/styles';
+import {
+ CardListGrid,
+ CardMeta,
+ CardTopicsList
+} from '$components/common/card/styles';
import EmptyHub from '$components/common/empty-hub';
import { prepareDatasets } from '$components/common/catalog/prepare-datasets';
@@ -54,17 +62,25 @@ const FoldWithTopMargin = styled(Fold)`
margin-top: ${glsp()};
`;
-interface StoryDataWithPath extends StoryData {path: string}
+interface StoryDataWithPath extends StoryData {
+ path: string;
+}
interface HubContentProps {
allStories: StoryDataWithPath[];
linkProperties: LinkProperties;
pathname: string;
- storiesString: {one: string, other: string};
+ storiesString: { one: string; other: string };
onFilterchanges: () => UseFiltersWithQueryResult;
}
-export default function HubContent(props:HubContentProps) {
- const { allStories, linkProperties, pathname, storiesString, onFilterchanges } = props;
+export default function HubContent(props: HubContentProps) {
+ const {
+ allStories,
+ linkProperties,
+ pathname,
+ storiesString,
+ onFilterchanges
+ } = props;
const browseControlsHeaderRef = useRef(null);
const { headerHeight } = useSlidingStickyHeaderProps();
const { search, taxonomies, onAction } = onFilterchanges();
@@ -77,10 +93,12 @@ export default function HubContent(props:HubContentProps) {
[pathAttributeKeyName]: pathname
};
- function getPillLinkProps(t){
+ function getPillLinkProps(t) {
return {
as: LinkElement,
- [pathAttributeKeyName]: `${pathname}?${FilterActions.TAXONOMY}=${encodeURIComponent(JSON.stringify({ Topics: t.id }))}`
+ [pathAttributeKeyName]: `${pathname}?${
+ FilterActions.TAXONOMY
+ }=${encodeURIComponent(JSON.stringify({ Topics: t.id }))}`
};
}
const displayStories = useMemo(
@@ -89,142 +107,138 @@ export default function HubContent(props:HubContentProps) {
search,
taxonomies,
sortField: 'pubDate',
- sortDir: 'desc',
+ sortDir: 'desc'
}) as StoryDataWithPath[],
[search, taxonomies, allStories]
);
const isFiltering = !!(
- (taxonomies && Object.keys(taxonomies).length )||
+ (taxonomies && Object.keys(taxonomies).length) ||
search
);
- return (
-
-
- Browse
-
-
-
-
-
-
- Showing{' '}
- {' '}
- out of {allStories.length}.
-
- {isFiltering && (
-
- Clear filters
-
- )}
-
-
- {displayStories.length ? (
-
- {displayStories.map((d) => {
- const pubDate = new Date(d.pubDate);
- const topics = getTaxonomy(d, TAXONOMY_TOPICS)?.values;
- return (
-
-
- {
- onAction(FilterActions.TAXONOMY_MULTISELECT, {
- key: TAXONOMY_SOURCE,
- value: id
- });
- browseControlsHeaderRef.current?.scrollIntoView();
- }}
- />
-
-
- {!isNaN(pubDate.getTime()) && (
+ return (
+
+
+
+ Browse
+
+
+
+
+
+
+ Showing{' '}
+ {' '}
+ out of {allStories.length}.
+
+ {isFiltering && (
+
+ Clear filters
+
+ )}
+
+
+ {displayStories.length ? (
+
+ {displayStories.map((d) => {
+ const pubDate = new Date(d.pubDate);
+ const topics = getTaxonomy(d, TAXONOMY_TOPICS)?.values;
+ return (
+
+
+ {
+ onAction(FilterActions.TAXONOMY_MULTISELECT, {
+ key: TAXONOMY_SOURCE,
+ value: id
+ });
+ browseControlsHeaderRef.current?.scrollIntoView();
+ }}
+ />
+
+
+ {!isNaN(pubDate.getTime()) && (
- )}
-
- }
- linkLabel='View more'
- linkProperties={{
- linkTo: `${d.asLink?.url ?? d.path}`,
- LinkElement,
- pathAttributeKeyName
- }}
- title={
-
- {d.name}
-
- }
- description={
-
- {d.description}
-
- }
- hideExternalLinkBadge={d.hideExternalLinkBadge}
- imgSrc={d.media?.src}
- imgAlt={d.media?.alt}
- footerContent={
- <>
- {topics?.length ? (
-
- Topics
- {topics.map((t) => (
-
- {
- browseControlsHeaderRef.current?.scrollIntoView();
- }}
- >
-
+ }
+ linkLabel='View more'
+ linkProperties={{
+ linkTo: `${d.asLink?.url ?? d.path}`,
+ LinkElement,
+ pathAttributeKeyName
+ }}
+ title={
+
+ {d.name}
+
+ }
+ description={
+
+ {getDescription(d)}
+
+ }
+ hideExternalLinkBadge={d.hideExternalLinkBadge}
+ imgSrc={getMediaProperty(undefined, d, 'src')}
+ imgAlt={getMediaProperty(undefined, d, 'alt')}
+ footerContent={
+ <>
+ {topics?.length ? (
+
+ Topics
+ {topics.map((t) => (
+
+ {
+ browseControlsHeaderRef.current?.scrollIntoView();
+ }}
>
- {t.name}
-
-
-
- ))}
-
- ) : null}
- >
- }
- />
-
- );
- })}
-
- ) : (
-
- There are no {storiesString.other.toLocaleLowerCase()} to
- show with the selected filters.
-
- )}
- );
-}
\ No newline at end of file
+
+ {t.name}
+
+
+
+ ))}
+
+ ) : null}
+ >
+ }
+ />
+
+ );
+ })}
+
+ ) : (
+
+ There are no {storiesString.other.toLocaleLowerCase()} to show with
+ the selected filters.
+
+ )}
+
+ );
+}
diff --git a/app/scripts/types/veda.ts b/app/scripts/types/veda.ts
index ee2873d31..ec3d1f663 100644
--- a/app/scripts/types/veda.ts
+++ b/app/scripts/types/veda.ts
@@ -63,10 +63,12 @@ export interface DatasetLayer extends DatasetLayerCommonProps {
id: string;
stacCol: string;
media?: Media;
+ cardMedia?: Media;
stacApiEndpoint?: string;
tileApiEndpoint?: string;
name: string;
description: string;
+ cardDescription?: string;
initialDatetime?: 'newest' | 'oldest' | string;
projection?: ProjectionOptions;
basemapId?: 'dark' | 'light' | 'satellite' | 'topo';
@@ -180,8 +182,10 @@ export interface DatasetData {
infoDescription?: string;
taxonomy: Taxonomy[];
description: string;
+ cardDescription?: string;
usage?: DatasetUsage[];
media?: Media;
+ cardMedia?: Media;
layers: DatasetLayer[];
related?: RelatedContentData[];
disableExplore?: boolean;
@@ -200,9 +204,11 @@ export interface StoryData {
id: string;
name: string;
description: string;
+ cardDescription?: string;
pubDate: string;
path?: string;
media?: Media;
+ cardMedia?: Media;
taxonomy: Taxonomy[];
related?: RelatedContentData[];
asLink?: LinkContentData;
diff --git a/parcel-resolver-veda/index.d.ts b/parcel-resolver-veda/index.d.ts
index 6be6a982e..eee428ff4 100644
--- a/parcel-resolver-veda/index.d.ts
+++ b/parcel-resolver-veda/index.d.ts
@@ -182,8 +182,10 @@ declare module 'veda' {
infoDescription?: string;
taxonomy: Taxonomy[];
description: string;
+ cardDescription?: string;
usage?: DatasetUsage[];
media?: Media;
+ cardMedia?: Media;
layers: DatasetLayer[];
related?: RelatedContentData[];
disableExplore?: boolean;
@@ -202,8 +204,10 @@ declare module 'veda' {
id: string;
name: string;
description: string;
+ cardDescription?: string;
pubDate: string;
media?: Media;
+ cardMedia?: Media;
taxonomy: Taxonomy[];
related?: RelatedContentData[];
asLink?: LinkContentData;