summaryrefslogtreecommitdiffstats
path: root/src/services/graphql
diff options
context:
space:
mode:
Diffstat (limited to 'src/services/graphql')
-rw-r--r--src/services/graphql/api.ts319
-rw-r--r--src/services/graphql/articles.query.ts191
-rw-r--r--src/services/graphql/articles.ts200
-rw-r--r--src/services/graphql/comments.mutation.ts30
-rw-r--r--src/services/graphql/comments.query.ts21
-rw-r--r--src/services/graphql/comments.ts102
-rw-r--r--src/services/graphql/contact.mutation.ts25
-rw-r--r--src/services/graphql/contact.ts26
-rw-r--r--src/services/graphql/mutations.ts82
-rw-r--r--src/services/graphql/queries.ts535
-rw-r--r--src/services/graphql/thematics.query.ts125
-rw-r--r--src/services/graphql/thematics.ts162
-rw-r--r--src/services/graphql/topics.query.ts137
-rw-r--r--src/services/graphql/topics.ts164
14 files changed, 1486 insertions, 633 deletions
diff --git a/src/services/graphql/api.ts b/src/services/graphql/api.ts
index a5be026..009aea4 100644
--- a/src/services/graphql/api.ts
+++ b/src/services/graphql/api.ts
@@ -1,25 +1,312 @@
-import { RequestType, VariablesType } from '@ts/types/app';
import { settings } from '@utils/config';
-import { GraphQLClient } from 'graphql-request';
+import {
+ articleBySlugQuery,
+ articlesCardQuery,
+ articlesEndCursor,
+ articlesQuery,
+ articlesSlugQuery,
+ totalArticlesQuery,
+} from './articles.query';
+import { sendCommentMutation } from './comments.mutation';
+import { commentsQuery } from './comments.query';
+import { sendMailMutation } from './contact.mutation';
+import {
+ thematicBySlugQuery,
+ thematicsListQuery,
+ thematicsSlugQuery,
+ totalThematicsQuery,
+} from './thematics.query';
+import {
+ topicBySlugQuery,
+ topicsListQuery,
+ topicsSlugQuery,
+ totalTopicsQuery,
+} from './topics.query';
-export const getGraphQLClient = (): GraphQLClient => {
- const apiUrl = settings.api.url;
+export type Mutations = typeof sendMailMutation | typeof sendCommentMutation;
- if (!apiUrl) throw new Error('API URL not defined.');
+export type Queries =
+ | typeof articlesQuery
+ | typeof articleBySlugQuery
+ | typeof articlesCardQuery
+ | typeof articlesEndCursor
+ | typeof articlesSlugQuery
+ | typeof commentsQuery
+ | typeof thematicBySlugQuery
+ | typeof thematicsListQuery
+ | typeof thematicsSlugQuery
+ | typeof topicBySlugQuery
+ | typeof topicsListQuery
+ | typeof topicsSlugQuery
+ | typeof totalArticlesQuery
+ | typeof totalThematicsQuery
+ | typeof totalTopicsQuery;
- return new GraphQLClient(apiUrl);
+export type ArticleResponse<T> = {
+ post: T;
};
-export const fetchApi = async <T extends RequestType>(
- query: string,
- variables: VariablesType<T>
-): Promise<T> => {
- const client = getGraphQLClient();
+export type ArticlesResponse<T> = {
+ posts: T;
+};
+
+export type CommentsResponse<T> = {
+ comments: T;
+};
+
+export type SendCommentResponse<T> = {
+ createComment: T;
+};
+
+export type SendMailResponse<T> = {
+ sendEmail: T;
+};
- try {
- return await client.request(query, variables);
- } catch (error) {
- console.error(error, undefined, 2);
- process.exit(1);
+export type ThematicResponse<T> = {
+ thematic: T;
+};
+
+export type ThematicsResponse<T> = {
+ thematics: T;
+};
+
+export type TopicResponse<T> = {
+ topic: T;
+};
+
+export type TopicsResponse<T> = {
+ topics: T;
+};
+
+export type PageInfo = {
+ endCursor: string;
+ hasNextPage: boolean;
+ total: number;
+};
+
+export type Edges<T> = {
+ cursor: string;
+ node: T;
+};
+
+export type EdgesResponse<T> = {
+ edges: Edges<T>[];
+ pageInfo: PageInfo;
+};
+
+export type NodeResponse<T> = {
+ node: T;
+};
+
+export type NodesResponse<T> = {
+ nodes: T[];
+};
+
+export type EndCursor = Pick<
+ EdgesResponse<Pick<PageInfo, 'endCursor'>>,
+ 'pageInfo'
+>;
+
+export type ResponseMap<T> = {
+ [articleBySlugQuery]: ArticleResponse<T>;
+ [articlesCardQuery]: ArticlesResponse<NodesResponse<T>>;
+ [articlesEndCursor]: ArticlesResponse<EndCursor>;
+ [articlesQuery]: ArticlesResponse<EdgesResponse<T>>;
+ [articlesSlugQuery]: ArticlesResponse<EdgesResponse<T>>;
+ [commentsQuery]: CommentsResponse<NodesResponse<T>>;
+ [sendCommentMutation]: SendCommentResponse<T>;
+ [sendMailMutation]: SendMailResponse<T>;
+ [thematicBySlugQuery]: ThematicResponse<T>;
+ [thematicsListQuery]: ThematicsResponse<EdgesResponse<T>>;
+ [thematicsSlugQuery]: ThematicsResponse<EdgesResponse<T>>;
+ [topicBySlugQuery]: TopicResponse<T>;
+ [topicsListQuery]: TopicsResponse<EdgesResponse<T>>;
+ [topicsSlugQuery]: TopicsResponse<EdgesResponse<T>>;
+ [totalArticlesQuery]: ArticlesResponse<T>;
+ [totalThematicsQuery]: ThematicsResponse<T>;
+ [totalTopicsQuery]: TopicsResponse<T>;
+};
+
+export type GraphQLResponse<
+ T extends keyof ResponseMap<U>,
+ U
+> = ResponseMap<U>[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 SearchVar = {
+ /**
+ * A search term.
+ */
+ search?: string;
+};
+
+export type SendCommentVars = {
+ /**
+ * The author name.
+ */
+ author: string;
+ /**
+ * The author e-mail address.
+ */
+ authorEmail: string;
+ /**
+ * The author website.
+ */
+ authorUrl: string;
+ /**
+ * A mutation id.
+ */
+ clientMutationId: string;
+ /**
+ * A post or page id.
+ */
+ commentOn: number;
+ /**
+ * The comment body.
+ */
+ content: string;
+ /**
+ * The comment parent.
+ */
+ parent?: number;
+};
+
+export type SendMailVars = {
+ /**
+ * The mail body.
+ */
+ body: string;
+ /**
+ * A mutation id.
+ */
+ clientMutationId: string;
+ /**
+ * The reply to e-mail address.
+ */
+ replyTo: string;
+ /**
+ * The mail subject.
+ */
+ subject: string;
+};
+
+export type VariablesMap = {
+ [articleBySlugQuery]: BySlugVar;
+ [articlesCardQuery]: EdgesVars;
+ [articlesEndCursor]: EdgesVars;
+ [articlesQuery]: EdgesVars;
+ [articlesSlugQuery]: EdgesVars;
+ [commentsQuery]: ByContentIdVar;
+ [sendCommentMutation]: SendCommentVars;
+ [sendMailMutation]: SendMailVars;
+ [thematicBySlugQuery]: BySlugVar;
+ [thematicsListQuery]: EdgesVars;
+ [thematicsSlugQuery]: EdgesVars;
+ [topicBySlugQuery]: BySlugVar;
+ [topicsListQuery]: EdgesVars;
+ [topicsSlugQuery]: EdgesVars;
+ [totalArticlesQuery]: SearchVar;
+ [totalThematicsQuery]: null;
+ [totalTopicsQuery]: null;
+};
+
+export type FetchAPIProps<T extends Queries | Mutations> = {
+ /**
+ * 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<T, U extends Queries | Mutations>({
+ api,
+ query,
+ variables,
+}: FetchAPIProps<U>): Promise<GraphQLResponse<U, T>> {
+ const response = await fetch(api, {
+ method: 'POST',
+ headers: {
+ 'content-type': 'application/json;charset=UTF-8',
+ },
+ body: JSON.stringify({
+ query,
+ variables,
+ }),
+ });
+
+ type JSONResponse = {
+ data?: GraphQLResponse<U, T>;
+ 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..3e1f575
--- /dev/null
+++ b/src/services/graphql/articles.query.ts
@@ -0,0 +1,191 @@
+/**
+ * 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
+ featuredImage {
+ node {
+ altText
+ mediaDetails {
+ height
+ width
+ }
+ sourceUrl
+ title
+ }
+ }
+ slug
+ title
+ }
+ }
+ }
+ author {
+ node {
+ gravatarUrl
+ name
+ url
+ }
+ }
+ commentCount
+ contentParts {
+ afterMore
+ beforeMore
+ }
+ databaseId
+ date
+ featuredImage {
+ node {
+ altText
+ mediaDetails {
+ height
+ width
+ }
+ sourceUrl
+ title
+ }
+ }
+ info {
+ 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
+ }
+ }
+ }
+ commentCount
+ contentParts {
+ beforeMore
+ }
+ databaseId
+ date
+ featuredImage {
+ node {
+ altText
+ mediaDetails {
+ height
+ width
+ }
+ sourceUrl
+ title
+ }
+ }
+ info {
+ 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($search: String = "") {
+ posts(where: {search: $search}) {
+ pageInfo {
+ total
+ }
+ }
+}`;
+
+/**
+ * Query the end cursor based on the queried posts number.
+ */
+export const articlesEndCursor = `query EndCursorAfter($first: Int) {
+ posts(first: $first) {
+ pageInfo {
+ hasNextPage
+ endCursor
+ }
+ }
+}`;
diff --git a/src/services/graphql/articles.ts b/src/services/graphql/articles.ts
new file mode 100644
index 0000000..27406ac
--- /dev/null
+++ b/src/services/graphql/articles.ts
@@ -0,0 +1,200 @@
+import { Slug, 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 { getImageFromRawData } from '@utils/helpers/images';
+import { getPageLinkFromRawData } from '@utils/helpers/pages';
+import {
+ EdgesResponse,
+ EdgesVars,
+ EndCursor,
+ fetchAPI,
+ getAPIUrl,
+ PageInfo,
+} from './api';
+import {
+ articleBySlugQuery,
+ articlesCardQuery,
+ articlesEndCursor,
+ articlesQuery,
+ articlesSlugQuery,
+ totalArticlesQuery,
+} from './articles.query';
+
+/**
+ * Retrieve the total number of articles.
+ *
+ * @returns {Promise<number>} - The articles total number.
+ */
+export const getTotalArticles = async (search?: string): Promise<number> => {
+ const response = await fetchAPI<TotalItems, typeof totalArticlesQuery>({
+ api: getAPIUrl(),
+ query: totalArticlesQuery,
+ variables: { search },
+ });
+
+ 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: author && getAuthorFromRawData(author.node, 'page'),
+ commentsCount: commentCount || 0,
+ cover: featuredImage?.node
+ ? getImageFromRawData(featuredImage.node)
+ : undefined,
+ dates: { publication: date, update: modified },
+ seo: {
+ description: seo?.metaDesc || '',
+ title: seo?.title || '',
+ },
+ thematics: acfPosts.postsInThematic?.map((thematic) =>
+ getPageLinkFromRawData(thematic, 'thematic')
+ ),
+ topics: acfPosts.postsInTopic?.map((topic) =>
+ getPageLinkFromRawData(topic, 'topic')
+ ),
+ wordsCount: info.wordsCount,
+ },
+ slug,
+ title,
+ };
+};
+
+/**
+ * Retrieve the given number of articles from API.
+ *
+ * @param {EdgesVars} props - An object of GraphQL variables.
+ * @returns {Promise<EdgesResponse<RawArticle>>} The articles data.
+ */
+export const getArticles = async (
+ props: EdgesVars
+): Promise<EdgesResponse<RawArticle>> => {
+ const response = await fetchAPI<RawArticle, typeof articlesQuery>({
+ api: getAPIUrl(),
+ query: articlesQuery,
+ variables: { ...props },
+ });
+
+ return response.posts;
+};
+
+/**
+ * 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: { publication: 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));
+};
+
+/**
+ * Retrieve an Article object by slug.
+ *
+ * @param {string} slug - The article slug.
+ * @returns {Promise<Article>} The requested article.
+ */
+export const getArticleBySlug = async (slug: string): Promise<Article> => {
+ const response = await fetchAPI<RawArticle, typeof articleBySlugQuery>({
+ api: getAPIUrl(),
+ query: articleBySlugQuery,
+ variables: { slug },
+ });
+
+ return getArticleFromRawData(response.post);
+};
+
+/**
+ * Retrieve all the articles slugs.
+ *
+ * @returns {Promise<string[]>} - An array of articles slugs.
+ */
+export const getAllArticlesSlugs = async (): Promise<string[]> => {
+ const totalArticles = await getTotalArticles();
+ const response = await fetchAPI<Slug, typeof articlesSlugQuery>({
+ api: getAPIUrl(),
+ query: articlesSlugQuery,
+ variables: { first: totalArticles },
+ });
+
+ return response.posts.edges.map((edge) => edge.node.slug);
+};
+
+/**
+ * Retrieve the last cursor.
+ *
+ * @param {EdgesVars} props - An object of GraphQL variables.
+ * @returns {Promise<string>} - The end cursor.
+ */
+export const getArticlesEndCursor = async (
+ props: EdgesVars
+): Promise<string> => {
+ const response = await fetchAPI<EndCursor, typeof articlesEndCursor>({
+ api: getAPIUrl(),
+ query: articlesEndCursor,
+ variables: { ...props },
+ });
+
+ return response.posts.pageInfo.endCursor;
+};
diff --git a/src/services/graphql/comments.mutation.ts b/src/services/graphql/comments.mutation.ts
new file mode 100644
index 0000000..f52c7e9
--- /dev/null
+++ b/src/services/graphql/comments.mutation.ts
@@ -0,0 +1,30 @@
+/**
+ * Send comment mutation.
+ */
+export const sendCommentMutation = `mutation CreateComment(
+ $author: String!
+ $authorEmail: String!
+ $authorUrl: String!
+ $content: String!
+ $parent: ID = null
+ $commentOn: Int!
+ $clientMutationId: String!
+) {
+ createComment(
+ input: {
+ author: $author
+ authorEmail: $authorEmail
+ authorUrl: $authorUrl
+ content: $content
+ parent: $parent
+ commentOn: $commentOn
+ clientMutationId: $clientMutationId
+ }
+ ) {
+ clientMutationId
+ success
+ comment {
+ approved
+ }
+ }
+}`;
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/comments.ts b/src/services/graphql/comments.ts
new file mode 100644
index 0000000..28ddfd0
--- /dev/null
+++ b/src/services/graphql/comments.ts
@@ -0,0 +1,102 @@
+import { Comment } from '@ts/types/app';
+import { RawComment } from '@ts/types/raw-data';
+import { getAuthorFromRawData } from '@utils/helpers/author';
+import { fetchAPI, getAPIUrl, SendCommentVars } from './api';
+import { sendCommentMutation } from './comments.mutation';
+import { commentsQuery } from './comments.query';
+
+/**
+ * Create a comments tree with replies.
+ *
+ * @param {Comment[]} comments - A flatten comments list.
+ * @returns {Comment[]} An array of comments with replies.
+ */
+export const buildCommentsTree = (comments: Comment[]): Comment[] => {
+ type CommentsHashTable = {
+ [key: string]: Comment;
+ };
+
+ const hashTable: CommentsHashTable = Object.create(null);
+ const commentsTree: Comment[] = [];
+
+ comments.forEach(
+ (comment) => (hashTable[comment.id] = { ...comment, replies: [] })
+ );
+
+ comments.forEach((comment) => {
+ if (!comment.parentId) {
+ commentsTree.push(hashTable[comment.id]);
+ } else {
+ hashTable[comment.parentId].replies.push(hashTable[comment.id]);
+ }
+ });
+
+ return commentsTree;
+};
+
+/**
+ * Convert a comment from RawComment to Comment type.
+ *
+ * @param {RawComment} comment - A raw comment.
+ * @returns {Comment} A formatted comment.
+ */
+export const getCommentFromRawData = (comment: RawComment): Comment => {
+ const { author, databaseId, date, parentDatabaseId, ...data } = comment;
+
+ return {
+ id: databaseId,
+ meta: {
+ author: getAuthorFromRawData(author.node, 'comment'),
+ date,
+ },
+ parentId: parentDatabaseId,
+ replies: [],
+ ...data,
+ };
+};
+
+/**
+ * Retrieve a comments list by post id.
+ *
+ * @param {number} id - A post id.
+ * @returns {Promise<Comment[]>} The comments list.
+ */
+export const getPostComments = async (id: number): Promise<Comment[]> => {
+ const response = await fetchAPI<RawComment, typeof commentsQuery>({
+ api: getAPIUrl(),
+ query: commentsQuery,
+ variables: { contentId: id },
+ });
+
+ const comments = response.comments.nodes.map((comment) =>
+ getCommentFromRawData(comment)
+ );
+
+ return buildCommentsTree(comments);
+};
+
+export type SentComment = {
+ clientMutationId: string;
+ success: boolean;
+ comment: {
+ approved: boolean;
+ } | null;
+};
+
+/**
+ * Send a comment using GraphQL API.
+ *
+ * @param {SendCommentVars} data - The comment data.
+ * @returns {Promise<SentEmail>} The mutation response.
+ */
+export const sendComment = async (
+ data: SendCommentVars
+): Promise<SentComment> => {
+ const response = await fetchAPI<SentComment, typeof sendCommentMutation>({
+ api: getAPIUrl(),
+ query: sendCommentMutation,
+ variables: { ...data },
+ });
+
+ return response.createComment;
+};
diff --git a/src/services/graphql/contact.mutation.ts b/src/services/graphql/contact.mutation.ts
new file mode 100644
index 0000000..b82fc07
--- /dev/null
+++ b/src/services/graphql/contact.mutation.ts
@@ -0,0 +1,25 @@
+/**
+ * Send mail mutation.
+ */
+export const sendMailMutation = `mutation SendEmail(
+ $subject: String!
+ $body: String!
+ $replyTo: String!
+ $clientMutationId: String!
+) {
+ sendEmail(
+ input: {
+ clientMutationId: $clientMutationId
+ body: $body
+ replyTo: $replyTo
+ subject: $subject
+ }
+ ) {
+ clientMutationId
+ message
+ sent
+ origin
+ replyTo
+ to
+ }
+}`;
diff --git a/src/services/graphql/contact.ts b/src/services/graphql/contact.ts
new file mode 100644
index 0000000..00c6ca2
--- /dev/null
+++ b/src/services/graphql/contact.ts
@@ -0,0 +1,26 @@
+import { fetchAPI, getAPIUrl, SendMailVars } from './api';
+import { sendMailMutation } from './contact.mutation';
+
+export type SentEmail = {
+ clientMutationId: string;
+ message: string;
+ origin: string;
+ replyTo: string;
+ sent: boolean;
+};
+
+/**
+ * Send an email using GraphQL API.
+ *
+ * @param {sendMailVars} data - The mail data.
+ * @returns {Promise<SentEmail>} The mutation response.
+ */
+export const sendMail = async (data: SendMailVars): Promise<SentEmail> => {
+ const response = await fetchAPI<SentEmail, typeof sendMailMutation>({
+ api: getAPIUrl(),
+ query: sendMailMutation,
+ variables: { ...data },
+ });
+
+ return response.sendEmail;
+};
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<CreatedComment> => {
- 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<CreateComment>(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<SendEmail>(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<number> => {
- const query = gql`
- query PostsTotal {
- posts {
- pageInfo {
- total
- }
- }
- }
- `;
-
- const response = await fetchApi<TotalArticles>(query, null);
- return response.posts.pageInfo.total;
-};
-
-export const getPublishedPosts = async ({
- first = 10,
- after = '',
- searchQuery = '',
-}: {
- first: number;
- after?: string;
- searchQuery?: string;
-}): Promise<PostsList> => {
- 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<RawPostsList>(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<Slug[]> => {
- // 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<AllPostsSlug>(query, null);
- return response.posts.nodes;
-};
-
-//==============================================================================
-// Single Post query
-//==============================================================================
-
-export const getPostBySlug = async (slug: string): Promise<Article> => {
- 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<PostBy>(query, variables);
-
- return getFormattedPost(response.post);
-};
-
-//==============================================================================
-// Comments query
-//==============================================================================
-
-export const getCommentsByPostId = async (id: number): Promise<Comment[]> => {
- 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<CommentsByPostId>(query, variables);
- const formattedComments = getFormattedComments(response.comments.nodes);
-
- return buildCommentsTree(formattedComments);
-};
-
-//==============================================================================
-// Topic query
-//==============================================================================
-
-export const getTopicBySlug = async (slug: string): Promise<Topic> => {
- 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<TopicBy>(query, variables);
-
- return getFormattedTopic(response.topic);
-};
-
-export const getAllTopicsSlug = async (): Promise<Slug[]> => {
- // 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<AllTopicsSlug>(query, null);
- return response.topics.nodes;
-};
-
-export const getAllTopics = async (): Promise<TopicPreview[]> => {
- // 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<AllTopics>(query, null);
- return response.topics.nodes;
-};
-
-//==============================================================================
-// Thematic query
-//==============================================================================
-
-export const getThematicBySlug = async (slug: string): Promise<Thematic> => {
- 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<ThematicBy>(query, variables);
-
- return getFormattedThematic(response.thematic);
-};
-
-export const getAllThematicsSlug = async (): Promise<Slug[]> => {
- // 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<AllThematicsSlug>(query, null);
- return response.thematics.nodes;
-};
-
-export const getAllThematics = async (): Promise<ThematicPreview[]> => {
- // 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<AllThematics>(query, null);
- return response.thematics.nodes;
-};
-
-export const getEndCursor = async ({
- first = 10,
-}: {
- first: number;
-}): Promise<string> => {
- const query = gql`
- query EndCursorAfter($first: Int) {
- posts(first: $first) {
- pageInfo {
- hasNextPage
- endCursor
- }
- }
- }
- `;
-
- const variables = { first };
- const response = await fetchApi<LastPostCursor>(query, variables);
-
- return response.posts.pageInfo.endCursor;
-};
diff --git a/src/services/graphql/thematics.query.ts b/src/services/graphql/thematics.query.ts
new file mode 100644
index 0000000..5a82133
--- /dev/null
+++ b/src/services/graphql/thematics.query.ts
@@ -0,0 +1,125 @@
+/**
+ * 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 {
+ wordsCount
+ }
+ modified
+ slug
+ title
+ }
+ }
+ }
+ contentParts {
+ afterMore
+ beforeMore
+ }
+ databaseId
+ date
+ featuredImage {
+ node {
+ altText
+ mediaDetails {
+ height
+ width
+ }
+ sourceUrl
+ title
+ }
+ }
+ info {
+ 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
+ }
+ }
+}`;
+
+/**
+ * Query the total number of thematics.
+ */
+export const totalThematicsQuery = `query ThematicsTotal {
+ thematics {
+ pageInfo {
+ total
+ }
+ }
+}`;
diff --git a/src/services/graphql/thematics.ts b/src/services/graphql/thematics.ts
new file mode 100644
index 0000000..4dc69e7
--- /dev/null
+++ b/src/services/graphql/thematics.ts
@@ -0,0 +1,162 @@
+import { PageLink, Slug, Thematic } from '@ts/types/app';
+import {
+ RawArticle,
+ RawThematic,
+ RawThematicPreview,
+ TotalItems,
+} from '@ts/types/raw-data';
+import { getImageFromRawData } from '@utils/helpers/images';
+import { getPageLinkFromRawData } from '@utils/helpers/pages';
+import { EdgesResponse, EdgesVars, fetchAPI, getAPIUrl } from './api';
+import { getArticleFromRawData } from './articles';
+import {
+ thematicBySlugQuery,
+ thematicsListQuery,
+ thematicsSlugQuery,
+ totalThematicsQuery,
+} from './thematics.query';
+
+/**
+ * Retrieve the total number of thematics.
+ *
+ * @returns {Promise<number>} - The thematics total number.
+ */
+export const getTotalThematics = async (): Promise<number> => {
+ const response = await fetchAPI<TotalItems, typeof totalThematicsQuery>({
+ api: getAPIUrl(),
+ query: totalThematicsQuery,
+ });
+
+ return response.thematics.pageInfo.total;
+};
+
+/**
+ * Retrieve the given number of thematics from API.
+ *
+ * @param {EdgesVars} props - An object of GraphQL variables.
+ * @returns {Promise<EdgesResponse<RawThematicPreview>>} The thematics data.
+ */
+export const getThematicsPreview = async (
+ props: EdgesVars
+): Promise<EdgesResponse<RawThematicPreview>> => {
+ const response = await fetchAPI<
+ RawThematicPreview,
+ typeof thematicsListQuery
+ >({ api: getAPIUrl(), query: thematicsListQuery, variables: props });
+
+ return response.thematics;
+};
+
+/**
+ * Convert raw data to an Thematic object.
+ *
+ * @param {RawThematic} data - The page raw data.
+ * @returns {Thematic} The page data.
+ */
+export const getThematicFromRawData = (data: RawThematic): Thematic => {
+ const {
+ acfThematics,
+ contentParts,
+ databaseId,
+ date,
+ featuredImage,
+ info,
+ modified,
+ slug,
+ title,
+ seo,
+ } = data;
+
+ /**
+ * Retrieve an array of related topics.
+ *
+ * @param posts - The thematic posts.
+ * @returns {PageLink[]} An array of topics links.
+ */
+ const getRelatedTopics = (posts: RawArticle[]): PageLink[] => {
+ const topics: PageLink[] = [];
+
+ posts.forEach((post) => {
+ if (post.acfPosts.postsInTopic) {
+ post.acfPosts.postsInTopic.forEach((topic) =>
+ topics.push(getPageLinkFromRawData(topic, 'topic'))
+ );
+ }
+ });
+
+ const topicsIds = topics.map((topic) => topic.id);
+ const uniqueTopics = topics.filter(
+ ({ id }, index) => !topicsIds.includes(id, index + 1)
+ );
+ const sortTopicByName = (a: PageLink, b: PageLink) => {
+ var nameA = a.name.toUpperCase(); // ignore upper and lowercase
+ var nameB = b.name.toUpperCase(); // ignore upper and lowercase
+ if (nameA < nameB) {
+ return -1;
+ }
+ if (nameA > nameB) {
+ return 1;
+ }
+
+ // names must be equal
+ return 0;
+ };
+
+ return uniqueTopics.sort(sortTopicByName);
+ };
+
+ return {
+ content: contentParts.afterMore,
+ id: databaseId,
+ intro: contentParts.beforeMore,
+ meta: {
+ articles: acfThematics.postsInThematic.map((post) =>
+ getArticleFromRawData(post)
+ ),
+ cover: featuredImage?.node
+ ? getImageFromRawData(featuredImage.node)
+ : undefined,
+ dates: { publication: date, update: modified },
+ seo: {
+ description: seo?.metaDesc || '',
+ title: seo?.title || '',
+ },
+ topics: getRelatedTopics(acfThematics.postsInThematic),
+ wordsCount: info.wordsCount,
+ },
+ slug,
+ title,
+ };
+};
+
+/**
+ * Retrieve a Thematic object by slug.
+ *
+ * @param {string} slug - The thematic slug.
+ * @returns {Promise<Article>} The requested thematic.
+ */
+export const getThematicBySlug = async (slug: string): Promise<Thematic> => {
+ const response = await fetchAPI<RawThematic, typeof thematicBySlugQuery>({
+ api: getAPIUrl(),
+ query: thematicBySlugQuery,
+ variables: { slug },
+ });
+
+ return getThematicFromRawData(response.thematic);
+};
+
+/**
+ * Retrieve all the thematics slugs.
+ *
+ * @returns {Promise<string[]>} - An array of thematics slugs.
+ */
+export const getAllThematicsSlugs = async (): Promise<string[]> => {
+ const totalThematics = await getTotalThematics();
+ const response = await fetchAPI<Slug, typeof thematicsSlugQuery>({
+ api: getAPIUrl(),
+ query: thematicsSlugQuery,
+ variables: { first: totalThematics },
+ });
+
+ return response.thematics.edges.map((edge) => edge.node.slug);
+};
diff --git a/src/services/graphql/topics.query.ts b/src/services/graphql/topics.query.ts
new file mode 100644
index 0000000..57b2569
--- /dev/null
+++ b/src/services/graphql/topics.query.ts
@@ -0,0 +1,137 @@
+/**
+ * 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 {
+ wordsCount
+ }
+ modified
+ slug
+ title
+ }
+ }
+ }
+ contentParts {
+ afterMore
+ beforeMore
+ }
+ databaseId
+ date
+ featuredImage {
+ node {
+ altText
+ mediaDetails {
+ height
+ width
+ }
+ sourceUrl
+ title
+ }
+ }
+ info {
+ 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
+ featuredImage {
+ node {
+ altText
+ mediaDetails {
+ height
+ width
+ }
+ sourceUrl
+ title
+ }
+ }
+ 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
+ }
+ }
+}`;
+
+/**
+ * Query the total number of topics.
+ */
+export const totalTopicsQuery = `query TopicsTotal {
+ topics {
+ pageInfo {
+ total
+ }
+ }
+}`;
diff --git a/src/services/graphql/topics.ts b/src/services/graphql/topics.ts
new file mode 100644
index 0000000..0b1971b
--- /dev/null
+++ b/src/services/graphql/topics.ts
@@ -0,0 +1,164 @@
+import { PageLink, Slug, Topic } from '@ts/types/app';
+import {
+ RawArticle,
+ RawTopic,
+ RawTopicPreview,
+ TotalItems,
+} from '@ts/types/raw-data';
+import { getImageFromRawData } from '@utils/helpers/images';
+import { getPageLinkFromRawData } from '@utils/helpers/pages';
+import { EdgesResponse, EdgesVars, fetchAPI, getAPIUrl } from './api';
+import { getArticleFromRawData } from './articles';
+import {
+ topicBySlugQuery,
+ topicsListQuery,
+ topicsSlugQuery,
+ totalTopicsQuery,
+} from './topics.query';
+
+/**
+ * Retrieve the total number of topics.
+ *
+ * @returns {Promise<number>} - The topics total number.
+ */
+export const getTotalTopics = async (): Promise<number> => {
+ const response = await fetchAPI<TotalItems, typeof totalTopicsQuery>({
+ api: getAPIUrl(),
+ query: totalTopicsQuery,
+ });
+
+ return response.topics.pageInfo.total;
+};
+
+/**
+ * Retrieve the given number of topics from API.
+ *
+ * @param {EdgesVars} props - An object of GraphQL variables.
+ * @returns {Promise<EdgesResponse<RawTopicPreview>>} The topics data.
+ */
+export const getTopicsPreview = async (
+ props: EdgesVars
+): Promise<EdgesResponse<RawTopicPreview>> => {
+ const response = await fetchAPI<RawTopicPreview, typeof topicsListQuery>({
+ api: getAPIUrl(),
+ query: topicsListQuery,
+ variables: props,
+ });
+
+ return response.topics;
+};
+
+/**
+ * Convert raw data to a Topic object.
+ *
+ * @param {RawTopic} data - The page raw data.
+ * @returns {Topic} The page data.
+ */
+export const getTopicFromRawData = (data: RawTopic): Topic => {
+ const {
+ acfTopics,
+ contentParts,
+ databaseId,
+ date,
+ featuredImage,
+ info,
+ modified,
+ slug,
+ title,
+ seo,
+ } = data;
+
+ /**
+ * Retrieve an array of related topics.
+ *
+ * @param posts - The topic posts.
+ * @returns {PageLink[]} An array of topics links.
+ */
+ const getRelatedThematics = (posts: RawArticle[]): PageLink[] => {
+ const thematics: PageLink[] = [];
+
+ posts.forEach((post) => {
+ if (post.acfPosts.postsInThematic) {
+ post.acfPosts.postsInThematic.forEach((thematic) =>
+ thematics.push(getPageLinkFromRawData(thematic, 'thematic'))
+ );
+ }
+ });
+
+ const thematicsIds = thematics.map((thematic) => thematic.id);
+ const uniqueThematics = thematics.filter(
+ ({ id }, index) => !thematicsIds.includes(id, index + 1)
+ );
+ const sortThematicByName = (a: PageLink, b: PageLink) => {
+ var nameA = a.name.toUpperCase(); // ignore upper and lowercase
+ var nameB = b.name.toUpperCase(); // ignore upper and lowercase
+ if (nameA < nameB) {
+ return -1;
+ }
+ if (nameA > nameB) {
+ return 1;
+ }
+
+ // names must be equal
+ return 0;
+ };
+
+ return uniqueThematics.sort(sortThematicByName);
+ };
+
+ return {
+ content: contentParts.afterMore,
+ id: databaseId,
+ intro: contentParts.beforeMore,
+ meta: {
+ articles: acfTopics.postsInTopic.map((post) =>
+ getArticleFromRawData(post)
+ ),
+ cover: featuredImage?.node
+ ? getImageFromRawData(featuredImage.node)
+ : undefined,
+ dates: { publication: date, update: modified },
+ website: acfTopics.officialWebsite,
+ seo: {
+ description: seo?.metaDesc || '',
+ title: seo?.title || '',
+ },
+ thematics: getRelatedThematics(acfTopics.postsInTopic),
+ wordsCount: info.wordsCount,
+ },
+ slug,
+ title,
+ };
+};
+
+/**
+ * Retrieve a Topic object by slug.
+ *
+ * @param {string} slug - The topic slug.
+ * @returns {Promise<Article>} The requested topic.
+ */
+export const getTopicBySlug = async (slug: string): Promise<Topic> => {
+ const response = await fetchAPI<RawTopic, typeof topicBySlugQuery>({
+ api: getAPIUrl(),
+ query: topicBySlugQuery,
+ variables: { slug },
+ });
+
+ return getTopicFromRawData(response.topic);
+};
+
+/**
+ * Retrieve all the topics slugs.
+ *
+ * @returns {Promise<string[]>} - An array of topics slugs.
+ */
+export const getAllTopicsSlugs = async (): Promise<string[]> => {
+ const totalTopics = await getTotalTopics();
+ const response = await fetchAPI<Slug, typeof topicsSlugQuery>({
+ api: getAPIUrl(),
+ query: topicsSlugQuery,
+ variables: { first: totalTopics },
+ });
+
+ return response.topics.edges.map((edge) => edge.node.slug);
+};