aboutsummaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
authorArmand Philippot <git@armandphilippot.com>2021-12-15 12:16:34 +0100
committerArmand Philippot <git@armandphilippot.com>2021-12-15 17:06:55 +0100
commit0fa8ae55c52852c34c9143a6ec43c954c6404df1 (patch)
tree32c5421025591386632c50200ce6bed3ce6e62b7 /src
parent15d247cb0d52d9c091fa040fe1d9d45e9e506050 (diff)
chore: retrieve posts list on blog page
Diffstat (limited to 'src')
-rw-r--r--src/components/PostsList/PostsList.module.scss5
-rw-r--r--src/components/PostsList/PostsList.tsx36
-rw-r--r--src/config/seo.ts4
-rw-r--r--src/config/website.ts1
-rw-r--r--src/pages/blog/index.tsx48
-rw-r--r--src/services/graphql/blog.ts130
-rw-r--r--src/ts/types/articles.ts38
-rw-r--r--src/ts/types/blog.ts33
-rw-r--r--src/ts/types/cover.ts9
-rw-r--r--src/ts/types/pagination.ts4
-rw-r--r--src/ts/types/taxonomies.ts14
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;