Skip to content

Commit

Permalink
Use graphql as data provider
Browse files Browse the repository at this point in the history
  • Loading branch information
abel-castro committed Dec 20, 2024
1 parent beb1293 commit 8173cad
Show file tree
Hide file tree
Showing 18 changed files with 788 additions and 54 deletions.
3 changes: 2 additions & 1 deletion .env.template
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
BLOG_API_URL=http://localhost:8000/api/posts
ROOT_URL=http://localhost:3000
ROOT_URL=http://localhost:3000
BLOG_GRAPHQL_URL=http://localhost:8000/graphql
8 changes: 8 additions & 0 deletions apollo-client.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { ApolloClient, InMemoryCache } from '@apollo/client';

const apolloClient = new ApolloClient({
uri: process.env.BLOG_GRAPHQL_URL,
cache: new InMemoryCache(),
});

export default apolloClient;
21 changes: 13 additions & 8 deletions app/components/posts/post-search.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,20 +4,25 @@ import { MagnifyingGlassIcon } from '@heroicons/react/24/outline';
import { usePathname, useRouter, useSearchParams } from 'next/navigation';
import { useDebouncedCallback } from 'use-debounce';

export function getSearchParams(searchParams: URLSearchParams, term: string) {
const params = new URLSearchParams(searchParams);
params.set('page', '1');

if (term) {
params.set('query', term);
} else {
params.delete('query');
}
return params;
}

