From 7e16f500cb7bc0cfd8bafbf6bb1555704f771231 Mon Sep 17 00:00:00 2001 From: Armand Philippot Date: Fri, 29 Apr 2022 12:13:34 +0200 Subject: chore: remove old pages, components, helpers and types Since I'm using new components, I will also rewrite the GraphQL queries so it is easier to start from scratch. --- src/services/graphql/api.ts | 25 -- src/services/graphql/mutations.ts | 82 ------ src/services/graphql/queries.ts | 535 -------------------------------------- 3 files changed, 642 deletions(-) delete mode 100644 src/services/graphql/api.ts delete mode 100644 src/services/graphql/mutations.ts delete mode 100644 src/services/graphql/queries.ts (limited to 'src/services/graphql') diff --git a/src/services/graphql/api.ts b/src/services/graphql/api.ts deleted file mode 100644 index a5be026..0000000 --- a/src/services/graphql/api.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { RequestType, VariablesType } from '@ts/types/app'; -import { settings } from '@utils/config'; -import { GraphQLClient } from 'graphql-request'; - -export const getGraphQLClient = (): GraphQLClient => { - const apiUrl = settings.api.url; - - if (!apiUrl) throw new Error('API URL not defined.'); - - return new GraphQLClient(apiUrl); -}; - -export const fetchApi = async ( - query: string, - variables: VariablesType -): Promise => { - const client = getGraphQLClient(); - - try { - return await client.request(query, variables); - } catch (error) { - console.error(error, undefined, 2); - process.exit(1); - } -}; diff --git a/src/services/graphql/mutations.ts b/src/services/graphql/mutations.ts deleted file mode 100644 index c697835..0000000 --- a/src/services/graphql/mutations.ts +++ /dev/null @@ -1,82 +0,0 @@ -import { CommentData, CreateComment, CreatedComment } from '@ts/types/comments'; -import { ContactData, SendEmail } from '@ts/types/contact'; -import { gql } from 'graphql-request'; -import { fetchApi } from './api'; - -//============================================================================== -// Comment mutation -//============================================================================== - -export const createComment = async ( - data: CommentData -): Promise => { - const mutation = gql` - mutation CreateComment( - $author: String! - $authorEmail: String! - $authorUrl: String! - $content: String! - $parent: ID! - $commentOn: Int! - $mutationId: String! - ) { - createComment( - input: { - author: $author - authorEmail: $authorEmail - authorUrl: $authorUrl - content: $content - parent: $parent - commentOn: $commentOn - clientMutationId: $mutationId - } - ) { - clientMutationId - success - comment { - approved - } - } - } - `; - - const variables = { ...data }; - const response = await fetchApi(mutation, variables); - - return response.createComment; -}; - -//============================================================================== -// Contact mutation -//============================================================================== - -export const sendMail = async (data: ContactData) => { - const mutation = gql` - mutation SendEmail( - $subject: String! - $body: String! - $replyTo: String! - $mutationId: String! - ) { - sendEmail( - input: { - clientMutationId: $mutationId - body: $body - replyTo: $replyTo - subject: $subject - } - ) { - clientMutationId - message - sent - origin - replyTo - to - } - } - `; - - const variables = { ...data }; - const response = await fetchApi(mutation, variables); - return response.sendEmail; -}; diff --git a/src/services/graphql/queries.ts b/src/services/graphql/queries.ts deleted file mode 100644 index 9caf62b..0000000 --- a/src/services/graphql/queries.ts +++ /dev/null @@ -1,535 +0,0 @@ -import { Slug } from '@ts/types/app'; -import { Article, PostBy, TotalArticles } from '@ts/types/articles'; -import { - AllPostsSlug, - LastPostCursor, - PostsList, - RawPostsList, -} from '@ts/types/blog'; -import { Comment, CommentsByPostId } from '@ts/types/comments'; -import { - AllTopics, - AllTopicsSlug, - AllThematics, - AllThematicsSlug, - Topic, - TopicBy, - TopicPreview, - Thematic, - ThematicBy, - ThematicPreview, -} from '@ts/types/taxonomies'; -import { - getFormattedPost, - getFormattedPostPreview, - getFormattedTopic, - getFormattedThematic, - getFormattedComments, - buildCommentsTree, -} from '@utils/helpers/format'; -import { gql } from 'graphql-request'; -import { fetchApi } from './api'; - -//============================================================================== -// Posts list queries -//============================================================================== - -export const getPostsTotal = async (): Promise => { - const query = gql` - query PostsTotal { - posts { - pageInfo { - total - } - } - } - `; - - const response = await fetchApi(query, null); - return response.posts.pageInfo.total; -}; - -export const getPublishedPosts = async ({ - first = 10, - after = '', - searchQuery = '', -}: { - first: number; - after?: string; - searchQuery?: string; -}): Promise => { - const query = gql` - query AllPublishedPosts($first: Int, $after: String, $searchQuery: String) { - posts( - after: $after - first: $first - where: { - status: PUBLISH - orderby: { field: DATE, order: DESC } - search: $searchQuery - } - ) { - edges { - cursor - node { - acfPosts { - postsInTopic { - ... on Topic { - 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 - info { - readingTime - wordsCount - } - databaseId - modified - slug - title - } - } - pageInfo { - endCursor - hasNextPage - total - } - } - } - `; - - const variables = { first, after, searchQuery }; - const response = await fetchApi(query, variables); - const formattedPosts = response.posts.edges.map((post) => { - return getFormattedPostPreview(post.node); - }); - - return { - posts: formattedPosts, - pageInfo: response.posts.pageInfo, - }; -}; - -export const getAllPostsSlug = async (): Promise => { - // 10 000 is an arbitrary number that I use for small websites. - const query = gql` - query AllPostsSlug { - posts(first: 10000) { - nodes { - slug - } - } - } - `; - - const response = await fetchApi(query, null); - return response.posts.nodes; -}; - -//============================================================================== -// Single Post query -//============================================================================== - -export const getPostBySlug = async (slug: string): Promise
=> { - const query = gql` - query PostBySlug($slug: ID!) { - post(id: $slug, idType: SLUG) { - acfPosts { - postsInTopic { - ... on Topic { - id - featuredImage { - node { - altText - sourceUrl - title - } - } - slug - title - } - } - postsInThematic { - ... on Thematic { - id - slug - title - } - } - } - author { - node { - firstName - lastName - name - } - } - commentCount - contentParts { - afterMore - beforeMore - } - databaseId - date - featuredImage { - node { - altText - sourceUrl - title - } - } - id - info { - readingTime - wordsCount - } - modified - seo { - metaDesc - title - } - title - } - } - `; - const variables = { slug }; - const response = await fetchApi(query, variables); - - return getFormattedPost(response.post); -}; - -//============================================================================== -// Comments query -//============================================================================== - -export const getCommentsByPostId = async (id: number): Promise => { - const query = gql` - query PostComments($id: ID!) { - comments(where: { contentId: $id, order: ASC, orderby: COMMENT_DATE }) { - nodes { - approved - author { - node { - databaseId - gravatarUrl - name - url - } - } - content - databaseId - date - parentDatabaseId - } - } - } - `; - - const variables = { id }; - const response = await fetchApi(query, variables); - const formattedComments = getFormattedComments(response.comments.nodes); - - return buildCommentsTree(formattedComments); -}; - -//============================================================================== -// Topic query -//============================================================================== - -export const getTopicBySlug = async (slug: string): Promise => { - const query = gql` - query TopicBySlug($slug: ID!) { - topic(id: $slug, idType: SLUG) { - acfTopics { - officialWebsite - postsInTopic { - ... on Post { - acfPosts { - postsInTopic { - ... on Topic { - databaseId - featuredImage { - node { - altText - sourceUrl - title - } - } - id - slug - title - } - } - postsInThematic { - ... on Thematic { - databaseId - id - slug - title - } - } - } - id - info { - readingTime - wordsCount - } - commentCount - contentParts { - beforeMore - } - databaseId - date - featuredImage { - node { - altText - sourceUrl - title - } - } - modified - slug - title - } - } - } - contentParts { - afterMore - beforeMore - } - databaseId - date - featuredImage { - node { - altText - sourceUrl - title - } - } - id - info { - readingTime - wordsCount - } - modified - seo { - metaDesc - title - } - title - } - } - `; - const variables = { slug }; - const response = await fetchApi(query, variables); - - return getFormattedTopic(response.topic); -}; - -export const getAllTopicsSlug = async (): Promise => { - // 10 000 is an arbitrary number that I use for small websites. - const query = gql` - query AllTopicsSlug { - topics(first: 10000) { - nodes { - slug - } - } - } - `; - const response = await fetchApi(query, null); - return response.topics.nodes; -}; - -export const getAllTopics = async (): Promise => { - // 10 000 is an arbitrary number that I use for small websites. - const query = gql` - query AllTopics { - topics(first: 10000, where: { orderby: { field: TITLE, order: ASC } }) { - nodes { - databaseId - slug - title - } - } - } - `; - - const response = await fetchApi(query, null); - return response.topics.nodes; -}; - -//============================================================================== -// Thematic query -//============================================================================== - -export const getThematicBySlug = async (slug: string): Promise => { - const query = gql` - query ThematicBySlug($slug: ID!) { - thematic(id: $slug, idType: SLUG) { - acfThematics { - postsInThematic { - ... on Post { - acfPosts { - postsInTopic { - ... on Topic { - databaseId - featuredImage { - node { - altText - sourceUrl - title - } - } - id - slug - title - } - } - postsInThematic { - ... on Thematic { - databaseId - id - slug - title - } - } - } - id - info { - readingTime - wordsCount - } - commentCount - contentParts { - beforeMore - } - databaseId - date - featuredImage { - node { - altText - sourceUrl - title - } - } - modified - slug - title - } - } - } - contentParts { - afterMore - beforeMore - } - databaseId - date - id - info { - readingTime - wordsCount - } - modified - seo { - metaDesc - title - } - title - } - } - `; - const variables = { slug }; - const response = await fetchApi(query, variables); - - return getFormattedThematic(response.thematic); -}; - -export const getAllThematicsSlug = async (): Promise => { - // 10 000 is an arbitrary number that I use for small websites. - const query = gql` - query AllThematicsSlug { - thematics(first: 10000) { - nodes { - slug - } - } - } - `; - const response = await fetchApi(query, null); - return response.thematics.nodes; -}; - -export const getAllThematics = async (): Promise => { - // 10 000 is an arbitrary number that I use for small websites. - const query = gql` - query AllThematics { - thematics( - first: 10000 - where: { orderby: { field: TITLE, order: ASC } } - ) { - nodes { - databaseId - slug - title - } - } - } - `; - - const response = await fetchApi(query, null); - return response.thematics.nodes; -}; - -export const getEndCursor = async ({ - first = 10, -}: { - first: number; -}): Promise => { - const query = gql` - query EndCursorAfter($first: Int) { - posts(first: $first) { - pageInfo { - hasNextPage - endCursor - } - } - } - `; - - const variables = { first }; - const response = await fetchApi(query, variables); - - return response.posts.pageInfo.endCursor; -}; -- cgit v1.2.3 From 9308a6dce03bd0c616e0ba6fec227473aaa44b33 Mon Sep 17 00:00:00 2001 From: Armand Philippot Date: Mon, 2 May 2022 12:55:13 +0200 Subject: refactor: rewrite API fetching method and GraphQL queries --- package.json | 1 - src/services/graphql/api.ts | 222 ++++++++++++++++++++++++++++++++ src/services/graphql/articles.query.ts | 174 +++++++++++++++++++++++++ src/services/graphql/comments.query.ts | 21 +++ src/services/graphql/thematics.query.ts | 116 +++++++++++++++++ src/services/graphql/topics.query.ts | 117 +++++++++++++++++ yarn.lock | 23 +--- 7 files changed, 651 insertions(+), 23 deletions(-) create mode 100644 src/services/graphql/api.ts create mode 100644 src/services/graphql/articles.query.ts create mode 100644 src/services/graphql/comments.query.ts create mode 100644 src/services/graphql/thematics.query.ts create mode 100644 src/services/graphql/topics.query.ts (limited to 'src/services/graphql') diff --git a/package.json b/package.json index 87c7596..5cec983 100644 --- a/package.json +++ b/package.json @@ -44,7 +44,6 @@ "@next/mdx": "^12.1.5", "feed": "^4.2.2", "graphql": "^16.1.0", - "graphql-request": "^4.2.0", "modern-normalize": "^1.1.0", "next": "^12.1.5", "next-sitemap": "^2.5.20", diff --git a/src/services/graphql/api.ts b/src/services/graphql/api.ts new file mode 100644 index 0000000..b0e8d3a --- /dev/null +++ b/src/services/graphql/api.ts @@ -0,0 +1,222 @@ +import { settings } from '@utils/config'; +import { + articleBySlugQuery, + articlesCardQuery, + articlesQuery, + articlesSlugQuery, + totalArticlesQuery, +} from './articles.query'; +import { commentsQuery } from './comments.query'; +import { + thematicBySlugQuery, + thematicsListQuery, + thematicsSlugQuery, +} from './thematics.query'; +import { + topicBySlugQuery, + topicsListQuery, + topicsSlugQuery, +} from './topics.query'; + +export type Queries = + | typeof articlesQuery + | typeof articleBySlugQuery + | typeof articlesCardQuery + | typeof articlesSlugQuery + | typeof commentsQuery + | typeof thematicBySlugQuery + | typeof thematicsListQuery + | typeof thematicsSlugQuery + | typeof topicBySlugQuery + | typeof topicsListQuery + | typeof topicsSlugQuery + | typeof totalArticlesQuery; + +export type ArticleResponse = { + post: T; +}; + +export type ArticlesResponse = { + posts: T; +}; + +export type CommentsResponse = { + comments: T[]; +}; + +export type ThematicResponse = { + thematic: T; +}; + +export type ThematicsResponse = { + thematics: T; +}; + +export type TopicResponse = { + topic: T; +}; + +export type TopicsResponse = { + topics: T; +}; + +export type PageInfo = { + endCursor: string; + hasNextPage: boolean; + total: number; +}; + +export type Edges = { + cursor: string; + node: T; +}; + +export type EdgesResponse = { + edges: Edges[]; + pageInfo: PageInfo; +}; + +export type NodeResponse = { + node: T; +}; + +export type NodesResponse = { + nodes: T[]; +}; + +export type ResponseMap = { + [articleBySlugQuery]: ArticleResponse>; + [articlesCardQuery]: ArticlesResponse>; + [articlesQuery]: ArticlesResponse>; + [articlesSlugQuery]: ArticlesResponse>; + [commentsQuery]: CommentsResponse>; + [thematicBySlugQuery]: ThematicResponse>; + [thematicsListQuery]: ThematicsResponse>; + [thematicsSlugQuery]: ThematicsResponse>; + [topicBySlugQuery]: TopicResponse>; + [topicsListQuery]: TopicsResponse>; + [topicsSlugQuery]: TopicsResponse>; + [totalArticlesQuery]: ArticlesResponse; +}; + +export type GraphQLResponse< + T extends keyof ResponseMap, + U +> = ResponseMap[T]; + +export type BySlugVar = { + /** + * A slug. + */ + slug: string; +}; + +export type EdgesVars = { + /** + * A cursor. + */ + after?: string; + /** + * The number of items to return. + */ + first: number; + /** + * A search query. + */ + search?: string; +}; + +export type ByContentIdVar = { + /** + * An article id. + */ + contentId: number; +}; + +export type VariablesMap = { + [articleBySlugQuery]: BySlugVar; + [articlesCardQuery]: EdgesVars; + [articlesQuery]: EdgesVars; + [articlesSlugQuery]: EdgesVars; + [commentsQuery]: ByContentIdVar; + [thematicBySlugQuery]: BySlugVar; + [thematicsListQuery]: EdgesVars; + [thematicsSlugQuery]: EdgesVars; + [topicBySlugQuery]: BySlugVar; + [topicsListQuery]: EdgesVars; + [topicsSlugQuery]: EdgesVars; + [totalArticlesQuery]: null; +}; + +export type FetchAPIProps = { + /** + * A GraphQL API URL. + */ + api: string; + /** + * A GraphQL query. + */ + query: T; + /** + * (Optional) The query variables. + */ + variables?: VariablesMap[T]; +}; + +/** + * Fetch a GraphQL API. + * @param {object} obj - An object. + * @param {string} obj.api - A GraphQL API URL. + * @param {Queries} obj.query - A GraphQL query. + * @param {object} [obj.variables] - The query variables. + */ +export async function fetchAPI({ + api, + query, + variables, +}: FetchAPIProps): Promise> { + const response = await fetch(api, { + method: 'POST', + headers: { + 'content-type': 'application/json;charset=UTF-8', + }, + body: JSON.stringify({ + query, + variables, + }), + }); + + type JSONResponse = { + data?: GraphQLResponse; + errors?: Array<{ message: string }>; + }; + + const { data, errors }: JSONResponse = await response.json(); + + if (response.ok) { + if (!data) return Promise.reject(new Error(`No data found"`)); + + return data; + } else { + console.error('Failed to fetch API'); + const error = new Error( + errors?.map((e) => e.message).join('\n') ?? 'unknown' + ); + return Promise.reject(error); + } +} + +/** + * Retrieve the API url from settings. + * + * @returns {string} The API url. + */ +export const getAPIUrl = (): string => { + const { url } = settings.api; + + if (!url) { + throw new Error('API url is not defined.'); + } + + return url; +}; diff --git a/src/services/graphql/articles.query.ts b/src/services/graphql/articles.query.ts new file mode 100644 index 0000000..e384aba --- /dev/null +++ b/src/services/graphql/articles.query.ts @@ -0,0 +1,174 @@ +/** + * Query the full article data using its slug. + */ +export const articleBySlugQuery = `query PostBy($slug: ID!) { + post(id: $slug, idType: SLUG) { + acfPosts { + postsInThematic { + ... on Thematic { + databaseId + slug + title + } + } + postsInTopic { + ... on Topic { + databaseId + slug + title + } + } + } + author { + node { + gravatarUrl + name + url + } + } + commentCount + contentParts { + afterMore + beforeMore + } + databaseId + date + featuredImage { + node { + altText + mediaDetails { + height + width + } + sourceUrl + title + } + } + info { + readingTime + wordsCount + } + modified + seo { + metaDesc + title + } + slug + title + } +}`; + +/** + * Query an array of partial articles. + */ +export const articlesQuery = `query Articles($after: String = "", $first: Int = 10, $search: String = "") { + posts( + after: $after + first: $first + where: {orderby: {field: DATE, order: DESC}, search: $search, status: PUBLISH} + ) { + edges { + cursor + node { + acfPosts { + postsInThematic { + ... on Thematic { + databaseId + slug + title + } + } + } + author { + node { + name + } + } + commentCount + contentParts { + beforeMore + } + databaseId + featuredImage { + node { + altText + mediaDetails { + height + width + } + sourceUrl + title + } + } + info { + readingTime + wordsCount + } + modified + slug + title + } + } + pageInfo { + endCursor + hasNextPage + total + } + } +}`; + +/** + * Query an array of articles with only the minimal data. + */ +export const articlesCardQuery = `query ArticlesCard($first: Int = 10) { + posts( + first: $first + where: {orderby: {field: DATE, order: DESC}, status: PUBLISH} + ) { + nodes { + databaseId + date + featuredImage { + node { + altText + mediaDetails { + height + width + } + sourceUrl + title + } + } + slug + title + } + } +}`; + +/** + * Query an array of articles slug. + */ +export const articlesSlugQuery = `query ArticlesSlug($first: Int = 10, $after: String = "") { + posts(after: $after, first: $first) { + edges { + cursor + node { + slug + } + } + pageInfo { + total + } + } +}`; + +/** + * Query the total number of articles. + */ +export const totalArticlesQuery = `query PostsTotal { + posts { + pageInfo { + total + } + } +}`; diff --git a/src/services/graphql/comments.query.ts b/src/services/graphql/comments.query.ts new file mode 100644 index 0000000..ef93e89 --- /dev/null +++ b/src/services/graphql/comments.query.ts @@ -0,0 +1,21 @@ +/** + * Query the comments data by post id. + */ +export const commentsQuery = `query CommentsByPostId($contentId: ID!) { + comments(where: {contentId: $contentId, order: ASC, orderby: COMMENT_DATE}) { + nodes { + approved + author { + node { + gravatarUrl + name + url + } + } + content + databaseId + date + parentDatabaseId + } + } +}`; diff --git a/src/services/graphql/thematics.query.ts b/src/services/graphql/thematics.query.ts new file mode 100644 index 0000000..76949ad --- /dev/null +++ b/src/services/graphql/thematics.query.ts @@ -0,0 +1,116 @@ +/** + * Query the full thematic data using its slug. + */ +export const thematicBySlugQuery = `query ThematicBy($slug: ID!) { + thematic(id: $slug, idType: SLUG) { + acfThematics { + postsInThematic { + ... on Post { + acfPosts { + postsInTopic { + ... on Topic { + databaseId + slug + title + } + } + } + commentCount + contentParts { + beforeMore + } + databaseId + date + featuredImage { + node { + altText + mediaDetails { + height + width + } + sourceUrl + title + } + } + info { + readingTime + wordsCount + } + modified + slug + title + } + } + } + contentParts { + afterMore + beforeMore + } + databaseId + date + featuredImage { + node { + altText + mediaDetails { + height + width + } + sourceUrl + title + } + } + info { + readingTime + wordsCount + } + modified + seo { + metaDesc + title + } + slug + title + } +}`; + +/** + * Query an array of partial thematics. + */ +export const thematicsListQuery = `query ThematicsList($after: String = "", $first: Int = 10) { + thematics( + after: $after + first: $first + where: {orderby: {field: TITLE, order: ASC}, status: PUBLISH} + ) { + edges { + cursor + node { + databaseId + slug + title + } + } + pageInfo { + endCursor + hasNextPage + total + } + } +}`; + +/** + * Query an array of thematics slug. + */ +export const thematicsSlugQuery = `query ThematicsSlug($first: Int = 10, $after: String = "") { + thematics(after: $after, first: $first) { + edges { + cursor + node { + slug + } + } + pageInfo { + total + } + } +}`; diff --git a/src/services/graphql/topics.query.ts b/src/services/graphql/topics.query.ts new file mode 100644 index 0000000..8783799 --- /dev/null +++ b/src/services/graphql/topics.query.ts @@ -0,0 +1,117 @@ +/** + * Query the full topic data using its slug. + */ +export const topicBySlugQuery = `query TopicBy($slug: ID!) { + topic(id: $slug, idType: SLUG) { + acfTopics { + officialWebsite + postsInTopic { + ... on Post { + acfPosts { + postsInThematic { + ... on Thematic { + databaseId + slug + title + } + } + } + commentCount + contentParts { + beforeMore + } + databaseId + date + featuredImage { + node { + altText + mediaDetails { + height + width + } + sourceUrl + title + } + } + info { + readingTime + wordsCount + } + modified + slug + title + } + } + } + contentParts { + afterMore + beforeMore + } + databaseId + date + featuredImage { + node { + altText + mediaDetails { + height + width + } + sourceUrl + title + } + } + info { + readingTime + wordsCount + } + modified + seo { + metaDesc + title + } + slug + title + } +}`; + +/** + * Query an array of partial topics. + */ +export const topicsListQuery = `query TopicsList($after: String = "", $first: Int = 10) { + topics( + after: $after + first: $first + where: {orderby: {field: TITLE, order: ASC}, status: PUBLISH} + ) { + edges { + cursor + node { + databaseId + slug + title + } + } + pageInfo { + endCursor + hasNextPage + total + } + } +}`; + +/** + * Query an array of topics slug. + */ +export const topicsSlugQuery = `query TopicsSlug($first: Int = 10, $after: String = "") { + topics(after: $after, first: $first) { + edges { + cursor + node { + slug + } + } + pageInfo { + total + } + } +}`; diff --git a/yarn.lock b/yarn.lock index db10eae..31bd5e5 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6310,13 +6310,6 @@ create-require@^1.1.0: resolved "https://registry.yarnpkg.com/create-require/-/create-require-1.1.1.tgz#c1d7e8f1e5f6cfc9ff65f9cd352d37348756c333" integrity sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ== -cross-fetch@^3.1.5: - version "3.1.5" - resolved "https://registry.yarnpkg.com/cross-fetch/-/cross-fetch-3.1.5.tgz#e1389f44d9e7ba767907f7af8454787952ab534f" - integrity sha512-lvb1SBsI0Z7GDwmuid+mU3kWVBwTVUbe7S0H52yaaAdQOXq2YktTCZdlAcNKFzE6QtRz0snpw9bNiPeOIkkQvw== - dependencies: - node-fetch "2.6.7" - cross-spawn@^6.0.0: version "6.0.5" resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-6.0.5.tgz#4a5ec7c64dfae22c3a14124dbacdee846d80cbc4" @@ -7617,11 +7610,6 @@ extglob@^2.0.4: snapdragon "^0.8.1" to-regex "^3.0.1" -extract-files@^9.0.0: - version "9.0.0" - resolved "https://registry.yarnpkg.com/extract-files/-/extract-files-9.0.0.tgz#8a7744f2437f81f5ed3250ed9f1550de902fe54a" - integrity sha512-CvdFfHkC95B4bBBk36hcEmvdR2awOdhhVUYH6S/zrVj3477zven/fJMYg7121h4T1xHZC+tetUpubpAhxwI7hQ== - fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3: version "3.1.3" resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525" @@ -8330,15 +8318,6 @@ graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.2.0, graceful-fs@^4.2.9: resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.10.tgz#147d3a006da4ca3ce14728c7aefc287c367d7a6c" integrity sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA== -graphql-request@^4.2.0: - version "4.2.0" - resolved "https://registry.yarnpkg.com/graphql-request/-/graphql-request-4.2.0.tgz#063377bc2dd29cc46aed3fddcc65fe97b805ba81" - integrity sha512-uFeMyhhl8ss4LFgjlfPeAn2pqYw+CJto+cjj71uaBYIMMK2jPIqgHm5KEFxUk0YDD41A8Bq31a2b4G2WJBlp2Q== - dependencies: - cross-fetch "^3.1.5" - extract-files "^9.0.0" - form-data "^3.0.0" - graphql@^16.1.0: version "16.3.0" resolved "https://registry.yarnpkg.com/graphql/-/graphql-16.3.0.tgz#a91e24d10babf9e60c706919bb182b53ccdffc05" @@ -11363,7 +11342,7 @@ node-dir@^0.1.10: dependencies: minimatch "^3.0.2" -node-fetch@2.6.7, node-fetch@^2.6.1: +node-fetch@^2.6.1: version "2.6.7" resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.7.tgz#24de9fba827e3b4ae44dc8b20256a379160052ad" integrity sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ== -- cgit v1.2.3 From ca921d7536cfe950b5a7d442977bbf900b48faf4 Mon Sep 17 00:00:00 2001 From: Armand Philippot Date: Mon, 2 May 2022 18:36:09 +0200 Subject: chore: fetch posts for rss feed --- src/services/graphql/articles.query.ts | 1 + src/services/graphql/articles.ts | 104 +++++++++++++++++++++++++++++++++ src/ts/types/app.ts | 86 +++++++++++++++++++++++++++ src/ts/types/raw-data.ts | 103 ++++++++++++++++++++++++++++++++ src/utils/helpers/author.ts | 32 ++++++++++ src/utils/helpers/dates.ts | 55 +++++++++++++++++ src/utils/helpers/images.ts | 18 ++++++ src/utils/helpers/pages.ts | 26 +++++++++ src/utils/helpers/rss.ts | 44 ++++++++------ 9 files changed, 450 insertions(+), 19 deletions(-) create mode 100644 src/services/graphql/articles.ts create mode 100644 src/ts/types/app.ts create mode 100644 src/ts/types/raw-data.ts create mode 100644 src/utils/helpers/author.ts create mode 100644 src/utils/helpers/dates.ts create mode 100644 src/utils/helpers/images.ts create mode 100644 src/utils/helpers/pages.ts (limited to 'src/services/graphql') diff --git a/src/services/graphql/articles.query.ts b/src/services/graphql/articles.query.ts index e384aba..e62835d 100644 --- a/src/services/graphql/articles.query.ts +++ b/src/services/graphql/articles.query.ts @@ -89,6 +89,7 @@ export const articlesQuery = `query Articles($after: String = "", $first: Int = beforeMore } databaseId + date featuredImage { node { altText diff --git a/src/services/graphql/articles.ts b/src/services/graphql/articles.ts new file mode 100644 index 0000000..e5ce7a5 --- /dev/null +++ b/src/services/graphql/articles.ts @@ -0,0 +1,104 @@ +import { Article } from '@ts/types/app'; +import { RawArticle, TotalItems } from '@ts/types/raw-data'; +import { getAuthorFromRawData } from '@utils/helpers/author'; +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'; + +/** + * Retrieve the total number of articles. + * + * @returns {Promise} - The articles total number. + */ +export const getTotalArticles = async (): Promise => { + const response = await fetchAPI({ + api: getAPIUrl(), + query: totalArticlesQuery, + }); + + return response.posts.pageInfo.total; +}; + +export type GetArticlesReturn = { + articles: Article[]; + pageInfo: PageInfo; +}; + +/** + * Convert raw data to an Article object. + * + * @param {RawArticle} data - The page raw data. + * @returns {Article} The page data. + */ +export const getArticleFromRawData = (data: RawArticle): Article => { + const { + acfPosts, + author, + commentCount, + contentParts, + databaseId, + date, + featuredImage, + info, + modified, + slug, + title, + seo, + } = data; + + return { + content: contentParts.afterMore, + id: databaseId, + intro: contentParts.beforeMore, + meta: { + author: getAuthorFromRawData(author.node, 'page'), + commentsCount: commentCount || 0, + cover: featuredImage?.node + ? getImageFromRawData(featuredImage.node) + : undefined, + dates: { + publication: date, + update: modified, + }, + readingTime: info.readingTime, + seo: { + description: seo?.metaDesc || '', + title: seo?.title || '', + }, + thematics: acfPosts.postsInThematic?.map((thematic) => + getPageLinkFromRawData(thematic) + ), + topics: acfPosts.postsInTopic?.map((topic) => + getPageLinkFromRawData(topic) + ), + wordsCount: info.wordsCount, + }, + slug, + title, + }; +}; + +/** + * Retrieve the given number of articles from API. + * + * @param {EdgesVars} obj - An object. + * @param {number} obj.first - The number of articles. + * @returns {Promise} - The articles data. + */ +export const getArticles = async ({ + first, +}: EdgesVars): Promise => { + const response = await fetchAPI({ + api: getAPIUrl(), + query: articlesQuery, + variables: { first }, + }); + + return { + articles: response.posts.edges.map((edge) => + getArticleFromRawData(edge.node) + ), + pageInfo: response.posts.pageInfo, + }; +}; diff --git a/src/ts/types/app.ts b/src/ts/types/app.ts new file mode 100644 index 0000000..b09f3d5 --- /dev/null +++ b/src/ts/types/app.ts @@ -0,0 +1,86 @@ +export type AuthorKind = 'page' | 'comment'; + +export type Author = { + avatar?: Image; + description?: T extends 'page' ? string | undefined : never; + name: string; + website?: string; +}; + +export type CommentMeta = { + author: Author<'comment'>; + date: string; +}; + +export type Comment = { + approved: boolean; + content: string; + id: number; + meta: CommentMeta; + parentId: number; + replies: Comment[]; +}; + +export type Dates = { + publication: string; + update: string; +}; + +export type Image = { + alt: string; + height: number; + src: string; + title?: string; + width: number; +}; + +export type Repos = { + github?: string; + gitlab?: string; +}; + +export type SEO = { + description: string; + title: string; +}; + +export type PageKind = 'article' | 'project' | 'thematic' | 'topic'; + +export type Meta = { + articles?: T extends 'thematic' | 'topic' ? Article[] : never; + author: Author<'page'>; + commentsCount?: T extends 'article' ? number : never; + cover?: Image; + dates: Dates; + license?: T extends 'projects' ? string : never; + readingTime: number; + repos?: T extends 'projects' ? Repos : never; + seo: SEO; + technologies?: T extends 'projects' ? string[] : never; + thematics?: T extends 'article' | 'topic' ? PageLink[] : never; + topics?: T extends 'article' | 'thematic' ? PageLink[] : never; + website?: T extends 'topic' ? string : never; + wordsCount: number; +}; + +export type Page = { + content: string; + id: number; + intro: string; + meta?: Meta; + slug: string; + title: string; +}; + +export type PageLink = { + id: number; + name: string; + slug: string; +}; + +export type Article = Page<'article'>; +export type ArticleCard = Pick & + Pick, 'cover' | 'dates'>; +export type Project = Page<'project'>; +export type Thematic = Page<'thematic'>; +export type Topic = Page<'topic'>; diff --git a/src/ts/types/raw-data.ts b/src/ts/types/raw-data.ts new file mode 100644 index 0000000..43a2453 --- /dev/null +++ b/src/ts/types/raw-data.ts @@ -0,0 +1,103 @@ +/** + * Types for raw data coming from GraphQL API. + */ + +import { NodeResponse, PageInfo } from '@services/graphql/api'; +import { AuthorKind } from './app'; + +export type ACFPosts = { + postsInThematic?: RawThematicPreview[]; + postsInTopic?: RawTopicPreview[]; +}; + +export type ACFThematics = { + postsInThematic: RawArticle[]; +}; + +export type ACFTopics = { + officialWebsite: string; + postsInTopic: RawArticle[]; +}; + +export type ContentParts = { + afterMore: string; + beforeMore: string; +}; + +export type Info = { + readingTime: number; + wordsCount: number; +}; + +export type RawAuthor = { + description?: T extends 'page' ? string | undefined : never; + gravatarUrl?: string; + name: string; + url?: string; +}; + +export type RawComment = { + approved: boolean; + author: NodeResponse>; + content: string; + databaseId: number; + date: string; + parentDatabaseId: number; +}; + +export type RawCover = { + altText: string; + mediaDetails: { + width: number; + height: number; + }; + sourceUrl: string; + title?: string; +}; + +export type RawArticle = RawPage & { + acfPosts: ACFPosts; + commentCount: number | null; +}; + +export type RawArticlePreview = Pick< + RawArticle, + 'databaseId' | 'date' | 'featuredImage' | 'slug' | 'title' +>; + +export type RawPage = { + author: NodeResponse>; + contentParts: ContentParts; + databaseId: number; + date: string; + featuredImage: NodeResponse | null; + info: Info; + modified: string; + seo?: RawSEO; + slug: string; + title: string; +}; + +export type RawSEO = { + metaDesc: string; + title: string; +}; + +export type RawThematic = RawPage & { + acfThematics: ACFThematics; +}; + +export type RawThematicPreview = Pick< + RawThematic, + 'databaseId' | 'slug' | 'title' +>; + +export type RawTopic = RawPage & { + acfTopics: ACFTopics; +}; + +export type RawTopicPreview = Pick; + +export type TotalItems = { + pageInfo: Pick; +}; diff --git a/src/utils/helpers/author.ts b/src/utils/helpers/author.ts new file mode 100644 index 0000000..cf125fc --- /dev/null +++ b/src/utils/helpers/author.ts @@ -0,0 +1,32 @@ +import { type Author, type AuthorKind } from '@ts/types/app'; +import { type RawAuthor } from '@ts/types/raw-data'; + +/** + * Convert author raw data to regular data. + * + * @param {RawAuthor} data - The author raw data. + * @param {AuthorKind} kind - The author kind. Either `page` or `comment`. + * @param {number} [avatarSize] - The author avatar size. + * @returns {Author} The author data. + */ +export const getAuthorFromRawData = ( + data: RawAuthor, + kind: AuthorKind, + avatarSize: number = 80 +): Author => { + const { name, description, gravatarUrl, url } = data; + + return { + name, + avatar: gravatarUrl + ? { + alt: `${name} avatar`, + height: avatarSize, + src: gravatarUrl, + width: avatarSize, + } + : undefined, + description, + website: url, + }; +}; diff --git a/src/utils/helpers/dates.ts b/src/utils/helpers/dates.ts new file mode 100644 index 0000000..fa167a7 --- /dev/null +++ b/src/utils/helpers/dates.ts @@ -0,0 +1,55 @@ +import { Dates } from '@ts/types/app'; +import { settings } from '@utils/config'; + +/** + * Format a date based on a locale. + * + * @param {string} date - The date. + * @param {string} [locale] - A locale. + * @returns {string} The locale date string. + */ +export const getFormattedDate = ( + date: string, + locale: string = settings.locales.defaultLocale +): string => { + const dateOptions: Intl.DateTimeFormatOptions = { + day: 'numeric', + month: 'long', + year: 'numeric', + }; + + return new Date(date).toLocaleDateString(locale, dateOptions); +}; + +/** + * Format a time based on a locale. + * + * @param {string} time - The time. + * @param {string} [locale] - A locale. + * @returns {string} The locale time string. + */ +export const getFormattedTime = ( + time: string, + locale: string = settings.locales.defaultLocale +): string => { + const formattedTime = new Date(time).toLocaleTimeString(locale, { + hour: 'numeric', + minute: 'numeric', + }); + + return locale === 'fr' ? formattedTime.replace(':', 'h') : formattedTime; +}; + +/** + * Retrieve a Dates object. + * + * @param publication - The publication date. + * @param update - The update date. + * @returns {Dates} A Dates object. + */ +export const getDates = (publication: string, update: string): Dates => { + return { + publication: getFormattedDate(publication), + update: getFormattedDate(update), + }; +}; diff --git a/src/utils/helpers/images.ts b/src/utils/helpers/images.ts new file mode 100644 index 0000000..30bb8be --- /dev/null +++ b/src/utils/helpers/images.ts @@ -0,0 +1,18 @@ +import { Image } from '@ts/types/app'; +import { RawCover } from '@ts/types/raw-data'; + +/** + * Retrieve an Image object from raw data. + * + * @param image - The cover raw data. + * @returns {Image} - An Image object. + */ +export const getImageFromRawData = (image: RawCover): Image => { + return { + alt: image.altText, + height: image.mediaDetails.height, + src: image.sourceUrl, + title: image.title, + width: image.mediaDetails.width, + }; +}; diff --git a/src/utils/helpers/pages.ts b/src/utils/helpers/pages.ts new file mode 100644 index 0000000..d757f8c --- /dev/null +++ b/src/utils/helpers/pages.ts @@ -0,0 +1,26 @@ +import { type PageLink } from '@ts/types/app'; +import { + type RawThematicPreview, + type RawTopicPreview, +} from '@ts/types/raw-data'; + +/** + * Convert raw data to a Link object. + * + * @param data - An object. + * @param {number} data.databaseId - The data id. + * @param {string} data.slug - The data slug. + * @param {string} data.title - The data name. + * @returns {PageLink} The link data (id, slug and title). + */ +export const getPageLinkFromRawData = ( + data: RawThematicPreview | RawTopicPreview +): PageLink => { + const { databaseId, slug, title } = data; + + return { + id: databaseId, + name: title, + slug, + }; +}; diff --git a/src/utils/helpers/rss.ts b/src/utils/helpers/rss.ts index 10a8e77..95d3b7b 100644 --- a/src/utils/helpers/rss.ts +++ b/src/utils/helpers/rss.ts @@ -1,20 +1,26 @@ -import { getPostsTotal, getPublishedPosts } from '@services/graphql/queries'; -import { ArticlePreview } from '@ts/types/articles'; -import { PostsList } from '@ts/types/blog'; +import { getArticles, getTotalArticles } from '@services/graphql/articles'; +import { Article } from '@ts/types/app'; import { settings } from '@utils/config'; import { Feed } from 'feed'; -const getAllPosts = async (): Promise => { - const totalPosts = await getPostsTotal(); - const posts: ArticlePreview[] = []; +/** + * Retrieve the data for all the articles. + * + * @returns {Promise} - All the articles. + */ +const getAllArticles = async (): Promise => { + const totalArticles = await getTotalArticles(); + const { articles } = await getArticles({ first: totalArticles }); - const postsList: PostsList = await getPublishedPosts({ first: totalPosts }); - posts.push(...postsList.posts); - - return posts; + return articles; }; -export const generateFeed = async () => { +/** + * Generate a new feed. + * + * @returns {Promise} - The feed. + */ +export const generateFeed = async (): Promise => { const author = { name: settings.name, email: process.env.APP_AUTHOR_EMAIL, @@ -38,16 +44,16 @@ export const generateFeed = async () => { title, }); - const posts = await getAllPosts(); + const articles = await getAllArticles(); - posts.forEach((post) => { + articles.forEach((article) => { feed.addItem({ - content: post.intro, - date: new Date(post.dates.publication), - description: post.intro, - id: post.id, - link: `${settings.url}/article/${post.slug}`, - title: post.title, + content: article.intro, + date: new Date(article.meta!.dates.publication), + description: article.intro, + id: `${article.id}`, + link: `${settings.url}/article/${article.slug}`, + title: article.title, }); }); -- cgit v1.2.3 From 732d0943f8041d76262222a092b014f2557085ef Mon Sep 17 00:00:00 2001 From: Armand Philippot Date: Mon, 2 May 2022 18:57:29 +0200 Subject: chore: add homepage --- src/components/atoms/lists/list.module.scss | 16 + src/components/atoms/lists/list.stories.tsx | 13 +- src/components/atoms/lists/list.tsx | 4 +- src/components/molecules/layout/card.module.scss | 3 +- src/components/molecules/layout/card.tsx | 2 +- src/components/molecules/layout/page-header.tsx | 2 +- src/components/molecules/nav/nav.stories.tsx | 26 ++ src/components/molecules/nav/nav.tsx | 7 +- .../organisms/layout/cards-list.module.scss | 4 +- .../organisms/layout/cards-list.stories.tsx | 13 + src/components/organisms/layout/cards-list.tsx | 8 +- src/components/templates/layout/layout.module.scss | 9 - src/components/templates/layout/layout.stories.tsx | 10 +- src/components/templates/layout/layout.tsx | 107 ++++-- src/pages/index.tsx | 365 +++++++++++++++++++++ src/services/graphql/articles.ts | 52 ++- src/styles/pages/Home.module.scss | 49 --- src/styles/pages/home.module.scss | 36 ++ src/utils/hooks/use-settings.tsx | 112 +++++++ 19 files changed, 745 insertions(+), 93 deletions(-) create mode 100644 src/pages/index.tsx delete mode 100644 src/styles/pages/Home.module.scss create mode 100644 src/styles/pages/home.module.scss create mode 100644 src/utils/hooks/use-settings.tsx (limited to 'src/services/graphql') 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' }, @@ -71,6 +71,15 @@ const items: ListItem[] = [ { id: 'item-4', value: 'Item 4' }, ]; +/** + * List Stories - Flex list + */ +export const Flex = Template.bind({}); +Flex.args = { + items, + kind: 'flex', +}; + /** * List Stories - Ordered list */ 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 = ({ {title} - {tagline &&
{tagline}
} +
{tagline}
{meta && (