diff --git a/app/scripts/components/common/card/index.tsx b/app/scripts/components/common/card/index.tsx index af58fe462..c9b34a2fe 100644 --- a/app/scripts/components/common/card/index.tsx +++ b/app/scripts/components/common/card/index.tsx @@ -2,7 +2,6 @@ import React, { lazy, MouseEventHandler, ComponentType } from 'react'; import styled, { css } from 'styled-components'; import { format } from 'date-fns'; import { CollecticonExpandTopRight } from '@devseed-ui/collecticons'; -import { Link } from 'react-router-dom'; const SmartLink = lazy(() => import('../smart-link')); import { glsp, @@ -221,7 +220,7 @@ export function ExternalLinkFlag() { } export interface LinkProperties { - LinkElement: JSX.Element | ((props: any) => JSX.Element) | ComponentType; + LinkElement: string | ComponentType | undefined; pathAttributeKeyName: string; onLinkClick?: MouseEventHandler; } @@ -322,7 +321,7 @@ function CardComponent(props: CardComponentPropsType) { {isExternalLink && } {!isExternalLink && tagLabels && parentTo && ( tagLabels.map((label) => ( - + {label} )) diff --git a/app/scripts/components/stories/hub/container.tsx b/app/scripts/components/stories/hub/container.tsx new file mode 100644 index 000000000..1733084c1 --- /dev/null +++ b/app/scripts/components/stories/hub/container.tsx @@ -0,0 +1,52 @@ +import React from 'react'; +import { useNavigate } from 'react-router'; + +import { stories, getString } from 'veda'; +import HubContent from './hub-content'; + +import { useFiltersWithQS } from '$components/common/catalog/controls/hooks/use-filters-with-query'; +import { LayoutProps } from '$components/common/layout-root'; + +import PageHero from '$components/common/page-hero'; + +import { PageMainContent } from '$styles/page'; +import { STORIES_PATH, getStoryPath } from '$utils/routes'; +import { FeaturedStories } from '$components/common/featured-slider-section'; +import { + ComponentOverride, + ContentOverride +} from '$components/common/page-overrides'; + +import SmartLink from '$components/common/smart-link'; + +const allStories = Object.values(stories).map((d) => d!.data).filter(d => !d.isHidden).map((d) => ({ ...d, path: getStoryPath(d)})); + +function StoriesHubContainer() { + const controlVars = useFiltersWithQS({navigate: useNavigate()}); + return ( + + + + + + + + controlVars} + linkProperties={{ LinkElement: SmartLink, pathAttributeKeyName: 'to'}} + pathname={STORIES_PATH} + storiesString={{one: getString('stories').one,other: getString('stories').other}} + /> + + + ); +} + +export default StoriesHubContainer; \ No newline at end of file diff --git a/app/scripts/components/stories/hub/hub-content.tsx b/app/scripts/components/stories/hub/hub-content.tsx new file mode 100644 index 000000000..1352d1eb8 --- /dev/null +++ b/app/scripts/components/stories/hub/hub-content.tsx @@ -0,0 +1,229 @@ +import React, { useRef, useMemo } from 'react'; + +import styled from 'styled-components'; +import { glsp } from '@devseed-ui/theme-provider'; +import { Button } from '@devseed-ui/button'; +import { CollecticonXmarkSmall } from '@devseed-ui/collecticons'; +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 { + Fold, + FoldHeader, + FoldHeadline, + FoldTitle +} 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 EmptyHub from '$components/common/empty-hub'; +import { prepareDatasets } from '$components/common/catalog/prepare-datasets'; + +import TextHighlight from '$components/common/text-highlight'; +import Pluralize from '$utils/pluralize'; +import { Pill } from '$styles/pill'; + +import { CardSourcesList } from '$components/common/card-sources'; +import { + getTaxonomy, + TAXONOMY_SOURCE, + TAXONOMY_TOPICS +} from '$utils/veda-data/taxonomies'; +import { generateTaxonomies } from '$utils/veda-data/taxonomies'; +import { StoryData } from '$types/veda'; +import { UseFiltersWithQueryResult } from '$components/common/catalog/controls/hooks/use-filters-with-query'; + +const StoryCount = styled(Subtitle)` + grid-column: 1 / -1; + display: flex; + gap: ${glsp(0.5)}; + span { + text-transform: uppercase; + line-height: 1.5rem; + } +`; + +const BrowseFoldHeader = styled(FoldHeader)` + flex-flow: column nowrap; + align-items: flex-start; +`; + +const FoldWithTopMargin = styled(Fold)` + margin-top: ${glsp()}; +`; + +interface StoryDataWithPath extends StoryData {path: string} +interface HubContentProps { + allStories: StoryDataWithPath[]; + linkProperties: LinkProperties; + pathname: string; + storiesString: {one: string, other: string}; + onFilterchanges: () => UseFiltersWithQueryResult; +} + +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(); + + const { LinkElement, pathAttributeKeyName } = linkProperties; + const storyTaxonomies = generateTaxonomies(allStories); + + const ButtonLinkProps = { + forwardedAs: LinkElement, + [pathAttributeKeyName]: pathname + }; + + function getPillLinkProps(t){ + return { + as: LinkElement, + [pathAttributeKeyName]: `${pathname}?${FilterActions.TAXONOMY}=${encodeURIComponent(JSON.stringify({ Topics: t.id }))}` + }; + } + const displayStories = useMemo( + () => + prepareDatasets(allStories, { + search, + taxonomies, + sortField: 'pubDate', + sortDir: 'desc', + }) as StoryDataWithPath[], + [search, taxonomies, allStories] + ); + + const isFiltering = !!( + (taxonomies && Object.keys(taxonomies).length )|| + search + ); + + return ( + + + Browse + + + + + + + Showing{' '} + {' '} + out of {allStories.length}. + + {isFiltering && ( + + )} + + + {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} + + } + imgSrc={d.media?.src} + imgAlt={d.media?.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 diff --git a/app/scripts/components/stories/hub/index.tsx b/app/scripts/components/stories/hub/index.tsx deleted file mode 100644 index 68e5a4c27..000000000 --- a/app/scripts/components/stories/hub/index.tsx +++ /dev/null @@ -1,245 +0,0 @@ -import React, { useMemo, useRef } from 'react'; -import { useNavigate } from 'react-router'; -import { Link } from 'react-router-dom'; -import styled from 'styled-components'; -import { stories, storyTaxonomies, getString } from 'veda'; -import { glsp } from '@devseed-ui/theme-provider'; -import { Subtitle } from '@devseed-ui/typography'; -import { Button } from '@devseed-ui/button'; -import { CollecticonXmarkSmall } from '@devseed-ui/collecticons'; -import { VerticalDivider } from '@devseed-ui/toolbar'; - -import PublishedDate from '$components/common/pub-date'; -import BrowseControls from '$components/common/browse-controls'; -import { FilterActions } from '$components/common/catalog/utils'; -import { useFiltersWithQS } from '$components/common/catalog/controls/hooks/use-filters-with-query'; -import { LayoutProps } from '$components/common/layout-root'; -import { useSlidingStickyHeaderProps } from '$components/common/layout-root/useSlidingStickyHeaderProps'; -import PageHero from '$components/common/page-hero'; -import { - Fold, - FoldHeader, - FoldHeadline, - FoldTitle -} from '$components/common/fold'; -import { Card } from '$components/common/card'; -import { CardListGrid, CardMeta, CardTopicsList } from '$components/common/card/styles'; -import EmptyHub from '$components/common/empty-hub'; -import { PageMainContent } from '$styles/page'; -import { STORIES_PATH, getStoryPath } from '$utils/routes'; -import TextHighlight from '$components/common/text-highlight'; -import Pluralize from '$utils/pluralize'; -import { Pill } from '$styles/pill'; -import { FeaturedStories } from '$components/common/featured-slider-section'; -import { CardSourcesList } from '$components/common/card-sources'; -import { - getTaxonomy, - TAXONOMY_SOURCE, - TAXONOMY_TOPICS -} from '$utils/veda-data/taxonomies'; -import { - ComponentOverride, - ContentOverride -} from '$components/common/page-overrides'; -import { prepareDatasets } from '$components/common/catalog/prepare-datasets'; -import SmartLink from '$components/common/smart-link'; - -const allStories = Object.values(stories).map((d) => d!.data).filter(d => !d.isHidden); - -const StoryCount = styled(Subtitle)` - grid-column: 1 / -1; - display: flex; - gap: ${glsp(0.5)}; - - span { - text-transform: uppercase; - line-height: 1.5rem; - } -`; - -const BrowseFoldHeader = styled(FoldHeader)` - flex-flow: column nowrap; - align-items: flex-start; -`; - -const FoldWithTopMargin = styled(Fold)` - margin-top: ${glsp()}; -`; - -function StoriesHub() { - const controlVars = useFiltersWithQS({navigate: useNavigate()}); - - const { search, taxonomies, onAction } = controlVars; - - - const displayStories = useMemo( - () => - prepareDatasets(allStories, { - search, - taxonomies, - sortField: 'pubDate', - sortDir: 'desc', - }), - [search, taxonomies] - ); - - const isFiltering = !!( - (taxonomies && Object.keys(taxonomies).length )|| - search - ); - - const browseControlsHeaderRef = useRef(null); - const { headerHeight } = useSlidingStickyHeaderProps(); - - return ( - - - - - - - - - - - Browse - - - - - - - Showing{' '} - {' '} - out of {allStories.length}. - - {isFiltering && ( - - )} - - - {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 ?? getStoryPath(d)}`, - LinkElement: SmartLink, - pathAttributeKeyName: 'to' - }} - title={ - - {d.name} - - } - description={ - - {d.description} - - } - imgSrc={d.media?.src} - imgAlt={d.media?.alt} - footerContent={ - <> - {topics?.length ? ( - -
    Topics
    - {topics.map((t) => ( -
    - { - browseControlsHeaderRef.current?.scrollIntoView(); - }} - > - - {t.name} - - -
    - ))} -
    - ) : null} - - } - /> -
  • - ); - })} -
    - ) : ( - - There are no {getString('stories').other.toLocaleLowerCase()} to - show with the selected filters. - - )} -
    -
    -
    - ); -} - -export default StoriesHub; \ No newline at end of file diff --git a/app/scripts/index.ts b/app/scripts/index.ts index da9959aec..8cf849fb5 100644 --- a/app/scripts/index.ts +++ b/app/scripts/index.ts @@ -14,8 +14,10 @@ import DevseedUiThemeProvider from './theme-provider'; import CatalogView from './components/common/catalog'; import { PageMainContent } from '$styles/page'; import PageHero from '$components/common/page-hero'; +import StoriesHubContent from '$components/stories/hub/hub-content'; import { useFiltersWithQS } from '$components/common/catalog/controls/hooks/use-filters-with-query'; + export { // COMPONENTS Block, @@ -35,6 +37,7 @@ export { PageMainContent, PageHero, ReactQueryProvider, - // HOOKS - useFiltersWithQS, + StoriesHubContent, + // HOOKS and utility functions + useFiltersWithQS }; diff --git a/app/scripts/main.tsx b/app/scripts/main.tsx index d75577007..c0d755e67 100644 --- a/app/scripts/main.tsx +++ b/app/scripts/main.tsx @@ -26,7 +26,7 @@ const Home = lazy(() => import('$components/home')); const About = lazy(() => import('$components/about')); const Development = lazy(() => import('$components/development')); -const StoriesHub = lazy(() => import('$components/stories/hub')); +const StoriesHub = lazy(() => import('$components/stories/hub/container')); const StoriesSingle = lazy(() => import('$components/stories/single')); const DataCatalog = lazy(() => import('$components/data-catalog/container')); diff --git a/app/scripts/types/veda.ts b/app/scripts/types/veda.ts index 0bbfcdbbb..363d0ada5 100644 --- a/app/scripts/types/veda.ts +++ b/app/scripts/types/veda.ts @@ -201,6 +201,7 @@ export interface StoryData { name: string; description: string; pubDate: string; + path?: string; media?: Media; taxonomy: Taxonomy[]; related?: RelatedContentData[]; diff --git a/app/scripts/utils/veda-data/taxonomies.ts b/app/scripts/utils/veda-data/taxonomies.ts index 709fe0d7a..fff424f61 100644 --- a/app/scripts/utils/veda-data/taxonomies.ts +++ b/app/scripts/utils/veda-data/taxonomies.ts @@ -9,7 +9,7 @@ import { // @NOTE: Independent of veda faux modules -export function generateTaxonomies(data: DatasetDataWithEnhancedLayers[] | DatasetData[]): Taxonomy[] { +export function generateTaxonomies(data: DatasetDataWithEnhancedLayers[] | DatasetData[] | StoryData []): Taxonomy[] { const concat = (arr, v) => (arr || []).concat(v); const taxonomyData = {};