diff options
| author | Armand Philippot <git@armandphilippot.com> | 2021-12-15 12:16:34 +0100 |
|---|---|---|
| committer | Armand Philippot <git@armandphilippot.com> | 2021-12-15 17:06:55 +0100 |
| commit | 0fa8ae55c52852c34c9143a6ec43c954c6404df1 (patch) | |
| tree | 32c5421025591386632c50200ce6bed3ce6e62b7 | |
| parent | 15d247cb0d52d9c091fa040fe1d9d45e9e506050 (diff) | |
chore: retrieve posts list on blog page
| -rw-r--r-- | src/components/PostsList/PostsList.module.scss | 5 | ||||
| -rw-r--r-- | src/components/PostsList/PostsList.tsx | 36 | ||||
| -rw-r--r-- | src/config/seo.ts | 4 | ||||
| -rw-r--r-- | src/config/website.ts | 1 | ||||
| -rw-r--r-- | src/pages/blog/index.tsx | 48 | ||||
| -rw-r--r-- | src/services/graphql/blog.ts | 130 | ||||
| -rw-r--r-- | src/ts/types/articles.ts | 38 | ||||
| -rw-r--r-- | src/ts/types/blog.ts | 33 | ||||
| -rw-r--r-- | src/ts/types/cover.ts | 9 | ||||
| -rw-r--r-- | src/ts/types/pagination.ts | 4 | ||||
| -rw-r--r-- | src/ts/types/taxonomies.ts | 14 |
11 files changed, 322 insertions, 0 deletions
diff --git a/src/components/PostsList/PostsList.module.scss b/src/components/PostsList/PostsList.module.scss new file mode 100644 index 0000000..9f78f68 --- /dev/null +++ b/src/components/PostsList/PostsList.module.scss @@ -0,0 +1,5 @@ +@use "@styles/abstracts/placeholders"; + +.wrapper { + @extend %reset-ordered-list; +} diff --git a/src/components/PostsList/PostsList.tsx b/src/components/PostsList/PostsList.tsx new file mode 100644 index 0000000..ca3e788 --- /dev/null +++ b/src/components/PostsList/PostsList.tsx @@ -0,0 +1,36 @@ +import Link from 'next/link'; +import { ArticlePreview } from '@ts/types/articles'; +import styles from './PostsList.module.scss'; + +type TitleLevel = 2 | 3 | 4 | 5 | 6; + +const PostsList = ({ + posts, + titleLevel, +}: { + posts: ArticlePreview[]; + titleLevel: TitleLevel; +}) => { + const TitleTag = `h${titleLevel}` as keyof JSX.IntrinsicElements; + + const postsList = posts.map((post) => { + return ( + <li key={post.id}> + <article> + <header> + <TitleTag> + <Link href={`/article/${post.slug}`}> + <a>{post.title}</a> + </Link> + </TitleTag> + </header> + <div dangerouslySetInnerHTML={{ __html: post.content }}></div> + </article> + </li> + ); + }); + + return <ol className={styles.wrapper}>{postsList}</ol>; +}; + +export default PostsList; diff --git a/src/config/seo.ts b/src/config/seo.ts index afccfe5..98bccfc 100644 --- a/src/config/seo.ts +++ b/src/config/seo.ts @@ -5,4 +5,8 @@ export const seo = { title: t`Armand Philippot | Front-end developer`, description: t`Armand Philippot is a front-end developer located in France. He codes, he writes and he plays. Discover is website.`, }, + blog: { + title: t`Blog: development, open source | Armand Philippot`, + description: t`Discover Armand Philippot's writings. He talks about web development and open source mostly.`, + }, }; diff --git a/src/config/website.ts b/src/config/website.ts index 41eb9d9..717b616 100644 --- a/src/config/website.ts +++ b/src/config/website.ts @@ -7,4 +7,5 @@ export const config = { startYear: '2012', endYear: new Date().getFullYear(), }, + postsPerPage: 10, }; diff --git a/src/pages/blog/index.tsx b/src/pages/blog/index.tsx new file mode 100644 index 0000000..7057982 --- /dev/null +++ b/src/pages/blog/index.tsx @@ -0,0 +1,48 @@ +import { ReactElement } from 'react'; +import { GetStaticProps } from 'next'; +import Head from 'next/head'; +import { t } from '@lingui/macro'; +import Layout from '@components/Layouts/Layout'; +import { seo } from '@config/seo'; +import { config } from '@config/website'; +import { getPublishedPosts } from '@services/graphql/blog'; +import { NextPageWithLayout } from '@ts/types/app'; +import { BlogPageProps } from '@ts/types/blog'; +import { loadTranslation } from '@utils/helpers/i18n'; +import PostsList from '@components/PostsList/PostsList'; + +const Blog: NextPageWithLayout<BlogPageProps> = ({ data }) => { + const { posts, pageInfo } = data; + + return ( + <> + <Head> + <title>{seo.blog.title}</title> + <meta name="description" content={seo.blog.description} /> + </Head> + <h1>{t`Blog`}</h1> + <PostsList posts={posts} titleLevel={2} /> + </> + ); +}; + +Blog.getLayout = function getLayout(page: ReactElement) { + return <Layout>{page}</Layout>; +}; + +export const getStaticProps: GetStaticProps = async (context) => { + const translation = await loadTranslation( + context.locale!, + process.env.NODE_ENV === 'production' + ); + const data = await getPublishedPosts(config.postsPerPage); + + return { + props: { + data, + translation, + }, + }; +}; + +export default Blog; diff --git a/src/services/graphql/blog.ts b/src/services/graphql/blog.ts new file mode 100644 index 0000000..266af87 --- /dev/null +++ b/src/services/graphql/blog.ts @@ -0,0 +1,130 @@ +import { ArticlePreview } from '@ts/types/articles'; +import { + fetchPostsListReturn, + getPostsListReturn, + PostsListResponse, +} from '@ts/types/blog'; +import { gql } from 'graphql-request'; +import { getGraphQLClient } from './client'; + +export const fetchPublishedPosts: fetchPostsListReturn = async ( + first = 10, + after = '' +) => { + const client = getGraphQLClient(); + const query = gql` + query AllPublishedPosts($first: Int, $after: String) { + posts( + after: $after + first: $first + where: { status: PUBLISH, orderby: { field: DATE, order: DESC } } + ) { + edges { + cursor + node { + acfPosts { + postsInSubject { + ... on Subject { + databaseId + featuredImage { + node { + altText + sourceUrl + title + } + } + id + slug + title + } + } + postsInThematic { + ... on Thematic { + databaseId + id + slug + title + } + } + } + commentCount + contentParts { + beforeMore + } + date + featuredImage { + node { + altText + sourceUrl + title + } + } + id + databaseId + modified + slug + title + } + } + pageInfo { + endCursor + hasNextPage + } + } + } + `; + + const variables = { first, after }; + + try { + const response: PostsListResponse = await client.request(query, variables); + return response; + } catch (error) { + console.error(JSON.stringify(error, undefined, 2)); + process.exit(1); + } +}; + +export const getPublishedPosts: getPostsListReturn = async ( + first = 10, + after = '' +) => { + const rawPostsList = await fetchPublishedPosts(first, after); + const postsList: ArticlePreview[] = rawPostsList.posts.edges.map((post) => { + const { + acfPosts, + commentCount, + contentParts, + databaseId, + date, + id, + modified, + slug, + title, + } = post.node; + const content = contentParts.beforeMore; + const dates = { publication: date, update: modified }; + const subjects = + acfPosts.postsInSubject && acfPosts.postsInSubject?.length > 0 + ? acfPosts.postsInSubject + : []; + const thematics = + acfPosts.postsInThematics && acfPosts.postsInThematics?.length > 0 + ? acfPosts.postsInThematics + : []; + + return { + commentCount, + content, + databaseId, + date: dates, + id, + slug, + subjects, + thematics, + title, + }; + }); + + return { posts: postsList, pageInfo: rawPostsList.posts.pageInfo }; +}; diff --git a/src/ts/types/articles.ts b/src/ts/types/articles.ts new file mode 100644 index 0000000..5d5fbc5 --- /dev/null +++ b/src/ts/types/articles.ts @@ -0,0 +1,38 @@ +import { Cover, CoverResponse } from './cover'; +import { SubjectPreview, ThematicPreview } from './taxonomies'; + +export type ArticleDates = { + publication: string; + update: string; +}; + +export type ArticlePreviewResponse = { + acfPosts: { + postsInSubject: SubjectPreview[] | null; + postsInThematics: ThematicPreview[] | null; + }; + commentCount: number; + contentParts: { + beforeMore: string; + }; + databaseId: number; + date: string; + featuredImage: CoverResponse | null; + id: string; + modified: string; + slug: string; + title: string; +}; + +export type ArticlePreview = { + commentCount: number; + content: string; + databaseId: number; + date: ArticleDates; + featuredImage?: Cover | object; + id: string; + slug: string; + subjects: SubjectPreview[] | []; + thematics: ThematicPreview[] | []; + title: string; +}; diff --git a/src/ts/types/blog.ts b/src/ts/types/blog.ts new file mode 100644 index 0000000..345deed --- /dev/null +++ b/src/ts/types/blog.ts @@ -0,0 +1,33 @@ +import { ArticlePreview, ArticlePreviewResponse } from './articles'; +import { PageInfo } from './pagination'; + +export type PostsListEdge = { + cursor: string; + node: ArticlePreviewResponse; +}; + +export type PostsListResponse = { + posts: { + edges: PostsListEdge[]; + pageInfo: PageInfo; + }; +}; + +export type PostsList = { + posts: ArticlePreview[]; + pageInfo: PageInfo; +}; + +export type fetchPostsListReturn = ( + first?: number, + after?: string +) => Promise<PostsListResponse>; + +export type getPostsListReturn = ( + first?: number, + after?: string +) => Promise<PostsList>; + +export type BlogPageProps = { + data: PostsList; +}; diff --git a/src/ts/types/cover.ts b/src/ts/types/cover.ts new file mode 100644 index 0000000..165a26f --- /dev/null +++ b/src/ts/types/cover.ts @@ -0,0 +1,9 @@ +export type Cover = { + altText: string; + sourceUrl: string; + title: string; +}; + +export type CoverResponse = { + node: Cover; +}; diff --git a/src/ts/types/pagination.ts b/src/ts/types/pagination.ts new file mode 100644 index 0000000..45830d9 --- /dev/null +++ b/src/ts/types/pagination.ts @@ -0,0 +1,4 @@ +export type PageInfo = { + endCursor: string; + hasNextPage: boolean; +}; diff --git a/src/ts/types/taxonomies.ts b/src/ts/types/taxonomies.ts new file mode 100644 index 0000000..dd45852 --- /dev/null +++ b/src/ts/types/taxonomies.ts @@ -0,0 +1,14 @@ +import { Cover } from './cover'; + +type TaxonomyPreview = { + databaseId: number; + id: string; + slug: string; + title: string; +}; + +export type SubjectPreview = TaxonomyPreview & { + cover: Cover; +}; + +export type ThematicPreview = TaxonomyPreview; |
