├── .env.example ├── .eslintrc.js ├── .github ├── FUNDING.yml ├── issue-branch.yml └── workflows │ ├── create-branch.yml │ ├── issue-autolink.yml │ ├── lint.yml │ └── release-please.yml ├── .gitignore ├── .husky ├── commit-msg ├── post-merge └── pre-commit ├── .npmrc ├── .prettierignore ├── .prettierrc.js ├── .vscode ├── css.code-snippets ├── extensions.json ├── settings.json └── typescriptreact.code-snippets ├── README.md ├── commitlint.config.js ├── jest.config.js ├── jest.setup.js ├── next-env.d.ts ├── next-sitemap.config.js ├── next.config.js ├── package.json ├── postcss.config.js ├── public ├── favicon.ico ├── 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 │ ├── large-og.jpg │ ├── manifest.json │ ├── ms-icon-144x144.png │ ├── ms-icon-150x150.png │ ├── ms-icon-310x310.png │ └── ms-icon-70x70.png ├── fonts │ └── inter-var-latin.woff2 ├── images │ ├── large-og.png │ └── new-tab.png └── svg │ └── Vercel.svg ├── src ├── components │ ├── DismissableToast.tsx │ ├── Logo.tsx │ ├── NextImage.tsx │ ├── Seo.tsx │ ├── Skeleton.tsx │ ├── buttons │ │ └── Button.tsx │ ├── content │ │ ├── StyledCode.tsx │ │ ├── StyledJSON.tsx │ │ └── TryAlert.tsx │ ├── forms │ │ ├── DatePicker.tsx │ │ ├── DropzoneInput.tsx │ │ ├── FilePreview.tsx │ │ ├── Input.tsx │ │ ├── PasswordInput.tsx │ │ ├── SelectInput.tsx │ │ └── TextArea.tsx │ ├── hoc │ │ └── withAuth.tsx │ ├── layout │ │ ├── DashboardLayout.tsx │ │ ├── Footer.tsx │ │ ├── Header.tsx │ │ ├── Layout.tsx │ │ ├── NavigationList.tsx │ │ └── Sidebar.tsx │ └── links │ │ ├── ArrowLink.tsx │ │ ├── BackToHome.tsx │ │ ├── ButtonLink.tsx │ │ ├── PrimaryLink.tsx │ │ ├── UnderlineLink.tsx │ │ └── UnstyledLink.tsx ├── constant │ ├── env.ts │ ├── navigation.ts │ └── toast.ts ├── hooks │ └── toast │ │ ├── useLoadingToast.tsx │ │ └── useRQWithToast.tsx ├── lib │ ├── __tests__ │ │ └── helper.test.ts │ ├── axios-mock.ts │ ├── axios.ts │ ├── clsxm.ts │ ├── helper.ts │ └── logger.ts ├── pages │ ├── 404.tsx │ ├── _app.tsx │ ├── _document.tsx │ ├── api │ │ ├── hello.ts │ │ └── mock │ │ │ ├── login.ts │ │ │ └── me.ts │ ├── auth.tsx │ ├── dashboard.tsx │ ├── index.tsx │ ├── login.tsx │ ├── optional.tsx │ ├── protected.tsx │ └── sandbox │ │ ├── auth-rq.tsx │ │ ├── auth.tsx │ │ ├── rhf.tsx │ │ └── toast-rq.tsx ├── store │ └── useAuthStore.tsx ├── styles │ └── globals.css └── types │ ├── api.ts │ ├── auth.ts │ └── dropzone.ts ├── tailwind.config.js ├── tsconfig.json ├── vercel.json └── yarn.lock /.env.example: -------------------------------------------------------------------------------- 1 | 2 | # DEVELOPMENT TOOLS 3 | # Ideally, don't add them to production deployment envs 4 | NEXT_PUBLIC_SHOW_LOGGER="false" -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | browser: true, 4 | es2021: true, 5 | node: true, 6 | }, 7 | plugins: ['@typescript-eslint', 'simple-import-sort', 'unused-imports'], 8 | extends: [ 9 | 'eslint:recommended', 10 | 'next', 11 | 'next/core-web-vitals', 12 | 'plugin:@typescript-eslint/recommended', 13 | 'prettier', 14 | ], 15 | rules: { 16 | 'no-unused-vars': 'off', 17 | 'no-console': 'warn', 18 | '@typescript-eslint/explicit-module-boundary-types': 'off', 19 | 'react/no-unescaped-entities': 'off', 20 | 21 | 'react/display-name': 'off', 22 | 'react/jsx-curly-brace-presence': [ 23 | 'warn', 24 | { props: 'never', children: 'never' }, 25 | ], 26 | 27 | //#region //*=========== Unused Import =========== 28 | '@typescript-eslint/no-unused-vars': 'off', 29 | 'unused-imports/no-unused-imports': 'warn', 30 | 'unused-imports/no-unused-vars': [ 31 | 'warn', 32 | { 33 | vars: 'all', 34 | varsIgnorePattern: '^_', 35 | args: 'after-used', 36 | argsIgnorePattern: '^_', 37 | }, 38 | ], 39 | //#endregion //*======== Unused Import =========== 40 | 41 | //#region //*=========== Import Sort =========== 42 | 'simple-import-sort/exports': 'warn', 43 | 'simple-import-sort/imports': [ 44 | 'warn', 45 | { 46 | groups: [ 47 | // ext library & side effect imports 48 | ['^@?\\w', '^\\u0000'], 49 | // {s}css files 50 | ['^.+\\.s?css$'], 51 | // Lib and hooks 52 | ['^@/lib', '^@/hooks'], 53 | // static data 54 | ['^@/data'], 55 | // components 56 | ['^@/components', '^@/container'], 57 | // zustand store 58 | ['^@/store'], 59 | // Other imports 60 | ['^@/'], 61 | // relative paths up until 3 level 62 | [ 63 | '^\\./?$', 64 | '^\\.(?!/?$)', 65 | '^\\.\\./?$', 66 | '^\\.\\.(?!/?$)', 67 | '^\\.\\./\\.\\./?$', 68 | '^\\.\\./\\.\\.(?!/?$)', 69 | '^\\.\\./\\.\\./\\.\\./?$', 70 | '^\\.\\./\\.\\./\\.\\.(?!/?$)', 71 | ], 72 | ['^@/types'], 73 | // other that didnt fit in 74 | ['^'], 75 | ], 76 | }, 77 | ], 78 | //#endregion //*======== Import Sort =========== 79 | }, 80 | globals: { 81 | React: true, 82 | JSX: true, 83 | }, 84 | }; 85 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: theodorusclarence 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry 13 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 14 | -------------------------------------------------------------------------------- /.github/issue-branch.yml: -------------------------------------------------------------------------------- 1 | # https://github.com/robvanderleek/create-issue-branch#option-2-configure-github-action 2 | 3 | # ex: i4-lower_camel_upper 4 | branchName: 'i${issue.number}-${issue.title,}' 5 | branches: 6 | - label: epic 7 | skip: true 8 | - label: debt 9 | skip: true 10 | -------------------------------------------------------------------------------- /.github/workflows/create-branch.yml: -------------------------------------------------------------------------------- 1 | name: Create Branch from Issue 2 | 3 | on: 4 | issues: 5 | types: [assigned] 6 | 7 | jobs: 8 | create_issue_branch_job: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - name: Create Issue Branch 12 | uses: robvanderleek/create-issue-branch@main 13 | env: 14 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 15 | -------------------------------------------------------------------------------- /.github/workflows/issue-autolink.yml: -------------------------------------------------------------------------------- 1 | name: 'Issue Autolink' 2 | on: 3 | pull_request: 4 | types: [opened] 5 | 6 | jobs: 7 | issue-links: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: tkt-actions/add-issue-links@v1.6.0 11 | with: 12 | repo-token: '${{ secrets.GITHUB_TOKEN }}' 13 | branch-prefix: 'i' 14 | resolve: 'true' 15 | -------------------------------------------------------------------------------- /.github/workflows/lint.yml: -------------------------------------------------------------------------------- 1 | # https://github.com/kentcdodds/kentcdodds.com/blob/main/.github/workflows/deployment.yml 2 | name: Code Check 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: {} 8 | 9 | jobs: 10 | lint: 11 | name: ⬣ ESLint 12 | runs-on: ubuntu-latest 13 | steps: 14 | - name: 🛑 Cancel Previous Runs 15 | uses: styfle/cancel-workflow-action@0.9.1 16 | 17 | - name: ⬇️ Checkout repo 18 | uses: actions/checkout@v2 19 | 20 | - name: ⎔ Setup node 21 | uses: actions/setup-node@v2 22 | with: 23 | node-version: 16 24 | 25 | - name: 📥 Download deps 26 | uses: bahmutov/npm-install@v1 27 | 28 | - name: 🔬 Lint 29 | run: yarn lint:strict 30 | 31 | typecheck: 32 | name: ʦ TypeScript 33 | runs-on: ubuntu-latest 34 | steps: 35 | - name: 🛑 Cancel Previous Runs 36 | uses: styfle/cancel-workflow-action@0.9.1 37 | 38 | - name: ⬇️ Checkout repo 39 | uses: actions/checkout@v2 40 | 41 | - name: ⎔ Setup node 42 | uses: actions/setup-node@v2 43 | with: 44 | node-version: 16 45 | 46 | - name: 📥 Download deps 47 | uses: bahmutov/npm-install@v1 48 | 49 | - name: 🔎 Type check 50 | run: yarn typecheck 51 | 52 | prettier: 53 | name: 💅 Prettier 54 | runs-on: ubuntu-latest 55 | steps: 56 | - name: 🛑 Cancel Previous Runs 57 | uses: styfle/cancel-workflow-action@0.9.1 58 | 59 | - name: ⬇️ Checkout repo 60 | uses: actions/checkout@v2 61 | 62 | - name: ⎔ Setup node 63 | uses: actions/setup-node@v2 64 | with: 65 | node-version: 16 66 | 67 | - name: 📥 Download deps 68 | uses: bahmutov/npm-install@v1 69 | 70 | - name: 🔎 Type check 71 | run: yarn format:check 72 | 73 | test: 74 | name: 🃏 Test 75 | runs-on: ubuntu-latest 76 | steps: 77 | - name: 🛑 Cancel Previous Runs 78 | uses: styfle/cancel-workflow-action@0.9.1 79 | 80 | - name: ⬇️ Checkout repo 81 | uses: actions/checkout@v2 82 | 83 | - name: ⎔ Setup node 84 | uses: actions/setup-node@v2 85 | with: 86 | node-version: 16 87 | 88 | - name: 📥 Download deps 89 | uses: bahmutov/npm-install@v1 90 | 91 | - name: 🃏 Run jest 92 | run: yarn test 93 | -------------------------------------------------------------------------------- /.github/workflows/release-please.yml: -------------------------------------------------------------------------------- 1 | name: release-please 2 | on: 3 | # !Option 1: Manual Trigger from GitHub 4 | # workflow_dispatch: 5 | # !Option 2: Release on every push on main branch 6 | push: 7 | branches: 8 | - main 9 | jobs: 10 | release-please: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: google-github-actions/release-please-action@v3 14 | with: 15 | release-type: node 16 | package-name: release-please-action 17 | -------------------------------------------------------------------------------- /.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 | 36 | # next-sitemap 37 | robots.txt 38 | sitemap.xml 39 | sitemap-*.xml -------------------------------------------------------------------------------- /.husky/commit-msg: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | npx --no-install commitlint --edit "$1" 5 | -------------------------------------------------------------------------------- /.husky/post-merge: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | yarn 5 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | npx lint-staged -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | v16.14.0 2 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 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 | /.next/ 14 | /out/ 15 | 16 | # production 17 | /build 18 | 19 | # misc 20 | .DS_Store 21 | *.pem 22 | 23 | # debug 24 | npm-debug.log* 25 | yarn-debug.log* 26 | yarn-error.log* 27 | 28 | # local env files 29 | .env.local 30 | .env.development.local 31 | .env.test.local 32 | .env.production.local 33 | 34 | # vercel 35 | .vercel 36 | 37 | # changelog 38 | CHANGELOG.md 39 | -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | arrowParens: 'always', 3 | singleQuote: true, 4 | jsxSingleQuote: true, 5 | tabWidth: 2, 6 | semi: true, 7 | }; 8 | -------------------------------------------------------------------------------- /.vscode/css.code-snippets: -------------------------------------------------------------------------------- 1 | { 2 | "Region CSS": { 3 | "prefix": "regc", 4 | "body": [ 5 | "/* #region /**=========== ${1} =========== */", 6 | "$0", 7 | "/* #endregion /**======== ${1} =========== */" 8 | ] 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | // Tailwind CSS Intellisense 4 | "bradlc.vscode-tailwindcss", 5 | "esbenp.prettier-vscode", 6 | "dbaeumer.vscode-eslint", 7 | "aaron-bond.better-comments" 8 | ] 9 | } 10 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "css.validate": false, 3 | "editor.formatOnSave": true, 4 | "editor.tabSize": 2, 5 | "editor.codeActionsOnSave": { 6 | "source.fixAll": true 7 | }, 8 | "headwind.runOnSave": false, 9 | "typescript.preferences.importModuleSpecifier": "non-relative" 10 | } 11 | -------------------------------------------------------------------------------- /.vscode/typescriptreact.code-snippets: -------------------------------------------------------------------------------- 1 | { 2 | //#region //*=========== React =========== 3 | "import React": { 4 | "prefix": "ir", 5 | "body": ["import * as React from 'react';"] 6 | }, 7 | "React.useState": { 8 | "prefix": "us", 9 | "body": [ 10 | "const [${1}, set${1/(^[a-zA-Z])(.*)/${1:/upcase}${2}/}] = React.useState<$3>(${2:initial${1/(^[a-zA-Z])(.*)/${1:/upcase}${2}/}})$0" 11 | ] 12 | }, 13 | "React.useEffect": { 14 | "prefix": "uf", 15 | "body": ["React.useEffect(() => {", " $0", "}, []);"] 16 | }, 17 | "React.useReducer": { 18 | "prefix": "ur", 19 | "body": [ 20 | "const [state, dispatch] = React.useReducer(${0:someReducer}, {", 21 | " ", 22 | "})" 23 | ] 24 | }, 25 | "React.useRef": { 26 | "prefix": "urf", 27 | "body": ["const ${1:someRef} = React.useRef($0)"] 28 | }, 29 | "React Functional Component": { 30 | "prefix": "rc", 31 | "body": [ 32 | "import * as React from 'react';\n", 33 | "export default function ${1:${TM_FILENAME_BASE}}() {", 34 | " return (", 35 | "
", 36 | " $0", 37 | "
", 38 | " )", 39 | "}" 40 | ] 41 | }, 42 | "React Functional Component with Props": { 43 | "prefix": "rcp", 44 | "body": [ 45 | "import * as React from 'react';\n", 46 | "import clsxm from '@/lib/clsxm';\n", 47 | "type ${1:${TM_FILENAME_BASE}}Props= {\n", 48 | "} & React.ComponentPropsWithoutRef<'div'>\n", 49 | "export default function ${1:${TM_FILENAME_BASE}}({className, ...rest}: ${1:${TM_FILENAME_BASE}}Props) {", 50 | " return (", 51 | "
", 52 | " $0", 53 | "
", 54 | " )", 55 | "}" 56 | ] 57 | }, 58 | //#endregion //*======== React =========== 59 | 60 | //#region //*=========== Commons =========== 61 | "Region": { 62 | "prefix": "reg", 63 | "scope": "javascript, typescript, javascriptreact, typescriptreact", 64 | "body": [ 65 | "//#region //*=========== ${1} ===========", 66 | "${TM_SELECTED_TEXT}$0", 67 | "//#endregion //*======== ${1} ===========" 68 | ] 69 | }, 70 | "Region CSS": { 71 | "prefix": "regc", 72 | "scope": "css, scss", 73 | "body": [ 74 | "/* #region /**=========== ${1} =========== */", 75 | "${TM_SELECTED_TEXT}$0", 76 | "/* #endregion /**======== ${1} =========== */" 77 | ] 78 | }, 79 | //#endregion //*======== Commons =========== 80 | 81 | //#region //*=========== Nextjs =========== 82 | "Next Pages": { 83 | "prefix": "np", 84 | "body": [ 85 | "import * as React from 'react';\n", 86 | "import Layout from '@/components/layout/Layout';", 87 | "import Seo from '@/components/Seo';\n", 88 | "export default function ${1:${TM_FILENAME_BASE/(^[a-zA-Z])(.*)/${1:/upcase}${2}/}}Page() {", 89 | " return (", 90 | " ", 91 | " \n", 92 | "
\n", 93 | "
", 94 | "
", 95 | " $0", 96 | "
", 97 | "
", 98 | "
", 99 | "
", 100 | " )", 101 | "}" 102 | ] 103 | }, 104 | "Next API": { 105 | "prefix": "napi", 106 | "body": [ 107 | "import { NextApiRequest, NextApiResponse } from 'next';\n", 108 | "export default async function handler(req: NextApiRequest, res: NextApiResponse) {", 109 | " if (req.method === 'GET') {", 110 | " res.status(200).json({ name: 'Bambang' });", 111 | " } else {", 112 | " res.status(405).json({ message: 'Method Not Allowed' });", 113 | " }", 114 | "}" 115 | ] 116 | }, 117 | "Get Static Props": { 118 | "prefix": "gsp", 119 | "body": [ 120 | "export const getStaticProps = async (context: GetStaticPropsContext) => {", 121 | " return {", 122 | " props: {}", 123 | " };", 124 | "}" 125 | ] 126 | }, 127 | "Get Static Paths": { 128 | "prefix": "gspa", 129 | "body": [ 130 | "export const getStaticPaths: GetStaticPaths = async () => {", 131 | " return {", 132 | " paths: [", 133 | " { params: { $1 }}", 134 | " ],", 135 | " fallback: ", 136 | " };", 137 | "}" 138 | ] 139 | }, 140 | "Get Server Side Props": { 141 | "prefix": "gssp", 142 | "body": [ 143 | "export const getServerSideProps = async (context: GetServerSidePropsContext) => {", 144 | " return {", 145 | " props: {}", 146 | " };", 147 | "}" 148 | ] 149 | }, 150 | "Infer Get Static Props": { 151 | "prefix": "igsp", 152 | "body": "InferGetStaticPropsType" 153 | }, 154 | "Infer Get Server Side Props": { 155 | "prefix": "igssp", 156 | "body": "InferGetServerSidePropsType" 157 | }, 158 | "Import useRouter": { 159 | "prefix": "imust", 160 | "body": ["import { useRouter } from 'next/router';"] 161 | }, 162 | "Import Next Image": { 163 | "prefix": "imimg", 164 | "body": ["import Image from 'next/image';"] 165 | }, 166 | "Import Next Link": { 167 | "prefix": "iml", 168 | "body": ["import Link from 'next/link';"] 169 | }, 170 | //#endregion //*======== Nextjs =========== 171 | 172 | //#region //*=========== Snippet Wrap =========== 173 | "Wrap with Fragment": { 174 | "prefix": "ff", 175 | "body": ["<>", "\t${TM_SELECTED_TEXT}", ""] 176 | }, 177 | "Wrap with clsx": { 178 | "prefix": "cx", 179 | "body": ["{clsx(${TM_SELECTED_TEXT}$0)}"] 180 | }, 181 | "Wrap with clsxm": { 182 | "prefix": "cxm", 183 | "body": ["{clsxm(${TM_SELECTED_TEXT}$0, className)}"] 184 | }, 185 | //#endregion //*======== Snippet Wrap =========== 186 | 187 | "Logger": { 188 | "prefix": "lg", 189 | "body": [ 190 | "logger({ ${1:${CLIPBOARD}} }, '${TM_FILENAME} line ${TM_LINE_NUMBER}')" 191 | ] 192 | } 193 | } 194 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 |

