Showing
35 changed files
with
1003 additions
and
0 deletions
project/.gitignore
0 → 100644
1 | +node_modules |
project/lerna.json
0 → 100644
project/package.json
0 → 100644
1 | +{ | ||
2 | + "name": "graphql-community", | ||
3 | + "private": true, | ||
4 | + "devDependencies": { | ||
5 | + "lerna": "^4.0.0" | ||
6 | + }, | ||
7 | + "scripts": { | ||
8 | + "build": "lerna run build", | ||
9 | + "build:api" : "lerna exec --scrop @graphql-community/api --stream yarn build", | ||
10 | + "build:web": "lerna exec --scrop @graphql-community/api --stream yarn build", | ||
11 | + "dev:web": "lerna exec --scrop @graphql-community/web --stream yarn dev", | ||
12 | + "dev:api": "lerna exec --scrop @graphql-community/api --stream yarn start:dev" | ||
13 | + }, | ||
14 | + "workspaces": [ | ||
15 | + "packages/**" | ||
16 | + ] | ||
17 | +} |
project/packages/api/.eslintrc.js
0 → 100644
1 | +module.exports = { | ||
2 | + parser: '@typescript-eslint/parser', | ||
3 | + parserOptions: { | ||
4 | + project: 'tsconfig.json', | ||
5 | + sourceType: 'module', | ||
6 | + }, | ||
7 | + plugins: ['@typescript-eslint/eslint-plugin'], | ||
8 | + extends: [ | ||
9 | + 'plugin:@typescript-eslint/recommended', | ||
10 | + 'plugin:prettier/recommended', | ||
11 | + ], | ||
12 | + root: true, | ||
13 | + env: { | ||
14 | + node: true, | ||
15 | + jest: true, | ||
16 | + }, | ||
17 | + ignorePatterns: ['.eslintrc.js'], | ||
18 | + rules: { | ||
19 | + '@typescript-eslint/interface-name-prefix': 'off', | ||
20 | + '@typescript-eslint/explicit-function-return-type': 'off', | ||
21 | + '@typescript-eslint/explicit-module-boundary-types': 'off', | ||
22 | + '@typescript-eslint/no-explicit-any': 'off', | ||
23 | + }, | ||
24 | +}; |
project/packages/api/.gitignore
0 → 100644
1 | +# compiled output | ||
2 | +/dist | ||
3 | +/node_modules | ||
4 | + | ||
5 | +# Logs | ||
6 | +logs | ||
7 | +*.log | ||
8 | +npm-debug.log* | ||
9 | +yarn-debug.log* | ||
10 | +yarn-error.log* | ||
11 | +lerna-debug.log* | ||
12 | + | ||
13 | +# OS | ||
14 | +.DS_Store | ||
15 | + | ||
16 | +# Tests | ||
17 | +/coverage | ||
18 | +/.nyc_output | ||
19 | + | ||
20 | +# IDEs and editors | ||
21 | +/.idea | ||
22 | +.project | ||
23 | +.classpath | ||
24 | +.c9/ | ||
25 | +*.launch | ||
26 | +.settings/ | ||
27 | +*.sublime-workspace | ||
28 | + | ||
29 | +# IDE - VSCode | ||
30 | +.vscode/* | ||
31 | +!.vscode/settings.json | ||
32 | +!.vscode/tasks.json | ||
33 | +!.vscode/launch.json | ||
34 | +!.vscode/extensions.json | ||
... | \ No newline at end of file | ... | \ No newline at end of file |
project/packages/api/.prettierrc
0 → 100644
project/packages/api/README.md
0 → 100644
1 | +<p align="center"> | ||
2 | + <a href="http://nestjs.com/" target="blank"><img src="https://nestjs.com/img/logo_text.svg" width="320" alt="Nest Logo" /></a> | ||
3 | +</p> | ||
4 | + | ||
5 | +[circleci-image]: https://img.shields.io/circleci/build/github/nestjs/nest/master?token=abc123def456 | ||
6 | +[circleci-url]: https://circleci.com/gh/nestjs/nest | ||
7 | + | ||
8 | + <p align="center">A progressive <a href="http://nodejs.org" target="_blank">Node.js</a> framework for building efficient and scalable server-side applications.</p> | ||
9 | + <p align="center"> | ||
10 | +<a href="https://www.npmjs.com/~nestjscore" target="_blank"><img src="https://img.shields.io/npm/v/@nestjs/core.svg" alt="NPM Version" /></a> | ||
11 | +<a href="https://www.npmjs.com/~nestjscore" target="_blank"><img src="https://img.shields.io/npm/l/@nestjs/core.svg" alt="Package License" /></a> | ||
12 | +<a href="https://www.npmjs.com/~nestjscore" target="_blank"><img src="https://img.shields.io/npm/dm/@nestjs/common.svg" alt="NPM Downloads" /></a> | ||
13 | +<a href="https://circleci.com/gh/nestjs/nest" target="_blank"><img src="https://img.shields.io/circleci/build/github/nestjs/nest/master" alt="CircleCI" /></a> | ||
14 | +<a href="https://coveralls.io/github/nestjs/nest?branch=master" target="_blank"><img src="https://coveralls.io/repos/github/nestjs/nest/badge.svg?branch=master#9" alt="Coverage" /></a> | ||
15 | +<a href="https://discord.gg/G7Qnnhy" target="_blank"><img src="https://img.shields.io/badge/discord-online-brightgreen.svg" alt="Discord"/></a> | ||
16 | +<a href="https://opencollective.com/nest#backer" target="_blank"><img src="https://opencollective.com/nest/backers/badge.svg" alt="Backers on Open Collective" /></a> | ||
17 | +<a href="https://opencollective.com/nest#sponsor" target="_blank"><img src="https://opencollective.com/nest/sponsors/badge.svg" alt="Sponsors on Open Collective" /></a> | ||
18 | + <a href="https://paypal.me/kamilmysliwiec" target="_blank"><img src="https://img.shields.io/badge/Donate-PayPal-ff3f59.svg"/></a> | ||
19 | + <a href="https://opencollective.com/nest#sponsor" target="_blank"><img src="https://img.shields.io/badge/Support%20us-Open%20Collective-41B883.svg" alt="Support us"></a> | ||
20 | + <a href="https://twitter.com/nestframework" target="_blank"><img src="https://img.shields.io/twitter/follow/nestframework.svg?style=social&label=Follow"></a> | ||
21 | +</p> | ||
22 | + <!--[![Backers on Open Collective](https://opencollective.com/nest/backers/badge.svg)](https://opencollective.com/nest#backer) | ||
23 | + [![Sponsors on Open Collective](https://opencollective.com/nest/sponsors/badge.svg)](https://opencollective.com/nest#sponsor)--> | ||
24 | + | ||
25 | +## Description | ||
26 | + | ||
27 | +[Nest](https://github.com/nestjs/nest) framework TypeScript starter repository. | ||
28 | + | ||
29 | +## Installation | ||
30 | + | ||
31 | +```bash | ||
32 | +$ npm install | ||
33 | +``` | ||
34 | + | ||
35 | +## Running the app | ||
36 | + | ||
37 | +```bash | ||
38 | +# development | ||
39 | +$ npm run start | ||
40 | + | ||
41 | +# watch mode | ||
42 | +$ npm run start:dev | ||
43 | + | ||
44 | +# production mode | ||
45 | +$ npm run start:prod | ||
46 | +``` | ||
47 | + | ||
48 | +## Test | ||
49 | + | ||
50 | +```bash | ||
51 | +# unit tests | ||
52 | +$ npm run test | ||
53 | + | ||
54 | +# e2e tests | ||
55 | +$ npm run test:e2e | ||
56 | + | ||
57 | +# test coverage | ||
58 | +$ npm run test:cov | ||
59 | +``` | ||
60 | + | ||
61 | +## Support | ||
62 | + | ||
63 | +Nest is an MIT-licensed open source project. It can grow thanks to the sponsors and support by the amazing backers. If you'd like to join them, please [read more here](https://docs.nestjs.com/support). | ||
64 | + | ||
65 | +## Stay in touch | ||
66 | + | ||
67 | +- Author - [Kamil Myśliwiec](https://kamilmysliwiec.com) | ||
68 | +- Website - [https://nestjs.com](https://nestjs.com/) | ||
69 | +- Twitter - [@nestframework](https://twitter.com/nestframework) | ||
70 | + | ||
71 | +## License | ||
72 | + | ||
73 | +Nest is [MIT licensed](LICENSE). |
project/packages/api/nest-cli.json
0 → 100644
project/packages/api/package.json
0 → 100644
1 | +{ | ||
2 | + "name": "@graphql-community/api", | ||
3 | + "version": "0.0.1", | ||
4 | + "description": "", | ||
5 | + "author": "", | ||
6 | + "private": true, | ||
7 | + "license": "UNLICENSED", | ||
8 | + "scripts": { | ||
9 | + "prebuild": "rimraf dist", | ||
10 | + "build": "nest build", | ||
11 | + "format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"", | ||
12 | + "start": "nest start", | ||
13 | + "start:dev": "nest start --watch", | ||
14 | + "start:debug": "nest start --debug --watch", | ||
15 | + "start:prod": "node dist/main", | ||
16 | + "lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix", | ||
17 | + "test": "jest", | ||
18 | + "test:watch": "jest --watch", | ||
19 | + "test:cov": "jest --coverage", | ||
20 | + "test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand", | ||
21 | + "test:e2e": "jest --config ./test/jest-e2e.json" | ||
22 | + }, | ||
23 | + "dependencies": { | ||
24 | + "@nestjs/common": "^7.6.15", | ||
25 | + "@nestjs/core": "^7.6.15", | ||
26 | + "@nestjs/platform-express": "^7.6.15", | ||
27 | + "reflect-metadata": "^0.1.13", | ||
28 | + "rimraf": "^3.0.2", | ||
29 | + "rxjs": "^6.6.6" | ||
30 | + }, | ||
31 | + "devDependencies": { | ||
32 | + "@nestjs/cli": "^7.6.0", | ||
33 | + "@nestjs/schematics": "^7.3.0", | ||
34 | + "@nestjs/testing": "^7.6.15", | ||
35 | + "@types/express": "^4.17.11", | ||
36 | + "@types/jest": "^26.0.22", | ||
37 | + "@types/node": "^14.14.36", | ||
38 | + "@types/supertest": "^2.0.10", | ||
39 | + "@typescript-eslint/eslint-plugin": "^4.19.0", | ||
40 | + "@typescript-eslint/parser": "^4.19.0", | ||
41 | + "eslint": "^7.22.0", | ||
42 | + "eslint-config-prettier": "^8.1.0", | ||
43 | + "eslint-plugin-prettier": "^3.3.1", | ||
44 | + "jest": "^26.6.3", | ||
45 | + "prettier": "^2.2.1", | ||
46 | + "supertest": "^6.1.3", | ||
47 | + "ts-jest": "^26.5.4", | ||
48 | + "ts-loader": "^8.0.18", | ||
49 | + "ts-node": "^9.1.1", | ||
50 | + "tsconfig-paths": "^3.9.0", | ||
51 | + "typescript": "^4.2.3" | ||
52 | + }, | ||
53 | + "jest": { | ||
54 | + "moduleFileExtensions": [ | ||
55 | + "js", | ||
56 | + "json", | ||
57 | + "ts" | ||
58 | + ], | ||
59 | + "rootDir": "src", | ||
60 | + "testRegex": ".*\\.spec\\.ts$", | ||
61 | + "transform": { | ||
62 | + "^.+\\.(t|j)s$": "ts-jest" | ||
63 | + }, | ||
64 | + "collectCoverageFrom": [ | ||
65 | + "**/*.(t|j)s" | ||
66 | + ], | ||
67 | + "coverageDirectory": "../coverage", | ||
68 | + "testEnvironment": "node" | ||
69 | + } | ||
70 | +} |
1 | +import { Test, TestingModule } from '@nestjs/testing'; | ||
2 | +import { AppController } from './app.controller'; | ||
3 | +import { AppService } from './app.service'; | ||
4 | + | ||
5 | +describe('AppController', () => { | ||
6 | + let appController: AppController; | ||
7 | + | ||
8 | + beforeEach(async () => { | ||
9 | + const app: TestingModule = await Test.createTestingModule({ | ||
10 | + controllers: [AppController], | ||
11 | + providers: [AppService], | ||
12 | + }).compile(); | ||
13 | + | ||
14 | + appController = app.get<AppController>(AppController); | ||
15 | + }); | ||
16 | + | ||
17 | + describe('root', () => { | ||
18 | + it('should return "Hello World!"', () => { | ||
19 | + expect(appController.getHello()).toBe('Hello World!'); | ||
20 | + }); | ||
21 | + }); | ||
22 | +}); |
project/packages/api/src/app.controller.ts
0 → 100644
1 | +import { Controller, Get } from '@nestjs/common'; | ||
2 | +import { AppService } from './app.service'; | ||
3 | + | ||
4 | +@Controller() | ||
5 | +export class AppController { | ||
6 | + constructor(private readonly appService: AppService) {} | ||
7 | + | ||
8 | + @Get() | ||
9 | + getHello(): string { | ||
10 | + return this.appService.getHello(); | ||
11 | + } | ||
12 | +} |
project/packages/api/src/app.module.ts
0 → 100644
project/packages/api/src/app.service.ts
0 → 100644
project/packages/api/src/main.ts
0 → 100644
project/packages/api/test/app.e2e-spec.ts
0 → 100644
1 | +import { Test, TestingModule } from '@nestjs/testing'; | ||
2 | +import { INestApplication } from '@nestjs/common'; | ||
3 | +import * as request from 'supertest'; | ||
4 | +import { AppModule } from './../src/app.module'; | ||
5 | + | ||
6 | +describe('AppController (e2e)', () => { | ||
7 | + let app: INestApplication; | ||
8 | + | ||
9 | + beforeEach(async () => { | ||
10 | + const moduleFixture: TestingModule = await Test.createTestingModule({ | ||
11 | + imports: [AppModule], | ||
12 | + }).compile(); | ||
13 | + | ||
14 | + app = moduleFixture.createNestApplication(); | ||
15 | + await app.init(); | ||
16 | + }); | ||
17 | + | ||
18 | + it('/ (GET)', () => { | ||
19 | + return request(app.getHttpServer()) | ||
20 | + .get('/') | ||
21 | + .expect(200) | ||
22 | + .expect('Hello World!'); | ||
23 | + }); | ||
24 | +}); |
project/packages/api/test/jest-e2e.json
0 → 100644
project/packages/api/tsconfig.build.json
0 → 100644
project/packages/api/tsconfig.json
0 → 100644
1 | +{ | ||
2 | + "compilerOptions": { | ||
3 | + "module": "commonjs", | ||
4 | + "declaration": true, | ||
5 | + "removeComments": true, | ||
6 | + "emitDecoratorMetadata": true, | ||
7 | + "experimentalDecorators": true, | ||
8 | + "allowSyntheticDefaultImports": true, | ||
9 | + "target": "es2017", | ||
10 | + "sourceMap": true, | ||
11 | + "outDir": "./dist", | ||
12 | + "baseUrl": "./", | ||
13 | + "incremental": true | ||
14 | + } | ||
15 | +} |
project/packages/web/.gitignore
0 → 100644
1 | +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. | ||
2 | + | ||
3 | +# dependencies | ||
4 | +/node_modules | ||
5 | +/.pnp | ||
6 | +.pnp.js | ||
7 | + | ||
8 | +# testing | ||
9 | +/coverage | ||
10 | + | ||
11 | +# next.js | ||
12 | +/.next/ | ||
13 | +/out/ | ||
14 | + | ||
15 | +# production | ||
16 | +/build | ||
17 | + | ||
18 | +# misc | ||
19 | +.DS_Store | ||
20 | +*.pem | ||
21 | + | ||
22 | +# debug | ||
23 | +npm-debug.log* | ||
24 | +yarn-debug.log* | ||
25 | +yarn-error.log* | ||
26 | + | ||
27 | +# local env files | ||
28 | +.env.local | ||
29 | +.env.development.local | ||
30 | +.env.test.local | ||
31 | +.env.production.local | ||
32 | + | ||
33 | +# vercel | ||
34 | +.vercel |
project/packages/web/README.md
0 → 100644
1 | +# Apollo Example | ||
2 | + | ||
3 | +[Apollo](https://www.apollographql.com/client/) is a GraphQL client that allows you to easily query the exact data you need from a GraphQL server. In addition to fetching and mutating data, Apollo analyzes your queries and their results to construct a client-side cache of your data, which is kept up to date as further queries and mutations are run. | ||
4 | + | ||
5 | +In this simple example, we integrate Apollo seamlessly with [Next.js data fetching methods](https://nextjs.org/docs/basic-features/data-fetching) to fetch queries in the server and hydrate them in the browser. | ||
6 | + | ||
7 | +This example relies on [Prisma + Nexus](https://github.com/prisma-labs/nextjs-graphql-api-examples) for its GraphQL backend. | ||
8 | + | ||
9 | +## Demo | ||
10 | + | ||
11 | +[https://next-with-apollo.now.sh](https://next-with-apollo.now.sh) | ||
12 | + | ||
13 | +## Deploy your own | ||
14 | + | ||
15 | +Deploy the example using [Vercel](https://vercel.com?utm_source=github&utm_medium=readme&utm_campaign=next-example): | ||
16 | + | ||
17 | +[![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/git/external?repository-url=https://github.com/vercel/next.js/tree/canary/examples/with-apollo&project-name=with-apollo&repository-name=with-apollo) | ||
18 | + | ||
19 | +## How to use | ||
20 | + | ||
21 | +Execute [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app) with [npm](https://docs.npmjs.com/cli/init) or [Yarn](https://yarnpkg.com/lang/en/docs/cli/create/) to bootstrap the example: | ||
22 | + | ||
23 | +```bash | ||
24 | +npx create-next-app --example with-apollo with-apollo-app | ||
25 | +# or | ||
26 | +yarn create next-app --example with-apollo with-apollo-app | ||
27 | +``` | ||
28 | + | ||
29 | +Deploy it to the cloud with [Vercel](https://vercel.com/new?utm_source=github&utm_medium=readme&utm_campaign=next-example) ([Documentation](https://nextjs.org/docs/deployment)). |
project/packages/web/components/App.js
0 → 100644
1 | +export default function App({ children }) { | ||
2 | + return ( | ||
3 | + <main> | ||
4 | + {children} | ||
5 | + <style jsx global>{` | ||
6 | + * { | ||
7 | + font-family: Menlo, Monaco, 'Lucida Console', 'Liberation Mono', | ||
8 | + 'DejaVu Sans Mono', 'Bitstream Vera Sans Mono', 'Courier New', | ||
9 | + monospace, serif; | ||
10 | + } | ||
11 | + body { | ||
12 | + margin: 0; | ||
13 | + padding: 25px 50px; | ||
14 | + } | ||
15 | + a { | ||
16 | + color: #22bad9; | ||
17 | + } | ||
18 | + p { | ||
19 | + font-size: 14px; | ||
20 | + line-height: 24px; | ||
21 | + } | ||
22 | + article { | ||
23 | + margin: 0 auto; | ||
24 | + max-width: 650px; | ||
25 | + } | ||
26 | + button { | ||
27 | + align-items: center; | ||
28 | + background-color: #22bad9; | ||
29 | + border: 0; | ||
30 | + color: white; | ||
31 | + display: flex; | ||
32 | + padding: 5px 7px; | ||
33 | + transition: background-color 0.3s; | ||
34 | + } | ||
35 | + button:active { | ||
36 | + background-color: #1b9db7; | ||
37 | + } | ||
38 | + button:disabled { | ||
39 | + background-color: #b5bebf; | ||
40 | + } | ||
41 | + button:focus { | ||
42 | + outline: none; | ||
43 | + } | ||
44 | + `}</style> | ||
45 | + </main> | ||
46 | + ) | ||
47 | +} |
project/packages/web/components/Header.js
0 → 100644
1 | +import { useRouter } from 'next/router' | ||
2 | +import Link from 'next/link' | ||
3 | + | ||
4 | +export default function Header() { | ||
5 | + const { pathname } = useRouter() | ||
6 | + | ||
7 | + return ( | ||
8 | + <header> | ||
9 | + <Link href="/"> | ||
10 | + <a className={pathname === '/' ? 'is-active' : ''}>Home</a> | ||
11 | + </Link> | ||
12 | + <Link href="/about"> | ||
13 | + <a className={pathname === '/about' ? 'is-active' : ''}>About</a> | ||
14 | + </Link> | ||
15 | + <Link href="/client-only"> | ||
16 | + <a className={pathname === '/client-only' ? 'is-active' : ''}> | ||
17 | + Client-Only | ||
18 | + </a> | ||
19 | + </Link> | ||
20 | + <Link href="/ssr"> | ||
21 | + <a className={pathname === '/ssr' ? 'is-active' : ''}>SSR</a> | ||
22 | + </Link> | ||
23 | + <style jsx>{` | ||
24 | + header { | ||
25 | + margin-bottom: 25px; | ||
26 | + } | ||
27 | + a { | ||
28 | + font-size: 14px; | ||
29 | + margin-right: 15px; | ||
30 | + text-decoration: none; | ||
31 | + } | ||
32 | + .is-active { | ||
33 | + text-decoration: underline; | ||
34 | + } | ||
35 | + `}</style> | ||
36 | + </header> | ||
37 | + ) | ||
38 | +} |
project/packages/web/components/InfoBox.js
0 → 100644
1 | +const InfoBox = ({ children }) => ( | ||
2 | + <div className="info"> | ||
3 | + <style jsx>{` | ||
4 | + .info { | ||
5 | + margin-top: 20px; | ||
6 | + margin-bottom: 20px; | ||
7 | + padding-top: 20px; | ||
8 | + padding-bottom: 20px; | ||
9 | + border-top: 1px solid #ececec; | ||
10 | + border-bottom: 1px solid #ececec; | ||
11 | + } | ||
12 | + `}</style> | ||
13 | + {children} | ||
14 | + </div> | ||
15 | +) | ||
16 | + | ||
17 | +export default InfoBox |
project/packages/web/components/PostList.js
0 → 100644
1 | +import { gql, useQuery, NetworkStatus } from '@apollo/client' | ||
2 | +import ErrorMessage from './ErrorMessage' | ||
3 | +import PostUpvoter from './PostUpvoter' | ||
4 | + | ||
5 | +export const ALL_POSTS_QUERY = gql` | ||
6 | + query allPosts($first: Int!, $skip: Int!) { | ||
7 | + allPosts(orderBy: { createdAt: desc }, first: $first, skip: $skip) { | ||
8 | + id | ||
9 | + title | ||
10 | + votes | ||
11 | + url | ||
12 | + createdAt | ||
13 | + } | ||
14 | + _allPostsMeta { | ||
15 | + count | ||
16 | + } | ||
17 | + } | ||
18 | +` | ||
19 | + | ||
20 | +export const allPostsQueryVars = { | ||
21 | + skip: 0, | ||
22 | + first: 10, | ||
23 | +} | ||
24 | + | ||
25 | +export default function PostList() { | ||
26 | + const { loading, error, data, fetchMore, networkStatus } = useQuery( | ||
27 | + ALL_POSTS_QUERY, | ||
28 | + { | ||
29 | + variables: allPostsQueryVars, | ||
30 | + // Setting this value to true will make the component rerender when | ||
31 | + // the "networkStatus" changes, so we are able to know if it is fetching | ||
32 | + // more data | ||
33 | + notifyOnNetworkStatusChange: true, | ||
34 | + } | ||
35 | + ) | ||
36 | + | ||
37 | + const loadingMorePosts = networkStatus === NetworkStatus.fetchMore | ||
38 | + | ||
39 | + const loadMorePosts = () => { | ||
40 | + fetchMore({ | ||
41 | + variables: { | ||
42 | + skip: allPosts.length, | ||
43 | + }, | ||
44 | + }) | ||
45 | + } | ||
46 | + | ||
47 | + if (error) return <ErrorMessage message="Error loading posts." /> | ||
48 | + if (loading && !loadingMorePosts) return <div>Loading</div> | ||
49 | + | ||
50 | + const { allPosts, _allPostsMeta } = data | ||
51 | + const areMorePosts = allPosts.length < _allPostsMeta.count | ||
52 | + | ||
53 | + return ( | ||
54 | + <section> | ||
55 | + <ul> | ||
56 | + {allPosts.map((post, index) => ( | ||
57 | + <li key={post.id}> | ||
58 | + <div> | ||
59 | + <span>{index + 1}. </span> | ||
60 | + <a href={post.url}>{post.title}</a> | ||
61 | + <PostUpvoter id={post.id} votes={post.votes} /> | ||
62 | + </div> | ||
63 | + </li> | ||
64 | + ))} | ||
65 | + </ul> | ||
66 | + {areMorePosts && ( | ||
67 | + <button onClick={() => loadMorePosts()} disabled={loadingMorePosts}> | ||
68 | + {loadingMorePosts ? 'Loading...' : 'Show More'} | ||
69 | + </button> | ||
70 | + )} | ||
71 | + <style jsx>{` | ||
72 | + section { | ||
73 | + padding-bottom: 20px; | ||
74 | + } | ||
75 | + li { | ||
76 | + display: block; | ||
77 | + margin-bottom: 10px; | ||
78 | + } | ||
79 | + div { | ||
80 | + align-items: center; | ||
81 | + display: flex; | ||
82 | + } | ||
83 | + a { | ||
84 | + font-size: 14px; | ||
85 | + margin-right: 10px; | ||
86 | + text-decoration: none; | ||
87 | + padding-bottom: 0; | ||
88 | + border: 0; | ||
89 | + } | ||
90 | + span { | ||
91 | + font-size: 14px; | ||
92 | + margin-right: 5px; | ||
93 | + } | ||
94 | + ul { | ||
95 | + margin: 0; | ||
96 | + padding: 0; | ||
97 | + } | ||
98 | + button:before { | ||
99 | + align-self: center; | ||
100 | + border-style: solid; | ||
101 | + border-width: 6px 4px 0 4px; | ||
102 | + border-color: #ffffff transparent transparent transparent; | ||
103 | + content: ''; | ||
104 | + height: 0; | ||
105 | + margin-right: 5px; | ||
106 | + width: 0; | ||
107 | + } | ||
108 | + `}</style> | ||
109 | + </section> | ||
110 | + ) | ||
111 | +} |
1 | +import { gql, useMutation } from '@apollo/client' | ||
2 | + | ||
3 | +const UPDATE_POST_MUTATION = gql` | ||
4 | + mutation votePost($id: String!) { | ||
5 | + votePost(id: $id) { | ||
6 | + id | ||
7 | + votes | ||
8 | + __typename | ||
9 | + } | ||
10 | + } | ||
11 | +` | ||
12 | + | ||
13 | +export default function PostUpvoter({ votes, id }) { | ||
14 | + const [updatePost] = useMutation(UPDATE_POST_MUTATION) | ||
15 | + | ||
16 | + const upvotePost = () => { | ||
17 | + updatePost({ | ||
18 | + variables: { | ||
19 | + id, | ||
20 | + }, | ||
21 | + optimisticResponse: { | ||
22 | + __typename: 'Mutation', | ||
23 | + votePost: { | ||
24 | + __typename: 'Post', | ||
25 | + id, | ||
26 | + votes: votes + 1, | ||
27 | + }, | ||
28 | + }, | ||
29 | + }) | ||
30 | + } | ||
31 | + | ||
32 | + return ( | ||
33 | + <button onClick={() => upvotePost()}> | ||
34 | + {votes} | ||
35 | + <style jsx>{` | ||
36 | + button { | ||
37 | + background-color: transparent; | ||
38 | + border: 1px solid #e4e4e4; | ||
39 | + color: #000; | ||
40 | + } | ||
41 | + button:active { | ||
42 | + background-color: transparent; | ||
43 | + } | ||
44 | + button:before { | ||
45 | + align-self: center; | ||
46 | + border-color: transparent transparent #000000 transparent; | ||
47 | + border-style: solid; | ||
48 | + border-width: 0 4px 6px 4px; | ||
49 | + content: ''; | ||
50 | + height: 0; | ||
51 | + margin-right: 5px; | ||
52 | + width: 0; | ||
53 | + } | ||
54 | + `}</style> | ||
55 | + </button> | ||
56 | + ) | ||
57 | +} |
project/packages/web/components/Submit.js
0 → 100644
1 | +import { gql, useMutation } from '@apollo/client' | ||
2 | + | ||
3 | +const CREATE_POST_MUTATION = gql` | ||
4 | + mutation createPost($title: String!, $url: String!) { | ||
5 | + createPost(title: $title, url: $url) { | ||
6 | + id | ||
7 | + title | ||
8 | + votes | ||
9 | + url | ||
10 | + createdAt | ||
11 | + } | ||
12 | + } | ||
13 | +` | ||
14 | + | ||
15 | +export default function Submit() { | ||
16 | + const [createPost, { loading }] = useMutation(CREATE_POST_MUTATION) | ||
17 | + | ||
18 | + const handleSubmit = (event) => { | ||
19 | + event.preventDefault() | ||
20 | + const form = event.target | ||
21 | + const formData = new window.FormData(form) | ||
22 | + const title = formData.get('title') | ||
23 | + const url = formData.get('url') | ||
24 | + form.reset() | ||
25 | + | ||
26 | + createPost({ | ||
27 | + variables: { title, url }, | ||
28 | + update: (cache, { data: { createPost } }) => { | ||
29 | + cache.modify({ | ||
30 | + fields: { | ||
31 | + allPosts(existingPosts = []) { | ||
32 | + const newPostRef = cache.writeFragment({ | ||
33 | + data: createPost, | ||
34 | + fragment: gql` | ||
35 | + fragment NewPost on allPosts { | ||
36 | + id | ||
37 | + type | ||
38 | + } | ||
39 | + `, | ||
40 | + }) | ||
41 | + return [newPostRef, ...existingPosts] | ||
42 | + }, | ||
43 | + }, | ||
44 | + }) | ||
45 | + }, | ||
46 | + }) | ||
47 | + } | ||
48 | + | ||
49 | + return ( | ||
50 | + <form onSubmit={handleSubmit}> | ||
51 | + <h1>Submit</h1> | ||
52 | + <input placeholder="title" name="title" type="text" required /> | ||
53 | + <input placeholder="url" name="url" type="url" required /> | ||
54 | + <button type="submit" disabled={loading}> | ||
55 | + Submit | ||
56 | + </button> | ||
57 | + <style jsx>{` | ||
58 | + form { | ||
59 | + border-bottom: 1px solid #ececec; | ||
60 | + padding-bottom: 20px; | ||
61 | + margin-bottom: 20px; | ||
62 | + } | ||
63 | + h1 { | ||
64 | + font-size: 20px; | ||
65 | + } | ||
66 | + input { | ||
67 | + display: block; | ||
68 | + margin-bottom: 10px; | ||
69 | + } | ||
70 | + `}</style> | ||
71 | + </form> | ||
72 | + ) | ||
73 | +} |
project/packages/web/lib/apolloClient.js
0 → 100644
1 | +import { useMemo } from 'react' | ||
2 | +import { ApolloClient, HttpLink, InMemoryCache } from '@apollo/client' | ||
3 | +import { concatPagination } from '@apollo/client/utilities' | ||
4 | +import merge from 'deepmerge' | ||
5 | +import isEqual from 'lodash/isEqual' | ||
6 | + | ||
7 | +export const APOLLO_STATE_PROP_NAME = '__APOLLO_STATE__' | ||
8 | + | ||
9 | +let apolloClient | ||
10 | + | ||
11 | +function createApolloClient() { | ||
12 | + return new ApolloClient({ | ||
13 | + ssrMode: typeof window === 'undefined', | ||
14 | + link: new HttpLink({ | ||
15 | + uri: 'https://nextjs-graphql-with-prisma-simple.vercel.app/api', // Server URL (must be absolute) | ||
16 | + credentials: 'same-origin', // Additional fetch() options like `credentials` or `headers` | ||
17 | + }), | ||
18 | + cache: new InMemoryCache({ | ||
19 | + typePolicies: { | ||
20 | + Query: { | ||
21 | + fields: { | ||
22 | + allPosts: concatPagination(), | ||
23 | + }, | ||
24 | + }, | ||
25 | + }, | ||
26 | + }), | ||
27 | + }) | ||
28 | +} | ||
29 | + | ||
30 | +export function initializeApollo(initialState = null) { | ||
31 | + const _apolloClient = apolloClient ?? createApolloClient() | ||
32 | + | ||
33 | + // If your page has Next.js data fetching methods that use Apollo Client, the initial state | ||
34 | + // gets hydrated here | ||
35 | + if (initialState) { | ||
36 | + // Get existing cache, loaded during client side data fetching | ||
37 | + const existingCache = _apolloClient.extract() | ||
38 | + | ||
39 | + // Merge the existing cache into data passed from getStaticProps/getServerSideProps | ||
40 | + const data = merge(initialState, existingCache, { | ||
41 | + // combine arrays using object equality (like in sets) | ||
42 | + arrayMerge: (destinationArray, sourceArray) => [ | ||
43 | + ...sourceArray, | ||
44 | + ...destinationArray.filter((d) => | ||
45 | + sourceArray.every((s) => !isEqual(d, s)) | ||
46 | + ), | ||
47 | + ], | ||
48 | + }) | ||
49 | + | ||
50 | + // Restore the cache with the merged data | ||
51 | + _apolloClient.cache.restore(data) | ||
52 | + } | ||
53 | + // For SSG and SSR always create a new Apollo Client | ||
54 | + if (typeof window === 'undefined') return _apolloClient | ||
55 | + // Create the Apollo Client once in the client | ||
56 | + if (!apolloClient) apolloClient = _apolloClient | ||
57 | + | ||
58 | + return _apolloClient | ||
59 | +} | ||
60 | + | ||
61 | +export function addApolloState(client, pageProps) { | ||
62 | + if (pageProps?.props) { | ||
63 | + pageProps.props[APOLLO_STATE_PROP_NAME] = client.cache.extract() | ||
64 | + } | ||
65 | + | ||
66 | + return pageProps | ||
67 | +} | ||
68 | + | ||
69 | +export function useApollo(pageProps) { | ||
70 | + const state = pageProps[APOLLO_STATE_PROP_NAME] | ||
71 | + const store = useMemo(() => initializeApollo(state), [state]) | ||
72 | + return store | ||
73 | +} |
project/packages/web/package.json
0 → 100644
1 | +{ | ||
2 | + "name": "@graphql-community/web", | ||
3 | + "version": "1.0.0", | ||
4 | + "scripts": { | ||
5 | + "dev": "next", | ||
6 | + "build": "next build", | ||
7 | + "start": "next start" | ||
8 | + }, | ||
9 | + "dependencies": { | ||
10 | + "@apollo/client": "3.1.1", | ||
11 | + "deepmerge": "^4.2.2", | ||
12 | + "lodash": "4.17.20", | ||
13 | + "graphql": "^15.3.0", | ||
14 | + "next": "latest", | ||
15 | + "prop-types": "^15.6.2", | ||
16 | + "react": "^16.7.0", | ||
17 | + "react-dom": "^16.7.0" | ||
18 | + }, | ||
19 | + "license": "MIT" | ||
20 | +} |
project/packages/web/pages/_app.js
0 → 100644
1 | +import { ApolloProvider } from '@apollo/client' | ||
2 | +import { useApollo } from '../lib/apolloClient' | ||
3 | + | ||
4 | +export default function App({ Component, pageProps }) { | ||
5 | + const apolloClient = useApollo(pageProps) | ||
6 | + | ||
7 | + return ( | ||
8 | + <ApolloProvider client={apolloClient}> | ||
9 | + <Component {...pageProps} /> | ||
10 | + </ApolloProvider> | ||
11 | + ) | ||
12 | +} |
project/packages/web/pages/about.js
0 → 100644
1 | +import App from '../components/App' | ||
2 | +import Header from '../components/Header' | ||
3 | + | ||
4 | +const AboutPage = () => ( | ||
5 | + <App> | ||
6 | + <Header /> | ||
7 | + <article> | ||
8 | + <h1>The Idea Behind This Example</h1> | ||
9 | + <p> | ||
10 | + <a href="https://www.apollographql.com/client/">Apollo</a> is a GraphQL | ||
11 | + client that allows you to easily query the exact data you need from a | ||
12 | + GraphQL server. In addition to fetching and mutating data, Apollo | ||
13 | + analyzes your queries and their results to construct a client-side cache | ||
14 | + of your data, which is kept up to date as further queries and mutations | ||
15 | + are run, fetching more results from the server. | ||
16 | + </p> | ||
17 | + <p> | ||
18 | + In this simple example, we integrate Apollo seamlessly with{' '} | ||
19 | + <a href="https://github.com/vercel/next.js">Next</a> by calling{' '} | ||
20 | + <a href="https://nextjs.org/docs/basic-features/data-fetching#getstaticprops-static-generation"> | ||
21 | + getStaticProps | ||
22 | + </a>{' '} | ||
23 | + at our Page component. This approach lets us opt out of getInitialProps | ||
24 | + and let us use all the niceties provided by{' '} | ||
25 | + <a href="https://github.com/vercel/next.js">Next</a>. | ||
26 | + </p> | ||
27 | + <p> | ||
28 | + On initial page load, while on the server and inside{' '} | ||
29 | + <a href="https://nextjs.org/docs/basic-features/data-fetching#getstaticprops-static-generation"> | ||
30 | + getStaticProps | ||
31 | + </a> | ||
32 | + , we fetch the query used to get the list of posts. At the point in | ||
33 | + which the query promise resolves, our Apollo Client store is completely | ||
34 | + initialized. Then we serve the initial HTML with the fetched data and | ||
35 | + hydrate Apollo in the browser. | ||
36 | + </p> | ||
37 | + <p> | ||
38 | + This example relies on <a href="http://graph.cool">graph.cool</a> for | ||
39 | + its GraphQL backend. | ||
40 | + </p> | ||
41 | + </article> | ||
42 | + </App> | ||
43 | +) | ||
44 | + | ||
45 | +export default AboutPage |
project/packages/web/pages/client-only.js
0 → 100644
1 | +import App from '../components/App' | ||
2 | +import InfoBox from '../components/InfoBox' | ||
3 | +import Header from '../components/Header' | ||
4 | +import Submit from '../components/Submit' | ||
5 | +import PostList from '../components/PostList' | ||
6 | + | ||
7 | +const ClientOnlyPage = (props) => ( | ||
8 | + <App> | ||
9 | + <Header /> | ||
10 | + <InfoBox> | ||
11 | + ℹ️ This page shows how to use Apollo only in the client. If you{' '} | ||
12 | + <a href="/client-only">reload</a> this page, you will see a loader since | ||
13 | + Apollo didn't fetch any data on the server. This is useful when the page | ||
14 | + doesn't have SEO requirements or blocking data fetching requirements. | ||
15 | + </InfoBox> | ||
16 | + <Submit /> | ||
17 | + <PostList /> | ||
18 | + </App> | ||
19 | +) | ||
20 | + | ||
21 | +export default ClientOnlyPage |
project/packages/web/pages/index.js
0 → 100644
1 | +import App from '../components/App' | ||
2 | +import InfoBox from '../components/InfoBox' | ||
3 | +import Header from '../components/Header' | ||
4 | +import Submit from '../components/Submit' | ||
5 | +import PostList, { | ||
6 | + ALL_POSTS_QUERY, | ||
7 | + allPostsQueryVars, | ||
8 | +} from '../components/PostList' | ||
9 | +import { initializeApollo, addApolloState } from '../lib/apolloClient' | ||
10 | + | ||
11 | +const IndexPage = () => ( | ||
12 | + <App> | ||
13 | + <Header /> | ||
14 | + <InfoBox>ℹ️ This page shows how to use SSG with Apollo.</InfoBox> | ||
15 | + <Submit /> | ||
16 | + <PostList /> | ||
17 | + </App> | ||
18 | +) | ||
19 | + | ||
20 | +export async function getStaticProps() { | ||
21 | + const apolloClient = initializeApollo() | ||
22 | + | ||
23 | + await apolloClient.query({ | ||
24 | + query: ALL_POSTS_QUERY, | ||
25 | + variables: allPostsQueryVars, | ||
26 | + }) | ||
27 | + | ||
28 | + return addApolloState(apolloClient, { | ||
29 | + props: {}, | ||
30 | + revalidate: 1, | ||
31 | + }) | ||
32 | +} | ||
33 | + | ||
34 | +export default IndexPage |
project/packages/web/pages/ssr.js
0 → 100644
1 | +import App from '../components/App' | ||
2 | +import InfoBox from '../components/InfoBox' | ||
3 | +import Header from '../components/Header' | ||
4 | +import Submit from '../components/Submit' | ||
5 | +import PostList, { | ||
6 | + ALL_POSTS_QUERY, | ||
7 | + allPostsQueryVars, | ||
8 | +} from '../components/PostList' | ||
9 | +import { initializeApollo, addApolloState } from '../lib/apolloClient' | ||
10 | + | ||
11 | +const SSRPage = () => ( | ||
12 | + <App> | ||
13 | + <Header /> | ||
14 | + <InfoBox>ℹ️ This page shows how to use SSR with Apollo.</InfoBox> | ||
15 | + <Submit /> | ||
16 | + <PostList /> | ||
17 | + </App> | ||
18 | +) | ||
19 | + | ||
20 | +export async function getServerSideProps() { | ||
21 | + const apolloClient = initializeApollo() | ||
22 | + | ||
23 | + await apolloClient.query({ | ||
24 | + query: ALL_POSTS_QUERY, | ||
25 | + variables: allPostsQueryVars, | ||
26 | + }) | ||
27 | + | ||
28 | + return addApolloState(apolloClient, { | ||
29 | + props: {}, | ||
30 | + }) | ||
31 | +} | ||
32 | + | ||
33 | +export default SSRPage |
project/yarn.lock
0 → 100644
This diff could not be displayed because it is too large.
-
Please register or login to post a comment