├── .env.example ├── .eslintrc.cjs ├── .gitignore ├── LICENSE ├── README.md ├── components.json ├── demo ├── dark.png ├── example-01-dark.png ├── example-01-light.png └── light.png ├── next.config.mjs ├── package-lock.json ├── package.json ├── postcss.config.cjs ├── prettier.config.cjs ├── prisma └── schema.prisma ├── public └── favicon.ico ├── src ├── components │ ├── common │ │ ├── alert-modal.tsx │ │ ├── heading.tsx │ │ ├── loading.tsx │ │ └── modal.tsx │ ├── constants │ │ └── side-nav.tsx │ ├── dashboard │ │ ├── date-range-picker.tsx │ │ ├── overview.tsx │ │ └── recent-sales.tsx │ ├── layout │ │ ├── header.tsx │ │ ├── index.tsx │ │ ├── mobile-sidebar.tsx │ │ ├── side-nav.tsx │ │ ├── sidebar.tsx │ │ ├── subnav-accordion.tsx │ │ ├── theme-toggle.tsx │ │ ├── user-avatar.tsx │ │ └── user-nav.tsx │ ├── page-component │ │ └── example │ │ │ ├── employee │ │ │ ├── cell-action.tsx │ │ │ ├── client.tsx │ │ │ ├── columns.tsx │ │ │ └── employee-form.tsx │ │ │ ├── example-02 │ │ │ └── multi-select-input.tsx │ │ │ └── example-03 │ │ │ ├── email-form.tsx │ │ │ ├── form-context.tsx │ │ │ ├── form-step.tsx │ │ │ ├── password-form.tsx │ │ │ └── username-form.tsx │ └── ui │ │ ├── accordion.tsx │ │ ├── alert-dialog.tsx │ │ ├── alert.tsx │ │ ├── aspect-ratio.tsx │ │ ├── avatar.tsx │ │ ├── badge.tsx │ │ ├── button.tsx │ │ ├── calendar.tsx │ │ ├── card.tsx │ │ ├── checkbox.tsx │ │ ├── collapsible.tsx │ │ ├── command.tsx │ │ ├── context-menu.tsx │ │ ├── data-table │ │ ├── data-table-column-header.tsx │ │ ├── data-table-pagination.tsx │ │ ├── data-table-toolbar.tsx │ │ ├── data-table-view-options.tsx │ │ └── data-table.tsx │ │ ├── date-picker.tsx │ │ ├── dialog.tsx │ │ ├── dropdown-menu.tsx │ │ ├── form.tsx │ │ ├── hover-card.tsx │ │ ├── input.tsx │ │ ├── label.tsx │ │ ├── menubar.tsx │ │ ├── navigation-menu.tsx │ │ ├── popover.tsx │ │ ├── progress.tsx │ │ ├── radio-group.tsx │ │ ├── scroll-area.tsx │ │ ├── select.tsx │ │ ├── separator.tsx │ │ ├── sheet.tsx │ │ ├── skeleton.tsx │ │ ├── slider.tsx │ │ ├── switch.tsx │ │ ├── table.tsx │ │ ├── tabs.tsx │ │ ├── textarea.tsx │ │ ├── toast.tsx │ │ ├── toaster.tsx │ │ ├── toggle-group.tsx │ │ ├── toggle.tsx │ │ ├── tooltip.tsx │ │ └── use-toast.ts ├── env.mjs ├── hooks │ └── useSidebar.tsx ├── lib │ ├── utils.ts │ └── validators.tsx ├── pages │ ├── _app.tsx │ ├── api │ │ ├── auth │ │ │ └── [...nextauth].ts │ │ └── trpc │ │ │ └── [trpc].ts │ ├── example │ │ ├── employees │ │ │ ├── [id] │ │ │ │ └── index.tsx │ │ │ └── index.tsx │ │ ├── example-02 │ │ │ └── index.tsx │ │ └── example-03 │ │ │ └── index.tsx │ └── index.tsx ├── providers │ └── modal-provider.tsx ├── server │ ├── api │ │ ├── root.ts │ │ ├── routers │ │ │ └── employee.ts │ │ └── trpc.ts │ ├── auth.ts │ └── db.ts ├── styles │ └── globals.css ├── types │ └── index.tsx └── utils │ └── api.ts ├── tailwind.config.ts └── tsconfig.json /.env.example: -------------------------------------------------------------------------------- 1 | # Since the ".env" file is gitignored, you can use the ".env.example" file to 2 | # build a new ".env" file when you clone the repo. Keep this file up-to-date 3 | # when you add new variables to `.env`. 4 | 5 | # This file will be committed to version control, so make sure not to have any 6 | # secrets in it. If you are cloning this repo, create a copy of this file named 7 | # ".env" and populate it with your secrets. 8 | 9 | # When adding additional environment variables, the schema in "/src/env.mjs" 10 | # should be updated accordingly. 11 | 12 | # Prisma 13 | # https://www.prisma.io/docs/reference/database-reference/connection-urls#env 14 | DATABASE_URL="file:./db.sqlite" 15 | 16 | # Next Auth 17 | # You can generate a new secret on the command line with: 18 | # openssl rand -base64 32 19 | # https://next-auth.js.org/configuration/options#secret 20 | # NEXTAUTH_SECRET="" 21 | NEXTAUTH_URL="http://localhost:3000" 22 | 23 | # Next Auth Discord Provider 24 | DISCORD_CLIENT_ID="" 25 | DISCORD_CLIENT_SECRET="" 26 | -------------------------------------------------------------------------------- /.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line @typescript-eslint/no-var-requires 2 | const path = require("path"); 3 | 4 | /** @type {import("eslint").Linter.Config} */ 5 | const config = { 6 | overrides: [ 7 | { 8 | extends: [ 9 | "plugin:@typescript-eslint/recommended-requiring-type-checking", 10 | ], 11 | files: ["*.ts", "*.tsx"], 12 | parserOptions: { 13 | project: path.join(__dirname, "tsconfig.json"), 14 | }, 15 | }, 16 | ], 17 | parser: "@typescript-eslint/parser", 18 | parserOptions: { 19 | project: path.join(__dirname, "tsconfig.json"), 20 | }, 21 | plugins: ["@typescript-eslint"], 22 | extends: ["next/core-web-vitals", "plugin:@typescript-eslint/recommended"], 23 | rules: { 24 | "@typescript-eslint/consistent-type-imports": [ 25 | "warn", 26 | { 27 | prefer: "type-imports", 28 | fixStyle: "inline-type-imports", 29 | }, 30 | ], 31 | "@typescript-eslint/no-unused-vars": ["warn", { argsIgnorePattern: "^_" }], 32 | }, 33 | }; 34 | 35 | module.exports = config; 36 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # database 12 | /prisma/db.sqlite 13 | /prisma/db.sqlite-journal 14 | 15 | # next.js 16 | /.next/ 17 | /out/ 18 | next-env.d.ts 19 | 20 | # production 21 | /build 22 | 23 | # misc 24 | .DS_Store 25 | *.pem 26 | 27 | # debug 28 | npm-debug.log* 29 | yarn-debug.log* 30 | yarn-error.log* 31 | .pnpm-debug.log* 32 | 33 | # local env files 34 | # do not commit any .env files to git, except for the .env.example file. https://create.t3.gg/en/usage/env-variables#using-environment-variables 35 | .env 36 | .env*.local 37 | 38 | # vercel 39 | .vercel 40 | 41 | # typescript 42 | *.tsbuildinfo 43 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 t3-app-template 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 | # [T3 App Template]: T3 Stack with shadcn-ui Admin Layout 2 | 3 | ## Overview 4 | 5 | This project is a management interface developed using the [T3 Stack](https://create.t3.gg/), encapsulated with [shadcn-ui](https://ui.shadcn.com/) 6 | . It aims to provide a responsive, user-friendly, and feature-rich admin interface suitable for various devices and screen sizes. 7 | 8 | ## Features 9 | 10 | - Theme Switching: Allows users to switch the interface theme according to their preference or requirement. 11 | - Responsive Sidebar: A flexible sidebar that adapts to different screen sizes and devices. 12 | - Sidebar Shrinking: Users can expand or collapse the sidebar as needed to optimize screen space. 13 | - Secondary Menu in Sidebar: For better organization and access, the sidebar includes multi-level menus. 14 | - Mobile Sidebar: Specially designed for mobile devices to ensure smooth usage on smaller screens. 15 | - Tanstack Table: Use tanstack table to implement curd operation 16 | - NextAuth: Implemented Github login using NextAuth 17 | - Global: Header, Loading and Modal 18 | - Select: Implement multi-select input example 19 | - Step Form: Implement step-form example 20 | - Other Features...: Waiting for further updates 21 | 22 | ## Quick Start 23 | 24 | 1. Clone the repository: git clone https://github.com/gaofubin/t3-app-template.git 25 | 2. Install dependencies: npm install 26 | 3. Start the project: npm run dev 27 | 28 | ## Dashboard Demo 29 | 30 | > Demo Link: https://t3-app-template.vercel.app/ 31 | 32 | ![Alt text](demo/light.png) 33 | 34 | ![Alt text](demo/dark.png) 35 | 36 | ![Alt text](demo/example-01-light.png) 37 | 38 | ![Alt text](demo/example-01-dark.png) 39 | -------------------------------------------------------------------------------- /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.ts", 8 | "css": "src/styles/globals.css", 9 | "baseColor": "slate", 10 | "cssVariables": true 11 | }, 12 | "aliases": { 13 | "components": "@/components", 14 | "utils": "@/lib/utils" 15 | } 16 | } -------------------------------------------------------------------------------- /demo/dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gaofubin/t3-app-template/db31bd3c03b7eb0a099be07c261d856f3b6d4110/demo/dark.png -------------------------------------------------------------------------------- /demo/example-01-dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gaofubin/t3-app-template/db31bd3c03b7eb0a099be07c261d856f3b6d4110/demo/example-01-dark.png -------------------------------------------------------------------------------- /demo/example-01-light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gaofubin/t3-app-template/db31bd3c03b7eb0a099be07c261d856f3b6d4110/demo/example-01-light.png -------------------------------------------------------------------------------- /demo/light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gaofubin/t3-app-template/db31bd3c03b7eb0a099be07c261d856f3b6d4110/demo/light.png -------------------------------------------------------------------------------- /next.config.mjs: -------------------------------------------------------------------------------- 1 | /** 2 | * Run `build` or `dev` with `SKIP_ENV_VALIDATION` to skip env validation. This is especially useful 3 | * for Docker builds. 4 | */ 5 | await import("./src/env.mjs"); 6 | 7 | /** @type {import("next").NextConfig} */ 8 | const config = { 9 | reactStrictMode: true, 10 | 11 | /** 12 | * If you have `experimental: { appDir: true }` set, then you must comment the below `i18n` config 13 | * out. 14 | * 15 | * @see https://github.com/vercel/next.js/issues/41980 16 | */ 17 | i18n: { 18 | locales: ["en"], 19 | defaultLocale: "en", 20 | }, 21 | }; 22 | export default config; 23 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "t3-app-template", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "build": "next build", 7 | "dev": "next dev", 8 | "postinstall": "prisma generate", 9 | "lint": "next lint", 10 | "start": "next start" 11 | }, 12 | "dependencies": { 13 | "@clerk/nextjs": "^4.29.3", 14 | "@hookform/resolvers": "^3.3.2", 15 | "@next-auth/prisma-adapter": "^1.0.5", 16 | "@prisma/client": "^4.11.0", 17 | "@radix-ui/react-accordion": "^1.1.2", 18 | "@radix-ui/react-alert-dialog": "^1.0.5", 19 | "@radix-ui/react-aspect-ratio": "^1.0.3", 20 | "@radix-ui/react-avatar": "^1.0.4", 21 | "@radix-ui/react-checkbox": "^1.0.4", 22 | "@radix-ui/react-collapsible": "^1.0.3", 23 | "@radix-ui/react-context-menu": "^2.1.5", 24 | "@radix-ui/react-dialog": "^1.0.5", 25 | "@radix-ui/react-dropdown-menu": "^2.0.6", 26 | "@radix-ui/react-hover-card": "^1.0.7", 27 | "@radix-ui/react-icons": "^1.3.0", 28 | "@radix-ui/react-label": "^2.0.2", 29 | "@radix-ui/react-menubar": "^1.0.4", 30 | "@radix-ui/react-navigation-menu": "^1.1.4", 31 | "@radix-ui/react-popover": "^1.0.7", 32 | "@radix-ui/react-progress": "^1.0.3", 33 | "@radix-ui/react-radio-group": "^1.1.3", 34 | "@radix-ui/react-scroll-area": "^1.0.5", 35 | "@radix-ui/react-select": "^2.0.0", 36 | "@radix-ui/react-separator": "^1.0.3", 37 | "@radix-ui/react-slider": "^1.1.2", 38 | "@radix-ui/react-slot": "^1.0.2", 39 | "@radix-ui/react-switch": "^1.0.3", 40 | "@radix-ui/react-tabs": "^1.0.4", 41 | "@radix-ui/react-toast": "^1.1.5", 42 | "@radix-ui/react-toggle": "^1.0.3", 43 | "@radix-ui/react-toggle-group": "^1.0.4", 44 | "@radix-ui/react-tooltip": "^1.0.7", 45 | "@tanstack/react-query": "^4.28.0", 46 | "@tanstack/react-table": "^8.11.6", 47 | "@trpc/client": "^10.18.0", 48 | "@trpc/next": "^10.18.0", 49 | "@trpc/react-query": "^10.18.0", 50 | "@trpc/server": "^10.18.0", 51 | "class-variance-authority": "^0.7.0", 52 | "clsx": "^2.0.0", 53 | "cmdk": "^0.2.0", 54 | "date-fns": "^2.30.0", 55 | "lucide-react": "^0.294.0", 56 | "next": "^13.2.4", 57 | "next-auth": "^4.21.0", 58 | "next-themes": "^0.2.1", 59 | "react": "18.2.0", 60 | "react-day-picker": "^8.9.1", 61 | "react-dom": "18.2.0", 62 | "react-hook-form": "^7.48.2", 63 | "react-hot-toast": "^2.4.1", 64 | "react-icons": "^5.0.1", 65 | "react-spinners": "^0.13.8", 66 | "recharts": "^2.10.3", 67 | "superjson": "1.12.2", 68 | "tailwind-merge": "^2.1.0", 69 | "tailwindcss-animate": "^1.0.7", 70 | "zod": "^3.22.4", 71 | "zustand": "^4.4.7" 72 | }, 73 | "devDependencies": { 74 | "@types/eslint": "^8.21.3", 75 | "@types/jest": "^29.5.12", 76 | "@types/mocha": "^10.0.6", 77 | "@types/node": "^18.15.5", 78 | "@types/prettier": "^2.7.2", 79 | "@types/react": "^18.0.28", 80 | "@types/react-dom": "^18.0.11", 81 | "@typescript-eslint/eslint-plugin": "^5.56.0", 82 | "@typescript-eslint/parser": "^5.56.0", 83 | "autoprefixer": "^10.4.14", 84 | "eslint": "^8.36.0", 85 | "eslint-config-next": "^13.2.4", 86 | "jest": "^29.7.0", 87 | "mocha": "^10.4.0", 88 | "postcss": "^8.4.21", 89 | "prettier": "^2.8.6", 90 | "prettier-plugin-tailwindcss": "^0.2.6", 91 | "prisma": "^4.11.0", 92 | "tailwindcss": "^3.3.0", 93 | "typescript": "^5.0.2" 94 | }, 95 | "ct3aMetadata": { 96 | "initVersion": "7.12.0" 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /postcss.config.cjs: -------------------------------------------------------------------------------- 1 | const config = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | }; 7 | 8 | module.exports = config; 9 | -------------------------------------------------------------------------------- /prettier.config.cjs: -------------------------------------------------------------------------------- 1 | /** @type {import("prettier").Config} */ 2 | const config = { 3 | plugins: [require.resolve("prettier-plugin-tailwindcss")], 4 | }; 5 | 6 | module.exports = config; 7 | -------------------------------------------------------------------------------- /prisma/schema.prisma: -------------------------------------------------------------------------------- 1 | // This is your Prisma schema file, 2 | // learn more about it in the docs: https://pris.ly/d/prisma-schema 3 | 4 | generator client { 5 | provider = "prisma-client-js" 6 | } 7 | 8 | datasource db { 9 | provider = "postgresql" 10 | // NOTE: When using postgresql, mysql or sqlserver, uncomment the @db.Text annotations in model Account below 11 | // Further reading: 12 | // https://next-auth.js.org/adapters/prisma#create-the-prisma-schema 13 | // https://www.prisma.io/docs/reference/api-reference/prisma-schema-reference#string 14 | url = env("DATABASE_URL") 15 | } 16 | 17 | model Example { 18 | id String @id @default(cuid()) 19 | name String 20 | createdAt DateTime @default(now()) 21 | updatedAt DateTime @updatedAt 22 | } 23 | 24 | // Necessary for Next auth 25 | model Account { 26 | id String @id @default(cuid()) 27 | userId String 28 | type String 29 | provider String 30 | providerAccountId String 31 | refresh_token String? // @db.Text 32 | access_token String? // @db.Text 33 | expires_at Int? 34 | token_type String? 35 | scope String? 36 | id_token String? // @db.Text 37 | session_state String? 38 | user User @relation(fields: [userId], references: [id], onDelete: Cascade) 39 | 40 | @@unique([provider, providerAccountId]) 41 | } 42 | 43 | model Session { 44 | id String @id @default(cuid()) 45 | sessionToken String @unique 46 | userId String 47 | expires DateTime 48 | user User @relation(fields: [userId], references: [id], onDelete: Cascade) 49 | } 50 | 51 | model User { 52 | id String @id @default(cuid()) 53 | name String? 54 | email String? @unique 55 | emailVerified DateTime? 56 | image String? 57 | accounts Account[] 58 | sessions Session[] 59 | } 60 | 61 | model VerificationToken { 62 | identifier String 63 | token String @unique 64 | expires DateTime 65 | 66 | @@unique([identifier, token]) 67 | } 68 | 69 | 70 | model Employee { 71 | id String @id @default(cuid()) 72 | firstName String @map("first_name") 73 | lastName String @map("last_name") 74 | gender String 75 | createAt DateTime @default(now()) @map("create_at") 76 | updateAt DateTime @default(now()) @updatedAt @map("update_at") 77 | } -------------------------------------------------------------------------------- /src/components/common/alert-modal.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useEffect, useState } from "react"; 4 | import { Button } from "@/components/ui/button"; 5 | import { 6 | Dialog, 7 | DialogHeader, 8 | DialogContent, 9 | DialogTitle, 10 | DialogDescription, 11 | } from "@/components/ui/dialog"; 12 | 13 | interface AlertModalProps { 14 | title: string; 15 | description: string; 16 | name?: string; 17 | loading: boolean; 18 | isOpen: boolean; 19 | onClose: () => void; 20 | onConfirm: () => void; 21 | } 22 | 23 | export const AlertModal = ({ 24 | title, 25 | description, 26 | name, 27 | isOpen, 28 | onClose, 29 | onConfirm, 30 | loading, 31 | }: AlertModalProps) => { 32 | console.log(); 33 | const [isMounted, setIsMounted] = useState(false); 34 | useEffect(() => { 35 | setIsMounted(true); 36 | }, []); 37 | 38 | if (!isMounted) { 39 | return null; 40 | } 41 | 42 | const onChange = (open: boolean) => { 43 | if (!open) { 44 | onClose(); 45 | } 46 | }; 47 | 48 | return ( 49 | 50 | 51 | 52 | {title} 53 | {description} 54 | 55 |
56 | Are you sure want to delete 57 | {name}? 58 |
59 |
60 | 63 | 66 |
67 |
68 |
69 | ); 70 | }; 71 | -------------------------------------------------------------------------------- /src/components/common/heading.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | interface HeadingProps { 4 | title: string; 5 | description: string; 6 | } 7 | 8 | export const Heading = ({ title, description }: HeadingProps) => { 9 | return ( 10 |
11 |

{title}

12 |

{description}

13 |
14 | ); 15 | }; 16 | -------------------------------------------------------------------------------- /src/components/common/loading.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import { ClipLoader } from "react-spinners" 4 | 5 | export const Loading = () => { 6 | return ( 7 |
8 | 9 |
10 | ) 11 | } -------------------------------------------------------------------------------- /src/components/common/modal.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { 4 | Dialog, 5 | DialogTitle, 6 | DialogHeader, 7 | DialogContent, 8 | DialogDescription, 9 | } from "@/components/ui/dialog"; 10 | import { useModal } from "@/providers/modal-provider"; 11 | import { 12 | Sheet, 13 | SheetContent, 14 | SheetDescription, 15 | SheetHeader, 16 | SheetTitle, 17 | } from "@/components/ui/sheet"; 18 | import { cn } from "@/lib/utils"; 19 | 20 | interface ModalProps { 21 | type: "dialog" | "sheet"; 22 | className?: string; 23 | title: string; 24 | description: string; 25 | children?: React.ReactNode; 26 | } 27 | 28 | export const Modal: React.FC = ({ 29 | type, 30 | className, 31 | title, 32 | description, 33 | children, 34 | }) => { 35 | const { isOpen, toggleModal } = useModal(); 36 | 37 | return ( 38 | <> 39 | {type === "dialog" ? ( 40 | 41 | 42 | 43 | {title} 44 | {description} 45 | 46 | {children} 47 | 48 | 49 | ) : type === "sheet" ? ( 50 | 51 | 52 | 53 | {title} 54 | {description} 55 | 56 | {children} 57 | 58 | 59 | ) : null} 60 | 61 | ); 62 | }; -------------------------------------------------------------------------------- /src/components/constants/side-nav.tsx: -------------------------------------------------------------------------------- 1 | import { BookOpenCheck, LayoutDashboard } from "lucide-react"; 2 | import { type NavItem } from "@/types"; 3 | 4 | export const NavItems: NavItem[] = [ 5 | { 6 | title: "Dashboard", 7 | icon: LayoutDashboard, 8 | href: "/", 9 | color: "text-sky-500", 10 | }, 11 | { 12 | title: "Example", 13 | icon: BookOpenCheck, 14 | href: "/example", 15 | color: "text-orange-500", 16 | isChildren: true, 17 | children: [ 18 | { 19 | title: "Example-01", 20 | icon: BookOpenCheck, 21 | color: "text-red-500", 22 | href: "/example/employees", 23 | }, 24 | { 25 | title: "Example-02", 26 | icon: BookOpenCheck, 27 | color: "text-red-500", 28 | href: "/example/example-02", 29 | }, 30 | { 31 | title: "Example-03", 32 | icon: BookOpenCheck, 33 | color: "text-red-500", 34 | href: "/example/example-03", 35 | }, 36 | ], 37 | }, 38 | ]; 39 | -------------------------------------------------------------------------------- /src/components/dashboard/date-range-picker.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import * as React from "react"; 4 | import { CalendarIcon } from "@radix-ui/react-icons"; 5 | import { addDays, format } from "date-fns"; 6 | import { type DateRange } from "react-day-picker"; 7 | 8 | import { cn } from "@/lib/utils"; 9 | import { Button } from "@/components/ui/button"; 10 | import { Calendar } from "@/components/ui/calendar"; 11 | import { 12 | Popover, 13 | PopoverContent, 14 | PopoverTrigger, 15 | } from "@/components/ui/popover"; 16 | 17 | export function CalendarDateRangePicker({ 18 | className, 19 | }: React.HTMLAttributes) { 20 | const [date, setDate] = React.useState({ 21 | from: new Date(2023, 0, 20), 22 | to: addDays(new Date(2023, 0, 20), 20), 23 | }); 24 | 25 | return ( 26 |
27 | 28 | 29 | 51 | 52 | 53 | 61 | 62 | 63 |
64 | ); 65 | } -------------------------------------------------------------------------------- /src/components/dashboard/overview.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { Bar, BarChart, ResponsiveContainer, XAxis, YAxis } from "recharts"; 4 | 5 | const data = [ 6 | { 7 | name: "Jan", 8 | total: Math.floor(Math.random() * 5000) + 1000, 9 | }, 10 | { 11 | name: "Feb", 12 | total: Math.floor(Math.random() * 5000) + 1000, 13 | }, 14 | { 15 | name: "Mar", 16 | total: Math.floor(Math.random() * 5000) + 1000, 17 | }, 18 | { 19 | name: "Apr", 20 | total: Math.floor(Math.random() * 5000) + 1000, 21 | }, 22 | { 23 | name: "May", 24 | total: Math.floor(Math.random() * 5000) + 1000, 25 | }, 26 | { 27 | name: "Jun", 28 | total: Math.floor(Math.random() * 5000) + 1000, 29 | }, 30 | { 31 | name: "Jul", 32 | total: Math.floor(Math.random() * 5000) + 1000, 33 | }, 34 | { 35 | name: "Aug", 36 | total: Math.floor(Math.random() * 5000) + 1000, 37 | }, 38 | { 39 | name: "Sep", 40 | total: Math.floor(Math.random() * 5000) + 1000, 41 | }, 42 | { 43 | name: "Oct", 44 | total: Math.floor(Math.random() * 5000) + 1000, 45 | }, 46 | { 47 | name: "Nov", 48 | total: Math.floor(Math.random() * 5000) + 1000, 49 | }, 50 | { 51 | name: "Dec", 52 | total: Math.floor(Math.random() * 5000) + 1000, 53 | }, 54 | ]; 55 | 56 | export function Overview() { 57 | return ( 58 | 59 | 60 | 67 | `$${value}`} 74 | /> 75 | 76 | 77 | 78 | ); 79 | } -------------------------------------------------------------------------------- /src/components/dashboard/recent-sales.tsx: -------------------------------------------------------------------------------- 1 | import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"; 2 | 3 | export function RecentSales() { 4 | return ( 5 |
6 |
7 | 8 | 9 | OM 10 | 11 |
12 |

