Skip to content

Commit d5ab948

Browse files
authored
Feat/graphql: Integrate Apollo Client with Server Components (#17)
* Feat: Add Apollo Client integration with CustomApolloProvider - Install cookie handling library * Feat: Replace session storage with cookie management for authentication * Chore: Change fake api endpoint * Feat: Add graphql client page * Feat: Add support for graphql file upload * Chore: Change page uri * Feat: Create apollo server client * Docs: Add graphql related explanation * Feat: Add landpad detail page to show how to use apollo client in server * Feat: Add landpad detail test file * Fix: Handle fetch failed error occurred when during github actions - ref: vercel/next.js#44062 (comment)
1 parent 98ac00a commit d5ab948

24 files changed

Lines changed: 644 additions & 14 deletions

README.md

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
# NextJS-App-Router-All-In-One
1+
# NextJS15-GraphQL-Boilerplate
22

3-
`Next15 + App router` boilerplate
3+
`Next15 + GraphQL` boilerplate
44

55
## How to use
66

@@ -65,6 +65,16 @@ $ yarn g
6565

6666
[You can check detail here](./.github/workflows/pull-request-build-check.yml)
6767

68+
### GraphQL
69+
70+
#### Apollo client with [experimental-nextjs-app-support](https://www.npmjs.com/package/@apollo/experimental-nextjs-app-support)
71+
72+
You can see client apollo provider [here](./src/providers/CustomApolloProvider.tsx)
73+
74+
YOu can see server apollo client [here](./src/graphql/apolloServerClient.ts)
75+
76+
#### Graphql upload with [apollo-upload-client](apollo-upload-client)
77+
6878
### Folder Structure
6979

7080
```bash
@@ -82,6 +92,7 @@ $ yarn g
8292
│ ├── assets # Static assets like images, fonts, etc.
8393
│ ├── components # Reusable React components
8494
│ ├── constant # Constants used throughout the application
95+
│ ├── graphql # Graphql related files like apollo client
8596
│ ├── hooks # Custom React hooks
8697
│ ├── libs # Library files and utilities
8798
│ ├── provider # Context providers for global state management
@@ -112,4 +123,4 @@ $ yarn g
112123
- [x] Jest(with server component)
113124
- [x] Storybook(with server component)
114125
- [x] Generator
115-
- [ ] GraphQL
126+
- [x] GraphQL

package.json

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,11 @@
2222
"e2e:headless": "start-server-and-test dev http://localhost:3000 \"cypress run --e2e\""
2323
},
2424
"dependencies": {
25+
"@apollo/client": "^3.12.4",
26+
"@apollo/experimental-nextjs-app-support": "^0.11.7",
27+
"apollo-upload-client": "^18.0.1",
28+
"cookies-next": "^5.0.2",
29+
"graphql": "^16.10.0",
2530
"next": "15.1.0",
2631
"react": "^19",
2732
"react-dom": "^19",
@@ -43,6 +48,7 @@
4348
"@testing-library/jest-dom": "^6.6.3",
4449
"@testing-library/react": "^16.1.0",
4550
"@trivago/prettier-plugin-sort-imports": "^5.2.0",
51+
"@types/apollo-upload-client": "^18.0.0",
4652
"@types/jest": "^29.5.14",
4753
"@types/node": "^22",
4854
"@types/react": "^19",

src/app/cars/page.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ import 'server-only';
22

33
import Cars from '~/components/Cars';
44

5+
export const dynamic = 'force-dynamic';
6+
57
export default async function CarsPage(...props: any) {
68
return <Cars latency={4000} />;
79
}

src/app/landpads/[id]/loading.tsx

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
'use client';
2+
3+
const LandpadLoading = () => {
4+
return <div>Loading</div>;
5+
};
6+
7+
export default LandpadLoading;

src/app/landpads/[id]/page-ui.tsx

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
'use client';
2+
3+
import Link from 'next/link';
4+
5+
import { Landpad } from '../page-ui';
6+
import { LandpadIdPageStyled } from './styled';
7+
8+
interface LandpadIdUIPageProps {
9+
data: Landpad;
10+
}
11+
12+
const LandpadsIdUIPage = ({ data }: LandpadIdUIPageProps) => {
13+
return (
14+
<LandpadIdPageStyled>
15+
<h1>{data.full_name}</h1>
16+
<p>{data.details}</p>
17+
<p className="status">{data.status}</p>
18+
<Link href={data.wikipedia} target="_blank">
19+
{data.wikipedia}
20+
</Link>
21+
<Link href={'/landpads'}>
22+
<p>Go to landpads page</p>
23+
</Link>
24+
</LandpadIdPageStyled>
25+
);
26+
};
27+
28+
export default LandpadsIdUIPage;
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import { render } from '@testing-library/react';
2+
3+
import { apolloServerClient } from '~/graphql/apolloServerClient';
4+
import { GET_ONE_LANDPAD } from '~/graphql/query/landpad';
5+
6+
import LandpadsIdpage from './page';
7+
8+
jest.mock('../../../graphql/apolloServerClient', () => ({
9+
apolloServerClient: jest.fn(),
10+
}));
11+
12+
describe('LandpadDetailPage', () => {
13+
it('should call useQuery with correct variables', async () => {
14+
const response = {
15+
attempted_landings: null,
16+
details: 'details',
17+
full_name: 'full_name',
18+
id: 'id',
19+
landing_type: null,
20+
location: { latitude: 0, longitude: 0, name: 'name', region: 'region' },
21+
status: 'status',
22+
successful_landings: null,
23+
wikipedia: 'wikipedia',
24+
};
25+
26+
const mockClient = {
27+
query: jest.fn().mockResolvedValue({ data: { landpad: response } }),
28+
};
29+
(apolloServerClient as jest.Mock).mockResolvedValue(mockClient);
30+
31+
const params = { id: '1' };
32+
33+
render(await LandpadsIdpage({ params: Promise.resolve(params) }));
34+
35+
expect(mockClient.query).toHaveBeenCalledWith({
36+
query: GET_ONE_LANDPAD,
37+
variables: params,
38+
});
39+
});
40+
});

src/app/landpads/[id]/page.tsx

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import 'server-only';
2+
3+
import { apolloServerClient } from '~/graphql/apolloServerClient';
4+
import { GET_ONE_LANDPAD } from '~/graphql/query/landpad';
5+
6+
import { Landpad } from '../page-ui';
7+
import LandpadsIdUIPage from './page-ui';
8+
9+
const LandpadsIdpage = async ({ params }: { params: Promise<{ id: string }> }) => {
10+
const { id } = await params;
11+
12+
const client = await apolloServerClient();
13+
const { data } = await client.query<{ landpad: Landpad }, { id: string }>({
14+
query: GET_ONE_LANDPAD,
15+
variables: { id },
16+
});
17+
18+
return <LandpadsIdUIPage data={data.landpad} />;
19+
};
20+
21+
export default LandpadsIdpage;

src/app/landpads/[id]/styled.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
import styled from 'styled-components';
2+
3+
export const LandpadIdPageStyled = styled.div``;

src/app/landpads/page-ui.test.tsx

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import { render, screen } from '@testing-library/react';
2+
3+
import LandpadsUIPage from './page-ui';
4+
5+
describe('LandpadsUIPage', () => {
6+
it('should display loading indicator when loading is true', () => {
7+
render(<LandpadsUIPage data={undefined} loading={true} />);
8+
9+
expect(screen.getByText('loading')).toBeInTheDocument();
10+
});
11+
12+
it('should render landpad data when loading is false', () => {
13+
const data = [
14+
{
15+
attempted_landings: null,
16+
details: 'Landing site details',
17+
full_name: 'Landing Pad 1',
18+
id: '1',
19+
landing_type: null,
20+
location: { latitude: 0, longitude: 0, name: 'Location 1', region: 'Region 1' },
21+
status: 'Active',
22+
successful_landings: null,
23+
wikipedia: 'https://wikipedia.org',
24+
},
25+
];
26+
27+
render(<LandpadsUIPage data={data} loading={false} />);
28+
29+
expect(screen.getByText('Landing Pad 1')).toBeInTheDocument();
30+
expect(screen.getByText('Landing site details')).toBeInTheDocument();
31+
expect(screen.getByText('Active')).toBeInTheDocument();
32+
expect(screen.getByText('https://wikipedia.org')).toBeInTheDocument();
33+
});
34+
});

src/app/landpads/page-ui.tsx

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
'use client';
2+
3+
import Link from 'next/link';
4+
5+
import { LandpadsLoadingStyled, LandpadsPageStyled } from './styled';
6+
7+
export interface Landpad {
8+
attempted_landings: string | null;
9+
details: string;
10+
full_name: string;
11+
id: string;
12+
landing_type: string | null;
13+
location: { latitude: number; longitude: number; name: string; region: string } | null;
14+
status: string;
15+
successful_landings: string | null;
16+
wikipedia: string;
17+
}
18+
19+
interface LandpadsUIPageProps {
20+
data: Landpad[] | undefined;
21+
loading: boolean;
22+
}
23+
24+
const LandpadsUIPage = ({ data, loading }: LandpadsUIPageProps) => {
25+
if (loading) return <LandpadsLoadingStyled>loading</LandpadsLoadingStyled>;
26+
27+
return (
28+
<LandpadsPageStyled>
29+
{data?.map(v => (
30+
<Link key={v.id} href={`/landpads/${v.id}`}>
31+
<h1>{v.full_name}</h1>
32+
<p>{v.details}</p>
33+
<p className="status">{v.status}</p>
34+
<p
35+
onClick={e => {
36+
e.preventDefault();
37+
window.open(v.wikipedia);
38+
}}
39+
>
40+
{v.wikipedia}
41+
</p>
42+
</Link>
43+
))}
44+
</LandpadsPageStyled>
45+
);
46+
};
47+
48+
export default LandpadsUIPage;

0 commit comments

Comments
 (0)