├── .cspell.json ├── .editorconfig ├── .env.local.example ├── .eslintrc.cjs ├── .github ├── actions │ └── ci-setup │ │ └── action.yml ├── banner.png ├── litebanner.png └── workflows │ ├── ci.yml │ ├── release.yml │ └── update-dependencies.yml ├── .gitignore ├── .prettierignore ├── .vscode ├── settings.json └── tasks.json ├── CHANGELOG.md ├── LICENSE ├── README.md ├── TROUBLESHOOTING.md ├── commitlint.config.cjs ├── components.json ├── e2e └── about.spec.ts ├── lint-staged.config.cjs ├── next-env.d.ts ├── next-sitemap.config.cjs ├── next.config.mjs ├── package.json ├── playwright.config.ts ├── pnpm-lock.yaml ├── postcss.config.mjs ├── prettier.config.cjs ├── public ├── assets │ ├── admin-image.png │ ├── help-assets │ │ ├── organisations-teams.png │ │ └── teams-projects.png │ ├── image-background-login.png │ ├── login-asset-dashboard.png │ ├── logo-login.png │ ├── onboardingFeatures │ │ ├── adminPanel.png │ │ ├── authentication.png │ │ ├── layout.png │ │ ├── nextjs-type-supa.png │ │ └── userImpersonation.png │ └── user-image.png ├── e2e.png ├── images │ ├── admin-dashboard-2.png │ ├── admin-dashboard-maintenance-mode.png │ ├── admin-dashboard-users-2.png │ ├── admin-dashboard-users.png │ ├── projects-view-2.png │ ├── projects-view.png │ ├── supabase-dashboard-api-logs.png │ ├── supabase-dashboard.png │ ├── supabase-graphs-2.png │ ├── supabase-graphs.png │ ├── supabase-sql-editor.png │ └── supabase-tables.png ├── logos │ ├── acme-logo-dark.png │ ├── acme-logo-light.png │ ├── logo-black.png │ ├── logo.png │ ├── nextbase copy.png │ ├── nextbase-dark-logo.png │ ├── nextbase-light-logo.png │ ├── nextbase.png │ ├── nextbase@2x copy.png │ ├── nextbase@2x.png │ ├── nextbase@3x copy.png │ ├── nextbase@3x.png │ ├── nextbase_navlogo.svg │ └── nextbase_navlogo_small.svg ├── mockups │ ├── auth.png │ ├── layouts.png │ ├── maintenance-mode.png │ ├── nst.png │ ├── orgs-teams.png │ ├── placeholder.jpeg │ ├── sign-in-graphic.png │ ├── stripe-gif.gif │ ├── stripe-integration.png │ ├── unstyled-components.png │ └── user-impersonation.png └── unit.png ├── src ├── app │ ├── (dynamic-pages) │ │ ├── (login-pages) │ │ │ └── (login-pages) │ │ │ │ ├── ClientLayout.tsx │ │ │ │ ├── auth │ │ │ │ ├── auth-code-error │ │ │ │ │ └── page.tsx │ │ │ │ ├── callback │ │ │ │ │ └── route.ts │ │ │ │ └── confirm │ │ │ │ │ └── route.ts │ │ │ │ ├── forgot-password │ │ │ │ ├── ForgotPassword.tsx │ │ │ │ └── page.tsx │ │ │ │ ├── layout.tsx │ │ │ │ ├── login │ │ │ │ ├── Login.tsx │ │ │ │ ├── NewLogin.tsx │ │ │ │ └── page.tsx │ │ │ │ ├── sign-up │ │ │ │ ├── Signup.tsx │ │ │ │ └── page.tsx │ │ │ │ └── update-password │ │ │ │ ├── UpdatePassword.tsx │ │ │ │ └── page.tsx │ │ ├── (main-pages) │ │ │ ├── (logged-in-pages) │ │ │ │ ├── dashboard │ │ │ │ │ ├── ClientPage.tsx │ │ │ │ │ ├── new │ │ │ │ │ │ └── page.tsx │ │ │ │ │ └── page.tsx │ │ │ │ ├── layout.tsx │ │ │ │ ├── private-item │ │ │ │ │ ├── [privateItemId] │ │ │ │ │ │ ├── ConfirmDeleteItemDialog.tsx │ │ │ │ │ │ └── page.tsx │ │ │ │ │ └── not-found.tsx │ │ │ │ └── private-items │ │ │ │ │ └── page.tsx │ │ │ ├── ItemsList.tsx │ │ │ ├── PrivateItemsList.tsx │ │ │ ├── item │ │ │ │ ├── [itemId] │ │ │ │ │ ├── ConfirmDeleteItemDialog.tsx │ │ │ │ │ └── page.tsx │ │ │ │ └── not-found.tsx │ │ │ ├── items │ │ │ │ └── page.tsx │ │ │ ├── new │ │ │ │ ├── ClientPage.tsx │ │ │ │ └── page.tsx │ │ │ └── page.tsx │ │ ├── DynamicLayoutProviders.tsx │ │ └── layout.tsx │ ├── (external-pages) │ │ ├── about │ │ │ └── page.tsx │ │ └── layout.tsx │ ├── Banner.tsx │ ├── ClientLayout.tsx │ ├── LoginNavLink.tsx │ ├── MobileNavigation.tsx │ ├── NavLink.tsx │ ├── Navbar.tsx │ └── layout.tsx ├── components │ ├── Auth │ │ ├── Email.tsx │ │ ├── EmailAndPassword.tsx │ │ ├── EmailConfirmationPendingCard.tsx │ │ ├── Icons.tsx │ │ ├── Password.tsx │ │ ├── RedirectingPleaseWaitCard.tsx │ │ └── RenderProviders.tsx │ ├── Button │ │ ├── Button.tsx │ │ └── index.ts │ ├── Footer.tsx │ └── ui │ │ ├── Typography │ │ ├── Blockquote.tsx │ │ ├── H1.tsx │ │ ├── H2.tsx │ │ ├── H3.tsx │ │ ├── H4.tsx │ │ ├── Large.tsx │ │ ├── List.tsx │ │ ├── P.tsx │ │ ├── Small.tsx │ │ ├── Subtle.tsx │ │ └── index.ts │ │ ├── accordion.tsx │ │ ├── alert-dialog.tsx │ │ ├── alert.tsx │ │ ├── aspect-ratio.tsx │ │ ├── avatar.tsx │ │ ├── badge.tsx │ │ ├── breadcrumb.tsx │ │ ├── button.tsx │ │ ├── calendar.tsx │ │ ├── card.tsx │ │ ├── carousel.tsx │ │ ├── chart.tsx │ │ ├── checkbox.tsx │ │ ├── collapsible.tsx │ │ ├── command.tsx │ │ ├── context-menu.tsx │ │ ├── dialog.tsx │ │ ├── drawer.tsx │ │ ├── dropdown-menu.tsx │ │ ├── form.tsx │ │ ├── hover-card.tsx │ │ ├── input-otp.tsx │ │ ├── input.tsx │ │ ├── label.tsx │ │ ├── menubar.tsx │ │ ├── navigation-menu.tsx │ │ ├── pagination.tsx │ │ ├── popover.tsx │ │ ├── progress.tsx │ │ ├── radio-group.tsx │ │ ├── resizable.tsx │ │ ├── scroll-area.tsx │ │ ├── select.tsx │ │ ├── separator.tsx │ │ ├── sheet.tsx │ │ ├── sidebar.tsx │ │ ├── skeleton.tsx │ │ ├── slider.tsx │ │ ├── sonner.tsx │ │ ├── switch.tsx │ │ ├── table.tsx │ │ ├── tabs.tsx │ │ ├── textarea.tsx │ │ ├── toast.tsx │ │ ├── toaster.tsx │ │ ├── toggle-group.tsx │ │ ├── toggle.tsx │ │ └── tooltip.tsx ├── constants.ts ├── contexts │ └── RouterProgressionContext.tsx ├── data │ ├── anon │ │ ├── items.ts │ │ └── privateItems.ts │ ├── auth │ │ └── auth.ts │ └── user │ │ ├── privateItems.ts │ │ └── security.ts ├── environment.d.ts ├── hooks │ ├── use-mobile.ts │ └── use-toast.ts ├── lib │ ├── database.types.ts │ ├── safe-action.ts │ └── utils.ts ├── middleware.ts ├── rsc-data │ └── supabase.ts ├── sometest.test.ts ├── styles │ ├── globals.css │ └── utils.css ├── supabase-clients │ ├── client.ts │ ├── middleware.ts │ └── server.ts ├── types.ts └── utils │ ├── classNames.ts │ ├── cn.ts │ ├── createSuspenseResource.ts │ ├── helpers.ts │ └── supabase-queries.ts ├── supabase ├── .gitignore ├── config.toml ├── migrations │ ├── 20230208104717_init.sql │ └── 20230208104718_private_items.sql └── seed.sql ├── tailwind.config.js ├── tsconfig.json └── vitest.config.ts /.cspell.json: -------------------------------------------------------------------------------- 1 | { 2 | "allowCompoundWords": true, 3 | "ignoreRegExpList": [ 4 | "/.*[0-9].*/" 5 | ], 6 | "language": "en", 7 | "minWordLength": 5, 8 | "version": "0.2", 9 | "words": [] 10 | } 11 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | end_of_line = lf 6 | indent_size = 2 7 | indent_style = space 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | -------------------------------------------------------------------------------- /.env.local.example: -------------------------------------------------------------------------------- 1 | SUPABASE_PROJECT_REF=ref 2 | NEXT_PUBLIC_SUPABASE_URL=url 3 | NEXT_PUBLIC_SUPABASE_ANON_KEY=key 4 | -------------------------------------------------------------------------------- /.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | var tsConfigs = ['./tsconfig.json']; 2 | 3 | var ruleOverrides = {}; 4 | 5 | module.exports = { 6 | overrides: [ 7 | { 8 | extends: [ 9 | 'eslint:recommended', 10 | 'plugin:@typescript-eslint/recommended', 11 | 'plugin:@next/next/recommended', 12 | 'prettier', 13 | ], 14 | parser: '@typescript-eslint/parser', 15 | parserOptions: { 16 | project: tsConfigs, 17 | }, 18 | plugins: ['@typescript-eslint', 'prettier'], 19 | rules: { 20 | 'prettier/prettier': 1, 21 | '@typescript-eslint/no-unused-vars': 1, 22 | }, 23 | files: ['src/**/*.ts', 'src/**/*.tsx'], 24 | }, 25 | { 26 | extends: [ 27 | 'eslint:recommended', 28 | 'plugin:@typescript-eslint/recommended', 29 | 'prettier', 30 | ], 31 | parser: '@typescript-eslint/parser', 32 | parserOptions: { 33 | project: tsConfigs, 34 | }, 35 | plugins: [ 36 | '@typescript-eslint', 37 | 'plugin:playwright/playwright-test', 38 | 'prettier', 39 | ], 40 | rules: { 41 | 'prettier/prettier': 'error', 42 | }, 43 | files: ['e2e/**/*.spec.ts'], 44 | }, 45 | ], 46 | root: true, 47 | ignorePatterns: ['*.js', '*.mjs', '*.cjs', '*.json'], 48 | }; 49 | -------------------------------------------------------------------------------- /.github/actions/ci-setup/action.yml: -------------------------------------------------------------------------------- 1 | name: 'CI setup' 2 | runs: 3 | using: 'composite' 4 | steps: 5 | - name: Use Node.js ${{ matrix.node-version }} 6 | uses: actions/setup-node@v3 7 | with: 8 | node-version: ${{ matrix.node-version }} 9 | - name: Get yarn cache directory path 10 | id: yarn-cache-dir-path 11 | run: echo "::set-output name=dir::$(yarn cache dir)" 12 | shell: bash 13 | - uses: actions/cache@v2 14 | id: yarn-cache # use this to check for `cache-hit` (`steps.yarn-cache.outputs.cache-hit != 'true'`) 15 | with: 16 | path: | 17 | node_modules 18 | */*/node_modules 19 | key: ${{ runner.os }}-${{ hashFiles('**/yarn.lock') }} 20 | - name: Install project dependencies 21 | if: steps.yarn-cache.outputs.cache-hit != 'true' # Over here! 22 | run: yarn 23 | shell: bash 24 | -------------------------------------------------------------------------------- /.github/banner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/imbhargav5/nextbase-nextjs-supabase-starter/3a9445436aa019244fe7423ab2393b052e243fff/.github/banner.png -------------------------------------------------------------------------------- /.github/litebanner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/imbhargav5/nextbase-nextjs-supabase-starter/3a9445436aa019244fe7423ab2393b052e243fff/.github/litebanner.png -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | pull_request: 7 | branches: [main] 8 | 9 | jobs: 10 | build: 11 | strategy: 12 | matrix: 13 | node-version: [18.x] 14 | # See supported Node.js release schedule at https://nodejs.org/en/about/releases/ 15 | 16 | name: Build with ${{ matrix.node-version }} 17 | runs-on: ubuntu-latest 18 | if: ${{ github.repository == 'imbhargav5/nextbase-nextjs13-supabase-starter'}} 19 | steps: 20 | - name: 'Check if user has write access' 21 | uses: 'lannonbr/repo-permission-check-action@2.0.0' 22 | with: 23 | permission: 'write' 24 | env: 25 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 26 | - if: github.event_name == 'pull_request' 27 | name: ⬇️ Checkout repo 28 | uses: actions/checkout@v3 29 | with: 30 | fetch-depth: 0 31 | persist-credentials: false 32 | - if: github.event_name != 'pull_request' 33 | name: ⬇️ Checkout repo 34 | uses: actions/checkout@v3 35 | with: 36 | persist-credentials: false 37 | - name: 🚂 CI Setup 38 | uses: ./.github/actions/ci-setup 39 | - run: yarn run build 40 | 41 | test: 42 | strategy: 43 | matrix: 44 | node-version: [18.x] 45 | 46 | name: Run all tests 47 | runs-on: ubuntu-latest 48 | if: ${{ github.repository == 'imbhargav5/nextbase-nextjs13-supabase-starter'}} 49 | steps: 50 | - name: 'Check if user has write access' 51 | uses: 'lannonbr/repo-permission-check-action@2.0.0' 52 | with: 53 | permission: 'write' 54 | env: 55 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 56 | - if: github.event_name == 'pull_request' 57 | name: ⬇️ Checkout repo 58 | uses: actions/checkout@v3 59 | with: 60 | fetch-depth: 0 61 | persist-credentials: false 62 | - if: github.event_name != 'pull_request' 63 | name: ⬇️ Checkout repo 64 | uses: actions/checkout@v3 65 | with: 66 | persist-credentials: false 67 | - name: 🚂 CI Setup 68 | uses: ./.github/actions/ci-setup 69 | - if: github.event_name == 'pull_request' 70 | uses: wagoid/commitlint-github-action@v5 71 | - name: Linter 72 | run: yarn run lint 73 | 74 | - name: Type checking 75 | run: yarn run tsc 76 | 77 | - name: Run unit tests 78 | run: yarn run test 79 | - name: Install playwright browsers 80 | run: yarn playwright install --with-deps 81 | 82 | - name: Run e2e tests 83 | run: yarn run test:e2e 84 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | workflow_run: 5 | workflows: ['CI'] 6 | types: 7 | - completed 8 | branches: 9 | - main 10 | 11 | jobs: 12 | release: 13 | strategy: 14 | matrix: 15 | node-version: [18.x] 16 | 17 | name: Create a new release 18 | runs-on: ubuntu-latest 19 | if: ${{ github.repository == 'imbhargav5/nextbase-nextjs13-supabase-starter'}} 20 | steps: 21 | - name: 'Check if user has write access' 22 | uses: 'lannonbr/repo-permission-check-action@2.0.0' 23 | with: 24 | permission: 'write' 25 | env: 26 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 27 | - if: github.event_name == 'pull_request' 28 | name: ⬇️ Checkout repo 29 | uses: actions/checkout@v3 30 | with: 31 | ref: ${{ github.event.pull_request.head.ref }} 32 | persist-credentials: false 33 | - if: github.event_name != 'pull_request' 34 | name: ⬇️ Checkout repo 35 | uses: actions/checkout@v3 36 | with: 37 | persist-credentials: false 38 | - name: 🚂 CI Setup 39 | uses: ./.github/actions/ci-setup 40 | - name: Semantic release 41 | env: 42 | GITHUB_TOKEN: ${{ secrets.GH_TOKEN }} 43 | run: yarn semantic-release 44 | -------------------------------------------------------------------------------- /.github/workflows/update-dependencies.yml: -------------------------------------------------------------------------------- 1 | name: Update dependencies 2 | 3 | on: 4 | workflow_dispatch: 5 | schedule: 6 | - cron: '0 0 */7 * *' 7 | 8 | jobs: 9 | update: 10 | strategy: 11 | matrix: 12 | node-version: [18.x] 13 | 14 | name: Update all dependencies 15 | runs-on: ubuntu-latest 16 | if: ${{ github.repository == 'imbhargav5/nextbase-nextjs13-supabase-starter'}} 17 | steps: 18 | - name: 'Check if user has write access' 19 | uses: 'lannonbr/repo-permission-check-action@2.0.0' 20 | with: 21 | permission: 'write' 22 | env: 23 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 24 | - if: github.event_name == 'pull_request' 25 | name: ⬇️ Checkout repo 26 | uses: actions/checkout@v3 27 | with: 28 | ref: ${{ github.event.pull_request.head.ref }} 29 | persist-credentials: false 30 | - if: github.event_name != 'pull_request' 31 | name: ⬇️ Checkout repo 32 | uses: actions/checkout@v3 33 | with: 34 | persist-credentials: false 35 | - name: 🚂 CI Setup 36 | uses: ./.github/actions/ci-setup 37 | - run: yarn npm-check-updates -u # Update dependencies 38 | - run: rm -Rf node_modules yarn.lock 39 | - run: yarn 40 | - name: Create Pull Request 41 | uses: peter-evans/create-pull-request@v4 42 | with: 43 | token: ${{ secrets.GH_TOKEN }} 44 | commit-message: 'build: update dependencies to the latest version' 45 | title: Update dependencies to the latest version 46 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .env.local 2 | .env.*.local 3 | node_modules 4 | tsconfig.tsbuildinfo 5 | supabase/.temp/project-ref 6 | .DS_Store 7 | 8 | # next.js 9 | /.next 10 | /out 11 | 12 | # next-sitemap 13 | public/robots.txt 14 | public/sitemap.xml 15 | public/sitemap-*.xml 16 | 17 | # debug 18 | npm-debug.log* 19 | pnpm-debug.log* 20 | yarn-debug.log* 21 | yarn-error.log* 22 | 23 | *.tsbuildinfo 24 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | src/app/*.css 2 | src/lib/database.types.ts 3 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "eslint.workingDirectories": ["./"], 3 | "editor.defaultFormatter": "esbenp.prettier-vscode", 4 | "editor.formatOnSave": true, 5 | "editor.codeActionsOnSave": { 6 | "source.fixAll.eslint": "explicit", 7 | "source.organizeImports": "explicit", 8 | "source.sortMembers": "explicit" 9 | }, 10 | "eslint.codeActionsOnSave.rules": ["!@typescript-eslint/*", "*"], 11 | "typescript.tsdk": "node_modules/typescript/lib", 12 | "eslint.packageManager": "yarn", 13 | "eslint.validate": [ 14 | "javascript", 15 | "javascriptreact", 16 | "typescript", 17 | "typescriptreact" 18 | ], 19 | "files.exclude": { 20 | "**/.git": true, 21 | "**/.svn": true, 22 | "**/.hg": true, 23 | "**/CVS": true, 24 | "**/.DS_Store": true, 25 | "**/node_modules": false 26 | }, 27 | "typescript.enablePromptUseWorkspaceTsdk": true, 28 | "[typescriptreact]": { 29 | "editor.defaultFormatter": "vscode.typescript-language-features" 30 | }, 31 | "cSpell.words": ["Reflio", "signup"] 32 | } 33 | -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0.0", 3 | 4 | "tasks": [ 5 | { 6 | "label": "Start Supabase", 7 | "type": "shell", 8 | "command": "pnpm supabase start", 9 | "problemMatcher": [], 10 | "presentation": { 11 | "echo": true, 12 | "reveal": "silent", 13 | "focus": false, 14 | "panel": "dedicated", 15 | "showReuseMessage": true, 16 | "clear": false 17 | } 18 | }, 19 | { 20 | "label": "Run Server", 21 | "type": "shell", 22 | "command": "pnpm dev", 23 | "problemMatcher": [], 24 | 25 | "presentation": { 26 | "echo": true, 27 | "reveal": "silent", 28 | "focus": false, 29 | "panel": "dedicated", 30 | "showReuseMessage": true, 31 | "clear": false 32 | } 33 | }, 34 | { 35 | "label": "Development", 36 | "detail": "Start all tools for development", 37 | "dependsOn": ["Start Supabase", "Run Server"], 38 | "problemMatcher": [], 39 | "group": { 40 | "kind": "build", 41 | "isDefault": true 42 | } 43 | } 44 | ] 45 | } 46 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # [2.2.0](https://github.com/imbhargav5/nextbase-nextjs13-supabase-starter/compare/v2.1.0...v2.2.0) (2023-11-25) 2 | 3 | 4 | ### Bug Fixes 5 | 6 | * **build:** fixes ([d50ee82](https://github.com/imbhargav5/nextbase-nextjs13-supabase-starter/commit/d50ee82a3d766b584a2fc66ea1a4b9409f83e8fc)) 7 | 8 | 9 | ### Features 10 | 11 | * **upgrade:** next.js 14 + improved data fetching ([074f464](https://github.com/imbhargav5/nextbase-nextjs13-supabase-starter/commit/074f464b00778918c2e2cd14f470272067e0d2d3)) 12 | 13 | # [2.1.0](https://github.com/imbhargav5/nextbase-nextjs13-supabase-starter/compare/v2.0.1...v2.1.0) (2023-09-21) 14 | 15 | 16 | ### Features 17 | 18 | * **auth:** login added ([6ca6940](https://github.com/imbhargav5/nextbase-nextjs13-supabase-starter/commit/6ca6940aa2a22e75daf807573afd8057db000808)) 19 | 20 | ## [2.0.1](https://github.com/imbhargav5/nextbase-nextjs13-supabase-starter/compare/v2.0.0...v2.0.1) (2023-07-31) 21 | 22 | 23 | ### Bug Fixes 24 | 25 | * **build:** build fixes ([7bdeaeb](https://github.com/imbhargav5/nextbase-nextjs13-supabase-starter/commit/7bdeaeb4cc8581a7027919ac04b957a6f941c6d0)) 26 | 27 | # [2.0.0](https://github.com/imbhargav5/nextbase-nextjs13-supabase-starter/compare/v1.0.3...v2.0.0) (2023-07-31) 28 | 29 | 30 | ### Features 31 | 32 | * **core:** server actions ([4bc69f6](https://github.com/imbhargav5/nextbase-nextjs13-supabase-starter/commit/4bc69f6c90a61e097635f4871520e4cde55736fc)) 33 | 34 | 35 | ### BREAKING CHANGES 36 | 37 | * **core:** implicit auth -> pkce 38 | 39 | ## [1.0.3](https://github.com/imbhargav5/nextbase-nextjs13-supabase-starter/compare/v1.0.2...v1.0.3) (2023-02-26) 40 | 41 | 42 | ### Bug Fixes 43 | 44 | * **upgrade:** upgrade to next 13.2 ([7ac20ea](https://github.com/imbhargav5/nextbase-nextjs13-supabase-starter/commit/7ac20ea9926645d85120f19c81aeb8184f25fe97)) 45 | 46 | ## [1.0.2](https://github.com/imbhargav5/nextbase-nextjs13-supabase-starter/compare/v1.0.1...v1.0.2) (2023-02-14) 47 | 48 | 49 | ### Bug Fixes 50 | 51 | * **tailwindcss:** production build ([5872cf3](https://github.com/imbhargav5/nextbase-nextjs13-supabase-starter/commit/5872cf38a4d704ecf319d0d12c4ca211e2dc17b4)) 52 | 53 | ## [1.0.1](https://github.com/imbhargav5/nextbase-nextjs13-supabase-starter/compare/v1.0.0...v1.0.1) (2023-02-08) 54 | 55 | 56 | ### Bug Fixes 57 | 58 | * **react-no-ssr:** remove react no ssr ([0030eaa](https://github.com/imbhargav5/nextbase-nextjs13-supabase-starter/commit/0030eaab147b3f4184c42dca160d8030bb944cfe)) 59 | 60 | # 1.0.0 (2023-02-08) 61 | 62 | 63 | ### Features 64 | 65 | * **items:** ability to view and add items ([9610939](https://github.com/imbhargav5/nextbase-nextjs13-supabase-starter/commit/9610939028ecd510757c3d89ab2124c3d452bb64)) 66 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Bhargav Ponnapalli 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 | # NextBase Starter 2 | 3 | ![NextBase Lite Open Source Free Boilerplate](https://github.com/imbhargav5/nextbase-nextjs13-supabase-starter/blob/main/.github/litebanner.png?raw=true) 4 | 5 | Nextbase Lite is a simple Next.js 13 + Supabase boilerplate. It includes a Next.js 13 app with Typescript, Supabase and Tailwind CSS. It includes the all new `app` folder, `layout` components, React `server components` and more! 6 | 7 | ## Features 8 | 9 | - 🚀 Next.js 15 10 | - 💻 Data fetching examples in React server and client components. Suspenseful data fetching with minimal loading screens. 11 | - ⚛️ React query setup configured 12 | - 🔥 React Hot Toast component 13 | - 💻 Fully typed with Typescript. Includes automatic type generation for Supabase tables 14 | - 🎨 Tailwindcss 15 | - 🧪 Unit testing and integration testing setups built-in 16 | - 💚 Eslint, typescript, prettier, postcss configured for dev and test environments 17 | - 📈 Automatic sitemap generation 18 | - 🔍 SEO metadata, JSON-LD and Open Graph tags with NEXT SEO 19 | - ✍️ Semantic release with Automatic changelog generation 20 | - 🎨 Prettier Code formatter 21 | - 💎 Minimal styling 22 | - 📖 Codebase which is easy to read and modify 23 | 24 | ### Development 25 | 26 | 1. Clone the repo 27 | 2. Install dependencies with `yarn` 28 | 3. Create a Supabase account if you don't have one already 29 | 4. Create a new project in Supabase 30 | 5. Link Supabase to your project using `yarn supabase link --project-ref `. You can get your project ref from the Supabase Project dashboard (Project Settings -> API) 31 | 6. Duplicate `.env.local.example` and rename it to `.env.local` and add the Project ref, Supabase URL and anon key. 32 | 7. Push the database schema to your Supabase project using `yarn supabase db push`. 33 | 8. Generate types for your Supabase tables using `yarn generate:types:local`. 34 | 9. Run `yarn dev` to start the development server. 35 | 36 | ### Testing 37 | 38 | 1. Unit test using `yarn test` 39 | 2. End-to-end test using `yarn test:e2e` 40 | 41 | ### Deployment 42 | 43 | This is a simple Next.js project. Deployment is the same as any other Next.js project. You can deploy it to Vercel, Netlify, or any other hosting provider. 44 | 45 | ### Contributing 46 | 47 | Contributions are welcome. Please open an issue or a PR. 48 | 49 | ### License 50 | 51 | MIT 52 | 53 | ### Troubleshooting 54 | 55 | Checkout the [TROUBLESHOOTING.md](./TROUBLESHOOTING.md) file for common issues. 56 | 57 | ## Premium NextBase Boilerplate 58 | 59 | Also checkout our premium boilerplate with more features. It includes a fully functional authentication system, user profiles, organisations, row level security, and more. 60 | 61 | [![NextBase Boilerplate](https://github.com/imbhargav5/nextbase-nextjs13-supabase-starter/blob/main/.github/banner.png?raw=true)](https://usenextbase.com) 62 | -------------------------------------------------------------------------------- /TROUBLESHOOTING.md: -------------------------------------------------------------------------------- 1 | ## HUSKY setup 2 | 3 | If you have trouble running husky pre commit hooks, make sure they have executable permissions. You can do this by running 4 | 5 | ```bash 6 | chmod ug+x .husky/* 7 | chmod ug+x .git/hooks/* 8 | ``` 9 | 10 | ## Github Actions 11 | 12 | `GH_TOKEN` - Needs to have write access for semantic release action. Generate a personal access token else RELEASE action will fail. 13 | -------------------------------------------------------------------------------- /commitlint.config.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { extends: ['@commitlint/config-conventional'] }; 2 | -------------------------------------------------------------------------------- /components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema.json", 3 | "style": "default", 4 | "rsc": true, 5 | "tsx": true, 6 | "tailwind": { 7 | "config": "tailwind.config.js", 8 | "css": "src/app/globals.css", 9 | "baseColor": "zinc", 10 | "cssVariables": true, 11 | "prefix": "" 12 | }, 13 | "aliases": { 14 | "components": "@/components", 15 | "utils": "@/lib/utils", 16 | "ui": "@/components/ui", 17 | "lib": "@/lib", 18 | "hooks": "@/hooks" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /e2e/about.spec.ts: -------------------------------------------------------------------------------- 1 | import { test, expect } from '@playwright/test' 2 | 3 | test('should have text in about page', async ({ page }) => { 4 | // Start from the index page (the baseURL is set via the webServer in the playwright.config.ts) 5 | await page.goto('/about') 6 | await expect(page.locator('h1')).toContainText('About') 7 | }) 8 | -------------------------------------------------------------------------------- /lint-staged.config.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | '*.{js,jsx,ts,tsx,cjs,mjs}': ['eslint --fix', 'eslint'], 3 | '**/*.ts?(x)': ['tsc-files --noEmit'], 4 | '*.{json,yaml}': ['prettier --write'], 5 | }; 6 | -------------------------------------------------------------------------------- /next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | 4 | // NOTE: This file should not be edited 5 | // see https://nextjs.org/docs/app/api-reference/config/typescript for more information. 6 | -------------------------------------------------------------------------------- /next-sitemap.config.cjs: -------------------------------------------------------------------------------- 1 | /** @type {import('next-sitemap').IConfig} */ 2 | module.exports = { 3 | siteUrl: 'https://my-awesome-saas.com', // FIXME: Change to your production URL 4 | generateRobotsTxt: true, 5 | }; 6 | -------------------------------------------------------------------------------- /next.config.mjs: -------------------------------------------------------------------------------- 1 | export default { 2 | images: { 3 | remotePatterns: [ 4 | { 5 | protocol: 'https', 6 | hostname: '*.supabase.co', 7 | port: '', 8 | pathname: '/**', 9 | }, 10 | { 11 | protocol: 'https', 12 | hostname: '*.supabase.com', 13 | port: '', 14 | pathname: '/**', 15 | }, 16 | { 17 | protocol: 'https', 18 | hostname: '*.unsplash.com', 19 | port: '', 20 | pathname: '/**', 21 | }, 22 | ], 23 | }, 24 | }; 25 | -------------------------------------------------------------------------------- /playwright.config.ts: -------------------------------------------------------------------------------- 1 | import { PlaywrightTestConfig, devices } from '@playwright/test' 2 | import path from 'path' 3 | 4 | // Use process.env.PORT by default and fallback to port 3000 5 | const PORT = process.env.PORT || 3000 6 | 7 | // Set webServer.url and use.baseURL with the location of the WebServer respecting the correct set port 8 | const baseURL = `http://localhost:${PORT}` 9 | 10 | // Reference: https://playwright.dev/docs/test-configuration 11 | const config: PlaywrightTestConfig = { 12 | // Timeout per test 13 | timeout: 30 * 1000, 14 | // Test directory 15 | testDir: path.join(__dirname, 'e2e'), 16 | // If a test fails, retry it additional 2 times 17 | retries: 2, 18 | // Artifacts folder where screenshots, videos, and traces are stored. 19 | outputDir: 'test-results/', 20 | 21 | // Run your local dev server before starting the tests: 22 | // https://playwright.dev/docs/test-advanced#launching-a-development-web-server-during-the-tests 23 | webServer: { 24 | command: 'yarn run dev', 25 | url: baseURL, 26 | timeout: 120 * 1000, 27 | reuseExistingServer: !process.env.CI, 28 | }, 29 | 30 | use: { 31 | // Use baseURL so to make navigations relative. 32 | // More information: https://playwright.dev/docs/api/class-testoptions#test-options-base-url 33 | baseURL, 34 | 35 | // Retry a test if its failing with enabled tracing. This allows you to analyse the DOM, console logs, network traffic etc. 36 | // More information: https://playwright.dev/docs/trace-viewer 37 | trace: 'retry-with-trace', 38 | 39 | // All available context options: https://playwright.dev/docs/api/class-browser#browser-new-context 40 | // contextOptions: { 41 | // ignoreHTTPSErrors: true, 42 | // }, 43 | }, 44 | 45 | projects: [ 46 | { 47 | name: 'Desktop Chrome', 48 | use: { 49 | ...devices['Desktop Chrome'], 50 | }, 51 | }, 52 | // { 53 | // name: 'Desktop Firefox', 54 | // use: { 55 | // ...devices['Desktop Firefox'], 56 | // }, 57 | // }, 58 | // { 59 | // name: 'Desktop Safari', 60 | // use: { 61 | // ...devices['Desktop Safari'], 62 | // }, 63 | // }, 64 | // Test against mobile viewports. 65 | { 66 | name: 'Mobile Chrome', 67 | use: { 68 | ...devices['Pixel 5'], 69 | }, 70 | }, 71 | { 72 | name: 'Mobile Safari', 73 | use: devices['iPhone 12'], 74 | }, 75 | ], 76 | } 77 | export default config 78 | -------------------------------------------------------------------------------- /postcss.config.mjs: -------------------------------------------------------------------------------- 1 | export default { 2 | plugins: { 3 | "@tailwindcss/postcss": {}, 4 | }, 5 | }; 6 | -------------------------------------------------------------------------------- /prettier.config.cjs: -------------------------------------------------------------------------------- 1 | /** 2 | * Options for Prettier. 3 | * 4 | * @see https://prettier.io/docs/en/options.html 5 | */ 6 | 7 | module.exports = { 8 | singleQuote: true, 9 | }; 10 | -------------------------------------------------------------------------------- /public/assets/admin-image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/imbhargav5/nextbase-nextjs-supabase-starter/3a9445436aa019244fe7423ab2393b052e243fff/public/assets/admin-image.png -------------------------------------------------------------------------------- /public/assets/help-assets/organisations-teams.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/imbhargav5/nextbase-nextjs-supabase-starter/3a9445436aa019244fe7423ab2393b052e243fff/public/assets/help-assets/organisations-teams.png -------------------------------------------------------------------------------- /public/assets/help-assets/teams-projects.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/imbhargav5/nextbase-nextjs-supabase-starter/3a9445436aa019244fe7423ab2393b052e243fff/public/assets/help-assets/teams-projects.png -------------------------------------------------------------------------------- /public/assets/image-background-login.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/imbhargav5/nextbase-nextjs-supabase-starter/3a9445436aa019244fe7423ab2393b052e243fff/public/assets/image-background-login.png -------------------------------------------------------------------------------- /public/assets/login-asset-dashboard.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/imbhargav5/nextbase-nextjs-supabase-starter/3a9445436aa019244fe7423ab2393b052e243fff/public/assets/login-asset-dashboard.png -------------------------------------------------------------------------------- /public/assets/logo-login.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/imbhargav5/nextbase-nextjs-supabase-starter/3a9445436aa019244fe7423ab2393b052e243fff/public/assets/logo-login.png -------------------------------------------------------------------------------- /public/assets/onboardingFeatures/adminPanel.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/imbhargav5/nextbase-nextjs-supabase-starter/3a9445436aa019244fe7423ab2393b052e243fff/public/assets/onboardingFeatures/adminPanel.png -------------------------------------------------------------------------------- /public/assets/onboardingFeatures/authentication.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/imbhargav5/nextbase-nextjs-supabase-starter/3a9445436aa019244fe7423ab2393b052e243fff/public/assets/onboardingFeatures/authentication.png -------------------------------------------------------------------------------- /public/assets/onboardingFeatures/layout.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/imbhargav5/nextbase-nextjs-supabase-starter/3a9445436aa019244fe7423ab2393b052e243fff/public/assets/onboardingFeatures/layout.png -------------------------------------------------------------------------------- /public/assets/onboardingFeatures/nextjs-type-supa.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/imbhargav5/nextbase-nextjs-supabase-starter/3a9445436aa019244fe7423ab2393b052e243fff/public/assets/onboardingFeatures/nextjs-type-supa.png -------------------------------------------------------------------------------- /public/assets/onboardingFeatures/userImpersonation.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/imbhargav5/nextbase-nextjs-supabase-starter/3a9445436aa019244fe7423ab2393b052e243fff/public/assets/onboardingFeatures/userImpersonation.png -------------------------------------------------------------------------------- /public/assets/user-image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/imbhargav5/nextbase-nextjs-supabase-starter/3a9445436aa019244fe7423ab2393b052e243fff/public/assets/user-image.png -------------------------------------------------------------------------------- /public/e2e.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/imbhargav5/nextbase-nextjs-supabase-starter/3a9445436aa019244fe7423ab2393b052e243fff/public/e2e.png -------------------------------------------------------------------------------- /public/images/admin-dashboard-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/imbhargav5/nextbase-nextjs-supabase-starter/3a9445436aa019244fe7423ab2393b052e243fff/public/images/admin-dashboard-2.png -------------------------------------------------------------------------------- /public/images/admin-dashboard-maintenance-mode.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/imbhargav5/nextbase-nextjs-supabase-starter/3a9445436aa019244fe7423ab2393b052e243fff/public/images/admin-dashboard-maintenance-mode.png -------------------------------------------------------------------------------- /public/images/admin-dashboard-users-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/imbhargav5/nextbase-nextjs-supabase-starter/3a9445436aa019244fe7423ab2393b052e243fff/public/images/admin-dashboard-users-2.png -------------------------------------------------------------------------------- /public/images/admin-dashboard-users.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/imbhargav5/nextbase-nextjs-supabase-starter/3a9445436aa019244fe7423ab2393b052e243fff/public/images/admin-dashboard-users.png -------------------------------------------------------------------------------- /public/images/projects-view-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/imbhargav5/nextbase-nextjs-supabase-starter/3a9445436aa019244fe7423ab2393b052e243fff/public/images/projects-view-2.png -------------------------------------------------------------------------------- /public/images/projects-view.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/imbhargav5/nextbase-nextjs-supabase-starter/3a9445436aa019244fe7423ab2393b052e243fff/public/images/projects-view.png -------------------------------------------------------------------------------- /public/images/supabase-dashboard-api-logs.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/imbhargav5/nextbase-nextjs-supabase-starter/3a9445436aa019244fe7423ab2393b052e243fff/public/images/supabase-dashboard-api-logs.png -------------------------------------------------------------------------------- /public/images/supabase-dashboard.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/imbhargav5/nextbase-nextjs-supabase-starter/3a9445436aa019244fe7423ab2393b052e243fff/public/images/supabase-dashboard.png -------------------------------------------------------------------------------- /public/images/supabase-graphs-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/imbhargav5/nextbase-nextjs-supabase-starter/3a9445436aa019244fe7423ab2393b052e243fff/public/images/supabase-graphs-2.png -------------------------------------------------------------------------------- /public/images/supabase-graphs.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/imbhargav5/nextbase-nextjs-supabase-starter/3a9445436aa019244fe7423ab2393b052e243fff/public/images/supabase-graphs.png -------------------------------------------------------------------------------- /public/images/supabase-sql-editor.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/imbhargav5/nextbase-nextjs-supabase-starter/3a9445436aa019244fe7423ab2393b052e243fff/public/images/supabase-sql-editor.png -------------------------------------------------------------------------------- /public/images/supabase-tables.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/imbhargav5/nextbase-nextjs-supabase-starter/3a9445436aa019244fe7423ab2393b052e243fff/public/images/supabase-tables.png -------------------------------------------------------------------------------- /public/logos/acme-logo-dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/imbhargav5/nextbase-nextjs-supabase-starter/3a9445436aa019244fe7423ab2393b052e243fff/public/logos/acme-logo-dark.png -------------------------------------------------------------------------------- /public/logos/acme-logo-light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/imbhargav5/nextbase-nextjs-supabase-starter/3a9445436aa019244fe7423ab2393b052e243fff/public/logos/acme-logo-light.png -------------------------------------------------------------------------------- /public/logos/logo-black.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/imbhargav5/nextbase-nextjs-supabase-starter/3a9445436aa019244fe7423ab2393b052e243fff/public/logos/logo-black.png -------------------------------------------------------------------------------- /public/logos/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/imbhargav5/nextbase-nextjs-supabase-starter/3a9445436aa019244fe7423ab2393b052e243fff/public/logos/logo.png -------------------------------------------------------------------------------- /public/logos/nextbase copy.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/imbhargav5/nextbase-nextjs-supabase-starter/3a9445436aa019244fe7423ab2393b052e243fff/public/logos/nextbase copy.png -------------------------------------------------------------------------------- /public/logos/nextbase-dark-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/imbhargav5/nextbase-nextjs-supabase-starter/3a9445436aa019244fe7423ab2393b052e243fff/public/logos/nextbase-dark-logo.png -------------------------------------------------------------------------------- /public/logos/nextbase-light-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/imbhargav5/nextbase-nextjs-supabase-starter/3a9445436aa019244fe7423ab2393b052e243fff/public/logos/nextbase-light-logo.png -------------------------------------------------------------------------------- /public/logos/nextbase.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/imbhargav5/nextbase-nextjs-supabase-starter/3a9445436aa019244fe7423ab2393b052e243fff/public/logos/nextbase.png -------------------------------------------------------------------------------- /public/logos/nextbase@2x copy.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/imbhargav5/nextbase-nextjs-supabase-starter/3a9445436aa019244fe7423ab2393b052e243fff/public/logos/nextbase@2x copy.png -------------------------------------------------------------------------------- /public/logos/nextbase@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/imbhargav5/nextbase-nextjs-supabase-starter/3a9445436aa019244fe7423ab2393b052e243fff/public/logos/nextbase@2x.png -------------------------------------------------------------------------------- /public/logos/nextbase@3x copy.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/imbhargav5/nextbase-nextjs-supabase-starter/3a9445436aa019244fe7423ab2393b052e243fff/public/logos/nextbase@3x copy.png -------------------------------------------------------------------------------- /public/logos/nextbase@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/imbhargav5/nextbase-nextjs-supabase-starter/3a9445436aa019244fe7423ab2393b052e243fff/public/logos/nextbase@3x.png -------------------------------------------------------------------------------- /public/mockups/auth.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/imbhargav5/nextbase-nextjs-supabase-starter/3a9445436aa019244fe7423ab2393b052e243fff/public/mockups/auth.png -------------------------------------------------------------------------------- /public/mockups/layouts.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/imbhargav5/nextbase-nextjs-supabase-starter/3a9445436aa019244fe7423ab2393b052e243fff/public/mockups/layouts.png -------------------------------------------------------------------------------- /public/mockups/maintenance-mode.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/imbhargav5/nextbase-nextjs-supabase-starter/3a9445436aa019244fe7423ab2393b052e243fff/public/mockups/maintenance-mode.png -------------------------------------------------------------------------------- /public/mockups/nst.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/imbhargav5/nextbase-nextjs-supabase-starter/3a9445436aa019244fe7423ab2393b052e243fff/public/mockups/nst.png -------------------------------------------------------------------------------- /public/mockups/orgs-teams.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/imbhargav5/nextbase-nextjs-supabase-starter/3a9445436aa019244fe7423ab2393b052e243fff/public/mockups/orgs-teams.png -------------------------------------------------------------------------------- /public/mockups/placeholder.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/imbhargav5/nextbase-nextjs-supabase-starter/3a9445436aa019244fe7423ab2393b052e243fff/public/mockups/placeholder.jpeg -------------------------------------------------------------------------------- /public/mockups/sign-in-graphic.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/imbhargav5/nextbase-nextjs-supabase-starter/3a9445436aa019244fe7423ab2393b052e243fff/public/mockups/sign-in-graphic.png -------------------------------------------------------------------------------- /public/mockups/stripe-gif.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/imbhargav5/nextbase-nextjs-supabase-starter/3a9445436aa019244fe7423ab2393b052e243fff/public/mockups/stripe-gif.gif -------------------------------------------------------------------------------- /public/mockups/stripe-integration.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/imbhargav5/nextbase-nextjs-supabase-starter/3a9445436aa019244fe7423ab2393b052e243fff/public/mockups/stripe-integration.png -------------------------------------------------------------------------------- /public/mockups/unstyled-components.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/imbhargav5/nextbase-nextjs-supabase-starter/3a9445436aa019244fe7423ab2393b052e243fff/public/mockups/unstyled-components.png -------------------------------------------------------------------------------- /public/mockups/user-impersonation.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/imbhargav5/nextbase-nextjs-supabase-starter/3a9445436aa019244fe7423ab2393b052e243fff/public/mockups/user-impersonation.png -------------------------------------------------------------------------------- /public/unit.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/imbhargav5/nextbase-nextjs-supabase-starter/3a9445436aa019244fe7423ab2393b052e243fff/public/unit.png -------------------------------------------------------------------------------- /src/app/(dynamic-pages)/(login-pages)/(login-pages)/ClientLayout.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import type { ReactNode } from 'react'; 4 | 5 | export function ClientLayout({ children }: { children: ReactNode }) { 6 | return ( 7 |
8 |
9 |
10 |
{children}
11 |
12 |
13 |
14 | ); 15 | } 16 | -------------------------------------------------------------------------------- /src/app/(dynamic-pages)/(login-pages)/(login-pages)/auth/auth-code-error/page.tsx: -------------------------------------------------------------------------------- 1 | import { Typography as T } from 'src/components/ui/Typography'; 2 | 3 | export default function AuthErrorPage() { 4 | return ( 5 |
6 | Authentication Error 7 | An error occurred during authentication. Please try again. 8 |
9 | ); 10 | } 11 | -------------------------------------------------------------------------------- /src/app/(dynamic-pages)/(login-pages)/(login-pages)/auth/callback/route.ts: -------------------------------------------------------------------------------- 1 | import { createRouteHandlerClient } from '@supabase/auth-helpers-nextjs'; 2 | import { revalidatePath } from 'next/cache'; 3 | import { cookies } from 'next/headers'; 4 | import { NextResponse } from 'next/server'; 5 | 6 | export async function GET(request: Request) { 7 | const requestUrl = new URL(request.url); 8 | const code = requestUrl.searchParams.get('code'); 9 | const next = requestUrl.searchParams.get('next'); 10 | 11 | if (code) { 12 | const supabase = createRouteHandlerClient({ cookies }); 13 | try { 14 | // Exchange the code for a session 15 | await supabase.auth.exchangeCodeForSession(code); 16 | } catch (error) { 17 | // Handle error 18 | console.error('Failed to exchange code for session: ', error); 19 | // Potentially return an error response here 20 | } 21 | } 22 | revalidatePath('/', 'layout'); 23 | 24 | let redirectTo = new URL('/dashboard', requestUrl.origin); 25 | 26 | if (next) { 27 | // decode next param 28 | const decodedNext = decodeURIComponent(next); 29 | // validate next param 30 | redirectTo = new URL(decodedNext, requestUrl.origin); 31 | } 32 | return NextResponse.redirect(redirectTo); 33 | } 34 | -------------------------------------------------------------------------------- /src/app/(dynamic-pages)/(login-pages)/(login-pages)/auth/confirm/route.ts: -------------------------------------------------------------------------------- 1 | import { createRouteHandlerClient } from '@supabase/auth-helpers-nextjs'; 2 | import { cookies } from 'next/headers'; 3 | import { NextRequest, NextResponse } from 'next/server'; 4 | 5 | export async function GET(req: NextRequest) { 6 | const { searchParams } = new URL(req.url); 7 | const token_hash = searchParams.get('token_hash'); 8 | const next = searchParams.get('next') ?? '/dashboard'; 9 | if (token_hash) { 10 | const supabase = createRouteHandlerClient({ cookies }); 11 | const { error } = await supabase.auth.verifyOtp({ 12 | type: 'magiclink', 13 | token_hash, 14 | }); 15 | if (!error) { 16 | return NextResponse.redirect(new URL(`/${next.slice(1)}`, req.url)); 17 | } 18 | } 19 | // return the user to an error page with some instructions 20 | return NextResponse.redirect(new URL('/auth/auth-code-error', req.url)); 21 | } 22 | -------------------------------------------------------------------------------- /src/app/(dynamic-pages)/(login-pages)/(login-pages)/forgot-password/ForgotPassword.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { useAction } from 'next-safe-action/hooks'; 4 | import { useRef, useState, type JSX } from 'react'; 5 | import { toast } from 'sonner'; 6 | 7 | import { Email } from '@/components/Auth/Email'; 8 | import { EmailConfirmationPendingCard } from '@/components/Auth/EmailConfirmationPendingCard'; 9 | import { T } from '@/components/ui/Typography'; 10 | import { Card } from '@/components/ui/card'; 11 | import { resetPasswordAction } from '@/data/auth/auth'; 12 | 13 | export function ForgotPassword(): JSX.Element { 14 | const [successMessage, setSuccessMessage] = useState(null); 15 | const toastRef = useRef(undefined); 16 | 17 | const { execute, status } = useAction(resetPasswordAction, { 18 | onExecute: () => { 19 | toastRef.current = toast.loading('Sending password reset link...'); 20 | }, 21 | onSuccess: () => { 22 | toast.success('Password reset link sent!', { 23 | id: toastRef.current, 24 | }); 25 | toastRef.current = undefined; 26 | setSuccessMessage('A password reset link has been sent to your email!'); 27 | }, 28 | onError: ({ error }) => { 29 | const errorMessage = 30 | error.serverError ?? 'Failed to send password reset link'; 31 | toast.error(errorMessage, { 32 | id: toastRef.current, 33 | }); 34 | toastRef.current = undefined; 35 | }, 36 | }); 37 | 38 | return ( 39 | <> 40 | {successMessage ? ( 41 | 47 | ) : ( 48 | 49 |
50 | Forgot Password 51 | 52 | Enter your email to receive a Magic Link to reset your password. 53 | 54 | 55 | { 57 | execute({ email }); 58 | }} 59 | isLoading={status === 'executing'} 60 | view="forgot-password" 61 | /> 62 |
63 |
64 | )} 65 | 66 | ); 67 | } 68 | -------------------------------------------------------------------------------- /src/app/(dynamic-pages)/(login-pages)/(login-pages)/forgot-password/page.tsx: -------------------------------------------------------------------------------- 1 | import { ForgotPassword } from './ForgotPassword'; 2 | 3 | export default function ForgotPasswordPage() { 4 | return ; 5 | } 6 | -------------------------------------------------------------------------------- /src/app/(dynamic-pages)/(login-pages)/(login-pages)/layout.tsx: -------------------------------------------------------------------------------- 1 | import { ClientLayout } from './ClientLayout'; 2 | 3 | export default function AuthLayout({ 4 | children, 5 | }: { 6 | children: React.ReactNode; 7 | }) { 8 | return {children}; 9 | } 10 | -------------------------------------------------------------------------------- /src/app/(dynamic-pages)/(login-pages)/(login-pages)/login/page.tsx: -------------------------------------------------------------------------------- 1 | import { z } from 'zod'; 2 | import { Login } from './Login'; 3 | 4 | const SearchParamsSchema = z.object({ 5 | next: z.string().optional(), 6 | nextActionType: z.string().optional(), 7 | }); 8 | 9 | export default async function LoginPage(props: { 10 | searchParams: Promise; 11 | }) { 12 | const searchParams = await props.searchParams; 13 | const { next, nextActionType } = SearchParamsSchema.parse(searchParams); 14 | return ; 15 | } 16 | -------------------------------------------------------------------------------- /src/app/(dynamic-pages)/(login-pages)/(login-pages)/sign-up/page.tsx: -------------------------------------------------------------------------------- 1 | import { z } from 'zod'; 2 | import { SignUp } from './Signup'; 3 | 4 | const SearchParamsSchema = z.object({ 5 | next: z.string().optional(), 6 | nextActionType: z.string().optional(), 7 | }); 8 | 9 | export default async function SignUpPage(props: { 10 | searchParams: Promise; 11 | }) { 12 | const searchParams = await props.searchParams; 13 | const { next, nextActionType } = SearchParamsSchema.parse(searchParams); 14 | return ; 15 | } 16 | -------------------------------------------------------------------------------- /src/app/(dynamic-pages)/(login-pages)/(login-pages)/update-password/UpdatePassword.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { useAction } from 'next-safe-action/hooks'; 4 | import { useRouter } from 'next/navigation'; 5 | import { useRef } from 'react'; 6 | import { toast } from 'sonner'; 7 | 8 | import { Password } from '@/components/Auth/Password'; 9 | import { 10 | Card, 11 | CardContent, 12 | CardDescription, 13 | CardHeader, 14 | CardTitle, 15 | } from '@/components/ui/card'; 16 | import { updatePasswordAction } from '@/data/user/security'; 17 | 18 | export function UpdatePassword() { 19 | const router = useRouter(); 20 | const toastRef = useRef(undefined); 21 | 22 | const { execute, status } = useAction(updatePasswordAction, { 23 | onExecute: () => { 24 | toastRef.current = toast.loading('Updating password...'); 25 | }, 26 | onSuccess: () => { 27 | toast.success('Password updated!', { 28 | id: toastRef.current, 29 | }); 30 | toastRef.current = undefined; 31 | router.push('/auth/callback'); 32 | }, 33 | onError: ({ error }) => { 34 | const errorMessage = error.serverError ?? 'Failed to update password'; 35 | toast.error(errorMessage, { 36 | id: toastRef.current, 37 | }); 38 | toastRef.current = undefined; 39 | }, 40 | }); 41 | 42 | return ( 43 |
44 |
45 | 46 | 47 | Reset Password 48 | 49 | Enter your email to receive a Magic Link to reset your password. 50 | 51 | 52 | 53 | execute({ password })} 56 | label="Create your new Password" 57 | buttonLabel="Confirm Password" 58 | /> 59 | 60 | 61 |
62 |
63 | ); 64 | } 65 | -------------------------------------------------------------------------------- /src/app/(dynamic-pages)/(login-pages)/(login-pages)/update-password/page.tsx: -------------------------------------------------------------------------------- 1 | import { getCachedLoggedInVerifiedSupabaseUser } from '@/rsc-data/supabase'; 2 | import { UpdatePassword } from './UpdatePassword'; 3 | 4 | export default async function UpdatePasswordPage() { 5 | await getCachedLoggedInVerifiedSupabaseUser(); 6 | return ; 7 | } 8 | -------------------------------------------------------------------------------- /src/app/(dynamic-pages)/(main-pages)/(logged-in-pages)/dashboard/new/page.tsx: -------------------------------------------------------------------------------- 1 | import { T } from '@/components/ui/Typography'; 2 | import { CreatePrivateItemForm } from '../ClientPage'; 3 | 4 | export default function NewPrivateItemPage() { 5 | return ( 6 |
7 |
8 | Create New Private Item 9 | Add a new private item that only you can access 10 |
11 | 12 |
13 | ); 14 | } 15 | -------------------------------------------------------------------------------- /src/app/(dynamic-pages)/(main-pages)/(logged-in-pages)/dashboard/page.tsx: -------------------------------------------------------------------------------- 1 | import { Button } from '@/components/ui/button'; 2 | import { Card, CardContent, CardHeader } from '@/components/ui/card'; 3 | import { Skeleton } from '@/components/ui/skeleton'; 4 | import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; 5 | import { T } from '@/components/ui/Typography'; 6 | import { getAllItems } from '@/data/anon/items'; 7 | import { getAllPrivateItems } from '@/data/anon/privateItems'; 8 | import { PlusCircle } from 'lucide-react'; 9 | import Link from 'next/link'; 10 | import { Suspense } from 'react'; 11 | import { ItemsList } from '../../ItemsList'; 12 | import { PrivateItemsList } from '../../PrivateItemsList'; 13 | 14 | export const dynamic = 'force-dynamic'; 15 | 16 | async function ItemsListContainer() { 17 | const items = await getAllItems(); 18 | return ; 19 | } 20 | 21 | async function PrivateItemsListContainer() { 22 | const privateItems = await getAllPrivateItems(); 23 | return ; 24 | } 25 | 26 | function ListSkeleton() { 27 | return ( 28 |
29 |
30 |
31 | 32 | 33 |
34 | 35 |
36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | {Array(3) 44 | .fill(0) 45 | .map((_, i) => ( 46 | 47 | ))} 48 | 49 | 50 |
51 | ); 52 | } 53 | 54 | export default function DashboardPage() { 55 | return ( 56 |
57 |
58 | Dashboard 59 | 60 | 63 | 64 |
65 | 66 | 67 | 68 | Private Items 69 | Public Items 70 | 71 | 72 | 73 | }> 74 | 75 | 76 | 77 | 78 | 79 | }> 80 | 81 | 82 | 83 | 84 |
85 | ); 86 | } 87 | -------------------------------------------------------------------------------- /src/app/(dynamic-pages)/(main-pages)/(logged-in-pages)/layout.tsx: -------------------------------------------------------------------------------- 1 | import { Avatar, AvatarFallback } from '@/components/ui/avatar'; 2 | import { Button } from '@/components/ui/button'; 3 | import { getCachedLoggedInVerifiedSupabaseUser } from '@/rsc-data/supabase'; 4 | import { Home, List, Lock, PlusCircle, User } from 'lucide-react'; 5 | import Link from 'next/link'; 6 | import { redirect } from 'next/navigation'; 7 | import { ReactNode } from 'react'; 8 | 9 | export default async function Layout({ children }: { children: ReactNode }) { 10 | try { 11 | await getCachedLoggedInVerifiedSupabaseUser(); 12 | } catch (error) { 13 | redirect('/login'); 14 | } 15 | 16 | return ( 17 |
18 |
19 |
20 | 24 | 25 | Nextbase 26 | 27 | 28 | 51 | 52 |
53 | 59 | 60 | 61 | 62 | 63 | 64 | 65 |
66 |
67 |
68 | 69 |
{children}
70 |
71 | ); 72 | } 73 | -------------------------------------------------------------------------------- /src/app/(dynamic-pages)/(main-pages)/(logged-in-pages)/private-item/[privateItemId]/ConfirmDeleteItemDialog.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { Trash } from 'lucide-react'; 4 | import { useAction } from 'next-safe-action/hooks'; 5 | import { useRouter } from 'next/navigation'; 6 | import { useRef, useState, type JSX } from 'react'; 7 | import { toast } from 'sonner'; 8 | 9 | import { 10 | AlertDialog, 11 | AlertDialogAction, 12 | AlertDialogCancel, 13 | AlertDialogContent, 14 | AlertDialogDescription, 15 | AlertDialogFooter, 16 | AlertDialogHeader, 17 | AlertDialogTitle, 18 | } from '@/components/ui/alert-dialog'; 19 | import { Button } from '@/components/ui/button'; 20 | import { deletePrivateItemAction } from '@/data/user/privateItems'; 21 | import { AlertTriangle } from 'lucide-react'; 22 | 23 | type Props = { 24 | itemId: string; 25 | }; 26 | 27 | export const ConfirmDeleteItemDialog = ({ itemId }: Props): JSX.Element => { 28 | const [open, setOpen] = useState(false); 29 | const [showAlert, setShowAlert] = useState(false); 30 | const toastRef = useRef(undefined); 31 | const router = useRouter(); 32 | 33 | const { execute, status } = useAction(deletePrivateItemAction, { 34 | onExecute: () => { 35 | toastRef.current = toast.loading('Deleting item...'); 36 | }, 37 | onSuccess: () => { 38 | toast.success('Item deleted', { id: toastRef.current }); 39 | toastRef.current = undefined; 40 | router.refresh(); 41 | router.push('/'); 42 | setOpen(false); 43 | }, 44 | onError: ({ error }) => { 45 | const errorMessage = error.serverError ?? 'Failed to delete item'; 46 | toast.error(errorMessage, { id: toastRef.current }); 47 | toastRef.current = undefined; 48 | }, 49 | }); 50 | 51 | const handleDelete = () => { 52 | execute({ id: itemId }); 53 | }; 54 | 55 | return ( 56 | <> 57 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | Confirm Deletion 72 | 73 | 74 | This action cannot be undone. This will permanently delete the 75 | item and remove it from our servers. 76 | 77 | 78 | 79 | 80 | Cancel 81 | 82 | 87 | {status === 'executing' ? 'Deleting...' : 'Delete'} 88 | 89 | 90 | 91 | 92 | 93 | ); 94 | }; 95 | -------------------------------------------------------------------------------- /src/app/(dynamic-pages)/(main-pages)/(logged-in-pages)/private-item/[privateItemId]/page.tsx: -------------------------------------------------------------------------------- 1 | import { Button } from '@/components/ui/button'; 2 | import { 3 | Card, 4 | CardContent, 5 | CardFooter, 6 | CardHeader, 7 | } from '@/components/ui/card'; 8 | import { Separator } from '@/components/ui/separator'; 9 | import { Skeleton } from '@/components/ui/skeleton'; 10 | import { T } from '@/components/ui/Typography'; 11 | import { getPrivateItem } from '@/data/anon/privateItems'; 12 | import { ArrowLeft, Calendar, Info } from 'lucide-react'; 13 | import Link from 'next/link'; 14 | import { notFound } from 'next/navigation'; 15 | import { Suspense } from 'react'; 16 | import { ConfirmDeleteItemDialog } from './ConfirmDeleteItemDialog'; 17 | 18 | async function PrivateItem({ privateItemId }: { privateItemId: string }) { 19 | const item = await getPrivateItem(privateItemId); 20 | 21 | return ( 22 | 23 | 24 |
25 | 29 | Back to dashboard 30 | 31 |
32 | {item.name} 33 |
34 | 35 | Private Item 36 |
37 |
38 | 39 | 40 |
41 |
42 | Description 43 | {item.description} 44 |
45 | 46 | {item.created_at && ( 47 |
48 | 49 | 50 | Created on {new Date(item.created_at).toLocaleDateString()} 51 | 52 |
53 | )} 54 |
55 |
56 | 57 | 60 | 61 | 62 |
63 | ); 64 | } 65 | 66 | // Loading skeleton component 67 | function PrivateItemSkeleton() { 68 | return ( 69 | 70 | 71 |
72 | 73 |
74 | 75 | 76 |
77 | 78 | 79 |
80 |
81 | 82 | 83 |
84 | 85 |
86 |
87 | 88 | 89 | 90 | 91 |
92 | ); 93 | } 94 | 95 | export default async function PrivateItemPage(props: { 96 | params: Promise<{ 97 | privateItemId: string; 98 | }>; 99 | }) { 100 | const params = await props.params; 101 | const { privateItemId } = params; 102 | 103 | try { 104 | return ( 105 |
106 | }> 107 | 108 | 109 |
110 | ); 111 | } catch (error) { 112 | return notFound(); 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /src/app/(dynamic-pages)/(main-pages)/(logged-in-pages)/private-item/not-found.tsx: -------------------------------------------------------------------------------- 1 | import { Button } from '@/components/ui/button'; 2 | import { 3 | Card, 4 | CardContent, 5 | CardFooter, 6 | CardHeader, 7 | } from '@/components/ui/card'; 8 | import { T } from '@/components/ui/Typography'; 9 | import { FileQuestion } from 'lucide-react'; 10 | import Link from 'next/link'; 11 | 12 | export default function NotFound() { 13 | return ( 14 |
15 | 16 | 17 | 18 | Item Not Found 19 | 20 | 21 | 22 | The requested item could not be found. It may have been deleted or 23 | you might not have permission to view it. 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 |
33 | ); 34 | } 35 | -------------------------------------------------------------------------------- /src/app/(dynamic-pages)/(main-pages)/(logged-in-pages)/private-items/page.tsx: -------------------------------------------------------------------------------- 1 | import { Badge } from '@/components/ui/badge'; 2 | import { Button } from '@/components/ui/button'; 3 | import { Card, CardContent, CardHeader } from '@/components/ui/card'; 4 | import { Skeleton } from '@/components/ui/skeleton'; 5 | import { T } from '@/components/ui/Typography'; 6 | import { getAllPrivateItems } from '@/data/anon/privateItems'; 7 | import { Lock, PlusCircle } from 'lucide-react'; 8 | import Link from 'next/link'; 9 | import { Suspense } from 'react'; 10 | import { PrivateItemsList } from '../../PrivateItemsList'; 11 | 12 | export const dynamic = 'force-dynamic'; 13 | 14 | function PrivateItemsListSkeleton() { 15 | return ( 16 |
17 |
18 |
19 | 20 | 21 |
22 | 23 |
24 | 25 | 26 | 27 | 28 | 29 | 30 | {Array(3) 31 | .fill(0) 32 | .map((_, i) => ( 33 | 34 | ))} 35 | 36 | 37 |
38 | ); 39 | } 40 | 41 | async function PrivateItemsListWrapper() { 42 | const privateItems = await getAllPrivateItems(); 43 | return ; 44 | } 45 | 46 | export default function PrivateItemsPage() { 47 | return ( 48 |
49 |
50 | Private Items 51 | 52 | Secure 53 | 54 |
55 | 56 | Browse your private items that only you can access 57 | 58 | 59 |
60 | 61 | 64 | 65 |
66 | 67 | }> 68 | 69 | 70 |
71 | ); 72 | } 73 | -------------------------------------------------------------------------------- /src/app/(dynamic-pages)/(main-pages)/ItemsList.tsx: -------------------------------------------------------------------------------- 1 | import { T } from '@/components/ui/Typography'; 2 | import { Button } from '@/components/ui/button'; 3 | import { 4 | Card, 5 | CardContent, 6 | CardDescription, 7 | CardHeader, 8 | CardTitle, 9 | } from '@/components/ui/card'; 10 | import { 11 | Table, 12 | TableBody, 13 | TableCell, 14 | TableHead, 15 | TableHeader, 16 | TableRow, 17 | } from '@/components/ui/table'; 18 | import { Table as TableType } from '@/types'; 19 | import { Clock, ExternalLink, PlusCircle } from 'lucide-react'; 20 | import Link from 'next/link'; 21 | 22 | interface ItemsListProps { 23 | items: TableType<'items'>[]; 24 | showActions?: boolean; 25 | } 26 | 27 | export const ItemsList = ({ items, showActions = true }: ItemsListProps) => { 28 | return ( 29 |
30 | {showActions && ( 31 |
32 |
33 | Public Items 34 | 35 | This is an open database. Items are automatically cleared every 24 36 | hours. 37 | 38 |
39 | 40 | 43 | 44 |
45 | )} 46 | 47 | {items.length ? ( 48 | 49 | 50 | 51 | 52 | Name 53 | 54 | Description 55 | 56 | Actions 57 | 58 | 59 | 60 | {items.map((item) => ( 61 | 62 | 63 | {item.name} 64 | {item.created_at && ( 65 |
66 | 67 | 68 | {new Date(item.created_at).toLocaleDateString()} 69 | 70 |
71 | )} 72 |
73 | 74 | {item.description.length > 100 75 | ? `${item.description.slice(0, 100)}...` 76 | : item.description} 77 | 78 | 79 | 80 | 87 | 88 | 89 |
90 | ))} 91 |
92 |
93 |
94 | ) : ( 95 | 96 | 97 | No Items Available 98 | 99 | There are no items in the database. Create your first item! 100 | 101 | 102 | 103 | 104 | 107 | 108 | 109 | 110 | )} 111 |
112 | ); 113 | }; 114 | -------------------------------------------------------------------------------- /src/app/(dynamic-pages)/(main-pages)/PrivateItemsList.tsx: -------------------------------------------------------------------------------- 1 | import { T } from '@/components/ui/Typography'; 2 | import { Badge } from '@/components/ui/badge'; 3 | import { Button } from '@/components/ui/button'; 4 | import { 5 | Card, 6 | CardContent, 7 | CardDescription, 8 | CardHeader, 9 | CardTitle, 10 | } from '@/components/ui/card'; 11 | import { 12 | Table, 13 | TableBody, 14 | TableCell, 15 | TableHead, 16 | TableHeader, 17 | TableRow, 18 | } from '@/components/ui/table'; 19 | import { Table as TableType } from '@/types'; 20 | import { Clock, ExternalLink, Lock, PlusCircle } from 'lucide-react'; 21 | import Link from 'next/link'; 22 | 23 | interface PrivateItemsListProps { 24 | privateItems: TableType<'private_items'>[]; 25 | showActions?: boolean; 26 | } 27 | 28 | export const PrivateItemsList = ({ 29 | privateItems, 30 | showActions = true, 31 | }: PrivateItemsListProps) => { 32 | return ( 33 |
34 | {showActions && ( 35 |
36 |
37 |
38 | Private Items 39 | 40 | Secure 41 | 42 |
43 | These items are only visible to logged in users 44 |
45 | 46 | 49 | 50 |
51 | )} 52 | 53 | {privateItems.length ? ( 54 | 55 | 56 | 57 | 58 | Name 59 | 60 | Description 61 | 62 | Actions 63 | 64 | 65 | 66 | {privateItems.map((item) => ( 67 | 68 | 69 | {item.name} 70 | {item.created_at && ( 71 |
72 | 73 | 74 | {new Date(item.created_at).toLocaleDateString()} 75 | 76 |
77 | )} 78 |
79 | 80 | {item.description.length > 100 81 | ? `${item.description.slice(0, 100)}...` 82 | : item.description} 83 | 84 | 85 | 86 | 93 | 94 | 95 |
96 | ))} 97 |
98 |
99 |
100 | ) : ( 101 | 102 | 103 | No Private Items Available 104 | 105 | You haven't created any private items yet. Create your first one! 106 | 107 | 108 | 109 | 110 | 114 | 115 | 116 | 117 | )} 118 |
119 | ); 120 | }; 121 | -------------------------------------------------------------------------------- /src/app/(dynamic-pages)/(main-pages)/item/[itemId]/ConfirmDeleteItemDialog.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { AlertTriangle, Trash } from 'lucide-react'; 4 | import { useAction } from 'next-safe-action/hooks'; 5 | import { useRouter } from 'next/navigation'; 6 | import { useRef, useState, type JSX } from 'react'; 7 | import { toast } from 'sonner'; 8 | 9 | import { 10 | AlertDialog, 11 | AlertDialogAction, 12 | AlertDialogCancel, 13 | AlertDialogContent, 14 | AlertDialogDescription, 15 | AlertDialogFooter, 16 | AlertDialogHeader, 17 | AlertDialogTitle, 18 | } from '@/components/ui/alert-dialog'; 19 | import { Button } from '@/components/ui/button'; 20 | import { deleteItemAction } from '@/data/anon/items'; 21 | 22 | type ConfirmDeleteItemDialogProps = { 23 | itemId: string; 24 | }; 25 | 26 | export const ConfirmDeleteItemDialog = ({ 27 | itemId, 28 | }: ConfirmDeleteItemDialogProps): JSX.Element => { 29 | const [showAlert, setShowAlert] = useState(false); 30 | const router = useRouter(); 31 | const toastRef = useRef(undefined); 32 | 33 | const { execute, status } = useAction(deleteItemAction, { 34 | onExecute: () => { 35 | toastRef.current = toast.loading('Deleting item'); 36 | }, 37 | onSuccess: () => { 38 | toast.success('Item deleted', { id: toastRef.current }); 39 | toastRef.current = undefined; 40 | router.refresh(); 41 | router.push('/'); 42 | setShowAlert(false); 43 | }, 44 | onError: ({ error }) => { 45 | const errorMessage = error.serverError ?? 'Failed to delete item'; 46 | toast.error(errorMessage, { id: toastRef.current }); 47 | toastRef.current = undefined; 48 | setShowAlert(false); 49 | }, 50 | }); 51 | 52 | const handleDelete = () => { 53 | execute({ id: itemId }); 54 | }; 55 | 56 | return ( 57 | <> 58 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | Confirm Deletion 73 | 74 | 75 | This action cannot be undone. This will permanently delete the 76 | item and remove it from our servers. 77 | 78 | 79 | 80 | 81 | Cancel 82 | 83 | 88 | {status === 'executing' ? 'Deleting...' : 'Delete'} 89 | 90 | 91 | 92 | 93 | 94 | ); 95 | }; 96 | -------------------------------------------------------------------------------- /src/app/(dynamic-pages)/(main-pages)/item/[itemId]/page.tsx: -------------------------------------------------------------------------------- 1 | import { Button } from '@/components/ui/button'; 2 | import { 3 | Card, 4 | CardContent, 5 | CardFooter, 6 | CardHeader, 7 | } from '@/components/ui/card'; 8 | import { Separator } from '@/components/ui/separator'; 9 | import { Skeleton } from '@/components/ui/skeleton'; 10 | import { T } from '@/components/ui/Typography'; 11 | import { getItem } from '@/data/anon/items'; 12 | import { ArrowLeft, Calendar, Info } from 'lucide-react'; 13 | import Link from 'next/link'; 14 | import { notFound } from 'next/navigation'; 15 | import { Suspense } from 'react'; 16 | import { ConfirmDeleteItemDialog } from './ConfirmDeleteItemDialog'; 17 | 18 | async function Item({ itemId }: { itemId: string }) { 19 | const item = await getItem(itemId); 20 | 21 | return ( 22 | 23 | 24 |
25 | 29 | Back to home 30 | 31 |
32 | {item.name} 33 |
34 | 35 | Public Item 36 |
37 |
38 | 39 | 40 |
41 |
42 | Description 43 | {item.description} 44 |
45 | 46 | {item.created_at && ( 47 |
48 | 49 | 50 | Created on {new Date(item.created_at).toLocaleDateString()} 51 | 52 |
53 | )} 54 |
55 |
56 | 57 | 60 | 61 | 62 |
63 | ); 64 | } 65 | 66 | // Loading skeleton component 67 | function ItemSkeleton() { 68 | return ( 69 | 70 | 71 |
72 | 73 |
74 | 75 | 76 |
77 | 78 | 79 |
80 |
81 | 82 | 83 |
84 | 85 |
86 |
87 | 88 | 89 | 90 | 91 |
92 | ); 93 | } 94 | 95 | export default async function ItemPage(props: { 96 | params: Promise<{ 97 | itemId: string; 98 | }>; 99 | }) { 100 | const params = await props.params; 101 | const { itemId } = params; 102 | 103 | try { 104 | return ( 105 |
106 | }> 107 | 108 | 109 |
110 | ); 111 | } catch (error) { 112 | return notFound(); 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /src/app/(dynamic-pages)/(main-pages)/item/not-found.tsx: -------------------------------------------------------------------------------- 1 | export default function NotFound() { 2 | return ( 3 |
4 |

5 | Not Found 6 |

7 |

Could not find requested item.

8 |
9 | ); 10 | } 11 | -------------------------------------------------------------------------------- /src/app/(dynamic-pages)/(main-pages)/items/page.tsx: -------------------------------------------------------------------------------- 1 | import { Card, CardContent, CardHeader } from '@/components/ui/card'; 2 | import { Skeleton } from '@/components/ui/skeleton'; 3 | import { T } from '@/components/ui/Typography'; 4 | import { getAllItems } from '@/data/anon/items'; 5 | import { Suspense } from 'react'; 6 | import { ItemsList } from '../ItemsList'; 7 | 8 | export const dynamic = 'force-dynamic'; 9 | 10 | function ItemsListSkeleton() { 11 | return ( 12 |
13 |
14 |
15 | 16 | 17 |
18 | 19 |
20 | 21 | 22 | 23 | 24 | 25 | 26 | {Array(3) 27 | .fill(0) 28 | .map((_, i) => ( 29 | 30 | ))} 31 | 32 | 33 |
34 | ); 35 | } 36 | 37 | async function ItemsListWrapper() { 38 | const items = await getAllItems(); 39 | return ; 40 | } 41 | 42 | export default function ItemsPage() { 43 | return ( 44 |
45 |
46 | Public Items 47 | Browse all publicly available items 48 |
49 | 50 | }> 51 | 52 | 53 |
54 | ); 55 | } 56 | -------------------------------------------------------------------------------- /src/app/(dynamic-pages)/(main-pages)/new/page.tsx: -------------------------------------------------------------------------------- 1 | import { ClientPage } from './ClientPage'; 2 | 3 | export default function NewItem() { 4 | return ( 5 |
6 | 7 |
8 | ); 9 | } 10 | -------------------------------------------------------------------------------- /src/app/(dynamic-pages)/DynamicLayoutProviders.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | import React, { Suspense } from 'react'; 3 | 4 | import { Toaster as SonnerToaster } from 'sonner'; 5 | import { ThemeProvider, useTheme } from 'next-themes'; 6 | import { AppProgressBar as ProgressBar } from 'next-nprogress-bar'; 7 | 8 | function CustomerToaster() { 9 | const theme = useTheme(); 10 | const currentTheme = theme.theme === 'light' ? 'light' : 'dark'; 11 | return ; 12 | } 13 | 14 | /** 15 | * This is a wrapper for the app that provides the supabase client, the router event wrapper 16 | * the react-query client, supabase listener, and the navigation progress bar. 17 | * 18 | * The listener is used to listen for changes to the user's session and update the UI accordingly. 19 | */ 20 | export function DynamicLayoutProviders({ 21 | // Layouts must accept a children prop. 22 | // This will be populated with nested layouts or pages 23 | children, 24 | }: { 25 | children: React.ReactNode; 26 | }) { 27 | return ( 28 | 29 | {children} 30 | 31 | 37 | 38 | 39 | 40 | ); 41 | } 42 | -------------------------------------------------------------------------------- /src/app/(dynamic-pages)/layout.tsx: -------------------------------------------------------------------------------- 1 | // do not cache this layout 2 | export const dynamic = 'force-dynamic'; 3 | 4 | export const metadata = { 5 | icons: { 6 | icon: '/images/logo-black-main.ico', 7 | }, 8 | title: 'Nextbase Open source', 9 | description: 'Nextbase Open source', 10 | }; 11 | 12 | export default async function RootLayout({ 13 | children, 14 | }: { 15 | children: React.ReactNode; 16 | }) { 17 | return <>{children}; 18 | } 19 | -------------------------------------------------------------------------------- /src/app/(external-pages)/about/page.tsx: -------------------------------------------------------------------------------- 1 | export default function About() { 2 | return

About

; 3 | } 4 | -------------------------------------------------------------------------------- /src/app/(external-pages)/layout.tsx: -------------------------------------------------------------------------------- 1 | export const dynamic = 'force-static'; 2 | export const revalidate = 60; 3 | 4 | export default function Layout({ children }: { children: React.ReactNode }) { 5 | return
{children}
; 6 | } 7 | -------------------------------------------------------------------------------- /src/app/Banner.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Link from 'next/link'; 3 | 4 | const Banner: React.FC = () => { 5 | return ( 6 |
7 |

8 | Checkout premium Nextbase starter templates with integrated 9 | authentication, payments and admin panel{' '} 10 | 16 | here 17 | 18 |

19 |
20 | ); 21 | }; 22 | 23 | export default Banner; 24 | -------------------------------------------------------------------------------- /src/app/ClientLayout.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; 3 | import { Toaster } from 'react-hot-toast'; 4 | // Create a client 5 | const queryClient = new QueryClient(); 6 | // This layout component can be used with React state, context and more as it is a client component. 7 | export const ClientLayout = ({ children }: { children: React.ReactNode }) => { 8 | return ( 9 |
10 |
11 | 12 | {children} 13 | 14 | 15 |
16 |
17 | ); 18 | }; 19 | -------------------------------------------------------------------------------- /src/app/LoginNavLink.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | import { NavLink } from './NavLink'; 3 | 4 | export function LoginNavLink() { 5 | return Login; 6 | } 7 | -------------------------------------------------------------------------------- /src/app/MobileNavigation.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | import { ComponentProps, useState } from 'react'; 3 | import Link from 'next/link'; 4 | import { Dialog } from '@headlessui/react'; 5 | 6 | function MenuIcon(props: ComponentProps<'svg'>) { 7 | return ( 8 | 18 | ); 19 | } 20 | 21 | function CloseIcon(props: ComponentProps<'svg'>) { 22 | return ( 23 | 33 | ); 34 | } 35 | 36 | export function MobileNavigation() { 37 | const [isOpen, setIsOpen] = useState(false); 38 | 39 | // useEffect(() => { 40 | // if (!isOpen) return 41 | 42 | // function onRouteChange() { 43 | // setIsOpen(false) 44 | // } 45 | 46 | // router.events.on('routeChangeComplete', onRouteChange) 47 | // router.events.on('routeChangeError', onRouteChange) 48 | 49 | // return () => { 50 | // router.events.off('routeChangeComplete', onRouteChange) 51 | // router.events.off('routeChangeError', onRouteChange) 52 | // } 53 | // }, [router, isOpen]) 54 | 55 | return ( 56 | <> 57 | 65 | 71 | 72 |
73 | 80 | 81 | Nextbase Logo 86 | 87 |
88 |
89 |
90 | 91 | ); 92 | } 93 | -------------------------------------------------------------------------------- /src/app/NavLink.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | import { cn } from '@/lib/utils'; 3 | import Link from 'next/link'; 4 | import { usePathname } from 'next/navigation'; 5 | import { ComponentProps } from 'react'; 6 | 7 | export function NavLink({ 8 | href, 9 | ...props 10 | }: { 11 | href: string; 12 | } & ComponentProps) { 13 | const pathname = usePathname(); 14 | const isActive = pathname === href; 15 | return ( 16 | 21 | ); 22 | } 23 | -------------------------------------------------------------------------------- /src/app/layout.tsx: -------------------------------------------------------------------------------- 1 | import Footer from '@/components/Footer'; 2 | import '@/styles/globals.css'; 3 | import { Inter, Roboto_Mono } from 'next/font/google'; 4 | import { ClientLayout } from './ClientLayout'; 5 | import { ExternalNavigation } from './Navbar'; 6 | 7 | const inter = Inter({ 8 | subsets: ['latin'], 9 | display: 'swap', 10 | variable: '--font-inter', 11 | }); 12 | 13 | const roboto_mono = Roboto_Mono({ 14 | subsets: ['latin'], 15 | display: 'swap', 16 | variable: '--font-roboto-mono', 17 | }); 18 | 19 | export const metadata = { 20 | title: 'Nextbase Open source starter', 21 | description: 'Built with Next.js, Supabase, and Tailwind CSS', 22 | }; 23 | 24 | export default async function RootLayout({ 25 | children, 26 | }: { 27 | children: React.ReactNode; 28 | }) { 29 | return ( 30 | 31 | 32 | 33 |
34 | 35 | {children} 36 |
37 |
38 | 39 | 40 | ); 41 | } 42 | -------------------------------------------------------------------------------- /src/components/Auth/Email.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | import { Button } from '@/components/Button'; 3 | import { T } from '@/components/ui/Typography'; 4 | import { Input } from '@/components/ui/input'; 5 | import { Label } from '@/components/ui/label'; 6 | import Link from 'next/link'; 7 | import { useMemo, useState } from 'react'; 8 | 9 | export const Email = ({ 10 | onSubmit, 11 | view, 12 | isLoading, 13 | successMessage, 14 | label = 'Email address', 15 | defaultValue, 16 | className, 17 | style, 18 | }: { 19 | onSubmit: (email: string) => void; 20 | view: 'sign-in' | 'sign-up' | 'update-email' | 'forgot-password'; 21 | isLoading: boolean; 22 | successMessage?: string | null | undefined; 23 | label?: string; 24 | defaultValue?: string; 25 | className?: string; 26 | style?: React.CSSProperties; 27 | }) => { 28 | const [email, setEmail] = useState(defaultValue ?? ''); 29 | 30 | const buttonLabelText = useMemo(() => { 31 | switch (view) { 32 | case 'sign-in': 33 | return 'Login with Magic Link'; 34 | case 'sign-up': 35 | return 'Sign up with Magic Link'; 36 | case 'update-email': 37 | return 'Update Email'; 38 | case 'forgot-password': 39 | return 'Reset password'; 40 | } 41 | }, [view]); 42 | 43 | return ( 44 |
{ 46 | event.preventDefault(); 47 | onSubmit(email); 48 | }} 49 | data-testid="magic-link-form" 50 | className={className} 51 | style={style} 52 | > 53 |
54 |
55 | 58 |
59 | setEmail(event.target.value)} 66 | autoComplete={'email'} 67 | placeholder="placeholder@email.com" 68 | required 69 | /> 70 |
71 |
72 |
73 | {view === 'forgot-password' ? ( 74 |
75 | 79 | Log in instead? 80 | 81 |
82 | ) : null} 83 |
84 |
85 | 88 |
89 |
90 | {successMessage ? ( 91 | 92 | {successMessage} 93 | 94 | ) : null} 95 |
96 |
97 |
98 | ); 99 | }; 100 | -------------------------------------------------------------------------------- /src/components/Auth/EmailConfirmationPendingCard.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | import { ArrowLeftIcon, Fingerprint, MailIcon } from 'lucide-react'; 3 | 4 | import type React from 'react'; 5 | 6 | import { useRouter } from 'next/navigation'; 7 | import { Button } from '../ui/button'; 8 | import { 9 | Card, 10 | CardDescription, 11 | CardFooter, 12 | CardHeader, 13 | CardTitle, 14 | } from '../ui/card'; 15 | 16 | interface IConfirmationPendingCardProps { 17 | message: string; 18 | heading: string; 19 | type: 'login' | 'sign-up' | 'reset-password'; 20 | resetSuccessMessage: React.Dispatch>; 21 | resendEmail?: () => void; 22 | } 23 | 24 | export function EmailConfirmationPendingCard({ 25 | message, 26 | heading, 27 | type, 28 | resetSuccessMessage, 29 | resendEmail, 30 | }: IConfirmationPendingCardProps) { 31 | const router = useRouter(); 32 | return ( 33 |
34 | 35 | 36 | {type === 'reset-password' ? ( 37 | 38 | ) : ( 39 | 40 | )} 41 | {heading} 42 | {message} 43 | 44 | 45 | 61 | 62 | 63 | {type === 'sign-up' && resendEmail && ( 64 |

65 | Didnt receive the email?{' '} 66 | 73 |

74 | )} 75 |
76 | ); 77 | } 78 | -------------------------------------------------------------------------------- /src/components/Auth/Password.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | import { Button } from '@/components/Button'; 3 | import { T } from '@/components/ui/Typography'; 4 | import { Label } from '@/components/ui/label'; 5 | import { classNames } from '@/utils/classNames'; 6 | import { CSSProperties, useState } from 'react'; 7 | 8 | export const Password = ({ 9 | onSubmit, 10 | isLoading, 11 | successMessage, 12 | label = 'Password', 13 | buttonLabel = 'Update', 14 | className, 15 | style, 16 | }: { 17 | onSubmit: (password: string) => void; 18 | isLoading: boolean; 19 | successMessage?: string; 20 | label?: string; 21 | buttonLabel?: string; 22 | className?: string; 23 | style?: CSSProperties; 24 | }) => { 25 | const [password, setPassword] = useState(''); 26 | 27 | return ( 28 |
{ 30 | event.preventDefault(); 31 | onSubmit(password); 32 | }} 33 | className={className} 34 | style={style} 35 | > 36 |
37 |
38 | 41 |
42 | setPassword(event.target.value)} 49 | autoComplete="email" 50 | required 51 | className="block w-full appearance-none rounded-md border bg-gray-50/10 dark:bg-gray-800/20 h-10 px-3 py-3 placeholder-muted-foreground shadow-xs focus:border-blue-500 focus:outline-hidden focus:ring-blue-500 sm:text-sm" 52 | /> 53 |
54 |
55 |
56 | {isLoading ? ( 57 | 69 | ) : ( 70 | 81 | )} 82 |
83 |
84 | {successMessage ? ( 85 | 86 | {successMessage} 87 | 88 | ) : null} 89 |
90 |
91 |
92 | ); 93 | }; 94 | -------------------------------------------------------------------------------- /src/components/Auth/RedirectingPleaseWaitCard.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | import { Settings } from 'lucide-react'; 3 | 4 | import { Card, CardDescription, CardHeader, CardTitle } from '../ui/card'; 5 | 6 | interface RedirectingPleaseWaitCardProps { 7 | message: string; 8 | heading: string; 9 | } 10 | 11 | export function RedirectingPleaseWaitCard({ 12 | message, 13 | heading, 14 | }: RedirectingPleaseWaitCardProps) { 15 | return ( 16 |
17 | 18 | 19 | 20 | {heading} 21 | {message} 22 | 23 | 24 |
25 | ); 26 | } 27 | -------------------------------------------------------------------------------- /src/components/Auth/RenderProviders.tsx: -------------------------------------------------------------------------------- 1 | import * as SocialIcons from '@/components/Auth/Icons'; 2 | import { T } from '@/components/ui/Typography'; 3 | import { Button } from '@/components/ui/button'; 4 | import { 5 | HoverCard, 6 | HoverCardContent, 7 | HoverCardTrigger, 8 | } from '@/components/ui/hover-card'; 9 | import { AuthProvider } from '@/types'; 10 | import { Fragment } from 'react'; 11 | 12 | function capitalize(word: string) { 13 | const lower = word.toLowerCase(); 14 | return word.charAt(0).toUpperCase() + lower.slice(1); 15 | } 16 | 17 | const isDemo = true; 18 | 19 | export const RenderProviders = ({ 20 | providers, 21 | onProviderLoginRequested, 22 | isLoading, 23 | }: { 24 | providers: AuthProvider[]; 25 | onProviderLoginRequested: (provider: AuthProvider) => void; 26 | isLoading: boolean; 27 | }) => { 28 | return ( 29 |
30 | {providers.map((provider) => { 31 | const AuthIcon = SocialIcons[provider]; 32 | const component = ( 33 | 45 | ); 46 | return ( 47 | 48 | {isDemo ? ( 49 | 50 | 51 |
{component}
52 |
53 | 54 | 55 | ⚠️ As this is a demo, the social media authentication 56 | buttons aren't linked. However, you can connect them in your 57 | dev environment using the supabase dashboard for your 58 | project. 59 | 60 | 61 |
62 | ) : ( 63 | component 64 | )} 65 |
66 | ); 67 | })} 68 |
69 | ); 70 | }; 71 | -------------------------------------------------------------------------------- /src/components/Button/Button.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | import { Button as TailwindButton } from '@/components/ui/button'; 3 | import { ComponentPropsWithRef } from 'react'; 4 | 5 | export function Button({ 6 | className: classNameProp, 7 | disabled: disabledProp, 8 | ...props 9 | }: ComponentPropsWithRef) { 10 | const disabled = disabledProp; 11 | const className = classNameProp; 12 | 13 | const buttonElement = ( 14 | 19 | ); 20 | 21 | return buttonElement; 22 | } 23 | -------------------------------------------------------------------------------- /src/components/Button/index.ts: -------------------------------------------------------------------------------- 1 | export { Button } from './Button'; 2 | -------------------------------------------------------------------------------- /src/components/ui/Typography/Blockquote.tsx: -------------------------------------------------------------------------------- 1 | import { cn } from '@/lib/utils'; 2 | import { ComponentProps } from 'react'; 3 | 4 | export function Blockquote({ 5 | className, 6 | ...rest 7 | }: ComponentProps<'blockquote'>) { 8 | const classNames = cn( 9 | 'mt-6 border-l-2 border-slate-300 pl-6 italic text-slate-800 dark:border-slate-600 dark:text-slate-200', 10 | className 11 | ); 12 | return
; 13 | } 14 | -------------------------------------------------------------------------------- /src/components/ui/Typography/H1.tsx: -------------------------------------------------------------------------------- 1 | import { cn } from '@/lib/utils'; 2 | import { ComponentProps } from 'react'; 3 | 4 | export function H1({ className, ...rest }: ComponentProps<'h1'>) { 5 | const classNames = cn( 6 | 'scroll-m-20 text-4xl font-extrabold tracking-tight lg:text-5xl', 7 | className 8 | ); 9 | return

; 10 | } 11 | -------------------------------------------------------------------------------- /src/components/ui/Typography/H2.tsx: -------------------------------------------------------------------------------- 1 | import { cn } from '@/lib/utils'; 2 | import { ComponentProps } from 'react'; 3 | 4 | export function H2({ className, ...rest }: ComponentProps<'h2'>) { 5 | const classNames = cn( 6 | 'mt-10 scroll-m-20 pb-2 text-3xl font-semibold tracking-tight transition-colors first:mt-0 dark:border-b-neutral-700', 7 | className 8 | ); 9 | return

; 10 | } 11 | -------------------------------------------------------------------------------- /src/components/ui/Typography/H3.tsx: -------------------------------------------------------------------------------- 1 | import { cn } from '@/lib/utils'; 2 | import { ComponentProps } from 'react'; 3 | 4 | export function H3({ className, ...rest }: ComponentProps<'h3'>) { 5 | const classNames = cn( 6 | 'mt-8 scroll-m-20 text-2xl font-semibold tracking-tight', 7 | className 8 | ); 9 | return

; 10 | } 11 | -------------------------------------------------------------------------------- /src/components/ui/Typography/H4.tsx: -------------------------------------------------------------------------------- 1 | import { cn } from '@/lib/utils'; 2 | import { ComponentProps } from 'react'; 3 | 4 | export function H4({ className, ...rest }: ComponentProps<'h4'>) { 5 | const classNames = cn( 6 | 'mt-8 scroll-m-20 text-xl font-semibold tracking-tight', 7 | className 8 | ); 9 | return

; 10 | } 11 | -------------------------------------------------------------------------------- /src/components/ui/Typography/Large.tsx: -------------------------------------------------------------------------------- 1 | import { cn } from '@/lib/utils'; 2 | import { ComponentProps } from 'react'; 3 | 4 | export function Large({ className, ...rest }: ComponentProps<'div'>) { 5 | const classNames = cn('text-lg font-semibold text-foreground', className); 6 | return
; 7 | } 8 | -------------------------------------------------------------------------------- /src/components/ui/Typography/List.tsx: -------------------------------------------------------------------------------- 1 | import { cn } from '@/lib/utils'; 2 | import { ComponentProps } from 'react'; 3 | 4 | export function List({ className, ...rest }: ComponentProps<'ul'>) { 5 | const classNames = cn('my-6 ml-6 list-disc [&>li]:mt-2', className); 6 | return
    ; 7 | } 8 | -------------------------------------------------------------------------------- /src/components/ui/Typography/P.tsx: -------------------------------------------------------------------------------- 1 | import { cn } from '@/lib/utils'; 2 | import { ComponentPropsWithoutRef } from 'react'; 3 | import { Slot } from '@radix-ui/react-slot'; 4 | 5 | export function P({ 6 | className, 7 | asChild, 8 | ...rest 9 | }: ComponentPropsWithoutRef<'p'> & { 10 | asChild?: boolean; 11 | }) { 12 | const classNames = cn('leading-7', '&:not(:first-child):mt-6', className); 13 | const Component = asChild ? Slot : 'p'; 14 | return ; 15 | } 16 | -------------------------------------------------------------------------------- /src/components/ui/Typography/Small.tsx: -------------------------------------------------------------------------------- 1 | import { cn } from '@/lib/utils'; 2 | import { ComponentProps } from 'react'; 3 | 4 | export function Small({ className, ...rest }: ComponentProps<'small'>) { 5 | const classNames = cn('text-sm font-medium leading-none', className); 6 | return ; 7 | } 8 | -------------------------------------------------------------------------------- /src/components/ui/Typography/Subtle.tsx: -------------------------------------------------------------------------------- 1 | import { cn } from '@/lib/utils'; 2 | import { ComponentProps } from 'react'; 3 | 4 | export function Subtle({ className, ...rest }: ComponentProps<'p'>) { 5 | const classNames = cn('text-sm text-muted-foreground', className); 6 | return

    ; 7 | } 8 | -------------------------------------------------------------------------------- /src/components/ui/Typography/index.ts: -------------------------------------------------------------------------------- 1 | import { Blockquote } from './Blockquote'; 2 | import { H1 } from './H1'; 3 | import { H2 } from './H2'; 4 | import { H3 } from './H3'; 5 | import { H4 } from './H4'; 6 | import { Large } from './Large'; 7 | import { List } from './List'; 8 | import { P } from './P'; 9 | import { Small } from './Small'; 10 | import { Subtle } from './Subtle'; 11 | 12 | const Typography = { 13 | H1, 14 | H2, 15 | H3, 16 | H4, 17 | Blockquote, 18 | P, 19 | Large, 20 | Small, 21 | Subtle, 22 | List, 23 | }; 24 | export { Blockquote, H1, H2, H3, H4, Large, List, P, Small, Subtle }; 25 | 26 | export { Typography as T, Typography }; 27 | -------------------------------------------------------------------------------- /src/components/ui/accordion.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as AccordionPrimitive from "@radix-ui/react-accordion" 5 | import { ChevronDownIcon } from "lucide-react" 6 | 7 | import { cn } from "@/lib/utils" 8 | 9 | function Accordion({ 10 | ...props 11 | }: React.ComponentProps) { 12 | return 13 | } 14 | 15 | function AccordionItem({ 16 | className, 17 | ...props 18 | }: React.ComponentProps) { 19 | return ( 20 | 25 | ) 26 | } 27 | 28 | function AccordionTrigger({ 29 | className, 30 | children, 31 | ...props 32 | }: React.ComponentProps) { 33 | return ( 34 | 35 | svg]:rotate-180", 39 | className 40 | )} 41 | {...props} 42 | > 43 | {children} 44 | 45 | 46 | 47 | ) 48 | } 49 | 50 | function AccordionContent({ 51 | className, 52 | children, 53 | ...props 54 | }: React.ComponentProps) { 55 | return ( 56 | 61 |
    {children}
    62 |
    63 | ) 64 | } 65 | 66 | export { Accordion, AccordionItem, AccordionTrigger, AccordionContent } 67 | -------------------------------------------------------------------------------- /src/components/ui/alert-dialog.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog" 5 | 6 | import { cn } from "@/lib/utils" 7 | import { buttonVariants } from "@/components/ui/button" 8 | 9 | function AlertDialog({ 10 | ...props 11 | }: React.ComponentProps) { 12 | return 13 | } 14 | 15 | function AlertDialogTrigger({ 16 | ...props 17 | }: React.ComponentProps) { 18 | return ( 19 | 20 | ) 21 | } 22 | 23 | function AlertDialogPortal({ 24 | ...props 25 | }: React.ComponentProps) { 26 | return ( 27 | 28 | ) 29 | } 30 | 31 | function AlertDialogOverlay({ 32 | className, 33 | ...props 34 | }: React.ComponentProps) { 35 | return ( 36 | 44 | ) 45 | } 46 | 47 | function AlertDialogContent({ 48 | className, 49 | ...props 50 | }: React.ComponentProps) { 51 | return ( 52 | 53 | 54 | 62 | 63 | ) 64 | } 65 | 66 | function AlertDialogHeader({ 67 | className, 68 | ...props 69 | }: React.ComponentProps<"div">) { 70 | return ( 71 |
    76 | ) 77 | } 78 | 79 | function AlertDialogFooter({ 80 | className, 81 | ...props 82 | }: React.ComponentProps<"div">) { 83 | return ( 84 |
    92 | ) 93 | } 94 | 95 | function AlertDialogTitle({ 96 | className, 97 | ...props 98 | }: React.ComponentProps) { 99 | return ( 100 | 105 | ) 106 | } 107 | 108 | function AlertDialogDescription({ 109 | className, 110 | ...props 111 | }: React.ComponentProps) { 112 | return ( 113 | 118 | ) 119 | } 120 | 121 | function AlertDialogAction({ 122 | className, 123 | ...props 124 | }: React.ComponentProps) { 125 | return ( 126 | 130 | ) 131 | } 132 | 133 | function AlertDialogCancel({ 134 | className, 135 | ...props 136 | }: React.ComponentProps) { 137 | return ( 138 | 142 | ) 143 | } 144 | 145 | export { 146 | AlertDialog, 147 | AlertDialogPortal, 148 | AlertDialogOverlay, 149 | AlertDialogTrigger, 150 | AlertDialogContent, 151 | AlertDialogHeader, 152 | AlertDialogFooter, 153 | AlertDialogTitle, 154 | AlertDialogDescription, 155 | AlertDialogAction, 156 | AlertDialogCancel, 157 | } 158 | -------------------------------------------------------------------------------- /src/components/ui/alert.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import { cva, type VariantProps } from "class-variance-authority" 3 | 4 | import { cn } from "@/lib/utils" 5 | 6 | const alertVariants = cva( 7 | "relative w-full rounded-lg border px-4 py-3 text-sm grid has-[>svg]:grid-cols-[calc(var(--spacing)*4)_1fr] grid-cols-[0_1fr] has-[>svg]:gap-x-3 gap-y-0.5 items-start [&>svg]:size-4 [&>svg]:translate-y-0.5 [&>svg]:text-current", 8 | { 9 | variants: { 10 | variant: { 11 | default: "bg-card text-card-foreground", 12 | destructive: 13 | "text-destructive bg-card [&>svg]:text-current *:data-[slot=alert-description]:text-destructive/90", 14 | }, 15 | }, 16 | defaultVariants: { 17 | variant: "default", 18 | }, 19 | } 20 | ) 21 | 22 | function Alert({ 23 | className, 24 | variant, 25 | ...props 26 | }: React.ComponentProps<"div"> & VariantProps) { 27 | return ( 28 |
    34 | ) 35 | } 36 | 37 | function AlertTitle({ className, ...props }: React.ComponentProps<"div">) { 38 | return ( 39 |
    47 | ) 48 | } 49 | 50 | function AlertDescription({ 51 | className, 52 | ...props 53 | }: React.ComponentProps<"div">) { 54 | return ( 55 |
    63 | ) 64 | } 65 | 66 | export { Alert, AlertTitle, AlertDescription } 67 | -------------------------------------------------------------------------------- /src/components/ui/aspect-ratio.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as AspectRatioPrimitive from "@radix-ui/react-aspect-ratio" 4 | 5 | function AspectRatio({ 6 | ...props 7 | }: React.ComponentProps) { 8 | return 9 | } 10 | 11 | export { AspectRatio } 12 | -------------------------------------------------------------------------------- /src/components/ui/avatar.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as AvatarPrimitive from "@radix-ui/react-avatar" 5 | 6 | import { cn } from "@/lib/utils" 7 | 8 | function Avatar({ 9 | className, 10 | ...props 11 | }: React.ComponentProps) { 12 | return ( 13 | 21 | ) 22 | } 23 | 24 | function AvatarImage({ 25 | className, 26 | ...props 27 | }: React.ComponentProps) { 28 | return ( 29 | 34 | ) 35 | } 36 | 37 | function AvatarFallback({ 38 | className, 39 | ...props 40 | }: React.ComponentProps) { 41 | return ( 42 | 50 | ) 51 | } 52 | 53 | export { Avatar, AvatarImage, AvatarFallback } 54 | -------------------------------------------------------------------------------- /src/components/ui/badge.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import { Slot } from "@radix-ui/react-slot" 3 | import { cva, type VariantProps } from "class-variance-authority" 4 | 5 | import { cn } from "@/lib/utils" 6 | 7 | const badgeVariants = cva( 8 | "inline-flex items-center justify-center rounded-md border px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive transition-[color,box-shadow] overflow-hidden", 9 | { 10 | variants: { 11 | variant: { 12 | default: 13 | "border-transparent bg-primary text-primary-foreground [a&]:hover:bg-primary/90", 14 | secondary: 15 | "border-transparent bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90", 16 | destructive: 17 | "border-transparent bg-destructive text-white [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60", 18 | outline: 19 | "text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground", 20 | }, 21 | }, 22 | defaultVariants: { 23 | variant: "default", 24 | }, 25 | } 26 | ) 27 | 28 | function Badge({ 29 | className, 30 | variant, 31 | asChild = false, 32 | ...props 33 | }: React.ComponentProps<"span"> & 34 | VariantProps & { asChild?: boolean }) { 35 | const Comp = asChild ? Slot : "span" 36 | 37 | return ( 38 | 43 | ) 44 | } 45 | 46 | export { Badge, badgeVariants } 47 | -------------------------------------------------------------------------------- /src/components/ui/breadcrumb.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import { Slot } from "@radix-ui/react-slot" 3 | import { ChevronRight, MoreHorizontal } from "lucide-react" 4 | 5 | import { cn } from "@/lib/utils" 6 | 7 | function Breadcrumb({ ...props }: React.ComponentProps<"nav">) { 8 | return