├── .editorconfig ├── .eslintignore ├── .eslintrc.json ├── .gitattributes ├── .gitignore ├── .husky ├── .gitignore └── pre-commit ├── .prettierignore ├── .prettierrc ├── .vscode └── settings.json ├── README.md ├── jest.config.js ├── jest.setup.js ├── next-env.d.ts ├── package.json ├── public ├── favicon.ico └── vercel.svg ├── src ├── components │ ├── Button.tsx │ ├── Header.tsx │ ├── Page.tsx │ ├── assets │ │ ├── code-brackets.svg │ │ ├── colors.svg │ │ ├── comments.svg │ │ ├── direction.svg │ │ ├── flow.svg │ │ ├── plugin.svg │ │ ├── repo.svg │ │ └── stackalt.svg │ ├── button.css │ ├── header.css │ └── page.css └── pages │ ├── api │ └── hello.ts │ └── index.tsx ├── tests ├── __mocks__ │ └── fileMock.js ├── pages │ ├── __snapshots__ │ │ └── index.test.tsx.snap │ └── index.test.tsx └── testUtils.ts ├── tsconfig.json └── yarn.lock /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | indent_size = 2 6 | indent_style = space 7 | insert_final_newline = true 8 | max_line_length = 120 9 | trim_trailing_whitespace = true 10 | 11 | [COMMIT_EDITMSG] 12 | max_line_length = 72 13 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | **/node_modules/* 2 | **/out/* 3 | **/.next/* 4 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["next", "prettier"], 3 | "rules": { 4 | "react/react-in-jsx-scope": 0, 5 | "react/display-name": 0, 6 | "react/prop-types": 0, 7 | "explicit-function-return-type": 0, 8 | "explicit-member-accessibility": 0, 9 | "indent": 0, 10 | "member-delimiter-style": 0, 11 | "no-explicit-any": 0, 12 | "no-var-requires": 0, 13 | "no-use-before-define": 0, 14 | "no-unused-vars": [ 15 | 2, 16 | { 17 | "argsIgnorePattern": "^_" 18 | } 19 | ], 20 | "no-console": [ 21 | 2, 22 | { 23 | "allow": ["warn", "error"] 24 | } 25 | ] 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # This file has been generated using '.gitattributes Generator'. 2 | # You can generate yours at http://ihopepeace.github.io/gitattributes_generator 3 | 4 | * text=auto 5 | 6 | # Explicitly declare text files that should be normalized and converted 7 | # to native line endings on checkout. 8 | *.js text 9 | *.json text 10 | *.jsx text 11 | *.md text 12 | *.svg text 13 | 14 | # Declare files that should have CRLF line endings on checkout. 15 | 16 | # Declare files that should have LF line endings on checkout. 17 | *.ts eol=lf 18 | *.tsx eol=lf 19 | 20 | # Declare files that are truly binary and shouldn't be modified. 21 | *.ico binary 22 | *.jpeg binary 23 | *.jpg binary 24 | *.png binary 25 | -------------------------------------------------------------------------------- /.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 | *.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 35 | -------------------------------------------------------------------------------- /.husky/.gitignore: -------------------------------------------------------------------------------- 1 | _ 2 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | yarn lint-staged --verbose 5 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .next 3 | yarn.lock 4 | package-lock.json 5 | public 6 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "arrowParens": "avoid", 3 | "printWidth": 120, 4 | "semi": false, 5 | "singleQuote": true, 6 | "tabWidth": 2, 7 | "trailingComma": "es5" 8 | } 9 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "css.validate": false, 3 | "editor.defaultFormatter": "esbenp.prettier-vscode", 4 | "editor.formatOnSave": true, 5 | "editor.tabSize": 2, 6 | "eslint.validate": ["typescript", "typescriptreact"], 7 | "eslint.workingDirectories": ["src", "tests"], 8 | "eslint.alwaysShowStatus": true, 9 | "typescript.updateImportsOnFileMove.enabled": "always", 10 | "editor.codeActionsOnSave": { 11 | "source.fixAll": true 12 | }, 13 | "[json]": { 14 | "editor.defaultFormatter": "esbenp.prettier-vscode", 15 | "editor.formatOnSave": true 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # A strongly-opinionated NextJS Template 2 | 3 | This is an extended version of the official template, [`with-typescript-eslint-jest`](https://github.com/vercel/next.js/tree/canary/examples/with-typescript-eslint-jest). 4 | 5 | ## 🖥 Technology Stack 6 | 7 | - [Typescript](https://github.com/microsoft/TypeScript) 8 | - Formatting with [Prettier](https://github.com/prettier/prettier) 9 | - Linting with [ESLint](https://github.com/eslint/eslint) 10 | - Unit testing with [Jest](https://github.com/facebook/jest) and [React Testing Library](https://github.com/testing-library/react-testing-library) 11 | - Commit hooks using [Husky](https://github.com/typicode/husky): linting, type-checking, and formatting 12 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | const nextJest = require('next/jest') 2 | 3 | const createJestConfig = nextJest({ 4 | // Provide the path to your Next.js app to load next.config.js and .env files in your test environment 5 | dir: './', 6 | }) 7 | 8 | // Add any custom config to be passed to Jest 9 | const customJestConfig = { 10 | // Add more setup options before each test is run 11 | // setupFilesAfterEnv: ['/jest.setup.js'], 12 | // if using TypeScript with a baseUrl set to the root directory then you need the below for alias' to work 13 | moduleDirectories: ['node_modules', '/'], 14 | testEnvironment: 'jest-environment-jsdom', 15 | } 16 | 17 | // createJestConfig is exported this way to ensure that next/jest can load the Next.js config which is async 18 | module.exports = createJestConfig(customJestConfig) 19 | -------------------------------------------------------------------------------- /jest.setup.js: -------------------------------------------------------------------------------- 1 | // Optional: configure or set up a testing framework before each test. 2 | // If you delete this file, remove `setupFilesAfterEnv` from `jest.config.js` 3 | 4 | // Used for __tests__/testing-library.js 5 | // Learn more: https://github.com/testing-library/jest-dom 6 | import '@testing-library/jest-dom/extend-expect' 7 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "with-typescript-eslint-jest", 3 | "author": { 4 | "email": "github@maacpiash.com", 5 | "name": "Ahad Chowdhury", 6 | "url": "https://www.maacpiash.com" 7 | }, 8 | "license": "MIT", 9 | "version": "1.0.0", 10 | "scripts": { 11 | "build": "next build", 12 | "clean": "rimraf out", 13 | "dev": "next", 14 | "export": "next export", 15 | "format": "prettier --write .", 16 | "lint": "next lint", 17 | "start": "next start", 18 | "test": "jest", 19 | "type-check": "tsc --pretty --noEmit" 20 | }, 21 | "lint-staged": { 22 | "*.@(ts|tsx)": [ 23 | "yarn format", 24 | "yarn lint", 25 | "yarn type-check" 26 | ] 27 | }, 28 | "dependencies": { 29 | "next": "^12.1.5", 30 | "react": "^18.0.0", 31 | "react-dom": "^18.0.0" 32 | }, 33 | "devDependencies": { 34 | "@testing-library/jest-dom": "^5.16.4", 35 | "@testing-library/react": "^13.1.1", 36 | "@types/jest": "^26.0.23", 37 | "@types/node": "^15.12.4", 38 | "@types/react": "^17.0.11", 39 | "eslint": "^8.13.0", 40 | "eslint-config-next": "^12.1.5", 41 | "eslint-config-prettier": "^8.5.0", 42 | "husky": "^6.0.0", 43 | "jest": "^27.5.1", 44 | "lint-staged": "^11.0.0", 45 | "prettier": "^2.3.1", 46 | "typescript": "^4.3.4" 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maacpiash/nextjs-template/be5d12a1e08c7d4babea1b4194b173ef3993ee83/public/favicon.ico -------------------------------------------------------------------------------- /public/vercel.svg: -------------------------------------------------------------------------------- 1 | 3 | 4 | -------------------------------------------------------------------------------- /src/components/Button.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import './button.css' 3 | 4 | export interface ButtonProps { 5 | /** 6 | * Is this the principal call to action on the page? 7 | */ 8 | primary?: boolean 9 | /** 10 | * What background color to use 11 | */ 12 | backgroundColor?: string 13 | /** 14 | * How large should the button be? 15 | */ 16 | size?: 'small' | 'medium' | 'large' 17 | /** 18 | * Button contents 19 | */ 20 | label: string 21 | /** 22 | * Optional click handler 23 | */ 24 | onClick?: () => void 25 | } 26 | 27 | /** 28 | * Primary UI component for user interaction 29 | */ 30 | export const Button: React.FC = ({ 31 | primary = false, 32 | size = 'medium', 33 | backgroundColor, 34 | label, 35 | ...props 36 | }) => { 37 | const mode = primary ? 'storybook-button--primary' : 'storybook-button--secondary' 38 | return ( 39 | 47 | ) 48 | } 49 | -------------------------------------------------------------------------------- /src/components/Header.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | import { Button } from './Button' 4 | import './header.css' 5 | 6 | export interface HeaderProps { 7 | user?: {} 8 | onLogin: () => void 9 | onLogout: () => void 10 | onCreateAccount: () => void 11 | } 12 | 13 | export const Header: React.FC = ({ user, onLogin, onLogout, onCreateAccount }) => ( 14 |
15 |
16 |
17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 |

Acme

25 |
26 |
27 | {user ? ( 28 |
36 |
37 |
38 | ) 39 | -------------------------------------------------------------------------------- /src/components/Page.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | import { Header } from './Header' 4 | import './page.css' 5 | 6 | export interface PageProps { 7 | user?: {} 8 | onLogin: () => void 9 | onLogout: () => void 10 | onCreateAccount: () => void 11 | } 12 | 13 | export const Page: React.FC = ({ user, onLogin, onLogout, onCreateAccount }) => ( 14 |
15 |
16 | 17 |
18 |

Pages in Storybook

19 |

20 | We recommend building UIs with a{' '} 21 | 22 | component-driven 23 | {' '} 24 | process starting with atomic components and ending with pages. 25 |

26 |

27 | Render pages with mock data. This makes it easy to build and review page states without needing to navigate to 28 | them in your app. Here are some handy patterns for managing page data in Storybook: 29 |

30 |
    31 |
  • 32 | Use a higher-level connected component. Storybook helps you compose such data from the "args" of child 33 | component stories 34 |
  • 35 |
  • 36 | Assemble data in the page component from your services. You can mock these services out using Storybook. 37 |
  • 38 |
39 |

40 | Get a guided tutorial on component-driven development at{' '} 41 | 42 | Storybook tutorials 43 | 44 | . Read more in the{' '} 45 | 46 | docs 47 | 48 | . 49 |

50 |
51 | Tip Adjust the width of the canvas with the{' '} 52 | 53 | 54 | 59 | 60 | 61 | Viewports addon in the toolbar 62 |
63 |
64 |
65 | ) 66 | -------------------------------------------------------------------------------- /src/components/assets/code-brackets.svg: -------------------------------------------------------------------------------- 1 | illustration/code-brackets -------------------------------------------------------------------------------- /src/components/assets/colors.svg: -------------------------------------------------------------------------------- 1 | illustration/colors -------------------------------------------------------------------------------- /src/components/assets/comments.svg: -------------------------------------------------------------------------------- 1 | illustration/comments -------------------------------------------------------------------------------- /src/components/assets/direction.svg: -------------------------------------------------------------------------------- 1 | illustration/direction -------------------------------------------------------------------------------- /src/components/assets/flow.svg: -------------------------------------------------------------------------------- 1 | illustration/flow -------------------------------------------------------------------------------- /src/components/assets/plugin.svg: -------------------------------------------------------------------------------- 1 | illustration/plugin -------------------------------------------------------------------------------- /src/components/assets/repo.svg: -------------------------------------------------------------------------------- 1 | illustration/repo -------------------------------------------------------------------------------- /src/components/assets/stackalt.svg: -------------------------------------------------------------------------------- 1 | illustration/stackalt -------------------------------------------------------------------------------- /src/components/button.css: -------------------------------------------------------------------------------- 1 | .storybook-button { 2 | font-family: 'Nunito Sans', 'Helvetica Neue', Helvetica, Arial, sans-serif; 3 | font-weight: 700; 4 | border: 0; 5 | border-radius: 3em; 6 | cursor: pointer; 7 | display: inline-block; 8 | line-height: 1; 9 | } 10 | .storybook-button--primary { 11 | color: white; 12 | background-color: #1ea7fd; 13 | } 14 | .storybook-button--secondary { 15 | color: #333; 16 | background-color: transparent; 17 | box-shadow: rgba(0, 0, 0, 0.15) 0px 0px 0px 1px inset; 18 | } 19 | .storybook-button--small { 20 | font-size: 12px; 21 | padding: 10px 16px; 22 | } 23 | .storybook-button--medium { 24 | font-size: 14px; 25 | padding: 11px 20px; 26 | } 27 | .storybook-button--large { 28 | font-size: 16px; 29 | padding: 12px 24px; 30 | } 31 | -------------------------------------------------------------------------------- /src/components/header.css: -------------------------------------------------------------------------------- 1 | .wrapper { 2 | font-family: 'Nunito Sans', 'Helvetica Neue', Helvetica, Arial, sans-serif; 3 | border-bottom: 1px solid rgba(0, 0, 0, 0.1); 4 | padding: 15px 20px; 5 | display: flex; 6 | align-items: center; 7 | justify-content: space-between; 8 | } 9 | 10 | svg { 11 | display: inline-block; 12 | vertical-align: top; 13 | } 14 | 15 | h1 { 16 | font-weight: 900; 17 | font-size: 20px; 18 | line-height: 1; 19 | margin: 6px 0 6px 10px; 20 | display: inline-block; 21 | vertical-align: top; 22 | } 23 | 24 | button + button { 25 | margin-left: 10px; 26 | } 27 | -------------------------------------------------------------------------------- /src/components/page.css: -------------------------------------------------------------------------------- 1 | section { 2 | font-family: 'Nunito Sans', 'Helvetica Neue', Helvetica, Arial, sans-serif; 3 | font-size: 14px; 4 | line-height: 24px; 5 | padding: 48px 20px; 6 | margin: 0 auto; 7 | max-width: 600px; 8 | color: #333; 9 | } 10 | 11 | h2 { 12 | font-weight: 900; 13 | font-size: 32px; 14 | line-height: 1; 15 | margin: 0 0 4px; 16 | display: inline-block; 17 | vertical-align: top; 18 | } 19 | 20 | p { 21 | margin: 1em 0; 22 | } 23 | 24 | a { 25 | text-decoration: none; 26 | color: #1ea7fd; 27 | } 28 | 29 | ul { 30 | padding-left: 30px; 31 | margin: 1em 0; 32 | } 33 | 34 | li { 35 | margin-bottom: 8px; 36 | } 37 | 38 | .tip { 39 | display: inline-block; 40 | border-radius: 1em; 41 | font-size: 11px; 42 | line-height: 12px; 43 | font-weight: 700; 44 | background: #e7fdd8; 45 | color: #66bf3c; 46 | padding: 4px 12px; 47 | margin-right: 10px; 48 | vertical-align: top; 49 | } 50 | 51 | .tip-wrapper { 52 | font-size: 13px; 53 | line-height: 20px; 54 | margin-top: 40px; 55 | margin-bottom: 40px; 56 | } 57 | 58 | .tip-wrapper svg { 59 | display: inline-block; 60 | height: 12px; 61 | width: 12px; 62 | margin-right: 4px; 63 | vertical-align: top; 64 | margin-top: 3px; 65 | } 66 | 67 | .tip-wrapper svg path { 68 | fill: #1ea7fd; 69 | } 70 | -------------------------------------------------------------------------------- /src/pages/api/hello.ts: -------------------------------------------------------------------------------- 1 | // Next.js API route support: https://nextjs.org/docs/api-routes/introduction 2 | 3 | import { NextApiRequest, NextApiResponse } from 'next' 4 | 5 | const handler = (req: NextApiRequest, res: NextApiResponse) => { 6 | res.status(200).json({ name: 'John Doe' }) 7 | } 8 | 9 | export default handler 10 | -------------------------------------------------------------------------------- /src/pages/index.tsx: -------------------------------------------------------------------------------- 1 | import Head from 'next/head' 2 | import Image from 'next/image' 3 | 4 | export const Home = (): JSX.Element => ( 5 |
6 | 7 | Create Next App 8 | 9 | 10 | 11 |
12 |

13 | Welcome to Next.js! 14 |

15 | 16 |

17 | Get started by editing pages/index.tsx 18 |

19 | 20 | 27 | 28 | 52 |
53 | 54 | 63 | 64 | 190 | 191 | 204 |
205 | ) 206 | 207 | export default Home 208 | -------------------------------------------------------------------------------- /tests/__mocks__/fileMock.js: -------------------------------------------------------------------------------- 1 | module.exports = 'test-file-stub' 2 | -------------------------------------------------------------------------------- /tests/pages/__snapshots__/index.test.tsx.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`Home page matches snapshot 1`] = ` 4 | 5 |
8 |
9 |

12 | Welcome to 13 | 16 | Next.js! 17 | 18 |

19 |

22 | Get started by editing 23 | 24 | pages/index.tsx 25 | 26 |

27 | 30 | 78 |
79 | 110 | 238 | 253 |
254 |
255 | `; 256 | -------------------------------------------------------------------------------- /tests/pages/index.test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { render, fireEvent } from '../testUtils' 3 | import { Home } from '../../src/pages' 4 | 5 | describe('Home page', () => { 6 | it('matches snapshot', () => { 7 | const { asFragment } = render(, {}) 8 | expect(asFragment()).toMatchSnapshot() 9 | }) 10 | 11 | it('clicking button triggers alert', () => { 12 | const { getByText } = render(, {}) 13 | window.alert = jest.fn() 14 | fireEvent.click(getByText('Test Button')) 15 | expect(window.alert).toHaveBeenCalledWith('With typescript and Jest') 16 | }) 17 | }) 18 | -------------------------------------------------------------------------------- /tests/testUtils.ts: -------------------------------------------------------------------------------- 1 | import { render } from '@testing-library/react' 2 | // import { ThemeProvider } from "my-ui-lib" 3 | // import { TranslationProvider } from "my-i18n-lib" 4 | // import defaultStrings from "i18n/en-x-default" 5 | 6 | const Providers = ({ children }) => { 7 | return children 8 | // return ( 9 | // 10 | // 11 | // {children} 12 | // 13 | // 14 | // ) 15 | } 16 | 17 | const customRender = (ui, options = {}) => render(ui, { wrapper: Providers, ...options }) 18 | 19 | // re-export everything 20 | export * from '@testing-library/react' 21 | 22 | // override render method 23 | export { customRender as render } 24 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "strict": false, 8 | "forceConsistentCasingInFileNames": true, 9 | "noEmit": true, 10 | "esModuleInterop": true, 11 | "module": "esnext", 12 | "moduleResolution": "node", 13 | "resolveJsonModule": true, 14 | "isolatedModules": true, 15 | "jsx": "preserve" 16 | }, 17 | "exclude": ["node_modules", ".next", "out"], 18 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", "**/*.js"] 19 | } 20 | --------------------------------------------------------------------------------