├── .eslintrc.cjs ├── .github └── CODEOWNERS ├── .gitignore ├── .prettierignore ├── .prettierrc ├── LICENSE ├── README.md ├── components.json ├── index.html ├── netlify.toml ├── package-lock.json ├── package.json ├── pnpm-lock.yaml ├── postcss.config.js ├── public └── images │ ├── favicon.png │ ├── favicon.svg │ └── shadcn-admin.png ├── src ├── assets │ └── vite.svg ├── components │ ├── apiCall.tsx │ ├── app-shell.tsx │ ├── coming-soon.tsx │ ├── custom │ │ ├── breadcrumb.tsx │ │ ├── button.tsx │ │ ├── layout.tsx │ │ ├── password-input.tsx │ │ ├── pin-input-original.tsx │ │ └── pin-input.tsx │ ├── loader.tsx │ ├── nav.tsx │ ├── search.tsx │ ├── sidebar.tsx │ ├── theme-provider.tsx │ ├── theme-switch.tsx │ ├── top-nav.tsx │ ├── ui │ │ ├── avatar.tsx │ │ ├── badge.tsx │ │ ├── calendar.tsx │ │ ├── card.tsx │ │ ├── checkbox.tsx │ │ ├── collapsible.tsx │ │ ├── command.tsx │ │ ├── dialog.tsx │ │ ├── dropdown-menu.tsx │ │ ├── form.tsx │ │ ├── input.tsx │ │ ├── label.tsx │ │ ├── popover.tsx │ │ ├── radio-group.tsx │ │ ├── select.tsx │ │ ├── separator.tsx │ │ ├── switch.tsx │ │ ├── table.tsx │ │ ├── tabs.tsx │ │ ├── textarea.tsx │ │ ├── toast.tsx │ │ ├── toaster.tsx │ │ ├── tooltip.tsx │ │ └── use-toast.ts │ └── user-nav.tsx ├── config.tsx ├── data │ └── sidelinks.tsx ├── hooks │ ├── use-check-active-nav.tsx │ ├── use-is-collapsed.tsx │ └── use-local-storage.tsx ├── index.css ├── lib │ └── utils.ts ├── main.tsx ├── pages │ ├── apps │ │ ├── data.tsx │ │ └── index.tsx │ ├── auth │ │ ├── components │ │ │ ├── forgot-form.tsx │ │ │ ├── otp-form.tsx │ │ │ ├── sign-up-form.tsx │ │ │ └── user-auth-form.tsx │ │ ├── forgot-password.tsx │ │ ├── otp.tsx │ │ ├── sign-in-2.tsx │ │ ├── sign-in.tsx │ │ └── sign-up.tsx │ ├── dashboard │ │ ├── components │ │ │ ├── overview.tsx │ │ │ └── recent-sales.tsx │ │ └── index.tsx │ ├── errors │ │ ├── general-error.tsx │ │ ├── maintenance-error.tsx │ │ └── not-found-error.tsx │ ├── extra-components │ │ └── index.tsx │ ├── settings │ │ ├── account │ │ │ ├── account-form.tsx │ │ │ └── index.tsx │ │ ├── appearance │ │ │ ├── appearance-form.tsx │ │ │ └── index.tsx │ │ ├── components │ │ │ └── sidebar-nav.tsx │ │ ├── display │ │ │ ├── display-form.tsx │ │ │ └── index.tsx │ │ ├── error-example │ │ │ └── index.tsx │ │ ├── index.tsx │ │ ├── notifications │ │ │ ├── index.tsx │ │ │ └── notifications-form.tsx │ │ └── profile │ │ │ ├── index.tsx │ │ │ └── profile-form.tsx │ └── tasks │ │ ├── components │ │ ├── columns.tsx │ │ ├── data-table-column-header.tsx │ │ ├── data-table-faceted-filter.tsx │ │ ├── data-table-pagination.tsx │ │ ├── data-table-row-actions.tsx │ │ ├── data-table-toolbar.tsx │ │ ├── data-table-view-options.tsx │ │ └── data-table.tsx │ │ ├── data │ │ ├── data.tsx │ │ ├── schema.ts │ │ └── tasks.ts │ │ └── index.tsx ├── redux │ ├── actions │ │ └── signIn.tsx │ ├── reduces │ │ ├── index.tsx │ │ └── signIn.tsx │ └── store.tsx ├── router.tsx └── vite-env.d.ts ├── tailwind.config.js ├── tsconfig.json ├── tsconfig.node.json └── vite.config.ts /.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { browser: true, es2020: true }, 4 | extends: [ 5 | 'eslint:recommended', 6 | 'plugin:@typescript-eslint/recommended', 7 | 'plugin:react-hooks/recommended', 8 | ], 9 | ignorePatterns: ['dist', '.eslintrc.cjs', 'src/components/ui'], 10 | parser: '@typescript-eslint/parser', 11 | plugins: ['react-refresh'], 12 | rules: { 13 | 'react-refresh/only-export-components': [ 14 | 'warn', 15 | { allowConstantExport: true }, 16 | ], 17 | }, 18 | } 19 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | ## PR approval owners 2 | * @leremitt/approvers -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | # Ignore everything 2 | /* 3 | 4 | # Except these files & folders 5 | !/src 6 | !index.html 7 | !package.json 8 | !tsconfig.json 9 | !tsconfig.node.json 10 | !vite.config.ts 11 | !.prettierrc 12 | !README.md 13 | !CHANGELOG.md 14 | !.eslintrc.cjs 15 | !postcss.config.js -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "arrowParens": "always", 3 | "semi": false, 4 | "tabWidth": 2, 5 | "printWidth": 80, 6 | "singleQuote": true, 7 | "jsxSingleQuote": true, 8 | "trailingComma": "es5", 9 | "bracketSpacing": true, 10 | "endOfLine": "lf", 11 | "plugins": ["prettier-plugin-tailwindcss"] 12 | } 13 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Sat Naing 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 | Install dependencies 2 | 3 | ```bash 4 | npm install 5 | ``` 6 | 7 | Start the server 8 | 9 | ```bash 10 | npm run dev 11 | ``` 12 | -------------------------------------------------------------------------------- /components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema.json", 3 | "style": "new-york", 4 | "rsc": false, 5 | "tsx": true, 6 | "tailwind": { 7 | "config": "tailwind.config.js", 8 | "css": "src/index.css", 9 | "baseColor": "slate", 10 | "cssVariables": true, 11 | "prefix": "" 12 | }, 13 | "aliases": { 14 | "components": "@/components", 15 | "utils": "@/lib/utils" 16 | } 17 | } -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | Template 11 | 12 | 16 | 17 | 18 | 19 | 20 | 21 | 25 | 29 | 30 | 31 | 32 | 33 | 34 | 38 | 42 | 43 | 44 | 45 | 46 |
47 | 48 | 49 | 50 | -------------------------------------------------------------------------------- /netlify.toml: -------------------------------------------------------------------------------- 1 | [[redirects]] 2 | from = "/*" 3 | to = "/index.html" 4 | status = 200 -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "template", 3 | "private": true, 4 | "version": "1.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "tsc && vite build", 9 | "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0", 10 | "preview": "vite preview", 11 | "format:check": "prettier --check .", 12 | "format": "prettier --write ." 13 | }, 14 | "dependencies": { 15 | "@hookform/resolvers": "^3.3.4", 16 | "@radix-ui/react-avatar": "^1.0.4", 17 | "@radix-ui/react-checkbox": "^1.0.4", 18 | "@radix-ui/react-collapsible": "^1.0.3", 19 | "@radix-ui/react-dialog": "^1.0.5", 20 | "@radix-ui/react-dropdown-menu": "^2.0.6", 21 | "@radix-ui/react-icons": "^1.3.0", 22 | "@radix-ui/react-label": "^2.0.2", 23 | "@radix-ui/react-popover": "^1.0.7", 24 | "@radix-ui/react-radio-group": "^1.1.3", 25 | "@radix-ui/react-select": "^2.0.0", 26 | "@radix-ui/react-separator": "^1.0.3", 27 | "@radix-ui/react-slot": "^1.0.2", 28 | "@radix-ui/react-switch": "^1.0.3", 29 | "@radix-ui/react-tabs": "^1.0.4", 30 | "@radix-ui/react-toast": "^1.1.5", 31 | "@radix-ui/react-tooltip": "^1.0.7", 32 | "@reduxjs/toolkit": "^2.2.5", 33 | "@tabler/icons-react": "^3.2.0", 34 | "@tanstack/react-table": "^8.16.0", 35 | "axios": "^1.6.8", 36 | "class-variance-authority": "^0.7.0", 37 | "clsx": "^2.1.0", 38 | "cmdk": "^0.2.1", 39 | "dayjs": "^1.11.10", 40 | "react": "^18.2.0", 41 | "react-day-picker": "^8.10.1", 42 | "react-dom": "^18.2.0", 43 | "react-hook-form": "^7.51.3", 44 | "react-redux": "^9.1.2", 45 | "react-router-dom": "^6.22.3", 46 | "react-syntax-highlighter": "^15.5.0", 47 | "recharts": "^2.12.5", 48 | "redux-persist": "^6.0.0", 49 | "tailwind-merge": "^2.2.2", 50 | "tailwindcss-animate": "^1.0.7", 51 | "zod": "^3.22.4" 52 | }, 53 | "devDependencies": { 54 | "@types/node": "^20.12.7", 55 | "@types/react": "^18.2.79", 56 | "@types/react-dom": "^18.2.25", 57 | "@types/react-syntax-highlighter": "^15.5.11", 58 | "@typescript-eslint/eslint-plugin": "^6.14.0", 59 | "@typescript-eslint/parser": "^6.14.0", 60 | "@vitejs/plugin-react-swc": "^3.6.0", 61 | "autoprefixer": "^10.4.19", 62 | "eslint": "^8.55.0", 63 | "eslint-plugin-react-hooks": "^4.6.0", 64 | "eslint-plugin-react-refresh": "^0.4.5", 65 | "postcss": "^8.4.38", 66 | "prettier": "^3.2.5", 67 | "prettier-plugin-tailwindcss": "^0.5.14", 68 | "tailwindcss": "^3.4.3", 69 | "typescript": "^5.4.5", 70 | "vite": "^5.2.9" 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /public/images/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Busipalli-Hemadri/Boilerplate-UI/e63eca097d9c166c3efd1c12174fe98270b15b27/public/images/favicon.png -------------------------------------------------------------------------------- /public/images/favicon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /public/images/shadcn-admin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Busipalli-Hemadri/Boilerplate-UI/e63eca097d9c166c3efd1c12174fe98270b15b27/public/images/shadcn-admin.png -------------------------------------------------------------------------------- /src/assets/vite.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /src/components/apiCall.tsx: -------------------------------------------------------------------------------- 1 | import axios, { AxiosError } from 'axios'; 2 | import { toast } from '@/components/ui/use-toast' 3 | 4 | export async function Post(url: string, data: object, navigate: any) { 5 | try { 6 | const response = await axios({ method: 'POST', url: url, data: data, headers: { 'Authorization': 'Barer ' + sessionStorage.getItem('token'), Origin: 'http://localhost:3000' } }) 7 | return response 8 | } 9 | catch (error: any) { 10 | const err = error as AxiosError 11 | if (err.response) { 12 | if (err.response.status < 200 || err.response.status >= 300) { 13 | if (err.response.status === 403 || err.response.status === 401) { 14 | navigate('/sign-in') 15 | toast({ 16 | variant: "destructive", 17 | title: "Please Login Again", 18 | }); 19 | 20 | sessionStorage.removeItem('token') 21 | } 22 | else if(err.response.status === 422) { 23 | return null 24 | } 25 | else { 26 | toast({ 27 | variant: "destructive", 28 | title: "Please Try Again", 29 | }); 30 | } 31 | } 32 | } 33 | else { 34 | toast({ 35 | variant: "destructive", 36 | title: "Please Check Your Network", 37 | }) 38 | } 39 | return null 40 | } 41 | }; 42 | 43 | export async function Get(url: string, id: string, navigate: any) { 44 | try { 45 | const response = await axios({ method: 'GET', url: url + id, headers: { 'Authorization': 'Barer ' + sessionStorage.getItem('token'), Origin: 'http://localhost:3000' } }) 46 | return response 47 | } 48 | catch (error: any) { 49 | const err = error as AxiosError 50 | if (err.response) { 51 | if (err.response.status < 200 || err.response.status >= 300) { 52 | if (err.response.status === 403 || err.response.status === 401) { 53 | navigate('/sign-in') 54 | toast({ 55 | variant: "destructive", 56 | title: "Please Login Again", 57 | }); 58 | sessionStorage.removeItem('token') 59 | } 60 | else if(err.response.status === 422) { 61 | return null 62 | } 63 | else { 64 | toast({ 65 | variant: "destructive", 66 | title: "Please Try Again", 67 | }); 68 | } 69 | } 70 | } 71 | else { 72 | toast({ 73 | variant: "destructive", 74 | title: "Please Check Your Network", 75 | }) 76 | } 77 | return null 78 | } 79 | }; 80 | 81 | export async function GetAll(url: string, navigate: any) { 82 | 83 | try { 84 | const response = await axios({ method: 'GET', url: url, headers: { 'Authorization': 'Barer ' + sessionStorage.getItem('token'), Origin: 'http://localhost:3000' } }) 85 | return response 86 | } 87 | catch (error: any) { 88 | const err = error as AxiosError 89 | if (err.response) { 90 | if (err.response.status < 200 || err.response.status >= 300) { 91 | if (err.response.status === 403 || err.response.status === 401) { 92 | navigate('/sign-in') 93 | toast({ 94 | variant: "destructive", 95 | title: "Please Login Again", 96 | }); 97 | 98 | sessionStorage.removeItem('token') 99 | } 100 | else if(err.response.status === 422) { 101 | return null 102 | } 103 | else { 104 | toast({ 105 | variant: "destructive", 106 | title: "Please Try Again", 107 | }); 108 | } 109 | } 110 | } 111 | else { 112 | toast({ 113 | variant: "destructive", 114 | title: "Please Check Your Network", 115 | }) 116 | } 117 | return null 118 | } 119 | }; 120 | 121 | export async function Put(url: string, id: string, data: object, navigate: any) { 122 | try { 123 | const response = await axios({ method: 'put', url: url + id, data: data, headers: { 'Authorization': 'Barer ' + sessionStorage.getItem('token'), Origin: 'http://localhost:3000' } }) 124 | return response 125 | } 126 | catch (error: any) { 127 | const err = error as AxiosError 128 | if (err.response) { 129 | if (err.response.status < 200 || err.response.status >= 300) { 130 | if (err.response.status === 403 || err.response.status === 401) { 131 | navigate('/sign-in') 132 | toast({ 133 | variant: "destructive", 134 | title: "Please Login Again", 135 | }); 136 | sessionStorage.removeItem('token') 137 | } 138 | else if(err.response.status === 422) { 139 | return null 140 | } 141 | else { 142 | toast({ 143 | variant: "destructive", 144 | title: "Please Try Again", 145 | }); 146 | } 147 | } 148 | } 149 | else { 150 | toast({ 151 | variant: "destructive", 152 | title: "Please Check Your Network", 153 | }) 154 | } 155 | return null 156 | } 157 | }; 158 | 159 | export async function Delete(url: string, id: string, navigate: any) { 160 | 161 | try { 162 | const response = await axios({ method: 'delete', url: url + id, headers: { 'Authorization': 'Barer ' + sessionStorage.getItem('token'), Origin: 'http://localhost:3000' } }) 163 | return response 164 | } 165 | catch (error: any) { 166 | const err = error as AxiosError 167 | if (err.response) { 168 | if (err.response.status < 200 || err.response.status >= 300) { 169 | if (err.response.status === 403 || err.response.status === 401) { 170 | navigate('/sign-in') 171 | toast({ 172 | variant: "destructive", 173 | title: "Please Login Again", 174 | }); 175 | sessionStorage.removeItem('token') 176 | } 177 | else if(err.response.status === 422) { 178 | return null 179 | } 180 | else { 181 | toast({ 182 | variant: "destructive", 183 | title: "Please Try Again", 184 | }); 185 | } 186 | } 187 | } 188 | else { 189 | toast({ 190 | variant: "destructive", 191 | title: "Please Check Your Network", 192 | }) 193 | } 194 | return null 195 | } 196 | }; 197 | -------------------------------------------------------------------------------- /src/components/app-shell.tsx: -------------------------------------------------------------------------------- 1 | import { Outlet } from 'react-router-dom' 2 | import Sidebar from './sidebar' 3 | import useIsCollapsed from '@/hooks/use-is-collapsed' 4 | 5 | export default function AppShell() { 6 | const [isCollapsed, setIsCollapsed] = useIsCollapsed() 7 | return ( 8 |
9 | 10 |
14 | 15 |
16 |
17 | ) 18 | } 19 | -------------------------------------------------------------------------------- /src/components/coming-soon.tsx: -------------------------------------------------------------------------------- 1 | import { IconPlanet } from '@tabler/icons-react' 2 | 3 | export default function ComingSoon() { 4 | return ( 5 |
6 |
7 | 8 |

Coming Soon 👀

9 |

10 | This page has not been created yet.
11 | Stay tuned though! 12 |

13 |
14 |
15 | ) 16 | } 17 | -------------------------------------------------------------------------------- /src/components/custom/breadcrumb.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import { cn } from '@/lib/utils' 3 | 4 | interface BreadcrumbProps extends React.ComponentPropsWithoutRef<'nav'> { 5 | children: 6 | | React.ReactElement 7 | | React.ReactElement[] 8 | separator?: React.ReactNode 9 | } 10 | 11 | const BreadcrumbContext = React.createContext(false) 12 | 13 | const Breadcrumb = React.forwardRef( 14 | ({ className, children, separator, ...props }, ref) => { 15 | const validChildren = getValidChildren(children) 16 | 17 | const count = validChildren.length 18 | 19 | const clones = validChildren.map((child, index) => 20 | React.cloneElement(child, { 21 | separator, 22 | isLastChild: count === index + 1, 23 | }) 24 | ) 25 | 26 | return ( 27 | 28 | 31 | 32 | ) 33 | } 34 | ) 35 | Breadcrumb.displayName = 'Breadcrumb' 36 | 37 | interface InternalBreadcrumbItemProps { 38 | separator?: React.ReactNode 39 | isLastChild: boolean 40 | } 41 | 42 | interface BreadcrumbItemProps 43 | extends Omit< 44 | React.ComponentPropsWithoutRef<'li'>, 45 | keyof InternalBreadcrumbItemProps 46 | > {} 47 | 48 | const BreadcrumbItem = React.forwardRef( 49 | ({ className, children, ...props }, ref) => { 50 | const { separator, isLastChild, ...rest } = 51 | props as InternalBreadcrumbItemProps 52 | 53 | // Check if BreadcrumbItem is used within Breadcrumb 54 | const isInsideBreadcrumb = React.useContext(BreadcrumbContext) 55 | if (!isInsideBreadcrumb) { 56 | throw new Error( 57 | `${BreadcrumbItem.displayName} must be used within ${Breadcrumb.displayName}.` 58 | ) 59 | } 60 | 61 | return ( 62 |
  • 63 | {children} 64 | {!isLastChild && ( 65 | {separator ?? '/'} 66 | )} 67 |
  • 68 | ) 69 | } 70 | ) 71 | BreadcrumbItem.displayName = 'BreadcrumbItem' 72 | 73 | /* ========== Util Func ========== */ 74 | 75 | const getValidChildren = (children: React.ReactNode) => 76 | React.Children.toArray(children).filter((child) => { 77 | if (React.isValidElement(child) && child.type === BreadcrumbItem) { 78 | return React.isValidElement(child) 79 | } 80 | throw new Error( 81 | `${Breadcrumb.displayName} can only have ${BreadcrumbItem.displayName} as children.` 82 | ) 83 | }) as React.ReactElement[] 84 | 85 | export { Breadcrumb, BreadcrumbItem } 86 | -------------------------------------------------------------------------------- /src/components/custom/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 | import { IconLoader2 } from '@tabler/icons-react' 7 | 8 | const buttonVariants = cva( 9 | 'inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50', 10 | { 11 | variants: { 12 | variant: { 13 | default: 14 | 'bg-primary text-primary-foreground shadow hover:bg-primary/90', 15 | destructive: 16 | 'bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90', 17 | outline: 18 | 'border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground', 19 | secondary: 20 | 'bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80', 21 | ghost: 'hover:bg-accent hover:text-accent-foreground', 22 | link: 'text-primary underline-offset-4 hover:underline', 23 | }, 24 | size: { 25 | default: 'h-9 px-4 py-2', 26 | sm: 'h-8 rounded-md px-3 text-xs', 27 | lg: 'h-10 rounded-md px-8', 28 | icon: 'h-9 w-9', 29 | }, 30 | }, 31 | defaultVariants: { 32 | variant: 'default', 33 | size: 'default', 34 | }, 35 | } 36 | ) 37 | 38 | export interface ButtonProps 39 | extends React.ButtonHTMLAttributes, 40 | VariantProps { 41 | asChild?: boolean 42 | loading?: boolean 43 | leftSection?: JSX.Element 44 | rightSection?: JSX.Element 45 | } 46 | 47 | const Button = React.forwardRef( 48 | ( 49 | { 50 | className, 51 | variant, 52 | size, 53 | asChild = false, 54 | children, 55 | disabled, 56 | loading = false, 57 | leftSection, 58 | rightSection, 59 | ...props 60 | }, 61 | ref 62 | ) => { 63 | const Comp = asChild ? Slot : 'button' 64 | return ( 65 | 71 | {((leftSection && loading) || 72 | (!leftSection && !rightSection && loading)) && ( 73 | 74 | )} 75 | {!loading && leftSection &&
    {leftSection}
    } 76 | {children} 77 | {!loading && rightSection &&
    {rightSection}
    } 78 | {rightSection && loading && ( 79 | 80 | )} 81 |
    82 | ) 83 | } 84 | ) 85 | Button.displayName = 'Button' 86 | 87 | // eslint-disable-next-line react-refresh/only-export-components 88 | export { Button, buttonVariants } 89 | -------------------------------------------------------------------------------- /src/components/custom/layout.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import { cn } from '@/lib/utils' 3 | 4 | interface LayoutProps extends React.HTMLAttributes { 5 | fadedBelow?: boolean 6 | fixedHeight?: boolean 7 | } 8 | 9 | const Layout = React.forwardRef( 10 | ({ className, fadedBelow = false, fixedHeight = false, ...props }, ref) => ( 11 |
    22 | ) 23 | ) 24 | Layout.displayName = 'Layout' 25 | 26 | const LayoutHeader = React.forwardRef< 27 | HTMLDivElement, 28 | React.HTMLAttributes 29 | >(({ className, ...props }, ref) => ( 30 |
    38 | )) 39 | LayoutHeader.displayName = 'LayoutHeader' 40 | 41 | interface LayoutBodyProps extends React.HTMLAttributes { 42 | fixedHeight?: boolean 43 | } 44 | 45 | const LayoutBody = React.forwardRef( 46 | ({ className, fixedHeight, ...props }, ref) => ( 47 |
    56 | ) 57 | ) 58 | LayoutBody.displayName = 'LayoutBody' 59 | 60 | export { Layout, LayoutHeader, LayoutBody } 61 | -------------------------------------------------------------------------------- /src/components/custom/password-input.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import { IconEye, IconEyeOff } from '@tabler/icons-react' 3 | import { Button } from './button' 4 | import { cn } from '@/lib/utils' 5 | 6 | export interface PasswordInputProps 7 | extends Omit, 'type'> {} 8 | 9 | const PasswordInput = React.forwardRef( 10 | ({ className, ...props }, ref) => { 11 | const [showPassword, setShowPassword] = React.useState(false) 12 | return ( 13 |
    14 | 23 | 32 |
    33 | ) 34 | } 35 | ) 36 | PasswordInput.displayName = 'PasswordInput' 37 | 38 | export { PasswordInput } 39 | -------------------------------------------------------------------------------- /src/components/loader.tsx: -------------------------------------------------------------------------------- 1 | import { IconLoader } from '@tabler/icons-react' 2 | 3 | export default function Loader() { 4 | return ( 5 |
    6 | 7 | loading 8 |
    9 | ) 10 | } 11 | -------------------------------------------------------------------------------- /src/components/search.tsx: -------------------------------------------------------------------------------- 1 | import { Input } from '@/components/ui/input' 2 | 3 | export function Search() { 4 | return ( 5 |
    6 | 11 |
    12 | ) 13 | } 14 | -------------------------------------------------------------------------------- /src/components/sidebar.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'react' 2 | import { IconChevronsLeft, IconMenu2, IconX } from '@tabler/icons-react' 3 | import { Layout, LayoutHeader } from './custom/layout' 4 | import { Button } from './custom/button' 5 | import Nav from './nav' 6 | import { cn } from '@/lib/utils' 7 | import { sidelinks } from '@/data/sidelinks' 8 | 9 | interface SidebarProps extends React.HTMLAttributes { 10 | isCollapsed: boolean 11 | setIsCollapsed: React.Dispatch> 12 | } 13 | 14 | export default function Sidebar2({ 15 | className, 16 | isCollapsed, 17 | setIsCollapsed, 18 | }: SidebarProps) { 19 | const [navOpened, setNavOpened] = useState(false) 20 | 21 | /* Make body not scrollable when navBar is opened */ 22 | useEffect(() => { 23 | if (navOpened) { 24 | document.body.classList.add('overflow-hidden') 25 | } else { 26 | document.body.classList.remove('overflow-hidden') 27 | } 28 | }, [navOpened]) 29 | 30 | return ( 31 | 122 | ) 123 | } 124 | -------------------------------------------------------------------------------- /src/components/theme-provider.tsx: -------------------------------------------------------------------------------- 1 | import { createContext, useContext, useEffect, useState } from 'react' 2 | 3 | type Theme = 'dark' | 'light' | 'system' 4 | 5 | type ThemeProviderProps = { 6 | children: React.ReactNode 7 | defaultTheme?: Theme 8 | storageKey?: string 9 | } 10 | 11 | type ThemeProviderState = { 12 | theme: Theme 13 | setTheme: (theme: Theme) => void 14 | } 15 | 16 | const initialState: ThemeProviderState = { 17 | theme: 'system', 18 | setTheme: () => null, 19 | } 20 | 21 | const ThemeProviderContext = createContext(initialState) 22 | 23 | export function ThemeProvider({ 24 | children, 25 | defaultTheme = 'system', 26 | storageKey = 'vite-ui-theme', 27 | ...props 28 | }: ThemeProviderProps) { 29 | const [theme, setTheme] = useState( 30 | () => (localStorage.getItem(storageKey) as Theme) || defaultTheme 31 | ) 32 | 33 | useEffect(() => { 34 | const root = window.document.documentElement 35 | 36 | root.classList.remove('light', 'dark') 37 | 38 | if (theme === 'system') { 39 | const systemTheme = window.matchMedia('(prefers-color-scheme: dark)') 40 | .matches 41 | ? 'dark' 42 | : 'light' 43 | 44 | root.classList.add(systemTheme) 45 | return 46 | } 47 | 48 | root.classList.add(theme) 49 | }, [theme]) 50 | 51 | const value = { 52 | theme, 53 | setTheme: (theme: Theme) => { 54 | localStorage.setItem(storageKey, theme) 55 | setTheme(theme) 56 | }, 57 | } 58 | 59 | return ( 60 | 61 | {children} 62 | 63 | ) 64 | } 65 | 66 | // eslint-disable-next-line react-refresh/only-export-components 67 | export const useTheme = () => { 68 | const context = useContext(ThemeProviderContext) 69 | 70 | if (context === undefined) 71 | throw new Error('useTheme must be used within a ThemeProvider') 72 | 73 | return context 74 | } 75 | -------------------------------------------------------------------------------- /src/components/theme-switch.tsx: -------------------------------------------------------------------------------- 1 | import { IconMoon, IconSun } from '@tabler/icons-react' 2 | import { useTheme } from './theme-provider' 3 | import { Button } from './custom/button' 4 | import { useEffect } from 'react' 5 | 6 | export default function ThemeSwitch() { 7 | const { theme, setTheme } = useTheme() 8 | 9 | /* Update theme-color meta tag 10 | * when theme is updated */ 11 | useEffect(() => { 12 | const themeColor = theme === 'dark' ? '#020817' : '#fff' 13 | const metaThemeColor = document.querySelector("meta[name='theme-color']") 14 | metaThemeColor && metaThemeColor.setAttribute('content', themeColor) 15 | }, [theme]) 16 | 17 | return ( 18 | 26 | ) 27 | } 28 | -------------------------------------------------------------------------------- /src/components/top-nav.tsx: -------------------------------------------------------------------------------- 1 | import { cn } from '@/lib/utils' 2 | import { Link } from 'react-router-dom' 3 | import { 4 | DropdownMenu, 5 | DropdownMenuContent, 6 | DropdownMenuItem, 7 | DropdownMenuTrigger, 8 | } from '@/components/ui/dropdown-menu' 9 | import { Button } from './custom/button' 10 | import { IconMenu } from '@tabler/icons-react' 11 | 12 | interface TopNavProps extends React.HTMLAttributes { 13 | links: { 14 | title: string 15 | href: string 16 | isActive: boolean 17 | }[] 18 | } 19 | 20 | export function TopNav({ className, links, ...props }: TopNavProps) { 21 | return ( 22 | <> 23 |
    24 | 25 | 26 | 29 | 30 | 31 | {links.map(({ title, href, isActive }) => ( 32 | 33 | 37 | {title} 38 | 39 | 40 | ))} 41 | 42 | 43 |
    44 | 45 | 62 | 63 | ) 64 | } 65 | -------------------------------------------------------------------------------- /src/components/ui/avatar.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import * as AvatarPrimitive from "@radix-ui/react-avatar" 3 | 4 | import { cn } from "@/lib/utils" 5 | 6 | const Avatar = React.forwardRef< 7 | React.ElementRef, 8 | React.ComponentPropsWithoutRef 9 | >(({ className, ...props }, ref) => ( 10 | 18 | )) 19 | Avatar.displayName = AvatarPrimitive.Root.displayName 20 | 21 | const AvatarImage = React.forwardRef< 22 | React.ElementRef, 23 | React.ComponentPropsWithoutRef 24 | >(({ className, ...props }, ref) => ( 25 | 30 | )) 31 | AvatarImage.displayName = AvatarPrimitive.Image.displayName 32 | 33 | const AvatarFallback = React.forwardRef< 34 | React.ElementRef, 35 | React.ComponentPropsWithoutRef 36 | >(({ className, ...props }, ref) => ( 37 | 45 | )) 46 | AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName 47 | 48 | export { Avatar, AvatarImage, AvatarFallback } 49 | -------------------------------------------------------------------------------- /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-md 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 shadow 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 shadow 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/calendar.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import { ChevronLeftIcon, ChevronRightIcon } from '@radix-ui/react-icons' 3 | import { DayPicker } from 'react-day-picker' 4 | 5 | import { cn } from '@/lib/utils' 6 | import { buttonVariants } from '@/components/custom/button' 7 | 8 | export type CalendarProps = React.ComponentProps 9 | 10 | function Calendar({ 11 | className, 12 | classNames, 13 | showOutsideDays = true, 14 | ...props 15 | }: CalendarProps) { 16 | return ( 17 | .day-range-end)]:rounded-r-md [&:has(>.day-range-start)]:rounded-l-md first:[&:has([aria-selected])]:rounded-l-md last:[&:has([aria-selected])]:rounded-r-md' 41 | : '[&:has([aria-selected])]:rounded-md' 42 | ), 43 | day: cn( 44 | buttonVariants({ variant: 'ghost' }), 45 | 'h-8 w-8 p-0 font-normal aria-selected:opacity-100' 46 | ), 47 | day_range_start: 'day-range-start', 48 | day_range_end: 'day-range-end', 49 | day_selected: 50 | 'bg-primary text-primary-foreground hover:bg-primary hover:text-primary-foreground focus:bg-primary focus:text-primary-foreground', 51 | day_today: 'bg-accent text-accent-foreground', 52 | day_outside: 53 | 'day-outside text-muted-foreground opacity-50 aria-selected:bg-accent/50 aria-selected:text-muted-foreground aria-selected:opacity-30', 54 | day_disabled: 'text-muted-foreground opacity-50', 55 | day_range_middle: 56 | 'aria-selected:bg-accent aria-selected:text-accent-foreground', 57 | day_hidden: 'invisible', 58 | ...classNames, 59 | }} 60 | components={{ 61 | IconLeft: () => , 62 | IconRight: () => , 63 | }} 64 | {...props} 65 | /> 66 | ) 67 | } 68 | Calendar.displayName = 'Calendar' 69 | 70 | export { Calendar } 71 | -------------------------------------------------------------------------------- /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 |

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

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

    61 | )) 62 | CardContent.displayName = "CardContent" 63 | 64 | const CardFooter = React.forwardRef< 65 | HTMLDivElement, 66 | React.HTMLAttributes 67 | >(({ className, ...props }, ref) => ( 68 |
    73 | )) 74 | CardFooter.displayName = "CardFooter" 75 | 76 | export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent } 77 | -------------------------------------------------------------------------------- /src/components/ui/checkbox.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import * as CheckboxPrimitive from "@radix-ui/react-checkbox" 3 | import { CheckIcon } from "@radix-ui/react-icons" 4 | 5 | import { cn } from "@/lib/utils" 6 | 7 | const Checkbox = React.forwardRef< 8 | React.ElementRef, 9 | React.ComponentPropsWithoutRef 10 | >(({ className, ...props }, ref) => ( 11 | 19 | 22 | 23 | 24 | 25 | )) 26 | Checkbox.displayName = CheckboxPrimitive.Root.displayName 27 | 28 | export { Checkbox } 29 | -------------------------------------------------------------------------------- /src/components/ui/collapsible.tsx: -------------------------------------------------------------------------------- 1 | import * as CollapsiblePrimitive from "@radix-ui/react-collapsible" 2 | 3 | const Collapsible = CollapsiblePrimitive.Root 4 | 5 | const CollapsibleTrigger = CollapsiblePrimitive.CollapsibleTrigger 6 | 7 | const CollapsibleContent = CollapsiblePrimitive.CollapsibleContent 8 | 9 | export { Collapsible, CollapsibleTrigger, CollapsibleContent } 10 | -------------------------------------------------------------------------------- /src/components/ui/command.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import { type DialogProps } from "@radix-ui/react-dialog" 3 | import { MagnifyingGlassIcon } from "@radix-ui/react-icons" 4 | import { Command as CommandPrimitive } from "cmdk" 5 | 6 | import { cn } from "@/lib/utils" 7 | import { Dialog, DialogContent } from "@/components/ui/dialog" 8 | 9 | const Command = React.forwardRef< 10 | React.ElementRef, 11 | React.ComponentPropsWithoutRef 12 | >(({ className, ...props }, ref) => ( 13 | 21 | )) 22 | Command.displayName = CommandPrimitive.displayName 23 | 24 | interface CommandDialogProps extends DialogProps {} 25 | 26 | const CommandDialog = ({ children, ...props }: CommandDialogProps) => { 27 | return ( 28 | 29 | 30 | 31 | {children} 32 | 33 | 34 | 35 | ) 36 | } 37 | 38 | const CommandInput = React.forwardRef< 39 | React.ElementRef, 40 | React.ComponentPropsWithoutRef 41 | >(({ className, ...props }, ref) => ( 42 |
    43 | 44 | 52 |
    53 | )) 54 | 55 | CommandInput.displayName = CommandPrimitive.Input.displayName 56 | 57 | const CommandList = React.forwardRef< 58 | React.ElementRef, 59 | React.ComponentPropsWithoutRef 60 | >(({ className, ...props }, ref) => ( 61 | 66 | )) 67 | 68 | CommandList.displayName = CommandPrimitive.List.displayName 69 | 70 | const CommandEmpty = React.forwardRef< 71 | React.ElementRef, 72 | React.ComponentPropsWithoutRef 73 | >((props, ref) => ( 74 | 79 | )) 80 | 81 | CommandEmpty.displayName = CommandPrimitive.Empty.displayName 82 | 83 | const CommandGroup = React.forwardRef< 84 | React.ElementRef, 85 | React.ComponentPropsWithoutRef 86 | >(({ className, ...props }, ref) => ( 87 | 95 | )) 96 | 97 | CommandGroup.displayName = CommandPrimitive.Group.displayName 98 | 99 | const CommandSeparator = React.forwardRef< 100 | React.ElementRef, 101 | React.ComponentPropsWithoutRef 102 | >(({ className, ...props }, ref) => ( 103 | 108 | )) 109 | CommandSeparator.displayName = CommandPrimitive.Separator.displayName 110 | 111 | const CommandItem = React.forwardRef< 112 | React.ElementRef, 113 | React.ComponentPropsWithoutRef 114 | >(({ className, ...props }, ref) => ( 115 | 123 | )) 124 | 125 | CommandItem.displayName = CommandPrimitive.Item.displayName 126 | 127 | const CommandShortcut = ({ 128 | className, 129 | ...props 130 | }: React.HTMLAttributes) => { 131 | return ( 132 | 139 | ) 140 | } 141 | CommandShortcut.displayName = "CommandShortcut" 142 | 143 | export { 144 | Command, 145 | CommandDialog, 146 | CommandInput, 147 | CommandList, 148 | CommandEmpty, 149 | CommandGroup, 150 | CommandItem, 151 | CommandShortcut, 152 | CommandSeparator, 153 | } 154 | -------------------------------------------------------------------------------- /src/components/ui/dialog.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import * as DialogPrimitive from "@radix-ui/react-dialog" 3 | import { Cross2Icon } from "@radix-ui/react-icons" 4 | 5 | import { cn } from "@/lib/utils" 6 | 7 | const Dialog = DialogPrimitive.Root 8 | 9 | const DialogTrigger = DialogPrimitive.Trigger 10 | 11 | const DialogPortal = DialogPrimitive.Portal 12 | 13 | const DialogClose = DialogPrimitive.Close 14 | 15 | const DialogOverlay = React.forwardRef< 16 | React.ElementRef, 17 | React.ComponentPropsWithoutRef 18 | >(({ className, ...props }, ref) => ( 19 | 27 | )) 28 | DialogOverlay.displayName = DialogPrimitive.Overlay.displayName 29 | 30 | const DialogContent = React.forwardRef< 31 | React.ElementRef, 32 | React.ComponentPropsWithoutRef 33 | >(({ className, children, ...props }, ref) => ( 34 | 35 | 36 | 44 | {children} 45 | 46 | 47 | Close 48 | 49 | 50 | 51 | )) 52 | DialogContent.displayName = DialogPrimitive.Content.displayName 53 | 54 | const DialogHeader = ({ 55 | className, 56 | ...props 57 | }: React.HTMLAttributes) => ( 58 |
    65 | ) 66 | DialogHeader.displayName = "DialogHeader" 67 | 68 | const DialogFooter = ({ 69 | className, 70 | ...props 71 | }: React.HTMLAttributes) => ( 72 |
    79 | ) 80 | DialogFooter.displayName = "DialogFooter" 81 | 82 | const DialogTitle = React.forwardRef< 83 | React.ElementRef, 84 | React.ComponentPropsWithoutRef 85 | >(({ className, ...props }, ref) => ( 86 | 94 | )) 95 | DialogTitle.displayName = DialogPrimitive.Title.displayName 96 | 97 | const DialogDescription = React.forwardRef< 98 | React.ElementRef, 99 | React.ComponentPropsWithoutRef 100 | >(({ className, ...props }, ref) => ( 101 | 106 | )) 107 | DialogDescription.displayName = DialogPrimitive.Description.displayName 108 | 109 | export { 110 | Dialog, 111 | DialogPortal, 112 | DialogOverlay, 113 | DialogTrigger, 114 | DialogClose, 115 | DialogContent, 116 | DialogHeader, 117 | DialogFooter, 118 | DialogTitle, 119 | DialogDescription, 120 | } 121 | -------------------------------------------------------------------------------- /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 |