Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Stories Hub export #1162

Merged
merged 12 commits into from
Oct 8, 2024
5 changes: 2 additions & 3 deletions app/scripts/components/common/card/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -221,7 +220,7 @@ export function ExternalLinkFlag() {
}

export interface LinkProperties {
LinkElement: JSX.Element | ((props: any) => JSX.Element) | ComponentType<any>;
LinkElement: string | ComponentType<any> | undefined;
pathAttributeKeyName: string;
onLinkClick?: MouseEventHandler;
}
Expand Down Expand Up @@ -322,7 +321,7 @@ function CardComponent(props: CardComponentPropsType) {
{isExternalLink && <ExternalLinkFlag />}
{!isExternalLink && tagLabels && parentTo && (
tagLabels.map((label) => (
<CardLabel as={Link} to={parentTo} key={label}>
<CardLabel as={linkProperties.LinkElement} to={parentTo} key={label}>
{label}
</CardLabel>
))
Expand Down
52 changes: 52 additions & 0 deletions app/scripts/components/stories/hub/container.tsx
Original file line number Diff line number Diff line change
@@ -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 StoriesHub() {
hanbyul-here marked this conversation as resolved.
Show resolved Hide resolved
const controlVars = useFiltersWithQS({navigate: useNavigate()});
return (
<PageMainContent>
<LayoutProps
title={getString('stories').other}
description={getString('storiesBanner').other}
/>
<ComponentOverride with='storiesHubHero'>
<PageHero
title={getString('stories').other}
description={getString('storiesBanner').other}
/>
</ComponentOverride>
<FeaturedStories />
<ContentOverride with='storiesHubContent'>
<HubContent
allStories={allStories}
onFilterchanges={() => controlVars}
linkProperties={{ LinkElement: SmartLink, pathAttributeKeyName: 'to'}}
storiesPagePath={STORIES_PATH}
storiesString={{one: getString('stories').one,other: getString('stories').other}}
/>
</ContentOverride>
</PageMainContent>
);
}

export default StoriesHub;
229 changes: 229 additions & 0 deletions app/scripts/components/stories/hub/hub-content.tsx
Original file line number Diff line number Diff line change
@@ -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;
storiesPagePath: string;
hanbyul-here marked this conversation as resolved.
Show resolved Hide resolved
storiesString: {one: string, other: string};
onFilterchanges: () => UseFiltersWithQueryResult;
}

export default function HubContent(props:HubContentProps) {
const { allStories, linkProperties, storiesPagePath, storiesString, onFilterchanges } = props;
const browseControlsHeaderRef = useRef<HTMLDivElement>(null);
const { headerHeight } = useSlidingStickyHeaderProps();
const { search, taxonomies, onAction } = onFilterchanges();

const { LinkElement, pathAttributeKeyName } = linkProperties;
const storyTaxonomies = generateTaxonomies(allStories);

const ButtonLinkProps = {
forwardedAs: LinkElement,
[pathAttributeKeyName]: storiesPagePath
};

function getPillLinkProps(t){
return {
as: LinkElement,
[pathAttributeKeyName]: `${storiesPagePath}?${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 ( <FoldWithTopMargin>
<BrowseFoldHeader
ref={browseControlsHeaderRef}
style={{
scrollMarginTop: `${headerHeight + 16}px`
}}
>
<FoldHeadline>
<FoldTitle>Browse</FoldTitle>
</FoldHeadline>
<BrowseControls
search={search}
taxonomies={taxonomies}
onAction={onAction}
taxonomiesOptions={storyTaxonomies}
/>
</BrowseFoldHeader>

<StoryCount>
<span>
Showing{' '}
<Pluralize
singular={storiesString.one}
plural={storiesString.other}
count={displayStories.length}
showCount={true}
/>{' '}
out of {allStories.length}.
</span>
{isFiltering && (
<Button {...ButtonLinkProps} size='small'>
Clear filters <CollecticonXmarkSmall />
</Button>
)}
</StoryCount>

{displayStories.length ? (
<CardListGrid>
{displayStories.map((d) => {
const pubDate = new Date(d.pubDate);
const topics = getTaxonomy(d, TAXONOMY_TOPICS)?.values;
return (
<li key={d.id}>
<Card
cardType='classic'
overline={
<CardMeta>
<CardSourcesList
sources={getTaxonomy(d, TAXONOMY_SOURCE)?.values}
rootPath={storiesPagePath}
onSourceClick={(id) => {
onAction(FilterActions.TAXONOMY_MULTISELECT, {
key: TAXONOMY_SOURCE,
value: id
});
browseControlsHeaderRef.current?.scrollIntoView();
}}
/>
<VerticalDivider variation='dark' />

{!isNaN(pubDate.getTime()) && (
<PublishedDate date={pubDate} />
)}
</CardMeta>
}
linkLabel='View more'
linkProperties={{
linkTo: `${d.asLink?.url ?? d.path}`,
LinkElement,
pathAttributeKeyName
}}
title={
<TextHighlight
value={search}
disabled={search.length < 3}
>
{d.name}
</TextHighlight>
}
description={
<TextHighlight
value={search}
disabled={search.length < 3}
>
{d.description}
</TextHighlight>
}
imgSrc={d.media?.src}
imgAlt={d.media?.alt}
footerContent={
<>
{topics?.length ? (
<CardTopicsList>
<dt>Topics</dt>
{topics.map((t) => (
<dd key={t.id}>
<Pill
{...getPillLinkProps(t)}
onClick={() => {
browseControlsHeaderRef.current?.scrollIntoView();
}}
>
<TextHighlight
value={search}
disabled={search.length < 3}
>
{t.name}
</TextHighlight>
</Pill>
</dd>
))}
</CardTopicsList>
) : null}
</>
}
/>
</li>
);
})}
</CardListGrid>
) : (
<EmptyHub>
There are no {storiesString.other.toLocaleLowerCase()} to
show with the selected filters.
</EmptyHub>
)}
</FoldWithTopMargin>);
}
Loading
Loading