├── .eslintrc.cjs ├── .gitignore ├── .husky └── pre-commit ├── .prettierignore ├── .prettierrc ├── LICENSE ├── README.md ├── components.json ├── index.html ├── package-lock.json ├── package.json ├── postcss.config.js ├── public └── vite.svg ├── src ├── App.tsx ├── assets │ └── react.svg ├── components │ ├── layout │ │ └── dashboard-layout.tsx │ ├── shared │ │ ├── alert-modal.tsx │ │ ├── breadcrumbs.tsx │ │ ├── dashboard-nav.tsx │ │ ├── data-table-skeleton.tsx │ │ ├── data-table.tsx │ │ ├── fileupload.tsx │ │ ├── header.tsx │ │ ├── heading.tsx │ │ ├── mobile-sidebar.tsx │ │ ├── page-head.tsx │ │ ├── pagination-section.tsx │ │ ├── popup-modal.tsx │ │ ├── sidebar.tsx │ │ ├── table-search-input.tsx │ │ ├── theme-toggle.tsx │ │ └── user-nav.tsx │ └── ui │ │ ├── accordion.tsx │ │ ├── alert-dialog.tsx │ │ ├── alert.tsx │ │ ├── aspect-ratio.tsx │ │ ├── avatar.tsx │ │ ├── badge.tsx │ │ ├── breadcrumb.tsx │ │ ├── button.tsx │ │ ├── calendar.tsx │ │ ├── card.tsx │ │ ├── carousel.tsx │ │ ├── checkbox.tsx │ │ ├── collapsible.tsx │ │ ├── command.tsx │ │ ├── context-menu.tsx │ │ ├── dialog.tsx │ │ ├── drawer.tsx │ │ ├── dropdown-menu.tsx │ │ ├── form.tsx │ │ ├── hover-card.tsx │ │ ├── icons.tsx │ │ ├── input-otp.tsx │ │ ├── input.tsx │ │ ├── label.tsx │ │ ├── menubar.tsx │ │ ├── modal.tsx │ │ ├── navigation-menu.tsx │ │ ├── pagination.tsx │ │ ├── popover.tsx │ │ ├── progress.tsx │ │ ├── radio-group.tsx │ │ ├── resizable.tsx │ │ ├── scroll-area.tsx │ │ ├── select.tsx │ │ ├── separator.tsx │ │ ├── sheet.tsx │ │ ├── skeleton.tsx │ │ ├── slider.tsx │ │ ├── sonner.tsx │ │ ├── switch.tsx │ │ ├── table.tsx │ │ ├── tabs.tsx │ │ ├── textarea.tsx │ │ ├── toast.tsx │ │ ├── toaster.tsx │ │ ├── toggle-group.tsx │ │ ├── toggle.tsx │ │ ├── tooltip.tsx │ │ └── use-toast.ts ├── constants │ └── data.ts ├── hooks │ └── use-sidebar.tsx ├── index.css ├── lib │ ├── api.ts │ └── utils.ts ├── main.tsx ├── pages │ ├── auth │ │ └── signin │ │ │ ├── components │ │ │ └── user-auth-form.tsx │ │ │ └── index.tsx │ ├── dashboard │ │ ├── components │ │ │ ├── overview.tsx │ │ │ └── recent-sales.tsx │ │ └── index.tsx │ ├── form │ │ └── index.tsx │ ├── not-found │ │ └── index.tsx │ └── students │ │ ├── StudentDetailPage.tsx │ │ ├── components │ │ ├── bio.tsx │ │ ├── count-card.tsx │ │ ├── feed.tsx │ │ ├── interest-channel.tsx │ │ ├── parent-detail-card.tsx │ │ ├── student-feed-table │ │ │ ├── cell-action.tsx │ │ │ ├── columns.tsx │ │ │ ├── index.tsx │ │ │ └── student-table-action.tsx │ │ ├── student-forms │ │ │ └── student-create-form.tsx │ │ ├── students-table │ │ │ ├── cell-action.tsx │ │ │ ├── columns.tsx │ │ │ ├── index.tsx │ │ │ └── student-table-action.tsx │ │ └── time-spent-card.tsx │ │ ├── index.tsx │ │ └── queries │ │ └── queries.ts ├── providers │ ├── index.tsx │ └── theme-provider.tsx ├── routes │ ├── hooks │ │ ├── index.tsx │ │ ├── use-pathname.tsx │ │ └── use-router.tsx │ └── index.tsx ├── types │ └── index.tsx └── vite-env.d.ts ├── tailwind.config.js ├── tsconfig.json ├── tsconfig.node.json ├── vercel.json ├── vite.config.ts └── vite.config.ts.timestamp-1710421433144-871e35c4a0331.mjs /.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'], 10 | parser: '@typescript-eslint/parser', 11 | plugins: ['react-refresh'], 12 | rules: { 13 | 'react/jsx-no-target-blank': 'off', 14 | 'react/prop-types': 'off', 15 | '@typescript-eslint/no-explicit-any': 'warn', 16 | '@typescript-eslint/no-unused-vars': 'warn', 17 | 'react-refresh/only-export-components': [ 18 | 'warn', 19 | { allowConstantExport: true } 20 | ] 21 | } 22 | }; 23 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | npx lint-staged -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | dist/ -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "arrowParens": "always", 3 | "bracketSpacing": true, 4 | "semi": true, 5 | "useTabs": false, 6 | "trailingComma": "none", 7 | "singleQuote": true, 8 | "tabWidth": 2, 9 | "endOfLine": "lf", 10 | "plugins": ["prettier-plugin-tailwindcss"] 11 | } 12 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Kiranism 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 | 2 | 3 | 4 | 5 | 6 |
React Admin Dashboard Starter Template With Shadcn-ui
7 |
Built with the Vite + React Ts
8 |
9 |
10 | View Demo 11 | 12 |
13 | 14 | ## Overview 15 | 16 | This is a starter template using the following stack: 17 | 18 | - Js Library - [React 18](https://react.dev/) 19 | - Language - [TypeScript](https://www.typescriptlang.org) 20 | - Styling - [Tailwind CSS](https://tailwindcss.com) 21 | - Components - [Shadcn-ui](https://ui.shadcn.com) 22 | - Schema Validations - [Zod](https://zod.dev) 23 | - Async state management - [Tanstack Query aka React Query](https://tanstack.com/query/latest/docs/framework/react/overview) 24 | - Tables - [Tanstack Tables](https://ui.shadcn.com/docs/components/data-table) 25 | - Forms - [React Hook Form](https://ui.shadcn.com/docs/components/form) 26 | - Linting - [ESLint](https://eslint.org) 27 | - Formatting - [Prettier](https://prettier.io) 28 | - Pre-commit hook - [Husky](https://typicode.github.io/husky/) 29 | 30 | ## Pages 31 | 32 | | Pages | Specifications | 33 | | :--------------------------------------------------------------------- | :------------------------------------------------------------------------------- | 34 | | [Signup](https://react-shadcn-dashboard-starter.vercel.app/login) | Custom auth. | 35 | | [Dashboard](https://react-shadcn-dashboard-starter.vercel.app/) | Cards with recharts graphs for analytics with dark mode ✅. | 36 | | [Students](https://react-shadcn-dashboard-starter.vercel.app/students) | Tanstack tables with students details with server side searching, pagination etc | 37 | | [404](https://react-shadcn-dashboard-starter.vercel.app/404) | Not Found Page | 38 | | - | - | 39 | 40 | ## Getting Started 41 | 42 | Follow these steps to clone the repository and start the development server: 43 | 44 | - `git clone https://github.com/Kiranism/react-shadcn-dashboard-starter.git` 45 | - `npm install` 46 | - `npm run dev` 47 | 48 | You should now be able to access the application at http://localhost:5173. 49 | -------------------------------------------------------------------------------- /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": "stone", 10 | "cssVariables": true, 11 | "prefix": "" 12 | }, 13 | "aliases": { 14 | "components": "@/components", 15 | "utils": "@/lib/utils" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Vite + React + TS 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-shadcn-dashboard", 3 | "private": true, 4 | "version": "0.0.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 | "format": "prettier . --write", 11 | "preview": "vite preview", 12 | "prepare": "husky" 13 | }, 14 | "lint-staged": { 15 | "**/*": [ 16 | "prettier --write --ignore-unknown" 17 | ] 18 | }, 19 | "dependencies": { 20 | "@eslint/config-array": "^0.19.0", 21 | "@eslint/object-schema": "^2.1.4", 22 | "@hookform/resolvers": "^3.3.4", 23 | "@radix-ui/react-accordion": "^1.1.2", 24 | "@radix-ui/react-alert-dialog": "^1.0.5", 25 | "@radix-ui/react-aspect-ratio": "^1.0.3", 26 | "@radix-ui/react-avatar": "^1.0.4", 27 | "@radix-ui/react-checkbox": "^1.0.4", 28 | "@radix-ui/react-collapsible": "^1.0.3", 29 | "@radix-ui/react-context-menu": "^2.1.5", 30 | "@radix-ui/react-dialog": "^1.0.5", 31 | "@radix-ui/react-dropdown-menu": "^2.0.6", 32 | "@radix-ui/react-hover-card": "^1.0.7", 33 | "@radix-ui/react-icons": "^1.3.0", 34 | "@radix-ui/react-label": "^2.0.2", 35 | "@radix-ui/react-menubar": "^1.0.4", 36 | "@radix-ui/react-navigation-menu": "^1.1.4", 37 | "@radix-ui/react-popover": "^1.0.7", 38 | "@radix-ui/react-progress": "^1.0.3", 39 | "@radix-ui/react-radio-group": "^1.1.3", 40 | "@radix-ui/react-scroll-area": "^1.0.5", 41 | "@radix-ui/react-select": "^2.0.0", 42 | "@radix-ui/react-separator": "^1.0.3", 43 | "@radix-ui/react-slider": "^1.1.2", 44 | "@radix-ui/react-slot": "^1.0.2", 45 | "@radix-ui/react-switch": "^1.0.3", 46 | "@radix-ui/react-tabs": "^1.0.4", 47 | "@radix-ui/react-toast": "^1.1.5", 48 | "@radix-ui/react-toggle": "^1.0.3", 49 | "@radix-ui/react-toggle-group": "^1.0.4", 50 | "@radix-ui/react-tooltip": "^1.0.7", 51 | "@tanstack/react-query": "^5.28.0", 52 | "@tanstack/react-query-devtools": "^5.28.0", 53 | "@tanstack/react-table": "^8.14.0", 54 | "axios": "^1.6.8", 55 | "class-variance-authority": "^0.7.0", 56 | "clsx": "^2.1.0", 57 | "cmdk": "^1.0.0", 58 | "date-fns": "^3.6.0", 59 | "embla-carousel-react": "^8.0.0", 60 | "input-otp": "^1.2.2", 61 | "lucide-react": "^0.358.0", 62 | "next-themes": "^0.3.0", 63 | "react": "^18.2.0", 64 | "react-day-picker": "^8.10.0", 65 | "react-dom": "^18.2.0", 66 | "react-dropzone": "^14.2.3", 67 | "react-error-boundary": "^4.0.13", 68 | "react-helmet-async": "^2.0.4", 69 | "react-hook-form": "^7.51.1", 70 | "react-resizable-panels": "^2.0.13", 71 | "react-router-dom": "^6.22.3", 72 | "recharts": "^2.12.2", 73 | "sonner": "^1.4.41", 74 | "tailwind-merge": "^2.2.2", 75 | "tailwindcss-animate": "^1.0.7", 76 | "use-debounce": "^10.0.0", 77 | "vaul": "^0.9.0", 78 | "zod": "^3.22.4" 79 | }, 80 | "devDependencies": { 81 | "@types/node": "^20.11.27", 82 | "@types/react": "^18.2.64", 83 | "@types/react-dom": "^18.2.21", 84 | "@typescript-eslint/eslint-plugin": "^7.1.1", 85 | "@typescript-eslint/parser": "^7.1.1", 86 | "@vitejs/plugin-react-swc": "^3.5.0", 87 | "autoprefixer": "^10.4.18", 88 | "eslint": "^8.57.0", 89 | "eslint-plugin-react-hooks": "^4.6.0", 90 | "eslint-plugin-react-refresh": "^0.4.5", 91 | "husky": "^9.0.11", 92 | "lint-staged": "^15.2.2", 93 | "postcss": "^8.4.35", 94 | "prettier": "^3.2.5", 95 | "prettier-plugin-tailwindcss": "^0.5.12", 96 | "tailwindcss": "^3.4.1", 97 | "typescript": "^5.2.2", 98 | "vite": "^5.1.6" 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {} 5 | } 6 | }; 7 | -------------------------------------------------------------------------------- /public/vite.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/App.tsx: -------------------------------------------------------------------------------- 1 | import AppProvider from './providers'; 2 | import AppRouter from './routes'; 3 | 4 | export default function App() { 5 | return ( 6 | 7 | 8 | 9 | ); 10 | } 11 | -------------------------------------------------------------------------------- /src/assets/react.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/components/layout/dashboard-layout.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from 'react'; 2 | import Sidebar from '../shared/sidebar'; 3 | import Header from '../shared/header'; 4 | import MobileSidebar from '../shared/mobile-sidebar'; 5 | import { MenuIcon } from 'lucide-react'; 6 | 7 | export default function DashboardLayout({ 8 | children 9 | }: { 10 | children: React.ReactNode; 11 | }) { 12 | const [sidebarOpen, setSidebarOpen] = useState(false); 13 | 14 | return ( 15 |
16 | 20 | 21 |
22 |
23 | 30 |
31 |
32 |
33 | {children} 34 |
35 |
36 |
37 | ); 38 | } 39 | -------------------------------------------------------------------------------- /src/components/shared/alert-modal.tsx: -------------------------------------------------------------------------------- 1 | import { Button } from '@/components/ui/button'; 2 | import { Modal } from '@/components/ui/modal'; 3 | 4 | type TAlertModalProps = { 5 | isOpen: boolean; 6 | onClose: () => void; 7 | onConfirm: () => void; 8 | loading: boolean; 9 | title?: string; 10 | description?: string; 11 | }; 12 | export const AlertModal = ({ 13 | isOpen, 14 | onClose, 15 | onConfirm, 16 | loading, 17 | title = 'Are you sure?', 18 | description = 'Are you sure you want to continue?' 19 | }: TAlertModalProps) => { 20 | return ( 21 | 27 |
28 | 31 | 34 |
35 |
36 | ); 37 | }; 38 | -------------------------------------------------------------------------------- /src/components/shared/breadcrumbs.tsx: -------------------------------------------------------------------------------- 1 | import { Slash } from 'lucide-react'; 2 | import { 3 | Breadcrumb, 4 | BreadcrumbItem, 5 | BreadcrumbLink, 6 | BreadcrumbList, 7 | BreadcrumbPage, 8 | BreadcrumbSeparator 9 | } from '@/components/ui/breadcrumb'; 10 | import { Fragment } from 'react'; 11 | 12 | type BreadcrumbItemProps = { 13 | title: string; 14 | link: string; 15 | }; 16 | 17 | export function Breadcrumbs({ items }: { items: BreadcrumbItemProps[] }) { 18 | return ( 19 | 20 | 21 | {items.map((item, index) => ( 22 | 23 | {index !== items.length - 1 && ( 24 | 25 | {item.title} 26 | 27 | )} 28 | {index < items.length - 1 && ( 29 | 30 | 31 | 32 | )} 33 | {index === items.length - 1 && ( 34 | {item.title} 35 | )} 36 | 37 | ))} 38 | 39 | 40 | ); 41 | } 42 | -------------------------------------------------------------------------------- /src/components/shared/dashboard-nav.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | import { Icons } from '@/components/ui/icons'; 3 | import { cn } from '@/lib/utils'; 4 | import { NavItem } from '@/types'; 5 | import { Dispatch, SetStateAction } from 'react'; 6 | import { useSidebar } from '@/hooks/use-sidebar'; 7 | import { 8 | Tooltip, 9 | TooltipContent, 10 | TooltipProvider, 11 | TooltipTrigger 12 | } from '@/components/ui/tooltip'; 13 | import { usePathname } from '@/routes/hooks'; 14 | import { Link } from 'react-router-dom'; 15 | 16 | interface DashboardNavProps { 17 | items: NavItem[]; 18 | setOpen?: Dispatch>; 19 | isMobileNav?: boolean; 20 | } 21 | 22 | export default function DashboardNav({ 23 | items, 24 | setOpen, 25 | isMobileNav = false 26 | }: DashboardNavProps) { 27 | const path = usePathname(); 28 | const { isMinimized } = useSidebar(); 29 | 30 | if (!items?.length) { 31 | return null; 32 | } 33 | 34 | console.log('isActive', isMobileNav, isMinimized); 35 | 36 | return ( 37 | 81 | ); 82 | } 83 | -------------------------------------------------------------------------------- /src/components/shared/data-table-skeleton.tsx: -------------------------------------------------------------------------------- 1 | import { Skeleton } from '@/components/ui/skeleton'; 2 | import { 3 | Table, 4 | TableBody, 5 | TableCell, 6 | TableHead, 7 | TableHeader, 8 | TableRow 9 | } from '@/components/ui/table'; 10 | 11 | type DataTableSkeletonProps = { 12 | columnCount: number; 13 | rowCount?: number; 14 | searchableColumnCount?: number; 15 | filterableColumnCount?: number; 16 | showViewOptions?: boolean; 17 | }; 18 | 19 | export function DataTableSkeleton({ 20 | columnCount, 21 | rowCount = 10, 22 | searchableColumnCount = 0, 23 | filterableColumnCount = 0, 24 | showViewOptions = true 25 | }: DataTableSkeletonProps) { 26 | return ( 27 |
28 |
29 |
30 | {searchableColumnCount > 0 31 | ? Array.from({ length: searchableColumnCount }).map((_, i) => ( 32 | 33 | )) 34 | : null} 35 | {filterableColumnCount > 0 36 | ? Array.from({ length: filterableColumnCount }).map((_, i) => ( 37 | 38 | )) 39 | : null} 40 |
41 | {showViewOptions ? ( 42 | 43 | ) : null} 44 |
45 |
46 | 47 | 48 | {Array.from({ length: 1 }).map((_, i) => ( 49 | 50 | {Array.from({ length: columnCount }).map((_, i) => ( 51 | 52 | 53 | 54 | ))} 55 | 56 | ))} 57 | 58 | 59 | {Array.from({ length: rowCount }).map((_, i) => ( 60 | 61 | {Array.from({ length: columnCount }).map((_, i) => ( 62 | 63 | 64 | 65 | ))} 66 | 67 | ))} 68 | 69 |
70 |
71 |
72 |
73 | 74 |
75 |
76 |
77 | 78 | 79 |
80 |
81 | 82 |
83 |
84 | 85 | 86 | 87 | 88 |
89 |
90 |
91 |
92 | ); 93 | } 94 | -------------------------------------------------------------------------------- /src/components/shared/fileupload.tsx: -------------------------------------------------------------------------------- 1 | import { AvatarIcon } from '@radix-ui/react-icons'; 2 | import { CameraIcon } from 'lucide-react'; 3 | import { useEffect, useState } from 'react'; 4 | import { Accept, useDropzone } from 'react-dropzone'; 5 | 6 | type TFileUploadProps = { 7 | onChange: (value: File[]) => void; 8 | value: File[]; 9 | }; 10 | export default function FileUpload({ onChange, value }: TFileUploadProps) { 11 | console.log('files=>', value); 12 | 13 | const { getRootProps, getInputProps } = useDropzone({ 14 | accept: 'image/*' as unknown as Accept, 15 | onDrop: (acceptedFiles: File[]) => { 16 | console.log('files acceptedFiles=>', acceptedFiles); 17 | onUpdateFile(acceptedFiles); 18 | } 19 | }); 20 | 21 | const onUpdateFile = (newFiles: File[]) => { 22 | onChange(newFiles); 23 | }; 24 | return ( 25 |
26 |
27 |
28 | 29 | {value && !!value.length ? ( 30 | 31 | ) : ( 32 | 33 | )} 34 | 35 |

36 | 37 | Add Image 38 |

39 |
40 |
41 |
42 | ); 43 | } 44 | 45 | function ImagePreview({ file }: { file: File }) { 46 | const [objectUrl, setObjectUrl] = useState(null); 47 | 48 | useEffect(() => { 49 | // Create an object URL for the file 50 | const url = URL.createObjectURL(file); 51 | setObjectUrl(url); 52 | 53 | // Clean up the object URL when the component unmounts 54 | return () => { 55 | URL.revokeObjectURL(url); 56 | }; 57 | }, [file]); 58 | 59 | return objectUrl ? ( 60 | Preview 65 | ) : null; 66 | } 67 | -------------------------------------------------------------------------------- /src/components/shared/header.tsx: -------------------------------------------------------------------------------- 1 | import { navItems } from '@/constants/data'; 2 | import { usePathname } from '@/routes/hooks'; 3 | import Heading from './heading'; 4 | import UserNav from './user-nav'; 5 | import { ModeToggle } from './theme-toggle'; 6 | 7 | // Custom hook to find the matched path 8 | const useMatchedPath = (pathname: string) => { 9 | const matchedPath = 10 | navItems.find((item) => item.href === pathname) || 11 | navItems.find( 12 | (item) => pathname.startsWith(item.href + '/') && item.href !== '/' 13 | ); 14 | return matchedPath?.title || ''; 15 | }; 16 | 17 | export default function Header() { 18 | const pathname = usePathname(); 19 | const headingText = useMatchedPath(pathname); 20 | 21 | return ( 22 |
23 | 24 |
25 | 26 | 27 |
28 |
29 | ); 30 | } 31 | -------------------------------------------------------------------------------- /src/components/shared/heading.tsx: -------------------------------------------------------------------------------- 1 | type THeadingProps = { 2 | title: string; 3 | description?: string; 4 | className?: string; 5 | }; 6 | 7 | export default function Heading({ 8 | title, 9 | description, 10 | className 11 | }: THeadingProps) { 12 | return ( 13 |
14 |

15 | {title} 16 |

17 |

{description}

18 |
19 | ); 20 | } 21 | -------------------------------------------------------------------------------- /src/components/shared/mobile-sidebar.tsx: -------------------------------------------------------------------------------- 1 | import DashboardNav from '@/components/shared/dashboard-nav'; 2 | import { Sheet, SheetContent } from '@/components/ui/sheet'; 3 | import { navItems } from '@/constants/data'; 4 | import { Dispatch, SetStateAction } from 'react'; 5 | import { Link } from 'react-router-dom'; 6 | 7 | type TMobileSidebarProps = { 8 | className?: string; 9 | setSidebarOpen: Dispatch>; 10 | sidebarOpen: boolean; 11 | }; 12 | export default function MobileSidebar({ 13 | setSidebarOpen, 14 | sidebarOpen 15 | }: TMobileSidebarProps) { 16 | return ( 17 | <> 18 | 19 | 20 |
21 |
22 | 23 | Logo 24 | 25 |
26 | 27 |
28 |
29 |
30 |
31 |
32 | 33 | ); 34 | } 35 | -------------------------------------------------------------------------------- /src/components/shared/page-head.tsx: -------------------------------------------------------------------------------- 1 | import { Helmet } from 'react-helmet-async'; 2 | 3 | export default function PageHead({ title = 'Kutubi' }) { 4 | return ( 5 | 6 | {title} 7 | 8 | ); 9 | } 10 | -------------------------------------------------------------------------------- /src/components/shared/pagination-section.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | Pagination, 3 | PaginationContent, 4 | PaginationEllipsis, 5 | PaginationItem, 6 | PaginationLink, 7 | PaginationNext, 8 | PaginationNextLast, 9 | PaginationPrevious, 10 | PaginationPreviousLast 11 | } from '@/components/ui/pagination'; 12 | 13 | type TPaginationSectionProps = { 14 | totalPosts: number; 15 | postsPerPage: number; 16 | currentPage: number; 17 | setCurrentPage: (page: number) => void; 18 | }; 19 | export default function PaginationSection({ 20 | totalPosts, 21 | postsPerPage, 22 | currentPage, 23 | setCurrentPage 24 | }: TPaginationSectionProps) { 25 | const pageNumbers: number[] = []; 26 | for (let i = 1; i <= Math.ceil(totalPosts / postsPerPage); i++) { 27 | pageNumbers.push(i); 28 | } 29 | 30 | const maxPageNum = 2; // Maximum page numbers to display at once 31 | const pageNumLimit = Math.floor(maxPageNum / 2); // Current page should be in the middle if possible 32 | 33 | const activePages = pageNumbers.slice( 34 | Math.max(0, currentPage - 1 - pageNumLimit), 35 | Math.min(currentPage - 1 + pageNumLimit + 1, pageNumbers.length) 36 | ); 37 | 38 | const handleNextPage = () => { 39 | if (currentPage < pageNumbers.length) { 40 | setCurrentPage(currentPage + 1); 41 | } 42 | }; 43 | 44 | const handlePrevPage = () => { 45 | if (currentPage > 1) { 46 | setCurrentPage(currentPage - 1); 47 | } 48 | }; 49 | 50 | const handlePrevPageLast = () => { 51 | if (currentPage > 1) { 52 | setCurrentPage(1); 53 | } 54 | }; 55 | 56 | const handleNextPageLast = () => { 57 | if (currentPage < pageNumbers.length) { 58 | setCurrentPage(pageNumbers.length); 59 | } 60 | }; 61 | 62 | // Function to render page numbers with ellipsis 63 | const renderPages = () => { 64 | const renderedPages = activePages.map((page, idx) => ( 65 | 69 | setCurrentPage(page)}> 70 | {page} 71 | 72 | 73 | )); 74 | 75 | // Add ellipsis at the start if necessary 76 | if (activePages[0] > 1) { 77 | renderedPages.unshift( 78 | setCurrentPage(activePages[0] - 1)} 81 | /> 82 | ); 83 | } 84 | 85 | // Add ellipsis at the end if necessary 86 | if (activePages[activePages.length - 1] < pageNumbers.length) { 87 | renderedPages.push( 88 | 91 | setCurrentPage(activePages[activePages.length - 1] + 1) 92 | } 93 | /> 94 | ); 95 | } 96 | 97 | return renderedPages; 98 | }; 99 | 100 | return ( 101 |
102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | {renderPages()} 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 |
118 | ); 119 | } 120 | -------------------------------------------------------------------------------- /src/components/shared/popup-modal.tsx: -------------------------------------------------------------------------------- 1 | import { Button } from '@/components/ui/button'; 2 | import { Modal } from '@/components/ui/modal'; 3 | import { Plus } from 'lucide-react'; 4 | import { useState } from 'react'; 5 | import { ScrollArea } from '../ui/scroll-area'; 6 | 7 | type TPopupModalProps = { 8 | onConfirm?: () => void; 9 | loading?: boolean; 10 | renderModal: (onClose: () => void) => React.ReactNode; 11 | }; 12 | export default function PopupModal({ renderModal }: TPopupModalProps) { 13 | const [isOpen, setIsOpen] = useState(false); 14 | const onClose = () => setIsOpen(false); 15 | return ( 16 | <> 17 | 20 | 25 | 26 | {renderModal(onClose)} 27 | 28 | 29 | 30 | ); 31 | } 32 | -------------------------------------------------------------------------------- /src/components/shared/sidebar.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | import DashboardNav from '@/components/shared/dashboard-nav'; 3 | import { navItems } from '@/constants/data'; 4 | import { useSidebar } from '@/hooks/use-sidebar'; 5 | import { cn } from '@/lib/utils'; 6 | import { ChevronsLeft } from 'lucide-react'; 7 | import { useState } from 'react'; 8 | 9 | type SidebarProps = { 10 | className?: string; 11 | }; 12 | 13 | export default function Sidebar({ className }: SidebarProps) { 14 | const { isMinimized, 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 | 54 | ); 55 | } 56 | -------------------------------------------------------------------------------- /src/components/shared/table-search-input.tsx: -------------------------------------------------------------------------------- 1 | import React, { useCallback } from 'react'; 2 | import { Input } from '../ui/input'; 3 | import { useDebounce } from 'use-debounce'; 4 | import { useSearchParams } from 'react-router-dom'; 5 | 6 | export default function TableSearchInput({ 7 | placeholder 8 | }: { 9 | placeholder?: string; 10 | }) { 11 | const [searchParams, setSearchParams] = useSearchParams(); 12 | const country = searchParams.get('search') || ''; 13 | const [searchTerm, setSearchTerm] = React.useState(country); 14 | // debounce the search input 15 | const [debouncedValue] = useDebounce(searchTerm, 1000); 16 | const handleSettingSearchParams = useCallback((newSearchValue: string) => { 17 | // Update the URL with the new search value 18 | if ( 19 | newSearchValue === '' || 20 | newSearchValue === undefined || 21 | !newSearchValue 22 | ) { 23 | searchParams.delete('search'); 24 | setSearchParams(searchParams); 25 | return; 26 | } 27 | setSearchParams({ 28 | ...Object.fromEntries(searchParams), 29 | page: '1', // Spread the existing search params 30 | search: newSearchValue // Update the search value 31 | }); 32 | }, []); 33 | 34 | React.useEffect(() => { 35 | handleSettingSearchParams(debouncedValue); 36 | }, [debouncedValue, handleSettingSearchParams]); 37 | return ( 38 | setSearchTerm(event.target.value)} 42 | className="w-full md:max-w-sm" 43 | /> 44 | ); 45 | } 46 | -------------------------------------------------------------------------------- /src/components/shared/theme-toggle.tsx: -------------------------------------------------------------------------------- 1 | import { Moon, Sun } from 'lucide-react'; 2 | 3 | import { Button } from '@/components/ui/button'; 4 | import { 5 | DropdownMenu, 6 | DropdownMenuContent, 7 | DropdownMenuItem, 8 | DropdownMenuTrigger 9 | } from '@/components/ui/dropdown-menu'; 10 | import { useTheme } from '@/providers/theme-provider'; 11 | 12 | export function ModeToggle() { 13 | const { setTheme } = useTheme(); 14 | 15 | return ( 16 | 17 | 18 | 23 | 24 | 25 | setTheme('light')}> 26 | Light 27 | 28 | setTheme('dark')}> 29 | Dark 30 | 31 | setTheme('system')}> 32 | System 33 | 34 | 35 | 36 | ); 37 | } 38 | -------------------------------------------------------------------------------- /src/components/shared/user-nav.tsx: -------------------------------------------------------------------------------- 1 | import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'; 2 | import { Button } from '@/components/ui/button'; 3 | import { 4 | DropdownMenu, 5 | DropdownMenuContent, 6 | DropdownMenuGroup, 7 | DropdownMenuItem, 8 | DropdownMenuLabel, 9 | DropdownMenuSeparator, 10 | DropdownMenuShortcut, 11 | DropdownMenuTrigger 12 | } from '@/components/ui/dropdown-menu'; 13 | 14 | export default function UserNav() { 15 | return ( 16 | 17 | 18 | 29 | 30 | 31 | 32 |
33 |

{'Admin'}

34 |

35 | {'admin@gmail.com'} 36 |

37 |
38 |
39 | 40 | 41 | 42 | Profile 43 | ⇧⌘P 44 | 45 | 46 | Billing 47 | ⌘B 48 | 49 | 50 | Settings 51 | ⌘S 52 | 53 | New Team 54 | 55 | 56 | console.log('logout')}> 57 | Log out 58 | ⇧⌘Q 59 | 60 |
61 |
62 | ); 63 | } 64 | -------------------------------------------------------------------------------- /src/components/ui/accordion.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import * as AccordionPrimitive from '@radix-ui/react-accordion'; 3 | import { ChevronDownIcon } from '@radix-ui/react-icons'; 4 | 5 | import { cn } from '@/lib/utils'; 6 | 7 | const Accordion = AccordionPrimitive.Root; 8 | 9 | const AccordionItem = React.forwardRef< 10 | React.ElementRef, 11 | React.ComponentPropsWithoutRef 12 | >(({ className, ...props }, ref) => ( 13 | 18 | )); 19 | AccordionItem.displayName = 'AccordionItem'; 20 | 21 | const AccordionTrigger = React.forwardRef< 22 | React.ElementRef, 23 | React.ComponentPropsWithoutRef 24 | >(({ className, children, ...props }, ref) => ( 25 | 26 | svg]:rotate-180', 30 | className 31 | )} 32 | {...props} 33 | > 34 | {children} 35 | 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 }; 56 | -------------------------------------------------------------------------------- /src/components/ui/alert-dialog.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import * as AlertDialogPrimitive from '@radix-ui/react-alert-dialog'; 3 | 4 | import { cn } from '@/lib/utils'; 5 | import { buttonVariants } from '@/components/ui/button'; 6 | 7 | const AlertDialog = AlertDialogPrimitive.Root; 8 | 9 | const AlertDialogTrigger = AlertDialogPrimitive.Trigger; 10 | 11 | const AlertDialogPortal = AlertDialogPrimitive.Portal; 12 | 13 | const AlertDialogOverlay = React.forwardRef< 14 | React.ElementRef, 15 | React.ComponentPropsWithoutRef 16 | >(({ className, ...props }, ref) => ( 17 | 25 | )); 26 | AlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName; 27 | 28 | const AlertDialogContent = React.forwardRef< 29 | React.ElementRef, 30 | React.ComponentPropsWithoutRef 31 | >(({ className, ...props }, ref) => ( 32 | 33 | 34 | 42 | 43 | )); 44 | AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName; 45 | 46 | const AlertDialogHeader = ({ 47 | className, 48 | ...props 49 | }: React.HTMLAttributes) => ( 50 |
57 | ); 58 | AlertDialogHeader.displayName = 'AlertDialogHeader'; 59 | 60 | const AlertDialogFooter = ({ 61 | className, 62 | ...props 63 | }: React.HTMLAttributes) => ( 64 |
71 | ); 72 | AlertDialogFooter.displayName = 'AlertDialogFooter'; 73 | 74 | const AlertDialogTitle = React.forwardRef< 75 | React.ElementRef, 76 | React.ComponentPropsWithoutRef 77 | >(({ className, ...props }, ref) => ( 78 | 83 | )); 84 | AlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName; 85 | 86 | const AlertDialogDescription = React.forwardRef< 87 | React.ElementRef, 88 | React.ComponentPropsWithoutRef 89 | >(({ className, ...props }, ref) => ( 90 | 95 | )); 96 | AlertDialogDescription.displayName = 97 | AlertDialogPrimitive.Description.displayName; 98 | 99 | const AlertDialogAction = React.forwardRef< 100 | React.ElementRef, 101 | React.ComponentPropsWithoutRef 102 | >(({ className, ...props }, ref) => ( 103 | 108 | )); 109 | AlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName; 110 | 111 | const AlertDialogCancel = React.forwardRef< 112 | React.ElementRef, 113 | React.ComponentPropsWithoutRef 114 | >(({ className, ...props }, ref) => ( 115 | 124 | )); 125 | AlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName; 126 | 127 | export { 128 | AlertDialog, 129 | AlertDialogPortal, 130 | AlertDialogOverlay, 131 | AlertDialogTrigger, 132 | AlertDialogContent, 133 | AlertDialogHeader, 134 | AlertDialogFooter, 135 | AlertDialogTitle, 136 | AlertDialogDescription, 137 | AlertDialogAction, 138 | AlertDialogCancel 139 | }; 140 | -------------------------------------------------------------------------------- /src/components/ui/alert.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { cva, type VariantProps } from 'class-variance-authority'; 3 | 4 | import { cn } from '@/lib/utils'; 5 | 6 | const alertVariants = cva( 7 | 'relative w-full rounded-lg border px-4 py-3 text-sm [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground [&>svg~*]:pl-7', 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 | import * as AspectRatioPrimitive from '@radix-ui/react-aspect-ratio'; 2 | 3 | const AspectRatio = AspectRatioPrimitive.Root; 4 | 5 | export { AspectRatio }; 6 | -------------------------------------------------------------------------------- /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/breadcrumb.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { ChevronRightIcon, DotsHorizontalIcon } from '@radix-ui/react-icons'; 3 | import { Slot } from '@radix-ui/react-slot'; 4 | 5 | import { cn } from '@/lib/utils'; 6 | 7 | const Breadcrumb = React.forwardRef< 8 | HTMLElement, 9 | React.ComponentPropsWithoutRef<'nav'> & { 10 | separator?: React.ReactNode; 11 | } 12 | >(({ ...props }, ref) =>