├── .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 }) => {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 | ![React Avançado](https://raw.githubusercontent.com/React-Avancado/boilerplate/master/public/img/logo-gh.svg) 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 | {name 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 | --------------------------------------------------------------------------------