├── .editorconfig
├── .env.development
├── .env.production
├── .eslintignore
├── .eslintrc.json
├── .github
└── workflows
│ └── ci.yml
├── .gitignore
├── .husky
├── .gitignore
└── pre-commit
├── .jest
└── setup.tsx
├── .prettierignore
├── .prettierrc
├── .storybook
├── main.js
└── preview.js
├── .vscode
└── settings.json
├── README.md
├── codegen.yml
├── generators
├── plopfile.js
└── templates
│ ├── Component.tsx.hbs
│ ├── stories.tsx.hbs
│ ├── styles.ts.hbs
│ └── test.tsx.hbs
├── jest.config.js
├── next-env.d.ts
├── next.config.js
├── package.json
├── public
├── img
│ ├── icon-192.png
│ └── icon-512.png
└── manifest.json
├── src
├── components
│ └── Avatar
│ │ ├── index.tsx
│ │ ├── stories.tsx
│ │ ├── styles.ts
│ │ └── test.tsx
├── graphql
│ ├── client.ts
│ ├── generated
│ │ └── graphql.ts
│ └── queries.ts
├── pages
│ ├── _app.tsx
│ ├── _document.tsx
│ └── index.tsx
├── styles
│ └── global.ts
├── templates
│ └── Home
│ │ ├── index.tsx
│ │ ├── styles.ts
│ │ └── test.tsx
└── types
│ └── jest-styled-components.d.ts
├── tsconfig.json
└── yarn.lock
/.editorconfig:
--------------------------------------------------------------------------------
1 | # editorconfig.org
2 | root = true
3 |
4 | [*]
5 | indent_style = space
6 | indent_size = 2
7 | end_of_line = lf
8 | charset = utf-8
9 | trim_trailing_whitespace = true
10 | insert_final_newline = true
11 |
--------------------------------------------------------------------------------
/.env.development:
--------------------------------------------------------------------------------
1 | GRAPHQL_HOST=https://rickandmortyapi.com/graphql
2 | GRAPHQL_TOKEN=
3 |
--------------------------------------------------------------------------------
/.env.production:
--------------------------------------------------------------------------------
1 | GRAPHQL_HOST=https://rickandmortyapi.com/graphql
2 |
--------------------------------------------------------------------------------
/.eslintignore:
--------------------------------------------------------------------------------
1 | !.storybook
2 | !.jest
3 |
--------------------------------------------------------------------------------
/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "env": {
3 | "browser": true,
4 | "es2020": true,
5 | "jest": true,
6 | "node": true
7 | },
8 | "settings": {
9 | "react": {
10 | "version": "detect"
11 | }
12 | },
13 | "extends": [
14 | "eslint:recommended",
15 | "plugin:react/recommended",
16 | "plugin:@typescript-eslint/eslint-recommended",
17 | "plugin:@typescript-eslint/recommended",
18 | "plugin:prettier/recommended",
19 | "plugin:@next/next/recommended"
20 | ],
21 | "parser": "@typescript-eslint/parser",
22 | "parserOptions": {
23 | "ecmaFeatures": {
24 | "jsx": true
25 | },
26 | "ecmaVersion": 11,
27 | "sourceType": "module"
28 | },
29 | "plugins": ["react", "react-hooks", "@typescript-eslint"],
30 | "rules": {
31 | "react-hooks/rules-of-hooks": "error",
32 | "react-hooks/exhaustive-deps": "warn",
33 | "react/prop-types": "off",
34 | "react/react-in-jsx-scope": "off",
35 | "@typescript-eslint/explicit-module-boundary-types": "off"
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/.github/workflows/ci.yml:
--------------------------------------------------------------------------------
1 | name: ci
2 | on: [pull_request]
3 |
4 | jobs:
5 | build:
6 | runs-on: ubuntu-latest
7 | steps:
8 | - name: Checkout Repository
9 | uses: actions/checkout@v2
10 |
11 | - name: Setup Node
12 | uses: actions/setup-node@v1
13 | with:
14 | node-version: 14.x
15 |
16 | - uses: actions/cache@v2
17 | id: yarn-cache
18 | with:
19 | path: |
20 | ~/cache
21 | !~/cache/exclude
22 | **/node_modules
23 | key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
24 | restore-keys: |
25 | ${{ runner.os }}-yarn-
26 |
27 | - name: Install dependencies
28 | run: yarn install
29 |
30 | - name: Linting
31 | run: yarn lint
32 |
33 | - name: Test
34 | run: yarn test:ci
35 |
36 | - name: Build
37 | run: yarn build
38 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
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 |
21 | # debug
22 | npm-debug.log*
23 | yarn-debug.log*
24 | yarn-error.log*
25 |
26 | # local env files
27 | .env.local
28 | .env.development.local
29 | .env.test.local
30 | .env.production.local
31 |
32 | # sw stuff
33 | public/sw.js
34 | public/workbox-*.js
35 |
36 | # storybook
37 | storybook-static
38 |
--------------------------------------------------------------------------------
/.husky/.gitignore:
--------------------------------------------------------------------------------
1 | _
2 |
--------------------------------------------------------------------------------
/.husky/pre-commit:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | . "$(dirname "$0")/_/husky.sh"
3 |
4 | npx --no-install lint-staged
5 |
--------------------------------------------------------------------------------
/.jest/setup.tsx:
--------------------------------------------------------------------------------
1 | import '@testing-library/jest-dom'
2 | import 'jest-styled-components'
3 |
4 | // Fix next img in test env
5 | /* eslint-disable */
6 | jest.mock("next/image", () => ({ src, alt }) =>
)
7 |
--------------------------------------------------------------------------------
/.prettierignore:
--------------------------------------------------------------------------------
1 | !.storybook
2 | !.jest
3 |
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "trailingComma": "none",
3 | "semi": false,
4 | "singleQuote": true
5 | }
6 |
--------------------------------------------------------------------------------
/.storybook/main.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | staticDirs: ['../public'],
3 | stories: ['../src/components/**/stories.tsx'],
4 | addons: ['@storybook/addon-essentials', 'storybook-addon-next-router'],
5 | core: {
6 | builder: 'webpack5'
7 | },
8 | webpackFinal: (config) => {
9 | config.resolve.modules.push(`${process.cwd()}/src`)
10 | return config
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/.storybook/preview.js:
--------------------------------------------------------------------------------
1 | import { RouterContext } from 'next/dist/shared/lib/router-context'
2 | import GlobalStyles from '../src/styles/global'
3 |
4 | import * as nextImage from 'next/image'
5 |
6 | // This is needed to use next/image
7 | /* eslint-disable */
8 | Object.defineProperty(nextImage, 'default', {
9 | configurable: true,
10 | value: (props) =>
11 | })
12 |
13 | export const parameters = {
14 | nextRouter: {
15 | Provider: RouterContext.Provider
16 | }
17 | }
18 |
19 | export const decorators = [
20 | (Story) => (
21 | <>
22 |
23 |
24 | >
25 | )
26 | ]
27 |
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "editor.formatOnSave": false,
3 | "editor.codeActionsOnSave": {
4 | "source.fixAll.eslint": true
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | 
2 |
3 | This is a very simple boilerplate made with [Next.js](https://nextjs.org/) and [GraphQL Request](https://github.com/prisma-labs/graphql-request) to start small projects with GraphQL.
4 |
5 | [**View Demo**](https://boilerplate-graphql-demo.vercel.app/)
6 |
7 | ## What is inside?
8 |
9 | This project uses lot of stuff as:
10 |
11 | - [TypeScript](https://www.typescriptlang.org/)
12 | - [NextJS](https://nextjs.org/)
13 | - [GraphQL Request](https://github.com/prisma-labs/graphql-request)
14 | - [Styled Components](https://styled-components.com/)
15 | - [Jest](https://jestjs.io/)
16 | - [React Testing Library](https://testing-library.com/docs/react-testing-library/intro)
17 | - [Storybook](https://storybook.js.org/)
18 | - [Eslint](https://eslint.org/)
19 | - [Prettier](https://prettier.io/)
20 | - [Husky](https://github.com/typicode/husky)
21 |
22 | ## Getting Started
23 |
24 | First, run the development server:
25 |
26 | ```bash
27 | npm run dev
28 | # or
29 | yarn dev
30 | ```
31 |
32 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
33 |
34 | You can start editing the page by modifying `pages/index.js`. The page auto-updates as you edit the file.
35 |
36 | ## Working with GraphQL
37 |
38 | In order to work with GraphQL we have two variables that you need to define in your `.env`. Those are:
39 |
40 | - `GRAPHQL_HOST`: your GraphQL API url
41 | - `GRAPHQL_TOKEN`: your token if the API needs authentication
42 |
43 | If you need to use authentication, just edit the [src/graphql/client.ts] to use the token part. After that, you're ready to go!
44 |
45 | ## How to generate your GraphQL Types
46 |
47 | We use the amazing [graphql-codegen](https://www.graphql-code-generator.com/) to generate our types based on the API. All you have to do is:
48 |
49 | - Define your API inside the [codegen.yml](codegen.yml) file
50 | - Run `yarn codegen` and that's it! All your generated types will be inside [src/graphql/generated](src/graphql/generated) folder.
51 |
52 | ## Commands
53 |
54 | - `dev`: runs your application on `localhost:3000`
55 | - `build`: creates the production build version
56 | - `start`: starts a simple server with the build production code
57 | - `codegen`: creates the graphql generated types based in your API
58 | - `lint`: runs the linter in all components and pages
59 | - `test`: runs jest to test all components and pages
60 | - `test:watch`: runs jest in watch mode
61 | - `storybook`: runs storybook on `localhost:6006`
62 | - `build-storybook`: create the build version of storybook
63 |
64 | ## Learn More
65 |
66 | To learn more about Next.js, take a look at the following resources:
67 |
68 | - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
69 | - [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
70 |
71 | You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome!
72 |
73 | ## Deploy on Vercel
74 |
75 | The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/import?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
76 |
77 | Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details.
78 |
--------------------------------------------------------------------------------
/codegen.yml:
--------------------------------------------------------------------------------
1 | overwrite: true
2 | schema: "https://rickandmortyapi.com/graphql"
3 | documents: "src/graphql/**/*.ts"
4 | generates:
5 | src/graphql/generated/graphql.ts:
6 | plugins:
7 | - "typescript"
8 | - "typescript-operations"
9 | - add:
10 | content: '/* eslint-disable */'
11 |
--------------------------------------------------------------------------------
/generators/plopfile.js:
--------------------------------------------------------------------------------
1 | module.exports = (plop) => {
2 | plop.setGenerator('component', {
3 | description: 'Create a component',
4 | prompts: [
5 | {
6 | type: 'input',
7 | name: 'name',
8 | message: 'What is your component name?'
9 | }
10 | ],
11 | actions: [
12 | {
13 | type: 'add',
14 | path: '../src/components/{{pascalCase name}}/index.tsx',
15 | templateFile: 'templates/Component.tsx.hbs'
16 | },
17 | {
18 | type: 'add',
19 | path: '../src/components/{{pascalCase name}}/styles.ts',
20 | templateFile: 'templates/styles.ts.hbs'
21 | },
22 | {
23 | type: 'add',
24 | path: '../src/components/{{pascalCase name}}/stories.tsx',
25 | templateFile: 'templates/stories.tsx.hbs'
26 | },
27 | {
28 | type: 'add',
29 | path: '../src/components/{{pascalCase name}}/test.tsx',
30 | templateFile: 'templates/test.tsx.hbs'
31 | }
32 | ]
33 | })
34 | }
35 |
--------------------------------------------------------------------------------
/generators/templates/Component.tsx.hbs:
--------------------------------------------------------------------------------
1 | import * as S from './styles'
2 |
3 | const {{pascalCase name}} = () => (
4 |
5 | {{pascalCase name}}
6 |
7 | )
8 |
9 | export default {{pascalCase name}}
10 |
--------------------------------------------------------------------------------
/generators/templates/stories.tsx.hbs:
--------------------------------------------------------------------------------
1 | import { Story, Meta } from '@storybook/react/types-6-0'
2 | import {{pascalCase name}} from '.'
3 |
4 | export default {
5 | title: '{{pascalCase name}}',
6 | component: {{pascalCase name}}
7 | } as Meta
8 |
9 | export const Default: Story = () => <{{pascalCase name}} />
10 |
--------------------------------------------------------------------------------
/generators/templates/styles.ts.hbs:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components'
2 |
3 | export const Wrapper = styled.main``
4 |
--------------------------------------------------------------------------------
/generators/templates/test.tsx.hbs:
--------------------------------------------------------------------------------
1 | import { render, screen } from '@testing-library/react'
2 |
3 | import {{pascalCase name}} from '.'
4 |
5 | describe('<{{pascalCase name}} />', () => {
6 | it('should render the heading', () => {
7 | const { container } = render(<{{pascalCase name}} />)
8 |
9 | expect(screen.getByRole('heading', { name: /{{pascalCase name}}/i })).toBeInTheDocument()
10 |
11 | expect(container.firstChild).toMatchSnapshot()
12 | })
13 | })
14 |
--------------------------------------------------------------------------------
/jest.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | testEnvironment: 'jsdom',
3 | testPathIgnorePatterns: ['/node_modules/', '/.next/'],
4 | collectCoverage: true,
5 | collectCoverageFrom: [
6 | 'src/components/**/*.ts(x)?',
7 | 'src/templates/**/*.ts(x)?',
8 | '!src/**/stories.tsx'
9 | ],
10 | setupFilesAfterEnv: ['/.jest/setup.tsx'],
11 | modulePaths: ['/src/'],
12 | transform: {
13 | '^.+\\.(js|jsx|ts|tsx)$': ['babel-jest', { presets: ['next/babel'] }]
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/next-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 | ///
3 |
4 | // NOTE: This file should not be edited
5 | // see https://nextjs.org/docs/basic-features/typescript for more information.
6 |
--------------------------------------------------------------------------------
/next.config.js:
--------------------------------------------------------------------------------
1 | // eslint-disable-next-line @typescript-eslint/no-var-requires
2 | const withPWA = require('next-pwa')
3 | const isProd = process.env.NODE_ENV === 'production'
4 |
5 | module.exports = withPWA({
6 | swcMinify: true,
7 | experimental: {
8 | // Enables the styled-components SWC transform
9 | styledComponents: true
10 | },
11 | images: {
12 | domains: ['rickandmortyapi.com']
13 | },
14 | pwa: {
15 | dest: 'public',
16 | disable: !isProd
17 | }
18 | })
19 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-avancado-boilerplate",
3 | "version": "1.0.0",
4 | "private": true,
5 | "scripts": {
6 | "dev": "next dev",
7 | "build": "next build",
8 | "start": "next start",
9 | "lint": "eslint src --max-warnings=0",
10 | "test": "jest --maxWorkers=50%",
11 | "test:watch": "jest --watch --maxWorkers=25%",
12 | "test:ci": "jest --runInBand",
13 | "generate": "yarn plop --plopfile generators/plopfile.js",
14 | "storybook": "start-storybook -p 6006",
15 | "build-storybook": "build-storybook",
16 | "postinstall": "husky install",
17 | "codegen": "graphql-codegen --config codegen.yml"
18 | },
19 | "lint-staged": {
20 | "src/**/*": [
21 | "yarn lint --fix",
22 | "yarn test --findRelatedTests --bail"
23 | ]
24 | },
25 | "dependencies": {
26 | "graphql": "^16.3.0",
27 | "graphql-request": "^4.0.0",
28 | "next": "12.0.10",
29 | "next-pwa": "^5.4.4",
30 | "react": "17.0.2",
31 | "react-dom": "17.0.2",
32 | "styled-components": "5.3.3"
33 | },
34 | "devDependencies": {
35 | "@graphql-codegen/add": "^3.1.1",
36 | "@graphql-codegen/cli": "2.4.0",
37 | "@graphql-codegen/typescript": "2.4.2",
38 | "@graphql-codegen/typescript-operations": "2.2.3",
39 | "@storybook/addon-essentials": "6.4.17",
40 | "@storybook/builder-webpack5": "^6.4.17",
41 | "@storybook/manager-webpack5": "^6.4.17",
42 | "@storybook/react": "6.4.17",
43 | "@testing-library/jest-dom": "^5.16.1",
44 | "@testing-library/react": "^12.1.2",
45 | "@types/jest": "^27.4.0",
46 | "@types/node": "^17.0.14",
47 | "@types/react": "^17.0.38",
48 | "@types/styled-components": "^5.1.21",
49 | "@typescript-eslint/eslint-plugin": "5.10.2",
50 | "@typescript-eslint/parser": "5.10.2",
51 | "eslint": "^8.8.0",
52 | "eslint-config-next": "^12.0.10",
53 | "eslint-config-prettier": "^8.3.0",
54 | "eslint-plugin-prettier": "^4.0.0",
55 | "eslint-plugin-react": "^7.28.0",
56 | "eslint-plugin-react-hooks": "^4.3.0",
57 | "husky": "^7.0.4",
58 | "jest": "^27.4.7",
59 | "jest-styled-components": "^7.0.8",
60 | "lint-staged": "^12.3.2",
61 | "plop": "^3.0.5",
62 | "prettier": "^2.5.1",
63 | "storybook-addon-next-router": "^3.1.1",
64 | "typescript": "^4.5.5",
65 | "webpack": "5.68.0"
66 | }
67 | }
68 |
--------------------------------------------------------------------------------
/public/img/icon-192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/React-Avancado/boilerplate-graphql/2d2cc875f839c9ec55cae641f3946b4fc48fc47d/public/img/icon-192.png
--------------------------------------------------------------------------------
/public/img/icon-512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/React-Avancado/boilerplate-graphql/2d2cc875f839c9ec55cae641f3946b4fc48fc47d/public/img/icon-512.png
--------------------------------------------------------------------------------
/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "React Avançado - Boilerplate",
3 | "short_name": "React Avançado",
4 | "icons": [
5 | {
6 | "src": "/img/icon-192.png",
7 | "type": "image/png",
8 | "sizes": "192x192"
9 | },
10 | {
11 | "src": "/img/icon-512.png",
12 | "type": "image/png",
13 | "sizes": "512x512"
14 | }
15 | ],
16 | "background_color": "#06092B",
17 | "description": "Boilerplate utilizando Typescript, React, NextJS e Styled Components!",
18 | "display": "fullscreen",
19 | "start_url": "/",
20 | "theme_color": "#06092B"
21 | }
22 |
--------------------------------------------------------------------------------
/src/components/Avatar/index.tsx:
--------------------------------------------------------------------------------
1 | import { Character } from 'graphql/generated/graphql'
2 | import Image from 'next/image'
3 |
4 | import * as S from './styles'
5 |
6 | export type AvatarProps = Pick
7 |
8 | const placeholderImage =
9 | 'https://rickandmortyapi.com/api/character/avatar/19.jpeg'
10 |
11 | const placeholderName = 'Rick and Morty Character'
12 |
13 | const Avatar = ({ name, image }: AvatarProps) => (
14 |
15 |
21 | {name || placeholderName}
22 |
23 | )
24 |
25 | export default Avatar
26 |
--------------------------------------------------------------------------------
/src/components/Avatar/stories.tsx:
--------------------------------------------------------------------------------
1 | import { Story, Meta } from '@storybook/react/types-6-0'
2 | import Avatar, { AvatarProps } from '.'
3 |
4 | export default {
5 | title: 'Avatar',
6 | component: Avatar,
7 | argTypes: {
8 | name: {
9 | control: {
10 | type: 'text'
11 | }
12 | }
13 | }
14 | } as Meta
15 |
16 | export const Default: Story = (args) =>
17 |
--------------------------------------------------------------------------------
/src/components/Avatar/styles.ts:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components'
2 |
3 | export const Wrapper = styled.main`
4 | margin: auto;
5 | max-width: 300px;
6 |
7 | img {
8 | border-radius: 50%;
9 | }
10 | `
11 |
12 | export const Title = styled.h2`
13 | font-size: 18px;
14 | margin-top: 8px;
15 | text-align: center;
16 | `
17 |
--------------------------------------------------------------------------------
/src/components/Avatar/test.tsx:
--------------------------------------------------------------------------------
1 | import { render, screen } from '@testing-library/react'
2 |
3 | import Avatar from '.'
4 |
5 | describe('', () => {
6 | it('should render with placeholders', () => {
7 | render()
8 |
9 | expect(screen.getByRole('img')).toHaveAttribute(
10 | 'src',
11 | 'https://rickandmortyapi.com/api/character/avatar/19.jpeg'
12 | )
13 |
14 | expect(
15 | screen.getByRole('heading', { name: /Rick and Morty Character/i })
16 | ).toBeInTheDocument()
17 | })
18 |
19 | it('should render with passed values', () => {
20 | render()
21 |
22 | expect(screen.getByRole('img')).toHaveAttribute('src', '/morty.jpg')
23 | expect(screen.getByRole('heading', { name: /Morty/i })).toBeInTheDocument()
24 | })
25 | })
26 |
--------------------------------------------------------------------------------
/src/graphql/client.ts:
--------------------------------------------------------------------------------
1 | import { GraphQLClient } from 'graphql-request'
2 |
3 | const endpoint = process.env.GRAPHQL_HOST || ''
4 |
5 | // If your API needs an authorization, use like this
6 |
7 | // const client = new GraphQLClient(endpoint, {
8 | // headers: {
9 | // authorization: `Bearer ${process.env.GRAPHQL_TOKEN}`
10 | // }
11 | // })
12 |
13 | const client = new GraphQLClient(endpoint)
14 |
15 | export default client
16 |
--------------------------------------------------------------------------------
/src/graphql/generated/graphql.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable */
2 | export type Maybe = T | null;
3 | export type Exact = { [K in keyof T]: T[K] };
4 | export type MakeOptional = Omit & { [SubKey in K]?: Maybe };
5 | export type MakeMaybe = Omit & { [SubKey in K]: Maybe };
6 | /** All built-in and custom scalars, mapped to their actual values */
7 | export type Scalars = {
8 | ID: string;
9 | String: string;
10 | Boolean: boolean;
11 | Int: number;
12 | Float: number;
13 | /** The `Upload` scalar type represents a file upload. */
14 | Upload: any;
15 | };
16 |
17 | export enum CacheControlScope {
18 | Public = 'PUBLIC',
19 | Private = 'PRIVATE'
20 | }
21 |
22 | export type Character = {
23 | __typename?: 'Character';
24 | /** The id of the character. */
25 | id?: Maybe;
26 | /** The name of the character. */
27 | name?: Maybe;
28 | /** The status of the character ('Alive', 'Dead' or 'unknown'). */
29 | status?: Maybe;
30 | /** The species of the character. */
31 | species?: Maybe;
32 | /** The type or subspecies of the character. */
33 | type?: Maybe;
34 | /** The gender of the character ('Female', 'Male', 'Genderless' or 'unknown'). */
35 | gender?: Maybe;
36 | /** The character's origin location */
37 | origin?: Maybe;
38 | /** The character's last known location */
39 | location?: Maybe;
40 | /**
41 | * Link to the character's image.
42 | * All images are 300x300px and most are medium shots or portraits since they are intended to be used as avatars.
43 | */
44 | image?: Maybe;
45 | /** Episodes in which this character appeared. */
46 | episode: Array>;
47 | /** Time at which the character was created in the database. */
48 | created?: Maybe;
49 | };
50 |
51 | export type Characters = {
52 | __typename?: 'Characters';
53 | info?: Maybe;
54 | results?: Maybe>>;
55 | };
56 |
57 | export type Episode = {
58 | __typename?: 'Episode';
59 | /** The id of the episode. */
60 | id?: Maybe;
61 | /** The name of the episode. */
62 | name?: Maybe;
63 | /** The air date of the episode. */
64 | air_date?: Maybe;
65 | /** The code of the episode. */
66 | episode?: Maybe;
67 | /** List of characters who have been seen in the episode. */
68 | characters: Array>;
69 | /** Time at which the episode was created in the database. */
70 | created?: Maybe;
71 | };
72 |
73 | export type Episodes = {
74 | __typename?: 'Episodes';
75 | info?: Maybe;
76 | results?: Maybe>>;
77 | };
78 |
79 | export type FilterCharacter = {
80 | name?: Maybe;
81 | status?: Maybe;
82 | species?: Maybe;
83 | type?: Maybe;
84 | gender?: Maybe;
85 | };
86 |
87 | export type FilterEpisode = {
88 | name?: Maybe;
89 | episode?: Maybe;
90 | };
91 |
92 | export type FilterLocation = {
93 | name?: Maybe;
94 | type?: Maybe;
95 | dimension?: Maybe;
96 | };
97 |
98 | export type Info = {
99 | __typename?: 'Info';
100 | /** The length of the response. */
101 | count?: Maybe;
102 | /** The amount of pages. */
103 | pages?: Maybe;
104 | /** Number of the next page (if it exists) */
105 | next?: Maybe;
106 | /** Number of the previous page (if it exists) */
107 | prev?: Maybe;
108 | };
109 |
110 | export type Location = {
111 | __typename?: 'Location';
112 | /** The id of the location. */
113 | id?: Maybe;
114 | /** The name of the location. */
115 | name?: Maybe;
116 | /** The type of the location. */
117 | type?: Maybe;
118 | /** The dimension in which the location is located. */
119 | dimension?: Maybe;
120 | /** List of characters who have been last seen in the location. */
121 | residents: Array>;
122 | /** Time at which the location was created in the database. */
123 | created?: Maybe;
124 | };
125 |
126 | export type Locations = {
127 | __typename?: 'Locations';
128 | info?: Maybe;
129 | results?: Maybe>>;
130 | };
131 |
132 | export type Query = {
133 | __typename?: 'Query';
134 | /** Get a specific character by ID */
135 | character?: Maybe;
136 | /** Get the list of all characters */
137 | characters?: Maybe;
138 | /** Get a list of characters selected by ids */
139 | charactersByIds?: Maybe>>;
140 | /** Get a specific locations by ID */
141 | location?: Maybe;
142 | /** Get the list of all locations */
143 | locations?: Maybe;
144 | /** Get a list of locations selected by ids */
145 | locationsByIds?: Maybe>>;
146 | /** Get a specific episode by ID */
147 | episode?: Maybe;
148 | /** Get the list of all episodes */
149 | episodes?: Maybe;
150 | /** Get a list of episodes selected by ids */
151 | episodesByIds?: Maybe>>;
152 | };
153 |
154 |
155 | export type QueryCharacterArgs = {
156 | id: Scalars['ID'];
157 | };
158 |
159 |
160 | export type QueryCharactersArgs = {
161 | page?: Maybe;
162 | filter?: Maybe;
163 | };
164 |
165 |
166 | export type QueryCharactersByIdsArgs = {
167 | ids: Array;
168 | };
169 |
170 |
171 | export type QueryLocationArgs = {
172 | id: Scalars['ID'];
173 | };
174 |
175 |
176 | export type QueryLocationsArgs = {
177 | page?: Maybe;
178 | filter?: Maybe;
179 | };
180 |
181 |
182 | export type QueryLocationsByIdsArgs = {
183 | ids: Array;
184 | };
185 |
186 |
187 | export type QueryEpisodeArgs = {
188 | id: Scalars['ID'];
189 | };
190 |
191 |
192 | export type QueryEpisodesArgs = {
193 | page?: Maybe;
194 | filter?: Maybe;
195 | };
196 |
197 |
198 | export type QueryEpisodesByIdsArgs = {
199 | ids: Array;
200 | };
201 |
202 | export type GetCharactersQueryVariables = Exact<{ [key: string]: never; }>;
203 |
204 |
205 | export type GetCharactersQuery = { __typename?: 'Query', characters?: Maybe<{ __typename?: 'Characters', results?: Maybe, image?: Maybe }>>> }> };
206 |
--------------------------------------------------------------------------------
/src/graphql/queries.ts:
--------------------------------------------------------------------------------
1 | import { gql } from 'graphql-request'
2 |
3 | export const GET_CHARACTERS = gql`
4 | query getCharacters {
5 | characters {
6 | results {
7 | name
8 | image
9 | }
10 | }
11 | }
12 | `
13 |
--------------------------------------------------------------------------------
/src/pages/_app.tsx:
--------------------------------------------------------------------------------
1 | import { AppProps } from 'next/app'
2 | import Head from 'next/head'
3 |
4 | import GlobalStyles from 'styles/global'
5 |
6 | function App({ Component, pageProps }: AppProps) {
7 | return (
8 | <>
9 |
10 | Rick and Morty - React Avançado Boilerplate
11 |
12 |
13 |
14 |
15 |
19 |
20 |
21 |
22 | >
23 | )
24 | }
25 |
26 | export default App
27 |
--------------------------------------------------------------------------------
/src/pages/_document.tsx:
--------------------------------------------------------------------------------
1 | import Document, {
2 | Html,
3 | Head,
4 | Main,
5 | NextScript,
6 | DocumentContext
7 | } from 'next/document'
8 | import { ServerStyleSheet } from 'styled-components'
9 |
10 | export default class MyDocument extends Document {
11 | static async getInitialProps(ctx: DocumentContext) {
12 | const sheet = new ServerStyleSheet()
13 | const originalRenderPage = ctx.renderPage
14 |
15 | try {
16 | ctx.renderPage = () =>
17 | originalRenderPage({
18 | enhanceApp: (App) =>
19 | function enhance(props) {
20 | return sheet.collectStyles()
21 | }
22 | })
23 |
24 | const initialProps = await Document.getInitialProps(ctx)
25 | return {
26 | ...initialProps,
27 | styles: (
28 | <>
29 | {initialProps.styles}
30 | {sheet.getStyleElement()}
31 | >
32 | )
33 | }
34 | } finally {
35 | sheet.seal()
36 | }
37 | }
38 |
39 | render() {
40 | return (
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 | )
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/src/pages/index.tsx:
--------------------------------------------------------------------------------
1 | import Home, { HomeProps } from 'templates/Home'
2 |
3 | import client from 'graphql/client'
4 | import { GetCharactersQuery } from 'graphql/generated/graphql'
5 | import { GET_CHARACTERS } from 'graphql/queries'
6 |
7 | export default function Index({ characters }: HomeProps) {
8 | return
9 | }
10 |
11 | export const getStaticProps = async () => {
12 | const { characters } = await client.request(
13 | GET_CHARACTERS
14 | )
15 |
16 | return {
17 | revalidate: 60,
18 | props: {
19 | characters: characters?.results
20 | }
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/src/styles/global.ts:
--------------------------------------------------------------------------------
1 | import { createGlobalStyle } from 'styled-components'
2 |
3 | const GlobalStyles = createGlobalStyle`
4 | * {
5 | margin: 0;
6 | padding: 0;
7 | box-sizing: border-box;
8 | }
9 |
10 | html {
11 | font-size: 62.5%;
12 | }
13 |
14 | html, body, #__next {
15 | height: 100%;
16 | }
17 |
18 | body {
19 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif
20 | }
21 | `
22 |
23 | export default GlobalStyles
24 |
--------------------------------------------------------------------------------
/src/templates/Home/index.tsx:
--------------------------------------------------------------------------------
1 | import Avatar from 'components/Avatar'
2 | import { Character } from 'graphql/generated/graphql'
3 |
4 | import * as S from './styles'
5 |
6 | export type HomeProps = {
7 | characters?: Pick[]
8 | }
9 |
10 | const Home = ({ characters }: HomeProps) => (
11 |
12 | Rick and Morty Characters
13 |
14 |
15 | {characters?.map(({ name, image }) => (
16 |
17 | ))}
18 |
19 |
20 | )
21 |
22 | export default Home
23 |
--------------------------------------------------------------------------------
/src/templates/Home/styles.ts:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components'
2 |
3 | export const Main = styled.main`
4 | background: #ade1e2;
5 | `
6 |
7 | export const Title = styled.h1`
8 | font-size: 32px;
9 | text-align: center;
10 | padding: 64px 0;
11 | `
12 |
13 | export const Grid = styled.div`
14 | display: grid;
15 | max-width: 1200px;
16 | margin: auto;
17 | grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
18 | grid-gap: 32px;
19 | padding-bottom: 64px;
20 | align-items: center;
21 | `
22 |
--------------------------------------------------------------------------------
/src/templates/Home/test.tsx:
--------------------------------------------------------------------------------
1 | import { render, screen } from '@testing-library/react'
2 |
3 | import Home from '.'
4 |
5 | describe('', () => {
6 | it('should render the heading', () => {
7 | render()
8 |
9 | expect(
10 | screen.getByRole('heading', { name: /Rick and Morty Characters/i })
11 | ).toBeInTheDocument()
12 | })
13 |
14 | it('should render characters', () => {
15 | const characters = [
16 | {
17 | name: 'Ricky',
18 | image: '/ricky.jpg'
19 | },
20 | {
21 | name: 'Morty',
22 | image: '/morty.jpg'
23 | }
24 | ]
25 |
26 | render()
27 |
28 | expect(screen.getAllByRole('img')).toHaveLength(2)
29 | })
30 | })
31 |
--------------------------------------------------------------------------------
/src/types/jest-styled-components.d.ts:
--------------------------------------------------------------------------------
1 | // Types provided from the official repo:
2 | // https://github.com/styled-components/jest-styled-components/blob/master/typings/index.d.ts
3 |
4 | /* eslint-disable @typescript-eslint/no-explicit-any */
5 | /* eslint-disable @typescript-eslint/ban-types */
6 | import { Plugin, NewPlugin } from 'pretty-format'
7 |
8 | declare global {
9 | namespace jest {
10 | interface AsymmetricMatcher {
11 | $$typeof: symbol
12 | sample?: string | RegExp | object | Array | Function
13 | }
14 |
15 | type Value = string | number | RegExp | AsymmetricMatcher | undefined
16 |
17 | interface Options {
18 | media?: string
19 | modifier?: string
20 | supports?: string
21 | }
22 |
23 | // eslint-disable-next-line @typescript-eslint/no-unused-vars
24 | interface Matchers {
25 | toHaveStyleRule(property: string, value?: Value, options?: Options): R
26 | }
27 | }
28 | }
29 |
30 | export declare const styleSheetSerializer: Exclude
31 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "baseUrl": "src",
4 | "target": "es5",
5 | "lib": [
6 | "dom",
7 | "dom.iterable",
8 | "esnext"
9 | ],
10 | "allowJs": true,
11 | "skipLibCheck": true,
12 | "strict": true,
13 | "forceConsistentCasingInFileNames": true,
14 | "noEmit": true,
15 | "esModuleInterop": true,
16 | "module": "esnext",
17 | "moduleResolution": "node",
18 | "resolveJsonModule": true,
19 | "isolatedModules": true,
20 | "jsx": "preserve",
21 | "incremental": true
22 | },
23 | "exclude": [
24 | "node_modules"
25 | ],
26 | "include": [
27 | "next-env.d.ts",
28 | "**/*.ts",
29 | "**/*.tsx"
30 | ]
31 | }
32 |
--------------------------------------------------------------------------------