├── .env.example ├── .eslintrc.js ├── .github ├── issue-branch.yml ├── pull_request_template.md └── workflows │ ├── create-branch.yml │ ├── git-sync.yml │ ├── issue-autolink.yml │ ├── lint.yml │ └── release-please.yml ├── .gitignore ├── .husky ├── README.md ├── commit-msg ├── post-merge ├── pre-commit └── pre-push ├── .npmrc ├── .prettierignore ├── .prettierrc.js ├── .vscode ├── css.code-snippets ├── extensions.json ├── settings.json └── typescriptreact.code-snippets ├── CODEOWNERS ├── LICENSE ├── 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-chrome-192x192.png │ ├── android-chrome-512x512.png │ ├── apple-touch-icon.png │ ├── browserconfig.xml │ ├── favicon-16x16.png │ ├── favicon-32x32.png │ ├── favicon.ico │ ├── large-og.jpg │ ├── mstile-150x150.png │ ├── safari-pinned-tab.svg │ └── site.webmanifest ├── fonts │ ├── AvertaStd │ │ ├── AvertaStd-Bold.woff2 │ │ ├── AvertaStd-BoldItalic.woff2 │ │ ├── AvertaStd-Regular.woff2 │ │ ├── AvertaStd-RegularItalic.woff2 │ │ ├── AvertaStd-Semibold.woff2 │ │ └── AvertaStd-SemiboldItalic.woff2 │ └── inter-var-latin.woff2 ├── images │ ├── large-og.png │ └── new-tab.png └── svg │ └── Vercel.svg ├── src ├── __tests__ │ └── pages │ │ └── 404.test.tsx ├── components │ ├── Banner.tsx │ ├── Breadcrumb.tsx │ ├── DismissableToast.tsx │ ├── NextImage.tsx │ ├── Seo.tsx │ ├── Skeleton.tsx │ ├── Tooltip.tsx │ ├── buttons │ │ ├── Button.tsx │ │ ├── IconButton.tsx │ │ └── TextButton.tsx │ ├── cards │ │ └── MacCard.tsx │ ├── dialog │ │ └── BaseDialog.tsx │ ├── forms │ │ ├── Checkbox.tsx │ │ ├── DatePicker.tsx │ │ ├── DropzoneInput.tsx │ │ ├── ErrorMessage.tsx │ │ ├── FilePreview.tsx │ │ ├── Input.tsx │ │ ├── PasswordInput.tsx │ │ ├── Radio.tsx │ │ ├── SearchableSelectInput.tsx │ │ ├── TextArea.tsx │ │ └── boilerplate.tsx │ ├── layout │ │ ├── Header.tsx │ │ └── Layout.tsx │ ├── links │ │ ├── ArrowLink.tsx │ │ ├── ButtonLink.tsx │ │ ├── IconLink.tsx │ │ ├── PrimaryLink.tsx │ │ ├── UnderlineLink.tsx │ │ └── UnstyledLink.tsx │ ├── modal │ │ └── Modal.tsx │ ├── popover │ │ └── Popover.tsx │ ├── table │ │ ├── Filter.tsx │ │ ├── PaginatedTable.tsx │ │ ├── PaginationControl.tsx │ │ ├── ServerTable.tsx │ │ ├── TBody.tsx │ │ ├── THead.tsx │ │ ├── TOption.tsx │ │ └── Table.tsx │ ├── tag │ │ └── Tag.tsx │ └── typography │ │ └── Typography.tsx ├── constant │ ├── env.ts │ ├── repo.ts │ └── toast.ts ├── hooks │ ├── toast │ │ ├── useLoadingToast.tsx │ │ ├── useMutationToast.tsx │ │ └── useQueryToast.tsx │ ├── useDialog.tsx │ ├── useRenderCount.tsx │ └── useServerTable.tsx ├── lib │ ├── __tests__ │ │ └── helper.test.ts │ ├── axios-mock.ts │ ├── axios.ts │ ├── clsxm.ts │ ├── content │ │ └── banner.tsx │ ├── helper.ts │ ├── logger.ts │ ├── pagination.ts │ └── table.ts ├── pages │ ├── 404.page.tsx │ ├── _app.page.tsx │ ├── _document.page.tsx │ ├── api │ │ ├── hello.ts │ │ └── mock │ │ │ ├── login.api.ts │ │ │ ├── me.api.ts │ │ │ └── users.api.ts │ ├── index.page.tsx │ ├── navigation │ │ ├── components │ │ │ └── Navigation.tsx │ │ └── index.page.tsx │ ├── nested-form │ │ ├── components │ │ │ └── FieldArrayForm.tsx │ │ └── index.page.tsx │ └── sandbox │ │ ├── banner.page.tsx │ │ ├── breadcrumb.page.tsx │ │ ├── button.page.tsx │ │ ├── colors.page.tsx │ │ ├── components │ │ ├── CountryFilterPopup.tsx │ │ ├── ExampleModal.tsx │ │ └── ExamplePopover.tsx │ │ ├── dialog.page.tsx │ │ ├── form.page.tsx │ │ ├── icon-button.page.tsx │ │ ├── mac-cards.page.tsx │ │ ├── modal.page.tsx │ │ ├── popover.page.tsx │ │ ├── table.page.tsx │ │ ├── text-button.page.tsx │ │ ├── toast-rq.page.tsx │ │ ├── tooltip.page.tsx │ │ └── typography.page.tsx ├── store │ └── useDialogStore.tsx ├── styles │ ├── globals.css │ └── nprogress.css └── types │ ├── api.ts │ ├── dropzone.ts │ └── helper.ts ├── tailwind.config.js ├── tsconfig.json ├── vercel.json └── yarn.lock /.env.example: -------------------------------------------------------------------------------- 1 | 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/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/pull_request_template.md: -------------------------------------------------------------------------------- 1 | # Description & Technical Solution 2 | 3 | Describe problems, if any, clearly and concisely. 4 | Summarize the impact to the system. 5 | Please also include relevant motivation and context. 6 | Please include a summary of the technical solution and how it solves the problem. 7 | 8 | # Checklist 9 | 10 | - [ ] I have commented my code, particularly in hard-to-understand areas. 11 | - [ ] Already rebased against main branch. 12 | 13 | # Screenshots 14 | 15 | Provide screenshots or videos of the changes made if any. 16 | -------------------------------------------------------------------------------- /.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/git-sync.yml: -------------------------------------------------------------------------------- 1 | name: Git Sync 2 | on: 3 | push: 4 | branches: 5 | - main 6 | workflow_dispatch: 7 | jobs: 8 | git-sync: 9 | if: ${{ github.repository == 'ppdbultimate/design-system-moon' }} 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: 🛑 Cancel Previous Runs 13 | uses: styfle/cancel-workflow-action@0.9.1 14 | - name: 🔗 Synchronize Repository 15 | uses: wei/git-sync@v3 16 | with: 17 | source_repo: 'https://theodorusclarence:${{ secrets.PAT }}@github.com/ppdbultimate/design-system-moon.git' 18 | source_branch: 'main' 19 | destination_repo: 'https://theodorusclarence:${{ secrets.PAT }}@github.com/theodorusclarence/aether-design-system.git' 20 | destination_branch: 'main' 21 | -------------------------------------------------------------------------------- /.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 | permissions: 10 | pull-requests: write 11 | steps: 12 | - uses: tkt-actions/add-issue-links@v1.8.1 13 | with: 14 | repo-token: '${{ secrets.GITHUB_TOKEN }}' 15 | branch-prefix: 'i' 16 | resolve: 'true' 17 | -------------------------------------------------------------------------------- /.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 | concurrency: 10 | group: ${{ github.job }}-${{ github.ref }} 11 | cancel-in-progress: true 12 | 13 | jobs: 14 | lint: 15 | name: ⬣ ESLint 16 | runs-on: ubuntu-latest 17 | steps: 18 | - name: ⬇️ Checkout repo 19 | uses: actions/checkout@v2 20 | 21 | - name: ⎔ Setup node 22 | uses: actions/setup-node@v3 23 | with: 24 | node-version: 16 25 | 26 | - name: 📥 Download deps 27 | uses: bahmutov/npm-install@v1 28 | 29 | - name: 🔬 Lint 30 | run: yarn lint:strict 31 | 32 | typecheck: 33 | name: ʦ TypeScript 34 | runs-on: ubuntu-latest 35 | steps: 36 | - name: ⬇️ Checkout repo 37 | uses: actions/checkout@v2 38 | 39 | - name: ⎔ Setup node 40 | uses: actions/setup-node@v3 41 | with: 42 | node-version: 16 43 | 44 | - name: 📥 Download deps 45 | uses: bahmutov/npm-install@v1 46 | 47 | - name: 🔎 Type check 48 | run: yarn typecheck 49 | 50 | prettier: 51 | name: 💅 Prettier 52 | runs-on: ubuntu-latest 53 | steps: 54 | - name: ⬇️ Checkout repo 55 | uses: actions/checkout@v2 56 | 57 | - name: ⎔ Setup node 58 | uses: actions/setup-node@v3 59 | with: 60 | node-version: 16 61 | 62 | - name: 📥 Download deps 63 | uses: bahmutov/npm-install@v1 64 | 65 | - name: 💅 Prettier check 66 | run: yarn format:check 67 | 68 | test: 69 | name: 🃏 Test 70 | runs-on: ubuntu-latest 71 | steps: 72 | - name: ⬇️ Checkout repo 73 | uses: actions/checkout@v2 74 | 75 | - name: ⎔ Setup node 76 | uses: actions/setup-node@v3 77 | with: 78 | node-version: 16 79 | 80 | - name: 📥 Download deps 81 | uses: bahmutov/npm-install@v1 82 | 83 | - name: 🃏 Run jest 84 | run: yarn test 85 | -------------------------------------------------------------------------------- /.github/workflows/release-please.yml: -------------------------------------------------------------------------------- 1 | name: release-please 2 | on: 3 | workflow_dispatch: 4 | jobs: 5 | release-please: 6 | runs-on: ubuntu-latest 7 | steps: 8 | - uses: google-github-actions/release-please-action@v3 9 | with: 10 | release-type: node 11 | package-name: release-please-action 12 | -------------------------------------------------------------------------------- /.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 | .pnpm-debug.log* 27 | 28 | # local env files 29 | .env 30 | .env*.local 31 | 32 | # vercel 33 | .vercel 34 | 35 | # typescript 36 | *.tsbuildinfo 37 | next-env.d.ts 38 | 39 | # next-sitemap 40 | robots.txt 41 | sitemap.xml 42 | sitemap-*.xml -------------------------------------------------------------------------------- /.husky/README.md: -------------------------------------------------------------------------------- 1 | # How to create a hook 2 | 3 | Example: 4 | 5 | npx husky add .husky/pre-push "yarn typecheck" 6 | -------------------------------------------------------------------------------- /.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 -------------------------------------------------------------------------------- /.husky/pre-push: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | yarn typecheck 5 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @theodorusclarence -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Theodorus Clarence, Rizqi Tsani, Wina Tungmiharja 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Recursive Components 2 | 3 | Useful recursive components pattern that I've built with React 4 | -------------------------------------------------------------------------------- /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 | 3 | // Allow router mocks. 4 | // eslint-disable-next-line no-undef 5 | jest.mock('next/router', () => require('next-router-mock')); 6 | -------------------------------------------------------------------------------- /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://recursive-components.thcl.dev', 8 | generateRobotsTxt: true, 9 | robotsTxtOptions: { 10 | policies: [{ userAgent: '*', allow: '/' }], 11 | }, 12 | }; 13 | -------------------------------------------------------------------------------- /next.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = { 3 | eslint: { 4 | dirs: ['src'], 5 | }, 6 | 7 | reactStrictMode: true, 8 | pageExtensions: ['page.tsx', 'api.ts'], 9 | swcMinify: true, 10 | 11 | // Uncoment to add domain whitelist 12 | // images: { 13 | // domains: [ 14 | // 'res.cloudinary.com', 15 | // ], 16 | // }, 17 | 18 | // SVGR 19 | webpack(config) { 20 | config.module.rules.push({ 21 | test: /\.svg$/i, 22 | issuer: /\.[jt]sx?$/, 23 | use: [ 24 | { 25 | loader: '@svgr/webpack', 26 | options: { 27 | typescript: true, 28 | icon: true, 29 | }, 30 | }, 31 | ], 32 | }); 33 | 34 | return config; 35 | }, 36 | }; 37 | 38 | module.exports = nextConfig; 39 | -------------------------------------------------------------------------------- /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.7", 22 | "@radix-ui/react-popover": "^1.0.3", 23 | "@radix-ui/react-tooltip": "^1.0.3", 24 | "@tanstack/react-query": "^4.22.0", 25 | "@tanstack/react-query-devtools": "^4.22.0", 26 | "@tanstack/react-table": "^8.7.6", 27 | "auto-zustand-selectors-hook": "^2.0.0", 28 | "axios": "1.2.1", 29 | "clsx": "^1.2.1", 30 | "immer": "^9.0.17", 31 | "lodash.get": "^4.4.2", 32 | "next": "^13.1.1", 33 | "nprogress": "^0.2.0", 34 | "query-string": "^8.1.0", 35 | "react": "^18.2.0", 36 | "react-datepicker": "^4.8.0", 37 | "react-dom": "^18.2.0", 38 | "react-dropzone": "^14.2.3", 39 | "react-hook-form": "^7.41.5", 40 | "react-hot-toast": "^2.4.0", 41 | "react-icons": "^4.7.1", 42 | "react-image-lightbox": "^5.1.4", 43 | "react-select": "^5.7.0", 44 | "react-slick": "^0.29.0", 45 | "slick-carousel": "^1.8.1", 46 | "tailwind-merge": "^1.8.1", 47 | "tailwindcss-animate": "^1.0.5", 48 | "zustand": "^4.3.1" 49 | }, 50 | "devDependencies": { 51 | "@commitlint/cli": "^16.3.0", 52 | "@commitlint/config-conventional": "^16.2.4", 53 | "@svgr/webpack": "^6.5.1", 54 | "@tailwindcss/forms": "^0.5.3", 55 | "@testing-library/jest-dom": "^5.16.5", 56 | "@testing-library/react": "^13.4.0", 57 | "@types/lodash.get": "^4.4.7", 58 | "@types/nprogress": "^0.2.0", 59 | "@types/react": "^18.0.26", 60 | "@types/react-datepicker": "^4.8.0", 61 | "@types/react-slick": "^0.23.10", 62 | "@typescript-eslint/eslint-plugin": "^5.48.0", 63 | "@typescript-eslint/parser": "^5.48.0", 64 | "autoprefixer": "^10.4.13", 65 | "eslint": "^8.31.0", 66 | "eslint-config-next": "^13.1.1", 67 | "eslint-config-prettier": "^8.6.0", 68 | "eslint-plugin-simple-import-sort": "^7.0.0", 69 | "eslint-plugin-unused-imports": "^2.0.0", 70 | "husky": "^7.0.4", 71 | "jest": "^27.5.1", 72 | "lint-staged": "^12.5.0", 73 | "next-router-mock": "^0.7.5", 74 | "next-sitemap": "^2.5.28", 75 | "postcss": "^8.4.21", 76 | "prettier": "^2.8.1", 77 | "prettier-plugin-tailwindcss": "^0.1.13", 78 | "tailwindcss": "^3.2.4", 79 | "typescript": "^4.9.4" 80 | }, 81 | "lint-staged": { 82 | "**/*.{js,jsx,ts,tsx}": [ 83 | "eslint --max-warnings=0", 84 | "prettier -w" 85 | ], 86 | "**/*.{json,css,scss,md,webmanifest}": [ 87 | "prettier -w" 88 | ] 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | }; 7 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theodorusclarence/recursive-component/34e56c5efcfdf2cf1f6f85aac7760679abacf0d9/public/favicon.ico -------------------------------------------------------------------------------- /public/favicon/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theodorusclarence/recursive-component/34e56c5efcfdf2cf1f6f85aac7760679abacf0d9/public/favicon/android-chrome-192x192.png -------------------------------------------------------------------------------- /public/favicon/android-chrome-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theodorusclarence/recursive-component/34e56c5efcfdf2cf1f6f85aac7760679abacf0d9/public/favicon/android-chrome-512x512.png -------------------------------------------------------------------------------- /public/favicon/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theodorusclarence/recursive-component/34e56c5efcfdf2cf1f6f85aac7760679abacf0d9/public/favicon/apple-touch-icon.png -------------------------------------------------------------------------------- /public/favicon/browserconfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | #2b5797 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /public/favicon/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theodorusclarence/recursive-component/34e56c5efcfdf2cf1f6f85aac7760679abacf0d9/public/favicon/favicon-16x16.png -------------------------------------------------------------------------------- /public/favicon/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theodorusclarence/recursive-component/34e56c5efcfdf2cf1f6f85aac7760679abacf0d9/public/favicon/favicon-32x32.png -------------------------------------------------------------------------------- /public/favicon/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theodorusclarence/recursive-component/34e56c5efcfdf2cf1f6f85aac7760679abacf0d9/public/favicon/favicon.ico -------------------------------------------------------------------------------- /public/favicon/large-og.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theodorusclarence/recursive-component/34e56c5efcfdf2cf1f6f85aac7760679abacf0d9/public/favicon/large-og.jpg -------------------------------------------------------------------------------- /public/favicon/mstile-150x150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theodorusclarence/recursive-component/34e56c5efcfdf2cf1f6f85aac7760679abacf0d9/public/favicon/mstile-150x150.png -------------------------------------------------------------------------------- /public/favicon/safari-pinned-tab.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 7 | 8 | Created by potrace 1.14, written by Peter Selinger 2001-2017 9 | 10 | 12 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /public/favicon/site.webmanifest: -------------------------------------------------------------------------------- 1 | { 2 | "name": "", 3 | "short_name": "", 4 | "icons": [ 5 | { 6 | "src": "/favicon/android-chrome-192x192.png", 7 | "sizes": "192x192", 8 | "type": "image/png" 9 | }, 10 | { 11 | "src": "/favicon/android-chrome-512x512.png", 12 | "sizes": "512x512", 13 | "type": "image/png" 14 | } 15 | ], 16 | "theme_color": "#ffffff", 17 | "background_color": "#ffffff", 18 | "display": "standalone" 19 | } 20 | -------------------------------------------------------------------------------- /public/fonts/AvertaStd/AvertaStd-Bold.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theodorusclarence/recursive-component/34e56c5efcfdf2cf1f6f85aac7760679abacf0d9/public/fonts/AvertaStd/AvertaStd-Bold.woff2 -------------------------------------------------------------------------------- /public/fonts/AvertaStd/AvertaStd-BoldItalic.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theodorusclarence/recursive-component/34e56c5efcfdf2cf1f6f85aac7760679abacf0d9/public/fonts/AvertaStd/AvertaStd-BoldItalic.woff2 -------------------------------------------------------------------------------- /public/fonts/AvertaStd/AvertaStd-Regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theodorusclarence/recursive-component/34e56c5efcfdf2cf1f6f85aac7760679abacf0d9/public/fonts/AvertaStd/AvertaStd-Regular.woff2 -------------------------------------------------------------------------------- /public/fonts/AvertaStd/AvertaStd-RegularItalic.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theodorusclarence/recursive-component/34e56c5efcfdf2cf1f6f85aac7760679abacf0d9/public/fonts/AvertaStd/AvertaStd-RegularItalic.woff2 -------------------------------------------------------------------------------- /public/fonts/AvertaStd/AvertaStd-Semibold.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theodorusclarence/recursive-component/34e56c5efcfdf2cf1f6f85aac7760679abacf0d9/public/fonts/AvertaStd/AvertaStd-Semibold.woff2 -------------------------------------------------------------------------------- /public/fonts/AvertaStd/AvertaStd-SemiboldItalic.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theodorusclarence/recursive-component/34e56c5efcfdf2cf1f6f85aac7760679abacf0d9/public/fonts/AvertaStd/AvertaStd-SemiboldItalic.woff2 -------------------------------------------------------------------------------- /public/fonts/inter-var-latin.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theodorusclarence/recursive-component/34e56c5efcfdf2cf1f6f85aac7760679abacf0d9/public/fonts/inter-var-latin.woff2 -------------------------------------------------------------------------------- /public/images/large-og.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theodorusclarence/recursive-component/34e56c5efcfdf2cf1f6f85aac7760679abacf0d9/public/images/large-og.png -------------------------------------------------------------------------------- /public/images/new-tab.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theodorusclarence/recursive-component/34e56c5efcfdf2cf1f6f85aac7760679abacf0d9/public/images/new-tab.png -------------------------------------------------------------------------------- /public/svg/Vercel.svg: -------------------------------------------------------------------------------- 1 | Vercel -------------------------------------------------------------------------------- /src/__tests__/pages/404.test.tsx: -------------------------------------------------------------------------------- 1 | import { render, screen } from '@testing-library/react'; 2 | 3 | import NotFoundPage from '@/pages/404.page'; 4 | 5 | describe('404', () => { 6 | it('renders a heading', () => { 7 | render(); 8 | 9 | const heading = screen.getByText(/not found/i); 10 | 11 | expect(heading).toBeInTheDocument(); 12 | }); 13 | }); 14 | -------------------------------------------------------------------------------- /src/components/Banner.tsx: -------------------------------------------------------------------------------- 1 | import clsx from 'clsx'; 2 | import * as React from 'react'; 3 | import { FiChevronLeft, FiChevronRight } from 'react-icons/fi'; 4 | import Slider, { CustomArrowProps, Settings } from 'react-slick'; 5 | 6 | import 'slick-carousel/slick/slick.css'; 7 | import 'slick-carousel/slick/slick-theme.css'; 8 | 9 | import { BANNER_CONTENT } from '@/lib/content/banner'; 10 | 11 | import IconButton from '@/components/buttons/IconButton'; 12 | import Typography from '@/components/typography/Typography'; 13 | 14 | type BannerProps = React.ComponentPropsWithoutRef<'div'>; 15 | 16 | function NextArrow({ onClick }: CustomArrowProps) { 17 | return ( 18 | 25 | ); 26 | } 27 | 28 | function PrevArrow({ onClick }: CustomArrowProps) { 29 | return ( 30 | 37 | ); 38 | } 39 | 40 | const settings: Settings = { 41 | autoplay: true, 42 | infinite: true, 43 | slidesToShow: 1, 44 | slidesToScroll: 1, 45 | adaptiveHeight: true, 46 | nextArrow: , 47 | prevArrow: , 48 | }; 49 | 50 | export default function Banner({ className, ...rest }: BannerProps) { 51 | return ( 52 |
56 |
57 | 58 | {BANNER_CONTENT.map((content, index) => ( 59 |
60 | 61 | {content()} 62 | 63 |
64 | ))} 65 |
66 |
67 |
68 | ); 69 | } 70 | -------------------------------------------------------------------------------- /src/components/Breadcrumb.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | import clsxm from '@/lib/clsxm'; 4 | 5 | import PrimaryLink from '@/components/links/PrimaryLink'; 6 | import Typography from '@/components/typography/Typography'; 7 | 8 | const breadcrumbs = { 9 | '/': 'Landing Page', 10 | '/sandbox/breadcrumb': 'Breadcrumb', 11 | }; 12 | type BreadcrumbProps = { 13 | crumbs: Array; 14 | } & React.ComponentPropsWithoutRef<'div'>; 15 | 16 | export default function Breadcrumb({ 17 | className, 18 | crumbs: _crumbs, 19 | ...rest 20 | }: BreadcrumbProps) { 21 | // split array into the last part and the rest 22 | const lastCrumb = _crumbs[_crumbs.length - 1]; 23 | const crumbs = _crumbs.slice(0, _crumbs.length - 1); 24 | 25 | return ( 26 |
27 | {crumbs.map((crumb) => ( 28 | 29 | 30 | {breadcrumbs[crumb]} 31 | 32 | / 33 | 34 | ))} 35 | 36 | {breadcrumbs[lastCrumb]} 37 | 38 |
39 | ); 40 | } 41 | -------------------------------------------------------------------------------- /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 | 15 | {(t) => ( 16 | 17 | {({ icon, message }) => ( 18 | <> 19 | {icon} 20 | {message} 21 | {t.type !== 'loading' && ( 22 | 28 | )} 29 | 30 | )} 31 | 32 | )} 33 | 34 |
35 | ); 36 | } 37 | -------------------------------------------------------------------------------- /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 | {...rest} 54 | /> 55 |
56 | ); 57 | } 58 | -------------------------------------------------------------------------------- /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: 'Recursive Components', 8 | siteName: 'Recursive Components', 9 | description: "Example of recursive components that I've built", 10 | /** Without additional '/' on the end, e.g. https://theodorusclarence.com */ 11 | url: 'https://recursive-components.thcl.dev', 12 | type: 'website', 13 | robots: 'follow, index', 14 | /** 15 | * No need to be filled, will be populated with openGraph function 16 | * If you wish to use a normal image, just specify the path below 17 | */ 18 | image: 'https://tsnext-tw.thcl.dev/images/large-og.png', 19 | }; 20 | 21 | type SeoProps = { 22 | date?: string; 23 | templateTitle?: string; 24 | } & Partial; 25 | 26 | export default function Seo(props: SeoProps) { 27 | const router = useRouter(); 28 | const meta = { 29 | ...defaultMeta, 30 | ...props, 31 | }; 32 | meta['title'] = props.templateTitle 33 | ? `${props.templateTitle} | ${meta.siteName}` 34 | : meta.title; 35 | 36 | meta['image'] = openGraph({ 37 | description: meta.description, 38 | siteName: props.templateTitle ? meta.siteName : meta.title, 39 | templateTitle: props.templateTitle, 40 | }); 41 | 42 | return ( 43 | 44 | {meta.title} 45 | 46 | 47 | 48 | 49 | {/* Open Graph */} 50 | 51 | 52 | 53 | 54 | 55 | {/* Twitter */} 56 | 57 | 58 | 59 | 60 | {meta.date && ( 61 | <> 62 | 63 | 68 | 69 | )} 70 | 71 | {/* Favicons */} 72 | {favicons.map((linkProps) => ( 73 | 74 | ))} 75 | 76 | 77 | 78 | 79 | ); 80 | } 81 | 82 | const favicons: Array> = [ 83 | { 84 | rel: 'apple-touch-icon', 85 | sizes: '180x180', 86 | href: '/favicon/apple-touch-icon.png', 87 | }, 88 | { 89 | rel: 'icon', 90 | type: 'image/png', 91 | sizes: '32x32', 92 | href: '/favicon/favicon-32x32.png', 93 | }, 94 | { 95 | rel: 'icon', 96 | type: 'image/png', 97 | sizes: '16x16', 98 | href: '/favicon/favicon-16x16.png', 99 | }, 100 | { rel: 'manifest', href: '/favicon/site.webmanifest' }, 101 | { 102 | rel: 'mask-icon', 103 | href: '/favicon/safari-pinned-tab.svg', 104 | color: '#00e887', 105 | }, 106 | { rel: 'shortcut icon', href: '/favicon/favicon.ico' }, 107 | ]; 108 | -------------------------------------------------------------------------------- /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/Tooltip.tsx: -------------------------------------------------------------------------------- 1 | import * as TooltipPrimitive from '@radix-ui/react-tooltip'; 2 | import * as React from 'react'; 3 | 4 | import clsxm from '@/lib/clsxm'; 5 | 6 | const TooltipProvider = TooltipPrimitive.Provider; 7 | 8 | const Tooltip = ({ ...props }) => ; 9 | Tooltip.displayName = TooltipPrimitive.Tooltip.displayName; 10 | 11 | const TooltipTrigger = TooltipPrimitive.Trigger; 12 | 13 | const TooltipContent = React.forwardRef< 14 | React.ElementRef, 15 | React.ComponentPropsWithoutRef 16 | >(({ className, sideOffset = 4, ...props }, ref) => ( 17 | 27 | )); 28 | TooltipContent.displayName = TooltipPrimitive.Content.displayName; 29 | 30 | export { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger }; 31 | -------------------------------------------------------------------------------- /src/components/buttons/Button.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { IconType } from 'react-icons'; 3 | import { ImSpinner2 } from 'react-icons/im'; 4 | 5 | import clsxm from '@/lib/clsxm'; 6 | 7 | const ButtonVariant = [ 8 | 'primary', 9 | 'secondary', 10 | 'danger', 11 | 'outline', 12 | 'ghost', 13 | ] as const; 14 | const ButtonSize = ['sm', 'base', 'lg'] as const; 15 | 16 | type ButtonProps = { 17 | isLoading?: boolean; 18 | variant?: typeof ButtonVariant[number]; 19 | size?: typeof ButtonSize[number]; 20 | leftIcon?: IconType; 21 | rightIcon?: IconType; 22 | leftIconClassName?: string; 23 | rightIconClassName?: string; 24 | } & React.ComponentPropsWithRef<'button'>; 25 | 26 | const Button = React.forwardRef( 27 | ( 28 | { 29 | children, 30 | className, 31 | disabled: buttonDisabled, 32 | isLoading, 33 | variant = 'primary', 34 | size = 'base', 35 | leftIcon: LeftIcon, 36 | rightIcon: RightIcon, 37 | leftIconClassName, 38 | rightIconClassName, 39 | ...rest 40 | }, 41 | ref 42 | ) => { 43 | const disabled = isLoading || buttonDisabled; 44 | 45 | return ( 46 | 155 | ); 156 | } 157 | ); 158 | 159 | export default Button; 160 | -------------------------------------------------------------------------------- /src/components/buttons/IconButton.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { IconType } from 'react-icons'; 3 | import { ImSpinner2 } from 'react-icons/im'; 4 | 5 | import clsxm from '@/lib/clsxm'; 6 | 7 | const IconButtonVariant = [ 8 | 'primary', 9 | 'secondary', 10 | 'danger', 11 | 'outline', 12 | 'ghost', 13 | ] as const; 14 | const IconButtonSize = ['sm', 'base', 'lg'] as const; 15 | 16 | type IconButtonProps = { 17 | isLoading?: boolean; 18 | variant?: typeof IconButtonVariant[number]; 19 | size?: typeof IconButtonSize[number]; 20 | icon?: IconType; 21 | iconClassName?: string; 22 | } & React.ComponentPropsWithRef<'button'>; 23 | 24 | const IconButton = React.forwardRef( 25 | ( 26 | { 27 | className, 28 | disabled: buttonDisabled, 29 | isLoading, 30 | variant = 'primary', 31 | size = 'base', 32 | icon: Icon, 33 | iconClassName, 34 | ...rest 35 | }, 36 | ref 37 | ) => { 38 | const disabled = isLoading || buttonDisabled; 39 | 40 | return ( 41 | 128 | ); 129 | } 130 | ); 131 | 132 | export default IconButton; 133 | -------------------------------------------------------------------------------- /src/components/buttons/TextButton.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | import clsxm from '@/lib/clsxm'; 4 | 5 | const TextButtonSize = ['sm', 'base'] as const; 6 | const TextButtonVariant = ['primary', 'secondary', 'basic', 'danger'] as const; 7 | 8 | type TextButtonProps = { 9 | size?: typeof TextButtonSize[number]; 10 | variant?: typeof TextButtonVariant[number]; 11 | } & React.ComponentPropsWithRef<'button'>; 12 | 13 | const TextButton = React.forwardRef( 14 | ( 15 | { children, className, size = 'base', variant = 'primary', ...rest }, 16 | ref 17 | ) => { 18 | return ( 19 | 59 | ); 60 | } 61 | ); 62 | 63 | export default TextButton; 64 | -------------------------------------------------------------------------------- /src/components/cards/MacCard.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | import clsxm from '@/lib/clsxm'; 4 | 5 | type MacCardProps = { 6 | childrenClassName?: string; 7 | } & React.ComponentPropsWithoutRef<'div'>; 8 | 9 | export default function MacCard({ 10 | className, 11 | children, 12 | childrenClassName, 13 | ...rest 14 | }: MacCardProps) { 15 | return ( 16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 | {children} 24 |
25 |
26 | ); 27 | } 28 | -------------------------------------------------------------------------------- /src/components/forms/Checkbox.tsx: -------------------------------------------------------------------------------- 1 | import clsx from 'clsx'; 2 | import get from 'lodash.get'; 3 | import * as React from 'react'; 4 | import { RegisterOptions, useFormContext } from 'react-hook-form'; 5 | 6 | import clsxm from '@/lib/clsxm'; 7 | 8 | import Typography from '@/components/typography/Typography'; 9 | 10 | enum CheckboxSize { 11 | 'sm', 12 | 'base', 13 | } 14 | 15 | export type CheckboxProps = { 16 | /** Input label */ 17 | label: string; 18 | name: string; 19 | /** Add value only if you're using grouped checkbox, omit value if using a single checkbox */ 20 | value?: string | number; 21 | /** Small text below input, useful for additional information */ 22 | helperText?: string; 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 | size?: keyof typeof CheckboxSize; 30 | } & Omit, 'size'>; 31 | 32 | export default function Checkbox({ 33 | label, 34 | name, 35 | value, 36 | placeholder = '', 37 | helperText, 38 | readOnly = false, 39 | hideError = false, 40 | validation, 41 | size = 'base', 42 | disabled, 43 | ...rest 44 | }: CheckboxProps) { 45 | const { 46 | register, 47 | formState: { errors }, 48 | } = useFormContext(); 49 | 50 | const error = get(errors, name); 51 | 52 | return ( 53 |
54 |
55 | 78 | 88 | {label} 89 | 90 |
91 | {!(!hideError && error) && helperText && ( 92 | 93 | {helperText} 94 | 95 | )} 96 | {!hideError && error && ( 97 | 98 | {error?.message?.toString()} 99 | 100 | )} 101 |
102 | ); 103 | } 104 | -------------------------------------------------------------------------------- /src/components/forms/DatePicker.tsx: -------------------------------------------------------------------------------- 1 | import clsx from 'clsx'; 2 | import get from 'lodash.get'; 3 | import ReactDatePicker, { ReactDatePickerProps } from 'react-datepicker'; 4 | import { Controller, RegisterOptions, useFormContext } from 'react-hook-form'; 5 | import { HiOutlineCalendar } from 'react-icons/hi'; 6 | 7 | import 'react-datepicker/dist/react-datepicker.css'; 8 | 9 | import clsxm from '@/lib/clsxm'; 10 | 11 | import Typography from '@/components/typography/Typography'; 12 | 13 | type DatePickerProps = { 14 | validation?: RegisterOptions; 15 | label: string; 16 | id: string; 17 | placeholder?: string; 18 | defaultYear?: number; 19 | defaultMonth?: number; 20 | defaultValue?: string; 21 | helperText?: string; 22 | readOnly?: boolean; 23 | /** Disable error style (not disabling error validation) */ 24 | hideError?: boolean; 25 | containerClassName?: string; 26 | } & Omit; 27 | 28 | export default function DatePicker({ 29 | validation, 30 | label, 31 | id, 32 | placeholder, 33 | defaultYear, 34 | defaultMonth, 35 | defaultValue, 36 | helperText, 37 | readOnly = false, 38 | hideError = false, 39 | disabled, 40 | containerClassName, 41 | ...rest 42 | }: DatePickerProps) { 43 | const { 44 | formState: { errors }, 45 | control, 46 | } = useFormContext(); 47 | const error = get(errors, id); 48 | 49 | // If there is a year default, then change the year to the props 50 | const defaultDate = new Date(); 51 | if (defaultYear) defaultDate.setFullYear(defaultYear); 52 | if (defaultMonth) defaultDate.setMonth(defaultMonth); 53 | 54 | return ( 55 |
56 | 57 | {label} 58 | 59 | 60 | ( 66 | <> 67 |
68 | 93 | 94 |
95 | {!(!hideError && error) && helperText && ( 96 | 97 | {helperText} 98 | 99 | )} 100 | {!hideError && error && ( 101 | 102 | {error?.message?.toString()} 103 | 104 | )} 105 | 106 | )} 107 | /> 108 |
109 | ); 110 | } 111 | -------------------------------------------------------------------------------- /src/components/forms/ErrorMessage.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { get, useFormState } from 'react-hook-form'; 3 | 4 | import clsxm from '@/lib/clsxm'; 5 | 6 | type ErrorMessageProps = { 7 | id: string; 8 | } & React.ComponentPropsWithoutRef<'p'>; 9 | 10 | export default function ErrorMessage({ 11 | id, 12 | className, 13 | ...rest 14 | }: ErrorMessageProps) { 15 | const { errors } = useFormState(); 16 | const error = get(errors, id); 17 | 18 | return ( 19 |

20 | {error.message?.toString()} 21 |

22 | ); 23 | } 24 | -------------------------------------------------------------------------------- /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 get from 'lodash.get'; 3 | import * as React from 'react'; 4 | import { RegisterOptions, useFormContext } from 'react-hook-form'; 5 | import { IconType } from 'react-icons'; 6 | 7 | import Typography from '@/components/typography/Typography'; 8 | 9 | export type InputProps = { 10 | /** Input label */ 11 | label: string | null; 12 | /** 13 | * id to be initialized with React Hook Form, 14 | * must be the same with the pre-defined types. 15 | */ 16 | id: string; 17 | /** Input placeholder */ 18 | placeholder?: string; 19 | /** Small text below input, useful for additional information */ 20 | helperText?: string; 21 | /** 22 | * Input type 23 | * @example text, email, password 24 | */ 25 | type?: React.HTMLInputTypeAttribute; 26 | /** Disables the input and shows defaultValue (can be set from React Hook Form) */ 27 | readOnly?: boolean; 28 | /** Disable error style (not disabling error validation) */ 29 | hideError?: boolean; 30 | /** Manual validation using RHF, it is encouraged to use yup resolver instead */ 31 | validation?: RegisterOptions; 32 | leftIcon?: IconType | string; 33 | rightNode?: React.ReactNode; 34 | containerClassName?: string; 35 | } & React.ComponentPropsWithoutRef<'input'>; 36 | 37 | export default function Input({ 38 | label, 39 | placeholder = '', 40 | helperText, 41 | id, 42 | type = 'text', 43 | disabled, 44 | readOnly = false, 45 | hideError = false, 46 | validation, 47 | leftIcon: LeftIcon, 48 | rightNode, 49 | containerClassName, 50 | ...rest 51 | }: InputProps) { 52 | const { 53 | register, 54 | formState: { errors }, 55 | } = useFormContext(); 56 | const error = get(errors, id); 57 | 58 | const withLabel = label !== null; 59 | return ( 60 |
    61 | {withLabel && ( 62 | 63 | {label} 64 | 65 | )} 66 |
    67 | {LeftIcon && ( 68 |
    69 | {typeof LeftIcon === 'string' ? ( 70 | {LeftIcon} 71 | ) : ( 72 | 73 | )} 74 |
    75 | )} 76 | 97 | 98 | {rightNode && ( 99 |
    100 | {rightNode} 101 |
    102 | )} 103 |
    104 | {!(!hideError && error) && helperText && ( 105 | 106 | {helperText} 107 | 108 | )} 109 | {!hideError && error && ( 110 | 111 | {error?.message?.toString()} 112 | 113 | )} 114 |
    115 | ); 116 | } 117 | -------------------------------------------------------------------------------- /src/components/forms/PasswordInput.tsx: -------------------------------------------------------------------------------- 1 | import clsx from 'clsx'; 2 | import get from 'lodash.get'; 3 | import { useState } from 'react'; 4 | import { RegisterOptions, useFormContext } from 'react-hook-form'; 5 | import { HiEye, HiEyeOff } from 'react-icons/hi'; 6 | 7 | import Typography from '@/components/typography/Typography'; 8 | 9 | export type PasswordInputProps = { 10 | /** Input label */ 11 | label: string; 12 | /** 13 | * id to be initialized with React Hook Form, 14 | * must be the same with the pre-defined types. 15 | */ 16 | id: string; 17 | /** Input placeholder */ 18 | placeholder?: string; 19 | /** Small text below input, useful for additional information */ 20 | helperText?: string; 21 | /** 22 | * Input type 23 | * @example text, email, password 24 | */ 25 | type?: React.HTMLInputTypeAttribute; 26 | /** Disables the input and shows defaultValue (can be set from React Hook Form) */ 27 | readOnly?: boolean; 28 | /** Disable error style (not disabling error validation) */ 29 | hideError?: boolean; 30 | /** Manual validation using RHF, it is encouraged to use yup resolver instead */ 31 | validation?: RegisterOptions; 32 | containerClassName?: string; 33 | } & React.ComponentPropsWithoutRef<'input'>; 34 | 35 | export default function PasswordInput({ 36 | label, 37 | placeholder = '', 38 | helperText, 39 | id, 40 | readOnly = false, 41 | hideError, 42 | validation, 43 | disabled, 44 | containerClassName, 45 | ...rest 46 | }: PasswordInputProps) { 47 | const { 48 | register, 49 | formState: { errors }, 50 | } = useFormContext(); 51 | 52 | const error = get(errors, id); 53 | 54 | const [showPassword, setShowPassword] = useState(false); 55 | const togglePassword = () => setShowPassword((prev) => !prev); 56 | 57 | return ( 58 |
    59 | 60 | {label} 61 | 62 |
    63 | 83 | 84 | 95 |
    96 | {!(!hideError && error) && helperText && ( 97 | 98 | {helperText} 99 | 100 | )} 101 | {!hideError && error && ( 102 | 103 | {error?.message?.toString()} 104 | 105 | )} 106 |
    107 | ); 108 | } 109 | -------------------------------------------------------------------------------- /src/components/forms/Radio.tsx: -------------------------------------------------------------------------------- 1 | import clsx from 'clsx'; 2 | import get from 'lodash.get'; 3 | import * as React from 'react'; 4 | import { RegisterOptions, useFormContext } from 'react-hook-form'; 5 | 6 | import clsxm from '@/lib/clsxm'; 7 | 8 | import Typography from '@/components/typography/Typography'; 9 | 10 | export type RadioProps = { 11 | /** Input label */ 12 | label: string; 13 | name: string; 14 | value: string; 15 | /** Small text below input, useful for additional information */ 16 | helperText?: string; 17 | /** Disables the input and shows defaultValue (can be set from React Hook Form) */ 18 | readOnly?: boolean; 19 | /** Disable error style (not disabling error validation) */ 20 | hideError?: boolean; 21 | /** Manual validation using RHF, it is encouraged to use yup resolver instead */ 22 | validation?: RegisterOptions; 23 | } & React.ComponentPropsWithoutRef<'input'>; 24 | 25 | export default function Radio({ 26 | label, 27 | placeholder = '', 28 | helperText, 29 | name, 30 | value, 31 | readOnly = false, 32 | hideError = false, 33 | validation, 34 | disabled, 35 | ...rest 36 | }: RadioProps) { 37 | const { 38 | register, 39 | formState: { errors }, 40 | } = useFormContext(); 41 | const error = get(errors, name); 42 | 43 | return ( 44 |
    45 |
    46 | 66 | 72 | {label} 73 | 74 |
    75 | {!(!hideError && error) && helperText && ( 76 | 77 | {helperText} 78 | 79 | )} 80 | {!hideError && error && ( 81 | 82 | {error?.message?.toString()} 83 | 84 | )} 85 |
    86 | ); 87 | } 88 | -------------------------------------------------------------------------------- /src/components/forms/TextArea.tsx: -------------------------------------------------------------------------------- 1 | import clsx from 'clsx'; 2 | import get from 'lodash.get'; 3 | import { RegisterOptions, useFormContext } from 'react-hook-form'; 4 | 5 | import Typography from '@/components/typography/Typography'; 6 | 7 | export type TextAreaProps = { 8 | label: string; 9 | id: string; 10 | placeholder?: string; 11 | helperText?: string; 12 | readOnly?: boolean; 13 | hideError?: boolean; 14 | validation?: RegisterOptions; 15 | containerClassName?: string; 16 | } & React.ComponentPropsWithoutRef<'textarea'>; 17 | 18 | export default function TextArea({ 19 | label, 20 | placeholder = '', 21 | helperText, 22 | id, 23 | readOnly = false, 24 | hideError = false, 25 | validation, 26 | disabled, 27 | containerClassName, 28 | ...rest 29 | }: TextAreaProps) { 30 | const { 31 | register, 32 | formState: { errors }, 33 | } = useFormContext(); 34 | const error = get(errors, id); 35 | 36 | return ( 37 |
    38 | 39 | {label} 40 | 41 |
    42 |