diff options
Diffstat (limited to 'src')
| -rw-r--r-- | src/pages/projets/[slug].tsx | 35 | ||||
| -rw-r--r-- | src/services/github/fetch-github-repo-meta.test.ts | 47 | ||||
| -rw-r--r-- | src/services/github/fetch-github-repo-meta.ts | 45 | ||||
| -rw-r--r-- | src/services/github/index.ts | 1 | ||||
| -rw-r--r-- | src/types/data.ts | 8 | ||||
| -rw-r--r-- | src/utils/constants.ts | 4 | ||||
| -rw-r--r-- | src/utils/helpers/graphql.ts | 3 | ||||
| -rw-r--r-- | src/utils/hooks/index.ts | 2 | ||||
| -rw-r--r-- | src/utils/hooks/use-github-api.tsx | 28 | ||||
| -rw-r--r-- | src/utils/hooks/use-github-repo-meta/index.ts | 1 | ||||
| -rw-r--r-- | src/utils/hooks/use-github-repo-meta/use-github-repo-meta.test.ts | 59 | ||||
| -rw-r--r-- | src/utils/hooks/use-github-repo-meta/use-github-repo-meta.ts | 37 |
12 files changed, 222 insertions, 48 deletions
diff --git a/src/pages/projets/[slug].tsx b/src/pages/projets/[slug].tsx index b4bc906..cac6037 100644 --- a/src/pages/projets/[slug].tsx +++ b/src/pages/projets/[slug].tsx @@ -26,7 +26,7 @@ import { mdxComponents } from '../../components/mdx'; import styles from '../../styles/pages/project.module.scss'; import type { NextPageWithLayout, Project, Repos } from '../../types'; import { CONFIG } from '../../utils/config'; -import { ROUTES } from '../../utils/constants'; +import { GITHUB_PSEUDO, ROUTES } from '../../utils/constants'; import { getSchemaJson, getSinglePageSchema, @@ -40,7 +40,7 @@ import { } from '../../utils/helpers/server'; import { useBreadcrumb, - useGithubApi, + useGithubRepoMeta, useHeadingsTree, } from '../../utils/hooks'; @@ -115,31 +115,30 @@ const ProjectPage: NextPageWithLayout<ProjectPageProps> = ({ project }) => { id: 'RwI3B9', }); - const { isError, isLoading, data } = useGithubApi( - /* - * Repo should be defined for each project so for now it is safe for my - * use-case. However, I should refactored it to handle cases where it is - * not defined. The logic should be extracted in another component I think. - * - * TODO: fix this hardly readable argument - */ - meta.repos ? meta.repos.github ?? '' : '' - ); + const { + isError, + isLoading, + meta: githubMeta, + } = useGithubRepoMeta({ + name: repos.github?.substring(repos.github.lastIndexOf('/') + 1) ?? '', + owner: GITHUB_PSEUDO, + }); if (isError) return 'Error'; - if (isLoading || !data) return <Spinner aria-label={loadingRepoPopularity} />; + if (isLoading || !githubMeta) + return <Spinner aria-label={loadingRepoPopularity} />; const overviewMeta: Partial<ProjectMeta> = { - creationDate: data.created_at, - lastUpdateDate: data.updated_at, + creationDate: githubMeta.createdAt, + lastUpdateDate: githubMeta.updatedAt, license, - popularity: repos?.github + popularity: repos.github ? { - count: data.stargazers_count, + count: githubMeta.stargazerCount, url: `https://github.com/${repos.github}/stargazers`, } : undefined, - repositories: repos ? getRepos(repos) : undefined, + repositories: getRepos(repos), technologies, }; diff --git a/src/services/github/fetch-github-repo-meta.test.ts b/src/services/github/fetch-github-repo-meta.test.ts new file mode 100644 index 0000000..324db9a --- /dev/null +++ b/src/services/github/fetch-github-repo-meta.test.ts @@ -0,0 +1,47 @@ +import { afterEach, describe, expect, it } from '@jest/globals'; +import { githubRepos } from '../../../tests/fixtures'; +import { fetchGithubRepoMeta } from './fetch-github-repo-meta'; + +describe('fetch-github-repo-meta', () => { + afterEach(() => { + window.history.replaceState({}, '', '/'); + }); + + it('returns the Github repository meta using GraphQL', async () => { + const result = await fetchGithubRepoMeta({ + name: githubRepos[0].name, + owner: githubRepos[0].owner, + }); + + // eslint-disable-next-line @typescript-eslint/no-magic-numbers + expect.assertions(3); + + expect(result.createdAt).toBe(githubRepos[0].createdAt); + expect(result.stargazerCount).toBe(githubRepos[0].stargazerCount); + expect(result.updatedAt).toBe(githubRepos[0].updatedAt); + }); + + it('rejects with an error when repository is not found', async () => { + const name = 'inexistent-repo'; + const owner = 'inexistent-owner'; + + window.history.replaceState({}, '', '/?error=true'); + expect.assertions(1); + + await expect(async () => + fetchGithubRepoMeta({ name, owner }) + ).rejects.toEqual( + new Error(`No data found for the following repository ${owner}/${name}.`) + ); + }); + + it('throws an error if the Github token is not defined', async () => { + process.env.NEXT_PUBLIC_GITHUB_TOKEN = ''; + + expect.assertions(1); + + await expect(async () => + fetchGithubRepoMeta({ name: 'any-name', owner: 'any-owner' }) + ).rejects.toThrowError(new Error('Github token is not defined.')); + }); +}); diff --git a/src/services/github/fetch-github-repo-meta.ts b/src/services/github/fetch-github-repo-meta.ts new file mode 100644 index 0000000..14c8516 --- /dev/null +++ b/src/services/github/fetch-github-repo-meta.ts @@ -0,0 +1,45 @@ +import type { GithubRepositoryMeta, Nullable } from '../../types'; +import { GITHUB_API } from '../../utils/constants'; +import { fetchGraphQL } from '../../utils/helpers'; + +export type GithubRepositoryResponse = { + repository: Nullable<GithubRepositoryMeta>; +}; + +const githubRepoQuery = `query GithubRepository($name: String!, $owner: String!) { + repository(name: $name, owner: $owner) { + createdAt + stargazerCount + updatedAt + } +}`; + +export type FetchGithubRepoMetaInput = { + name: string; + owner: string; +}; + +export const fetchGithubRepoMeta = async ({ + name, + owner, +}: FetchGithubRepoMetaInput) => { + const token = process.env.NEXT_PUBLIC_GITHUB_TOKEN; + + if (!token) throw new Error('Github token is not defined.'); + + const response = await fetchGraphQL<GithubRepositoryResponse>({ + headers: { + Authorization: `Bearer ${token}`, + }, + query: githubRepoQuery, + url: GITHUB_API, + variables: { name, owner }, + }); + + if (!response.repository) + return Promise.reject( + new Error(`No data found for the following repository ${owner}/${name}.`) + ); + + return response.repository; +}; diff --git a/src/services/github/index.ts b/src/services/github/index.ts new file mode 100644 index 0000000..44b0cbf --- /dev/null +++ b/src/services/github/index.ts @@ -0,0 +1 @@ +export * from './fetch-github-repo-meta'; diff --git a/src/types/data.ts b/src/types/data.ts index 1d0746d..f58d6e7 100644 --- a/src/types/data.ts +++ b/src/types/data.ts @@ -254,7 +254,7 @@ export type Repos = { export type ProjectMeta = Omit<PageMeta, 'wordsCount'> & { contexts?: string[]; license?: string; - repos?: Repos; + repos: Repos; tagline?: string; technologies?: string[]; }; @@ -288,3 +288,9 @@ export type Topic = Page & { id: number; meta: TopicMeta; }; + +export type GithubRepositoryMeta = { + createdAt: string; + stargazerCount: number; + updatedAt: string; +}; diff --git a/src/utils/constants.ts b/src/utils/constants.ts index 043a530..f9d6216 100644 --- a/src/utils/constants.ts +++ b/src/utils/constants.ts @@ -1,3 +1,7 @@ +export const GITHUB_API = 'https://api.github.com/graphql'; + +export const GITHUB_PSEUDO = 'ArmandPhilippot'; + export const PERSONAL_LINKS = { GITHUB: 'https://github.com/ArmandPhilippot', GITLAB: 'https://gitlab.com/ArmandPhilippot', diff --git a/src/utils/helpers/graphql.ts b/src/utils/helpers/graphql.ts index e07b151..2d78e00 100644 --- a/src/utils/helpers/graphql.ts +++ b/src/utils/helpers/graphql.ts @@ -20,6 +20,7 @@ type GraphQLResponse<T extends GraphQLData<unknown>> = { }; export type FetchGraphQLConfig = { + headers?: HeadersInit; query: string; url: string; variables?: Record<string, unknown>; @@ -35,6 +36,7 @@ export type FetchGraphQLConfig = { export const fetchGraphQL = async < T extends GraphQLData<unknown> = GraphQLData<unknown>, >({ + headers, query, url, variables, @@ -42,6 +44,7 @@ export const fetchGraphQL = async < const response = await fetch(url, { method: 'POST', headers: { + ...headers, 'content-type': 'application/json;charset=UTF-8', }, body: JSON.stringify({ diff --git a/src/utils/hooks/index.ts b/src/utils/hooks/index.ts index 1ee513d..f4d1583 100644 --- a/src/utils/hooks/index.ts +++ b/src/utils/hooks/index.ts @@ -5,7 +5,7 @@ export * from './use-boolean'; export * from './use-breadcrumb'; export * from './use-comments'; export * from './use-form'; -export * from './use-github-api'; +export * from './use-github-repo-meta'; export * from './use-headings-tree'; export * from './use-local-storage'; export * from './use-match-media'; diff --git a/src/utils/hooks/use-github-api.tsx b/src/utils/hooks/use-github-api.tsx deleted file mode 100644 index aa9e3f7..0000000 --- a/src/utils/hooks/use-github-api.tsx +++ /dev/null @@ -1,28 +0,0 @@ -import useSWR, { Fetcher } from 'swr'; -import { SWRResult } from '../../types'; - -export type RepoData = { - created_at: string; - updated_at: string; - stargazers_count: number; -}; - -const fetcher: Fetcher<RepoData, string> = (...args) => - fetch(...args).then((res) => res.json()); - -/** - * Retrieve data from Github API. - * - * @param repo - The Github repo (`owner/repo-name`). - * @returns The repository data. - */ -export const useGithubApi = (repo: string): SWRResult<RepoData> => { - const apiUrl = repo ? `https://api.github.com/repos/${repo}` : null; - const { data, error } = useSWR<RepoData>(apiUrl, fetcher); - - return { - data, - isLoading: !error && !data, - isError: error, - }; -}; diff --git a/src/utils/hooks/use-github-repo-meta/index.ts b/src/utils/hooks/use-github-repo-meta/index.ts new file mode 100644 index 0000000..352adc9 --- /dev/null +++ b/src/utils/hooks/use-github-repo-meta/index.ts @@ -0,0 +1 @@ +export * from './use-github-repo-meta'; diff --git a/src/utils/hooks/use-github-repo-meta/use-github-repo-meta.test.ts b/src/utils/hooks/use-github-repo-meta/use-github-repo-meta.test.ts new file mode 100644 index 0000000..d796f69 --- /dev/null +++ b/src/utils/hooks/use-github-repo-meta/use-github-repo-meta.test.ts @@ -0,0 +1,59 @@ +import { + afterEach, + beforeEach, + describe, + expect, + it, + jest, +} from '@jest/globals'; +import { renderHook, waitFor } from '@testing-library/react'; +import { githubRepos } from '../../../../tests/fixtures'; +import { useGithubRepoMeta } from './use-github-repo-meta'; + +describe('useGithubRepoMeta', () => { + beforeEach(() => { + /* Not sure why it is needed, but without it Jest was complaining with + * `Jest worker encountered 4 child process exceptions`... Maybe because of + * useSWR? */ + jest.useFakeTimers({ + doNotFake: ['queueMicrotask'], + }); + }); + + afterEach(() => { + jest.runOnlyPendingTimers(); + jest.useRealTimers(); + }); + + /* eslint-disable max-statements */ + it('fetches the requested repository', async () => { + const { result } = renderHook(() => + useGithubRepoMeta({ + name: githubRepos[0].name, + owner: githubRepos[0].owner, + }) + ); + + // Inaccurate assertions count because of waitFor... + //expect.assertions(11); + expect.hasAssertions(); + + expect(result.current.meta).toBeUndefined(); + expect(result.current.isError).toBe(false); + expect(result.current.isLoading).toBe(true); + expect(result.current.isValidating).toBe(true); + + jest.advanceTimersToNextTimer(); + + await waitFor(() => expect(result.current.meta).toBeDefined()); + expect(result.current.meta?.createdAt).toBe(githubRepos[0].createdAt); + expect(result.current.meta?.stargazerCount).toBe( + githubRepos[0].stargazerCount + ); + expect(result.current.meta?.updatedAt).toBe(githubRepos[0].updatedAt); + expect(result.current.isError).toBe(false); + expect(result.current.isLoading).toBe(false); + expect(result.current.isValidating).toBe(false); + }); + /* eslint-enable max-statements */ +}); diff --git a/src/utils/hooks/use-github-repo-meta/use-github-repo-meta.ts b/src/utils/hooks/use-github-repo-meta/use-github-repo-meta.ts new file mode 100644 index 0000000..888682e --- /dev/null +++ b/src/utils/hooks/use-github-repo-meta/use-github-repo-meta.ts @@ -0,0 +1,37 @@ +import useSWR from 'swr'; +import { + type FetchGithubRepoMetaInput, + fetchGithubRepoMeta, +} from '../../../services/github'; +import type { GithubRepositoryMeta, Maybe } from '../../../types'; + +export type UseGithubRepoMetaReturn<T extends Maybe<GithubRepositoryMeta>> = { + isError: boolean; + isLoading: boolean; + isValidating: boolean; + meta: T extends undefined + ? Maybe<GithubRepositoryMeta> + : GithubRepositoryMeta; +}; + +export const useGithubRepoMeta = <T extends Maybe<GithubRepositoryMeta>>( + input: FetchGithubRepoMetaInput, + fallback?: T +) => { + const { data, error, isLoading, isValidating } = useSWR( + input, + fetchGithubRepoMeta, + { + fallbackData: fallback, + } + ); + + if (error) console.error(error); + + return { + isError: !!error, + isLoading, + isValidating, + meta: data, + } as UseGithubRepoMetaReturn<T>; +}; |