Olivia Martin

13 |

14 | olivia.martin@email.com 15 |

16 |
17 |
+$1,999.00
18 |
19 |
20 | 21 | 22 | JL 23 | 24 |
25 |

Jackson Lee

26 |

jackson.lee@email.com

27 |
28 |
+$39.00
29 |
30 |
31 | 32 | 33 | IN 34 | 35 |
36 |

Isabella Nguyen

37 |

38 | isabella.nguyen@email.com 39 |

40 |
41 |
+$299.00
42 |
43 |
44 | 45 | 46 | WK 47 | 48 |
49 |

William Kim

50 |

will@email.com

51 |
52 |
+$99.00
53 |
54 |
55 | 56 | 57 | SD 58 | 59 |
60 |

Sofia Davis

61 |

sofia.davis@email.com

62 |
63 |
+$39.00
64 |
65 |
66 | ); 67 | } -------------------------------------------------------------------------------- /src/components/layout/header.tsx: -------------------------------------------------------------------------------- 1 | 2 | import { ThemeToggle } from "@/components/layout/theme-toggle"; 3 | import { cn } from "@/lib/utils"; 4 | import { MobileSidebar } from "@/components/layout/mobile-sidebar"; 5 | import Link from "next/link"; 6 | import { Boxes } from "lucide-react"; 7 | import { UserNav } from "@/components/layout/user-nav"; 8 | import { signIn, useSession } from "next-auth/react"; 9 | import { Button } from "@/components/ui/button"; 10 | 11 | export default function Header() { 12 | const { data: sessionData } = useSession(); 13 | return ( 14 |
15 | 42 |
43 | ); 44 | } -------------------------------------------------------------------------------- /src/components/layout/index.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import Header from "@/components/layout/header"; 3 | import Sidebar from "@/components/layout/sidebar"; 4 | 5 | export const Layout = ({ children }: { children: React.ReactNode }) => { 6 | return ( 7 | <> 8 |
9 |
10 | 11 |
12 | {children} 13 |
14 |
15 | 16 | ); 17 | }; -------------------------------------------------------------------------------- /src/components/layout/mobile-sidebar.tsx: -------------------------------------------------------------------------------- 1 | import { useState, useEffect } from "react"; 2 | import { MenuIcon } from "lucide-react"; 3 | import { Sheet, SheetContent, SheetTrigger } from "@/components/ui/sheet"; 4 | import { SideNav } from "@/components/layout/side-nav"; 5 | import { NavItems } from "@/components/constants/side-nav"; 6 | 7 | export const MobileSidebar = () => { 8 | const [open, setOpen] = useState(false); 9 | const [isMounted, setIsMounted] = useState(false); 10 | 11 | useEffect(() => { 12 | setIsMounted(true); 13 | }, []); 14 | 15 | if (!isMounted) { 16 | return null; 17 | } 18 | 19 | return ( 20 | <> 21 | 22 | 23 |
24 | 25 |

T3 app template

26 |
27 |
28 | 29 |
30 | 31 |
32 |
33 |
34 | 35 | ); 36 | }; -------------------------------------------------------------------------------- /src/components/layout/side-nav.tsx: -------------------------------------------------------------------------------- 1 | 2 | "use client"; 3 | import Link from "next/link"; 4 | 5 | import { type NavItem } from "@/types"; 6 | import { usePathname } from "next/navigation"; 7 | import { cn } from "@/lib/utils"; 8 | import { useSidebar } from "@/hooks/useSidebar"; 9 | import { buttonVariants } from "@/components/ui/button"; 10 | 11 | import { 12 | Accordion, 13 | AccordionContent, 14 | AccordionItem, 15 | AccordionTrigger, 16 | } from "@/components/layout/subnav-accordion"; 17 | import { useEffect, useState } from "react"; 18 | import { ChevronDownIcon } from "@radix-ui/react-icons"; 19 | 20 | interface SideNavProps { 21 | items: NavItem[]; 22 | setOpen?: (open: boolean) => void; 23 | className?: string; 24 | } 25 | 26 | export function SideNav({ items, setOpen, className }: SideNavProps) { 27 | const path = usePathname(); 28 | const { isOpen } = useSidebar(); 29 | const [openItem, setOpenItem] = useState(""); 30 | const [lastOpenItem, setLastOpenItem] = useState(""); 31 | 32 | useEffect(() => { 33 | if (isOpen) { 34 | setOpenItem(lastOpenItem); 35 | } else { 36 | setLastOpenItem(openItem); 37 | setOpenItem(""); 38 | } 39 | }, [isOpen]); 40 | 41 | return ( 42 | 131 | ); 132 | } 133 | -------------------------------------------------------------------------------- /src/components/layout/sidebar.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | import { SideNav } from "@/components/layout/side-nav"; 3 | import { NavItems } from "@/components/constants/side-nav"; 4 | 5 | import { cn } from "@/lib/utils"; 6 | import { useSidebar } from "@/hooks/useSidebar"; 7 | import { BsArrowLeftShort } from "react-icons/bs"; 8 | 9 | interface SidebarProps { 10 | className?: string; 11 | } 12 | 13 | export default function Sidebar({ className }: SidebarProps) { 14 | const { isOpen, toggle } = useSidebar(); 15 | const [status, setStatus] = useState(false); 16 | 17 | const handleToggle = () => { 18 | setStatus(true); 19 | toggle(); 20 | setTimeout(() => setStatus(false), 500); 21 | }; 22 | return ( 23 | 49 | ); 50 | } 51 | -------------------------------------------------------------------------------- /src/components/layout/subnav-accordion.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import * as React from "react"; 4 | import * as AccordionPrimitive from "@radix-ui/react-accordion"; 5 | 6 | import { cn } from "@/lib/utils"; 7 | 8 | const Accordion = AccordionPrimitive.Root; 9 | 10 | const AccordionItem = React.forwardRef< 11 | React.ElementRef, 12 | React.ComponentPropsWithoutRef 13 | >(({ className, ...props }, ref) => ( 14 | 19 | )); 20 | AccordionItem.displayName = "AccordionItem"; 21 | 22 | const AccordionTrigger = React.forwardRef< 23 | React.ElementRef, 24 | React.ComponentPropsWithoutRef 25 | >(({ className, children, ...props }, ref) => ( 26 | 27 | svg]:rotate-180", 31 | className 32 | )} 33 | {...props} 34 | > 35 | {children} 36 | 37 | 38 | )); 39 | AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName; 40 | 41 | const AccordionContent = React.forwardRef< 42 | React.ElementRef, 43 | React.ComponentPropsWithoutRef 44 | >(({ className, children, ...props }, ref) => ( 45 | 50 |
{children}
51 |
52 | )); 53 | AccordionContent.displayName = AccordionPrimitive.Content.displayName; 54 | 55 | export { Accordion, AccordionItem, AccordionTrigger, AccordionContent }; -------------------------------------------------------------------------------- /src/components/layout/theme-toggle.tsx: -------------------------------------------------------------------------------- 1 | import { Moon, Sun } from "lucide-react"; 2 | import { useTheme } from "next-themes"; 3 | import { Button } from "@/components/ui/button"; 4 | 5 | export function ThemeToggle() { 6 | const { setTheme, theme } = useTheme(); 7 | 8 | return ( 9 | 20 | ); 21 | } -------------------------------------------------------------------------------- /src/components/layout/user-avatar.tsx: -------------------------------------------------------------------------------- 1 | import { type User } from "@prisma/client" 2 | import { type AvatarProps } from "@radix-ui/react-avatar" 3 | 4 | import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar" 5 | 6 | import { User as UserIcon } from "lucide-react" 7 | 8 | interface UserAvatarProps extends AvatarProps { 9 | user: Pick 10 | } 11 | 12 | export function UserAvatar({ user, ...props }: UserAvatarProps) { 13 | return ( 14 | 15 | {user.image ? ( 16 | 17 | ) : ( 18 | 19 | {user.name} 20 | 21 | 22 | )} 23 | 24 | ) 25 | } -------------------------------------------------------------------------------- /src/components/layout/user-nav.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { signOut } from "next-auth/react"; 4 | import type { User } from "next-auth"; 5 | import { 6 | DropdownMenu, 7 | DropdownMenuContent, 8 | DropdownMenuItem, 9 | DropdownMenuSeparator, 10 | DropdownMenuTrigger, 11 | } from "@/components/ui/dropdown-menu"; 12 | import { Button } from "@/components/ui/button"; 13 | import { LogOut } from "lucide-react"; 14 | import { UserAvatar } from "@/components/layout/user-avatar"; 15 | 16 | type Props = { 17 | user: Pick; 18 | }; 19 | 20 | export function UserNav({ user }: Props) { 21 | return ( 22 | 23 | 24 | 28 | 29 | 30 |
31 |
32 | {user.name &&

{user.name}

} 33 | {user.email && ( 34 |

35 | {user.email} 36 |

37 | )} 38 |
39 |
40 | 41 | 42 | 52 | 53 |
54 |
55 | ); 56 | } -------------------------------------------------------------------------------- /src/components/page-component/example/employee/cell-action.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import { useState } from "react"; 3 | import { useRouter } from "next/navigation"; 4 | import toast from "react-hot-toast"; 5 | import { api } from "@/utils/api"; 6 | import { Button } from "@/components/ui/button"; 7 | import { 8 | Tooltip, 9 | TooltipContent, 10 | TooltipProvider, 11 | TooltipTrigger, 12 | } from "@/components/ui/tooltip"; 13 | 14 | import { Pencil, Trash2 } from "lucide-react"; 15 | import { AlertModal } from "@/components/common/alert-modal"; 16 | import { type EmployeeColumn } from "@/lib/validators"; 17 | 18 | interface CellActionProps { 19 | data: EmployeeColumn; 20 | } 21 | 22 | export function CellAction({ data }: CellActionProps) { 23 | const router = useRouter(); 24 | const [alertModalOpen, setAlertModalOpen] = useState(false); 25 | 26 | const { refetch } = api.employee.getAll.useQuery(undefined, { 27 | enabled: false, 28 | }); 29 | 30 | const { mutate: deleteEmployee, isLoading: deleteEmployeeIsLoading } = 31 | api.employee.delete.useMutation({ 32 | onError: (err) => { 33 | toast.error(err.message); 34 | }, 35 | onSuccess: async (data) => { 36 | toast.success("Delete Employee success"); 37 | await refetch(); 38 | }, 39 | }); 40 | 41 | return ( 42 |
43 | 44 | 45 | 46 | 56 | 57 | 58 |

Update employee

59 |
60 |
61 |
62 | 63 | 64 | 65 | 66 | 76 | 77 | 78 |

Delete employee

79 |
80 |
81 |
82 | 83 | setAlertModalOpen(false)} 89 | onConfirm={() => deleteEmployee(data.id)} 90 | loading={deleteEmployeeIsLoading} 91 | /> 92 |
93 | ); 94 | } 95 | -------------------------------------------------------------------------------- /src/components/page-component/example/employee/client.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import React from "react"; 3 | import { Heading } from "@/components/common/heading"; 4 | import { useRouter } from "next/navigation"; 5 | import { Button } from "@/components/ui/button"; 6 | import { Plus } from "lucide-react"; 7 | import { Separator } from "@/components/ui/separator"; 8 | import { DataTable } from "@/components/ui/data-table/data-table"; 9 | import { type EmployeeColumn } from "@/lib/validators"; 10 | import { columns } from "./columns"; 11 | 12 | interface EmployeeClientProps { 13 | data: EmployeeColumn[]; 14 | } 15 | export const EmployeeClient = ({ data }: EmployeeClientProps) => { 16 | const router = useRouter(); 17 | 18 | return ( 19 | <> 20 |
21 | 25 | 32 |
33 | 34 |
35 | 36 |
37 | 38 | ); 39 | }; 40 | -------------------------------------------------------------------------------- /src/components/page-component/example/employee/columns.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { Checkbox } from "@/components/ui/checkbox"; 4 | import { type ColumnDef } from "@tanstack/react-table"; 5 | import { type EmployeeColumn } from "@/lib/validators"; 6 | import { CellAction } from "./cell-action"; 7 | 8 | export const columns: ColumnDef[] = [ 9 | { 10 | id: "select", 11 | header: ({ table }) => ( 12 | table.toggleAllPageRowsSelected(!!value)} 18 | aria-label="Select all" 19 | className="translate-y-[2px]" 20 | /> 21 | ), 22 | cell: ({ row }) => ( 23 | row.toggleSelected(!!value)} 26 | aria-label="Select row" 27 | className="translate-y-[2px]" 28 | /> 29 | ), 30 | enableSorting: false, 31 | enableHiding: false, 32 | }, 33 | { 34 | accessorKey: "firstName", 35 | header: "First Name", 36 | }, 37 | { 38 | accessorKey: "lastName", 39 | header: "Last Name", 40 | }, 41 | { 42 | accessorKey: "gender", 43 | header: "Gender", 44 | }, 45 | { 46 | accessorKey: "createAt", 47 | header: "Create Time", 48 | }, 49 | { 50 | accessorKey: "updateAt", 51 | header: "Update Time", 52 | }, 53 | { 54 | id: "actions", 55 | enableSorting: false, 56 | cell: ({ row }) => , 57 | }, 58 | ]; 59 | -------------------------------------------------------------------------------- /src/components/page-component/example/employee/employee-form.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-misused-promises */ 2 | import React, { useState } from "react"; 3 | import { type Employee } from "@prisma/client"; 4 | import { useRouter } from "next/navigation"; 5 | import { useForm } from "react-hook-form"; 6 | import { type EmployeeFromValues, employeeFormSchema } from "@/lib/validators"; 7 | import { zodResolver } from "@hookform/resolvers/zod"; 8 | import { api } from "@/utils/api"; 9 | import toast from "react-hot-toast"; 10 | import { Heading } from "@/components/common/heading"; 11 | import { Button } from "@/components/ui/button"; 12 | import { Trash } from "lucide-react"; 13 | import { Separator } from "@/components/ui/separator"; 14 | import { 15 | Form, 16 | FormControl, 17 | FormField, 18 | FormItem, 19 | FormLabel, 20 | FormMessage, 21 | } from "@/components/ui/form"; 22 | import { Input } from "@/components/ui/input"; 23 | import { 24 | Select, 25 | SelectContent, 26 | SelectItem, 27 | SelectTrigger, 28 | SelectValue, 29 | } from "@/components/ui/select"; 30 | import { AlertModal } from "@/components/common/alert-modal"; 31 | 32 | interface EmployeeFormProps { 33 | initialData: Employee | null | undefined; 34 | } 35 | export const EmployeeForm = ({ initialData }: EmployeeFormProps) => { 36 | const router = useRouter(); 37 | const [open, setOpen] = useState(false); 38 | const [loading, setLoading] = useState(false); 39 | 40 | const title = initialData ? "Edit employee" : "Create employee"; 41 | const description = initialData ? "Edit a employee" : "Create a new employee"; 42 | const toastMessage = initialData 43 | ? "Employee updated successfully" 44 | : "Employee created successfully"; 45 | const action = initialData ? "Save Changes" : "Create"; 46 | 47 | const form = useForm({ 48 | resolver: zodResolver(employeeFormSchema), 49 | defaultValues: initialData || { 50 | firstName: "", 51 | lastName: "", 52 | gender: "", 53 | }, 54 | }); 55 | 56 | const { mutate: createEmployee } = api.employee.create.useMutation({ 57 | onError: (err) => { 58 | toast.error(err.message); 59 | }, 60 | onSuccess: (data) => { 61 | toast.success(toastMessage); 62 | router.push(`/example/employees`); 63 | }, 64 | }); 65 | 66 | const { mutate: updateEmployee } = api.employee.update.useMutation({ 67 | onError: (err) => { 68 | toast.error(err.message); 69 | }, 70 | onSuccess: (data) => { 71 | toast.success(toastMessage); 72 | router.push(`/example/employees`); 73 | }, 74 | }); 75 | 76 | const { mutate: deleteEmployee, isLoading: deleteEmployeeIsLoading } = 77 | api.employee.delete.useMutation({ 78 | onError: (err) => { 79 | toast.error(err.message); 80 | }, 81 | onSuccess: (data) => { 82 | toast.success(toastMessage); 83 | router.push(`/example/employees`); 84 | }, 85 | }); 86 | 87 | const onSubmit = (values: EmployeeFromValues) => { 88 | setLoading(true); 89 | if (initialData) { 90 | updateEmployee({ ...values, id: initialData.id }); 91 | } else { 92 | createEmployee(values); 93 | } 94 | setLoading(false); 95 | }; 96 | 97 | const onDelete = () => { 98 | deleteEmployee(initialData?.id as string); 99 | }; 100 | 101 | return ( 102 | <> 103 |
104 | 105 | {initialData && ( 106 | 114 | )} 115 |
116 | 117 | 118 |
119 | 123 |
124 | ( 128 | 129 | First Name 130 | 131 | 136 | 137 | 138 | )} 139 | /> 140 | ( 144 | 145 | Last Name 146 | 147 | 152 | 153 | 154 | )} 155 | /> 156 | ( 160 | 161 | Gender 162 | 181 | 182 | 183 | )} 184 | /> 185 |
186 |
187 | 190 | 200 |
201 |
202 | 203 | setOpen(false)} 209 | onConfirm={onDelete} 210 | loading={deleteEmployeeIsLoading} 211 | /> 212 | 213 | ); 214 | }; 215 | -------------------------------------------------------------------------------- /src/components/page-component/example/example-02/multi-select-input.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import * as React from "react"; 4 | import { X } from "lucide-react"; 5 | 6 | import { Badge } from "@/components/ui/badge"; 7 | import { Command, CommandGroup, CommandItem } from "@/components/ui/command"; 8 | import { Command as CommandPrimitive } from "cmdk"; 9 | 10 | type Framework = Record<"value" | "label", string>; 11 | 12 | const FRAMEWORKS = [ 13 | { 14 | value: "next.js", 15 | label: "Next.js", 16 | }, 17 | { 18 | value: "sveltekit", 19 | label: "SvelteKit", 20 | }, 21 | { 22 | value: "nuxt.js", 23 | label: "Nuxt.js", 24 | }, 25 | { 26 | value: "remix", 27 | label: "Remix", 28 | }, 29 | { 30 | value: "astro", 31 | label: "Astro", 32 | }, 33 | { 34 | value: "wordpress", 35 | label: "WordPress", 36 | }, 37 | { 38 | value: "express.js", 39 | label: "Express.js", 40 | }, 41 | { 42 | value: "nest.js", 43 | label: "Nest.js", 44 | }, 45 | ] satisfies Framework[]; 46 | 47 | export function FancyMultiSelect() { 48 | const inputRef = React.useRef(null); 49 | const [open, setOpen] = React.useState(false); 50 | const [selected, setSelected] = React.useState([]); 51 | const [inputValue, setInputValue] = React.useState(""); 52 | 53 | const handleUnselect = React.useCallback((framework: Framework) => { 54 | setSelected((prev) => prev.filter((s) => s.value !== framework.value)); 55 | }, []); 56 | 57 | const handleKeyDown = React.useCallback( 58 | (e: React.KeyboardEvent) => { 59 | const input = inputRef.current; 60 | if (input) { 61 | if (e.key === "Delete" || e.key === "Backspace") { 62 | if (input.value === "") { 63 | setSelected((prev) => { 64 | const newSelected = [...prev]; 65 | newSelected.pop(); 66 | return newSelected; 67 | }); 68 | } 69 | } 70 | // This is not a default behaviour of the field 71 | if (e.key === "Escape") { 72 | input.blur(); 73 | } 74 | } 75 | }, 76 | [] 77 | ); 78 | 79 | const selectables = FRAMEWORKS.filter( 80 | (framework) => 81 | !selected.some( 82 | (slect_framework) => framework.value === slect_framework.value 83 | ) 84 | ); 85 | 86 | return ( 87 | 91 |
92 |
93 | {selected.map((framework) => { 94 | return ( 95 | 96 | {framework.label} 97 | 112 | 113 | ); 114 | })} 115 | {/* Avoid having the "Search" Icon */} 116 | setOpen(false)} 121 | onFocus={() => setOpen(true)} 122 | placeholder="Select frameworks..." 123 | className="ml-2 flex-1 bg-transparent outline-none placeholder:text-muted-foreground" 124 | /> 125 |
126 |
127 |
128 | {open && selectables.length > 0 ? ( 129 |
130 | 131 | {selectables.map((framework) => { 132 | return ( 133 | { 136 | e.preventDefault(); 137 | e.stopPropagation(); 138 | }} 139 | onSelect={(value) => { 140 | setInputValue(""); 141 | setSelected((prev) => [...prev, framework]); 142 | }} 143 | className={"cursor-pointer"} 144 | > 145 | {framework.label} 146 | 147 | ); 148 | })} 149 | 150 |
151 | ) : null} 152 |
153 |
154 | ); 155 | } 156 | -------------------------------------------------------------------------------- /src/components/page-component/example/example-03/email-form.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useForm } from "react-hook-form"; 4 | import { useFormState } from "./form-context"; 5 | 6 | type TFormValues = { 7 | email: string; 8 | }; 9 | const EmailForm = () => { 10 | const { onHandleNext, onHandleBack, setFormData, formData } = useFormState(); 11 | const { register, handleSubmit } = useForm({ 12 | defaultValues: formData, 13 | }); 14 | 15 | function onHandleFormSubmit(data: TFormValues) { 16 | setFormData((prevFormData) => ({ ...prevFormData, ...data })); 17 | onHandleNext(); 18 | } 19 | 20 | return ( 21 | // eslint-disable-next-line @typescript-eslint/no-misused-promises 22 |
23 |
24 | 25 | 32 |
33 |
34 | 41 | 44 |
45 |
46 | ); 47 | }; 48 | 49 | export default EmailForm; 50 | -------------------------------------------------------------------------------- /src/components/page-component/example/example-03/form-context.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | type Dispatch, 3 | type SetStateAction, 4 | createContext, 5 | useContext, 6 | useState, 7 | } from "react"; 8 | 9 | interface TFormData { 10 | username: string; 11 | email: string; 12 | password: string; 13 | } 14 | 15 | interface FormContextProps { 16 | onHandleNext: () => void; 17 | onHandleBack: () => void; 18 | step: number; 19 | formData: TFormData; 20 | setFormData: Dispatch>; 21 | } 22 | 23 | const FormContext = createContext({ 24 | // eslint-disable-next-line @typescript-eslint/no-empty-function 25 | onHandleNext: () => {}, 26 | // eslint-disable-next-line @typescript-eslint/no-empty-function 27 | onHandleBack: () => {}, 28 | step: 1, 29 | formData: { 30 | username: "", 31 | email: "", 32 | password: "", 33 | }, 34 | // eslint-disable-next-line @typescript-eslint/no-empty-function 35 | setFormData: () => {}, 36 | }); 37 | 38 | interface FormProviderProps { 39 | children: React.ReactNode; 40 | } 41 | 42 | export function FormProvider({ children }: FormProviderProps) { 43 | const [step, setStep] = useState(1); 44 | const [formData, setFormData] = useState({ 45 | username: "", 46 | email: "", 47 | password: "", 48 | }); 49 | 50 | console.log(formData); 51 | function onHandleNext() { 52 | console.log(step); 53 | setStep((prevValue) => prevValue + 1); 54 | } 55 | 56 | function onHandleBack() { 57 | setStep((prevValue) => prevValue - 1); 58 | } 59 | 60 | return ( 61 | 64 | {children} 65 | 66 | ); 67 | } 68 | 69 | export function useFormState() { 70 | return useContext(FormContext); 71 | } 72 | -------------------------------------------------------------------------------- /src/components/page-component/example/example-03/form-step.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { useFormState } from "./form-context"; 3 | import UsernameForm from "./username-form"; 4 | import EmailForm from "./email-form"; 5 | import PasswordForm from "./password-form"; 6 | 7 | const FormStep = () => { 8 | const { step } = useFormState(); 9 | 10 | switch (step) { 11 | case 1: 12 | return ; 13 | case 2: 14 | return ; 15 | case 3: 16 | return ; 17 | default: 18 | return null; 19 | } 20 | }; 21 | 22 | export default FormStep; 23 | -------------------------------------------------------------------------------- /src/components/page-component/example/example-03/password-form.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useForm } from "react-hook-form"; 4 | import { useFormState } from "./form-context"; 5 | import { useState } from "react"; 6 | 7 | type TFormValues = { 8 | password: string; 9 | }; 10 | const PasswordForm = () => { 11 | const { onHandleBack, setFormData, formData } = useFormState(); 12 | const { register, handleSubmit } = useForm(); 13 | 14 | const [isCreated, setCreated] = useState(false); 15 | 16 | function onHandleFormSubmit(data: TFormValues) { 17 | setFormData((prevFormData) => ({ ...prevFormData, ...data })); 18 | setCreated(true); 19 | } 20 | 21 | return isCreated ? ( 22 |
23 |

Account created successfully

24 |
{JSON.stringify(formData)}
25 |
26 | ) : ( 27 | // eslint-disable-next-line @typescript-eslint/no-misused-promises 28 |
29 |
30 | 31 | 38 |
39 |
40 | 47 | 50 |
51 |
52 | ); 53 | }; 54 | 55 | export default PasswordForm; 56 | -------------------------------------------------------------------------------- /src/components/page-component/example/example-03/username-form.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useForm } from "react-hook-form"; 4 | import { useFormState } from "./form-context"; 5 | 6 | type TFormValues = { 7 | username: string; 8 | }; 9 | const UsernameForm = () => { 10 | const { onHandleNext, setFormData, formData } = useFormState(); 11 | 12 | const { register, handleSubmit } = useForm({ 13 | defaultValues: formData, 14 | }); 15 | 16 | function onHandleFormSubmit(data: TFormValues) { 17 | setFormData((prevFormData) => ({ ...prevFormData, ...data })); 18 | void onHandleNext(); 19 | } 20 | 21 | return ( 22 | // eslint-disable-next-line @typescript-eslint/no-misused-promises 23 |
24 |
25 | 26 | 33 |
34 |
35 | 38 |
39 |
40 | ); 41 | }; 42 | 43 | export default UsernameForm; 44 | -------------------------------------------------------------------------------- /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 { ChevronDown } from "lucide-react" 6 | 7 | import { cn } from "@/lib/utils" 8 | 9 | const Accordion = AccordionPrimitive.Root 10 | 11 | const AccordionItem = React.forwardRef< 12 | React.ElementRef, 13 | React.ComponentPropsWithoutRef 14 | >(({ className, ...props }, ref) => ( 15 | 20 | )) 21 | AccordionItem.displayName = "AccordionItem" 22 | 23 | const AccordionTrigger = React.forwardRef< 24 | React.ElementRef, 25 | React.ComponentPropsWithoutRef 26 | >(({ className, children, ...props }, ref) => ( 27 | 28 | svg]:rotate-180", 32 | className 33 | )} 34 | {...props} 35 | > 36 | {children} 37 | 38 | 39 | 40 | )) 41 | AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName 42 | 43 | const AccordionContent = React.forwardRef< 44 | React.ElementRef, 45 | React.ComponentPropsWithoutRef 46 | >(({ className, children, ...props }, ref) => ( 47 | 52 |
{children}
53 |
54 | )) 55 | 56 | AccordionContent.displayName = AccordionPrimitive.Content.displayName 57 | 58 | export { Accordion, AccordionItem, AccordionTrigger, AccordionContent } 59 | -------------------------------------------------------------------------------- /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 | const AlertDialog = AlertDialogPrimitive.Root 10 | 11 | const AlertDialogTrigger = AlertDialogPrimitive.Trigger 12 | 13 | const AlertDialogPortal = AlertDialogPrimitive.Portal 14 | 15 | const AlertDialogOverlay = React.forwardRef< 16 | React.ElementRef, 17 | React.ComponentPropsWithoutRef 18 | >(({ className, ...props }, ref) => ( 19 | 27 | )) 28 | AlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName 29 | 30 | const AlertDialogContent = React.forwardRef< 31 | React.ElementRef, 32 | React.ComponentPropsWithoutRef 33 | >(({ className, ...props }, ref) => ( 34 | 35 | 36 | 44 | 45 | )) 46 | AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName 47 | 48 | const AlertDialogHeader = ({ 49 | className, 50 | ...props 51 | }: React.HTMLAttributes) => ( 52 |
59 | ) 60 | AlertDialogHeader.displayName = "AlertDialogHeader" 61 | 62 | const AlertDialogFooter = ({ 63 | className, 64 | ...props 65 | }: React.HTMLAttributes) => ( 66 |
73 | ) 74 | AlertDialogFooter.displayName = "AlertDialogFooter" 75 | 76 | const AlertDialogTitle = React.forwardRef< 77 | React.ElementRef, 78 | React.ComponentPropsWithoutRef 79 | >(({ className, ...props }, ref) => ( 80 | 85 | )) 86 | AlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName 87 | 88 | const AlertDialogDescription = React.forwardRef< 89 | React.ElementRef, 90 | React.ComponentPropsWithoutRef 91 | >(({ className, ...props }, ref) => ( 92 | 97 | )) 98 | AlertDialogDescription.displayName = 99 | AlertDialogPrimitive.Description.displayName 100 | 101 | const AlertDialogAction = React.forwardRef< 102 | React.ElementRef, 103 | React.ComponentPropsWithoutRef 104 | >(({ className, ...props }, ref) => ( 105 | 110 | )) 111 | AlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName 112 | 113 | const AlertDialogCancel = React.forwardRef< 114 | React.ElementRef, 115 | React.ComponentPropsWithoutRef 116 | >(({ className, ...props }, ref) => ( 117 | 126 | )) 127 | AlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName 128 | 129 | export { 130 | AlertDialog, 131 | AlertDialogPortal, 132 | AlertDialogOverlay, 133 | AlertDialogTrigger, 134 | AlertDialogContent, 135 | AlertDialogHeader, 136 | AlertDialogFooter, 137 | AlertDialogTitle, 138 | AlertDialogDescription, 139 | AlertDialogAction, 140 | AlertDialogCancel, 141 | } 142 | -------------------------------------------------------------------------------- /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 p-4 [&>svg~*]:pl-7 [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground", 8 | { 9 | variants: { 10 | variant: { 11 | default: "bg-background text-foreground", 12 | destructive: 13 | "border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive", 14 | }, 15 | }, 16 | defaultVariants: { 17 | variant: "default", 18 | }, 19 | } 20 | ) 21 | 22 | const Alert = React.forwardRef< 23 | HTMLDivElement, 24 | React.HTMLAttributes & VariantProps 25 | >(({ className, variant, ...props }, ref) => ( 26 |
32 | )) 33 | Alert.displayName = "Alert" 34 | 35 | const AlertTitle = React.forwardRef< 36 | HTMLParagraphElement, 37 | React.HTMLAttributes 38 | >(({ className, ...props }, ref) => ( 39 |
44 | )) 45 | AlertTitle.displayName = "AlertTitle" 46 | 47 | const AlertDescription = React.forwardRef< 48 | HTMLParagraphElement, 49 | React.HTMLAttributes 50 | >(({ className, ...props }, ref) => ( 51 |
56 | )) 57 | AlertDescription.displayName = "AlertDescription" 58 | 59 | export { Alert, AlertTitle, AlertDescription } 60 | -------------------------------------------------------------------------------- /src/components/ui/aspect-ratio.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as AspectRatioPrimitive from "@radix-ui/react-aspect-ratio" 4 | 5 | const AspectRatio = AspectRatioPrimitive.Root 6 | 7 | export { AspectRatio } 8 | -------------------------------------------------------------------------------- /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 | const Avatar = React.forwardRef< 9 | React.ElementRef, 10 | React.ComponentPropsWithoutRef 11 | >(({ className, ...props }, ref) => ( 12 | 20 | )) 21 | Avatar.displayName = AvatarPrimitive.Root.displayName 22 | 23 | const AvatarImage = React.forwardRef< 24 | React.ElementRef, 25 | React.ComponentPropsWithoutRef 26 | >(({ className, ...props }, ref) => ( 27 | 32 | )) 33 | AvatarImage.displayName = AvatarPrimitive.Image.displayName 34 | 35 | const AvatarFallback = React.forwardRef< 36 | React.ElementRef, 37 | React.ComponentPropsWithoutRef 38 | >(({ className, ...props }, ref) => ( 39 | 47 | )) 48 | AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName 49 | 50 | export { Avatar, AvatarImage, AvatarFallback } 51 | -------------------------------------------------------------------------------- /src/components/ui/badge.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 badgeVariants = cva( 7 | "inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2", 8 | { 9 | variants: { 10 | variant: { 11 | default: 12 | "border-transparent bg-primary text-primary-foreground hover:bg-primary/80", 13 | secondary: 14 | "border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80", 15 | destructive: 16 | "border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80", 17 | outline: "text-foreground", 18 | }, 19 | }, 20 | defaultVariants: { 21 | variant: "default", 22 | }, 23 | } 24 | ) 25 | 26 | export interface BadgeProps 27 | extends React.HTMLAttributes, 28 | VariantProps {} 29 | 30 | function Badge({ className, variant, ...props }: BadgeProps) { 31 | return ( 32 |
33 | ) 34 | } 35 | 36 | export { Badge, badgeVariants } 37 | -------------------------------------------------------------------------------- /src/components/ui/button.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 buttonVariants = cva( 8 | "inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50", 9 | { 10 | variants: { 11 | variant: { 12 | default: "bg-primary text-primary-foreground hover:bg-primary/90", 13 | destructive: 14 | "bg-destructive text-destructive-foreground hover:bg-destructive/90", 15 | outline: 16 | "border border-input bg-background hover:bg-accent hover:text-accent-foreground", 17 | secondary: 18 | "bg-secondary text-secondary-foreground hover:bg-secondary/80", 19 | ghost: "hover:bg-accent hover:text-accent-foreground", 20 | link: "text-primary underline-offset-4 hover:underline", 21 | }, 22 | size: { 23 | default: "h-10 px-4 py-2", 24 | sm: "h-9 rounded-md px-3", 25 | lg: "h-11 rounded-md px-8", 26 | icon: "h-10 w-10", 27 | }, 28 | }, 29 | defaultVariants: { 30 | variant: "default", 31 | size: "default", 32 | }, 33 | } 34 | ) 35 | 36 | export interface ButtonProps 37 | extends React.ButtonHTMLAttributes, 38 | VariantProps { 39 | asChild?: boolean 40 | } 41 | 42 | const Button = React.forwardRef( 43 | ({ className, variant, size, asChild = false, ...props }, ref) => { 44 | const Comp = asChild ? Slot : "button" 45 | return ( 46 | 51 | ) 52 | } 53 | ) 54 | Button.displayName = "Button" 55 | 56 | export { Button, buttonVariants } 57 | -------------------------------------------------------------------------------- /src/components/ui/calendar.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import { ChevronLeft, ChevronRight } from "lucide-react" 5 | import { DayPicker } from "react-day-picker" 6 | 7 | import { cn } from "@/lib/utils" 8 | import { buttonVariants } from "@/components/ui/button" 9 | 10 | export type CalendarProps = React.ComponentProps 11 | 12 | function Calendar({ 13 | className, 14 | classNames, 15 | showOutsideDays = true, 16 | ...props 17 | }: CalendarProps) { 18 | return ( 19 | , 58 | IconRight: ({ ...props }) => , 59 | }} 60 | {...props} 61 | /> 62 | ) 63 | } 64 | Calendar.displayName = "Calendar" 65 | 66 | export { Calendar } 67 | -------------------------------------------------------------------------------- /src/components/ui/card.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | 3 | import { cn } from "@/lib/utils" 4 | 5 | const Card = React.forwardRef< 6 | HTMLDivElement, 7 | React.HTMLAttributes 8 | >(({ className, ...props }, ref) => ( 9 |
17 | )) 18 | Card.displayName = "Card" 19 | 20 | const CardHeader = React.forwardRef< 21 | HTMLDivElement, 22 | React.HTMLAttributes 23 | >(({ className, ...props }, ref) => ( 24 |
29 | )) 30 | CardHeader.displayName = "CardHeader" 31 | 32 | const CardTitle = React.forwardRef< 33 | HTMLParagraphElement, 34 | React.HTMLAttributes 35 | >(({ className, ...props }, ref) => ( 36 |

44 | )) 45 | CardTitle.displayName = "CardTitle" 46 | 47 | const CardDescription = React.forwardRef< 48 | HTMLParagraphElement, 49 | React.HTMLAttributes 50 | >(({ className, ...props }, ref) => ( 51 |

56 | )) 57 | CardDescription.displayName = "CardDescription" 58 | 59 | const CardContent = React.forwardRef< 60 | HTMLDivElement, 61 | React.HTMLAttributes 62 | >(({ className, ...props }, ref) => ( 63 |

64 | )) 65 | CardContent.displayName = "CardContent" 66 | 67 | const CardFooter = React.forwardRef< 68 | HTMLDivElement, 69 | React.HTMLAttributes 70 | >(({ className, ...props }, ref) => ( 71 |
76 | )) 77 | CardFooter.displayName = "CardFooter" 78 | 79 | export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent } 80 | -------------------------------------------------------------------------------- /src/components/ui/checkbox.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as CheckboxPrimitive from "@radix-ui/react-checkbox" 5 | import { Check } from "lucide-react" 6 | 7 | import { cn } from "@/lib/utils" 8 | 9 | const Checkbox = React.forwardRef< 10 | React.ElementRef, 11 | React.ComponentPropsWithoutRef 12 | >(({ className, ...props }, ref) => ( 13 | 21 | 24 | 25 | 26 | 27 | )) 28 | Checkbox.displayName = CheckboxPrimitive.Root.displayName 29 | 30 | export { Checkbox } 31 | -------------------------------------------------------------------------------- /src/components/ui/collapsible.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as CollapsiblePrimitive from "@radix-ui/react-collapsible" 4 | 5 | const Collapsible = CollapsiblePrimitive.Root 6 | 7 | const CollapsibleTrigger = CollapsiblePrimitive.CollapsibleTrigger 8 | 9 | const CollapsibleContent = CollapsiblePrimitive.CollapsibleContent 10 | 11 | export { Collapsible, CollapsibleTrigger, CollapsibleContent } 12 | -------------------------------------------------------------------------------- /src/components/ui/command.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import { type DialogProps } from "@radix-ui/react-dialog" 5 | import { Command as CommandPrimitive } from "cmdk" 6 | import { Search } from "lucide-react" 7 | 8 | import { cn } from "@/lib/utils" 9 | import { Dialog, DialogContent } from "@/components/ui/dialog" 10 | 11 | const Command = React.forwardRef< 12 | React.ElementRef, 13 | React.ComponentPropsWithoutRef 14 | >(({ className, ...props }, ref) => ( 15 | 23 | )) 24 | Command.displayName = CommandPrimitive.displayName 25 | 26 | // eslint-disable-next-line @typescript-eslint/no-empty-interface 27 | interface CommandDialogProps extends DialogProps { } 28 | 29 | const CommandDialog = ({ children, ...props }: CommandDialogProps) => { 30 | return ( 31 | 32 | 33 | 34 | {children} 35 | 36 | 37 | 38 | ) 39 | } 40 | 41 | const CommandInput = React.forwardRef< 42 | React.ElementRef, 43 | React.ComponentPropsWithoutRef 44 | >(({ className, ...props }, ref) => ( 45 |
46 | 47 | 55 |
56 | )) 57 | 58 | CommandInput.displayName = CommandPrimitive.Input.displayName 59 | 60 | const CommandList = React.forwardRef< 61 | React.ElementRef, 62 | React.ComponentPropsWithoutRef 63 | >(({ className, ...props }, ref) => ( 64 | 69 | )) 70 | 71 | CommandList.displayName = CommandPrimitive.List.displayName 72 | 73 | const CommandEmpty = React.forwardRef< 74 | React.ElementRef, 75 | React.ComponentPropsWithoutRef 76 | >((props, ref) => ( 77 | 82 | )) 83 | 84 | CommandEmpty.displayName = CommandPrimitive.Empty.displayName 85 | 86 | const CommandGroup = React.forwardRef< 87 | React.ElementRef, 88 | React.ComponentPropsWithoutRef 89 | >(({ className, ...props }, ref) => ( 90 | 98 | )) 99 | 100 | CommandGroup.displayName = CommandPrimitive.Group.displayName 101 | 102 | const CommandSeparator = React.forwardRef< 103 | React.ElementRef, 104 | React.ComponentPropsWithoutRef 105 | >(({ className, ...props }, ref) => ( 106 | 111 | )) 112 | CommandSeparator.displayName = CommandPrimitive.Separator.displayName 113 | 114 | const CommandItem = React.forwardRef< 115 | React.ElementRef, 116 | React.ComponentPropsWithoutRef 117 | >(({ className, ...props }, ref) => ( 118 | 126 | )) 127 | 128 | CommandItem.displayName = CommandPrimitive.Item.displayName 129 | 130 | const CommandShortcut = ({ 131 | className, 132 | ...props 133 | }: React.HTMLAttributes) => { 134 | return ( 135 | 142 | ) 143 | } 144 | CommandShortcut.displayName = "CommandShortcut" 145 | 146 | export { 147 | Command, 148 | CommandDialog, 149 | CommandInput, 150 | CommandList, 151 | CommandEmpty, 152 | CommandGroup, 153 | CommandItem, 154 | CommandShortcut, 155 | CommandSeparator, 156 | } 157 | -------------------------------------------------------------------------------- /src/components/ui/context-menu.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as ContextMenuPrimitive from "@radix-ui/react-context-menu" 5 | import { Check, ChevronRight, Circle } from "lucide-react" 6 | 7 | import { cn } from "@/lib/utils" 8 | 9 | const ContextMenu = ContextMenuPrimitive.Root 10 | 11 | const ContextMenuTrigger = ContextMenuPrimitive.Trigger 12 | 13 | const ContextMenuGroup = ContextMenuPrimitive.Group 14 | 15 | const ContextMenuPortal = ContextMenuPrimitive.Portal 16 | 17 | const ContextMenuSub = ContextMenuPrimitive.Sub 18 | 19 | const ContextMenuRadioGroup = ContextMenuPrimitive.RadioGroup 20 | 21 | const ContextMenuSubTrigger = React.forwardRef< 22 | React.ElementRef, 23 | React.ComponentPropsWithoutRef & { 24 | inset?: boolean 25 | } 26 | >(({ className, inset, children, ...props }, ref) => ( 27 | 36 | {children} 37 | 38 | 39 | )) 40 | ContextMenuSubTrigger.displayName = ContextMenuPrimitive.SubTrigger.displayName 41 | 42 | const ContextMenuSubContent = React.forwardRef< 43 | React.ElementRef, 44 | React.ComponentPropsWithoutRef 45 | >(({ className, ...props }, ref) => ( 46 | 54 | )) 55 | ContextMenuSubContent.displayName = ContextMenuPrimitive.SubContent.displayName 56 | 57 | const ContextMenuContent = React.forwardRef< 58 | React.ElementRef, 59 | React.ComponentPropsWithoutRef 60 | >(({ className, ...props }, ref) => ( 61 | 62 | 70 | 71 | )) 72 | ContextMenuContent.displayName = ContextMenuPrimitive.Content.displayName 73 | 74 | const ContextMenuItem = React.forwardRef< 75 | React.ElementRef, 76 | React.ComponentPropsWithoutRef & { 77 | inset?: boolean 78 | } 79 | >(({ className, inset, ...props }, ref) => ( 80 | 89 | )) 90 | ContextMenuItem.displayName = ContextMenuPrimitive.Item.displayName 91 | 92 | const ContextMenuCheckboxItem = React.forwardRef< 93 | React.ElementRef, 94 | React.ComponentPropsWithoutRef 95 | >(({ className, children, checked, ...props }, ref) => ( 96 | 105 | 106 | 107 | 108 | 109 | 110 | {children} 111 | 112 | )) 113 | ContextMenuCheckboxItem.displayName = 114 | ContextMenuPrimitive.CheckboxItem.displayName 115 | 116 | const ContextMenuRadioItem = React.forwardRef< 117 | React.ElementRef, 118 | React.ComponentPropsWithoutRef 119 | >(({ className, children, ...props }, ref) => ( 120 | 128 | 129 | 130 | 131 | 132 | 133 | {children} 134 | 135 | )) 136 | ContextMenuRadioItem.displayName = ContextMenuPrimitive.RadioItem.displayName 137 | 138 | const ContextMenuLabel = React.forwardRef< 139 | React.ElementRef, 140 | React.ComponentPropsWithoutRef & { 141 | inset?: boolean 142 | } 143 | >(({ className, inset, ...props }, ref) => ( 144 | 153 | )) 154 | ContextMenuLabel.displayName = ContextMenuPrimitive.Label.displayName 155 | 156 | const ContextMenuSeparator = React.forwardRef< 157 | React.ElementRef, 158 | React.ComponentPropsWithoutRef 159 | >(({ className, ...props }, ref) => ( 160 | 165 | )) 166 | ContextMenuSeparator.displayName = ContextMenuPrimitive.Separator.displayName 167 | 168 | const ContextMenuShortcut = ({ 169 | className, 170 | ...props 171 | }: React.HTMLAttributes) => { 172 | return ( 173 | 180 | ) 181 | } 182 | ContextMenuShortcut.displayName = "ContextMenuShortcut" 183 | 184 | export { 185 | ContextMenu, 186 | ContextMenuTrigger, 187 | ContextMenuContent, 188 | ContextMenuItem, 189 | ContextMenuCheckboxItem, 190 | ContextMenuRadioItem, 191 | ContextMenuLabel, 192 | ContextMenuSeparator, 193 | ContextMenuShortcut, 194 | ContextMenuGroup, 195 | ContextMenuPortal, 196 | ContextMenuSub, 197 | ContextMenuSubContent, 198 | ContextMenuSubTrigger, 199 | ContextMenuRadioGroup, 200 | } 201 | -------------------------------------------------------------------------------- /src/components/ui/data-table/data-table-column-header.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | ArrowDownIcon, 3 | ArrowUpIcon, 4 | CaretSortIcon, 5 | EyeNoneIcon, 6 | } from "@radix-ui/react-icons" 7 | import { type Column } from "@tanstack/react-table" 8 | 9 | import { cn } from "@/lib/utils" 10 | import { Button } from "@/components/ui/button" 11 | import { 12 | DropdownMenu, 13 | DropdownMenuContent, 14 | DropdownMenuItem, 15 | DropdownMenuSeparator, 16 | DropdownMenuTrigger, 17 | } from "@/components/ui/dropdown-menu" 18 | 19 | interface DataTableColumnHeaderProps 20 | extends React.HTMLAttributes { 21 | column: Column 22 | title: string 23 | } 24 | 25 | export function DataTableColumnHeader({ 26 | column, 27 | title, 28 | className, 29 | }: DataTableColumnHeaderProps) { 30 | if (!column.getCanSort()) { 31 | return
{title}
32 | } 33 | 34 | return ( 35 |
36 | 37 | 38 | 52 | 53 | 54 | column.toggleSorting(false)}> 55 | 56 | Asc 57 | 58 | column.toggleSorting(true)}> 59 | 60 | Desc 61 | 62 | 63 | column.toggleVisibility(false)}> 64 | 65 | Hide 66 | 67 | 68 | 69 |
70 | ) 71 | } 72 | -------------------------------------------------------------------------------- /src/components/ui/data-table/data-table-pagination.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | ChevronLeftIcon, 3 | ChevronRightIcon, 4 | DoubleArrowLeftIcon, 5 | DoubleArrowRightIcon, 6 | } from "@radix-ui/react-icons" 7 | import { type Table } from "@tanstack/react-table" 8 | 9 | import { Button } from "@/components/ui/button" 10 | import { 11 | Select, 12 | SelectContent, 13 | SelectItem, 14 | SelectTrigger, 15 | SelectValue, 16 | } from "@/components/ui/select" 17 | 18 | interface DataTablePaginationProps { 19 | table: Table 20 | } 21 | 22 | export function DataTablePagination({ 23 | table, 24 | }: DataTablePaginationProps) { 25 | return ( 26 |
27 |
28 | {table.getFilteredSelectedRowModel().rows.length} of{" "} 29 | {table.getFilteredRowModel().rows.length} row(s) selected. 30 |
31 |
32 |
33 |

Rows per page

34 | 51 |
52 |
53 | Page {table.getState().pagination.pageIndex + 1} of{" "} 54 | {table.getPageCount()} 55 |
56 |
57 | 66 | 75 | 84 | 93 |
94 |
95 |
96 | ) 97 | } 98 | -------------------------------------------------------------------------------- /src/components/ui/data-table/data-table-toolbar.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | 4 | import { type Table } from "@tanstack/react-table" 5 | import { Input } from "@/components/ui/input" 6 | import { DataTableViewOptions } from "@/components/ui/data-table/data-table-view-options" 7 | 8 | interface DataTableToolbarProps { 9 | table: Table 10 | } 11 | 12 | export function DataTableToolbar({ 13 | table, 14 | }: DataTableToolbarProps) { 15 | 16 | 17 | return ( 18 |
19 |
20 | table.setGlobalFilter(event.target.value)} 24 | className="h-8 w-[150px] lg:w-[250px]" 25 | /> 26 |
27 | 28 |
29 | ) 30 | } 31 | -------------------------------------------------------------------------------- /src/components/ui/data-table/data-table-view-options.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import { DropdownMenuTrigger } from "@radix-ui/react-dropdown-menu" 4 | import { MixerHorizontalIcon } from "@radix-ui/react-icons" 5 | import { type Table } from "@tanstack/react-table" 6 | 7 | import { Button } from "@/components/ui/button" 8 | import { 9 | DropdownMenu, 10 | DropdownMenuCheckboxItem, 11 | DropdownMenuContent, 12 | DropdownMenuLabel, 13 | DropdownMenuSeparator, 14 | } from "@/components/ui/dropdown-menu" 15 | 16 | interface DataTableViewOptionsProps { 17 | table: Table 18 | } 19 | 20 | export function DataTableViewOptions({ 21 | table, 22 | }: DataTableViewOptionsProps) { 23 | return ( 24 | 25 | 26 | 34 | 35 | 36 | Toggle columns 37 | 38 | {table 39 | .getAllColumns() 40 | .filter( 41 | (column) => 42 | typeof column.accessorFn !== "undefined" && column.getCanHide() 43 | ) 44 | .map((column) => { 45 | return ( 46 | column.toggleVisibility(!!value)} 51 | > 52 | {column.id} 53 | 54 | ) 55 | })} 56 | 57 | 58 | ) 59 | } 60 | -------------------------------------------------------------------------------- /src/components/ui/data-table/data-table.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import { 5 | type ColumnDef, 6 | type ColumnFiltersState, 7 | type SortingState, 8 | type VisibilityState, 9 | flexRender, 10 | getCoreRowModel, 11 | getFacetedRowModel, 12 | getFacetedUniqueValues, 13 | getFilteredRowModel, 14 | getPaginationRowModel, 15 | getSortedRowModel, 16 | useReactTable, 17 | } from "@tanstack/react-table" 18 | 19 | import { 20 | Table, 21 | TableBody, 22 | TableCell, 23 | TableHead, 24 | TableHeader, 25 | TableRow, 26 | } from "@/components/ui/table" 27 | 28 | import { DataTablePagination } from "@/components/ui/data-table/data-table-pagination" 29 | import { DataTableToolbar } from "@/components/ui/data-table/data-table-toolbar" 30 | 31 | interface DataTableProps { 32 | columns: ColumnDef[] 33 | data: TData[] 34 | } 35 | 36 | export function DataTable({ 37 | columns, 38 | data, 39 | }: DataTableProps) { 40 | const [rowSelection, setRowSelection] = React.useState({}) 41 | const [columnVisibility, setColumnVisibility] = 42 | React.useState({}) 43 | const [columnFilters, setColumnFilters] = React.useState( 44 | [] 45 | ) 46 | const [sorting, setSorting] = React.useState([]) 47 | 48 | const table = useReactTable({ 49 | data, 50 | columns, 51 | state: { 52 | sorting, 53 | columnVisibility, 54 | rowSelection, 55 | columnFilters, 56 | }, 57 | enableRowSelection: true, 58 | onRowSelectionChange: setRowSelection, 59 | onSortingChange: setSorting, 60 | onColumnFiltersChange: setColumnFilters, 61 | onColumnVisibilityChange: setColumnVisibility, 62 | getCoreRowModel: getCoreRowModel(), 63 | getFilteredRowModel: getFilteredRowModel(), 64 | getPaginationRowModel: getPaginationRowModel(), 65 | getSortedRowModel: getSortedRowModel(), 66 | getFacetedRowModel: getFacetedRowModel(), 67 | getFacetedUniqueValues: getFacetedUniqueValues(), 68 | }) 69 | 70 | return ( 71 |
72 | 73 |
74 | 75 | 76 | {table.getHeaderGroups().map((headerGroup) => ( 77 | 78 | {headerGroup.headers.map((header) => { 79 | return ( 80 | 81 | {header.isPlaceholder 82 | ? null 83 | : flexRender( 84 | header.column.columnDef.header, 85 | header.getContext() 86 | )} 87 | 88 | ) 89 | })} 90 | 91 | ))} 92 | 93 | 94 | {table.getRowModel().rows?.length ? ( 95 | table.getRowModel().rows.map((row) => ( 96 | 100 | {row.getVisibleCells().map((cell) => ( 101 | 102 | {flexRender( 103 | cell.column.columnDef.cell, 104 | cell.getContext() 105 | )} 106 | 107 | ))} 108 | 109 | )) 110 | ) : ( 111 | 112 | 116 | No results. 117 | 118 | 119 | )} 120 | 121 |
122 |
123 | 124 |
125 | ) 126 | } 127 | -------------------------------------------------------------------------------- /src/components/ui/date-picker.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import { format } from "date-fns"; 3 | import { Calendar as CalendarIcon } from "lucide-react"; 4 | 5 | import { cn } from "@/lib/utils"; 6 | import { Button } from "@/components/ui/button"; 7 | import { Calendar } from "@/components/ui/calendar"; 8 | import { 9 | Popover, 10 | PopoverContent, 11 | PopoverTrigger, 12 | } from "@/components/ui/popover"; 13 | import { forwardRef } from "react"; 14 | 15 | export const DatePicker = forwardRef< 16 | HTMLDivElement, 17 | { 18 | date?: Date; 19 | setDate: (date?: Date) => void; 20 | } 21 | >(function DatePickerCmp({ date, setDate }, ref) { 22 | return ( 23 | 24 | 25 | 35 | 36 | 37 | 43 | 44 | 45 | ); 46 | }); 47 | -------------------------------------------------------------------------------- /src/components/ui/dialog.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as DialogPrimitive from "@radix-ui/react-dialog" 5 | import { X } from "lucide-react" 6 | 7 | import { cn } from "@/lib/utils" 8 | 9 | const Dialog = DialogPrimitive.Root 10 | 11 | const DialogTrigger = DialogPrimitive.Trigger 12 | 13 | const DialogPortal = DialogPrimitive.Portal 14 | 15 | const DialogClose = DialogPrimitive.Close 16 | 17 | const DialogOverlay = React.forwardRef< 18 | React.ElementRef, 19 | React.ComponentPropsWithoutRef 20 | >(({ className, ...props }, ref) => ( 21 | 29 | )) 30 | DialogOverlay.displayName = DialogPrimitive.Overlay.displayName 31 | 32 | const DialogContent = React.forwardRef< 33 | React.ElementRef, 34 | React.ComponentPropsWithoutRef 35 | >(({ className, children, ...props }, ref) => ( 36 | 37 | 38 | 46 | {children} 47 | 48 | 49 | Close 50 | 51 | 52 | 53 | )) 54 | DialogContent.displayName = DialogPrimitive.Content.displayName 55 | 56 | const DialogHeader = ({ 57 | className, 58 | ...props 59 | }: React.HTMLAttributes) => ( 60 |
67 | ) 68 | DialogHeader.displayName = "DialogHeader" 69 | 70 | const DialogFooter = ({ 71 | className, 72 | ...props 73 | }: React.HTMLAttributes) => ( 74 |
81 | ) 82 | DialogFooter.displayName = "DialogFooter" 83 | 84 | const DialogTitle = React.forwardRef< 85 | React.ElementRef, 86 | React.ComponentPropsWithoutRef 87 | >(({ className, ...props }, ref) => ( 88 | 96 | )) 97 | DialogTitle.displayName = DialogPrimitive.Title.displayName 98 | 99 | const DialogDescription = React.forwardRef< 100 | React.ElementRef, 101 | React.ComponentPropsWithoutRef 102 | >(({ className, ...props }, ref) => ( 103 | 108 | )) 109 | DialogDescription.displayName = DialogPrimitive.Description.displayName 110 | 111 | export { 112 | Dialog, 113 | DialogPortal, 114 | DialogOverlay, 115 | DialogClose, 116 | DialogTrigger, 117 | DialogContent, 118 | DialogHeader, 119 | DialogFooter, 120 | DialogTitle, 121 | DialogDescription, 122 | } 123 | -------------------------------------------------------------------------------- /src/components/ui/form.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import * as LabelPrimitive from "@radix-ui/react-label" 3 | import { Slot } from "@radix-ui/react-slot" 4 | import { 5 | Controller, 6 | ControllerProps, 7 | FieldPath, 8 | FieldValues, 9 | FormProvider, 10 | useFormContext, 11 | } from "react-hook-form" 12 | 13 | import { cn } from "@/lib/utils" 14 | import { Label } from "@/components/ui/label" 15 | 16 | const Form = FormProvider 17 | 18 | type FormFieldContextValue< 19 | TFieldValues extends FieldValues = FieldValues, 20 | TName extends FieldPath = FieldPath 21 | > = { 22 | name: TName 23 | } 24 | 25 | const FormFieldContext = React.createContext( 26 | {} as FormFieldContextValue 27 | ) 28 | 29 | const FormField = < 30 | TFieldValues extends FieldValues = FieldValues, 31 | TName extends FieldPath = FieldPath 32 | >({ 33 | ...props 34 | }: ControllerProps) => { 35 | return ( 36 | 37 | 38 | 39 | ) 40 | } 41 | 42 | const useFormField = () => { 43 | const fieldContext = React.useContext(FormFieldContext) 44 | const itemContext = React.useContext(FormItemContext) 45 | const { getFieldState, formState } = useFormContext() 46 | 47 | const fieldState = getFieldState(fieldContext.name, formState) 48 | 49 | if (!fieldContext) { 50 | throw new Error("useFormField should be used within ") 51 | } 52 | 53 | const { id } = itemContext 54 | 55 | return { 56 | id, 57 | name: fieldContext.name, 58 | formItemId: `${id}-form-item`, 59 | formDescriptionId: `${id}-form-item-description`, 60 | formMessageId: `${id}-form-item-message`, 61 | ...fieldState, 62 | } 63 | } 64 | 65 | type FormItemContextValue = { 66 | id: string 67 | } 68 | 69 | const FormItemContext = React.createContext( 70 | {} as FormItemContextValue 71 | ) 72 | 73 | const FormItem = React.forwardRef< 74 | HTMLDivElement, 75 | React.HTMLAttributes 76 | >(({ className, ...props }, ref) => { 77 | const id = React.useId() 78 | 79 | return ( 80 | 81 |
82 | 83 | ) 84 | }) 85 | FormItem.displayName = "FormItem" 86 | 87 | const FormLabel = React.forwardRef< 88 | React.ElementRef, 89 | React.ComponentPropsWithoutRef 90 | >(({ className, ...props }, ref) => { 91 | const { error, formItemId } = useFormField() 92 | 93 | return ( 94 |