diff --git a/src/components/ArticleMetadata/ArticleMetadata.stories.tsx b/src/components/ArticleMetadata/ArticleMetadata.stories.tsx new file mode 100644 index 000000000..2dfa51574 --- /dev/null +++ b/src/components/ArticleMetadata/ArticleMetadata.stories.tsx @@ -0,0 +1,26 @@ +import { Meta, StoryFn } from '@storybook/react'; +import React from 'react'; + +import { ArticleMetadata } from '@/components'; +import { ContentTypeEnum } from '@/constants'; + +export default { + title: 'Components/ArticleMetadata', + component: ArticleMetadata, + args: { + contentType: ContentTypeEnum.ARTICLE, + date: '09 fév. 2021', + readingTime: 24, + authors: [{ username: 'jdoe', name: 'J. Doe' }], + isLoading: false, + }, +} as Meta; + +const Template: StoryFn = (args) => ; + +export const Overview = Template.bind({}); + +export const ArticleMetadataIsLoading = Template.bind({}); +ArticleMetadataIsLoading.args = { + isLoading: true, +}; diff --git a/src/components/ArticleMetadata/ArticleMetadata.tsx b/src/components/ArticleMetadata/ArticleMetadata.tsx new file mode 100644 index 000000000..28cf1261d --- /dev/null +++ b/src/components/ArticleMetadata/ArticleMetadata.tsx @@ -0,0 +1,66 @@ +import { ColorSystemProps, Flex, Skeleton, SpacingSystemProps, Text } from '@eleven-labs/design-system'; +import React from 'react'; + +import { SeparatorCircle } from '@/components'; + +export type ArticleMetadataOptions = { + date?: React.ReactNode; + readingTime?: number; + authors?: { username: string; name: string }[]; + isLoading?: boolean; + displayedFields?: ('contentType' | 'date' | 'readingTime' | 'authors')[]; +}; + +export type ArticleMetadataProps = ArticleMetadataOptions & SpacingSystemProps & ColorSystemProps; + +export const ArticleMetadata: React.FC = ({ + date, + readingTime, + authors, + isLoading = false, + displayedFields = ['date', 'readingTime', 'authors'], + ...props +}) => { + const fields = displayedFields.reduce((currentFields, displayedField, index) => { + switch (displayedField) { + case 'date': + currentFields.push( + + {date && {date}} + + ); + break; + case 'readingTime': + currentFields.push( + + {readingTime && {`${readingTime}mn`}} + + ); + break; + case 'authors': + currentFields.push( + + {authors && + authors.map((author, authorIndex) => ( + + {author.name} + {authorIndex !== authors.length - 1 ? ' & ' : ''} + + ))} + + ); + break; + } + + if (index !== displayedFields.length - 1) { + currentFields.push(); + } + + return currentFields; + }, []); + return ( + + {fields} + + ); +}; diff --git a/src/components/ArticleMetadata/index.ts b/src/components/ArticleMetadata/index.ts new file mode 100644 index 000000000..c329781b4 --- /dev/null +++ b/src/components/ArticleMetadata/index.ts @@ -0,0 +1 @@ +export * from './ArticleMetadata'; diff --git a/src/components/AutocompleteField/AutocompleteField.stories.tsx b/src/components/AutocompleteField/AutocompleteField.stories.tsx index a5f81a2c9..1f2fe7aba 100644 --- a/src/components/AutocompleteField/AutocompleteField.stories.tsx +++ b/src/components/AutocompleteField/AutocompleteField.stories.tsx @@ -3,6 +3,7 @@ import { Meta, StoryFn } from '@storybook/react'; import React from 'react'; import { AutocompleteField } from '@/components'; +import { ContentTypeEnum } from '@/constants'; export default { title: 'Components/AutocompleteField', @@ -30,29 +31,53 @@ AutocompleteFieldWithResult.args = { defaultValue: 'React', items: [ { + contentType: ContentTypeEnum.ARTICLE, title: 'React SSR', description: 'Lorem ipsum dolor sit react, consectetur adipiscing elit. In nec blandit neque', + date: '24 fév. 2021', + readingTime: 24, + authors: [{ username: 'jdoe', name: 'J. Doe' }], }, { + contentType: ContentTypeEnum.ARTICLE, title: 'React SSG', description: 'Mauris semper venenatis dolor vel posuere. Fusce imperdiet react purus euismod fermentum', + date: '22 fév. 2021', + readingTime: 22, + authors: [{ username: 'jdoe', name: 'J. Doe' }], }, { + contentType: ContentTypeEnum.TUTORIAL, title: 'React + Astro', description: 'Ut velit elit, finibus eu turpis quis, luctus sodales elit', + date: '18 fév. 2021', + readingTime: 18, + authors: [{ username: 'jdoe', name: 'J. Doe' }], }, { + contentType: ContentTypeEnum.ARTICLE, title: 'React + NextJS', description: 'Quisque ac consectetur massa. Praesent pellentesque, orci sit amet cursus venenatis', + date: '16 fév. 2021', + readingTime: 9, + authors: [{ username: 'jdoe', name: 'J. Doe' }], }, { + contentType: ContentTypeEnum.ARTICLE, title: 'React + Apollo Client', description: 'Phasellus ac sodales mi. Ut egestas dui react enim vehicula pulvinar', + date: '12 fév. 2021', + readingTime: 10, + authors: [{ username: 'jdoe', name: 'J. Doe' }], }, { + contentType: ContentTypeEnum.ARTICLE, title: 'React vs Vue', description: 'Suspendisse potenti. Etiam egestas lacus velit, et tempor metus mollis react. Donec ut vulputate leo', + date: '09 fév. 2021', + readingTime: 6, + authors: [{ username: 'jdoe', name: 'J. Doe' }], }, ], }; diff --git a/src/components/AutocompleteField/AutocompleteResult/AutocompleteResult.tsx b/src/components/AutocompleteField/AutocompleteResult/AutocompleteResult.tsx index 74df1ac72..55254109f 100644 --- a/src/components/AutocompleteField/AutocompleteResult/AutocompleteResult.tsx +++ b/src/components/AutocompleteField/AutocompleteResult/AutocompleteResult.tsx @@ -1,14 +1,30 @@ import './AutocompleteResult.scss'; -import { AsProps, Box, BoxProps, forwardRef, Heading, Link, Text, TextHighlight } from '@eleven-labs/design-system'; +import { + AsProps, + Box, + BoxProps, + Flex, + forwardRef, + Heading, + Link, + Text, + TextHighlight, +} from '@eleven-labs/design-system'; import classNames from 'classnames'; import React from 'react'; +import { ArticleMetadata, TutoTag } from '@/components'; +import { ContentTypeEnum } from '@/constants'; import { getPathFile } from '@/helpers/assetHelper'; export interface AutocompleteItem { + contentType: ContentTypeEnum.ARTICLE | ContentTypeEnum.TUTORIAL; title: string; description: string; + date: React.ReactNode; + readingTime: number; + authors?: { username: string; name: string }[]; } export type AutocompleteResultOptions = { @@ -41,7 +57,7 @@ export const AutocompleteResult = forwardRef( ); diff --git a/src/components/PostPreview/PostPreview.stories.tsx b/src/components/PostPreview/PostPreview.stories.tsx index 9a23838c3..465fa6b66 100644 --- a/src/components/PostPreview/PostPreview.stories.tsx +++ b/src/components/PostPreview/PostPreview.stories.tsx @@ -26,7 +26,7 @@ export default { const Template: StoryFn = (args) => ; -export const PostPreviewWithData = Template.bind({}); +export const Overview = Template.bind({}); export const PostPreviewIsLoading = Template.bind({}); PostPreviewIsLoading.args = { diff --git a/src/components/PostPreview/PostPreview.tsx b/src/components/PostPreview/PostPreview.tsx index b644c85b1..883ab9ca6 100644 --- a/src/components/PostPreview/PostPreview.tsx +++ b/src/components/PostPreview/PostPreview.tsx @@ -4,7 +4,7 @@ import { AsProps, Box, BoxProps, Flex, Heading, Link, Skeleton, Text } from '@el import classNames from 'classnames'; import React from 'react'; -import { SeparatorCircle, TutoTag } from '@/components'; +import { ArticleMetadata, TutoTag } from '@/components'; import { ContentTypeEnum } from '@/constants'; export type PostPreviewOptions = { @@ -65,25 +65,7 @@ export const PostPreview: React.FC = ({ {excerpt} - - - {date && {date}} - - - - {readingTime && {`${readingTime}mn`}} - - - - {authors && - authors.map((author, authorIndex) => ( - - {author.name} - {authorIndex !== authors.length - 1 ? ' & ' : ''} - - ))} - - + ); }; diff --git a/src/components/index.ts b/src/components/index.ts index 33d9c1f5e..abc2d78cf 100644 --- a/src/components/index.ts +++ b/src/components/index.ts @@ -10,3 +10,4 @@ export * from './SeparatorCircle'; export * from './BackLink'; export * from './ShareLinks'; export * from './TutoTag'; +export * from './ArticleMetadata'; diff --git a/src/containers/HeaderContainer/useHeaderContainer.tsx b/src/containers/HeaderContainer/useHeaderContainer.tsx index 124cc6de6..b30512d2d 100644 --- a/src/containers/HeaderContainer/useHeaderContainer.tsx +++ b/src/containers/HeaderContainer/useHeaderContainer.tsx @@ -9,21 +9,22 @@ import { LinkContainer } from '@/containers/LinkContainer'; import { trackContentSearchEvent } from '@/helpers/dataLayerHelper'; import { generatePath } from '@/helpers/routerHelper'; import { useAlgoliaSearchIndex } from '@/hooks/useAlgoliaSearchIndex'; +import { useDateToString } from '@/hooks/useDateToString'; import { useDebounce } from '@/hooks/useDebounce'; import { HeaderProps } from '@/templates/LayoutTemplate'; +import { AlgoliaPostData } from '@/types'; export const useHeaderContainer = (): HeaderProps => { const { t, i18n } = useTranslation(); const location = useLocation(); const navigate = useNavigate(); + const { getDateToString } = useDateToString(); const searchParams = new URLSearchParams(!IS_SSR ? location.search : ''); const [autocompleteIsDisplayed, setAutocompleteIsDisplayed] = React.useState(false); const [search, setSearch] = React.useState(searchParams.get('search') ?? ''); const debouncedSearch = useDebounce(search, 500); - const [searchHits, setSearchHits] = React.useState< - { objectID: string; slug: string; title: string; excerpt: string }[] - >([]); + const [searchHits, setSearchHits] = React.useState([]); const algoliaSearchIndex = useAlgoliaSearchIndex(); const onToggleSearch = React.useCallback(() => { @@ -49,7 +50,7 @@ export const useHeaderContainer = (): HeaderProps => { trackContentSearchEvent(debouncedSearch); setAutocompleteIsDisplayed(true); algoliaSearchIndex - .search<{ slug: string; title: string; excerpt: string }>(debouncedSearch, { + .search(debouncedSearch, { hitsPerPage: NUMBER_OF_ITEMS_PER_PAGE, facetFilters: [`lang:${i18n.language}`], }) @@ -63,8 +64,15 @@ export const useHeaderContainer = (): HeaderProps => { () => searchHits.map((hit) => ({ id: hit.objectID, + contentType: hit.contentType, title: hit.title, description: hit.excerpt, + date: getDateToString({ date: hit.date }), + readingTime: hit.readingTime, + authors: hit.authorUsernames.map((authorUsername, index) => ({ + username: authorUsername, + name: hit.authorNames[index], + })), as: LinkContainer, hrefLang: i18n.language, to: generatePath(PATHS.POST, { lang: i18n.language, slug: hit.slug }), diff --git a/src/helpers/indexationAlgoliaHelper.ts b/src/helpers/indexationAlgoliaHelper.ts index 36261d2b1..4b3df34d1 100644 --- a/src/helpers/indexationAlgoliaHelper.ts +++ b/src/helpers/indexationAlgoliaHelper.ts @@ -53,6 +53,7 @@ export const indexationAlglolia = async (options: { const algoliaSearchClient = getAlgoliaSearchClient({ appId: options.appId, apiKey: options.apiIndexingKey }); const algoliaSearchIndex = getAlgoliaSearchIndex({ algoliaSearchClient, index: options.index }); + await algoliaSearchIndex.clearObjects(); const objectIDs = await savePosts({ posts, authors, algoliaSearchIndex }); console.info(`Number of posts indexed on algolia: ${objectIDs.length}`); diff --git a/src/templates/LayoutTemplate/LayoutTemplate.stories.tsx b/src/templates/LayoutTemplate/LayoutTemplate.stories.tsx index 4e078dfa5..4eb461a87 100644 --- a/src/templates/LayoutTemplate/LayoutTemplate.stories.tsx +++ b/src/templates/LayoutTemplate/LayoutTemplate.stories.tsx @@ -113,7 +113,10 @@ LayoutTemplateWithAutocompleteIsOpen.args = { href: '#', }, autocompleteIsDisplayed: true, - autocomplete: AutocompleteFieldStories.AutocompleteFieldWithResult.args as HeaderProps['autocomplete'], + autocomplete: { + ...AutocompleteFieldStories.default.args, + ...AutocompleteFieldStories.AutocompleteFieldWithResult.args, + } as HeaderProps['autocomplete'], }), }; @@ -124,6 +127,9 @@ LayoutTemplateWithAutocompleteAndResultNotFound.args = { href: '#', }, autocompleteIsDisplayed: true, - autocomplete: AutocompleteFieldStories.AutocompleteFieldWithNoResult.args as HeaderProps['autocomplete'], + autocomplete: { + ...AutocompleteFieldStories.default.args, + ...AutocompleteFieldStories.AutocompleteFieldWithResult.args, + } as HeaderProps['autocomplete'], }), };