├── .gitignore ├── README.md ├── babel.config.js ├── components └── Tabs │ ├── Tabs.js │ ├── Tabs.module.css │ └── index.js ├── jest.config.js ├── package.json ├── pages ├── _app.js ├── api │ └── hello.js └── index.js ├── public ├── favicon.ico └── vercel.svg ├── styles ├── Home.module.css └── globals.css ├── utils └── slugify.js └── yarn.lock /.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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app). 2 | 3 | ## Getting Started 4 | 5 | First, run the development server: 6 | 7 | ```bash 8 | npm run dev 9 | # or 10 | yarn dev 11 | ``` 12 | 13 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. 14 | 15 | You can start editing the page by modifying `pages/index.js`. The page auto-updates as you edit the file. 16 | 17 | ## Learn More 18 | 19 | To learn more about Next.js, take a look at the following resources: 20 | 21 | - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. 22 | - [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. 23 | 24 | You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome! 25 | 26 | ## Deploy on Vercel 27 | 28 | 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. 29 | 30 | Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details. 31 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | [ 4 | "@babel/preset-env", 5 | { 6 | targets: { 7 | node: "current", 8 | }, 9 | }, 10 | ], 11 | "next/babel", 12 | ], 13 | }; 14 | -------------------------------------------------------------------------------- /components/Tabs/Tabs.js: -------------------------------------------------------------------------------- 1 | import { useState, useEffect } from "react"; 2 | import { useRouter } from "next/router"; 3 | import styles from "./Tabs.module.css"; 4 | 5 | import { slugify } from "../../utils/slugify"; 6 | 7 | const Tabs = ({ children, initialTab }) => { 8 | const [activeTab, setActiveTab] = useState(children[0].props.label); 9 | const router = useRouter(); 10 | 11 | const handleClick = (e, newActiveTab) => { 12 | e.preventDefault(); 13 | setActiveTab(slugify(newActiveTab)); 14 | }; 15 | 16 | useEffect(() => { 17 | if (initialTab.tab) { 18 | setActiveTab(initialTab.tab); 19 | console.log(initialTab); 20 | } 21 | }, []); 22 | 23 | useEffect(() => { 24 | router.push(`${router.pathname}?tab=${slugify(activeTab)}`, undefined, { 25 | shallow: true, 26 | }); 27 | console.log(activeTab); 28 | }, [activeTab]); 29 | 30 | return ( 31 |
32 | 47 | {children.map((one) => { 48 | if (slugify(one.props.label) == activeTab) 49 | return ( 50 |
51 | {one.props.children} 52 |
53 | ); 54 | })} 55 |
56 | ); 57 | }; 58 | 59 | export { Tabs }; 60 | -------------------------------------------------------------------------------- /components/Tabs/Tabs.module.css: -------------------------------------------------------------------------------- 1 | .tabs { 2 | list-style: none; 3 | padding: 0; 4 | margin: 0; 5 | display: flex; 6 | border-bottom: 2px solid blue; 7 | } 8 | 9 | .tabs > li { 10 | padding: 0.5em 1em; 11 | margin: 0 1em; 12 | } 13 | 14 | .current { 15 | background: white; 16 | color: blue; 17 | font-weight: bold; 18 | border: 2px solid blue; 19 | border-bottom: 0; 20 | border-top-right-radius: 5px; 21 | border-top-left-radius: 5px; 22 | position: relative; 23 | top: 2px; 24 | } 25 | 26 | .content { 27 | padding: 1em 2em; 28 | } 29 | -------------------------------------------------------------------------------- /components/Tabs/index.js: -------------------------------------------------------------------------------- 1 | import { Tabs } from "./Tabs"; 2 | export { Tabs }; 3 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | // For a detailed explanation regarding each configuration property, visit: 2 | // https://jestjs.io/docs/en/configuration.html 3 | 4 | module.exports = { 5 | // All imported modules in your tests should be mocked automatically 6 | // automock: false, 7 | 8 | // Stop running tests after `n` failures 9 | // bail: 0, 10 | 11 | // The directory where Jest should store its cached dependency information 12 | // cacheDirectory: "/private/var/folders/c3/470z4ly54yxb5smzhvv6g3yh0000gn/T/jest_dx", 13 | 14 | // Automatically clear mock calls and instances between every test 15 | // clearMocks: false, 16 | 17 | // Indicates whether the coverage information should be collected while executing the test 18 | // collectCoverage: false, 19 | 20 | // An array of glob patterns indicating a set of files for which coverage information should be collected 21 | // collectCoverageFrom: undefined, 22 | 23 | // The directory where Jest should output its coverage files 24 | // coverageDirectory: undefined, 25 | 26 | // An array of regexp pattern strings used to skip coverage collection 27 | // coveragePathIgnorePatterns: [ 28 | // "/node_modules/" 29 | // ], 30 | 31 | // Indicates which provider should be used to instrument code for coverage 32 | // coverageProvider: "babel", 33 | 34 | // A list of reporter names that Jest uses when writing coverage reports 35 | // coverageReporters: [ 36 | // "json", 37 | // "text", 38 | // "lcov", 39 | // "clover" 40 | // ], 41 | 42 | // An object that configures minimum threshold enforcement for coverage results 43 | // coverageThreshold: undefined, 44 | 45 | // A path to a custom dependency extractor 46 | // dependencyExtractor: undefined, 47 | 48 | // Make calling deprecated APIs throw helpful error messages 49 | // errorOnDeprecated: false, 50 | 51 | // Force coverage collection from ignored files using an array of glob patterns 52 | // forceCoverageMatch: [], 53 | 54 | // A path to a module which exports an async function that is triggered once before all test suites 55 | // globalSetup: undefined, 56 | 57 | // A path to a module which exports an async function that is triggered once after all test suites 58 | // globalTeardown: undefined, 59 | 60 | // A set of global variables that need to be available in all test environments 61 | // globals: {}, 62 | 63 | // The maximum amount of workers used to run your tests. Can be specified as % or a number. E.g. maxWorkers: 10% will use 10% of your CPU amount + 1 as the maximum worker number. maxWorkers: 2 will use a maximum of 2 workers. 64 | // maxWorkers: "50%", 65 | 66 | // An array of directory names to be searched recursively up from the requiring module's location 67 | // moduleDirectories: [ 68 | // "node_modules" 69 | // ], 70 | 71 | // An array of file extensions your modules use 72 | // moduleFileExtensions: [ 73 | // "js", 74 | // "json", 75 | // "jsx", 76 | // "ts", 77 | // "tsx", 78 | // "node" 79 | // ], 80 | 81 | // A map from regular expressions to module names or to arrays of module names that allow to stub out resources with a single module 82 | // moduleNameMapper: {}, 83 | 84 | // An array of regexp pattern strings, matched against all module paths before considered 'visible' to the module loader 85 | // modulePathIgnorePatterns: [], 86 | 87 | // Activates notifications for test results 88 | // notify: false, 89 | 90 | // An enum that specifies notification mode. Requires { notify: true } 91 | // notifyMode: "failure-change", 92 | 93 | // A preset that is used as a base for Jest's configuration 94 | // preset: undefined, 95 | 96 | // Run tests from one or more projects 97 | // projects: undefined, 98 | 99 | // Use this configuration option to add custom reporters to Jest 100 | // reporters: undefined, 101 | 102 | // Automatically reset mock state between every test 103 | // resetMocks: false, 104 | 105 | // Reset the module registry before running each individual test 106 | // resetModules: false, 107 | 108 | // A path to a custom resolver 109 | // resolver: undefined, 110 | 111 | // Automatically restore mock state between every test 112 | // restoreMocks: false, 113 | 114 | // The root directory that Jest should scan for tests and modules within 115 | // rootDir: undefined, 116 | 117 | // A list of paths to directories that Jest should use to search for files in 118 | // roots: [ 119 | // "" 120 | // ], 121 | 122 | // Allows you to use a custom runner instead of Jest's default test runner 123 | // runner: "jest-runner", 124 | 125 | // The paths to modules that run some code to configure or set up the testing environment before each test 126 | // setupFiles: [], 127 | 128 | // A list of paths to modules that run some code to configure or set up the testing framework before each test 129 | // setupFilesAfterEnv: [], 130 | 131 | // The number of seconds after which a test is considered as slow and reported as such in the results. 132 | // slowTestThreshold: 5, 133 | 134 | // A list of paths to snapshot serializer modules Jest should use for snapshot testing 135 | // snapshotSerializers: [], 136 | 137 | // The test environment that will be used for testing 138 | // testEnvironment: "jest-environment-jsdom", 139 | 140 | // Options that will be passed to the testEnvironment 141 | // testEnvironmentOptions: {}, 142 | 143 | // Adds a location field to test results 144 | // testLocationInResults: false, 145 | 146 | // The glob patterns Jest uses to detect test files 147 | // testMatch: [ 148 | // "**/__tests__/**/*.[jt]s?(x)", 149 | // "**/?(*.)+(spec|test).[tj]s?(x)" 150 | // ], 151 | 152 | // An array of regexp pattern strings that are matched against all test paths, matched tests are skipped 153 | // testPathIgnorePatterns: [ 154 | // "/node_modules/" 155 | // ], 156 | 157 | // The regexp pattern or array of patterns that Jest uses to detect test files 158 | // testRegex: [], 159 | 160 | // This option allows the use of a custom results processor 161 | // testResultsProcessor: undefined, 162 | 163 | // This option allows use of a custom test runner 164 | // testRunner: "jasmine2", 165 | 166 | // This option sets the URL for the jsdom environment. It is reflected in properties such as location.href 167 | // testURL: "http://localhost", 168 | 169 | // Setting this value to "fake" allows the use of fake timers for functions such as "setTimeout" 170 | // timers: "real", 171 | 172 | // A map from regular expressions to paths to transformers 173 | // transform: undefined, 174 | 175 | // An array of regexp pattern strings that are matched against all source file paths, matched files will skip transformation 176 | // transformIgnorePatterns: [ 177 | // "/node_modules/", 178 | // "\\.pnp\\.[^\\/]+$" 179 | // ], 180 | 181 | // An array of regexp pattern strings that are matched against all modules before the module loader will automatically return a mock for them 182 | // unmockedModulePathPatterns: undefined, 183 | 184 | // Indicates whether each individual test should be reported during the run 185 | // verbose: undefined, 186 | 187 | // An array of regexp patterns that are matched against all source file paths before re-running tests in watch mode 188 | // watchPathIgnorePatterns: [], 189 | 190 | // Whether to use watchman for file crawling 191 | // watchman: true, 192 | }; 193 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-tabs", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "start": "next start", 9 | "test": "jest", 10 | "test:watch": "yarn run test -- --watch" 11 | }, 12 | "dependencies": { 13 | "next": "9.5.2", 14 | "react": "16.13.1", 15 | "react-dom": "16.13.1" 16 | }, 17 | "devDependencies": { 18 | "@babel/core": "^7.11.5", 19 | "@babel/preset-env": "^7.11.5", 20 | "@testing-library/jest-dom": "^5.11.4", 21 | "@testing-library/react": "^11.0.0", 22 | "babel-jest": "^26.3.0", 23 | "jest": "^26.4.2" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /pages/_app.js: -------------------------------------------------------------------------------- 1 | import '../styles/globals.css' 2 | 3 | function MyApp({ Component, pageProps }) { 4 | return 5 | } 6 | 7 | export default MyApp 8 | -------------------------------------------------------------------------------- /pages/api/hello.js: -------------------------------------------------------------------------------- 1 | // Next.js API route support: https://nextjs.org/docs/api-routes/introduction 2 | 3 | export default (req, res) => { 4 | res.statusCode = 200 5 | res.json({ name: 'John Doe' }) 6 | } 7 | -------------------------------------------------------------------------------- /pages/index.js: -------------------------------------------------------------------------------- 1 | import Head from "next/head"; 2 | import styles from "../styles/Home.module.css"; 3 | import { useRouter } from "next/router"; 4 | 5 | import { Tabs } from "../components/Tabs"; 6 | 7 | export default function Home({ query }) { 8 | return ( 9 |
10 | 11 | React Tabs 12 | 13 | 14 | 15 |
16 | 17 |
18 |

