├── .babelrc ├── .eslintignore ├── .eslintrc ├── .gitignore ├── .prettierignore ├── .prettierrc ├── LICENSE ├── README.md ├── jest.config.js ├── jest.setup.js ├── next-env.d.ts ├── next.config.js ├── package-lock.json ├── package.json ├── postcss.config.js ├── src ├── components │ ├── Button.test.tsx │ ├── Button.tsx │ ├── Card.test.tsx │ ├── Card.tsx │ ├── Dropdown.test.tsx │ ├── Dropdown.tsx │ ├── Input.test.tsx │ ├── Input.tsx │ ├── List.test.tsx │ ├── List.tsx │ ├── Text.test.tsx │ ├── Text.tsx │ ├── __snapshots__ │ │ ├── Button.test.tsx.snap │ │ ├── Card.test.tsx.snap │ │ ├── Dropdown.test.tsx.snap │ │ ├── Input.test.tsx.snap │ │ ├── List.test.tsx.snap │ │ └── Text.test.tsx.snap │ └── layout │ │ ├── AppHeader.test.tsx │ │ ├── AppHeader.tsx │ │ ├── Layout.test.tsx │ │ ├── Layout.tsx │ │ ├── LayoutMDX.test.tsx │ │ ├── LayoutMDX.tsx │ │ ├── ThemeSwitch.test.tsx │ │ ├── ThemeSwitch.tsx │ │ └── __snapshots__ │ │ ├── AppHeader.test.tsx.snap │ │ ├── Layout.test.tsx.snap │ │ ├── LayoutMDX.test.tsx.snap │ │ └── ThemeSwitch.test.tsx.snap ├── pages │ ├── _app.tsx │ ├── _document.tsx │ ├── components.tsx │ ├── index.tsx │ └── typography.mdx ├── styles │ ├── main.css │ └── nprogress.css └── utils │ └── routes.ts ├── tailwind.config.js └── tsconfig.json /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["next/babel"] 3 | } 4 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | assets 2 | __snapshots__ 3 | styles 4 | .next 5 | node_modules 6 | *.mdx -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "@typescript-eslint/parser", 3 | "extends": [ 4 | "plugin:@typescript-eslint/recommended", 5 | "prettier/@typescript-eslint", 6 | "react-app", 7 | "plugin:prettier/recommended" 8 | ], 9 | "plugins": ["@typescript-eslint", "react"], 10 | "rules": { 11 | "@typescript-eslint/explicit-function-return-type": "off", 12 | "@typescript-eslint/explicit-module-boundary-types": "off", 13 | "jsx-a11y/anchor-is-valid": "off" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # Bower dependency directory (https://bower.io/) 27 | bower_components 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (https://nodejs.org/api/addons.html) 33 | build/Release 34 | 35 | # Dependency directories 36 | node_modules/ 37 | jspm_packages/ 38 | 39 | # TypeScript v1 declaration files 40 | typings/ 41 | 42 | # Optional npm cache directory 43 | .npm 44 | 45 | # Optional eslint cache 46 | .eslintcache 47 | 48 | # Optional REPL history 49 | .node_repl_history 50 | 51 | # Output of 'npm pack' 52 | *.tgz 53 | 54 | # Yarn Integrity file 55 | .yarn-integrity 56 | 57 | # dotenv environment variables file 58 | .env 59 | 60 | # next.js build output 61 | .next 62 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .next 3 | assets 4 | __snapshots__ 5 | styles 6 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "es5", 3 | "tabWidth": 2, 4 | "semi": false, 5 | "singleQuote": true 6 | } 7 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Lailo 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # NextJS with Tailwind CSS and TypeScript 2 | 3 | This is a demo project to show how to setup [NextJS](https://nextjs.org) with [TypeScript](https://www.typescriptlang.org/), [TailwindCSS](https://tailwindcss.com) and [Jest](https://jestjs.io). 4 | 5 | It shows how you can use a utility-first CSS framework with snapshot testing to have nice tests with different component stats (loading, disabled, active, ...). 6 | 7 | To reduce the CSS bundle size of TailwindCSS, I added [PurgeCSS](https://www.purgecss.com/). 8 | 9 | ## Development 10 | 11 | 1. `npm install` 12 | 2. `npm run dev` 13 | 14 | ## Testing 15 | 16 | `npm test` 17 | 18 | ## Production 19 | 20 | 1. `npm run build` 21 | 2. `npm run start` 22 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | const TEST_REGEX = '(/__tests__/.*|(\\.|/)(test|spec))\\.(jsx?|js?|tsx?|ts?)$' 2 | 3 | module.exports = { 4 | setupFiles: ['/jest.setup.js'], 5 | testRegex: TEST_REGEX, 6 | moduleNameMapper: { 7 | '@components(.*)$': '/src/components$1', 8 | '@pages(.*)$': '/src/pages$1', 9 | '@styles(.*)$': '/src/styles$1', 10 | '@utils(.*)$': '/src/utils$1', 11 | }, 12 | transform: { 13 | '^.+\\.tsx?$': 'babel-jest', 14 | '.+\\.(css|styl|less|sass|scss|png|jpg|ttf|woff|woff2)$': 15 | 'jest-transform-stub', 16 | }, 17 | testPathIgnorePatterns: ['/.next/', '/node_modules/'], 18 | moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx'], 19 | collectCoverage: false, 20 | snapshotSerializers: ['enzyme-to-json/serializer'], 21 | } 22 | -------------------------------------------------------------------------------- /jest.setup.js: -------------------------------------------------------------------------------- 1 | const Enzyme = require('enzyme') 2 | const Adapter = require('enzyme-adapter-react-16') 3 | 4 | Enzyme.configure({ adapter: new Adapter() }) 5 | -------------------------------------------------------------------------------- /next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | -------------------------------------------------------------------------------- /next.config.js: -------------------------------------------------------------------------------- 1 | const withMDX = require('@next/mdx')({ 2 | extension: /\.(md|mdx)$/, 3 | }) 4 | 5 | module.exports = withMDX({ 6 | pageExtensions: ['ts', 'tsx', 'mdx'], 7 | }) 8 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "next-with-tailwindcss", 3 | "version": "1.0.0", 4 | "description": "NextJS with Tailwind CSS", 5 | "scripts": { 6 | "dev": "next", 7 | "build": "next build", 8 | "start": "next start", 9 | "test": "npm run test:format && npm run test:lint && npm run test:unit", 10 | "test:unit": "jest", 11 | "test:unit:watch": "jest --watch", 12 | "test:format": "prettier --check .", 13 | "test:tsc": "tsc --noEmit", 14 | "test:lint": "eslint './src/**/*' --ext .tsx,.ts" 15 | }, 16 | "husky": { 17 | "hooks": { 18 | "pre-commit": "npm run test:tsc && npm run test:lint", 19 | "pre-push": "npm run test" 20 | } 21 | }, 22 | "repository": { 23 | "type": "git", 24 | "url": "git+https://github.com/lailo-ch/next-with-tailwindcss.git" 25 | }, 26 | "author": "Lailo", 27 | "license": "MIT", 28 | "bugs": { 29 | "url": "https://github.com/lailo-ch/next-with-tailwindcss/issues" 30 | }, 31 | "homepage": "https://github.com/lailo-ch/next-with-tailwindcss#readme", 32 | "dependencies": { 33 | "clsx": "^1.1.1", 34 | "next": "^9.4.4", 35 | "nprogress": "^0.2.0", 36 | "react": "^16.13.1", 37 | "react-dom": "^16.13.1", 38 | "react-icons": "^3.10.0", 39 | "react-switch": "^5.0.1", 40 | "use-dark-mode": "^2.3.1" 41 | }, 42 | "devDependencies": { 43 | "@mdx-js/loader": "^1.6.5", 44 | "@next/mdx": "^9.4.4", 45 | "@types/jest": "^25.2.3", 46 | "@types/node": "^14.0.9", 47 | "@types/react": "^16.9.35", 48 | "@typescript-eslint/eslint-plugin": "^3.1.0", 49 | "@typescript-eslint/parser": "^3.1.0", 50 | "babel-core": "^6.26.3", 51 | "babel-eslint": "^10.1.0", 52 | "babel-jest": "^26.0.1", 53 | "enzyme": "^3.11.0", 54 | "enzyme-adapter-react-16": "^1.15.2", 55 | "enzyme-to-json": "^3.5.0", 56 | "eslint": "^7.1.0", 57 | "eslint-config-prettier": "^6.11.0", 58 | "eslint-config-react-app": "^5.2.1", 59 | "eslint-plugin-flowtype": "^5", 60 | "eslint-plugin-import": "^2.20.2", 61 | "eslint-plugin-jsx-a11y": "^6.2.3", 62 | "eslint-plugin-prettier": "^3.1.3", 63 | "eslint-plugin-react": "^7.20.0", 64 | "eslint-plugin-react-hooks": "^4.0.4", 65 | "husky": "^4.2.5", 66 | "jest": "^26.0.1", 67 | "jest-transform-stub": "^2.0.0", 68 | "postcss-preset-env": "^6.7.0", 69 | "prettier": "2.0.5", 70 | "react-addons-test-utils": "^15.6.2", 71 | "react-test-renderer": "^16.13.1", 72 | "tailwindcss": "^1.4.6", 73 | "tailwindcss-dark-mode": "^1.1.4", 74 | "typescript": "^3.9.3" 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: ['tailwindcss', 'postcss-preset-env'], 3 | } 4 | -------------------------------------------------------------------------------- /src/components/Button.test.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import { shallow } from 'enzyme' 3 | import toJson from 'enzyme-to-json' 4 | 5 | import Button from './Button' 6 | 7 | describe('Button', () => { 8 | it('renders with correct label', () => { 9 | const wrapper = shallow() 10 | expect(wrapper.text()).toEqual('Hello') 11 | }) 12 | 13 | it('matches primary (default) type snapshot', () => { 14 | const wrapper = shallow() 15 | expect(toJson(wrapper)).toMatchSnapshot() 16 | }) 17 | 18 | it('matches secondary type snapshot', () => { 19 | const wrapper = shallow() 20 | expect(toJson(wrapper)).toMatchSnapshot() 21 | }) 22 | 23 | it('matches with external className as string', () => { 24 | const wrapper = shallow( 25 | 26 | ) 27 | expect(toJson(wrapper)).toMatchSnapshot() 28 | }) 29 | 30 | it('matches with external className as object', () => { 31 | const wrapper = shallow( 32 | 40 | ) 41 | expect(toJson(wrapper)).toMatchSnapshot() 42 | }) 43 | 44 | it('matches disabled state', () => { 45 | const wrapper = shallow() 46 | expect(toJson(wrapper)).toMatchSnapshot() 47 | }) 48 | 49 | it('matches loading state', () => { 50 | const wrapper = shallow() 51 | expect(toJson(wrapper)).toMatchSnapshot() 52 | }) 53 | }) 54 | -------------------------------------------------------------------------------- /src/components/Button.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import clsx from 'clsx' 3 | 4 | export interface Props { 5 | type?: 'primary' | 'secondary' 6 | className?: string | Record 7 | loading?: boolean 8 | } 9 | 10 | const Button: React.FC> = ({ 11 | children, 12 | className, 13 | type = 'primary', 14 | loading = false, 15 | ...props 16 | }) => { 17 | const disabled = props.disabled || loading 18 | 19 | const mergedClassName = clsx( 20 | 'px-4 py-2', 21 | { 'text-white': !disabled, 'text-gray-500 dark:text-gray-700': disabled }, 22 | { 23 | 'bg-primary hover:bg-primary-darker': !disabled && type === 'primary', 24 | 'bg-secondary hover:bg-secondary-darker': 25 | !disabled && type === 'secondary', 26 | 'bg-gray-300 dark:bg-gray-800': disabled, 27 | }, 28 | { 29 | 'cursor-not-allowed': disabled && !loading, 30 | 'cursor-wait': loading, 31 | }, 32 | className 33 | ) 34 | 35 | return ( 36 | 40 | ) 41 | } 42 | 43 | export default Button 44 | -------------------------------------------------------------------------------- /src/components/Card.test.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import { shallow } from 'enzyme' 3 | import toJson from 'enzyme-to-json' 4 | import Card from './Card' 5 | 6 | describe('Card', () => { 7 | it('matches snapshot', () => { 8 | const wrapper = shallow( 9 | 10 |

hello world

11 |
12 | ) 13 | expect(toJson(wrapper)).toMatchSnapshot() 14 | }) 15 | }) 16 | -------------------------------------------------------------------------------- /src/components/Card.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import clsx from 'clsx' 3 | 4 | export interface Props { 5 | className?: string | Record 6 | } 7 | 8 | const Card: React.FC = ({ className, children }) => { 9 | return ( 10 |
19 | {children} 20 |
21 | ) 22 | } 23 | 24 | export default Card 25 | -------------------------------------------------------------------------------- /src/components/Dropdown.test.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import { shallow } from 'enzyme' 3 | import toJson from 'enzyme-to-json' 4 | import Dropdown from './Dropdown' 5 | 6 | describe('Dropdown', () => { 7 | it('matches snapshot', () => { 8 | const wrapper = shallow( 9 | 10 |
    11 |
  • hello
  • 12 |
  • world
  • 13 |
14 |
15 | ) 16 | expect(toJson(wrapper)).toMatchSnapshot() 17 | }) 18 | }) 19 | -------------------------------------------------------------------------------- /src/components/Dropdown.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import clsx from 'clsx' 3 | 4 | export interface Props { 5 | className?: string | Record 6 | buttonLabel: string | React.ReactElement 7 | } 8 | 9 | const Dropdown: React.FC = ({ className, buttonLabel, children }) => { 10 | const node = React.useRef() 11 | const [showDropdown, setShowDropdown] = React.useState(false) 12 | 13 | const toggleDropdown = () => { 14 | setShowDropdown(!showDropdown) 15 | } 16 | 17 | const handleClickOutside = (e: MouseEvent) => { 18 | if (node?.current?.contains(e.target as Node)) { 19 | return 20 | } 21 | setShowDropdown(false) 22 | } 23 | 24 | React.useEffect(() => { 25 | document.addEventListener('mousedown', handleClickOutside) 26 | return () => document.removeEventListener('mousedown', handleClickOutside) 27 | }, []) 28 | 29 | return ( 30 |
31 | 40 | {showDropdown && ( 41 |
42 | {children} 43 |
44 | )} 45 |
46 | ) 47 | } 48 | 49 | export default Dropdown 50 | -------------------------------------------------------------------------------- /src/components/Input.test.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import { shallow } from 'enzyme' 3 | import toJson from 'enzyme-to-json' 4 | import { FiMail } from 'react-icons/fi' 5 | 6 | import Input from './Input' 7 | 8 | describe('Input', () => { 9 | it('renders with correct label', () => { 10 | const wrapper = shallow() 11 | expect(wrapper.find('label').first().text()).toEqual('Hello') 12 | }) 13 | 14 | it('renders with value', () => { 15 | const wrapper = shallow() 16 | expect(wrapper.find('input').props().value).toEqual('Hello') 17 | }) 18 | 19 | it('matches default snapshot', () => { 20 | const wrapper = shallow() 21 | expect(toJson(wrapper)).toMatchSnapshot() 22 | }) 23 | 24 | it('matches with icon snapshot', () => { 25 | const wrapper = shallow() 26 | expect(toJson(wrapper)).toMatchSnapshot() 27 | }) 28 | 29 | it('matches disabled snapshot', () => { 30 | const wrapper = shallow() 31 | expect(toJson(wrapper)).toMatchSnapshot() 32 | }) 33 | 34 | it('matches with error snapshot', () => { 35 | const wrapper = shallow( 36 | 37 | ) 38 | expect(toJson(wrapper)).toMatchSnapshot() 39 | }) 40 | 41 | it('matches custom class names snapshot', () => { 42 | const wrapper = shallow() 43 | expect(toJson(wrapper)).toMatchSnapshot() 44 | }) 45 | 46 | it('matches with help text', () => { 47 | const wrapper = shallow() 48 | expect(toJson(wrapper)).toMatchSnapshot() 49 | }) 50 | }) 51 | -------------------------------------------------------------------------------- /src/components/Input.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import clsx from 'clsx' 3 | import { IconType } from 'react-icons/lib/cjs' 4 | 5 | export interface Props { 6 | className?: string | Record 7 | startIcon?: IconType 8 | label?: string 9 | helpText?: string 10 | errorMessage?: string 11 | } 12 | 13 | const Input: React.FC> = ({ 14 | className, 15 | startIcon: StartIcon, 16 | label, 17 | helpText, 18 | errorMessage, 19 | ...props 20 | }) => { 21 | return ( 22 |
23 | {label && ( 24 | 33 | )} 34 |
49 | {StartIcon && ( 50 | 51 | )} 52 | 59 |
60 | {helpText && ( 61 |
62 | {helpText} 63 |
64 | )} 65 | {errorMessage && ( 66 |
67 | {errorMessage} 68 |
69 | )} 70 |
71 | ) 72 | } 73 | 74 | export default Input 75 | -------------------------------------------------------------------------------- /src/components/List.test.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import { shallow } from 'enzyme' 3 | import toJson from 'enzyme-to-json' 4 | import List from './List' 5 | 6 | describe('List', () => { 7 | it('matches empty ul snapshot', () => { 8 | const wrapper = shallow(items) 9 | expect(toJson(wrapper)).toMatchSnapshot() 10 | }) 11 | 12 | it('matches empty ol snapshot', () => { 13 | const wrapper = shallow(items) 14 | expect(toJson(wrapper)).toMatchSnapshot() 15 | }) 16 | 17 | it('matches li snapshot', () => { 18 | const wrapper = shallow(item text) 19 | expect(toJson(wrapper)).toMatchSnapshot() 20 | }) 21 | 22 | it('matches ul with item snapshot', () => { 23 | const wrapper = shallow( 24 | 25 | item text 26 | item text 27 | 28 | ) 29 | expect(toJson(wrapper)).toMatchSnapshot() 30 | }) 31 | 32 | it('matches empty ol snapshot', () => { 33 | const wrapper = shallow( 34 | 35 | item text 36 | item text 37 | 38 | ) 39 | expect(toJson(wrapper)).toMatchSnapshot() 40 | }) 41 | }) 42 | -------------------------------------------------------------------------------- /src/components/List.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import clsx from 'clsx' 3 | 4 | export interface Props { 5 | className?: string | Record 6 | ordered?: boolean 7 | item?: boolean 8 | } 9 | 10 | const List: React.FC = ({ 11 | className, 12 | item, 13 | children, 14 | ordered = false, 15 | }) => { 16 | const sharedStyles = 'text-base text-gray-700 dark:text-gray-300' 17 | 18 | if (item) { 19 | return
  • {children}
  • 20 | } 21 | 22 | const Component = ordered ? 'ol' : 'ul' 23 | return ( 24 | 32 | {children} 33 | 34 | ) 35 | } 36 | 37 | export default List 38 | -------------------------------------------------------------------------------- /src/components/Text.test.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import { shallow } from 'enzyme' 3 | import toJson from 'enzyme-to-json' 4 | import Text from './Text' 5 | 6 | describe('Text', () => { 7 | it('matches h1 snapshot', () => { 8 | const wrapper = shallow(Header 1) 9 | expect(toJson(wrapper)).toMatchSnapshot() 10 | }) 11 | 12 | it('matches h2 snapshot', () => { 13 | const wrapper = shallow(Header 2) 14 | expect(toJson(wrapper)).toMatchSnapshot() 15 | }) 16 | 17 | it('matches h3 snapshot', () => { 18 | const wrapper = shallow(Header 3) 19 | expect(toJson(wrapper)).toMatchSnapshot() 20 | }) 21 | 22 | it('matches h4 snapshot', () => { 23 | const wrapper = shallow(Header 4) 24 | expect(toJson(wrapper)).toMatchSnapshot() 25 | }) 26 | 27 | it('matches h5 snapshot', () => { 28 | const wrapper = shallow(Header 5) 29 | expect(toJson(wrapper)).toMatchSnapshot() 30 | }) 31 | 32 | it('matches h6 snapshot', () => { 33 | const wrapper = shallow(Header 6) 34 | expect(toJson(wrapper)).toMatchSnapshot() 35 | }) 36 | 37 | it('matches default text snapshot', () => { 38 | const wrapper = shallow(This is a paragraph) 39 | expect(toJson(wrapper)).toMatchSnapshot() 40 | }) 41 | 42 | it('matches small snapshot', () => { 43 | const wrapper = shallow(Small text) 44 | expect(toJson(wrapper)).toMatchSnapshot() 45 | }) 46 | 47 | it('matches bold snapshot', () => { 48 | const wrapper = shallow(Bold text) 49 | expect(toJson(wrapper)).toMatchSnapshot() 50 | }) 51 | }) 52 | -------------------------------------------------------------------------------- /src/components/Text.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import clsx from 'clsx' 3 | 4 | export interface Props { 5 | className?: string | Record 6 | h1?: boolean 7 | h2?: boolean 8 | h3?: boolean 9 | h4?: boolean 10 | h5?: boolean 11 | h6?: boolean 12 | p?: boolean 13 | small?: boolean 14 | bold?: boolean 15 | } 16 | 17 | const Text: React.FC = ({ 18 | className, 19 | children, 20 | h1 = false, 21 | h2 = false, 22 | h3 = false, 23 | h4 = false, 24 | h5 = false, 25 | h6 = false, 26 | small = false, 27 | bold = false, 28 | }) => { 29 | const sharedStyles = 'text-gray-700 dark:text-gray-300' 30 | const headerStyles = 'text-gray-900 dark:text-gray-100 font-black' 31 | 32 | if (h1) { 33 | return ( 34 |

    {children}

    35 | ) 36 | } 37 | 38 | if (h2) { 39 | return ( 40 |

    {children}

    41 | ) 42 | } 43 | 44 | if (h3) { 45 | return ( 46 |

    {children}

    47 | ) 48 | } 49 | 50 | if (h4) { 51 | return ( 52 |

    {children}

    53 | ) 54 | } 55 | 56 | if (h5) { 57 | return ( 58 |
    {children}
    59 | ) 60 | } 61 | 62 | if (h6) { 63 | return ( 64 |
    {children}
    65 | ) 66 | } 67 | 68 | if (small) { 69 | return ( 70 | 71 | {children} 72 | 73 | ) 74 | } 75 | 76 | return ( 77 |

    85 | {children} 86 |

    87 | ) 88 | } 89 | 90 | export default Text 91 | -------------------------------------------------------------------------------- /src/components/__snapshots__/Button.test.tsx.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`Button matches disabled state 1`] = ` 4 | 10 | `; 11 | 12 | exports[`Button matches loading state 1`] = ` 13 | 20 | `; 21 | 22 | exports[`Button matches primary (default) type snapshot 1`] = ` 23 | 29 | `; 30 | 31 | exports[`Button matches secondary type snapshot 1`] = ` 32 | 38 | `; 39 | 40 | exports[`Button matches with external className as object 1`] = ` 41 | 47 | `; 48 | 49 | exports[`Button matches with external className as string 1`] = ` 50 | 56 | `; 57 | -------------------------------------------------------------------------------- /src/components/__snapshots__/Card.test.tsx.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`Card matches snapshot 1`] = ` 4 |
    7 |

    8 | hello world 9 |

    10 |
    11 | `; 12 | -------------------------------------------------------------------------------- /src/components/__snapshots__/Dropdown.test.tsx.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`Dropdown matches snapshot 1`] = ` 4 |
    7 | 13 |
    14 | `; 15 | -------------------------------------------------------------------------------- /src/components/__snapshots__/Input.test.tsx.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`Input matches custom class names snapshot 1`] = ` 4 |
    7 |
    10 | 13 |
    14 |
    15 | `; 16 | 17 | exports[`Input matches default snapshot 1`] = ` 18 |
    21 |
    24 | 28 |
    29 |
    30 | `; 31 | 32 | exports[`Input matches disabled snapshot 1`] = ` 33 |
    36 |
    39 | 44 |
    45 |
    46 | `; 47 | 48 | exports[`Input matches with error snapshot 1`] = ` 49 |
    52 |
    55 | 59 |
    60 |
    63 | there is an error 64 |
    65 |
    66 | `; 67 | 68 | exports[`Input matches with help text 1`] = ` 69 |
    72 |
    75 | 78 |
    79 |
    82 | my help text 83 |
    84 |
    85 | `; 86 | 87 | exports[`Input matches with icon snapshot 1`] = ` 88 |
    91 |
    94 | 97 | 101 |
    102 |
    103 | `; 104 | -------------------------------------------------------------------------------- /src/components/__snapshots__/List.test.tsx.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`List matches empty ol snapshot 1`] = ` 4 |
      7 | items 8 |
    9 | `; 10 | 11 | exports[`List matches empty ol snapshot 2`] = ` 12 |
      15 | 18 | item text 19 | 20 | 23 | item text 24 | 25 |
    26 | `; 27 | 28 | exports[`List matches empty ul snapshot 1`] = ` 29 |
      32 | items 33 |
    34 | `; 35 | 36 | exports[`List matches li snapshot 1`] = ` 37 |
  • 40 | item text 41 |
  • 42 | `; 43 | 44 | exports[`List matches ul with item snapshot 1`] = ` 45 |
      48 | 51 | item text 52 | 53 | 56 | item text 57 | 58 |
    59 | `; 60 | -------------------------------------------------------------------------------- /src/components/__snapshots__/Text.test.tsx.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`Text matches bold snapshot 1`] = ` 4 |

    7 | Bold text 8 |

    9 | `; 10 | 11 | exports[`Text matches default text snapshot 1`] = ` 12 |

    15 | This is a paragraph 16 |

    17 | `; 18 | 19 | exports[`Text matches h1 snapshot 1`] = ` 20 |

    23 | Header 1 24 |

    25 | `; 26 | 27 | exports[`Text matches h2 snapshot 1`] = ` 28 |

    31 | Header 2 32 |

    33 | `; 34 | 35 | exports[`Text matches h3 snapshot 1`] = ` 36 |

    39 | Header 3 40 |

    41 | `; 42 | 43 | exports[`Text matches h4 snapshot 1`] = ` 44 |

    47 | Header 4 48 |

    49 | `; 50 | 51 | exports[`Text matches h5 snapshot 1`] = ` 52 |
    55 | Header 5 56 |
    57 | `; 58 | 59 | exports[`Text matches h6 snapshot 1`] = ` 60 |
    63 | Header 6 64 |
    65 | `; 66 | 67 | exports[`Text matches small snapshot 1`] = ` 68 | 71 | Small text 72 | 73 | `; 74 | -------------------------------------------------------------------------------- /src/components/layout/AppHeader.test.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import { shallow } from 'enzyme' 3 | import toJson from 'enzyme-to-json' 4 | 5 | import AppHeader from './AppHeader' 6 | 7 | describe('AppHeader', () => { 8 | it('matches snapshot', () => { 9 | const wrapper = shallow() 10 | expect(toJson(wrapper)).toMatchSnapshot() 11 | }) 12 | }) 13 | -------------------------------------------------------------------------------- /src/components/layout/AppHeader.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import clsx from 'clsx' 3 | import Link from 'next/link' 4 | import dynamic from 'next/dynamic' 5 | import Dropdown from '@components/Dropdown' 6 | import routes from '@utils/routes' 7 | import { FiMoreHorizontal } from 'react-icons/fi' 8 | import { AiOutlineFontColors } from 'react-icons/ai' 9 | import { MdFeaturedPlayList } from 'react-icons/md' 10 | 11 | const ThemeSwitch = dynamic(() => import('@components/layout/ThemeSwitch'), { 12 | ssr: false, 13 | }) 14 | 15 | export interface Props { 16 | className?: string | Record 17 | } 18 | 19 | const AppHeader: React.FC = ({ className }) => { 20 | return ( 21 |
    27 |
    28 | 29 | 33 | My Project 34 | 35 | 36 |
    37 |
    38 | }> 39 | 64 | 65 |
    66 |
    67 | ) 68 | } 69 | 70 | export default AppHeader 71 | -------------------------------------------------------------------------------- /src/components/layout/Layout.test.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import { shallow } from 'enzyme' 3 | import toJson from 'enzyme-to-json' 4 | import Layout from './Layout' 5 | 6 | describe('Layout', () => { 7 | it('matches snapshot', () => { 8 | const wrapper = shallow( 9 | 10 |

    hello world

    11 |
    12 | ) 13 | expect(toJson(wrapper)).toMatchSnapshot() 14 | }) 15 | 16 | it('matches snapshot with title', () => { 17 | const wrapper = shallow( 18 | 19 |

    hello world

    20 |
    21 | ) 22 | expect(toJson(wrapper)).toMatchSnapshot() 23 | }) 24 | 25 | it('matches snapshot with custom className', () => { 26 | const wrapper = shallow( 27 | 28 |

    hello world

    29 |
    30 | ) 31 | expect(toJson(wrapper)).toMatchSnapshot() 32 | }) 33 | }) 34 | -------------------------------------------------------------------------------- /src/components/layout/Layout.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import clsx from 'clsx' 3 | import Head from 'next/head' 4 | import AppHeader from '@components/layout/AppHeader' 5 | 6 | export interface Props { 7 | title?: string 8 | className?: string | Record 9 | } 10 | 11 | const Layout: React.FC = ({ 12 | title = 'NextJS with TypeScript and TailwindCSS', 13 | className, 14 | children, 15 | }) => { 16 | return ( 17 | <> 18 | 19 | {title} 20 | 21 |
    22 | 23 |
    {children}
    24 |
    25 | Made with ♥ by{' '} 26 | @lailo_ch 27 |
    28 |
    29 | 30 | ) 31 | } 32 | 33 | export default Layout 34 | -------------------------------------------------------------------------------- /src/components/layout/LayoutMDX.test.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import { shallow } from 'enzyme' 3 | import toJson from 'enzyme-to-json' 4 | import LayoutMDX from './LayoutMDX' 5 | 6 | describe('LayoutMDX', () => { 7 | it('matches snapshot', () => { 8 | const wrapper = shallow(hello world) 9 | expect(toJson(wrapper)).toMatchSnapshot() 10 | }) 11 | }) 12 | -------------------------------------------------------------------------------- /src/components/layout/LayoutMDX.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import { MDXProvider } from '@mdx-js/react' 3 | import Layout, { Props as LayoutProps } from './Layout' 4 | import Text from '@components/Text' 5 | import List from '@components/List' 6 | 7 | const components = { 8 | h1: ({ children }) => ( 9 | 10 | {children} 11 | 12 | ), 13 | h2: ({ children }) => ( 14 | 15 | {children} 16 | 17 | ), 18 | h3: ({ children }) => ( 19 | 20 | {children} 21 | 22 | ), 23 | h4: ({ children }) => ( 24 | 25 | {children} 26 | 27 | ), 28 | h5: ({ children }) => ( 29 | 30 | {children} 31 | 32 | ), 33 | h6: ({ children }) => ( 34 | 35 | {children} 36 | 37 | ), 38 | p: ({ children }) => {children}, 39 | strong: ({ children }) => ( 40 | 41 | {children} 42 | 43 | ), 44 | sub: ({ children }) => ( 45 | 46 | {children} 47 | 48 | ), 49 | ul: ({ children }) => {children}, 50 | ol: ({ children }) => ( 51 | 52 | {children} 53 | 54 | ), 55 | li: ({ children }) => {children}, 56 | } 57 | 58 | const LayoutMDX: React.FC = ({ children, ...props }) => { 59 | return ( 60 | 61 | {children} 62 | 63 | ) 64 | } 65 | 66 | export default LayoutMDX 67 | -------------------------------------------------------------------------------- /src/components/layout/ThemeSwitch.test.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import { shallow } from 'enzyme' 3 | import toJson from 'enzyme-to-json' 4 | 5 | import ThemeSwitch from './ThemeSwitch' 6 | 7 | describe('ThemeSwitch', () => { 8 | it('matches snapshot', () => { 9 | const wrapper = shallow() 10 | expect(toJson(wrapper)).toMatchSnapshot() 11 | }) 12 | }) 13 | -------------------------------------------------------------------------------- /src/components/layout/ThemeSwitch.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import Switch from 'react-switch' 3 | import { FiMoon, FiSun } from 'react-icons/fi' 4 | import useDarkMode from 'use-dark-mode' 5 | 6 | const MODE_TRANSITION_CLASS_NAME = 'dark-mode-transition' 7 | const MODE_TRANSITION_DURATION = 500 8 | 9 | function setDarkModeTransition() { 10 | document.documentElement.classList.add(MODE_TRANSITION_CLASS_NAME) 11 | setTimeout( 12 | () => document.documentElement.classList.remove(MODE_TRANSITION_CLASS_NAME), 13 | MODE_TRANSITION_DURATION 14 | ) 15 | } 16 | 17 | const ThemeSwitch: React.FC = () => { 18 | const { value: hasActiveDarkMode, toggle: activateDarkMode } = useDarkMode() 19 | 20 | const toggleDarkMode = () => { 21 | setDarkModeTransition() 22 | activateDarkMode() 23 | } 24 | 25 | return ( 26 | } 30 | uncheckedIcon={} 31 | onColor="#1a202c" 32 | offColor="#f7fafc" 33 | onHandleColor="#f7fafc" 34 | offHandleColor="#1a202c" 35 | className="text-gray-900 dark:text-gray-100 border border-gray-400 dark:border-0" 36 | /> 37 | ) 38 | } 39 | 40 | export default ThemeSwitch 41 | -------------------------------------------------------------------------------- /src/components/layout/__snapshots__/AppHeader.test.tsx.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`AppHeader matches snapshot 1`] = ` 4 |
    7 |
    10 | 14 | 18 | My Project 19 | 20 | 21 |
    22 |
    23 | } 25 | > 26 | 77 | 78 |
    79 |
    80 | `; 81 | -------------------------------------------------------------------------------- /src/components/layout/__snapshots__/Layout.test.tsx.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`Layout matches snapshot 1`] = ` 4 | 5 | 6 | 7 | NextJS with TypeScript and TailwindCSS 8 | 9 | 10 |
    13 | 14 |
    17 |

    18 | hello world 19 |

    20 |
    21 | 32 |
    33 |
    34 | `; 35 | 36 | exports[`Layout matches snapshot with custom className 1`] = ` 37 | 38 | 39 | 40 | NextJS with TypeScript and TailwindCSS 41 | 42 | 43 |
    46 | 47 |
    50 |

    51 | hello world 52 |

    53 |
    54 | 65 |
    66 |
    67 | `; 68 | 69 | exports[`Layout matches snapshot with title 1`] = ` 70 | 71 | 72 | 73 | My Awesome App 74 | 75 | 76 |
    79 | 80 |
    83 |

    84 | hello world 85 |

    86 |
    87 | 98 |
    99 |
    100 | `; 101 | -------------------------------------------------------------------------------- /src/components/layout/__snapshots__/LayoutMDX.test.tsx.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`LayoutMDX matches snapshot 1`] = ` 4 | 5 | 23 | hello world 24 | 25 | 26 | `; 27 | -------------------------------------------------------------------------------- /src/components/layout/__snapshots__/ThemeSwitch.test.tsx.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`ThemeSwitch matches snapshot 1`] = ` 4 | 12 | } 13 | className="text-gray-900 dark:text-gray-100 border border-gray-400 dark:border-0" 14 | disabled={false} 15 | height={28} 16 | offColor="#f7fafc" 17 | offHandleColor="#1a202c" 18 | onChange={[Function]} 19 | onColor="#1a202c" 20 | onHandleColor="#f7fafc" 21 | uncheckedIcon={ 22 | 25 | } 26 | width={56} 27 | /> 28 | `; 29 | -------------------------------------------------------------------------------- /src/pages/_app.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import Router from 'next/router' 3 | import { AppProps } from 'next/app' 4 | import NProgress from 'nprogress' 5 | 6 | import '@styles/main.css' 7 | import '@styles/nprogress.css' 8 | 9 | Router.events.on('routeChangeStart', () => NProgress.start()) 10 | Router.events.on('routeChangeComplete', () => NProgress.done()) 11 | Router.events.on('routeChangeError', () => NProgress.done()) 12 | 13 | const App: React.FC = ({ Component, pageProps }) => { 14 | return 15 | } 16 | 17 | export default App 18 | -------------------------------------------------------------------------------- /src/pages/_document.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import NextDocument, { 3 | Html, 4 | Head, 5 | Main, 6 | NextScript, 7 | DocumentContext, 8 | } from 'next/document' 9 | 10 | class Document extends NextDocument { 11 | static async getInitialProps(context: DocumentContext) { 12 | const initialProps = await NextDocument.getInitialProps(context) 13 | return { ...initialProps } 14 | } 15 | 16 | render() { 17 | return ( 18 | 19 | 20 | 21 | 25 | 29 | 30 | 31 |