diff options
Diffstat (limited to 'src')
| -rw-r--r-- | src/components/organisms/layout/posts-list.module.scss | 11 | ||||
| -rw-r--r-- | src/components/organisms/layout/posts-list.tsx | 8 | ||||
| -rw-r--r-- | src/components/organisms/layout/summary.module.scss | 1 | ||||
| -rw-r--r-- | src/pages/blog/index.tsx | 28 | ||||
| -rw-r--r-- | src/pages/thematique/[slug].tsx | 247 | ||||
| -rw-r--r-- | src/services/graphql/thematics.query.ts | 7 | ||||
| -rw-r--r-- | src/services/graphql/thematics.ts | 133 | ||||
| -rw-r--r-- | src/utils/helpers/pages.ts | 47 | 
8 files changed, 460 insertions, 22 deletions
| diff --git a/src/components/organisms/layout/posts-list.module.scss b/src/components/organisms/layout/posts-list.module.scss index 8021b2b..a006914 100644 --- a/src/components/organisms/layout/posts-list.module.scss +++ b/src/components/organisms/layout/posts-list.module.scss @@ -23,6 +23,17 @@  }  .year { +  padding-bottom: fun.convert-px(3); +  background: linear-gradient( +      to top, +      var(--color-primary-dark) 0.3rem, +      transparent 0.3rem +    ) +    0 0 / 3rem 100% no-repeat; +  font-size: var(--font-size-2xl); +  font-weight: 500; +  text-shadow: fun.convert-px(1) fun.convert-px(1) 0 var(--color-shadow-light); +    @include mix.media("screen") {      @include mix.dimensions("md") {        grid-column: 1; diff --git a/src/components/organisms/layout/posts-list.tsx b/src/components/organisms/layout/posts-list.tsx index 4d77d20..9dfe254 100644 --- a/src/components/organisms/layout/posts-list.tsx +++ b/src/components/organisms/layout/posts-list.tsx @@ -122,18 +122,20 @@ const PostsList: FC<PostsListProps> = ({     * @returns {JSX.Element | JSX.Element[]} The posts list.     */    const getPosts = (): JSX.Element | JSX.Element[] => { -    if (!byYear) return getList(posts); +    const firstLevel = titleLevel || 2; +    if (!byYear) return getList(posts, firstLevel);      const postsPerYear = sortPostsByYear(posts);      const years = Object.keys(postsPerYear).reverse(); +    const nextLevel = (firstLevel + 1) as HeadingLevel;      return years.map((year) => {        return (          <section key={year} className={styles.section}> -          <Heading level={2} className={styles.year}> +          <Heading level={firstLevel} className={styles.year}>              {year}            </Heading> -          {getList(postsPerYear[year], titleLevel)} +          {getList(postsPerYear[year], nextLevel)}          </section>        );      }); diff --git a/src/components/organisms/layout/summary.module.scss b/src/components/organisms/layout/summary.module.scss index 5f22fbb..9d28bc6 100644 --- a/src/components/organisms/layout/summary.module.scss +++ b/src/components/organisms/layout/summary.module.scss @@ -84,6 +84,7 @@    margin: 0;    background: none;    color: inherit; +  font-size: var(--font-size-2xl);    text-shadow: none;  } diff --git a/src/pages/blog/index.tsx b/src/pages/blog/index.tsx index 05e73db..3acf6a9 100644 --- a/src/pages/blog/index.tsx +++ b/src/pages/blog/index.tsx @@ -24,6 +24,10 @@ import {  } from '@ts/types/raw-data';  import { settings } from '@utils/config';  import { loadTranslation, type Messages } from '@utils/helpers/i18n'; +import { +  getLinksListItems, +  getPageLinkFromRawData, +} from '@utils/helpers/pages';  import usePagination from '@utils/hooks/use-pagination';  import useSettings from '@utils/hooks/use-settings';  import { GetStaticProps, NextPage } from 'next'; @@ -203,20 +207,6 @@ const BlogPage: NextPage<BlogPageProps> = ({      setSize((prevSize) => prevSize + 1);    }; -  const getLinksListItems = ( -    rawData: RawThematicPreview[] | RawTopicPreview[], -    kind: 'thematic' | 'topic' -  ): LinksListItems[] => { -    const baseUrl = kind === 'thematic' ? '/thematique/' : '/sujet/'; - -    return rawData.map((taxonomy) => { -      return { -        name: taxonomy.title, -        url: `${baseUrl}${taxonomy.slug}`, -      }; -    }); -  }; -    const thematicsListTitle = intl.formatMessage({      defaultMessage: 'Thematics',      description: 'BlogPage: thematics list widget title', @@ -251,13 +241,19 @@ const BlogPage: NextPage<BlogPageProps> = ({          widgets={[            <LinksListWidget              key="thematics-list" -            items={getLinksListItems(thematicsList, 'thematic')} +            items={getLinksListItems( +              thematicsList.map(getPageLinkFromRawData), +              'thematic' +            )}              title={thematicsListTitle}              level={2}            />,            <LinksListWidget              key="topics-list" -            items={getLinksListItems(topicsList, 'topic')} +            items={getLinksListItems( +              topicsList.map(getPageLinkFromRawData), +              'topic' +            )}              title={topicsListTitle}              level={2}            />, diff --git a/src/pages/thematique/[slug].tsx b/src/pages/thematique/[slug].tsx new file mode 100644 index 0000000..dd7e80d --- /dev/null +++ b/src/pages/thematique/[slug].tsx @@ -0,0 +1,247 @@ +import Heading from '@components/atoms/headings/heading'; +import { type BreadcrumbItem } from '@components/molecules/nav/breadcrumb'; +import PostsList, { type Post } from '@components/organisms/layout/posts-list'; +import LinksListWidget from '@components/organisms/widgets/links-list-widget'; +import PageLayout, { +  type PageLayoutProps, +} from '@components/templates/page/page-layout'; +import { +  getAllThematicsSlugs, +  getThematicBySlug, +  getThematicsPreview, +  getTotalThematics, +} from '@services/graphql/thematics'; +import { type Article, type PageLink, type Thematic } from '@ts/types/app'; +import { loadTranslation, type Messages } from '@utils/helpers/i18n'; +import { +  getLinksListItems, +  getPageLinkFromRawData, +  getPostMeta, +} from '@utils/helpers/pages'; +import useSettings from '@utils/hooks/use-settings'; +import { GetStaticPaths, GetStaticProps, NextPage } from 'next'; +import Head from 'next/head'; +import { useRouter } from 'next/router'; +import Script from 'next/script'; +import { ParsedUrlQuery } from 'querystring'; +import { useIntl } from 'react-intl'; +import { Article as ArticleSchema, Graph, WebPage } from 'schema-dts'; + +export type ThematicPageProps = { +  currentThematic: Thematic; +  thematics: PageLink[]; +  translation: Messages; +}; + +const ThematicPage: NextPage<ThematicPageProps> = ({ +  currentThematic, +  thematics, +}) => { +  const { content, intro, meta, slug, title } = currentThematic; +  const { articles, dates, seo, topics } = meta; +  const intl = useIntl(); +  const homeLabel = intl.formatMessage({ +    defaultMessage: 'Home', +    description: 'Breadcrumb: home label', +    id: 'j5k9Fe', +  }); +  const blogLabel = intl.formatMessage({ +    defaultMessage: 'Blog', +    description: 'Breadcrumb: blog label', +    id: 'Es52wh', +  }); +  const breadcrumb: BreadcrumbItem[] = [ +    { id: 'home', name: homeLabel, url: '/' }, +    { id: 'blog', name: blogLabel, url: '/blog' }, +    { id: 'thematic', name: title, url: `/thematique/${slug}` }, +  ]; + +  const headerMeta: PageLayoutProps['headerMeta'] = { +    publication: { date: dates.publication }, +    update: dates.update ? { date: dates.update } : undefined, +  }; + +  const { website } = useSettings(); +  const { asPath } = useRouter(); +  const pageUrl = `${website.url}${asPath}`; +  const pagePublicationDate = new Date(dates.publication); +  const pageUpdateDate = dates.update ? new Date(dates.update) : undefined; + +  const webpageSchema: WebPage = { +    '@id': `${pageUrl}`, +    '@type': 'WebPage', +    breadcrumb: { '@id': `${website.url}/#breadcrumb` }, +    name: seo.title, +    description: seo.description, +    inLanguage: website.locales.default, +    reviewedBy: { '@id': `${website.url}/#branding` }, +    url: `${website.url}`, +  }; + +  const articleSchema: ArticleSchema = { +    '@id': `${website.url}/#thematic`, +    '@type': 'Article', +    name: title, +    description: intro, +    author: { '@id': `${website.url}/#branding` }, +    copyrightYear: pagePublicationDate.getFullYear(), +    creator: { '@id': `${website.url}/#branding` }, +    dateCreated: pagePublicationDate.toISOString(), +    dateModified: pageUpdateDate && pageUpdateDate.toISOString(), +    datePublished: pagePublicationDate.toISOString(), +    editor: { '@id': `${website.url}/#branding` }, +    headline: title, +    inLanguage: website.locales.default, +    isPartOf: { '@id': `${website.url}/blog` }, +    license: 'https://creativecommons.org/licenses/by-sa/4.0/deed.fr', +    mainEntityOfPage: { '@id': `${pageUrl}` }, +    subjectOf: { '@id': `${website.url}/blog` }, +  }; + +  const schemaJsonLd: Graph = { +    '@context': 'https://schema.org', +    '@graph': [webpageSchema, articleSchema], +  }; + +  const getPosts = (array: Article[]): Post[] => { +    return array.map((article) => { +      const { +        intro: articleIntro, +        meta: articleMeta, +        slug: articleSlug, +        ...remainingData +      } = article; + +      const { cover, ...remainingMeta } = articleMeta; + +      return { +        cover, +        excerpt: articleIntro, +        meta: getPostMeta(remainingMeta), +        url: `/article/${articleSlug}`, +        ...remainingData, +      }; +    }); +  }; + +  const thematicsListTitle = intl.formatMessage({ +    defaultMessage: 'Other thematics', +    description: 'ThematicPage: other thematics list widget title', +    id: 'KVSWGP', +  }); + +  const topicsListTitle = intl.formatMessage({ +    defaultMessage: 'Related topics', +    description: 'ThematicPage: related topics list widget title', +    id: '/42Z0z', +  }); + +  return ( +    <> +      <Head> +        <title>{seo.title}</title> +        <meta name="description" content={seo.description} /> +        <meta property="og:url" content={`${pageUrl}`} /> +        <meta property="og:type" content="article" /> +        <meta property="og:title" content={title} /> +        <meta property="og:description" content={intro} /> +      </Head> +      <Script +        id="schema-project" +        type="application/ld+json" +        dangerouslySetInnerHTML={{ __html: JSON.stringify(schemaJsonLd) }} +      /> +      <PageLayout +        breadcrumb={breadcrumb} +        title={title} +        intro={intro} +        headerMeta={headerMeta} +        widgets={ +          topics +            ? [ +                <LinksListWidget +                  key="thematics" +                  items={getLinksListItems(thematics, 'thematic')} +                  title={thematicsListTitle} +                  level={2} +                />, +                <LinksListWidget +                  key="related-topics" +                  items={getLinksListItems(topics, 'topic')} +                  title={topicsListTitle} +                  level={2} +                />, +              ] +            : [] +        } +      > +        <div dangerouslySetInnerHTML={{ __html: content }} /> +        {articles && ( +          <> +            <Heading level={2}> +              {intl.formatMessage( +                { +                  defaultMessage: 'All posts in {thematicName}', +                  description: 'ThematicPage: posts list heading', +                  id: 'LszkU6', +                }, +                { thematicName: title } +              )} +            </Heading> +            <PostsList +              posts={getPosts(articles)} +              total={articles.length} +              titleLevel={3} +              byYear={true} +            /> +          </> +        )} +      </PageLayout> +    </> +  ); +}; + +interface ThematicParams extends ParsedUrlQuery { +  slug: string; +} + +export const getStaticProps: GetStaticProps<ThematicPageProps> = async ({ +  locale, +  params, +}) => { +  const currentThematic = await getThematicBySlug( +    params!.slug as ThematicParams['slug'] +  ); +  const totalThematics = await getTotalThematics(); +  const allThematicsEdges = await getThematicsPreview({ +    first: totalThematics, +  }); +  const allThematics = allThematicsEdges.edges.map((edge) => +    getPageLinkFromRawData(edge.node) +  ); +  const translation = await loadTranslation(locale); + +  return { +    props: { +      currentThematic: JSON.parse(JSON.stringify(currentThematic)), +      thematics: allThematics.filter( +        (thematic) => thematic.slug !== (params!.slug as ThematicParams['slug']) +      ), +      translation, +    }, +  }; +}; + +export const getStaticPaths: GetStaticPaths = async () => { +  const slugs = await getAllThematicsSlugs(); +  const paths = slugs.map((slug) => { +    return { params: { slug } }; +  }); + +  return { +    paths, +    fallback: false, +  }; +}; + +export default ThematicPage; diff --git a/src/services/graphql/thematics.query.ts b/src/services/graphql/thematics.query.ts index db8e751..3b3ebd6 100644 --- a/src/services/graphql/thematics.query.ts +++ b/src/services/graphql/thematics.query.ts @@ -15,6 +15,13 @@ export const thematicBySlugQuery = `query ThematicBy($slug: ID!) {                }              }            } +          author { +            node { +              gravatarUrl +              name +              url +            } +          }            commentCount            contentParts {              beforeMore diff --git a/src/services/graphql/thematics.ts b/src/services/graphql/thematics.ts index 7003e08..e526db8 100644 --- a/src/services/graphql/thematics.ts +++ b/src/services/graphql/thematics.ts @@ -1,6 +1,20 @@ -import { RawThematicPreview, TotalItems } from '@ts/types/raw-data'; +import { PageLink, Slug, Thematic } from '@ts/types/app'; +import { +  RawArticle, +  RawThematic, +  RawThematicPreview, +  TotalItems, +} from '@ts/types/raw-data'; +import { getImageFromRawData } from '@utils/helpers/images'; +import { getPageLinkFromRawData } from '@utils/helpers/pages';  import { EdgesResponse, EdgesVars, fetchAPI, getAPIUrl } from './api'; -import { thematicsListQuery, totalThematicsQuery } from './thematics.query'; +import { getArticleFromRawData } from './articles'; +import { +  thematicBySlugQuery, +  thematicsListQuery, +  thematicsSlugQuery, +  totalThematicsQuery, +} from './thematics.query';  /**   * Retrieve the total number of thematics. @@ -32,3 +46,118 @@ export const getThematicsPreview = async (    return response.thematics;  }; + +/** + * Convert raw data to an Thematic object. + * + * @param {RawThematic} data - The page raw data. + * @returns {Thematic} The page data. + */ +export const getThematicFromRawData = (data: RawThematic): Thematic => { +  const { +    acfThematics, +    contentParts, +    databaseId, +    date, +    featuredImage, +    info, +    modified, +    slug, +    title, +    seo, +  } = data; + +  /** +   * Retrieve an array of related topics. +   * +   * @param posts - The thematic posts. +   * @returns {PageLink[]} An array of topics links. +   */ +  const getRelatedTopics = (posts: RawArticle[]): PageLink[] => { +    const topics: PageLink[] = []; + +    posts.forEach((post) => { +      if (post.acfPosts.postsInTopic) { +        post.acfPosts.postsInTopic.forEach((topic) => +          topics.push(getPageLinkFromRawData(topic)) +        ); +      } +    }); + +    const topicsIds = topics.map((topic) => topic.id); +    const uniqueTopics = topics.filter( +      ({ id }, index) => !topicsIds.includes(id, index + 1) +    ); +    const sortTopicByName = (a: PageLink, b: PageLink) => { +      var nameA = a.name.toUpperCase(); // ignore upper and lowercase +      var nameB = b.name.toUpperCase(); // ignore upper and lowercase +      if (nameA < nameB) { +        return -1; +      } +      if (nameA > nameB) { +        return 1; +      } + +      // names must be equal +      return 0; +    }; + +    return uniqueTopics.sort(sortTopicByName); +  }; + +  return { +    content: contentParts.afterMore, +    id: databaseId, +    intro: contentParts.beforeMore, +    meta: { +      articles: acfThematics.postsInThematic.map((post) => +        getArticleFromRawData(post) +      ), +      cover: featuredImage?.node +        ? getImageFromRawData(featuredImage.node) +        : undefined, +      dates: { publication: date, update: modified }, +      readingTime: info.readingTime, +      seo: { +        description: seo?.metaDesc || '', +        title: seo?.title || '', +      }, +      topics: getRelatedTopics(acfThematics.postsInThematic), +      wordsCount: info.wordsCount, +    }, +    slug, +    title, +  }; +}; + +/** + * Retrieve a Thematic object by slug. + * + * @param {string} slug - The thematic slug. + * @returns {Promise<Article>} The requested thematic. + */ +export const getThematicBySlug = async (slug: string): Promise<Thematic> => { +  const response = await fetchAPI<RawThematic, typeof thematicBySlugQuery>({ +    api: getAPIUrl(), +    query: thematicBySlugQuery, +    variables: { slug }, +  }); + +  return getThematicFromRawData(response.thematic); +}; + +/** + * Retrieve all the thematics slugs. + * + * @returns {Promise<string[]>} - An array of thematics slugs. + */ +export const getAllThematicsSlugs = async (): Promise<string[]> => { +  const totalThematics = await getTotalThematics(); +  const response = await fetchAPI<Slug, typeof thematicsSlugQuery>({ +    api: getAPIUrl(), +    query: thematicsSlugQuery, +    variables: { first: totalThematics }, +  }); + +  return response.thematics.edges.map((edge) => edge.node.slug); +}; diff --git a/src/utils/helpers/pages.ts b/src/utils/helpers/pages.ts index d757f8c..93582f0 100644 --- a/src/utils/helpers/pages.ts +++ b/src/utils/helpers/pages.ts @@ -1,4 +1,6 @@ -import { type PageLink } from '@ts/types/app'; +import { type Post } from '@components/organisms/layout/posts-list'; +import { type LinksListItems } from '@components/organisms/widgets/links-list-widget'; +import { type Meta, type PageLink } from '@ts/types/app';  import {    type RawThematicPreview,    type RawTopicPreview, @@ -24,3 +26,46 @@ export const getPageLinkFromRawData = (      slug,    };  }; + +/** + * Convert page link data to an array of links items. + * + * @param {PageLink[]} links - An array of page links. + * @param {'thematic'|'topic'} kind - The page links kind. + * @returns {LinksListItem[]} An array of links items. + */ +export const getLinksListItems = ( +  links: PageLink[], +  kind: 'thematic' | 'topic' +): LinksListItems[] => { +  const baseUrl = kind === 'thematic' ? '/thematique/' : '/sujet/'; + +  return links.map((link) => { +    return { +      name: link.name, +      url: `${baseUrl}${link.slug}`, +    }; +  }); +}; + +/** + * Retrieve the formatted meta. + * + * @param {Meta<'article'>} meta - The article meta. + * @returns {Post['meta']} The formatted meta. + */ +export const getPostMeta = (meta: Meta<'article'>): Post['meta'] => { +  const { commentsCount, dates, thematics, topics, wordsCount } = meta; + +  return { +    commentsCount, +    dates, +    readingTime: { wordsCount: wordsCount || 0, onlyMinutes: true }, +    thematics: thematics?.map((thematic) => { +      return { ...thematic, url: `/thematique/${thematic.slug}` }; +    }), +    topics: topics?.map((topic) => { +      return { ...topic, url: `/sujet/${topic.slug}` }; +    }), +  }; +}; | 