Tab 1

19 |

20 | Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do 21 | eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut 22 | enim ad minim veniam, quis nostrud exercitation ullamco laboris 23 | nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in 24 | reprehenderit in voluptate velit esse cillum dolore eu fugiat 25 | nulla pariatur. Excepteur sint occaecat cupidatat non proident, 26 | sunt in culpa qui officia deserunt mollit anim id est laborum. 27 |

28 |
29 |
30 |

Tab 2

31 |

32 | Duis aute irure dolor in reprehenderit in voluptate velit esse 33 | cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat 34 | cupidatat non proident, sunt in culpa qui officia deserunt mollit 35 | anim id est laborum. 36 |

37 |
38 |
39 |

Tab 3

40 |

41 | Sunt in culpa qui officia deserunt mollit anim id est laborum. 42 | Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do 43 | eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut 44 | enim ad minim veniam, quis nostrud exercitation ullamco laboris 45 | nisi ut aliquip ex ea commodo consequat. Dolore eu fugiat nulla 46 | pariatur. Excepteur sint occaecat cupidatat non proident, sunt in 47 | culpa qui officia deserunt mollit anim id est laborum. 48 |

49 |
50 |
51 |
52 |
53 | ); 54 | } 55 | 56 | Home.getInitialProps = ({ query }) => { 57 | return { query }; 58 | }; 59 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/selfteachme/0025-react-tabbed-component/f8378b6ac41e39bd2e68655375fd2a35498a2988/public/favicon.ico -------------------------------------------------------------------------------- /public/vercel.svg: -------------------------------------------------------------------------------- 1 | 3 | 4 | -------------------------------------------------------------------------------- /styles/Home.module.css: -------------------------------------------------------------------------------- 1 | .container { 2 | padding: 2rem; 3 | } 4 | -------------------------------------------------------------------------------- /styles/globals.css: -------------------------------------------------------------------------------- 1 | html, 2 | body { 3 | padding: 0; 4 | margin: 0; 5 | font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen, 6 | Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif; 7 | } 8 | 9 | a { 10 | color: inherit; 11 | text-decoration: none; 12 | } 13 | 14 | * { 15 | box-sizing: border-box; 16 | } 17 | -------------------------------------------------------------------------------- /utils/slugify.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Turns a string into a slug 3 | * Reference: https://gist.github.com/codeguy/6684588 4 | */ 5 | export function slugify(str) { 6 | str = str.replace(/^\s+|\s+$/g, ""); // trim 7 | str = str.toLowerCase(); 8 | 9 | // remove accents, swap ñ for n, etc 10 | var from = "àáäâèéëêìíïîòóöôùúüûñç·/_,:;"; 11 | var to = "aaaaeeeeiiiioooouuuunc------"; 12 | for (var i = 0, l = from.length; i < l; i++) { 13 | str = str.replace(new RegExp(from.charAt(i), "g"), to.charAt(i)); 14 | } 15 | 16 | str = str 17 | .replace(/[^a-z0-9 -]/g, "") // remove invalid chars 18 | .replace(/\s+/g, "-") // collapse whitespace and replace by - 19 | .replace(/-+/g, "-"); // collapse dashes 20 | 21 | return str; 22 | } 23 | --------------------------------------------------------------------------------