export default function Search({ placeholder }: { placeholder: string }) {
const searchParams = useSearchParams();
const pathname = usePathname();
const { replace } = useRouter();

const handleSearch = useDebouncedCallback((term) => {
const params = new URLSearchParams(searchParams);
params.set('page', '1');

if (term) {
params.set('query', term);
} else {
params.delete('query');
}
const params = getSearchParams(searchParams, term);

Check warning on line 25 in app/components/posts/post-search.tsx

View check run for this annotation

Codecov / codecov/patch

app/components/posts/post-search.tsx#L25

Added line #L25 was not covered by tests
replace(`/?${params.toString()}`);
}, 300);

Expand Down
2 changes: 1 addition & 1 deletion app/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ export default async function Home({ searchParams }: HomeProps) {
currentPage: currentPage,
query: query,
};
const { posts, totalPages } = await activeDataProvider.getAll(options);
const { posts, totalPages } = await activeDataProvider.getPosts(options);

return (
<>
Expand Down
6 changes: 4 additions & 2 deletions data-providers/active.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,10 @@
// const jsonData = JSON.parse(readFileSync("tests/sample-data.json", "utf-8"));
// export const activeDataProvider = new MemoryDataProvider(jsonData);
// Rest-API Data Provider
import { RestAPIDataProvider } from './rest-api';
//import { RestAPIDataProvider } from './rest-api';
import apolloClient from '../apollo-client';
import { GraphqlDataProvider } from './graphql';

const activeDataProvider = new RestAPIDataProvider();
const activeDataProvider = new GraphqlDataProvider(apolloClient);

export default activeDataProvider;
10 changes: 4 additions & 6 deletions data-providers/base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,22 +2,20 @@ import { Post } from '../app/lib/definitions';
import { IDataProvider, PaginatedPosts, PostSearchOptions } from './interface';

export abstract class BaseDataProvider implements IDataProvider {
abstract getPostsFromStorage(
options: PostSearchOptions,
): Promise<PaginatedPosts>;
abstract getSinglePostFromStorage(slug: string): Promise<Post | null>;
abstract getPosts(options: PostSearchOptions): Promise<PaginatedPosts>;
abstract getPost(slug: string): Promise<Post | null>;

async getAll(options: PostSearchOptions): Promise<PaginatedPosts> {
return new Promise(async (resolve, reject) => {
const paginatedPosts = await this.getPostsFromStorage(options);
const paginatedPosts = await this.getPosts(options);

resolve(paginatedPosts);
});
}

async getBySlug(slug: string): Promise<Post | null> {
return new Promise(async (resolve, reject) => {
const matchingPost = await this.getSinglePostFromStorage(slug);
const matchingPost = await this.getPost(slug);
if (matchingPost) {
resolve(matchingPost);
} else {
Expand Down
109 changes: 109 additions & 0 deletions data-providers/graphql.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
import { ApolloClient, NormalizedCacheObject, gql } from '@apollo/client';

import { POST_PAGE_SIZE } from '../app/constants';
import { Post, PostsAPIResponse } from '../app/lib/definitions';
import { BaseDataProvider } from './base';
import { PaginatedPosts, PostSearchOptions } from './interface';
import { calculateTotalPages } from './utils';

const POST_FRAGMENT = gql`
fragment PostFragment on PostType {
title
date
content
slug
tags {
name
}
}
`;

export const GET_POST = gql`
query GetPosts($slug: String!) {
post(slug: $slug) {
...PostFragment
}
}
${POST_FRAGMENT}
`;

export const GET_POSTS = gql`
query GetPost($search: String, $limit: Int, $offset: Int) {
posts(search: $search, limit: $limit, offset: $offset) {
...PostFragment
}
totalPosts(search: $search)
}
${POST_FRAGMENT}
`;

export const GET_POST_METADATA = gql`
query GetPostMetadata($slug: String!) {
post(slug: $slug) {
title
metaDescription
}
}
`;

type PostsOffsetBased = {
posts: Post[];
totalPosts: number;
};

export class GraphqlDataProvider extends BaseDataProvider {
private client: ApolloClient<NormalizedCacheObject>;

constructor(client: ApolloClient<NormalizedCacheObject>) {
super();
this.client = client;
}

async getPosts(options: PostSearchOptions): Promise<PaginatedPosts> {
const currentPage = options.currentPage || 1;
const limit = options.pageSize || POST_PAGE_SIZE;

const offset = (currentPage - 1) * limit;

const variables = options.query
? { search: options.query, limit, offset }
: { limit, offset };

const { data } = await this.client.query<PostsOffsetBased>({
query: GET_POSTS,
variables,
});
return {
posts: data.posts,
totalPages: calculateTotalPages(data.totalPosts, POST_PAGE_SIZE),
};
}

async getPost(slug: string): Promise<Post | null> {
try {
const { data } = await this.client.query<{ post: Post }>({
query: GET_POST,
variables: { slug },
});

return data.post || null;
} catch (error) {
console.error('Error fetching post:', error);
return null;
}
}

async getPostMetadata(slug: string): Promise<Post | null> {
try {
const { data } = await this.client.query<{ post: Post }>({
query: GET_POST_METADATA,
variables: { slug },
});

return data.post || null;
} catch (error) {
console.error('Error fetching post metadata:', error);
return null;
}
}
}
6 changes: 2 additions & 4 deletions data-providers/memory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,7 @@ export class MemoryDataProvider extends BaseDataProvider {
this.postsFromJson = postsFromJson as Post[];
}

async getPostsFromStorage(
options: PostSearchOptions,
): Promise<PaginatedPosts> {
async getPosts(options: PostSearchOptions): Promise<PaginatedPosts> {
const posts = this.postsFromJson.map((post: Post) => {
return {
title: post.title,
Expand All @@ -33,7 +31,7 @@ export class MemoryDataProvider extends BaseDataProvider {
};
}

async getSinglePostFromStorage(slug: string): Promise<Post | null> {
async getPost(slug: string): Promise<Post | null> {
const post = this.postsFromJson.find(
(post: Post) => post.slug === slug,
);
Expand Down
8 changes: 3 additions & 5 deletions data-providers/rest-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,7 @@ import { PaginatedPosts, PostSearchOptions } from './interface';
import { calculateTotalPages } from './utils';

export class RestAPIDataProvider extends BaseDataProvider {
async getPostsFromStorage(
options: PostSearchOptions,
): Promise<PaginatedPosts> {
async getPosts(options: PostSearchOptions): Promise<PaginatedPosts> {
const apiUrl = process.env.BLOG_API_URL;
if (!apiUrl) {
throw new Error('BLOG_API_URL is not set');
Expand Down Expand Up @@ -49,7 +47,7 @@ export class RestAPIDataProvider extends BaseDataProvider {
};
}

async getSinglePostFromStorage(slug: string): Promise<Post | null> {
async getPost(slug: string): Promise<Post | null> {
const response = await fetch(`${process.env.BLOG_API_URL}/${slug}`);
if (!response.ok) {
return null;
Expand All @@ -59,4 +57,4 @@ export class RestAPIDataProvider extends BaseDataProvider {

return jsonResponse;
}
}
}
4 changes: 4 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
"test:e2e": "exec playwright test"
},
"dependencies": {
"@apollo/client": "^3.12.3",
"@heroicons/react": "^2.1.5",
"@mapbox/rehype-prism": "^0.9.0",
"@mdx-js/loader": "^3.0.1",
Expand All @@ -24,6 +25,7 @@
"@vercel/speed-insights": "^1.0.12",
"clsx": "^2.1.1",
"dotenv": "^16.4.5",
"graphql": "^16.9.0",
"highlight.js": "^11.10.0",
"install": "^0.13.0",
"next": "^15.0.0-rc.0",
Expand All @@ -38,7 +40,9 @@
},
"devDependencies": {
"@playwright/test": "^1.45.3",
"@testing-library/jest-dom": "^6.6.3",
"@testing-library/react": "^16.0.0",
"@testing-library/user-event": "^14.5.2",
"@trivago/prettier-plugin-sort-imports": "^4.3.0",
"@types/node": "^20.14.13",
"@types/react": "^18.3.3",
Expand Down
Loading

0 comments on commit 8173cad

Please sign in to comment.