aboutsummaryrefslogtreecommitdiffstats
path: root/src/utils
diff options
context:
space:
mode:
authorArmand Philippot <git@armandphilippot.com>2023-11-24 20:00:08 +0100
committerArmand Philippot <git@armandphilippot.com>2023-11-27 14:47:51 +0100
commitf111685c5886f3e77edfd3621c98d8ac1b9bcce4 (patch)
tree62a541fe3afeb64bf745443706fbfb02e96c5230 /src/utils
parentbee515641cb144be9a855ff2cac258d2fedab21d (diff)
refactor(services, types): reorganize GraphQL fetchers and data types
The Typescript mapped types was useful for autocompletion in fetchers but their are harder to maintain. I think it's better to keep each query close to its fetcher to have a better understanding of the fetched data. So I: * colocate queries with their own fetcher * colocate mutations with their own mutator * remove Typescript mapped types for queries and mutations * move data convertors inside graphql services * rename most of data types and fetchers
Diffstat (limited to 'src/utils')
-rw-r--r--src/utils/helpers/author.ts31
-rw-r--r--src/utils/helpers/graphql.ts64
-rw-r--r--src/utils/helpers/images.ts17
-rw-r--r--src/utils/helpers/index.ts3
-rw-r--r--src/utils/helpers/pages.tsx83
-rw-r--r--src/utils/helpers/rss.ts25
-rw-r--r--src/utils/helpers/server/projects.ts38
-rw-r--r--src/utils/hooks/use-article.ts28
-rw-r--r--src/utils/hooks/use-comments.ts21
-rw-r--r--src/utils/hooks/use-pagination/use-pagination.test.ts9
-rw-r--r--src/utils/hooks/use-pagination/use-pagination.ts17
-rw-r--r--src/utils/hooks/use-posts-list/use-posts-list.test.ts4
-rw-r--r--src/utils/hooks/use-posts-list/use-posts-list.ts50
13 files changed, 169 insertions, 221 deletions
diff --git a/src/utils/helpers/author.ts b/src/utils/helpers/author.ts
deleted file mode 100644
index a5e9bc6..0000000
--- a/src/utils/helpers/author.ts
+++ /dev/null
@@ -1,31 +0,0 @@
-import { type Author, type ContentKind, type RawAuthor } from '../../types';
-
-/**
- * Convert author raw data to regular data.
- *
- * @param {RawAuthor<ContentKind>} data - The author raw data.
- * @param {ContentKind} kind - The author kind. Either `page` or `comment`.
- * @param {number} [avatarSize] - The author avatar size.
- * @returns {Author<ContentKind>} The author data.
- */
-export const getAuthorFromRawData = (
- data: RawAuthor<typeof kind>,
- kind: ContentKind,
- avatarSize: number = 80
-): Author<typeof kind> => {
- 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/graphql.ts b/src/utils/helpers/graphql.ts
new file mode 100644
index 0000000..e07b151
--- /dev/null
+++ b/src/utils/helpers/graphql.ts
@@ -0,0 +1,64 @@
+import type { Nullable } from '../../types';
+import { CONFIG } from '../config';
+
+/**
+ * Retrieve the API url from settings.
+ *
+ * @returns {string} The API url.
+ */
+export const getGraphQLUrl = (): string => {
+ if (!CONFIG.api.url) throw new Error('You forgot to define the API url.');
+
+ return CONFIG.api.url;
+};
+
+export type GraphQLData<T> = Record<string, Nullable<T>>;
+
+type GraphQLResponse<T extends GraphQLData<unknown>> = {
+ data: T;
+ errors?: { message: string }[];
+};
+
+export type FetchGraphQLConfig = {
+ query: string;
+ url: string;
+ variables?: Record<string, unknown>;
+};
+
+/**
+ * Retrieve GraphQL data using fetch.
+ *
+ * @template T - The expected data type.
+ * @param {FetchGraphQLConfig} config - A configuration object.
+ * @returns {Promise<T>} The data.
+ */
+export const fetchGraphQL = async <
+ T extends GraphQLData<unknown> = GraphQLData<unknown>,
+>({
+ query,
+ url,
+ variables,
+}: FetchGraphQLConfig): Promise<T> => {
+ const response = await fetch(url, {
+ method: 'POST',
+ headers: {
+ 'content-type': 'application/json;charset=UTF-8',
+ },
+ body: JSON.stringify({
+ query,
+ variables,
+ }),
+ });
+
+ const { data, errors }: GraphQLResponse<T> = await response.json();
+
+ if (!response.ok) {
+ const error = new Error(
+ errors?.map((e) => e.message).join('\n') ?? 'Network response was not OK'
+ );
+
+ return Promise.reject(error);
+ }
+
+ return data;
+};
diff --git a/src/utils/helpers/images.ts b/src/utils/helpers/images.ts
deleted file mode 100644
index 6e0c2c5..0000000
--- a/src/utils/helpers/images.ts
+++ /dev/null
@@ -1,17 +0,0 @@
-import { type Image, type RawCover } from '../../types';
-
-/**
- * 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/index.ts b/src/utils/helpers/index.ts
index 92f9424..94fde45 100644
--- a/src/utils/helpers/index.ts
+++ b/src/utils/helpers/index.ts
@@ -1,5 +1,4 @@
-export * from './author';
-export * from './images';
+export * from './graphql';
export * from './pages';
export * from './reading-time';
export * from './refs';
diff --git a/src/utils/helpers/pages.tsx b/src/utils/helpers/pages.tsx
index 7b6bdca..9e015db 100644
--- a/src/utils/helpers/pages.tsx
+++ b/src/utils/helpers/pages.tsx
@@ -1,43 +1,7 @@
import NextImage from 'next/image';
import type { LinksWidgetItemData, PostData } from '../../components';
-import { getArticleFromRawData } from '../../services/graphql';
-import type {
- Article,
- EdgesResponse,
- PageLink,
- RawArticle,
- RawThematicPreview,
- RawTopicPreview,
-} from '../../types';
+import type { ArticlePreview, PageLink } from '../../types';
import { ROUTES } from '../constants';
-import { getImageFromRawData } from './images';
-
-/**
- * Convert raw data to a Link object.
- *
- * @param data - An object.
- * @param {number} data.databaseId - The data id.
- * @param {number} [data.logo] - The data logo.
- * @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,
- kind: 'thematic' | 'topic'
-): PageLink => {
- const { databaseId, featuredImage, slug, title } = data;
- const baseUrl = `${
- kind === 'thematic' ? ROUTES.THEMATICS.INDEX : ROUTES.TOPICS
- }/`;
-
- return {
- id: databaseId,
- logo: featuredImage ? getImageFromRawData(featuredImage.node) : undefined,
- name: title,
- url: `${baseUrl}${slug}`,
- };
-};
/**
* Method to sort PageLink objects by name.
@@ -73,55 +37,28 @@ export const getLinksItemData = (links: PageLink[]): LinksWidgetItemData[] =>
/**
* Retrieve the posts list with the article URL.
*
- * @param {Article[]} posts - An array of articles.
+ * @param {ArticlePreview[]} posts - An array of articles.
* @returns {PostData[]} An array of posts with full article URL.
*/
-export const getPostsWithUrl = (posts: Article[]): PostData[] =>
- posts.map(({ intro, meta, slug, title, ...post }) => {
+export const getPostsWithUrl = (posts: ArticlePreview[]): PostData[] =>
+ posts.map(({ id, intro, meta, slug, title, ...post }) => {
return {
...post,
cover: meta.cover ? <NextImage {...meta.cover} /> : undefined,
excerpt: intro,
heading: title,
+ id,
meta: {
publicationDate: meta.dates.publication,
updateDate: meta.dates.update,
wordsCount: meta.wordsCount,
- author: meta.author?.name,
thematics: meta.thematics,
- topics: meta.topics,
- comments:
- meta.commentsCount === undefined
- ? undefined
- : {
- count: meta.commentsCount,
- postHeading: title,
- url: `${ROUTES.ARTICLE}/${slug}#comments`,
- },
+ comments: {
+ count: meta.commentsCount ?? 0,
+ postHeading: title,
+ url: `${ROUTES.ARTICLE}/${slug}#comments`,
+ },
},
url: `${ROUTES.ARTICLE}/${slug}`,
};
});
-
-/**
- * Retrieve the posts list from raw data.
- *
- * @param {EdgesResponse<RawArticle>[]} rawData - The raw data.
- * @returns {PostData[]} An array of posts.
- */
-export const getPostsList = async (
- rawData: EdgesResponse<RawArticle>[]
-): Promise<PostData[]> => {
- const articlesList: RawArticle[] = [];
- rawData.forEach((articleData) => {
- articleData.edges.forEach((edge) => {
- articlesList.push(edge.node);
- });
- });
-
- return getPostsWithUrl(
- await Promise.all(
- articlesList.map(async (article) => getArticleFromRawData(article))
- )
- );
-};
diff --git a/src/utils/helpers/rss.ts b/src/utils/helpers/rss.ts
index d9c3b1e..82fa1ee 100644
--- a/src/utils/helpers/rss.ts
+++ b/src/utils/helpers/rss.ts
@@ -1,28 +1,25 @@
import { Feed } from 'feed';
import {
- getArticleFromRawData,
- getArticles,
- getTotalArticles,
+ convertPostPreviewToArticlePreview,
+ fetchPostsList,
+ fetchPostsCount,
} from '../../services/graphql';
-import type { Article } from '../../types';
+import type { ArticlePreview } from '../../types';
import { CONFIG } from '../config';
import { ROUTES } from '../constants';
/**
* Retrieve the data for all the articles.
*
- * @returns {Promise<Article[]>} - All the articles.
+ * @returns {Promise<ArticlePreview[]>} - All the articles.
*/
-const getAllArticles = async (): Promise<Article[]> => {
- const totalArticles = await getTotalArticles();
- const rawArticles = await getArticles({ first: totalArticles });
- const articles: Article[] = [];
+const getAllArticles = async (): Promise<ArticlePreview[]> => {
+ const totalPosts = await fetchPostsCount();
+ const posts = await fetchPostsList({ first: totalPosts });
- rawArticles.edges.forEach(async (edge) => {
- articles.push(await getArticleFromRawData(edge.node));
- });
-
- return articles;
+ return posts.edges.map((edge) =>
+ convertPostPreviewToArticlePreview(edge.node)
+ );
};
/**
diff --git a/src/utils/helpers/server/projects.ts b/src/utils/helpers/server/projects.ts
index ed73da8..c1a3d10 100644
--- a/src/utils/helpers/server/projects.ts
+++ b/src/utils/helpers/server/projects.ts
@@ -1,10 +1,6 @@
import { readdirSync } from 'fs';
import path from 'path';
-import {
- type MDXProjectMeta,
- type ProjectCard,
- type ProjectPreview,
-} from '../../../types';
+import type { MDXProjectMeta, Project, ProjectPreview } from '../../../types';
/**
* Retrieve all the projects filename.
@@ -24,9 +20,7 @@ export const getProjectFilenames = (): string[] => {
* @param {string} filename - The project filename.
* @returns {Promise<ProjectPreview>}
*/
-export const getProjectData = async (
- filename: string
-): Promise<ProjectPreview> => {
+export const getProjectData = async (filename: string): Promise<Project> => {
try {
const {
meta,
@@ -53,7 +47,7 @@ export const getProjectData = async (
},
},
slug: filename,
- title: title,
+ title,
};
} catch (err) {
console.error(err);
@@ -65,28 +59,24 @@ export const getProjectData = async (
* Retrieve all the projects data using filenames.
*
* @param {string[]} filenames - The filenames without extension.
- * @returns {Promise<ProjectCard[]>} - An array of projects data.
+ * @returns {Promise<ProjectPreview[]>} - An array of projects data.
*/
export const getProjectsData = async (
filenames: string[]
-): Promise<ProjectCard[]> => {
- return Promise.all(
- filenames.map(async (filename) => {
- const { id, meta, slug, title } = await getProjectData(filename);
- const { cover, dates, tagline, technologies } = meta;
- return { id, meta: { cover, dates, tagline, technologies }, slug, title };
- })
- );
-};
+): Promise<ProjectPreview[]> =>
+ Promise.all(filenames.map(async (filename) => getProjectData(filename)));
/**
* Method to sort an array of projects by publication date.
*
- * @param {ProjectCard} a - A single project.
- * @param {ProjectCard} b - A single project.
+ * @param {ProjectPreview} a - A single project.
+ * @param {ProjectPreview} b - A single project.
* @returns The result used by Array.sort() method: 1 || -1 || 0.
*/
-const sortProjectsByPublicationDate = (a: ProjectCard, b: ProjectCard) => {
+const sortProjectsByPublicationDate = (
+ a: ProjectPreview,
+ b: ProjectPreview
+) => {
if (a.meta.dates.publication < b.meta.dates.publication) return 1;
if (a.meta.dates.publication > b.meta.dates.publication) return -1;
return 0;
@@ -95,9 +85,9 @@ const sortProjectsByPublicationDate = (a: ProjectCard, b: ProjectCard) => {
/**
* Retrieve all projects in content folder sorted by publication date.
*
- * @returns {Promise<ProjectCard[]>} An array of projects.
+ * @returns {Promise<ProjectPreview[]>} An array of projects.
*/
-export const getProjectsCard = async (): Promise<ProjectCard[]> => {
+export const getProjectsCard = async (): Promise<ProjectPreview[]> => {
const filenames = getProjectFilenames();
const projects = await getProjectsData(filenames);
diff --git a/src/utils/hooks/use-article.ts b/src/utils/hooks/use-article.ts
index f339f7f..5e54ee4 100644
--- a/src/utils/hooks/use-article.ts
+++ b/src/utils/hooks/use-article.ts
@@ -1,11 +1,7 @@
import { useEffect, useState } from 'react';
import useSWR from 'swr';
-import {
- articleBySlugQuery,
- fetchAPI,
- getArticleFromRawData,
-} from '../../services/graphql';
-import type { Article, Maybe, RawArticle } from '../../types';
+import { convertPostToArticle, fetchPost } from '../../services/graphql';
+import type { Article, Maybe } from '../../types';
export type UseArticleConfig = {
/**
@@ -28,24 +24,12 @@ export const useArticle = ({
slug,
fallback,
}: UseArticleConfig): Article | undefined => {
- const { data } = useSWR(
- slug ? { query: articleBySlugQuery, variables: { slug } } : null,
- fetchAPI<RawArticle, typeof articleBySlugQuery>,
- {}
- );
- const [article, setArticle] = useState<Maybe<Article>>();
+ const { data } = useSWR(slug, fetchPost, {});
+ const [article, setArticle] = useState<Maybe<Article>>(fallback);
useEffect(() => {
- const getArticle = async () => {
- if (data) {
- setArticle(await getArticleFromRawData(data.post));
- } else {
- setArticle(fallback);
- }
- };
-
- getArticle();
- }, [data, fallback]);
+ if (data) convertPostToArticle(data).then((post) => setArticle(post));
+ }, [data]);
return article;
};
diff --git a/src/utils/hooks/use-comments.ts b/src/utils/hooks/use-comments.ts
index ac723e9..94a2d7e 100644
--- a/src/utils/hooks/use-comments.ts
+++ b/src/utils/hooks/use-comments.ts
@@ -1,9 +1,13 @@
import useSWR from 'swr';
-import { getAllComments } from '../../services/graphql';
+import {
+ type FetchCommentsListInput,
+ fetchCommentsList,
+ convertWPCommentToComment,
+ buildCommentsTree,
+} from '../../services/graphql';
import type { SingleComment } from '../../types';
-export type UseCommentsConfig = {
- contentId?: string | number;
+export type UseCommentsConfig = FetchCommentsListInput & {
fallback?: SingleComment[];
};
@@ -14,10 +18,15 @@ export type UseCommentsConfig = {
* @returns {SingleComment[]|undefined}
*/
export const useComments = ({
- contentId,
fallback,
+ ...input
}: UseCommentsConfig): SingleComment[] | undefined => {
- const { data } = useSWR(contentId ? { contentId } : null, getAllComments, {});
+ const { data } = useSWR(input, fetchCommentsList, {});
- return data ?? fallback;
+ if (!data) return fallback;
+
+ const comments = data.map(convertWPCommentToComment);
+ const commentsTree = buildCommentsTree(comments);
+
+ return commentsTree;
};
diff --git a/src/utils/hooks/use-pagination/use-pagination.test.ts b/src/utils/hooks/use-pagination/use-pagination.test.ts
index 20cb37e..18f3ac5 100644
--- a/src/utils/hooks/use-pagination/use-pagination.test.ts
+++ b/src/utils/hooks/use-pagination/use-pagination.test.ts
@@ -1,8 +1,11 @@
import { beforeEach, describe, expect, it, jest } from '@jest/globals';
import { act, renderHook, waitFor } from '@testing-library/react';
import { getConnection } from '../../../../tests/utils/graphql';
-import type { EdgesResponse, GraphQLEdgesInput, Search } from '../../../types';
-import { usePagination } from './use-pagination';
+import type { GraphQLConnection } from '../../../types';
+import {
+ type UsePaginationFetcherInput,
+ usePagination,
+} from './use-pagination';
type Data = {
id: number;
@@ -24,7 +27,7 @@ describe('usePagination', () => {
after,
first,
search,
- }: GraphQLEdgesInput & Search): Promise<EdgesResponse<Data>> => {
+ }: UsePaginationFetcherInput): Promise<GraphQLConnection<Data>> => {
const filteredData = search
? data.filter((d) => d.title.includes(search))
: data;
diff --git a/src/utils/hooks/use-pagination/use-pagination.ts b/src/utils/hooks/use-pagination/use-pagination.ts
index 4df521b..2a40aa4 100644
--- a/src/utils/hooks/use-pagination/use-pagination.ts
+++ b/src/utils/hooks/use-pagination/use-pagination.ts
@@ -1,22 +1,25 @@
import { useCallback } from 'react';
import useSWRInfinite, { type SWRInfiniteKeyLoader } from 'swr/infinite';
import type {
- EdgesResponse,
+ GraphQLConnection,
GraphQLEdgesInput,
Maybe,
Nullable,
- Search,
} from '../../../types';
+export type UsePaginationFetcherInput = GraphQLEdgesInput & {
+ search?: string;
+};
+
export type UsePaginationConfig<T> = {
/**
* The initial data.
*/
- fallback?: EdgesResponse<T>[];
+ fallback?: GraphQLConnection<T>[];
/**
* A function to fetch more data.
*/
- fetcher: (props: GraphQLEdgesInput & Search) => Promise<EdgesResponse<T>>;
+ fetcher: (props: UsePaginationFetcherInput) => Promise<GraphQLConnection<T>>;
/**
* The number of results per page.
*/
@@ -31,7 +34,7 @@ export type UsePaginationReturn<T> = {
/**
* The data from the API.
*/
- data: Maybe<EdgesResponse<T>[]>;
+ data: Maybe<GraphQLConnection<T>[]>;
/**
* An error thrown by fetcher.
*/
@@ -88,8 +91,8 @@ export const usePagination = <T>({
perPage,
searchQuery,
}: UsePaginationConfig<T>): UsePaginationReturn<T> => {
- const getKey: SWRInfiniteKeyLoader<EdgesResponse<T>> = useCallback(
- (pageIndex, previousPageData): Nullable<GraphQLEdgesInput & Search> => {
+ const getKey: SWRInfiniteKeyLoader<GraphQLConnection<T>> = useCallback(
+ (pageIndex, previousPageData): Nullable<UsePaginationFetcherInput> => {
if (previousPageData && !previousPageData.edges.length) return null;
return {
diff --git a/src/utils/hooks/use-posts-list/use-posts-list.test.ts b/src/utils/hooks/use-posts-list/use-posts-list.test.ts
index 1d11111..ff69de2 100644
--- a/src/utils/hooks/use-posts-list/use-posts-list.test.ts
+++ b/src/utils/hooks/use-posts-list/use-posts-list.test.ts
@@ -1,13 +1,13 @@
import { describe, expect, it } from '@jest/globals';
import { act, renderHook } from '@testing-library/react';
-import { getArticles } from '../../../services/graphql';
+import { fetchPostsList } from '../../../services/graphql';
import { usePostsList } from './use-posts-list';
describe('usePostsList', () => {
it('can return the first new result index when loading more posts', async () => {
const perPage = 5;
const { result } = renderHook(() =>
- usePostsList({ fetcher: getArticles, perPage })
+ usePostsList({ fetcher: fetchPostsList, perPage })
);
expect.assertions(2);
diff --git a/src/utils/hooks/use-posts-list/use-posts-list.ts b/src/utils/hooks/use-posts-list/use-posts-list.ts
index 980d531..bb77f31 100644
--- a/src/utils/hooks/use-posts-list/use-posts-list.ts
+++ b/src/utils/hooks/use-posts-list/use-posts-list.ts
@@ -1,29 +1,34 @@
-import { useCallback, useEffect, useState } from 'react';
-import type { PostData } from '../../../components';
-import type { Maybe, RawArticle } from '../../../types';
-import { getPostsList } from '../../helpers';
+import { useCallback, useState } from 'react';
+import type {
+ ArticlePreview,
+ GraphQLConnection,
+ GraphQLEdge,
+ Maybe,
+ WPPostPreview,
+} from '../../../types';
import {
type UsePaginationConfig,
usePagination,
type UsePaginationReturn,
} from '../use-pagination';
+import { convertPostPreviewToArticlePreview } from 'src/services/graphql';
export type usePostsListReturn = Omit<
- UsePaginationReturn<RawArticle>,
+ UsePaginationReturn<WPPostPreview>,
'data'
> & {
/**
- * The index of the first new result when loading more posts.
+ * The articles list.
*/
- firstNewResultIndex: Maybe<number>;
+ articles: Maybe<GraphQLConnection<ArticlePreview>[]>;
/**
- * The posts list.
+ * The index of the first new result when loading more posts.
*/
- posts: Maybe<PostData[]>;
+ firstNewResultIndex: Maybe<number>;
};
export const usePostsList = (
- config: UsePaginationConfig<RawArticle>
+ config: UsePaginationConfig<WPPostPreview>
): usePostsListReturn => {
const {
data,
@@ -40,15 +45,6 @@ export const usePostsList = (
} = usePagination(config);
const [firstNewResultIndex, setFirstNewResultIndex] =
useState<Maybe<number>>(undefined);
- const [posts, setPosts] = useState<Maybe<PostData[]>>(undefined);
-
- useEffect(() => {
- const getPosts = async () => {
- if (data) setPosts(await getPostsList(data));
- };
-
- getPosts();
- }, [data]);
const handleLoadMore = useCallback(async () => {
setFirstNewResultIndex(size * config.perPage + 1);
@@ -56,7 +52,22 @@ export const usePostsList = (
await loadMore();
}, [config.perPage, loadMore, size]);
+ const articles: Maybe<GraphQLConnection<ArticlePreview>[]> = data?.map(
+ (page): GraphQLConnection<ArticlePreview> => {
+ return {
+ edges: page.edges.map((edge): GraphQLEdge<ArticlePreview> => {
+ return {
+ cursor: edge.cursor,
+ node: convertPostPreviewToArticlePreview(edge.node),
+ };
+ }),
+ pageInfo: page.pageInfo,
+ };
+ }
+ );
+
return {
+ articles,
error,
firstNewResultIndex,
hasNextPage,
@@ -67,7 +78,6 @@ export const usePostsList = (
isRefreshing,
isValidating,
loadMore: handleLoadMore,
- posts,
size,
};
};