├── .babelrc ├── .env ├── .env.local.template ├── .eslintignore ├── .eslintrc.json ├── .github └── FUNDING.yml ├── .gitignore ├── .prettierignore ├── .vscode ├── extensions.json └── settings.json ├── README.md ├── amplify.yml ├── components ├── application │ ├── layout │ │ ├── PageFooter.jsx │ │ └── PageHeader.jsx │ ├── session │ │ ├── SessionDropdown.jsx │ │ ├── SessionMenuItem.jsx │ │ └── useSignOut.jsx │ ├── sidebar │ │ ├── LinkMenuItem.jsx │ │ ├── PageMenuItem.jsx │ │ ├── ScrollMenuItem.jsx │ │ ├── SidebarButton.jsx │ │ ├── SidebarItems.jsx │ │ └── SidebarLinks.jsx │ └── util │ │ ├── RenderIf.jsx │ │ └── ScrollTarget.jsx ├── content │ ├── ApiQuery.jsx │ └── ApiTest.jsx └── page │ ├── HomePage.jsx │ └── PrivatePage.jsx ├── env ├── .env.dev ├── .env.dev.local.template ├── .env.prod ├── .env.prod.local.template ├── .env.test └── .env.test.local.template ├── middleware.js ├── next-env.d.ts ├── next.config.mjs ├── package-lock.json ├── package.json ├── pages ├── _app.jsx ├── api │ ├── auth │ │ └── [...nextauth].jsx │ ├── hello.jsx │ └── private │ │ └── hello.jsx ├── coming-soon.jsx ├── index.jsx └── private.jsx ├── public └── images │ ├── favicon │ ├── android-icon-144x144.png │ ├── android-icon-192x192.png │ ├── android-icon-36x36.png │ ├── android-icon-48x48.png │ ├── android-icon-72x72.png │ ├── android-icon-96x96.png │ ├── apple-icon-114x114.png │ ├── apple-icon-120x120.png │ ├── apple-icon-144x144.png │ ├── apple-icon-152x152.png │ ├── apple-icon-180x180.png │ ├── apple-icon-57x57.png │ ├── apple-icon-60x60.png │ ├── apple-icon-72x72.png │ ├── apple-icon-76x76.png │ ├── apple-icon-precomposed.png │ ├── apple-icon.png │ ├── browserconfig.xml │ ├── favicon-16x16.png │ ├── favicon-32x32.png │ ├── favicon-96x96.png │ ├── favicon.ico │ ├── manifest.json │ ├── ms-icon-144x144.png │ ├── ms-icon-150x150.png │ ├── ms-icon-310x310.png │ └── ms-icon-70x70.png │ └── logo.png ├── semantic-ui ├── site │ ├── collections │ │ ├── breadcrumb.overrides │ │ ├── breadcrumb.variables │ │ ├── form.overrides │ │ ├── form.variables │ │ ├── grid.overrides │ │ ├── grid.variables │ │ ├── menu.overrides │ │ ├── menu.variables │ │ ├── message.overrides │ │ ├── message.variables │ │ ├── table.overrides │ │ └── table.variables │ ├── elements │ │ ├── button.overrides │ │ ├── button.variables │ │ ├── container.overrides │ │ ├── container.variables │ │ ├── divider.overrides │ │ ├── divider.variables │ │ ├── flag.overrides │ │ ├── flag.variables │ │ ├── header.overrides │ │ ├── header.variables │ │ ├── icon.overrides │ │ ├── icon.variables │ │ ├── image.overrides │ │ ├── image.variables │ │ ├── input.overrides │ │ ├── input.variables │ │ ├── label.overrides │ │ ├── label.variables │ │ ├── list.overrides │ │ ├── list.variables │ │ ├── loader.overrides │ │ ├── loader.variables │ │ ├── rail.overrides │ │ ├── rail.variables │ │ ├── reveal.overrides │ │ ├── reveal.variables │ │ ├── segment.overrides │ │ ├── segment.variables │ │ ├── step.overrides │ │ └── step.variables │ ├── globals │ │ ├── reset.overrides │ │ ├── reset.variables │ │ ├── site.overrides │ │ └── site.variables │ ├── modules │ │ ├── accordion.overrides │ │ ├── accordion.variables │ │ ├── chatroom.overrides │ │ ├── chatroom.variables │ │ ├── checkbox.overrides │ │ ├── checkbox.variables │ │ ├── dimmer.overrides │ │ ├── dimmer.variables │ │ ├── dropdown.overrides │ │ ├── dropdown.variables │ │ ├── embed.overrides │ │ ├── embed.variables │ │ ├── modal.overrides │ │ ├── modal.variables │ │ ├── nag.overrides │ │ ├── nag.variables │ │ ├── popup.overrides │ │ ├── popup.variables │ │ ├── progress.overrides │ │ ├── progress.variables │ │ ├── rating.overrides │ │ ├── rating.variables │ │ ├── search.overrides │ │ ├── search.variables │ │ ├── shape.overrides │ │ ├── shape.variables │ │ ├── sidebar.overrides │ │ ├── sidebar.variables │ │ ├── sticky.overrides │ │ ├── sticky.variables │ │ ├── tab.overrides │ │ ├── tab.variables │ │ ├── transition.overrides │ │ └── transition.variables │ └── views │ │ ├── ad.overrides │ │ ├── ad.variables │ │ ├── card.overrides │ │ ├── card.variables │ │ ├── comment.overrides │ │ ├── comment.variables │ │ ├── feed.overrides │ │ ├── feed.variables │ │ ├── item.overrides │ │ ├── item.variables │ │ ├── statistic.overrides │ │ └── statistic.variables └── theme.config ├── state ├── entitySlice.mjs ├── pageSlice.mjs └── store.mjs ├── styles.css ├── test ├── entity.test.jsx └── sample.test.mjs └── tsconfig.json /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "@babel/preset-env", 4 | [ 5 | "@babel/preset-react", 6 | { 7 | "runtime": "automatic" 8 | } 9 | ] 10 | ], 11 | "plugins": [ 12 | "lodash", 13 | [ 14 | "module-extension", 15 | { 16 | "mjs": "" 17 | } 18 | ], 19 | "@babel/plugin-syntax-import-assertions" 20 | ] 21 | } 22 | -------------------------------------------------------------------------------- /.env: -------------------------------------------------------------------------------- 1 | # These environment variables are populated in all environments. 2 | # 3 | # By default, environment variables are only available on the server side. 4 | # Prefix a variable with NEXT_PUBLIC_ to expose it to the browser. 5 | # 6 | # Learn more about how this template handles environment variables at https://karmanivero.us/blog/nextjs-template/#environment-variables 7 | 8 | # Site name elements for default META titles & menu header. 9 | SITE_NAME_TOKEN=Next.js Template 10 | 11 | # Default META description. 12 | NEXT_PUBLIC_SITE_DESCRIPTION=This is your site description! 13 | 14 | # Contact info. If populated, these will create links in footer & sidebar. 15 | NEXT_PUBLIC_EMAIL=me@karmanivero.us 16 | NEXT_PUBLIC_GITHUB_LINK=https://github.com/karmaniverous/template-nextjs 17 | NEXT_PUBLIC_TELEGRAM_HANDLE=karmaniverous 18 | NEXT_PUBLIC_TWITTER_HANDLE=karmaniverous 19 | 20 | # Copyright notice for page footer. 21 | NEXT_PUBLIC_SITE_COPYRIGHT=©️ 2022 Karmaniverous 22 | 23 | # Google Tag Manager Id. 24 | NEXT_PUBLIC_GTM_ID= 25 | -------------------------------------------------------------------------------- /.env.local.template: -------------------------------------------------------------------------------- 1 | # IF THIS FILE HAS A .template EXTENSION... 2 | # If you haven't already done so, create a copy of this file with the same name but without the .template extension. 3 | # 4 | # OTHERWISE... 5 | # 6 | # THIS FILE IS FOR APPLICATION SECRETS! 7 | # It is excluded from the GitHub repo. Any secrets it contains will remain local to this machine. 8 | # 9 | # These environment variables are populated in all environments. 10 | # 11 | # By default, environment variables are only available on the server side. 12 | # If you prefix any of these variables with NEXT_PUBLIC_, you will expose it to the browser! 13 | # 14 | # Learn more about how this template handles environment variables at https://karmanivero.us/blog/nextjs-template/#environment-variables 15 | 16 | # Populate with GitHub/GitLab personal access token to support release-it. 17 | # GitHub: repo scope 18 | # GitLab: api scope 19 | GITHUB_TOKEN= 20 | GITLAB_TOKEN= 21 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "es2021": true, 5 | "node": true 6 | }, 7 | "extends": [ 8 | "eslint:recommended", 9 | "plugin:mocha/recommended", 10 | "plugin:promise/recommended", 11 | "plugin:react/recommended", 12 | "plugin:react/jsx-runtime", 13 | "plugin:@next/next/recommended" 14 | ], 15 | "parser": "@babel/eslint-parser", 16 | "parserOptions": { 17 | "ecmaFeatures": { 18 | "jsx": true 19 | }, 20 | "ecmaVersion": "latest", 21 | "sourceType": "module" 22 | }, 23 | "plugins": ["mocha", "promise", "react"], 24 | "rules": { "react/jsx-uses-react": "off", "react/react-in-jsx-scope": "off" }, 25 | "settings": { 26 | "react": { 27 | "createClass": "createReactClass", // Regex for Component Factory to use, 28 | // default to "createReactClass" 29 | "pragma": "React", // Pragma to use, default to "React" 30 | "fragment": "Fragment", // Fragment to use (may be a property of ), default to "Fragment" 31 | "version": "detect", // React version. "detect" automatically picks the version you have installed. 32 | // You can also use `16.0`, `16.3`, etc, if you want to override the detected value. 33 | // It will default to "latest" and warn if missing, and to "detect" in the future 34 | "flowVersion": "0.53" // Flow version 35 | }, 36 | "propWrapperFunctions": [ 37 | // The names of any function used to wrap propTypes, e.g. `forbidExtraProps`. If this isn't set, any propTypes wrapped in a function will be skipped. 38 | "forbidExtraProps", 39 | { "property": "freeze", "object": "Object" }, 40 | { "property": "myFavoriteWrapper" }, 41 | // for rules that check exact prop wrappers 42 | { "property": "forbidExtraProps", "exact": true } 43 | ], 44 | "componentWrapperFunctions": [ 45 | // The name of any function used to wrap components, e.g. Mobx `observer` function. If this isn't set, components wrapped by these functions will be skipped. 46 | "observer", // `property` 47 | { "property": "styled" }, // `object` is optional 48 | { "property": "observer", "object": "Mobx" }, 49 | { "property": "observer", "object": "" } // sets `object` to whatever value `settings.react.pragma` is set to 50 | ], 51 | "formComponents": [ 52 | // Components used as alternatives to
for forms, eg. 53 | "CustomForm", 54 | { "name": "Form", "formAttribute": "endpoint" } 55 | ], 56 | "linkComponents": [ 57 | // Components used as alternatives to for linking, eg. 58 | "Hyperlink", 59 | { "name": "Link", "linkAttribute": "to" } 60 | ] 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: [karmaniverous] 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | 6 | # Runtime data 7 | pids 8 | *.pid 9 | *.seed 10 | 11 | # Directory for instrumented libs generated by jscoverage/JSCover 12 | lib-cov 13 | 14 | # Coverage directory used by tools like istanbul 15 | coverage 16 | 17 | # nyc test coverage 18 | .nyc_output 19 | 20 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 21 | .grunt 22 | 23 | # node-waf configuration 24 | .lock-wscript 25 | 26 | # Dependency directories 27 | node_modules 28 | jspm_packages 29 | 30 | # Optional npm cache directory 31 | .npm 32 | 33 | # Optional REPL history 34 | .node_repl_history 35 | .next 36 | 37 | # Local files 38 | .*.local 39 | 40 | # App-specific 41 | build/ 42 | .vercel 43 | *.code-workspace 44 | .mailmap 45 | .secret -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | .next/ 3 | build/ 4 | public/ 5 | *.code-workspace 6 | *.template -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "dbaeumer.vscode-eslint", 4 | "ms-vscode.vscode-typescript-next", 5 | "hbenl.vscode-mocha-test-adapter", 6 | "esbenp.prettier-vscode" 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "git.autofetch": true, 3 | "diffEditor.ignoreTrimWhitespace": false, 4 | "javascript.updateImportsOnFileMove.enabled": "always", 5 | "git.enableSmartCommit": true, 6 | "editor.formatOnSave": true, 7 | "editor.tabSize": 2, 8 | "editor.formatOnPaste": true, 9 | "editor.defaultFormatter": "esbenp.prettier-vscode", 10 | "javascript.preferences.importModuleSpecifierEnding": "minimal", 11 | "[markdown]": { 12 | "editor.wordWrap": "bounded" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Next.js Template 2 | 3 | Getting a [Next.js](https://nextjs.org/) application up and running is not a 4 | trivial exercise, especially if you want a robust and extensible result that 5 | will support a modern development process. 6 | 7 | Here's a plug-and-play 8 | [Next.js template](https://github.com/karmaniverous/nextjs-template) that offers 9 | the following features: 10 | 11 | - Tree-shakable support for the latest ES6 goodies with 12 | [`eslint`](https://www.npmjs.com/package/eslint) _uber alles_. 13 | 14 | - User registration & authentication via 15 | [NextAuth.js](https://next-auth.js.org/), by default against an 16 | [AWS Cognito User Pool](https://docs.aws.amazon.com/cognito/latest/developerguide/cognito-user-identity-pools.html) 17 | supporting native username/password authentication and one federated identity 18 | provider (Google). 19 | 20 | - Support for public & private API endpoints, both local to NextJS and at any 21 | [AWS API Gateway](https://aws.amazon.com/api-gateway/) secured by the same 22 | Cognito User Pool. 23 | 24 | - Configured to act as a front end & authentication client for my 25 | [AWS API Template](https://github.com/karmaniverous/aws-api-template) on the 26 | back end. 27 | 28 | - Fully integrated application state management with the 29 | [Redux Toolkit](https://redux-toolkit.js.org/), including support for 30 | difficult-to-serialize types like `Date` & `BigInt`. 31 | 32 | - Responsive UX with [Semantic UI React](https://react.semantic-ui.com/) with 33 | LESS theme overrides enabled & ready for input! 34 | 35 | - A responsive & attractive sample UI that encapsulates a ton of common use 36 | cases into an opinionated architecture and a library of utility components. 37 | 38 | - Automated [`lodash`](https://www.npmjs.com/package/lodash) cherry-picking with 39 | [`babel-plugin-lodash`](https://www.npmjs.com/package/babel-plugin-lodash). 40 | 41 | - Front & back-end testing with [`mocha`](https://www.npmjs.com/package/mocha), 42 | [`chai`](https://www.npmjs.com/package/chai), and the 43 | [React Testing Library](https://www.npmjs.com/package/@testing-library/react). 44 | Includes examples and a sweet testing console! 45 | 46 | - Code formatting at every save & paste with 47 | [`prettier`](https://www.npmjs.com/package/prettier). 48 | 49 | - One-button release to GitHub with 50 | [`release-it`](https://www.npmjs.com/package/release-it). 51 | 52 | **[Click here](https://karmanivero.us/blog/nextjs-template/) for full 53 | documentation & instructions!** 54 | -------------------------------------------------------------------------------- /amplify.yml: -------------------------------------------------------------------------------- 1 | # prettier-ignore 2 | version: 1 3 | frontend: 4 | phases: 5 | preBuild: 6 | commands: 7 | - npm ci 8 | - echo -e "\n" >> .env 9 | - cat env/.env.$ENV >> .env 10 | - echo -e "\nENV=$ENV" >> .env 11 | - echo -e "\nNEXTAUTH_COGNITO_CLIENT_SECRET=$NEXTAUTH_COGNITO_CLIENT_SECRET" >> .env 12 | - echo -e "\nNEXTAUTH_SECRET=$NEXTAUTH_SECRET" >> .env 13 | build: 14 | commands: 15 | - npm run build 16 | artifacts: 17 | baseDirectory: .next 18 | files: 19 | - '**/*' 20 | cache: 21 | paths: 22 | - node_modules/**/* 23 | -------------------------------------------------------------------------------- /components/application/layout/PageFooter.jsx: -------------------------------------------------------------------------------- 1 | // npm imports 2 | import { Menu, Segment } from 'semantic-ui-react'; 3 | 4 | // component imports 5 | import SidebarLinks from '../sidebar/SidebarLinks'; 6 | 7 | const PageFooter = () => { 8 | return ( 9 | 10 | 11 | {process.env.NEXT_PUBLIC_SITE_COPYRIGHT ? ( 12 | {process.env.NEXT_PUBLIC_SITE_COPYRIGHT} 13 | ) : null} 14 | 15 | 16 | 17 | 18 | 19 | 20 | ); 21 | }; 22 | 23 | export default PageFooter; 24 | -------------------------------------------------------------------------------- /components/application/layout/PageHeader.jsx: -------------------------------------------------------------------------------- 1 | // npm imports 2 | import Link from 'next/link'; 3 | import { useSelector } from 'react-redux'; 4 | import { Grid, Header, Image } from 'semantic-ui-react'; 5 | 6 | // component imports 7 | import SidebarButton from '../sidebar/SidebarButton'; 8 | import SessionDropdown from '../session/SessionDropdown'; 9 | 10 | const PageHeader = () => { 11 | const comingSoon = useSelector((state) => state.page.comingSoon); 12 | 13 | return ( 14 | 15 | 16 | 17 | 18 |
19 | 20 | 21 | HelloWorld 22 | Nice ta meetcha! 23 | 24 |
25 | 26 |
27 | 28 | 35 | {comingSoon ? null : } 36 | 37 | 38 | 46 | 47 | 48 |
49 |
50 | ); 51 | }; 52 | 53 | export default PageHeader; 54 | -------------------------------------------------------------------------------- /components/application/session/SessionDropdown.jsx: -------------------------------------------------------------------------------- 1 | // npm imports 2 | import { useSession, signIn } from 'next-auth/react'; 3 | import { Dropdown } from 'semantic-ui-react'; 4 | 5 | // component imports 6 | import useSignOut from './useSignOut'; 7 | 8 | const SessionDropdown = () => { 9 | // Get session. 10 | const { data: session } = useSession(); 11 | 12 | // Get signOut callback. 13 | const { signOut } = useSignOut(); 14 | 15 | return ( 16 | signIn('cognito')} 20 | simple 21 | text={session ? session.user.email : 'Sign Up / Sign In'} 22 | > 23 | {session ? ( 24 | 25 | 26 | 27 | ) : null} 28 | 29 | ); 30 | }; 31 | 32 | export default SessionDropdown; 33 | -------------------------------------------------------------------------------- /components/application/session/SessionMenuItem.jsx: -------------------------------------------------------------------------------- 1 | // npm imports 2 | import { useSession, signIn } from 'next-auth/react'; 3 | import { Menu, Icon } from 'semantic-ui-react'; 4 | 5 | // component imports 6 | import useSignOut from './useSignOut'; 7 | 8 | const SessionMenuItem = () => { 9 | // Get session. 10 | const { data: session } = useSession(); 11 | 12 | // Get signOut callback. 13 | const { signOut } = useSignOut(); 14 | 15 | return ( 16 | signIn('cognito')}> 17 | {session ? ( 18 | <> 19 | {session.user.email} 20 | 21 | 22 | 23 | Sign Out 24 | 25 | 26 | 27 | ) : ( 28 | 'Sign Up / Sign In' 29 | )} 30 | 31 | ); 32 | }; 33 | 34 | export default SessionMenuItem; 35 | -------------------------------------------------------------------------------- /components/application/session/useSignOut.jsx: -------------------------------------------------------------------------------- 1 | // npm imports 2 | import { useRouter } from 'next/router'; 3 | import { signOut as nextAuthSignOut } from 'next-auth/react'; 4 | import { useCallback } from 'react'; 5 | import { useSelector } from 'react-redux'; 6 | 7 | const useSignOut = () => { 8 | // Get page state. 9 | const logoutUrl = useSelector((state) => state.page.logoutUrl); 10 | 11 | // Get router. 12 | const router = useRouter(); 13 | 14 | const signOut = useCallback(() => { 15 | nextAuthSignOut({ redirect: false }); 16 | router.push(logoutUrl); 17 | }, []); 18 | 19 | return { signOut }; 20 | }; 21 | 22 | export default useSignOut; 23 | -------------------------------------------------------------------------------- /components/application/sidebar/LinkMenuItem.jsx: -------------------------------------------------------------------------------- 1 | // npm imports 2 | import PropTypes from 'prop-types'; 3 | import { Icon, Menu } from 'semantic-ui-react'; 4 | 5 | const LinkMenuItem = ({ children, icon, renderIf, target, ...props }) => { 6 | if (!renderIf) return null; 7 | 8 | return ( 9 | 14 | {icon ? : null} 15 | 16 | {children} 17 | 18 | ); 19 | }; 20 | 21 | LinkMenuItem.propTypes = { 22 | children: PropTypes.any, 23 | icon: PropTypes.string, 24 | renderIf: PropTypes.any, 25 | target: PropTypes.string, 26 | }; 27 | 28 | export default LinkMenuItem; 29 | -------------------------------------------------------------------------------- /components/application/sidebar/PageMenuItem.jsx: -------------------------------------------------------------------------------- 1 | // npm imports 2 | import { useRouter } from 'next/router'; 3 | import { useSession } from 'next-auth/react'; 4 | import PropTypes from 'prop-types'; 5 | import { useSelector } from 'react-redux'; 6 | import { animateScroll } from 'react-scroll'; 7 | import { Menu } from 'semantic-ui-react'; 8 | 9 | // redux imports 10 | import { PAGES, resolveRoute } from '../../../state/pageSlice.mjs'; 11 | 12 | const PageMenuItem = ({ children, page, ...props }) => { 13 | // Get page state. 14 | const currentPage = useSelector((state) => state.page.currentPage); 15 | 16 | // Get session. 17 | const { data: session } = useSession(); 18 | 19 | // Create router. 20 | const router = useRouter(); 21 | 22 | return ( 23 | 31 | router.push( 32 | resolveRoute({ 33 | currentPage: page, 34 | }), 35 | null, 36 | { shallow: !session } 37 | ) 38 | } 39 | > 40 | {children} 41 | 42 | ); 43 | }; 44 | 45 | PageMenuItem.propTypes = { 46 | children: PropTypes.any, 47 | page: PropTypes.oneOf(Object.values(PAGES)), 48 | }; 49 | 50 | export default PageMenuItem; 51 | -------------------------------------------------------------------------------- /components/application/sidebar/ScrollMenuItem.jsx: -------------------------------------------------------------------------------- 1 | // npm imports 2 | import PropTypes from 'prop-types'; 3 | import { useCallback } from 'react'; 4 | import { scroller } from 'react-scroll'; 5 | import { Menu } from 'semantic-ui-react'; 6 | 7 | const ScrollMenuItem = ({ children, name, ...props }) => { 8 | // Create scrolling callback. 9 | const doScroll = useCallback( 10 | (e, { name }) => scroller.scrollTo(name, { smooth: true }), 11 | [] 12 | ); 13 | 14 | return ( 15 | 16 | {children} 17 | 18 | ); 19 | }; 20 | 21 | ScrollMenuItem.propTypes = { 22 | children: PropTypes.any, 23 | name: PropTypes.string.isRequired, 24 | }; 25 | 26 | export default ScrollMenuItem; 27 | -------------------------------------------------------------------------------- /components/application/sidebar/SidebarButton.jsx: -------------------------------------------------------------------------------- 1 | // npm imports 2 | import { Button } from 'semantic-ui-react'; 3 | import { useSelector, useDispatch } from 'react-redux'; 4 | 5 | // redux imports 6 | import { setSidebarVisible } from '../../../state/pageSlice.mjs'; 7 | 8 | const SidebarButton = () => { 9 | // Get page state. 10 | const sidebarVisible = useSelector((state) => state.page.sidebarVisible); 11 | 12 | // Create dispatcher. 13 | const dispatch = useDispatch(); 14 | 15 | return ( 16 |