Next.js Authentication HOC Pattern

3 |

Authentication pattern example using HOC, Zustand, and React Query

4 |

Made by Theodorus Clarence & Rizqi Tsani

5 |
6 | -------------------------------------------------------------------------------- /commitlint.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: ['@commitlint/config-conventional'], 3 | rules: { 4 | // TODO Add Scope Enum Here 5 | // 'scope-enum': [2, 'always', ['yourscope', 'yourscope']], 6 | 'type-enum': [ 7 | 2, 8 | 'always', 9 | [ 10 | 'feat', 11 | 'fix', 12 | 'docs', 13 | 'chore', 14 | 'style', 15 | 'refactor', 16 | 'ci', 17 | 'test', 18 | 'perf', 19 | 'revert', 20 | 'vercel', 21 | ], 22 | ], 23 | }, 24 | }; 25 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line @typescript-eslint/no-var-requires 2 | const nextJest = require('next/jest'); 3 | 4 | const createJestConfig = nextJest({ 5 | // Provide the path to your Next.js app to load next.config.js and .env files in your test environment 6 | dir: './', 7 | }); 8 | 9 | // Add any custom config to be passed to Jest 10 | const customJestConfig = { 11 | // Add more setup options before each test is run 12 | setupFilesAfterEnv: ['/jest.setup.js'], 13 | 14 | // if using TypeScript with a baseUrl set to the root directory then you need the below for alias' to work 15 | moduleDirectories: ['node_modules', '/'], 16 | 17 | testEnvironment: 'jest-environment-jsdom', 18 | 19 | /** 20 | * Absolute imports and Module Path Aliases 21 | */ 22 | moduleNameMapper: { 23 | '^@/(.*)$': '/src/$1', 24 | '^~/(.*)$': '/public/$1', 25 | }, 26 | }; 27 | 28 | // createJestConfig is exported this way to ensure that next/jest can load the Next.js config which is async 29 | module.exports = createJestConfig(customJestConfig); 30 | -------------------------------------------------------------------------------- /jest.setup.js: -------------------------------------------------------------------------------- 1 | import '@testing-library/jest-dom/extend-expect'; 2 | -------------------------------------------------------------------------------- /next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | 4 | // NOTE: This file should not be edited 5 | // see https://nextjs.org/docs/basic-features/typescript for more information. 6 | -------------------------------------------------------------------------------- /next-sitemap.config.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @type {import('next-sitemap').IConfig} 3 | * @see https://github.com/iamvishnusankar/next-sitemap#readme 4 | */ 5 | module.exports = { 6 | /** Without additional '/' on the end, e.g. https://theodorusclarence.com */ 7 | siteUrl: 'https://auth-hoc.thcl.dev', 8 | generateRobotsTxt: true, 9 | robotsTxtOptions: { 10 | policies: [{ userAgent: '*', allow: '/' }], 11 | }, 12 | }; 13 | -------------------------------------------------------------------------------- /next.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | module.exports = { 3 | eslint: { 4 | dirs: ['src'], 5 | }, 6 | 7 | reactStrictMode: true, 8 | 9 | // Uncoment to add domain whitelist 10 | images: { 11 | domains: ['images.unsplash.com'], 12 | }, 13 | 14 | // SVGR 15 | webpack(config) { 16 | config.module.rules.push({ 17 | test: /\.svg$/i, 18 | issuer: /\.[jt]sx?$/, 19 | use: [ 20 | { 21 | loader: '@svgr/webpack', 22 | options: { 23 | typescript: true, 24 | icon: true, 25 | }, 26 | }, 27 | ], 28 | }); 29 | 30 | return config; 31 | }, 32 | }; 33 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ts-nextjs-tailwind-starter", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "start": "next start", 9 | "lint": "next lint", 10 | "lint:fix": "eslint src --fix && yarn format", 11 | "lint:strict": "eslint --max-warnings=0 src", 12 | "typecheck": "tsc --noEmit --incremental false", 13 | "test:watch": "jest --watch", 14 | "test": "jest", 15 | "format": "prettier -w .", 16 | "format:check": "prettier -c .", 17 | "postbuild": "next-sitemap --config next-sitemap.config.js", 18 | "prepare": "husky install" 19 | }, 20 | "dependencies": { 21 | "@headlessui/react": "^1.7.2", 22 | "@tanstack/react-query": "^4.3.4", 23 | "@tanstack/react-query-devtools": "^4.3.5", 24 | "auto-zustand-selectors-hook": "^2.0.0", 25 | "axios": "^0.27.2", 26 | "clsx": "^1.2.1", 27 | "immer": "^9.0.15", 28 | "next": "^12.2.5", 29 | "react": "^18.2.0", 30 | "react-datepicker": "^4.8.0", 31 | "react-dom": "^18.2.0", 32 | "react-dropzone": "^14.2.2", 33 | "react-highlight": "^0.14.0", 34 | "react-hook-form": "^7.34.2", 35 | "react-hot-toast": "^2.3.0", 36 | "react-icons": "^4.4.0", 37 | "react-image-lightbox": "^5.1.4", 38 | "tailwind-merge": "^1.6.0", 39 | "zustand": "^4.1.1" 40 | }, 41 | "devDependencies": { 42 | "@commitlint/cli": "^16.3.0", 43 | "@commitlint/config-conventional": "^16.2.4", 44 | "@svgr/webpack": "^6.3.1", 45 | "@tailwindcss/forms": "^0.5.3", 46 | "@tailwindcss/typography": "^0.5.7", 47 | "@testing-library/jest-dom": "^5.16.5", 48 | "@testing-library/react": "^13.3.0", 49 | "@types/react": "^18.0.18", 50 | "@types/react-datepicker": "^4.4.2", 51 | "@types/react-highlight": "^0.12.5", 52 | "@typescript-eslint/eslint-plugin": "^5.36.1", 53 | "@typescript-eslint/parser": "^5.36.1", 54 | "autoprefixer": "^10.4.8", 55 | "eslint": "^8.23.0", 56 | "eslint-config-next": "^12.2.5", 57 | "eslint-config-prettier": "^8.5.0", 58 | "eslint-plugin-simple-import-sort": "^7.0.0", 59 | "eslint-plugin-unused-imports": "^2.0.0", 60 | "husky": "^7.0.4", 61 | "jest": "^27.5.1", 62 | "lint-staged": "^12.5.0", 63 | "next-sitemap": "^2.5.28", 64 | "postcss": "^8.4.16", 65 | "prettier": "^2.7.1", 66 | "prettier-plugin-tailwindcss": "^0.1.13", 67 | "tailwindcss": "^3.1.8", 68 | "typescript": "^4.8.2" 69 | }, 70 | "lint-staged": { 71 | "src/**/*.{js,jsx,ts,tsx}": [ 72 | "eslint --max-warnings=0", 73 | "prettier -w" 74 | ], 75 | "src/**/*.{json,css,scss,md}": [ 76 | "prettier -w" 77 | ] 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | }; 7 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theodorusclarence/nextjs-with-auth-hoc/af85e0a0fb8ce83ae3522074fd6009bdc9cfadb1/public/favicon.ico -------------------------------------------------------------------------------- /public/favicon/android-icon-144x144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theodorusclarence/nextjs-with-auth-hoc/af85e0a0fb8ce83ae3522074fd6009bdc9cfadb1/public/favicon/android-icon-144x144.png -------------------------------------------------------------------------------- /public/favicon/android-icon-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theodorusclarence/nextjs-with-auth-hoc/af85e0a0fb8ce83ae3522074fd6009bdc9cfadb1/public/favicon/android-icon-192x192.png -------------------------------------------------------------------------------- /public/favicon/android-icon-36x36.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theodorusclarence/nextjs-with-auth-hoc/af85e0a0fb8ce83ae3522074fd6009bdc9cfadb1/public/favicon/android-icon-36x36.png -------------------------------------------------------------------------------- /public/favicon/android-icon-48x48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theodorusclarence/nextjs-with-auth-hoc/af85e0a0fb8ce83ae3522074fd6009bdc9cfadb1/public/favicon/android-icon-48x48.png -------------------------------------------------------------------------------- /public/favicon/android-icon-72x72.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theodorusclarence/nextjs-with-auth-hoc/af85e0a0fb8ce83ae3522074fd6009bdc9cfadb1/public/favicon/android-icon-72x72.png -------------------------------------------------------------------------------- /public/favicon/android-icon-96x96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theodorusclarence/nextjs-with-auth-hoc/af85e0a0fb8ce83ae3522074fd6009bdc9cfadb1/public/favicon/android-icon-96x96.png -------------------------------------------------------------------------------- /public/favicon/apple-icon-114x114.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theodorusclarence/nextjs-with-auth-hoc/af85e0a0fb8ce83ae3522074fd6009bdc9cfadb1/public/favicon/apple-icon-114x114.png -------------------------------------------------------------------------------- /public/favicon/apple-icon-120x120.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theodorusclarence/nextjs-with-auth-hoc/af85e0a0fb8ce83ae3522074fd6009bdc9cfadb1/public/favicon/apple-icon-120x120.png -------------------------------------------------------------------------------- /public/favicon/apple-icon-144x144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theodorusclarence/nextjs-with-auth-hoc/af85e0a0fb8ce83ae3522074fd6009bdc9cfadb1/public/favicon/apple-icon-144x144.png -------------------------------------------------------------------------------- /public/favicon/apple-icon-152x152.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theodorusclarence/nextjs-with-auth-hoc/af85e0a0fb8ce83ae3522074fd6009bdc9cfadb1/public/favicon/apple-icon-152x152.png -------------------------------------------------------------------------------- /public/favicon/apple-icon-180x180.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theodorusclarence/nextjs-with-auth-hoc/af85e0a0fb8ce83ae3522074fd6009bdc9cfadb1/public/favicon/apple-icon-180x180.png -------------------------------------------------------------------------------- /public/favicon/apple-icon-57x57.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theodorusclarence/nextjs-with-auth-hoc/af85e0a0fb8ce83ae3522074fd6009bdc9cfadb1/public/favicon/apple-icon-57x57.png -------------------------------------------------------------------------------- /public/favicon/apple-icon-60x60.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theodorusclarence/nextjs-with-auth-hoc/af85e0a0fb8ce83ae3522074fd6009bdc9cfadb1/public/favicon/apple-icon-60x60.png -------------------------------------------------------------------------------- /public/favicon/apple-icon-72x72.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theodorusclarence/nextjs-with-auth-hoc/af85e0a0fb8ce83ae3522074fd6009bdc9cfadb1/public/favicon/apple-icon-72x72.png -------------------------------------------------------------------------------- /public/favicon/apple-icon-76x76.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theodorusclarence/nextjs-with-auth-hoc/af85e0a0fb8ce83ae3522074fd6009bdc9cfadb1/public/favicon/apple-icon-76x76.png -------------------------------------------------------------------------------- /public/favicon/apple-icon-precomposed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theodorusclarence/nextjs-with-auth-hoc/af85e0a0fb8ce83ae3522074fd6009bdc9cfadb1/public/favicon/apple-icon-precomposed.png -------------------------------------------------------------------------------- /public/favicon/apple-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theodorusclarence/nextjs-with-auth-hoc/af85e0a0fb8ce83ae3522074fd6009bdc9cfadb1/public/favicon/apple-icon.png -------------------------------------------------------------------------------- /public/favicon/browserconfig.xml: -------------------------------------------------------------------------------- 1 | 2 | #ffffff -------------------------------------------------------------------------------- /public/favicon/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theodorusclarence/nextjs-with-auth-hoc/af85e0a0fb8ce83ae3522074fd6009bdc9cfadb1/public/favicon/favicon-16x16.png -------------------------------------------------------------------------------- /public/favicon/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theodorusclarence/nextjs-with-auth-hoc/af85e0a0fb8ce83ae3522074fd6009bdc9cfadb1/public/favicon/favicon-32x32.png -------------------------------------------------------------------------------- /public/favicon/favicon-96x96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theodorusclarence/nextjs-with-auth-hoc/af85e0a0fb8ce83ae3522074fd6009bdc9cfadb1/public/favicon/favicon-96x96.png -------------------------------------------------------------------------------- /public/favicon/large-og.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theodorusclarence/nextjs-with-auth-hoc/af85e0a0fb8ce83ae3522074fd6009bdc9cfadb1/public/favicon/large-og.jpg -------------------------------------------------------------------------------- /public/favicon/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "App", 3 | "icons": [ 4 | { 5 | "src": "/android-icon-36x36.png", 6 | "sizes": "36x36", 7 | "type": "image/png", 8 | "density": "0.75" 9 | }, 10 | { 11 | "src": "/android-icon-48x48.png", 12 | "sizes": "48x48", 13 | "type": "image/png", 14 | "density": "1.0" 15 | }, 16 | { 17 | "src": "/android-icon-72x72.png", 18 | "sizes": "72x72", 19 | "type": "image/png", 20 | "density": "1.5" 21 | }, 22 | { 23 | "src": "/android-icon-96x96.png", 24 | "sizes": "96x96", 25 | "type": "image/png", 26 | "density": "2.0" 27 | }, 28 | { 29 | "src": "/android-icon-144x144.png", 30 | "sizes": "144x144", 31 | "type": "image/png", 32 | "density": "3.0" 33 | }, 34 | { 35 | "src": "/android-icon-192x192.png", 36 | "sizes": "192x192", 37 | "type": "image/png", 38 | "density": "4.0" 39 | } 40 | ] 41 | } 42 | -------------------------------------------------------------------------------- /public/favicon/ms-icon-144x144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theodorusclarence/nextjs-with-auth-hoc/af85e0a0fb8ce83ae3522074fd6009bdc9cfadb1/public/favicon/ms-icon-144x144.png -------------------------------------------------------------------------------- /public/favicon/ms-icon-150x150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theodorusclarence/nextjs-with-auth-hoc/af85e0a0fb8ce83ae3522074fd6009bdc9cfadb1/public/favicon/ms-icon-150x150.png -------------------------------------------------------------------------------- /public/favicon/ms-icon-310x310.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theodorusclarence/nextjs-with-auth-hoc/af85e0a0fb8ce83ae3522074fd6009bdc9cfadb1/public/favicon/ms-icon-310x310.png -------------------------------------------------------------------------------- /public/favicon/ms-icon-70x70.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theodorusclarence/nextjs-with-auth-hoc/af85e0a0fb8ce83ae3522074fd6009bdc9cfadb1/public/favicon/ms-icon-70x70.png -------------------------------------------------------------------------------- /public/fonts/inter-var-latin.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theodorusclarence/nextjs-with-auth-hoc/af85e0a0fb8ce83ae3522074fd6009bdc9cfadb1/public/fonts/inter-var-latin.woff2 -------------------------------------------------------------------------------- /public/images/large-og.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theodorusclarence/nextjs-with-auth-hoc/af85e0a0fb8ce83ae3522074fd6009bdc9cfadb1/public/images/large-og.png -------------------------------------------------------------------------------- /public/images/new-tab.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theodorusclarence/nextjs-with-auth-hoc/af85e0a0fb8ce83ae3522074fd6009bdc9cfadb1/public/images/new-tab.png -------------------------------------------------------------------------------- /public/svg/Vercel.svg: -------------------------------------------------------------------------------- 1 | Vercel -------------------------------------------------------------------------------- /src/components/DismissableToast.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { toast, ToastBar, Toaster } from 'react-hot-toast'; 3 | import { HiX } from 'react-icons/hi'; 4 | 5 | export default function DismissableToast() { 6 | return ( 7 |
8 | 19 | {(t) => ( 20 | 21 | {({ icon, message }) => ( 22 | <> 23 | {icon} 24 | {message} 25 | {t.type !== 'loading' && ( 26 | 32 | )} 33 | 34 | )} 35 | 36 | )} 37 | 38 |
39 | ); 40 | } 41 | -------------------------------------------------------------------------------- /src/components/Logo.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | import clsxm from '@/lib/clsxm'; 4 | 5 | import Vercel from '~/svg/Vercel.svg'; 6 | 7 | type LogoProps = React.ComponentPropsWithoutRef<'div'>; 8 | 9 | export default function Logo({ className, ...rest }: LogoProps) { 10 | return ( 11 |
12 | 13 |
14 | ); 15 | } 16 | -------------------------------------------------------------------------------- /src/components/NextImage.tsx: -------------------------------------------------------------------------------- 1 | import Image, { ImageProps } from 'next/image'; 2 | import * as React from 'react'; 3 | 4 | import clsxm from '@/lib/clsxm'; 5 | 6 | type NextImageProps = { 7 | useSkeleton?: boolean; 8 | imgClassName?: string; 9 | blurClassName?: string; 10 | alt: string; 11 | } & ( 12 | | { width: string | number; height: string | number } 13 | | { layout: 'fill'; width?: string | number; height?: string | number } 14 | ) & 15 | ImageProps; 16 | 17 | /** 18 | * 19 | * @description Must set width using `w-` className 20 | * @param useSkeleton add background with pulse animation, don't use it if image is transparent 21 | */ 22 | export default function NextImage({ 23 | useSkeleton = false, 24 | src, 25 | width, 26 | height, 27 | alt, 28 | className, 29 | imgClassName, 30 | blurClassName, 31 | ...rest 32 | }: NextImageProps) { 33 | const [status, setStatus] = React.useState( 34 | useSkeleton ? 'loading' : 'complete' 35 | ); 36 | const widthIsSet = className?.includes('w-') ?? false; 37 | 38 | return ( 39 |
43 | {alt} setStatus('complete')} 53 | layout='responsive' 54 | {...rest} 55 | /> 56 |
57 | ); 58 | } 59 | -------------------------------------------------------------------------------- /src/components/Seo.tsx: -------------------------------------------------------------------------------- 1 | import Head from 'next/head'; 2 | import { useRouter } from 'next/router'; 3 | 4 | import { openGraph } from '@/lib/helper'; 5 | 6 | const defaultMeta = { 7 | title: 'Next.js Authentication HOC Pattern', 8 | siteName: 'Next.js Authentication HOC Pattern', 9 | description: 10 | 'Authentication pattern example using Higher Order Component, Zustand, and React Query', 11 | /** Without additional '/' on the end, e.g. https://theodorusclarence.com */ 12 | url: 'https://auth-hoc.thcl.dev', 13 | type: 'website', 14 | robots: 'follow, index', 15 | /** 16 | * No need to be filled, will be populated with openGraph function 17 | * If you wish to use a normal image, just specify the path below 18 | */ 19 | image: 'https://tsnext-tw.thcl.dev/images/large-og.png', 20 | }; 21 | 22 | type SeoProps = { 23 | date?: string; 24 | templateTitle?: string; 25 | } & Partial; 26 | 27 | export default function Seo(props: SeoProps) { 28 | const router = useRouter(); 29 | const meta = { 30 | ...defaultMeta, 31 | ...props, 32 | }; 33 | meta['title'] = props.templateTitle 34 | ? `${props.templateTitle} | ${meta.siteName}` 35 | : meta.title; 36 | 37 | // Use siteName if there is templateTitle 38 | // but show full title if there is none 39 | // ? Uncomment code below if you want to use default open graph 40 | meta['image'] = openGraph({ 41 | description: meta.description, 42 | siteName: props.templateTitle ? meta.siteName : meta.title, 43 | templateTitle: props.templateTitle, 44 | }); 45 | 46 | return ( 47 | 48 | {meta.title} 49 | 50 | 51 | 52 | 53 | {/* Open Graph */} 54 | 55 | 56 | 57 | 58 | 59 | {/* Twitter */} 60 | 61 | 62 | 63 | 64 | 65 | {meta.date && ( 66 | <> 67 | 68 | 73 | 78 | 79 | )} 80 | 81 | {/* Favicons */} 82 | {favicons.map((linkProps) => ( 83 | 84 | ))} 85 | 86 | 90 | 91 | 92 | ); 93 | } 94 | 95 | type Favicons = { 96 | rel: string; 97 | href: string; 98 | sizes?: string; 99 | type?: string; 100 | }; 101 | 102 | const favicons: Array = [ 103 | { 104 | rel: 'apple-touch-icon', 105 | sizes: '57x57', 106 | href: '/favicon/apple-icon-57x57.png', 107 | }, 108 | { 109 | rel: 'apple-touch-icon', 110 | sizes: '60x60', 111 | href: '/favicon/apple-icon-60x60.png', 112 | }, 113 | { 114 | rel: 'apple-touch-icon', 115 | sizes: '72x72', 116 | href: '/favicon/apple-icon-72x72.png', 117 | }, 118 | { 119 | rel: 'apple-touch-icon', 120 | sizes: '76x76', 121 | href: '/favicon/apple-icon-76x76.png', 122 | }, 123 | { 124 | rel: 'apple-touch-icon', 125 | sizes: '114x114', 126 | href: '/favicon/apple-icon-114x114.png', 127 | }, 128 | { 129 | rel: 'apple-touch-icon', 130 | sizes: '120x120', 131 | href: '/favicon/apple-icon-120x120.png', 132 | }, 133 | { 134 | rel: 'apple-touch-icon', 135 | sizes: '144x144', 136 | href: '/favicon/apple-icon-144x144.png', 137 | }, 138 | { 139 | rel: 'apple-touch-icon', 140 | sizes: '152x152', 141 | href: '/favicon/apple-icon-152x152.png', 142 | }, 143 | { 144 | rel: 'apple-touch-icon', 145 | sizes: '180x180', 146 | href: '/favicon/apple-icon-180x180.png', 147 | }, 148 | { 149 | rel: 'icon', 150 | type: 'image/png', 151 | sizes: '192x192', 152 | href: '/favicon/android-icon-192x192.png', 153 | }, 154 | { 155 | rel: 'icon', 156 | type: 'image/png', 157 | sizes: '32x32', 158 | href: '/favicon/favicon-32x32.png', 159 | }, 160 | { 161 | rel: 'icon', 162 | type: 'image/png', 163 | sizes: '96x96', 164 | href: '/favicon/favicon-96x96.png', 165 | }, 166 | { 167 | rel: 'icon', 168 | type: 'image/png', 169 | sizes: '16x16', 170 | href: '/favicon/favicon-16x16.png', 171 | }, 172 | { 173 | rel: 'manifest', 174 | href: '/favicon/manifest.json', 175 | }, 176 | ]; 177 | -------------------------------------------------------------------------------- /src/components/Skeleton.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | import clsxm from '@/lib/clsxm'; 4 | 5 | type SkeletonProps = React.ComponentPropsWithoutRef<'div'>; 6 | 7 | export default function Skeleton({ className, ...rest }: SkeletonProps) { 8 | return ( 9 |
19 | ); 20 | } 21 | -------------------------------------------------------------------------------- /src/components/buttons/Button.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { ImSpinner2 } from 'react-icons/im'; 3 | 4 | import clsxm from '@/lib/clsxm'; 5 | 6 | enum ButtonVariant { 7 | 'primary', 8 | 'outline', 9 | 'ghost', 10 | 'light', 11 | 'dark', 12 | } 13 | 14 | type ButtonProps = { 15 | isLoading?: boolean; 16 | isDarkBg?: boolean; 17 | variant?: keyof typeof ButtonVariant; 18 | } & React.ComponentPropsWithRef<'button'>; 19 | 20 | const Button = React.forwardRef( 21 | ( 22 | { 23 | children, 24 | className, 25 | disabled: buttonDisabled, 26 | isLoading, 27 | variant = 'primary', 28 | isDarkBg = false, 29 | ...rest 30 | }, 31 | ref 32 | ) => { 33 | const disabled = isLoading || buttonDisabled; 34 | 35 | return ( 36 | 104 | ); 105 | } 106 | ); 107 | 108 | export default Button; 109 | -------------------------------------------------------------------------------- /src/components/content/StyledCode.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import Highlight, { HighlightProps } from 'react-highlight'; 3 | 4 | import 'highlight.js/styles/nord.css'; 5 | 6 | import clsxm from '@/lib/clsxm'; 7 | 8 | type StyledCodeProps = HighlightProps; 9 | 10 | export default function StyledCode({ 11 | className, 12 | children, 13 | ...rest 14 | }: StyledCodeProps) { 15 | return ( 16 | 17 | {children} 18 | 19 | ); 20 | } 21 | -------------------------------------------------------------------------------- /src/components/content/StyledJSON.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import Highlight, { HighlightProps } from 'react-highlight'; 3 | 4 | import 'highlight.js/styles/nord.css'; 5 | 6 | import clsxm from '@/lib/clsxm'; 7 | 8 | type StyledJSONProps = { data: unknown } & HighlightProps; 9 | 10 | export default function StyledJSON({ 11 | className, 12 | data, 13 | ...rest 14 | }: StyledJSONProps) { 15 | return ( 16 | 17 | {JSON.stringify(data, null, 2)} 18 | 19 | ); 20 | } 21 | -------------------------------------------------------------------------------- /src/components/content/TryAlert.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | import clsxm from '@/lib/clsxm'; 4 | 5 | type TryAlertProps = { 6 | withoutTitle?: boolean; 7 | } & React.ComponentPropsWithoutRef<'div'>; 8 | 9 | export default function TryAlert({ 10 | className, 11 | children, 12 | withoutTitle = false, 13 | ...rest 14 | }: TryAlertProps) { 15 | return ( 16 |
23 | {!withoutTitle &&

Try it out!

} 24 | {children} 25 |
26 | ); 27 | } 28 | -------------------------------------------------------------------------------- /src/components/forms/DatePicker.tsx: -------------------------------------------------------------------------------- 1 | import clsx from 'clsx'; 2 | import ReactDatePicker, { ReactDatePickerProps } from 'react-datepicker'; 3 | import { Controller, RegisterOptions, useFormContext } from 'react-hook-form'; 4 | import { HiOutlineCalendar } from 'react-icons/hi'; 5 | 6 | import 'react-datepicker/dist/react-datepicker.css'; 7 | 8 | type DatePickerProps = { 9 | validation?: RegisterOptions; 10 | label: string; 11 | id: string; 12 | placeholder?: string; 13 | defaultYear?: number; 14 | defaultMonth?: number; 15 | defaultValue?: string; 16 | helperText?: string; 17 | readOnly?: boolean; 18 | } & Omit; 19 | 20 | export default function DatePicker({ 21 | validation, 22 | label, 23 | id, 24 | placeholder, 25 | defaultYear, 26 | defaultMonth, 27 | defaultValue, 28 | helperText, 29 | readOnly = false, 30 | ...rest 31 | }: DatePickerProps) { 32 | const { 33 | formState: { errors }, 34 | control, 35 | } = useFormContext(); 36 | 37 | // If there is a year default, then change the year to the props 38 | const defaultDate = new Date(); 39 | if (defaultYear) defaultDate.setFullYear(defaultYear); 40 | if (defaultMonth) defaultDate.setMonth(defaultMonth); 41 | 42 | return ( 43 |
44 | 47 | 48 | ( 54 | <> 55 |
56 | 79 | 80 |
81 |
82 | {helperText !== '' && ( 83 |

{helperText}

84 | )} 85 | {errors[id] && ( 86 | 87 | {errors[id]?.message as unknown as string} 88 | 89 | )} 90 |
91 | 92 | )} 93 | /> 94 |
95 | ); 96 | } 97 | -------------------------------------------------------------------------------- /src/components/forms/DropzoneInput.tsx: -------------------------------------------------------------------------------- 1 | import clsx from 'clsx'; 2 | import * as React from 'react'; 3 | import { Accept, FileRejection, useDropzone } from 'react-dropzone'; 4 | import { Controller, useFormContext } from 'react-hook-form'; 5 | 6 | import FilePreview from '@/components/forms/FilePreview'; 7 | 8 | import { FileWithPreview } from '@/types/dropzone'; 9 | 10 | type DropzoneInputProps = { 11 | accept?: Accept; 12 | helperText?: string; 13 | id: string; 14 | label: string; 15 | maxFiles?: number; 16 | readOnly?: boolean; 17 | validation?: Record; 18 | }; 19 | 20 | export default function DropzoneInput({ 21 | accept, 22 | helperText = '', 23 | id, 24 | label, 25 | maxFiles = 1, 26 | validation, 27 | readOnly, 28 | }: DropzoneInputProps) { 29 | const { 30 | control, 31 | getValues, 32 | setValue, 33 | setError, 34 | clearErrors, 35 | formState: { errors }, 36 | } = useFormContext(); 37 | 38 | //#region //*=========== Error Focus =========== 39 | const dropzoneRef = React.useRef(null); 40 | 41 | React.useEffect(() => { 42 | errors[id] && dropzoneRef.current?.focus(); 43 | }, [errors, id]); 44 | //#endregion //*======== Error Focus =========== 45 | 46 | const [files, setFiles] = React.useState( 47 | getValues(id) || [] 48 | ); 49 | 50 | const onDrop = React.useCallback( 51 | (acceptedFiles: T[], rejectedFiles: FileRejection[]) => { 52 | if (rejectedFiles && rejectedFiles.length > 0) { 53 | setValue(id, files ? [...files] : null); 54 | setError(id, { 55 | type: 'manual', 56 | message: rejectedFiles && rejectedFiles[0].errors[0].message, 57 | }); 58 | } else { 59 | const acceptedFilesPreview = acceptedFiles.map((file: T) => 60 | Object.assign(file, { 61 | preview: URL.createObjectURL(file), 62 | }) 63 | ); 64 | 65 | setFiles( 66 | files 67 | ? [...files, ...acceptedFilesPreview].slice(0, maxFiles) 68 | : acceptedFilesPreview 69 | ); 70 | 71 | setValue( 72 | id, 73 | files 74 | ? [...files, ...acceptedFiles].slice(0, maxFiles) 75 | : acceptedFiles, 76 | { 77 | shouldValidate: true, 78 | } 79 | ); 80 | clearErrors(id); 81 | } 82 | }, 83 | [clearErrors, files, id, maxFiles, setError, setValue] 84 | ); 85 | 86 | React.useEffect(() => { 87 | return () => { 88 | () => { 89 | files.forEach((file) => URL.revokeObjectURL(file.preview)); 90 | }; 91 | }; 92 | }, [files]); 93 | 94 | const deleteFile = ( 95 | e: React.MouseEvent, 96 | file: FileWithPreview 97 | ) => { 98 | e.preventDefault(); 99 | const newFiles = [...files]; 100 | 101 | newFiles.splice(newFiles.indexOf(file), 1); 102 | 103 | if (newFiles.length > 0) { 104 | setFiles(newFiles); 105 | setValue(id, newFiles, { 106 | shouldValidate: true, 107 | shouldDirty: true, 108 | shouldTouch: true, 109 | }); 110 | } else { 111 | setFiles([]); 112 | setValue(id, null, { 113 | shouldValidate: true, 114 | shouldDirty: true, 115 | shouldTouch: true, 116 | }); 117 | } 118 | }; 119 | 120 | const { getRootProps, getInputProps } = useDropzone({ 121 | onDrop, 122 | accept, 123 | maxFiles, 124 | maxSize: 1000000, 125 | }); 126 | 127 | return ( 128 |
129 | 132 | 133 | {readOnly && !(files?.length > 0) ? ( 134 |
135 | No file uploaded 136 |
137 | ) : files?.length >= maxFiles ? ( 138 |
    139 | {files.map((file, index) => ( 140 | 146 | ))} 147 |
148 | ) : ( 149 | ( 154 | <> 155 |
160 | 161 |
169 |
170 | 184 |

185 | Drag and drop file here, or click to choose file 186 |

187 |

{`${ 188 | maxFiles - (files?.length || 0) 189 | } file(s) remaining`}

190 |
191 |
192 |
193 | 194 |
195 | {helperText !== '' && ( 196 |

{helperText}

197 | )} 198 | {errors[id] && ( 199 |

200 | {errors[id]?.message as unknown as string} 201 |

202 | )} 203 |
204 | {!readOnly && !!files?.length && ( 205 |
    206 | {files.map((file, index) => ( 207 | 213 | ))} 214 |
215 | )} 216 | 217 | )} 218 | /> 219 | )} 220 |
221 | ); 222 | } 223 | -------------------------------------------------------------------------------- /src/components/forms/FilePreview.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { 3 | HiOutlineExternalLink, 4 | HiOutlineEye, 5 | HiOutlinePaperClip, 6 | HiOutlinePhotograph, 7 | HiX, 8 | } from 'react-icons/hi'; 9 | import Lightbox from 'react-image-lightbox'; 10 | 11 | import 'react-image-lightbox/style.css'; 12 | 13 | import UnstyledLink from '@/components/links/UnstyledLink'; 14 | 15 | import { FileWithPreview } from '@/types/dropzone'; 16 | 17 | type FilePreviewProps = { 18 | file: FileWithPreview; 19 | } & ( 20 | | { 21 | deleteFile?: ( 22 | e: React.MouseEvent, 23 | file: FileWithPreview 24 | ) => void; 25 | readOnly?: true; 26 | } 27 | | { 28 | deleteFile: ( 29 | e: React.MouseEvent, 30 | file: FileWithPreview 31 | ) => void; 32 | readOnly?: false; 33 | } 34 | ); 35 | 36 | export default function FilePreview({ 37 | deleteFile, 38 | file, 39 | readOnly, 40 | }: FilePreviewProps): React.ReactElement { 41 | const [index, setIndex] = React.useState(0); 42 | const [isOpen, setIsOpen] = React.useState(false); 43 | 44 | const images = [file.preview]; 45 | 46 | const handleDelete = (e: React.MouseEvent) => { 47 | e.stopPropagation(); 48 | deleteFile?.(e, file); 49 | }; 50 | 51 | const imagesType = ['image/png', 'image/jpg', 'image/jpeg']; 52 | 53 | return imagesType.includes(file.type) ? ( 54 | <> 55 |
  • 59 |
    60 |
    66 |
    67 | 74 | {!readOnly && ( 75 | 82 | )} 83 |
    84 |
  • 85 | {isOpen && ( 86 | setIsOpen(false)} 91 | onMovePrevRequest={() => 92 | setIndex( 93 | (prevIndex) => (prevIndex + images.length - 1) % images.length 94 | ) 95 | } 96 | onMoveNextRequest={() => 97 | setIndex((prevIndex) => (prevIndex + 1) % images.length) 98 | } 99 | /> 100 | )} 101 | 102 | ) : ( 103 |
  • 107 |
    108 |
    114 |
    115 | 119 | 120 | 121 | {!readOnly && ( 122 | 129 | )} 130 |
    131 |
  • 132 | ); 133 | } 134 | -------------------------------------------------------------------------------- /src/components/forms/Input.tsx: -------------------------------------------------------------------------------- 1 | import clsx from 'clsx'; 2 | import * as React from 'react'; 3 | import { RegisterOptions, useFormContext } from 'react-hook-form'; 4 | import { HiExclamationCircle } from 'react-icons/hi'; 5 | 6 | export type InputProps = { 7 | /** Input label */ 8 | label: string; 9 | /** 10 | * id to be initialized with React Hook Form, 11 | * must be the same with the pre-defined types. 12 | */ 13 | id: string; 14 | /** Input placeholder */ 15 | placeholder?: string; 16 | /** Small text below input, useful for additional information */ 17 | helperText?: string; 18 | /** 19 | * Input type 20 | * @example text, email, password 21 | */ 22 | type?: React.HTMLInputTypeAttribute; 23 | /** Disables the input and shows defaultValue (can be set from React Hook Form) */ 24 | readOnly?: boolean; 25 | /** Disable error style (not disabling error validation) */ 26 | hideError?: boolean; 27 | /** Manual validation using RHF, it is encouraged to use yup resolver instead */ 28 | validation?: RegisterOptions; 29 | } & React.ComponentPropsWithoutRef<'input'>; 30 | 31 | export default function Input({ 32 | label, 33 | placeholder = '', 34 | helperText, 35 | id, 36 | type = 'text', 37 | readOnly = false, 38 | hideError = false, 39 | validation, 40 | ...rest 41 | }: InputProps) { 42 | const { 43 | register, 44 | formState: { errors }, 45 | } = useFormContext(); 46 | 47 | return ( 48 |
    49 | 52 |
    53 | 71 | 72 | {!hideError && errors[id] && ( 73 |
    74 | 75 |
    76 | )} 77 |
    78 |
    79 | {helperText &&

    {helperText}

    } 80 | {!hideError && errors[id] && ( 81 | 82 | {errors[id]?.message as unknown as string} 83 | 84 | )} 85 |
    86 |
    87 | ); 88 | } 89 | -------------------------------------------------------------------------------- /src/components/forms/PasswordInput.tsx: -------------------------------------------------------------------------------- 1 | import clsx from 'clsx'; 2 | import { useState } from 'react'; 3 | import { RegisterOptions, useFormContext } from 'react-hook-form'; 4 | import { HiEye, HiEyeOff } from 'react-icons/hi'; 5 | 6 | export type PasswordInputProps = { 7 | /** Input label */ 8 | label: string; 9 | /** 10 | * id to be initialized with React Hook Form, 11 | * must be the same with the pre-defined types. 12 | */ 13 | id: string; 14 | /** Input placeholder */ 15 | placeholder?: string; 16 | /** Small text below input, useful for additional information */ 17 | helperText?: string; 18 | /** 19 | * Input type 20 | * @example text, email, password 21 | */ 22 | type?: React.HTMLInputTypeAttribute; 23 | /** Disables the input and shows defaultValue (can be set from React Hook Form) */ 24 | readOnly?: boolean; 25 | /** Disable error style (not disabling error validation) */ 26 | hideError?: boolean; 27 | /** Manual validation using RHF, it is encouraged to use yup resolver instead */ 28 | validation?: RegisterOptions; 29 | } & React.ComponentPropsWithoutRef<'input'>; 30 | 31 | export default function PasswordInput({ 32 | label, 33 | placeholder = '', 34 | helperText, 35 | id, 36 | readOnly = false, 37 | validation, 38 | ...rest 39 | }: PasswordInputProps) { 40 | const { 41 | register, 42 | formState: { errors }, 43 | } = useFormContext(); 44 | 45 | const [showPassword, setShowPassword] = useState(false); 46 | const togglePassword = () => setShowPassword((prev) => !prev); 47 | 48 | return ( 49 |
    50 | 53 |
    54 | 72 | 73 | 84 |
    85 |
    86 | {helperText &&

    {helperText}

    } 87 | {errors[id] && ( 88 | 89 | {errors[id]?.message as unknown as string} 90 | 91 | )} 92 |
    93 |
    94 | ); 95 | } 96 | -------------------------------------------------------------------------------- /src/components/forms/SelectInput.tsx: -------------------------------------------------------------------------------- 1 | import clsx from 'clsx'; 2 | import * as React from 'react'; 3 | import { RegisterOptions, useFormContext } from 'react-hook-form'; 4 | import { HiExclamationCircle } from 'react-icons/hi'; 5 | 6 | export type SelectInputProps = { 7 | label: string; 8 | id: string; 9 | placeholder?: string; 10 | helperText?: string; 11 | type?: string; 12 | readOnly?: boolean; 13 | validation?: RegisterOptions; 14 | children: React.ReactNode; 15 | } & React.ComponentPropsWithoutRef<'select'>; 16 | 17 | export default function SelectInput({ 18 | label, 19 | helperText, 20 | id, 21 | placeholder, 22 | readOnly = false, 23 | children, 24 | validation, 25 | ...rest 26 | }: SelectInputProps) { 27 | const { 28 | register, 29 | formState: { errors }, 30 | watch, 31 | } = useFormContext(); 32 | 33 | const value = watch(id); 34 | 35 | // Add disabled and selected attribute to option, will be used if readonly 36 | const readOnlyChildren = React.Children.map( 37 | children, 38 | (child) => { 39 | if (React.isValidElement(child)) { 40 | return React.cloneElement( 41 | child as React.ReactElement, 42 | { 43 | disabled: child.props.value !== rest?.defaultValue, 44 | // selected: child.props.value === rest?.defaultValue, 45 | } 46 | ); 47 | } 48 | } 49 | ); 50 | 51 | return ( 52 |
    53 | 56 |
    57 | 82 | 83 | {errors[id] && ( 84 |
    85 | 86 |
    87 | )} 88 |
    89 |
    90 | {helperText &&

    {helperText}

    } 91 | {errors[id] && ( 92 | 93 | {errors[id]?.message as unknown as string} 94 | 95 | )} 96 |
    97 |
    98 | ); 99 | } 100 | -------------------------------------------------------------------------------- /src/components/forms/TextArea.tsx: -------------------------------------------------------------------------------- 1 | import clsx from 'clsx'; 2 | import { RegisterOptions, useFormContext } from 'react-hook-form'; 3 | import { HiExclamationCircle } from 'react-icons/hi'; 4 | 5 | export type TextAreaProps = { 6 | label: string; 7 | id: string; 8 | placeholder?: string; 9 | helperText?: string; 10 | readOnly?: boolean; 11 | hideError?: boolean; 12 | validation?: RegisterOptions; 13 | } & React.ComponentPropsWithoutRef<'textarea'>; 14 | 15 | export default function TextArea({ 16 | label, 17 | placeholder = '', 18 | helperText, 19 | id, 20 | readOnly = false, 21 | hideError = false, 22 | validation, 23 | ...rest 24 | }: TextAreaProps) { 25 | const { 26 | register, 27 | formState: { errors }, 28 | } = useFormContext(); 29 | 30 | return ( 31 |
    32 | 35 |
    36 |