diff --git a/.eslintrc.json b/.eslintrc.json
new file mode 100644
index 0000000..1023c19
--- /dev/null
+++ b/.eslintrc.json
@@ -0,0 +1,9 @@
+{
+ "extends": "next/core-web-vitals",
+ "plugins": ["@typescript-eslint"],
+ "rules": {
+ "@typescript-eslint/no-unused-vars": "off",
+ "import/no-unresolved": "error",
+ "import/named": "off"
+ }
+}
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..8e9365e
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,40 @@
+# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
+
+# dependencies
+/node_modules
+/.pnp
+.pnp.js
+
+# testing
+/coverage
+
+# next.js
+/.next/
+/out/
+
+# production
+/build
+
+# misc
+.DS_Store
+*.pem
+
+# debug
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+
+# local env files
+.env*.local
+.env
+
+# vercel
+.vercel
+
+# typescript
+*.tsbuildinfo
+next-env.d.ts
+
+# Serwist
+public/sw*
+public/swe-worker*
diff --git a/.prettierrc b/.prettierrc
new file mode 100644
index 0000000..f395ecb
--- /dev/null
+++ b/.prettierrc
@@ -0,0 +1,11 @@
+{
+ "arrowParens": "always",
+ "bracketSpacing": true,
+ "semi": true,
+ "useTabs": false,
+ "trailingComma": "none",
+ "singleQuote": true,
+ "tabWidth": 2,
+ "endOfLine": "lf",
+ "plugins": ["prettier-plugin-tailwindcss"]
+}
diff --git a/.vscode/settings.json b/.vscode/settings.json
new file mode 100644
index 0000000..c55ca90
--- /dev/null
+++ b/.vscode/settings.json
@@ -0,0 +1,3 @@
+{
+ "cSpell.words": ["cuid"]
+}
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..73702d7
--- /dev/null
+++ b/README.md
@@ -0,0 +1,66 @@
+
+
+
+
+
+
Next.js 14 Admin Dashboard Starter Template With Shadcn-ui
+Built with the Next.js App Router
+
+
+
+## Overview
+
+This is a starter template using the following stack:
+
+- Framework - [Next.js 14](https://nextjs.org/13)
+- Language - [TypeScript](https://www.typescriptlang.org)
+- Styling - [Tailwind CSS](https://tailwindcss.com)
+- Components - [Shadcn-ui](https://ui.shadcn.com)
+- Schema Validations - [Zod](https://zod.dev)
+- State Management - [Zustand](https://zustand-demo.pmnd.rs)
+- Search params state manager - [Nuqs](https://nuqs.47ng.com/)
+- Auth - [Auth.js](https://authjs.dev/)
+- Tables - [Tanstack Tables](https://ui.shadcn.com/docs/components/data-table)
+- Forms - [React Hook Form](https://ui.shadcn.com/docs/components/form)
+- Command+k interface - [kbar](https://kbar.vercel.app/)
+- Linting - [ESLint](https://eslint.org)
+- Pre-commit Hooks - [Husky](https://typicode.github.io/husky/)
+- Formatting - [Prettier](https://prettier.io)
+
+_If you are looking for a React admin dashboard starter, here is the [repo](https://github.com/Kiranism/react-shadcn-dashboard-starter)._
+
+## Pages
+
+| Pages | Specifications |
+| :-------------------------------------------------------------------------------- | :---------------------------------------------------------------------------------------------------------------------------------- |
+| [Signup](https://next-shadcn-dashboard-starter.vercel.app/) | Authentication with **NextAuth** supports Social logins and email logins (Enter dummy email for demo). |
+| [Dashboard](https://next-shadcn-dashboard-starter.vercel.app/app) | Cards with recharts graphs for analytics. |
+| [Employee](https://next-shadcn-dashboard-starter.vercel.app/app/employee) | Tanstack tables with server side searching, filter, pagination by Nuqs which is a Type-safe search params state manager in nextjs). |
+| [Employee/new](https://next-shadcn-dashboard-starter.vercel.app/app/employee/new) | A Employee Form with shadcn form (react-hook-form + zod). |
+| [Product](https://next-shadcn-dashboard-starter.vercel.app/app/product) | Tanstack tables with server side searching, filter, pagination by Nuqs which is a Type-safe search params state manager in nextjs |
+| [Product/new](https://next-shadcn-dashboard-starter.vercel.app/app/product/new) | A Product Form with shadcn form (react-hook-form + zod). |
+| [Profile](https://next-shadcn-dashboard-starter.vercel.app/app/profile) | Mutistep dynamic forms using react-hook-form and zod for form validation. |
+| [Kanban Board](https://next-shadcn-dashboard-starter.vercel.app/app/kanban) | A Drag n Drop task management board with dnd-kit and zustand to persist state locally. |
+| [Not Found](https://next-shadcn-dashboard-starter.vercel.app/app/notfound) | Not Found Page Added in the root level |
+| - | - |
+
+## Getting Started
+
+Follow these steps to clone the repository and start the development server:
+
+- `git clone https://github.com/Kiranism/next-shadcn-dashboard-starter.git`
+- `npm install`
+- Create a `.env.local` file by copying the example environment file:
+ `cp env.example.txt .env.local`
+- Add the required environment variables to the `.env.local` file.
+- `npm run dev`
+
+You should now be able to access the application at http://localhost:3000.
+
+> [!WARNING]
+> After cloning or forking the repository, be cautious when pulling or syncing with the latest changes, as this may result in breaking conflicts.
+
+Cheers! 🥂
diff --git a/app/(auth)/(signin)/page.tsx b/app/(auth)/(signin)/page.tsx
new file mode 100644
index 0000000..3a659f6
--- /dev/null
+++ b/app/(auth)/(signin)/page.tsx
@@ -0,0 +1,19 @@
+import { Metadata } from 'next';
+import GoogleSignInButton from '../_components/google-auth-button';
+
+export const metadata: Metadata = {
+ title: 'Sign-in',
+ description: 'Sign-in with Google'
+};
+
+export default function SignInViewPage() {
+ return (
+
+ );
+}
diff --git a/app/(auth)/_components/google-auth-button.tsx b/app/(auth)/_components/google-auth-button.tsx
new file mode 100644
index 0000000..35e6ee5
--- /dev/null
+++ b/app/(auth)/_components/google-auth-button.tsx
@@ -0,0 +1,20 @@
+'use client';
+import { useSearchParams } from 'next/navigation';
+import { signIn } from 'next-auth/react';
+import { Button } from '@/components/ui/button';
+import { DrumIcon } from 'lucide-react';
+import { Icons } from '@/components/icons';
+import { RainbowButton } from '@/components/ui/rainbow-button';
+
+export default function GoogleSignInButton() {
+ const searchParams = useSearchParams();
+ const callbackUrl = searchParams.get('callbackUrl');
+ return (
+ signIn('google', { callbackUrl: callbackUrl ?? '/app' })}
+ >
+
+ Sign-in with Google
+
+ );
+}
diff --git a/app/api/auth/[...nextauth]/route.ts b/app/api/auth/[...nextauth]/route.ts
new file mode 100644
index 0000000..0aa1cd7
--- /dev/null
+++ b/app/api/auth/[...nextauth]/route.ts
@@ -0,0 +1,2 @@
+import { handlers } from '@/auth';
+export const { GET, POST } = handlers;
diff --git a/app/app/layout.tsx b/app/app/layout.tsx
new file mode 100644
index 0000000..717e027
--- /dev/null
+++ b/app/app/layout.tsx
@@ -0,0 +1,19 @@
+import type { Metadata } from 'next';
+import { cookies } from 'next/headers';
+
+export const metadata: Metadata = {
+ title: 'Next-Shadcn-TWA',
+ description:
+ 'Trusted Web Activities Template built with Next.js, TailwindCSS and Shad-CN'
+};
+
+export default function DashboardLayout({
+ children
+}: {
+ children: React.ReactNode;
+}) {
+ // Persisting the sidebar state in the cookie.
+ const cookieStore = cookies();
+ const defaultOpen = cookieStore.get('sidebar:state')?.value === 'true';
+ return <>{children}>;
+}
diff --git a/app/app/loading.tsx b/app/app/loading.tsx
new file mode 100644
index 0000000..91b45d0
--- /dev/null
+++ b/app/app/loading.tsx
@@ -0,0 +1,13 @@
+'use client';
+
+import { Loader } from '@/components/ui/loader';
+
+export default function Loading() {
+ return (
+
+ );
+}
diff --git a/app/app/overview/page.tsx b/app/app/overview/page.tsx
new file mode 100644
index 0000000..049ff8c
--- /dev/null
+++ b/app/app/overview/page.tsx
@@ -0,0 +1,23 @@
+import PageContainer from '@/components/layout/page-container';
+
+import { auth } from '@/auth';
+import { redirect } from 'next/navigation';
+import AuthInfo from '@/components/layout/auth-info';
+
+export default async function OverViewPage() {
+ const session = await auth();
+
+ if (!session?.user) {
+ redirect('/signin');
+ }
+
+ return (
+
+ );
+}
diff --git a/app/app/page.tsx b/app/app/page.tsx
new file mode 100644
index 0000000..018e297
--- /dev/null
+++ b/app/app/page.tsx
@@ -0,0 +1,12 @@
+import { auth } from '@/auth';
+import { redirect } from 'next/navigation';
+
+export default async function Dashboard() {
+ const session = await auth();
+
+ if (!session?.user) {
+ return redirect('/');
+ } else {
+ redirect('/app/overview');
+ }
+}
diff --git a/app/favicon.ico b/app/favicon.ico
new file mode 100644
index 0000000..718d6fe
Binary files /dev/null and b/app/favicon.ico differ
diff --git a/app/globals.css b/app/globals.css
new file mode 100644
index 0000000..bc15b7b
--- /dev/null
+++ b/app/globals.css
@@ -0,0 +1,115 @@
+@tailwind base;
+@tailwind components;
+@tailwind utilities;
+
+@layer base {
+ :root {
+ --background: 0 0% 99%;
+ --foreground: 20 51% 4%;
+ --muted: 67 60% 90%;
+ --muted-foreground: 20 7% 25%;
+ --popover: 20 66% 98%;
+ --popover-foreground: 20 51% 3%;
+ --card: 0 0% 100%;
+ --card-foreground: 20 51% 3%;
+ --border: 20 15% 94%;
+ --input: 20 15% 94%;
+ --primary: 67 94% 54%;
+ --primary-foreground: 0 0% 12%;
+ --secondary: 20 12% 92%;
+ --secondary-foreground: 20 100% 32%;
+ --accent: 67 94% 54%;
+ --accent-foreground: 0 0% 5%;
+ --destructive: 0 80% 50%;
+ --destructive-foreground: 0 0% 100%;
+ --ring: 67 94% 54%;
+ --radius: 0.5rem;
+ --chart-1: 67 94% 44%;
+ --chart-2: 173 58% 39%;
+ --chart-3: 197 37% 24%;
+ --chart-4: 43 74% 66%;
+ --chart-5: 27 87% 67%;
+ --sidebar-background: 0 0% 100%;
+ --sidebar-foreground: 240 5.3% 26.1%;
+ --sidebar-primary: 0 0% 0%;
+ --sidebar-primary-foreground: 0 0% 98%;
+ --sidebar-accent: 67 94% 54%;
+ --sidebar-accent-foreground: 240 5.9% 10%;
+ --sidebar-border: 220 13% 91%;
+ --sidebar-ring: 217.2 91.2% 59.8%;
+ --color-1: 0 100% 63%;
+ --color-2: 270 100% 63%;
+ --color-3: 210 100% 63%;
+ --color-4: 195 100% 63%;
+ --color-5: 90 100% 63%;
+ }
+
+ .dark {
+ --background: 0 0% 3%;
+ --foreground: 253 31% 98%;
+ --muted: 253 7% 13%;
+ --muted-foreground: 253 0% 63%;
+ --popover: 0 0% 1%;
+ --popover-foreground: 253 31% 98%;
+ --card: 0 0% 1%;
+ --card-foreground: 253 31% 99%;
+ --border: 0 0% 15%;
+ --input: 0 0% 15%;
+ --primary: 67 94% 54%;
+ --primary-foreground: 0 0% 1%;
+ --secondary: 0 0% 9%;
+ --secondary-foreground: 253 7% 69%;
+ --accent: 67 94% 54%;
+ --accent-foreground: 0 0% 0%;
+ --destructive: 339.2 90.36% 51.18%;
+ --destructive-foreground: 0 0% 100%;
+ --ring: 67 94% 54%;
+ --chart-1: 67 94% 54;
+ --chart-2: 253 13% 74%;
+ --chart-3: 30 80% 55%;
+ --chart-4: 280 65% 60%;
+ --chart-5: 340 75% 55%;
+ --sidebar-background: 0 0% 0%;
+ --sidebar-foreground: 240 4.8% 95.9%;
+ --sidebar-primary: 67 94% 54%;
+ --sidebar-primary-foreground: 0 0% 0%;
+ --sidebar-accent: 67 94% 54%;
+ --sidebar-accent-foreground: 0 0% 0%;
+ --sidebar-border: 240 3.7% 15.9%;
+ --sidebar-ring: 253 91% 58%%;
+ --color-1: 0 100% 63%;
+ --color-2: 270 100% 63%;
+ --color-3: 210 100% 63%;
+ --color-4: 195 100% 63%;
+ --color-5: 90 100% 63%;
+ }
+}
+
+@layer base {
+ * {
+ @apply border-border;
+ }
+ body {
+ @apply box-border bg-background text-foreground;
+ }
+}
+
+@layer utilities {
+ .min-h-screen {
+ min-height: 100vh; /* Fallback */
+ min-height: 100dvh;
+ }
+ .h-screen {
+ height: 100vh; /* Fallback */
+ height: 100dvh;
+ }
+}
+
+@layer base {
+ * {
+ @apply border-border;
+ }
+ body {
+ @apply bg-background text-foreground;
+ }
+}
diff --git a/app/layout.tsx b/app/layout.tsx
new file mode 100644
index 0000000..bef2093
--- /dev/null
+++ b/app/layout.tsx
@@ -0,0 +1,70 @@
+import { auth } from '@/auth';
+import Providers from '@/components/layout/providers';
+import { Toaster } from '@/components/ui/sonner';
+import NextTopLoader from 'nextjs-toploader';
+import './globals.css';
+
+import type { Metadata, Viewport } from 'next';
+
+const APP_NAME = 'Next-Shadcn-TWA';
+const APP_DEFAULT_TITLE = 'Next-Shadcn-TWA';
+const APP_TITLE_TEMPLATE = '%s - Next-Shadcn-TWA';
+const APP_DESCRIPTION = 'Next-Shadcn-TWA';
+
+export const metadata: Metadata = {
+ applicationName: APP_NAME,
+ title: {
+ default: APP_DEFAULT_TITLE,
+ template: APP_TITLE_TEMPLATE
+ },
+ description: APP_DESCRIPTION,
+ appleWebApp: {
+ capable: true,
+ statusBarStyle: 'default',
+ title: APP_DEFAULT_TITLE
+ // startUpImage: [],
+ },
+ formatDetection: {
+ telephone: false
+ },
+ openGraph: {
+ type: 'website',
+ siteName: APP_NAME,
+ title: {
+ default: APP_DEFAULT_TITLE,
+ template: APP_TITLE_TEMPLATE
+ },
+ description: APP_DESCRIPTION
+ },
+ twitter: {
+ card: 'summary',
+ title: {
+ default: APP_DEFAULT_TITLE,
+ template: APP_TITLE_TEMPLATE
+ },
+ description: APP_DESCRIPTION
+ }
+};
+
+export const viewport: Viewport = {
+ themeColor: '#FFFFFF'
+};
+
+export default async function RootLayout({
+ children
+}: {
+ children: React.ReactNode;
+}) {
+ const session = await auth();
+ return (
+
+
+
+
+
+ {children}
+
+
+
+ );
+}
diff --git a/app/manifest.json b/app/manifest.json
new file mode 100644
index 0000000..b43c8d8
--- /dev/null
+++ b/app/manifest.json
@@ -0,0 +1,26 @@
+{
+ "name": "Next-Shadcn-TWA",
+ "short_name": "Next-Shadcn-TWA",
+ "description": "Trusted Web Activity Template built with Next.js, TailwindCSS and Shad-CN",
+ "start_url": "/",
+ "display": "standalone",
+ "background_color": "#ffffff",
+ "theme_color": "#DEF81B",
+ "icons": [
+ {
+ "src": "/icons/192.png",
+ "sizes": "192x192",
+ "type": "image/png"
+ },
+ {
+ "src": "/icons/512.png",
+ "sizes": "512x512",
+ "type": "image/png"
+ },
+ {
+ "src": "/icons/700.png",
+ "sizes": "700x700",
+ "type": "image/png"
+ }
+ ]
+}
diff --git a/app/not-found.tsx b/app/not-found.tsx
new file mode 100644
index 0000000..9738cc6
--- /dev/null
+++ b/app/not-found.tsx
@@ -0,0 +1,32 @@
+'use client';
+
+import { useRouter } from 'next/navigation';
+
+import { Button } from '@/components/ui/button';
+
+export default function NotFound() {
+ const router = useRouter();
+
+ return (
+
+
+ 404
+
+
+ Something's missing
+
+
+ Sorry, the page you are looking for doesn't exist or has been
+ moved.
+
+
+ router.back()} variant="default" size="lg">
+ Go back
+
+ router.push('/app')} variant="ghost" size="lg">
+ Back to Home
+
+
+
+ );
+}
diff --git a/app/sw.ts b/app/sw.ts
new file mode 100644
index 0000000..a03b9b9
--- /dev/null
+++ b/app/sw.ts
@@ -0,0 +1,25 @@
+import { defaultCache } from '@serwist/next/worker';
+import type { PrecacheEntry, SerwistGlobalConfig } from 'serwist';
+import { Serwist } from 'serwist';
+
+// This declares the value of `injectionPoint` to TypeScript.
+// `injectionPoint` is the string that will be replaced by the
+// actual precache manifest. By default, this string is set to
+// `"self.__SW_MANIFEST"`.
+declare global {
+ interface WorkerGlobalScope extends SerwistGlobalConfig {
+ __SW_MANIFEST: (PrecacheEntry | string)[] | undefined;
+ }
+}
+
+declare const self: ServiceWorkerGlobalScope;
+
+const serwist = new Serwist({
+ precacheEntries: self.__SW_MANIFEST,
+ skipWaiting: true,
+ clientsClaim: true,
+ navigationPreload: true,
+ runtimeCaching: defaultCache
+});
+
+serwist.addEventListeners();
diff --git a/auth.config.ts b/auth.config.ts
new file mode 100644
index 0000000..69718b0
--- /dev/null
+++ b/auth.config.ts
@@ -0,0 +1,16 @@
+import { NextAuthConfig } from 'next-auth';
+import GoogleProvider from 'next-auth/providers/google';
+
+const authConfig = {
+ providers: [
+ GoogleProvider({
+ clientId: process.env.GOOGLE_CLIENT_ID ?? '',
+ clientSecret: process.env.GOOGLE_CLIENT_SECRET ?? ''
+ })
+ ],
+ pages: {
+ signIn: '/' //sigin page
+ }
+} satisfies NextAuthConfig;
+
+export default authConfig;
diff --git a/auth.ts b/auth.ts
new file mode 100644
index 0000000..f428227
--- /dev/null
+++ b/auth.ts
@@ -0,0 +1,4 @@
+import NextAuth from 'next-auth';
+import authConfig from './auth.config';
+
+export const { auth, handlers, signOut, signIn } = NextAuth(authConfig);
diff --git a/bun.lockb b/bun.lockb
new file mode 100644
index 0000000..e3ab63d
Binary files /dev/null and b/bun.lockb differ
diff --git a/components.json b/components.json
new file mode 100644
index 0000000..5045fe9
--- /dev/null
+++ b/components.json
@@ -0,0 +1,16 @@
+{
+ "$schema": "https://ui.shadcn.com/schema.json",
+ "style": "new-york",
+ "rsc": true,
+ "tsx": true,
+ "tailwind": {
+ "config": "tailwind.config.js",
+ "css": "app/globals.css",
+ "baseColor": "zinc",
+ "cssVariables": true
+ },
+ "aliases": {
+ "components": "@/components",
+ "utils": "@/lib/utils"
+ }
+}
diff --git a/components/breadcrumbs.tsx b/components/breadcrumbs.tsx
new file mode 100644
index 0000000..3148339
--- /dev/null
+++ b/components/breadcrumbs.tsx
@@ -0,0 +1,41 @@
+'use client';
+import {
+ Breadcrumb,
+ BreadcrumbItem,
+ BreadcrumbLink,
+ BreadcrumbList,
+ BreadcrumbPage,
+ BreadcrumbSeparator
+} from '@/components/ui/breadcrumb';
+import { useBreadcrumbs } from '@/hooks/use-breadcrumbs';
+import { Slash } from 'lucide-react';
+import { Fragment } from 'react';
+
+export function Breadcrumbs() {
+ const items = useBreadcrumbs();
+ if (items.length === 0) return null;
+
+ return (
+
+
+ {items.map((item, index) => (
+
+ {index !== items.length - 1 && (
+
+ {item.title}
+
+ )}
+ {index < items.length - 1 && (
+
+
+
+ )}
+ {index === items.length - 1 && (
+ {item.title}
+ )}
+
+ ))}
+
+
+ );
+}
diff --git a/components/date-range-picker.tsx b/components/date-range-picker.tsx
new file mode 100644
index 0000000..823eeb6
--- /dev/null
+++ b/components/date-range-picker.tsx
@@ -0,0 +1,63 @@
+'use client';
+import { Button } from '@/components/ui/button';
+import { Calendar } from '@/components/ui/calendar';
+import {
+ Popover,
+ PopoverContent,
+ PopoverTrigger
+} from '@/components/ui/popover';
+import { cn } from '@/lib/utils';
+import { CalendarIcon } from '@radix-ui/react-icons';
+import { addDays, format } from 'date-fns';
+import * as React from 'react';
+import { DateRange } from 'react-day-picker';
+
+export function CalendarDateRangePicker({
+ className
+}: React.HTMLAttributes) {
+ const [date, setDate] = React.useState({
+ from: new Date(2023, 0, 20),
+ to: addDays(new Date(2023, 0, 20), 20)
+ });
+
+ return (
+
+
+
+
+
+ {date?.from ? (
+ date.to ? (
+ <>
+ {format(date.from, 'LLL dd, y')} -{' '}
+ {format(date.to, 'LLL dd, y')}
+ >
+ ) : (
+ format(date.from, 'LLL dd, y')
+ )
+ ) : (
+ Pick a date
+ )}
+
+
+
+
+
+
+
+ );
+}
diff --git a/components/file-uploader.tsx b/components/file-uploader.tsx
new file mode 100644
index 0000000..1d17af5
--- /dev/null
+++ b/components/file-uploader.tsx
@@ -0,0 +1,315 @@
+'use client';
+
+import { CrossIcon, UploadIcon } from 'lucide-react';
+import Image from 'next/image';
+import * as React from 'react';
+import Dropzone, {
+ type DropzoneProps,
+ type FileRejection
+} from 'react-dropzone';
+import { toast } from 'sonner';
+
+import { Button } from '@/components/ui/button';
+import { Progress } from '@/components/ui/progress';
+import { ScrollArea } from '@/components/ui/scroll-area';
+import { useControllableState } from '@/hooks/use-controllable-state';
+import { cn, formatBytes } from '@/lib/utils';
+
+interface FileUploaderProps extends React.HTMLAttributes {
+ /**
+ * Value of the uploader.
+ * @type File[]
+ * @default undefined
+ * @example value={files}
+ */
+ value?: File[];
+
+ /**
+ * Function to be called when the value changes.
+ * @type React.Dispatch>
+ * @default undefined
+ * @example onValueChange={(files) => setFiles(files)}
+ */
+ onValueChange?: React.Dispatch>;
+
+ /**
+ * Function to be called when files are uploaded.
+ * @type (files: File[]) => Promise
+ * @default undefined
+ * @example onUpload={(files) => uploadFiles(files)}
+ */
+ onUpload?: (files: File[]) => Promise;
+
+ /**
+ * Progress of the uploaded files.
+ * @type Record | undefined
+ * @default undefined
+ * @example progresses={{ "file1.png": 50 }}
+ */
+ progresses?: Record;
+
+ /**
+ * Accepted file types for the uploader.
+ * @type { [key: string]: string[]}
+ * @default
+ * ```ts
+ * { "image/*": [] }
+ * ```
+ * @example accept={["image/png", "image/jpeg"]}
+ */
+ accept?: DropzoneProps['accept'];
+
+ /**
+ * Maximum file size for the uploader.
+ * @type number | undefined
+ * @default 1024 * 1024 * 2 // 2MB
+ * @example maxSize={1024 * 1024 * 2} // 2MB
+ */
+ maxSize?: DropzoneProps['maxSize'];
+
+ /**
+ * Maximum number of files for the uploader.
+ * @type number | undefined
+ * @default 1
+ * @example maxFiles={5}
+ */
+ maxFiles?: DropzoneProps['maxFiles'];
+
+ /**
+ * Whether the uploader should accept multiple files.
+ * @type boolean
+ * @default false
+ * @example multiple
+ */
+ multiple?: boolean;
+
+ /**
+ * Whether the uploader is disabled.
+ * @type boolean
+ * @default false
+ * @example disabled
+ */
+ disabled?: boolean;
+}
+
+export function FileUploader(props: FileUploaderProps) {
+ const {
+ value: valueProp,
+ onValueChange,
+ onUpload,
+ progresses,
+ accept = { 'image/*': [] },
+ maxSize = 1024 * 1024 * 2,
+ maxFiles = 1,
+ multiple = false,
+ disabled = false,
+ className,
+ ...dropzoneProps
+ } = props;
+
+ const [files, setFiles] = useControllableState({
+ prop: valueProp,
+ onChange: onValueChange
+ });
+
+ const onDrop = React.useCallback(
+ (acceptedFiles: File[], rejectedFiles: FileRejection[]) => {
+ if (!multiple && maxFiles === 1 && acceptedFiles.length > 1) {
+ toast.error('Cannot upload more than 1 file at a time');
+ return;
+ }
+
+ if ((files?.length ?? 0) + acceptedFiles.length > maxFiles) {
+ toast.error(`Cannot upload more than ${maxFiles} files`);
+ return;
+ }
+
+ const newFiles = acceptedFiles.map((file) =>
+ Object.assign(file, {
+ preview: URL.createObjectURL(file)
+ })
+ );
+
+ const updatedFiles = files ? [...files, ...newFiles] : newFiles;
+
+ setFiles(updatedFiles);
+
+ if (rejectedFiles.length > 0) {
+ rejectedFiles.forEach(({ file }) => {
+ toast.error(`File ${file.name} was rejected`);
+ });
+ }
+
+ if (
+ onUpload &&
+ updatedFiles.length > 0 &&
+ updatedFiles.length <= maxFiles
+ ) {
+ const target =
+ updatedFiles.length > 0 ? `${updatedFiles.length} files` : `file`;
+
+ toast.promise(onUpload(updatedFiles), {
+ loading: `Uploading ${target}...`,
+ success: () => {
+ setFiles([]);
+ return `${target} uploaded`;
+ },
+ error: `Failed to upload ${target}`
+ });
+ }
+ },
+
+ [files, maxFiles, multiple, onUpload, setFiles]
+ );
+
+ function onRemove(index: number) {
+ if (!files) return;
+ const newFiles = files.filter((_, i) => i !== index);
+ setFiles(newFiles);
+ onValueChange?.(newFiles);
+ }
+
+ // Revoke preview url when component unmounts
+ React.useEffect(() => {
+ return () => {
+ if (!files) return;
+ files.forEach((file) => {
+ if (isFileWithPreview(file)) {
+ URL.revokeObjectURL(file.preview);
+ }
+ });
+ };
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, []);
+
+ const isDisabled = disabled || (files?.length ?? 0) >= maxFiles;
+
+ return (
+
+
1 || multiple}
+ disabled={isDisabled}
+ >
+ {({ getRootProps, getInputProps, isDragActive }) => (
+
+
+ {isDragActive ? (
+
+
+
+
+
+ Drop the files here
+
+
+ ) : (
+
+
+
+
+
+
+ Drag {`'n'`} drop files here, or click to select files
+
+
+ You can upload
+ {maxFiles > 1
+ ? ` ${maxFiles === Infinity ? 'multiple' : maxFiles}
+ files (up to ${formatBytes(maxSize)} each)`
+ : ` a file with ${formatBytes(maxSize)}`}
+
+
+
+ )}
+
+ )}
+
+ {files?.length ? (
+
+
+ {files?.map((file, index) => (
+ onRemove(index)}
+ progress={progresses?.[file.name]}
+ />
+ ))}
+
+
+ ) : null}
+
+ );
+}
+
+interface FileCardProps {
+ file: File;
+ onRemove: () => void;
+ progress?: number;
+}
+
+function FileCard({ file, progress, onRemove }: FileCardProps) {
+ return (
+
+
+ {isFileWithPreview(file) ? (
+
+ ) : null}
+
+
+
+ {file.name}
+
+
+ {formatBytes(file.size)}
+
+
+ {progress ?
: null}
+
+
+
+
+
+ Remove file
+
+
+
+ );
+}
+
+function isFileWithPreview(file: File): file is File & { preview: string } {
+ return 'preview' in file && typeof file.preview === 'string';
+}
diff --git a/components/form-card-skeleton.tsx b/components/form-card-skeleton.tsx
new file mode 100644
index 0000000..4de0ce5
--- /dev/null
+++ b/components/form-card-skeleton.tsx
@@ -0,0 +1,52 @@
+import React from 'react';
+import { Card, CardContent, CardHeader } from './ui/card';
+import { Skeleton } from './ui/skeleton';
+
+export default function FormCardSkeleton() {
+ return (
+
+
+ {/* Title */}
+
+
+
+ {/* Image upload area skeleton */}
+
+ {/* Label */}
+ {/* Upload area */}
+
+
+ {/* Grid layout for form fields */}
+
+ {/* Product Name field */}
+
+ {/* Label */}
+ {/* Input */}
+
+
+ {/* Category field */}
+
+ {/* Label */}
+ {/* Select */}
+
+
+ {/* Price field */}
+
+ {/* Label */}
+ {/* Input */}
+
+
+
+ {/* Description field */}
+
+ {/* Label */}
+ {/* Textarea */}
+
+
+ {/* Submit button */}
+
+
+
+
+ );
+}
diff --git a/components/icons.tsx b/components/icons.tsx
new file mode 100644
index 0000000..c1c3fae
--- /dev/null
+++ b/components/icons.tsx
@@ -0,0 +1,107 @@
+import {
+ AlertTriangle,
+ ArrowRight,
+ Check,
+ ChevronLeft,
+ ChevronRight,
+ CircuitBoardIcon,
+ Command,
+ CreditCard,
+ File,
+ FileText,
+ HelpCircle,
+ Image,
+ Laptop,
+ LayoutDashboardIcon,
+ Loader2,
+ LogIn,
+ LucideIcon,
+ LucideProps,
+ LucideShoppingBag,
+ Moon,
+ MoreVertical,
+ Pizza,
+ Plus,
+ Settings,
+ SunMedium,
+ Trash,
+ Twitter,
+ User,
+ UserCircle2Icon,
+ UserPen,
+ UserX2Icon,
+ X
+} from 'lucide-react';
+
+export type Icon = LucideIcon;
+
+export const Icons = {
+ dashboard: LayoutDashboardIcon,
+ logo: Command,
+ login: LogIn,
+ close: X,
+ product: LucideShoppingBag,
+ spinner: Loader2,
+ kanban: CircuitBoardIcon,
+ chevronLeft: ChevronLeft,
+ chevronRight: ChevronRight,
+ trash: Trash,
+ employee: UserX2Icon,
+ post: FileText,
+ page: File,
+ userPen: UserPen,
+ user2: UserCircle2Icon,
+ media: Image,
+ settings: Settings,
+ billing: CreditCard,
+ ellipsis: MoreVertical,
+ add: Plus,
+ warning: AlertTriangle,
+ user: User,
+ arrowRight: ArrowRight,
+ help: HelpCircle,
+ pizza: Pizza,
+ sun: SunMedium,
+ moon: Moon,
+ laptop: Laptop,
+ gitHub: ({ ...props }: LucideProps) => (
+
+
+
+ ),
+ google: ({ ...props }: LucideProps) => (
+
+
+
+
+
+
+
+ ),
+ twitter: Twitter,
+ check: Check
+};
diff --git a/components/layout/ThemeToggle/theme-provider.tsx b/components/layout/ThemeToggle/theme-provider.tsx
new file mode 100644
index 0000000..04f313a
--- /dev/null
+++ b/components/layout/ThemeToggle/theme-provider.tsx
@@ -0,0 +1,11 @@
+'use client';
+
+import { ThemeProvider as NextThemesProvider } from 'next-themes';
+import { type ThemeProviderProps } from 'next-themes/dist/types';
+
+export default function ThemeProvider({
+ children,
+ ...props
+}: ThemeProviderProps) {
+ return {children} ;
+}
diff --git a/components/layout/ThemeToggle/theme-toggle.tsx b/components/layout/ThemeToggle/theme-toggle.tsx
new file mode 100644
index 0000000..b232ac5
--- /dev/null
+++ b/components/layout/ThemeToggle/theme-toggle.tsx
@@ -0,0 +1,37 @@
+'use client';
+import { MoonIcon, SunIcon } from '@radix-ui/react-icons';
+import { useTheme } from 'next-themes';
+
+import { Button } from '@/components/ui/button';
+import {
+ DropdownMenu,
+ DropdownMenuContent,
+ DropdownMenuItem,
+ DropdownMenuTrigger
+} from '@/components/ui/dropdown-menu';
+type CompProps = {};
+export default function ThemeToggle({}: CompProps) {
+ const { setTheme } = useTheme();
+ return (
+
+
+
+
+
+ Toggle theme
+
+
+
+ setTheme('light')}>
+ حالت روشن
+
+ setTheme('dark')}>
+ حالت تیره
+
+ setTheme('system')}>
+ حالت سیستم
+
+
+
+ );
+}
diff --git a/components/layout/auth-info.tsx b/components/layout/auth-info.tsx
new file mode 100644
index 0000000..7c9ecb8
--- /dev/null
+++ b/components/layout/auth-info.tsx
@@ -0,0 +1,43 @@
+'use client';
+import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
+
+import {
+ BadgeCheck,
+ Bell,
+ ChevronRight,
+ ChevronsUpDown,
+ CreditCard,
+ GalleryVerticalEnd,
+ LogOut
+} from 'lucide-react';
+import { signOut, useSession } from 'next-auth/react';
+import { usePathname } from 'next/navigation';
+import * as React from 'react';
+import { Button } from '../ui/button';
+
+export default function AuthInfo() {
+ const { data: session } = useSession();
+ const pathname = usePathname();
+
+ return (
+ signOut()} variant="outline">
+
+
+
+
+ {session?.user?.name?.slice(0, 2)?.toUpperCase() || 'CN'}
+
+
+
+
+ {session?.user?.name || ''}
+
+ {session?.user?.email || ''}
+
+
+
+ );
+}
diff --git a/components/layout/header.tsx b/components/layout/header.tsx
new file mode 100644
index 0000000..e6f26c5
--- /dev/null
+++ b/components/layout/header.tsx
@@ -0,0 +1,25 @@
+import React from 'react';
+import { SidebarTrigger } from '../ui/sidebar';
+import { Separator } from '../ui/separator';
+import { Breadcrumbs } from '../breadcrumbs';
+import SearchInput from '../search-input';
+import ThemeToggle from './ThemeToggle/theme-toggle';
+
+export default function Header() {
+ return (
+
+ );
+}
diff --git a/components/layout/page-container.tsx b/components/layout/page-container.tsx
new file mode 100644
index 0000000..ead51da
--- /dev/null
+++ b/components/layout/page-container.tsx
@@ -0,0 +1,22 @@
+import React from 'react';
+import { ScrollArea } from '@/components/ui/scroll-area';
+
+export default function PageContainer({
+ children,
+ scrollable = true
+}: {
+ children: React.ReactNode;
+ scrollable?: boolean;
+}) {
+ return (
+ <>
+ {scrollable ? (
+
+ {children}
+
+ ) : (
+ {children}
+ )}
+ >
+ );
+}
diff --git a/components/layout/providers.tsx b/components/layout/providers.tsx
new file mode 100644
index 0000000..2560313
--- /dev/null
+++ b/components/layout/providers.tsx
@@ -0,0 +1,20 @@
+'use client';
+import React from 'react';
+import ThemeProvider from './ThemeToggle/theme-provider';
+import { SessionProvider, SessionProviderProps } from 'next-auth/react';
+
+export default function Providers({
+ session,
+ children
+}: {
+ session: SessionProviderProps['session'];
+ children: React.ReactNode;
+}) {
+ return (
+ <>
+
+ {children}
+
+ >
+ );
+}
diff --git a/components/modals/alert-modal.tsx b/components/modals/alert-modal.tsx
new file mode 100644
index 0000000..bc33ae9
--- /dev/null
+++ b/components/modals/alert-modal.tsx
@@ -0,0 +1,46 @@
+'use client';
+import { useEffect, useState } from 'react';
+import { Button } from '@/components/ui/button';
+import { Modal } from '@/components/ui/modal';
+
+interface AlertModalProps {
+ isOpen: boolean;
+ onClose: () => void;
+ onConfirm: () => void;
+ loading: boolean;
+}
+
+export const AlertModal: React.FC = ({
+ isOpen,
+ onClose,
+ onConfirm,
+ loading
+}) => {
+ const [isMounted, setIsMounted] = useState(false);
+
+ useEffect(() => {
+ setIsMounted(true);
+ }, []);
+
+ if (!isMounted) {
+ return null;
+ }
+
+ return (
+
+
+
+ Cancel
+
+
+ Continue
+
+
+
+ );
+};
diff --git a/components/mode-toggle.tsx b/components/mode-toggle.tsx
new file mode 100644
index 0000000..77a6f14
--- /dev/null
+++ b/components/mode-toggle.tsx
@@ -0,0 +1,37 @@
+'use client';
+
+import * as React from 'react';
+import { useTheme } from 'next-themes';
+import { MoonIcon, SunIcon } from '@radix-ui/react-icons';
+
+import { Button } from '@/components/ui/button';
+import {
+ Tooltip,
+ TooltipContent,
+ TooltipTrigger,
+ TooltipProvider
+} from '@/components/ui/tooltip';
+
+export function ModeToggle() {
+ const { setTheme, theme } = useTheme();
+
+ return (
+
+
+
+ setTheme(theme === 'dark' ? 'light' : 'dark')}
+ >
+
+
+ Switch Theme
+
+
+ Switch Theme
+
+
+ );
+}
diff --git a/components/nav-main.tsx b/components/nav-main.tsx
new file mode 100644
index 0000000..6510855
--- /dev/null
+++ b/components/nav-main.tsx
@@ -0,0 +1,73 @@
+'use client';
+
+import { ChevronRight, type LucideIcon } from 'lucide-react';
+
+import {
+ Collapsible,
+ CollapsibleContent,
+ CollapsibleTrigger
+} from '@/components/ui/collapsible';
+import {
+ SidebarGroup,
+ SidebarGroupLabel,
+ SidebarMenu,
+ SidebarMenuButton,
+ SidebarMenuItem,
+ SidebarMenuSub,
+ SidebarMenuSubButton,
+ SidebarMenuSubItem
+} from '@/components/ui/sidebar';
+
+export function NavMain({
+ items
+}: {
+ items: {
+ title: string;
+ url: string;
+ icon?: LucideIcon;
+ isActive?: boolean;
+ items?: {
+ title: string;
+ url: string;
+ }[];
+ }[];
+}) {
+ return (
+
+ Platform
+
+ {items.map((item) => (
+
+
+
+
+ {item.icon && }
+ {item.title}
+
+
+
+
+
+ {item.items?.map((subItem) => (
+
+
+
+ {subItem.title}
+
+
+
+ ))}
+
+
+
+
+ ))}
+
+
+ );
+}
diff --git a/components/nav-projects.tsx b/components/nav-projects.tsx
new file mode 100644
index 0000000..ff3e600
--- /dev/null
+++ b/components/nav-projects.tsx
@@ -0,0 +1,89 @@
+'use client';
+
+import {
+ Folder,
+ Forward,
+ MoreHorizontal,
+ Trash2,
+ type LucideIcon
+} from 'lucide-react';
+
+import {
+ DropdownMenu,
+ DropdownMenuContent,
+ DropdownMenuItem,
+ DropdownMenuSeparator,
+ DropdownMenuTrigger
+} from '@/components/ui/dropdown-menu';
+import {
+ SidebarGroup,
+ SidebarGroupLabel,
+ SidebarMenu,
+ SidebarMenuAction,
+ SidebarMenuButton,
+ SidebarMenuItem,
+ useSidebar
+} from '@/components/ui/sidebar';
+
+export function NavProjects({
+ projects
+}: {
+ projects: {
+ name: string;
+ url: string;
+ icon: LucideIcon;
+ }[];
+}) {
+ const { isMobile } = useSidebar();
+
+ return (
+
+ Projects
+
+ {projects.map((item) => (
+
+
+
+
+ {item.name}
+
+
+
+
+
+
+ More
+
+
+
+
+
+ View Project
+
+
+
+ Share Project
+
+
+
+
+ Delete Project
+
+
+
+
+ ))}
+
+
+
+ More
+
+
+
+
+ );
+}
diff --git a/components/providers/theme-provider.tsx b/components/providers/theme-provider.tsx
new file mode 100644
index 0000000..530befd
--- /dev/null
+++ b/components/providers/theme-provider.tsx
@@ -0,0 +1,9 @@
+'use client';
+
+import * as React from 'react';
+import { ThemeProvider as NextThemesProvider } from 'next-themes';
+import { type ThemeProviderProps } from 'next-themes/dist/types';
+
+export function ThemeProvider({ children, ...props }: ThemeProviderProps) {
+ return {children} ;
+}
diff --git a/components/search-input.tsx b/components/search-input.tsx
new file mode 100644
index 0000000..011b519
--- /dev/null
+++ b/components/search-input.tsx
@@ -0,0 +1,24 @@
+'use client';
+import { Input } from '@/components/ui/input';
+import { useKBar } from 'kbar';
+import { ArrowRight, Search } from 'lucide-react';
+import { Button } from './ui/button';
+
+export default function SearchInput() {
+ const { query } = useKBar();
+ return (
+
+
+
+ جستجو
+
+ ⌘ K
+
+
+
+ );
+}
diff --git a/components/team-switcher.tsx b/components/team-switcher.tsx
new file mode 100644
index 0000000..bd10958
--- /dev/null
+++ b/components/team-switcher.tsx
@@ -0,0 +1,89 @@
+'use client';
+
+import * as React from 'react';
+import { ChevronsUpDown, Plus } from 'lucide-react';
+
+import {
+ DropdownMenu,
+ DropdownMenuContent,
+ DropdownMenuItem,
+ DropdownMenuLabel,
+ DropdownMenuSeparator,
+ DropdownMenuShortcut,
+ DropdownMenuTrigger
+} from '@/components/ui/dropdown-menu';
+import {
+ SidebarMenu,
+ SidebarMenuButton,
+ SidebarMenuItem,
+ useSidebar
+} from '@/components/ui/sidebar';
+
+export function TeamSwitcher({
+ teams
+}: {
+ teams: {
+ name: string;
+ logo: React.ElementType;
+ plan: string;
+ }[];
+}) {
+ const { isMobile } = useSidebar();
+ const [activeTeam, setActiveTeam] = React.useState(teams[0]);
+
+ return (
+
+
+
+
+
+
+
+
+ {activeTeam.name}
+
+ {activeTeam.plan}
+
+
+
+
+
+
+ Teams
+
+ {teams.map((team, index) => (
+ setActiveTeam(team)}
+ className="gap-2 p-2"
+ >
+
+
+
+ {team.name}
+ ⌘{index + 1}
+
+ ))}
+
+
+
+ Add team
+
+
+
+
+
+ );
+}
diff --git a/components/ui/accordion.tsx b/components/ui/accordion.tsx
new file mode 100644
index 0000000..0e8b3b1
--- /dev/null
+++ b/components/ui/accordion.tsx
@@ -0,0 +1,57 @@
+'use client';
+
+import * as React from 'react';
+import * as AccordionPrimitive from '@radix-ui/react-accordion';
+import { ChevronDownIcon } from '@radix-ui/react-icons';
+
+import { cn } from '@/lib/utils';
+
+const Accordion = AccordionPrimitive.Root;
+
+const AccordionItem = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+));
+AccordionItem.displayName = 'AccordionItem';
+
+const AccordionTrigger = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, children, ...props }, ref) => (
+
+ svg]:rotate-180',
+ className
+ )}
+ {...props}
+ >
+ {children}
+
+
+
+));
+AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName;
+
+const AccordionContent = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, children, ...props }, ref) => (
+
+ {children}
+
+));
+AccordionContent.displayName = AccordionPrimitive.Content.displayName;
+
+export { Accordion, AccordionItem, AccordionTrigger, AccordionContent };
diff --git a/components/ui/alert-dialog.tsx b/components/ui/alert-dialog.tsx
new file mode 100644
index 0000000..ca10623
--- /dev/null
+++ b/components/ui/alert-dialog.tsx
@@ -0,0 +1,145 @@
+'use client';
+
+import * as React from 'react';
+import * as AlertDialogPrimitive from '@radix-ui/react-alert-dialog';
+
+import { cn } from '@/lib/utils';
+import { buttonVariants } from '@/components/ui/button';
+
+const AlertDialog = AlertDialogPrimitive.Root;
+
+const AlertDialogTrigger = AlertDialogPrimitive.Trigger;
+
+const AlertDialogPortal = AlertDialogPrimitive.Portal;
+
+const AlertDialogOverlay = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+));
+AlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName;
+
+const AlertDialogContent = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+
+
+
+));
+AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName;
+
+const AlertDialogHeader = ({
+ className,
+ ...props
+}: React.HTMLAttributes) => (
+
+);
+AlertDialogHeader.displayName = 'AlertDialogHeader';
+
+const AlertDialogFooter = ({
+ className,
+ ...props
+}: React.HTMLAttributes) => (
+
+);
+AlertDialogFooter.displayName = 'AlertDialogFooter';
+
+const AlertDialogTitle = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+));
+AlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName;
+
+const AlertDialogDescription = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+));
+AlertDialogDescription.displayName =
+ AlertDialogPrimitive.Description.displayName;
+
+const AlertDialogAction = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+));
+AlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName;
+
+const AlertDialogCancel = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+ // yes, you have to set a timeout
+ setTimeout(() => (document.body.style.pointerEvents = ''), 100)
+ }
+ {...props}
+ />
+));
+AlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName;
+
+export {
+ AlertDialog,
+ AlertDialogPortal,
+ AlertDialogOverlay,
+ AlertDialogTrigger,
+ AlertDialogContent,
+ AlertDialogHeader,
+ AlertDialogFooter,
+ AlertDialogTitle,
+ AlertDialogDescription,
+ AlertDialogAction,
+ AlertDialogCancel
+};
diff --git a/components/ui/alert.tsx b/components/ui/alert.tsx
new file mode 100644
index 0000000..6f5b983
--- /dev/null
+++ b/components/ui/alert.tsx
@@ -0,0 +1,59 @@
+import * as React from 'react';
+import { cva, type VariantProps } from 'class-variance-authority';
+
+import { cn } from '@/lib/utils';
+
+const alertVariants = cva(
+ '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',
+ {
+ variants: {
+ variant: {
+ default: 'bg-background text-foreground',
+ destructive:
+ 'border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive'
+ }
+ },
+ defaultVariants: {
+ variant: 'default'
+ }
+ }
+);
+
+const Alert = React.forwardRef<
+ HTMLDivElement,
+ React.HTMLAttributes & VariantProps
+>(({ className, variant, ...props }, ref) => (
+
+));
+Alert.displayName = 'Alert';
+
+const AlertTitle = React.forwardRef<
+ HTMLParagraphElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+));
+AlertTitle.displayName = 'AlertTitle';
+
+const AlertDescription = React.forwardRef<
+ HTMLParagraphElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+));
+AlertDescription.displayName = 'AlertDescription';
+
+export { Alert, AlertTitle, AlertDescription };
diff --git a/components/ui/avatar.tsx b/components/ui/avatar.tsx
new file mode 100644
index 0000000..1346957
--- /dev/null
+++ b/components/ui/avatar.tsx
@@ -0,0 +1,50 @@
+'use client';
+
+import * as React from 'react';
+import * as AvatarPrimitive from '@radix-ui/react-avatar';
+
+import { cn } from '@/lib/utils';
+
+const Avatar = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+));
+Avatar.displayName = AvatarPrimitive.Root.displayName;
+
+const AvatarImage = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+));
+AvatarImage.displayName = AvatarPrimitive.Image.displayName;
+
+const AvatarFallback = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+));
+AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName;
+
+export { Avatar, AvatarImage, AvatarFallback };
diff --git a/components/ui/background-beams.tsx b/components/ui/background-beams.tsx
new file mode 100644
index 0000000..f0809b5
--- /dev/null
+++ b/components/ui/background-beams.tsx
@@ -0,0 +1,141 @@
+'use client';
+import React from 'react';
+import { motion } from 'framer-motion';
+import { cn } from '@/lib/utils';
+
+export const BackgroundBeams = React.memo(
+ ({ className }: { className?: string }) => {
+ const paths = [
+ 'M-380 -189C-380 -189 -312 216 152 343C616 470 684 875 684 875',
+ 'M-373 -197C-373 -197 -305 208 159 335C623 462 691 867 691 867',
+ 'M-366 -205C-366 -205 -298 200 166 327C630 454 698 859 698 859',
+ 'M-359 -213C-359 -213 -291 192 173 319C637 446 705 851 705 851',
+ 'M-352 -221C-352 -221 -284 184 180 311C644 438 712 843 712 843',
+ 'M-345 -229C-345 -229 -277 176 187 303C651 430 719 835 719 835',
+ 'M-338 -237C-338 -237 -270 168 194 295C658 422 726 827 726 827',
+ 'M-331 -245C-331 -245 -263 160 201 287C665 414 733 819 733 819',
+ 'M-324 -253C-324 -253 -256 152 208 279C672 406 740 811 740 811',
+ 'M-317 -261C-317 -261 -249 144 215 271C679 398 747 803 747 803',
+ 'M-310 -269C-310 -269 -242 136 222 263C686 390 754 795 754 795',
+ 'M-303 -277C-303 -277 -235 128 229 255C693 382 761 787 761 787',
+ 'M-296 -285C-296 -285 -228 120 236 247C700 374 768 779 768 779',
+ 'M-289 -293C-289 -293 -221 112 243 239C707 366 775 771 775 771',
+ 'M-282 -301C-282 -301 -214 104 250 231C714 358 782 763 782 763',
+ 'M-275 -309C-275 -309 -207 96 257 223C721 350 789 755 789 755',
+ 'M-268 -317C-268 -317 -200 88 264 215C728 342 796 747 796 747',
+ 'M-261 -325C-261 -325 -193 80 271 207C735 334 803 739 803 739',
+ 'M-254 -333C-254 -333 -186 72 278 199C742 326 810 731 810 731',
+ 'M-247 -341C-247 -341 -179 64 285 191C749 318 817 723 817 723',
+ 'M-240 -349C-240 -349 -172 56 292 183C756 310 824 715 824 715',
+ 'M-233 -357C-233 -357 -165 48 299 175C763 302 831 707 831 707',
+ 'M-226 -365C-226 -365 -158 40 306 167C770 294 838 699 838 699',
+ 'M-219 -373C-219 -373 -151 32 313 159C777 286 845 691 845 691',
+ 'M-212 -381C-212 -381 -144 24 320 151C784 278 852 683 852 683',
+ 'M-205 -389C-205 -389 -137 16 327 143C791 270 859 675 859 675',
+ 'M-198 -397C-198 -397 -130 8 334 135C798 262 866 667 866 667',
+ 'M-191 -405C-191 -405 -123 0 341 127C805 254 873 659 873 659',
+ 'M-184 -413C-184 -413 -116 -8 348 119C812 246 880 651 880 651',
+ 'M-177 -421C-177 -421 -109 -16 355 111C819 238 887 643 887 643',
+ 'M-170 -429C-170 -429 -102 -24 362 103C826 230 894 635 894 635',
+ 'M-163 -437C-163 -437 -95 -32 369 95C833 222 901 627 901 627',
+ 'M-156 -445C-156 -445 -88 -40 376 87C840 214 908 619 908 619',
+ 'M-149 -453C-149 -453 -81 -48 383 79C847 206 915 611 915 611',
+ 'M-142 -461C-142 -461 -74 -56 390 71C854 198 922 603 922 603',
+ 'M-135 -469C-135 -469 -67 -64 397 63C861 190 929 595 929 595',
+ 'M-128 -477C-128 -477 -60 -72 404 55C868 182 936 587 936 587',
+ 'M-121 -485C-121 -485 -53 -80 411 47C875 174 943 579 943 579',
+ 'M-114 -493C-114 -493 -46 -88 418 39C882 166 950 571 950 571',
+ 'M-107 -501C-107 -501 -39 -96 425 31C889 158 957 563 957 563',
+ 'M-100 -509C-100 -509 -32 -104 432 23C896 150 964 555 964 555',
+ 'M-93 -517C-93 -517 -25 -112 439 15C903 142 971 547 971 547',
+ 'M-86 -525C-86 -525 -18 -120 446 7C910 134 978 539 978 539',
+ 'M-79 -533C-79 -533 -11 -128 453 -1C917 126 985 531 985 531',
+ 'M-72 -541C-72 -541 -4 -136 460 -9C924 118 992 523 992 523',
+ 'M-65 -549C-65 -549 3 -144 467 -17C931 110 999 515 999 515',
+ 'M-58 -557C-58 -557 10 -152 474 -25C938 102 1006 507 1006 507',
+ 'M-51 -565C-51 -565 17 -160 481 -33C945 94 1013 499 1013 499',
+ 'M-44 -573C-44 -573 24 -168 488 -41C952 86 1020 491 1020 491',
+ 'M-37 -581C-37 -581 31 -176 495 -49C959 78 1027 483 1027 483'
+ ];
+ return (
+
+
+
+
+ {paths.map((path, index) => (
+
+ ))}
+
+ {paths.map((path, index) => (
+
+
+
+
+
+
+ ))}
+
+
+
+
+
+
+
+
+
+ );
+ }
+);
+
+BackgroundBeams.displayName = 'BackgroundBeams';
diff --git a/components/ui/badge.tsx b/components/ui/badge.tsx
new file mode 100644
index 0000000..4ce58c0
--- /dev/null
+++ b/components/ui/badge.tsx
@@ -0,0 +1,36 @@
+import * as React from 'react';
+import { cva, type VariantProps } from 'class-variance-authority';
+
+import { cn } from '@/lib/utils';
+
+const badgeVariants = cva(
+ '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',
+ {
+ variants: {
+ variant: {
+ default:
+ 'border-transparent bg-primary text-primary-foreground shadow hover:bg-primary/80',
+ secondary:
+ 'border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80',
+ destructive:
+ 'border-transparent bg-destructive text-destructive-foreground shadow hover:bg-destructive/80',
+ outline: 'text-foreground'
+ }
+ },
+ defaultVariants: {
+ variant: 'default'
+ }
+ }
+);
+
+export interface BadgeProps
+ extends React.HTMLAttributes,
+ VariantProps {}
+
+function Badge({ className, variant, ...props }: BadgeProps) {
+ return (
+
+ );
+}
+
+export { Badge, badgeVariants };
diff --git a/components/ui/breadcrumb.tsx b/components/ui/breadcrumb.tsx
new file mode 100644
index 0000000..f209322
--- /dev/null
+++ b/components/ui/breadcrumb.tsx
@@ -0,0 +1,115 @@
+import * as React from 'react';
+import { ChevronRightIcon, DotsHorizontalIcon } from '@radix-ui/react-icons';
+import { Slot } from '@radix-ui/react-slot';
+
+import { cn } from '@/lib/utils';
+
+const Breadcrumb = React.forwardRef<
+ HTMLElement,
+ React.ComponentPropsWithoutRef<'nav'> & {
+ separator?: React.ReactNode;
+ }
+>(({ ...props }, ref) => );
+Breadcrumb.displayName = 'Breadcrumb';
+
+const BreadcrumbList = React.forwardRef<
+ HTMLOListElement,
+ React.ComponentPropsWithoutRef<'ol'>
+>(({ className, ...props }, ref) => (
+
+));
+BreadcrumbList.displayName = 'BreadcrumbList';
+
+const BreadcrumbItem = React.forwardRef<
+ HTMLLIElement,
+ React.ComponentPropsWithoutRef<'li'>
+>(({ className, ...props }, ref) => (
+
+));
+BreadcrumbItem.displayName = 'BreadcrumbItem';
+
+const BreadcrumbLink = React.forwardRef<
+ HTMLAnchorElement,
+ React.ComponentPropsWithoutRef<'a'> & {
+ asChild?: boolean;
+ }
+>(({ asChild, className, ...props }, ref) => {
+ const Comp = asChild ? Slot : 'a';
+
+ return (
+
+ );
+});
+BreadcrumbLink.displayName = 'BreadcrumbLink';
+
+const BreadcrumbPage = React.forwardRef<
+ HTMLSpanElement,
+ React.ComponentPropsWithoutRef<'span'>
+>(({ className, ...props }, ref) => (
+
+));
+BreadcrumbPage.displayName = 'BreadcrumbPage';
+
+const BreadcrumbSeparator = ({
+ children,
+ className,
+ ...props
+}: React.ComponentProps<'li'>) => (
+ svg]:size-3.5', className)}
+ {...props}
+ >
+ {children ?? }
+
+);
+BreadcrumbSeparator.displayName = 'BreadcrumbSeparator';
+
+const BreadcrumbEllipsis = ({
+ className,
+ ...props
+}: React.ComponentProps<'span'>) => (
+
+
+ More
+
+);
+BreadcrumbEllipsis.displayName = 'BreadcrumbElipssis';
+
+export {
+ Breadcrumb,
+ BreadcrumbList,
+ BreadcrumbItem,
+ BreadcrumbLink,
+ BreadcrumbPage,
+ BreadcrumbSeparator,
+ BreadcrumbEllipsis
+};
diff --git a/components/ui/button.tsx b/components/ui/button.tsx
new file mode 100644
index 0000000..61c1494
--- /dev/null
+++ b/components/ui/button.tsx
@@ -0,0 +1,57 @@
+import * as React from 'react';
+import { Slot } from '@radix-ui/react-slot';
+import { cva, type VariantProps } from 'class-variance-authority';
+
+import { cn } from '@/lib/utils';
+
+const buttonVariants = cva(
+ 'inline-flex items-center justify-center 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',
+ {
+ variants: {
+ variant: {
+ default:
+ 'bg-accent text-accent-foreground hover:bg-accent-foreground hover:text-accent hover:border-accent border transition-all duration-300',
+ destructive:
+ 'bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90',
+ outline:
+ 'border border-input bg-transparent shadow-sm hover:bg-accent hover:text-accent-foreground',
+ secondary:
+ 'bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80',
+ ghost: 'hover:bg-accent hover:text-accent-foreground',
+ link: 'text-primary underline-offset-4 hover:underline'
+ },
+ size: {
+ default: 'h-9 px-4 py-2',
+ sm: 'h-8 rounded-md px-3 text-xs',
+ lg: 'h-10 rounded-md px-8',
+ icon: 'h-9 w-9'
+ }
+ },
+ defaultVariants: {
+ variant: 'default',
+ size: 'default'
+ }
+ }
+);
+
+export interface ButtonProps
+ extends React.ButtonHTMLAttributes,
+ VariantProps {
+ asChild?: boolean;
+}
+
+const Button = React.forwardRef(
+ ({ className, variant, size, asChild = false, ...props }, ref) => {
+ const Comp = asChild ? Slot : 'button';
+ return (
+
+ );
+ }
+);
+Button.displayName = 'Button';
+
+export { Button, buttonVariants };
diff --git a/components/ui/calendar.tsx b/components/ui/calendar.tsx
new file mode 100644
index 0000000..d79949c
--- /dev/null
+++ b/components/ui/calendar.tsx
@@ -0,0 +1,71 @@
+'use client';
+
+import * as React from 'react';
+import { ChevronLeftIcon, ChevronRightIcon } from '@radix-ui/react-icons';
+import { DayPicker } from 'react-day-picker';
+
+import { cn } from '@/lib/utils';
+import { buttonVariants } from '@/components/ui/button';
+
+export type CalendarProps = React.ComponentProps;
+
+function Calendar({
+ className,
+ classNames,
+ showOutsideDays = true,
+ ...props
+}: CalendarProps) {
+ return (
+ .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'
+ : '[&:has([aria-selected])]:rounded-md'
+ ),
+ day: cn(
+ buttonVariants({ variant: 'ghost' }),
+ 'h-8 w-8 p-0 font-normal aria-selected:opacity-100'
+ ),
+ day_range_start: 'day-range-start',
+ day_range_end: 'day-range-end',
+ day_selected:
+ 'bg-primary text-primary-foreground hover:bg-primary hover:text-primary-foreground focus:bg-primary focus:text-primary-foreground',
+ day_today: 'bg-accent text-accent-foreground',
+ day_outside: 'text-muted-foreground opacity-50',
+ day_disabled: 'text-muted-foreground opacity-50',
+ day_range_middle:
+ 'aria-selected:bg-accent aria-selected:text-accent-foreground',
+ day_hidden: 'invisible',
+ ...classNames
+ }}
+ components={{
+ IconLeft: ({ ...props }) => ,
+ IconRight: ({ ...props }) =>
+ }}
+ {...props}
+ />
+ );
+}
+Calendar.displayName = 'Calendar';
+
+export { Calendar };
diff --git a/components/ui/card.tsx b/components/ui/card.tsx
new file mode 100644
index 0000000..b9b4400
--- /dev/null
+++ b/components/ui/card.tsx
@@ -0,0 +1,83 @@
+import * as React from 'react';
+
+import { cn } from '@/lib/utils';
+
+const Card = React.forwardRef<
+ HTMLDivElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+));
+Card.displayName = 'Card';
+
+const CardHeader = React.forwardRef<
+ HTMLDivElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+));
+CardHeader.displayName = 'CardHeader';
+
+const CardTitle = React.forwardRef<
+ HTMLDivElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+));
+CardTitle.displayName = 'CardTitle';
+
+const CardDescription = React.forwardRef<
+ HTMLDivElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+));
+CardDescription.displayName = 'CardDescription';
+
+const CardContent = React.forwardRef<
+ HTMLDivElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+));
+CardContent.displayName = 'CardContent';
+
+const CardFooter = React.forwardRef<
+ HTMLDivElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+));
+CardFooter.displayName = 'CardFooter';
+
+export {
+ Card,
+ CardHeader,
+ CardFooter,
+ CardTitle,
+ CardDescription,
+ CardContent
+};
diff --git a/components/ui/chart.tsx b/components/ui/chart.tsx
new file mode 100644
index 0000000..90e0864
--- /dev/null
+++ b/components/ui/chart.tsx
@@ -0,0 +1,364 @@
+'use client';
+
+import * as React from 'react';
+import * as RechartsPrimitive from 'recharts';
+
+import { cn } from '@/lib/utils';
+
+// Format: { THEME_NAME: CSS_SELECTOR }
+const THEMES = { light: '', dark: '.dark' } as const;
+
+export type ChartConfig = {
+ [k in string]: {
+ label?: React.ReactNode;
+ icon?: React.ComponentType;
+ } & (
+ | { color?: string; theme?: never }
+ | { color?: never; theme: Record }
+ );
+};
+
+type ChartContextProps = {
+ config: ChartConfig;
+};
+
+const ChartContext = React.createContext(null);
+
+function useChart() {
+ const context = React.useContext(ChartContext);
+
+ if (!context) {
+ throw new Error('useChart must be used within a ');
+ }
+
+ return context;
+}
+
+const ChartContainer = React.forwardRef<
+ HTMLDivElement,
+ React.ComponentProps<'div'> & {
+ config: ChartConfig;
+ children: React.ComponentProps<
+ typeof RechartsPrimitive.ResponsiveContainer
+ >['children'];
+ }
+>(({ id, className, children, config, ...props }, ref) => {
+ const uniqueId = React.useId();
+ const chartId = `chart-${id || uniqueId.replace(/:/g, '')}`;
+
+ return (
+
+
+
+ {/* Debounce the chart to avoid laggy behavior on window resize */}
+
+ {children}
+
+
+
+ );
+});
+ChartContainer.displayName = 'Chart';
+
+const ChartStyle = ({ id, config }: { id: string; config: ChartConfig }) => {
+ const colorConfig = Object.entries(config).filter(
+ ([_, config]) => config.theme || config.color
+ );
+
+ if (!colorConfig.length) {
+ return null;
+ }
+
+ return (
+