diff options
| author | Armand Philippot <git@armandphilippot.com> | 2022-05-02 18:57:29 +0200 |
|---|---|---|
| committer | Armand Philippot <git@armandphilippot.com> | 2022-05-03 15:22:24 +0200 |
| commit | 732d0943f8041d76262222a092b014f2557085ef (patch) | |
| tree | 16f6f76648b479a9591400ab15bb3e9c914f2226 | |
| parent | ca921d7536cfe950b5a7d442977bbf900b48faf4 (diff) | |
chore: add homepage
19 files changed, 745 insertions, 93 deletions
diff --git a/src/components/atoms/lists/list.module.scss b/src/components/atoms/lists/list.module.scss index df3b49c..f647072 100644 --- a/src/components/atoms/lists/list.module.scss +++ b/src/components/atoms/lists/list.module.scss @@ -1,3 +1,5 @@ +@use "@styles/abstracts/placeholders"; + .list { margin: 0; @@ -36,4 +38,18 @@ margin-bottom: var(--spacing-2xs); } } + + &--flex { + @extend %reset-list; + + display: flex; + flex-flow: row wrap; + gap: var(--spacing-sm); + } + + &--flex &--flex { + display: initial; + position: relative; + top: var(--spacing-2xs); + } } diff --git a/src/components/atoms/lists/list.stories.tsx b/src/components/atoms/lists/list.stories.tsx index 3a80962..54fdd3a 100644 --- a/src/components/atoms/lists/list.stories.tsx +++ b/src/components/atoms/lists/list.stories.tsx @@ -39,8 +39,8 @@ export default { control: { type: 'select', }, - description: 'The list kind: ordered or unordered.', - options: ['ordered', 'unordered'], + description: 'The list kind: flex, ordered or unordered.', + options: ['flex', 'ordered', 'unordered'], table: { category: 'Options', defaultValue: { summary: 'unordered' }, @@ -72,6 +72,15 @@ const items: ListItem[] = [ ]; /** + * List Stories - Flex list + */ +export const Flex = Template.bind({}); +Flex.args = { + items, + kind: 'flex', +}; + +/** * List Stories - Ordered list */ export const Ordered = Template.bind({}); diff --git a/src/components/atoms/lists/list.tsx b/src/components/atoms/lists/list.tsx index 6726802..711ade1 100644 --- a/src/components/atoms/lists/list.tsx +++ b/src/components/atoms/lists/list.tsx @@ -30,9 +30,9 @@ export type ListProps = { */ itemsClassName?: string; /** - * The list kind (ordered or unordered). + * The list kind. */ - kind?: 'ordered' | 'unordered'; + kind?: 'ordered' | 'unordered' | 'flex'; /** * Set margin between list items. Default: true. */ diff --git a/src/components/molecules/layout/card.module.scss b/src/components/molecules/layout/card.module.scss index 85c319a..d5b9836 100644 --- a/src/components/molecules/layout/card.module.scss +++ b/src/components/molecules/layout/card.module.scss @@ -19,7 +19,8 @@ .cover { align-self: flex-start; - max-height: fun.convert-px(150); + place-content: center; + height: fun.convert-px(150); margin: auto; border-bottom: fun.convert-px(1) solid var(--color-border); } diff --git a/src/components/molecules/layout/card.tsx b/src/components/molecules/layout/card.tsx index 15927e9..89f100e 100644 --- a/src/components/molecules/layout/card.tsx +++ b/src/components/molecules/layout/card.tsx @@ -93,7 +93,7 @@ const Card: FC<CardProps> = ({ {title} </Heading> </header> - {tagline && <div className={styles.tagline}>{tagline}</div>} + <div className={styles.tagline}>{tagline}</div> {meta && ( <footer className={styles.footer}> <DescriptionList diff --git a/src/components/molecules/layout/page-header.tsx b/src/components/molecules/layout/page-header.tsx index 174e246..1663085 100644 --- a/src/components/molecules/layout/page-header.tsx +++ b/src/components/molecules/layout/page-header.tsx @@ -11,7 +11,7 @@ export type PageHeaderProps = { /** * The page introduction. */ - intro?: string; + intro?: string | JSX.Element; /** * The page metadata. */ diff --git a/src/components/molecules/nav/nav.stories.tsx b/src/components/molecules/nav/nav.stories.tsx index 25455fd..5cef5f0 100644 --- a/src/components/molecules/nav/nav.stories.tsx +++ b/src/components/molecules/nav/nav.stories.tsx @@ -11,6 +11,19 @@ export default { title: 'Molecules/Navigation/Nav', component: NavComponent, argTypes: { + 'aria-label': { + control: { + type: 'text', + }, + description: 'An accessible name for the navigation.', + table: { + category: 'Accessibility', + }, + type: { + name: 'string', + required: false, + }, + }, className: { control: { type: 'text', @@ -46,6 +59,19 @@ export default { required: true, }, }, + listClassName: { + control: { + type: 'text', + }, + description: 'Set additional classnames to the navigation list.', + table: { + category: 'Styles', + }, + type: { + name: 'string', + required: false, + }, + }, }, decorators: [ (Story) => ( diff --git a/src/components/molecules/nav/nav.tsx b/src/components/molecules/nav/nav.tsx index 2666ea2..581f813 100644 --- a/src/components/molecules/nav/nav.tsx +++ b/src/components/molecules/nav/nav.tsx @@ -24,6 +24,10 @@ export type NavItem = { export type NavProps = { /** + * An accessible name. + */ + 'aria-label'?: string; + /** * Set additional classnames to the navigation wrapper. */ className?: string; @@ -51,6 +55,7 @@ const Nav: FC<NavProps> = ({ items, kind, listClassName = '', + ...props }) => { const kindClass = `nav--${kind}`; @@ -71,7 +76,7 @@ const Nav: FC<NavProps> = ({ }; return ( - <nav className={`${styles[kindClass]} ${className}`}> + <nav className={`${styles[kindClass]} ${className}`} {...props}> <ul className={`${styles.nav__list} ${listClassName}`}>{getItems()}</ul> </nav> ); diff --git a/src/components/organisms/layout/cards-list.module.scss b/src/components/organisms/layout/cards-list.module.scss index 9fe428c..2763585 100644 --- a/src/components/organisms/layout/cards-list.module.scss +++ b/src/components/organisms/layout/cards-list.module.scss @@ -1,12 +1,10 @@ @use "@styles/abstracts/placeholders"; .wrapper { - --card-width: 30ch; - display: grid; grid-template-columns: repeat( auto-fit, - min(calc(100vw - (var(--spacing-md) * 2)), var(--card-width)) + min(calc(100vw - (var(--spacing-md) * 2)), var(--card-width, 30ch)) ); gap: var(--spacing-sm); place-content: center; diff --git a/src/components/organisms/layout/cards-list.stories.tsx b/src/components/organisms/layout/cards-list.stories.tsx index 7ff4365..fe0ebfd 100644 --- a/src/components/organisms/layout/cards-list.stories.tsx +++ b/src/components/organisms/layout/cards-list.stories.tsx @@ -12,6 +12,19 @@ export default { kind: 'unordered', }, argTypes: { + className: { + control: { + type: 'text', + }, + description: 'Set additional classnames to the list wrapper.', + table: { + category: 'Styles', + }, + type: { + name: 'string', + required: false, + }, + }, coverFit: { control: { type: 'select', diff --git a/src/components/organisms/layout/cards-list.tsx b/src/components/organisms/layout/cards-list.tsx index 33ffe23..1558d7c 100644 --- a/src/components/organisms/layout/cards-list.tsx +++ b/src/components/organisms/layout/cards-list.tsx @@ -15,6 +15,10 @@ export type CardsListItem = Omit< export type CardsListProps = { /** + * Set additional classnames to the list wrapper. + */ + className?: string; + /** * The cover fit. */ coverFit?: CardProps['coverFit']; @@ -38,6 +42,7 @@ export type CardsListProps = { * Return a list of Card components. */ const CardsList: FC<CardsListProps> = ({ + className = '', coverFit, items, kind = 'unordered', @@ -70,9 +75,10 @@ const CardsList: FC<CardsListProps> = ({ return ( <List + kind="flex" items={getCards(items)} withMargin={false} - className={`${styles.wrapper} ${styles[kindModifier]}`} + className={`${styles.wrapper} ${styles[kindModifier]} ${className}`} /> ); }; diff --git a/src/components/templates/layout/layout.module.scss b/src/components/templates/layout/layout.module.scss index 3533257..806d2d7 100644 --- a/src/components/templates/layout/layout.module.scss +++ b/src/components/templates/layout/layout.module.scss @@ -1,15 +1,6 @@ @use "@styles/abstracts/functions" as fun; @use "@styles/abstracts/mixins" as mix; -:global { - #__next { - flex: 1; - display: flex; - flex-flow: column nowrap; - min-height: 100vh; - } -} - .header { border-bottom: fun.convert-px(3) solid var(--color-border-light); } diff --git a/src/components/templates/layout/layout.stories.tsx b/src/components/templates/layout/layout.stories.tsx index f3579e3..2415412 100644 --- a/src/components/templates/layout/layout.stories.tsx +++ b/src/components/templates/layout/layout.stories.tsx @@ -36,7 +36,15 @@ export default { decorators: [ (Story) => ( <IntlProvider locale="en"> - <div id="__next"> + <div + id="__next" + style={{ + flex: 1, + display: 'flex', + flexFlow: 'column nowrap', + minHeight: '100vh', + }} + > <Story /> </div> </IntlProvider> diff --git a/src/components/templates/layout/layout.tsx b/src/components/templates/layout/layout.tsx index 601ced4..e1be1af 100644 --- a/src/components/templates/layout/layout.tsx +++ b/src/components/templates/layout/layout.tsx @@ -1,4 +1,3 @@ -import photo from '@assets/images/armand-philippot.jpg'; import ButtonLink from '@components/atoms/buttons/button-link'; import Career from '@components/atoms/icons/career'; import CCBySA from '@components/atoms/icons/cc-by-sa'; @@ -8,13 +7,19 @@ import Home from '@components/atoms/icons/home'; import PostsStack from '@components/atoms/icons/posts-stack'; import Main from '@components/atoms/layout/main'; import NoScript from '@components/atoms/layout/no-script'; -import Footer from '@components/organisms/layout/footer'; -import Header, { HeaderProps } from '@components/organisms/layout/header'; -import { settings } from '@utils/config'; +import Footer, { FooterProps } from '@components/organisms/layout/footer'; +import Header, { type HeaderProps } from '@components/organisms/layout/header'; +import useSettings from '@utils/hooks/use-settings'; +import Script from 'next/script'; import { FC, ReactNode } from 'react'; import { useIntl } from 'react-intl'; +import { Person, SearchAction, WebSite, WithContext } from 'schema-dts'; import styles from './layout.module.scss'; +export type QueryAction = SearchAction & { + 'query-input': string; +}; + export type LayoutProps = Pick<HeaderProps, 'isHome'> & { /** * The layout main content. @@ -33,6 +38,9 @@ export type LayoutProps = Pick<HeaderProps, 'isHome'> & { */ const Layout: FC<LayoutProps> = ({ children, isHome, ...props }) => { const intl = useIntl(); + const { website } = useSettings(); + const { baseline, copyright, locales, name, picture, url } = website; + const skipToContent = intl.formatMessage({ defaultMessage: 'Skip to content', description: 'Layout: Skip to content link', @@ -45,12 +53,12 @@ const Layout: FC<LayoutProps> = ({ children, isHome, ...props }) => { id: '7jVUT6', }); - const copyright = { + const copyrightData = { dates: { - start: settings.copyright.startYear, - end: settings.copyright.endYear, + start: copyright.start, + end: copyright.end, }, - owner: settings.name, + owner: name, icon: <CCBySA />, }; @@ -80,21 +88,77 @@ const Layout: FC<LayoutProps> = ({ children, isHome, ...props }) => { id: 'AE4kCD', }); - const nav: HeaderProps['nav'] = [ - { id: 'home', label: homeLabel, href: '#', logo: <Home /> }, - { id: 'blog', label: blogLabel, href: '#', logo: <PostsStack /> }, + const mainNav: HeaderProps['nav'] = [ + { id: 'home', label: homeLabel, href: '/', logo: <Home /> }, + { id: 'blog', label: blogLabel, href: '/blog', logo: <PostsStack /> }, { id: 'projects', label: projectsLabel, - href: '#', + href: '/projets', logo: <ComputerScreen />, }, - { id: 'cv', label: cvLabel, href: '#', logo: <Career /> }, - { id: 'contact', label: contactLabel, href: '#', logo: <Envelop /> }, + { id: 'cv', label: cvLabel, href: '/cv', logo: <Career /> }, + { id: 'contact', label: contactLabel, href: '/contact', logo: <Envelop /> }, + ]; + + const legalNoticeLabel = intl.formatMessage({ + defaultMessage: 'Legal notice', + description: 'Layout: Legal notice label', + id: 'nwbzKm', + }); + + const footerNav: FooterProps['navItems'] = [ + { id: 'legal-notice', label: legalNoticeLabel, href: '/mentions-legales' }, ]; + const searchActionSchema: QueryAction = { + '@type': 'SearchAction', + target: { + '@type': 'EntryPoint', + urlTemplate: `${url}/recherche?s={search_term_string}`, + }, + query: 'required', + 'query-input': 'required name=search_term_string', + }; + + const schemaJsonLd: WithContext<WebSite> = { + '@context': 'https://schema.org', + '@id': `${url}`, + '@type': 'WebSite', + name: name, + description: baseline, + url: url, + author: { '@id': `${url}/#branding` }, + copyrightYear: Number(copyright.start), + creator: { '@id': `${url}/#branding` }, + editor: { '@id': `${url}/#branding` }, + inLanguage: locales.default, + potentialAction: searchActionSchema, + }; + + const brandingSchema: WithContext<Person> = { + '@context': 'https://schema.org', + '@type': 'Person', + '@id': `${url}/#branding`, + name: name, + url: url, + jobTitle: baseline, + image: picture.src, + subjectOf: { '@id': `${url}` }, + }; + return ( <> + <Script + id="schema-layout" + type="application/ld+json" + dangerouslySetInnerHTML={{ __html: JSON.stringify(schemaJsonLd) }} + ></Script> + <Script + id="schema-branding" + type="application/ld+json" + dangerouslySetInnerHTML={{ __html: JSON.stringify(brandingSchema) }} + /> <noscript> <div className={styles['noscript-spacing']}></div> </noscript> @@ -103,10 +167,10 @@ const Layout: FC<LayoutProps> = ({ children, isHome, ...props }) => { {skipToContent} </ButtonLink> <Header - title={settings.name} - baseline={settings.baseline.fr} - photo={photo.src} - nav={nav} + title={name} + baseline={baseline} + photo={picture} + nav={mainNav} isHome={isHome} className={styles.header} withLink={true} @@ -114,7 +178,12 @@ const Layout: FC<LayoutProps> = ({ children, isHome, ...props }) => { <Main id="main" className={styles.main}> <article {...props}>{children}</article> </Main> - <Footer copyright={copyright} topId="top" className={styles.footer} /> + <Footer + copyright={copyrightData} + navItems={footerNav} + topId="top" + className={styles.footer} + /> <noscript> <NoScript message={noScript} position="top" /> </noscript> diff --git a/src/pages/index.tsx b/src/pages/index.tsx new file mode 100644 index 0000000..c965320 --- /dev/null +++ b/src/pages/index.tsx @@ -0,0 +1,365 @@ +import FeedIcon from '@assets/images/icon-feed.svg'; +import ButtonLink from '@components/atoms/buttons/button-link'; +import Envelop from '@components/atoms/icons/envelop'; +import Column, { type ColumnProps } from '@components/atoms/layout/column'; +import Section, { type SectionProps } from '@components/atoms/layout/section'; +import List, { type ListItem } from '@components/atoms/lists/list'; +import ResponsiveImage, { + type ResponsiveImageProps, +} from '@components/molecules/images/responsive-image'; +import Columns, { + type ColumnsProps, +} from '@components/molecules/layout/columns'; +import CardsList, { + type CardsListItem, +} from '@components/organisms/layout/cards-list'; +import Layout from '@components/templates/layout/layout'; +import HomePageContent from '@content/pages/homepage.mdx'; +import { getArticlesCard } from '@services/graphql/articles'; +import styles from '@styles/pages/home.module.scss'; +import { ArticleCard } from '@ts/types/app'; +import { loadTranslation, type Messages } from '@utils/helpers/i18n'; +import useSettings from '@utils/hooks/use-settings'; +import { NestedMDXComponents } from 'mdx/types'; +import { GetStaticProps, NextPage } from 'next'; +import Head from 'next/head'; +import Script from 'next/script'; +import { ReactElement } from 'react'; +import { useIntl } from 'react-intl'; +import { Graph, WebPage } from 'schema-dts'; + +type HomeProps = { + recentPosts: ArticleCard[]; + translation?: Messages; +}; + +/** + * Home page. + */ +const HomePage: NextPage<HomeProps> = ({ recentPosts }) => { + const intl = useIntl(); + + /** + * Retrieve a list of coding links. + * + * @returns {JSX.Element} - A list of links. + */ + const CodingLinks = (): JSX.Element => { + const links: ListItem[] = [ + { + id: 'web-development', + value: ( + <ButtonLink target="/thematique/developpement-web"> + {intl.formatMessage({ + defaultMessage: 'Web development', + description: 'HomePage: link to web development thematic', + id: 'vkF/RP', + })} + </ButtonLink> + ), + }, + { + id: 'projects', + value: ( + <ButtonLink target="/projets"> + {intl.formatMessage({ + defaultMessage: 'Projects', + description: 'HomePage: link to projects', + id: 'N44SOc', + })} + </ButtonLink> + ), + }, + ]; + + return <List kind="flex" items={links} className={styles.list} />; + }; + + /** + * Retrieve a list of Coldark repositories. + * + * @returns {JSX.Element} - A list of links. + */ + const ColdarkRepos = (): JSX.Element => { + const links: ListItem[] = [ + { + id: 'coldark-github', + value: ( + <ButtonLink + target="https://github.com/ArmandPhilippot/coldark" + external={true} + > + {intl.formatMessage({ + defaultMessage: 'Github', + description: 'HomePage: Github link', + id: '3f3PzH', + })} + </ButtonLink> + ), + }, + { + id: 'coldark-gitlab', + value: ( + <ButtonLink + target="https://gitlab.com/ArmandPhilippot/coldark" + external={true} + > + {intl.formatMessage({ + defaultMessage: 'Gitlab', + description: 'HomePage: Gitlab link', + id: '7AnwZ7', + })} + </ButtonLink> + ), + }, + ]; + + return <List kind="flex" items={links} className={styles.list} />; + }; + + /** + * Retrieve a list of links related to Free thematic. + * + * @returns {JSX.Element} - A list of links. + */ + const LibreLinks = (): JSX.Element => { + const links: ListItem[] = [ + { + id: 'free', + value: ( + <ButtonLink target="/thematique/libre"> + {intl.formatMessage({ + defaultMessage: 'Free', + description: 'HomePage: link to free thematic', + id: 'w8GrOf', + })} + </ButtonLink> + ), + }, + { + id: 'linux', + value: ( + <ButtonLink target="/thematique/linux"> + {intl.formatMessage({ + defaultMessage: 'Linux', + description: 'HomePage: link to Linux thematic', + id: 'jASD7k', + })} + </ButtonLink> + ), + }, + ]; + + return <List kind="flex" items={links} className={styles.list} />; + }; + + /** + * Retrieve the Shaarli link. + * + * @returns {JSX.Element} - A list of links + */ + const ShaarliLink = (): JSX.Element => { + const links: ListItem[] = [ + { + id: 'shaarli', + value: ( + <ButtonLink target="https://shaarli.armandphilippot.com/"> + {intl.formatMessage({ + defaultMessage: 'Shaarli', + description: 'HomePage: link to Shaarli', + id: 'i5L19t', + })} + </ButtonLink> + ), + }, + ]; + + return <List kind="flex" items={links} className={styles.list} />; + }; + + /** + * Retrieve the additional links. + * + * @returns {JSX.Element} - A list of links. + */ + const MoreLinks = (): JSX.Element => { + const links: ListItem[] = [ + { + id: 'contact-me', + value: ( + <ButtonLink target="/contact"> + <Envelop className={styles.icon} /> + {intl.formatMessage({ + defaultMessage: 'Contact me', + description: 'HomePage: contact button text', + id: 'sO/Iwj', + })} + </ButtonLink> + ), + }, + { + id: 'rss-feed', + value: ( + <ButtonLink target="/feed"> + <FeedIcon className={`${styles.icon} ${styles['icon--feed']}`} /> + {intl.formatMessage({ + defaultMessage: 'Subscribe', + description: 'HomePage: RSS feed subscription text', + id: 'T4YA64', + })} + </ButtonLink> + ), + }, + ]; + + return <List kind="flex" items={links} className={styles.list} />; + }; + + /** + * Get a cards list of recent posts. + * + * @returns {JSX.Element} - The cards list. + */ + const getRecentPosts = (): JSX.Element => { + const posts: CardsListItem[] = recentPosts.map((post) => { + return { + cover: post.cover, + id: post.slug, + meta: [ + { + id: 'publication', + term: intl.formatMessage({ + defaultMessage: 'Published on:', + description: 'HomePage: publication date label', + id: 'pT5nHk', + }), + value: [post.dates.publication], + }, + ], + title: post.title, + url: `/article/${post.slug}`, + }; + }); + + return ( + <CardsList + items={posts} + titleLevel={3} + className={`${styles.list} ${styles['list--cards']}`} + /> + ); + }; + + /** + * Create the page sections. + * + * @param {object} obj - An object containing the section body. + * @param {ReactElement[]} obj.children - The section body. + * @returns {JSX.Element} A section element. + */ + const getSection = ({ + children, + variant, + }: { + children: ReactElement[]; + variant: SectionProps['variant']; + }): JSX.Element => { + const [headingEl, ...content] = children; + const title = headingEl.props.children; + + return ( + <Section + title={title} + content={content} + variant={variant} + className={styles.section} + /> + ); + }; + + const components: NestedMDXComponents = { + CodingLinks: CodingLinks, + ColdarkRepos: ColdarkRepos, + Column: (props: ColumnProps) => <Column {...props} />, + Columns: (props: ColumnsProps) => ( + <Columns className={styles.columns} {...props} /> + ), + Image: (props: ResponsiveImageProps) => <ResponsiveImage {...props} />, + LibreLinks: LibreLinks, + MoreLinks: MoreLinks, + RecentPosts: getRecentPosts, + Section: getSection, + ShaarliLink: ShaarliLink, + }; + + const { website } = useSettings(); + + const pageTitle = intl.formatMessage( + { + defaultMessage: '{websiteName} | Front-end developer: WordPress/React', + description: 'HomePage: SEO - Page title', + id: 'PXp2hv', + }, + { websiteName: website.name } + ); + const pageDescription = intl.formatMessage( + { + defaultMessage: + '{websiteName} is a front-end developer located in France. He codes and he writes mostly about web development and open-source.', + description: 'HomePage: SEO - Meta description', + id: 'tMuNTy', + }, + { websiteName: website.name } + ); + + const webpageSchema: WebPage = { + '@id': `${website.url}/#home`, + '@type': 'WebPage', + name: pageTitle, + description: pageDescription, + author: { '@id': `${website.url}/#branding` }, + creator: { '@id': `${website.url}/#branding` }, + editor: { '@id': `${website.url}/#branding` }, + inLanguage: website.locales.default, + license: 'https://creativecommons.org/licenses/by-sa/4.0/deed.fr', + reviewedBy: { '@id': `${website.url}/#branding` }, + url: `${website.url}`, + }; + + const schemaJsonLd: Graph = { + '@context': 'https://schema.org', + '@graph': [webpageSchema], + }; + + return ( + <Layout> + <Head> + <title>{pageTitle}</title> + <meta name="description" content={pageDescription} /> + <meta property="og:url" content={website.url} /> + <meta property="og:title" content={pageTitle} /> + <meta property="og:description" content={pageDescription} /> + </Head> + <Script + id="schema-homepage" + type="application/ld+json" + dangerouslySetInnerHTML={{ __html: JSON.stringify(schemaJsonLd) }} + /> + <HomePageContent components={components} /> + </Layout> + ); +}; + +export const getStaticProps: GetStaticProps = async ({ locale }) => { + const translation = await loadTranslation(locale); + const recentPosts = await getArticlesCard({ first: 3 }); + + return { + props: { + recentPosts, + translation, + }, + }; +}; + +export default HomePage; diff --git a/src/services/graphql/articles.ts b/src/services/graphql/articles.ts index e5ce7a5..7aff3e0 100644 --- a/src/services/graphql/articles.ts +++ b/src/services/graphql/articles.ts @@ -1,10 +1,19 @@ -import { Article } from '@ts/types/app'; -import { RawArticle, TotalItems } from '@ts/types/raw-data'; +import { type Article, type ArticleCard } from '@ts/types/app'; +import { + type RawArticle, + type RawArticlePreview, + type TotalItems, +} from '@ts/types/raw-data'; import { getAuthorFromRawData } from '@utils/helpers/author'; +import { getDates } from '@utils/helpers/dates'; import { getImageFromRawData } from '@utils/helpers/images'; import { getPageLinkFromRawData } from '@utils/helpers/pages'; import { EdgesVars, fetchAPI, getAPIUrl, PageInfo } from './api'; -import { articlesQuery, totalArticlesQuery } from './articles.query'; +import { + articlesCardQuery, + articlesQuery, + totalArticlesQuery, +} from './articles.query'; /** * Retrieve the total number of articles. @@ -102,3 +111,40 @@ export const getArticles = async ({ pageInfo: response.posts.pageInfo, }; }; + +/** + * Convert a raw article preview to an article card. + * + * @param {RawArticlePreview} data - A raw article preview. + * @returns {ArticleCard} An article card. + */ +const getArticleCardFromRawData = (data: RawArticlePreview): ArticleCard => { + const { databaseId, date, featuredImage, slug, title } = data; + + return { + cover: featuredImage ? getImageFromRawData(featuredImage.node) : undefined, + dates: getDates(date, ''), + id: databaseId, + slug, + title, + }; +}; + +/** + * Retrieve the given number of article cards from API. + * + * @param {EdgesVars} obj - An object. + * @param {number} obj.first - The number of articles. + * @returns {Promise<ArticleCard[]>} - The article cards data. + */ +export const getArticlesCard = async ({ + first, +}: EdgesVars): Promise<ArticleCard[]> => { + const response = await fetchAPI<RawArticlePreview, typeof articlesCardQuery>({ + api: getAPIUrl(), + query: articlesCardQuery, + variables: { first }, + }); + + return response.posts.nodes.map((node) => getArticleCardFromRawData(node)); +}; diff --git a/src/styles/pages/Home.module.scss b/src/styles/pages/Home.module.scss deleted file mode 100644 index 8225a57..0000000 --- a/src/styles/pages/Home.module.scss +++ /dev/null @@ -1,49 +0,0 @@ -@use "@styles/abstracts/functions" as fun; -@use "@styles/abstracts/placeholders"; - -.links-list { - @extend %flex-list; - - gap: var(--spacing-md); - margin: 0 0 var(--spacing-md); -} - -.icon--feed { - width: fun.convert-px(20); -} - -:global { - [data-theme="dark"] { - :local { - .icon--feed { - filter: brightness(0.8) contrast(1.1); - } - } - } -} - -.section { - --icon-size: #{fun.convert-px(20)}; - - composes: grid from "@styles/layout/_grid.scss"; - padding: var(--spacing-md) 0; - background: var(--color-bg-secondary); - - &:not(:last-child) { - border-bottom: fun.convert-px(1) solid var(--color-border); - } - - &:nth-child(2n) { - background: var(--color-bg); - } - - > * { - grid-column: 2; - } - - :global { - .wp-block-columns { - margin: 0 0 var(--spacing-md); - } - } -} diff --git a/src/styles/pages/home.module.scss b/src/styles/pages/home.module.scss new file mode 100644 index 0000000..873a5a9 --- /dev/null +++ b/src/styles/pages/home.module.scss @@ -0,0 +1,36 @@ +@use "@styles/abstracts/functions" as fun; +@use "@styles/abstracts/mixins" as mix; + +.section { + --card-width: 25ch; + + &:last-of-type { + border-bottom: none; + } +} + +.columns { + margin: 0 0 var(--spacing-sm); +} + +.list { + margin: 0 0 var(--spacing-sm); + + &--cards { + @include mix.media("screen") { + @include mix.dimensions("md") { + margin: 0 calc(var(--spacing-sm) * -1) var(--spacing-sm); + } + } + } +} + +.icon { + --icon-size: #{fun.convert-px(20)}; + + margin-right: var(--spacing-2xs); + + &--feed { + width: var(--icon-size); + } +} diff --git a/src/utils/hooks/use-settings.tsx b/src/utils/hooks/use-settings.tsx new file mode 100644 index 0000000..a45e934 --- /dev/null +++ b/src/utils/hooks/use-settings.tsx @@ -0,0 +1,112 @@ +import photo from '@assets/images/armand-philippot.jpg'; +import { settings } from '@utils/config'; +import { useRouter } from 'next/router'; + +export type BlogSettings = { + /** + * The number of posts per page. + */ + postsPerPage: number; +}; + +export type CopyrightSettings = { + /** + * The copyright end year. + */ + end: string; + /** + * The copyright start year. + */ + start: string; +}; + +export type LocaleSettings = { + /** + * The default locale. + */ + default: string; + /** + * The supported locales. + */ + supported: string[]; +}; + +export type PictureSettings = { + /** + * The picture height. + */ + height: number; + /** + * The picture url. + */ + src: string; + /** + * The picture width. + */ + width: number; +}; + +export type WebsiteSettings = { + /** + * The website name. + */ + name: string; + /** + * The website baseline. + */ + baseline: string; + /** + * The website copyright dates. + */ + copyright: CopyrightSettings; + /** + * The website locales. + */ + locales: LocaleSettings; + /** + * A picture representing the website. + */ + picture: PictureSettings; + /** + * The website url. + */ + url: string; +}; + +export type UseSettingsReturn = { + blog: BlogSettings; + website: WebsiteSettings; +}; + +/** + * Retrieve the website and blog settings. + * + * @returns {UseSettingsReturn} - An object describing settings. + */ +const useSettings = (): UseSettingsReturn => { + const { baseline, copyright, locales, name, postsPerPage, url } = settings; + const router = useRouter(); + const locale = router.locale || locales.defaultLocale; + + return { + blog: { + postsPerPage, + }, + website: { + baseline: locale.startsWith('en') ? baseline.en : baseline.fr, + copyright: { + end: copyright.endYear, + start: copyright.startYear, + }, + locales: { + default: locales.defaultLocale, + supported: locales.supported, + }, + name, + picture: photo, + url, + }, + }; +}; + +export default useSettings; |
