I am batman

This commit is contained in:
2026-07-12 20:27:42 -04:00
commit 997feca642
242 changed files with 40038 additions and 0 deletions

11
.eslintrc.json Normal file
View File

@@ -0,0 +1,11 @@
{
"extends": "next/core-web-vitals",
"plugins": ["@typescript-eslint"],
"rules": {
"@typescript-eslint/no-unused-vars": "warn",
"import/no-unresolved": "off",
"import/named": "off",
"no-console": "warn",
"react-hooks/exhaustive-deps": "warn"
}
}

5
.github/instructions/00-main.md vendored Normal file
View File

@@ -0,0 +1,5 @@
# Frontend Instruction Entry Point (Do Not Edit)
1. Read the user's prompt input completely before taking action.
2. Act as a frontend engineer for this repository's architecture and UI patterns.
3. Update only the scoped instruction files in this directory when requested; do not modify this file.

15
.github/instructions/api.md vendored Normal file
View File

@@ -0,0 +1,15 @@
# Frontend API Instructions
## Scope
- Applies to API calls in `src/lib/api/api.ts` and feature-level data hooks.
## Rules
- Keep axios-based API calls centralized in `src/lib/api/api.ts`.
- Map backend response shapes to frontend-friendly models in hooks or API mappers.
- Follow existing tickets pattern: `api.ts` + feature hook (`useX`) + presentational components.
- Use optimistic updates carefully; ensure API failures do not leave inconsistent UI state.
- Keep request payload keys aligned with backend contract (`section_id`, `position`, etc.).
## Error Handling
- Surface recoverable errors to the user with clear toast messages.
- Avoid silent failures for create/update/delete flows.

View File

@@ -0,0 +1,14 @@
# Frontend Containerization Instructions
## Scope
- Applies to frontend Docker/build/runtime concerns for Next.js.
## Rules
- Keep `.next` as a generated artifact; never rely on stale local cache in source control.
- If dev manifest/cache issues appear, clear `.next` and restart dev server.
- Keep environment-driven API base URL behavior explicit and documented.
- Ensure Docker/compose settings match the expected runtime port and host bindings.
## Build Health
- Distinguish pre-existing lint/type issues from feature-specific regressions in reports.
- Do not weaken lint/type checks just to pass builds.

15
.github/instructions/state-and-dnd.md vendored Normal file
View File

@@ -0,0 +1,15 @@
# State and DnD Instructions
## Scope
- Applies to stateful feature logic and drag/drop interactions (e.g., Kanban with dnd-kit).
## Rules
- Keep business/data mutations in hooks; keep components mostly presentational.
- Persist ordering changes deterministically using explicit `position` updates.
- Avoid nested competing scroll containers when horizontal drag/drop is involved.
- Ensure drag overlays and scroll behavior work for mouse and touch sensors.
- When replacing local persistence with API-backed state, remove stale store dependencies cleanly.
## Reliability
- Debounce or batch expensive reorder API calls where appropriate.
- After structural changes (move/delete section), refresh canonical state from API when needed.

15
.github/instructions/ui.md vendored Normal file
View File

@@ -0,0 +1,15 @@
# UI Instructions
## Scope
- Applies to page/component work under `src/app`, `src/components`, and `src/features`.
## Rules
- Preserve existing design language and component primitives from `src/components/ui`.
- Keep layouts responsive with explicit overflow control for dense views (e.g., kanban horizontal overflow).
- Prefer composition over one-off custom widgets; reuse dialog, card, table, and form primitives.
- Keep interaction states clear (loading, empty, error) for data-driven pages.
- Avoid visual regressions in header/sidebar containment when introducing horizontal scroll regions.
## Accessibility
- Maintain keyboard access for dialogs, menus, and drag handles.
- Keep meaningful `sr-only` labels for icon-only controls.

50
.gitignore vendored Normal file
View File

@@ -0,0 +1,50 @@
# 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*
.env*.local
gitmvp.md
backend_documentation.md
# vercel
.vercel
# typescript
*.tsbuildinfo
next-env.d.ts
.idea/
# clerk configuration (can include secrets)
/.clerk/
# cursor
.cursorrules
/memory-bank
# Sentry Config File
.env.sentry-build-plugin

1
.husky/pre-commit Normal file
View File

@@ -0,0 +1 @@
npx lint-staged

1
.husky/pre-push Normal file
View File

@@ -0,0 +1 @@
pnpm run build

4
.npmrc Normal file
View File

@@ -0,0 +1,4 @@
legacy-peer-deps=true
shamefully-hoist=true
# added this to fix sentry warning
# https://docs.sentry.io/platforms/javascript/troubleshooting/#pnpm-resolving-import-in-the-middle-external-package-errors

48
.prettierignore Normal file
View File

@@ -0,0 +1,48 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.js
# testing
/coverage
# next.js
.next
/.next/
/out/
# production
/build
# misc
.DS_Store
*.pem
# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.pnpm-debug.log*
# local env files
.env.local
.env.development.local
.env.test.local
.env.production.local
# vercel
.vercel
# changelog
CHANGELOG.md
pnpm-lock.yaml
# Other common ignores
node_modules
.next
build
dist
ico

12
.prettierrc Normal file
View File

@@ -0,0 +1,12 @@
{
"arrowParens": "always",
"bracketSpacing": true,
"semi": true,
"useTabs": false,
"trailingComma": "none",
"jsxSingleQuote": true,
"singleQuote": true,
"tabWidth": 2,
"endOfLine": "lf",
"plugins": ["prettier-plugin-tailwindcss"]
}

31
.vscode/launch.json vendored Normal file
View File

@@ -0,0 +1,31 @@
{
"version": "0.2.0",
"configurations": [
{
"name": "Next.js: debug server-side",
"type": "node-terminal",
"request": "launch",
"command": "npm run dev"
},
{
"name": "Next.js: debug client-side",
"type": "chrome",
"request": "launch",
"url": "http://localhost:3000"
},
{
"name": "Next.js: debug full stack",
"type": "node-terminal",
"request": "launch",
"command": "npm run dev",
"serverReadyAction": {
"pattern": "- Local:.+(https?://.+)",
"uriFormat": "%s",
"action": "debugWithChrome"
},
"env": {
"NEXT_PUBLIC_SENTRY_DISABLED": "true"
}
}
]
}

21
LICENSE Normal file
View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2023 Kiranism
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

115
README.md Normal file
View File

@@ -0,0 +1,115 @@
<!-- <picture>
<source media="(prefers-color-scheme: dark)" srcset="https://user-images.githubusercontent.com/9113740/201498864-2a900c64-d88f-4ed4-b5cf-770bcb57e1f5.png">
<source media="(prefers-color-scheme: light)" srcset="https://user-images.githubusercontent.com/9113740/201498152-b171abb8-9225-487a-821c-6ff49ee48579.png">
</picture>
<div align="center"><strong>Next.js Admin Dashboard Starter Template With Shadcn-ui</strong></div>
<div align="center">Built with the Next.js 15 App Router</div>
<br />
<div align="center">
<a href="https://dub.sh/shadcn-dashboard">View Demo</a>
<span>
</div>
## Overview
This is a starter template using the following stack:
- Framework - [Next.js 15](https://nextjs.org/13)
- Language - [TypeScript](https://www.typescriptlang.org)
- Auth - [Clerk](https://go.clerk.com/ILdYhn7)
- Error tracking - [<picture><img alt="Sentry" src="public/assets/sentry.svg">
</picture>](https://sentry.io/for/nextjs/?utm_source=github&utm_medium=paid-community&utm_campaign=general-fy26q2-nextjs&utm_content=github-banner-project-tryfree)
- Styling - [Tailwind CSS v4](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/)
- Tables - [Tanstack Data Tables](https://ui.shadcn.com/docs/components/data-table) • [Dice table](https://www.diceui.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 Tanstack start dashboard template, here is the [repo](https://git.new/tanstack-start-dashboard)._
## Pages
| Pages | Specifications |
| :------------------------------------------------------------------------------------ | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| [Signup / Signin](https://go.clerk.com/ILdYhn7) | Authentication with **Clerk** provides secure authentication and user management with multiple sign-in options including passwordless authentication, social logins, and enterprise SSO - all designed to enhance security while delivering a seamless user experience. |
| [Dashboard (Overview)](https://shadcn-dashboard.kiranism.dev/dashboard) | Cards with Recharts graphs for analytics. Parallel routes in the overview sections feature independent loading, error handling, and isolated component rendering. |
| [Product](https://shadcn-dashboard.kiranism.dev/dashboard/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://shadcn-dashboard.kiranism.dev/dashboard/product/new) | A Product Form with shadcn form (react-hook-form + zod). |
| [Profile](https://shadcn-dashboard.kiranism.dev/dashboard/profile) | Clerk's full-featured account management UI that allows users to manage their profile and security settings |
| [Kanban Board](https://shadcn-dashboard.kiranism.dev/dashboard/kanban) | A Drag n Drop task management board with dnd-kit and zustand to persist state locally. |
| [Not Found](https://shadcn-dashboard.kiranism.dev/dashboard/notfound) | Not Found Page Added in the root level |
| [Global Error](https://sentry.io/for/nextjs/?utm_source=github&utm_medium=paid-community&utm_campaign=general-fy26q2-nextjs&utm_content=github-banner-project-tryfree) | A centralized error page that captures and displays errors across the application. Integrated with **Sentry** to log errors, provide detailed reports, and enable replay functionality for better debugging. |
## Feature based organization
```plaintext
src/
├── app/ # Next.js App Router directory
│ ├── (auth)/ # Auth route group
│ │ ├── (signin)/
│ ├── (dashboard)/ # Dashboard route group
│ │ ├── layout.tsx
│ │ ├── loading.tsx
│ │ └── page.tsx
│ └── api/ # API routes
│
├── components/ # Shared components
│ ├── ui/ # UI components (buttons, inputs, etc.)
│ └── layout/ # Layout components (header, sidebar, etc.)
│
├── features/ # Feature-based modules
│ ├── feature/
│ │ ├── components/ # Feature-specific components
│ │ ├── actions/ # Server actions
│ │ ├── schemas/ # Form validation schemas
│ │ └── utils/ # Feature-specific utilities
│ │
├── lib/ # Core utilities and configurations
│ ├── auth/ # Auth configuration
│ ├── db/ # Database utilities
│ └── utils/ # Shared utilities
│
├── hooks/ # Custom hooks
│ └── use-debounce.ts
│
├── stores/ # Zustand stores
│ └── dashboard-store.ts
│
└── types/ # TypeScript types
└── index.ts
```
## Getting Started
> [!NOTE]
> We are using **Next 15** with **React 19**, follow these steps:
Clone the repo:
```
git clone https://github.com/Kiranism/next-shadcn-dashboard-starter.git
```
- `pnpm install` ( we have legacy-peer-deps=true added in the .npmrc)
- 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.
- `pnpm run dev`
##### Environment Configuration Setup
To configure the environment for this project, refer to the `env.example.txt` file. This file contains the necessary environment variables required for authentication and error tracking.
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! 🥂 -->

16
components.json Normal file
View File

@@ -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"
}
}

65
env.example.txt Normal file
View File

@@ -0,0 +1,65 @@
# =================================================================
# Authentication Configuration (Clerk)
# =================================================================
# IMPORTANT: This template supports Clerk's keyless mode!
# You can start using the app immediately without any configuration.
# When you're ready to claim your application, simply click the Clerk
# popup at the bottom of the screen to get your API keys.
# Required: Clerk API Keys (Leave empty for keyless mode)
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=
CLERK_SECRET_KEY=
# Authentication Redirect URLs
# These control where users are directed after authentication actions
NEXT_PUBLIC_CLERK_SIGN_IN_URL="/auth/sign-in"
NEXT_PUBLIC_CLERK_SIGN_UP_URL="/auth/sign-up"
NEXT_PUBLIC_CLERK_AFTER_SIGN_IN_URL="/dashboard/overview"
NEXT_PUBLIC_CLERK_AFTER_SIGN_UP_URL="/dashboard/overview"
# =================================================================
# Error Tracking Configuration (Sentry)
# =================================================================
# To set up Sentry error tracking:
# 1. Create an account at https://sentry.io
# 2. Create a new project for Next.js
# 3. Follow the setup instructions below
# Step 1: Sentry DSN (Required)
# Found at: Settings > Projects > [Your Project] > Client Keys (DSN)
NEXT_PUBLIC_SENTRY_DSN= #Example: https://****@****.ingest.sentry.io/****
# Step 2: Organization & Project Details
# Found at: Settings > Organization > General Settings
NEXT_PUBLIC_SENTRY_ORG= # Example: acme-corp
NEXT_PUBLIC_SENTRY_PROJECT= # Example: nextjs-dashboard
# Step 3: Sentry Auth Token
# Sentry can automatically provide readable stack traces for errors using source maps, requiring a Sentry auth token.
# More info: https://docs.sentry.io/platforms/javascript/guides/nextjs/manual-setup/#step-4-add-readable-stack-traces-with-source-maps-optional
SENTRY_AUTH_TOKEN= #Example: sntrys_************************************
# Step 4: Environment Control (Optional)
# Set to 'true' to disable Sentry in development
NEXT_PUBLIC_SENTRY_DISABLED= "false"
# =================================================================
# Important Notes:
# =================================================================
# 1. Rename this file to '.env' for local development
# 2. Never commit the actual '.env' file to version control
# 3. Make sure to replace all placeholder values with real ones
# 4. Keep your secret keys private and never share them

62
next.config.ts Normal file
View File

@@ -0,0 +1,62 @@
import type { NextConfig } from 'next';
import { withSentryConfig } from '@sentry/nextjs';
// Define the base Next.js configuration
const baseConfig: NextConfig = {
images: {
remotePatterns: [
{
protocol: 'https',
hostname: 'api.slingacademy.com',
port: ''
}
],
domains: [
'cdn.jsdelivr.net', // <-- add this line
'avatars.githubusercontent.com', // <-- add this line
// add other domains if needed
],
},
transpilePackages: ['geist']
};
let configWithPlugins = baseConfig;
// Conditionally enable Sentry configuration
if (!process.env.NEXT_PUBLIC_SENTRY_DISABLED) {
configWithPlugins = withSentryConfig(configWithPlugins, {
// For all available options, see:
// https://www.npmjs.com/package/@sentry/webpack-plugin#options
// FIXME: Add your Sentry organization and project names
org: process.env.NEXT_PUBLIC_SENTRY_ORG,
project: process.env.NEXT_PUBLIC_SENTRY_PROJECT,
// Only print logs for uploading source maps in CI
silent: !process.env.CI,
// For all available options, see:
// https://docs.sentry.io/platforms/javascript/guides/nextjs/manual-setup/
// Upload a larger set of source maps for prettier stack traces (increases build time)
widenClientFileUpload: true,
// Upload a larger set of source maps for prettier stack traces (increases build time)
reactComponentAnnotation: {
enabled: true
},
// Route browser requests to Sentry through a Next.js rewrite to circumvent ad-blockers.
// This can increase your server load as well as your hosting bill.
// Note: Check that the configured route will not match with your Next.js middleware, otherwise reporting of client-
// side errors will fail.
tunnelRoute: '/monitoring',
// Automatically tree-shake Sentry logger statements to reduce bundle size
disableLogger: true,
// Disable Sentry telemetry
telemetry: false
});
}
const nextConfig = configWithPlugins;
export default nextConfig;

12477
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

120
package.json Normal file
View File

@@ -0,0 +1,120 @@
{
"name": "OpsMastery.v5-Client",
"version": "1.0.0",
"private": true,
"author": {
"name": "Kiran",
"url": "https://github.com/Kiranism"
},
"scripts": {
"dev": "next dev --turbopack",
"build": "next build",
"start": "next start",
"lint": "next lint",
"lint:fix": "eslint src --fix && pnpm format",
"lint:strict": "eslint --max-warnings=0 src",
"format": "prettier --write .",
"format:check": "prettier -c -w .",
"prepare": "husky"
},
"lint-staged": {
"**/*.{js,jsx,tsx,ts,css,less,scss,sass}": [
"prettier --write --no-error-on-unmatched-pattern"
]
},
"dependencies": {
"@dnd-kit/core": "^6.3.1",
"@dnd-kit/modifiers": "^7.0.0",
"@dnd-kit/sortable": "^8.0.0",
"@dnd-kit/utilities": "^3.2.2",
"@hookform/resolvers": "^3.9.1",
"@radix-ui/react-accordion": "^1.2.3",
"@radix-ui/react-alert-dialog": "^1.1.6",
"@radix-ui/react-aspect-ratio": "^1.1.2",
"@radix-ui/react-avatar": "^1.1.3",
"@radix-ui/react-checkbox": "^1.1.4",
"@radix-ui/react-collapsible": "^1.1.3",
"@radix-ui/react-context-menu": "^2.2.6",
"@radix-ui/react-dialog": "^1.1.6",
"@radix-ui/react-dropdown-menu": "^2.1.6",
"@radix-ui/react-hover-card": "^1.1.6",
"@radix-ui/react-icons": "^1.3.2",
"@radix-ui/react-label": "^2.1.2",
"@radix-ui/react-menubar": "^1.1.6",
"@radix-ui/react-navigation-menu": "^1.2.5",
"@radix-ui/react-popover": "^1.1.6",
"@radix-ui/react-progress": "^1.1.2",
"@radix-ui/react-radio-group": "^1.2.3",
"@radix-ui/react-scroll-area": "^1.2.3",
"@radix-ui/react-select": "^2.1.6",
"@radix-ui/react-separator": "^1.1.2",
"@radix-ui/react-slider": "^1.2.3",
"@radix-ui/react-slot": "^1.1.2",
"@radix-ui/react-switch": "^1.1.3",
"@radix-ui/react-tabs": "^1.1.3",
"@radix-ui/react-toast": "^1.2.6",
"@radix-ui/react-toggle": "^1.1.2",
"@radix-ui/react-toggle-group": "^1.1.2",
"@radix-ui/react-tooltip": "^1.1.8",
"@sentry/nextjs": "^9.19.0",
"@tabler/icons-react": "^3.31.0",
"@tailwindcss/postcss": "^4.0.0",
"@tanstack/react-table": "^8.21.2",
"axios": "^1.11.0",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"cmdk": "^1.1.1",
"date-fns": "^4.1.0",
"eslint": "8.48.0",
"eslint-config-next": "15.1.0",
"input-otp": "^1.4.2",
"jwt-decode": "^4.0.0",
"kbar": "^0.1.0-beta.45",
"lucide-react": "^0.476.0",
"match-sorter": "^8.0.0",
"motion": "^11.17.0",
"next": "15.3.2",
"next-themes": "^0.4.6",
"nextjs-toploader": "^3.7.15",
"nuqs": "^2.4.1",
"postcss": "8.4.49",
"react": "19.0.0",
"react-day-picker": "^8.10.1",
"react-dom": "19.0.0",
"react-dropzone": "^14.3.5",
"react-hook-form": "^7.54.1",
"react-icons": "^5.5.0",
"react-resizable-panels": "^2.1.7",
"react-responsive": "^10.0.0",
"recharts": "^2.15.1",
"sharp": "^0.33.5",
"sonner": "^1.7.1",
"sort-by": "^1.2.0",
"tailwind-merge": "^3.0.2",
"tailwindcss": "^4.0.0",
"tailwindcss-animate": "^1.0.7",
"typescript": "5.7.2",
"uuid": "^11.0.3",
"vaul": "^1.1.2",
"zod": "^3.24.1",
"zustand": "^5.0.7"
},
"devDependencies": {
"@faker-js/faker": "^9.3.0",
"@types/node": "22.10.2",
"@types/react": "19.0.1",
"@types/react-dom": "19.0.2",
"@types/sort-by": "^1.2.3",
"@types/uuid": "^10.0.0",
"@typescript-eslint/eslint-plugin": "^6.11.0",
"husky": "^9.1.7",
"lint-staged": "^15.2.11",
"prettier": "3.4.2",
"prettier-plugin-tailwindcss": "^0.6.11",
"tw-animate-css": "^1.2.4"
},
"overrides": {
"@types/react": "19.0.1",
"@types/react-dom": "19.0.2"
}
}

9098
pnpm-lock.yaml generated Normal file

File diff suppressed because it is too large Load Diff

5
postcss.config.js Normal file
View File

@@ -0,0 +1,5 @@
module.exports = {
plugins: {
'@tailwindcss/postcss': {}
}
};

1
public/assets/sentry.svg Normal file
View File

@@ -0,0 +1 @@
<svg class="css-lfbo6j e1igk8x04" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 222 66" width="80" height="24" style="background-color: rgb(88, 70, 116);"><path d="M29,2.26a4.67,4.67,0,0,0-8,0L14.42,13.53A32.21,32.21,0,0,1,32.17,40.19H27.55A27.68,27.68,0,0,0,12.09,17.47L6,28a15.92,15.92,0,0,1,9.23,12.17H4.62A.76.76,0,0,1,4,39.06l2.94-5a10.74,10.74,0,0,0-3.36-1.9l-2.91,5a4.54,4.54,0,0,0,1.69,6.24A4.66,4.66,0,0,0,4.62,44H19.15a19.4,19.4,0,0,0-8-17.31l2.31-4A23.87,23.87,0,0,1,23.76,44H36.07a35.88,35.88,0,0,0-16.41-31.8l4.67-8a.77.77,0,0,1,1.05-.27c.53.29,20.29,34.77,20.66,35.17a.76.76,0,0,1-.68,1.13H40.6q.09,1.91,0,3.81h4.78A4.59,4.59,0,0,0,50,39.43a4.49,4.49,0,0,0-.62-2.28Z M124.32,28.28,109.56,9.22h-3.68V34.77h3.73V15.19l15.18,19.58h3.26V9.22h-3.73ZM87.15,23.54h13.23V20.22H87.14V12.53h14.93V9.21H83.34V34.77h18.92V31.45H87.14ZM71.59,20.3h0C66.44,19.06,65,18.08,65,15.7c0-2.14,1.89-3.59,4.71-3.59a12.06,12.06,0,0,1,7.07,2.55l2-2.83a14.1,14.1,0,0,0-9-3c-5.06,0-8.59,3-8.59,7.27,0,4.6,3,6.19,8.46,7.52C74.51,24.74,76,25.78,76,28.11s-2,3.77-5.09,3.77a12.34,12.34,0,0,1-8.3-3.26l-2.25,2.69a15.94,15.94,0,0,0,10.42,3.85c5.48,0,9-2.95,9-7.51C79.75,23.79,77.47,21.72,71.59,20.3ZM195.7,9.22l-7.69,12-7.64-12h-4.46L186,24.67V34.78h3.84V24.55L200,9.22Zm-64.63,3.46h8.37v22.1h3.84V12.68h8.37V9.22H131.08ZM169.41,24.8c3.86-1.07,6-3.77,6-7.63,0-4.91-3.59-8-9.38-8H154.67V34.76h3.8V25.58h6.45l6.48,9.2h4.44l-7-9.82Zm-10.95-2.5V12.6h7.17c3.74,0,5.88,1.77,5.88,4.84s-2.29,4.86-5.84,4.86Z" transform="translate(11, 11)" fill="#ffffff"></path></svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

1
public/next.svg Normal file
View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 394 80"><path fill="#000" d="M262 0h68.5v12.7h-27.2v66.6h-13.6V12.7H262V0ZM149 0v12.7H94v20.4h44.3v12.6H94v21h55v12.6H80.5V0h68.7zm34.3 0h-17.8l63.8 79.4h17.9l-32-39.7 32-39.6h-17.9l-23 28.6-23-28.6zm18.3 56.7-9-11-27.1 33.7h17.8l18.3-22.7z"/><path fill="#000" d="M81 79.3 17 0H0v79.3h13.6V17l50.2 62.3H81Zm252.6-.4c-1 0-1.8-.4-2.5-1s-1.1-1.6-1.1-2.6.3-1.8 1-2.5 1.6-1 2.6-1 1.8.3 2.5 1a3.4 3.4 0 0 1 .6 4.3 3.7 3.7 0 0 1-3 1.8zm23.2-33.5h6v23.3c0 2.1-.4 4-1.3 5.5a9.1 9.1 0 0 1-3.8 3.5c-1.6.8-3.5 1.3-5.7 1.3-2 0-3.7-.4-5.3-1s-2.8-1.8-3.7-3.2c-.9-1.3-1.4-3-1.4-5h6c.1.8.3 1.6.7 2.2s1 1.2 1.6 1.5c.7.4 1.5.5 2.4.5 1 0 1.8-.2 2.4-.6a4 4 0 0 0 1.6-1.8c.3-.8.5-1.8.5-3V45.5zm30.9 9.1a4.4 4.4 0 0 0-2-3.3 7.5 7.5 0 0 0-4.3-1.1c-1.3 0-2.4.2-3.3.5-.9.4-1.6 1-2 1.6a3.5 3.5 0 0 0-.3 4c.3.5.7.9 1.3 1.2l1.8 1 2 .5 3.2.8c1.3.3 2.5.7 3.7 1.2a13 13 0 0 1 3.2 1.8 8.1 8.1 0 0 1 3 6.5c0 2-.5 3.7-1.5 5.1a10 10 0 0 1-4.4 3.5c-1.8.8-4.1 1.2-6.8 1.2-2.6 0-4.9-.4-6.8-1.2-2-.8-3.4-2-4.5-3.5a10 10 0 0 1-1.7-5.6h6a5 5 0 0 0 3.5 4.6c1 .4 2.2.6 3.4.6 1.3 0 2.5-.2 3.5-.6 1-.4 1.8-1 2.4-1.7a4 4 0 0 0 .8-2.4c0-.9-.2-1.6-.7-2.2a11 11 0 0 0-2.1-1.4l-3.2-1-3.8-1c-2.8-.7-5-1.7-6.6-3.2a7.2 7.2 0 0 1-2.4-5.7 8 8 0 0 1 1.7-5 10 10 0 0 1 4.3-3.5c2-.8 4-1.2 6.4-1.2 2.3 0 4.4.4 6.2 1.2 1.8.8 3.2 2 4.3 3.4 1 1.4 1.5 3 1.5 5h-5.8z"/></svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

1
public/vercel.svg Normal file
View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 283 64"><path fill="black" d="M141 16c-11 0-19 7-19 18s9 18 20 18c7 0 13-3 16-7l-7-5c-2 3-6 4-9 4-5 0-9-3-10-7h28v-3c0-11-8-18-19-18zm-9 15c1-4 4-7 9-7s8 3 9 7h-18zm117-15c-11 0-19 7-19 18s9 18 20 18c6 0 12-3 16-7l-8-5c-2 3-5 4-8 4-5 0-9-3-11-7h28l1-3c0-11-8-18-19-18zm-10 15c2-4 5-7 10-7s8 3 9 7h-19zm-39 3c0 6 4 10 10 10 4 0 7-2 9-5l8 5c-3 5-9 8-17 8-11 0-19-7-19-18s8-18 19-18c8 0 14 3 17 8l-8 5c-2-3-5-5-9-5-6 0-10 4-10 10zm83-29v46h-9V5h9zM37 0l37 64H0L37 0zm92 5-27 48L74 5h10l18 30 17-30h10zm59 12v10l-3-1c-6 0-10 4-10 10v15h-9V17h9v9c0-5 6-9 13-9z"/></svg>

After

Width:  |  Height:  |  Size: 629 B

View File

@@ -0,0 +1,5 @@
import ResetPasswordView from '@/features/auth/components/reset-password-view';
export default function Page() {
return <ResetPasswordView />;
}

View File

@@ -0,0 +1,27 @@
import { Metadata } from 'next';
import SignInViewPage from '@/features/auth/components/sign-in-view';
export const metadata: Metadata = {
title: 'Authentication | Sign In',
description: 'Sign In page for authentication.'
};
export default async function Page() {
let stars = 3000; // Default value
try {
const response = await fetch(
'https://api.github.com/repos/kiranism/next-shadcn-dashboard-starter',
{
next: { revalidate: 86400 }
}
);
if (response.ok) {
const data = await response.json();
stars = data.stargazers_count || stars; // Update stars if API response is valid
}
} catch (error) {
}
return <SignInViewPage stars={stars} />;
}

View File

@@ -0,0 +1,27 @@
import { Metadata } from 'next';
import SignUpViewPage from '@/features/auth/components/sign-up-view';
export const metadata: Metadata = {
title: 'Authentication | Sign Up',
description: 'Sign Up page for authentication.'
};
export default async function Page() {
let stars = 3000; // Default value
try {
const response = await fetch(
'https://api.github.com/repos/kiranism/next-shadcn-dashboard-starter',
{
next: { revalidate: 86400 }
}
);
if (response.ok) {
const data = await response.json();
stars = data.stargazers_count || stars; // Update stars if API response is valid
}
} catch (error) {
}
return <SignUpViewPage stars={stars} />;
}

View File

@@ -0,0 +1,22 @@
import { Card, CardHeader, CardContent, CardTitle, CardDescription } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import Link from 'next/link';
export default function VerificationFailedPage() {
return (
<div className="flex min-h-screen w-full items-center justify-center bg-background p-4">
<Card className="w-full max-w-md shadow-lg">
<CardHeader>
<CardTitle>Verification Failed</CardTitle>
<CardDescription>We could not verify your email. The link may be invalid or expired.</CardDescription>
</CardHeader>
<CardContent className="flex flex-col items-center gap-4">
<span className="text-red-500 text-2xl font-bold">❌ Verification Failed</span>
<Button asChild variant="outline" className="w-full mt-2">
<Link href="/auth/sign-up">Try Again</Link>
</Button>
</CardContent>
</Card>
</div>
);
}

View File

@@ -0,0 +1,22 @@
import { Card, CardHeader, CardContent, CardTitle, CardDescription } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import Link from 'next/link';
export default function VerificationSuccessPage() {
return (
<div className="flex min-h-screen w-full items-center justify-center bg-background p-4">
<Card className="w-full max-w-md shadow-lg">
<CardHeader>
<CardTitle>Email Verified</CardTitle>
<CardDescription>Your email has been successfully verified. You can now sign in.</CardDescription>
</CardHeader>
<CardContent className="flex flex-col items-center gap-4">
<span className="text-green-600 text-2xl font-bold">✅ Verified</span>
<Button asChild className="w-full mt-2">
<Link href="/auth/sign-in">Go to Sign In</Link>
</Button>
</CardContent>
</Card>
</div>
);
}

View File

@@ -0,0 +1,52 @@
"use client"
import PageContainer from '@/components/layout/page-container'
import CalendarSidebar from '@/features/calendar/components/CalendarSidebar'
import React, { useState } from 'react'
type CalendarEvent = {
id: string
title: string
date: string
description?: string
}
const dummyEvents: CalendarEvent[] = [
{ id: '1', title: 'Team Meeting', date: '2025-09-05', description: 'Discuss project updates' },
{ id: '2', title: 'Release Day', date: '2025-09-10', description: 'OpsMastery v5 launch' },
{ id: '3', title: '1:1 with Cody', date: '2025-09-12' },
{ id: '4', title: 'Sprint Planning', date: '2025-09-15', description: 'Plan next sprint tasks' },
{ id: '5', title: 'Demo Day', date: '2025-09-20', description: 'Showcase new features' },
]
const Page = () => {
const [selectedEvent, setSelectedEvent] = useState<CalendarEvent | null>(null)
const handleSelectEvent = (event: CalendarEvent) => {
setSelectedEvent(event)
}
return (
<PageContainer scrollable>
<div className="flex flex-1 min-h-0">
{/* Calendar Sidebar */}
<CalendarSidebar events={dummyEvents} onSelectEvent={handleSelectEvent} />
{/* Main Content */}
<div className="flex-1 overflow-auto">
{selectedEvent ? (
<div className="p-4">
<h2 className="font-semibold">{selectedEvent.title}</h2>
<p className="text-sm text-muted-foreground">{selectedEvent.date}</p>
{selectedEvent.description && <p>{selectedEvent.description}</p>}
</div>
) : (
<div className="p-4 text-muted-foreground">Select an event to see details.</div>
)}
</div>
</div>
</PageContainer>
)
}
export default Page

View File

@@ -0,0 +1,18 @@
'use client';
import { useState } from "react";
import ChatContainer from "@/features/chat/components/ChatContainer";
export default function Home() {
const [isSidebarCollapsed, setIsSidebarCollapsed] = useState(false);
const [selectedChatUser, setSelectedChatUser] = useState({
id: "1",
username: "Alice",
email: "alice@example.com",
avatar: "",
});
return (
<ChatContainer />
);
}

View File

@@ -0,0 +1,9 @@
import KanbanViewPage from '@/features/kanban/components/kanban-view-page';
export const metadata = {
title: 'Dashboard : Kanban view'
};
export default function page() {
return <KanbanViewPage />;
}

View File

@@ -0,0 +1,32 @@
import KBar from '@/components/kbar';
import AppSidebar from '@/components/layout/app-sidebar';
import Header from '@/components/layout/header';
import { SidebarInset, SidebarProvider } from '@/components/ui/sidebar';
import type { Metadata } from 'next';
import { cookies } from 'next/headers';
export const metadata: Metadata = {
title: 'Next Shadcn Dashboard Starter',
description: 'Basic dashboard with Next.js and Shadcn'
};
export default async function DashboardLayout({
children
}: {
children: React.ReactNode;
}) {
// Persisting the sidebar state in the cookie.
const cookieStore = await cookies();
const defaultOpen = cookieStore.get("sidebar_state")?.value === "true"
return (
<KBar>
<SidebarProvider defaultOpen={defaultOpen}>
<AppSidebar />
<SidebarInset>
<Header />
{children}
</SidebarInset>
</SidebarProvider>
</KBar>
);
}

View File

@@ -0,0 +1,16 @@
'use client';
import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert';
import { IconAlertCircle } from '@tabler/icons-react';
export default function AreaStatsError({ error }: { error: Error }) {
return (
<Alert variant='destructive'>
<IconAlertCircle className='h-4 w-4' />
<AlertTitle>Error</AlertTitle>
<AlertDescription>
Failed to load area statistics: {error.message}
</AlertDescription>
</Alert>
);
}

View File

@@ -0,0 +1,5 @@
import { AreaGraphSkeleton } from '@/features/overview/components/area-graph-skeleton';
export default function Loading() {
return <AreaGraphSkeleton />;
}

View File

@@ -0,0 +1,7 @@
import { delay } from '@/constants/mock-api';
import { AreaGraph } from '@/features/overview/components/area-graph';
export default async function AreaStats() {
await await delay(2000);
return <AreaGraph />;
}

View File

@@ -0,0 +1,60 @@
'use client';
import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert';
import { Button } from '@/components/ui/button';
import { Card, CardContent, CardHeader } from '@/components/ui/card';
import { IconAlertCircle } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import { useEffect, useTransition } from 'react';
import * as Sentry from '@sentry/nextjs';
interface StatsErrorProps {
error: Error;
reset: () => void; // Add reset function from error boundary
}
export default function StatsError({ error, reset }: StatsErrorProps) {
const router = useRouter();
const [isPending, startTransition] = useTransition();
useEffect(() => {
Sentry.captureException(error);
}, [error]);
// the reload fn ensures the refresh is deffered until the next render phase allowing react to handle any pending states before processing
const reload = () => {
startTransition(() => {
router.refresh();
reset();
});
};
return (
<Card className='border-red-500'>
<CardHeader className='flex flex-col items-stretch space-y-0 border-b p-0 sm:flex-row'>
<div className='flex flex-1 flex-col justify-center gap-1 px-6 py-5 sm:py-6'>
<Alert variant='destructive' className='border-none'>
<IconAlertCircle className='h-4 w-4' />
<AlertTitle>Error</AlertTitle>
<AlertDescription className='mt-2'>
Failed to load statistics: {error.message}
</AlertDescription>
</Alert>
</div>
</CardHeader>
<CardContent className='flex h-[316px] items-center justify-center p-6'>
<div className='text-center'>
<p className='text-muted-foreground mb-4 text-sm'>
Unable to display statistics at this time
</p>
<Button
onClick={() => reload()}
variant='outline'
className='min-w-[120px]'
disabled={isPending}
>
Try again
</Button>
</div>
</CardContent>
</Card>
);
}

View File

@@ -0,0 +1,5 @@
import { BarGraphSkeleton } from '@/features/overview/components/bar-graph-skeleton';
export default function Loading() {
return <BarGraphSkeleton />;
}

View File

@@ -0,0 +1,8 @@
import { delay } from '@/constants/mock-api';
import { BarGraph } from '@/features/overview/components/bar-graph';
export default async function BarStats() {
await await delay(1000);
return <BarGraph />;
}

View File

@@ -0,0 +1,16 @@
'use client';
import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert';
import { IconAlertCircle } from '@tabler/icons-react';
export default function PieStatsError({ error }: { error: Error }) {
return (
<Alert variant='destructive'>
<IconAlertCircle className='h-4 w-4' />
<AlertTitle>Error</AlertTitle>
<AlertDescription>
Failed to load pie statistics: {error.message}
</AlertDescription>
</Alert>
);
}

View File

@@ -0,0 +1,5 @@
import { PieGraphSkeleton } from '@/features/overview/components/pie-graph-skeleton';
export default function Loading() {
return <PieGraphSkeleton />;
}

View File

@@ -0,0 +1,7 @@
import { delay } from '@/constants/mock-api';
import { PieGraph } from '@/features/overview/components/pie-graph';
export default async function Stats() {
await delay(1000);
return <PieGraph />;
}

View File

@@ -0,0 +1,16 @@
'use client';
import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert';
import { IconAlertCircle } from '@tabler/icons-react';
export default function SalesError({ error }: { error: Error }) {
return (
<Alert variant='destructive'>
<IconAlertCircle className='h-4 w-4' />
<AlertTitle>Error</AlertTitle>
<AlertDescription>
Failed to load sales data: {error.message}
</AlertDescription>
</Alert>
);
}

View File

@@ -0,0 +1,6 @@
import { RecentSalesSkeleton } from '@/features/overview/components/recent-sales-skeleton';
import React from 'react';
export default function Loading() {
return <RecentSalesSkeleton />;
}

View File

@@ -0,0 +1,7 @@
import { delay } from '@/constants/mock-api';
import { RecentSales } from '@/features/overview/components/recent-sales';
export default async function Sales() {
await delay(3000);
return <RecentSales />;
}

View File

@@ -0,0 +1,16 @@
'use client';
import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert';
import { IconAlertCircle } from '@tabler/icons-react';
export default function OverviewError({ error }: { error: Error }) {
return (
<Alert variant='destructive'>
<IconAlertCircle className='h-4 w-4' />
<AlertTitle>Error</AlertTitle>
<AlertDescription>
Failed to load statistics: {error.message}
</AlertDescription>
</Alert>
);
}

View File

@@ -0,0 +1,131 @@
import PageContainer from '@/components/layout/page-container';
import { Badge } from '@/components/ui/badge';
import {
Card,
CardHeader,
CardTitle,
CardDescription,
CardAction,
CardFooter
} from '@/components/ui/card';
import { IconTrendingDown, IconTrendingUp } from '@tabler/icons-react';
import React from 'react';
export default function OverViewLayout({
sales,
pie_stats,
bar_stats,
area_stats
}: {
sales: React.ReactNode;
pie_stats: React.ReactNode;
bar_stats: React.ReactNode;
area_stats: React.ReactNode;
}) {
return (
<PageContainer>
<div className='flex flex-1 flex-col space-y-2'>
<div className='*:data-[slot=card]:from-primary/5 *:data-[slot=card]:to-card dark:*:data-[slot=card]:bg-card grid grid-cols-1 gap-4 *:data-[slot=card]:bg-gradient-to-t *:data-[slot=card]:shadow-xs md:grid-cols-2 lg:grid-cols-4'>
<Card className='@container/card'>
<CardHeader>
<CardDescription>Total Revenue</CardDescription>
<CardTitle className='text-2xl font-semibold tabular-nums @[250px]/card:text-3xl'>
$1,250.00
</CardTitle>
<CardAction>
<Badge variant='outline'>
<IconTrendingUp />
+12.5%
</Badge>
</CardAction>
</CardHeader>
<CardFooter className='flex-col items-start gap-1.5 text-sm'>
<div className='line-clamp-1 flex gap-2 font-medium'>
Trending up this month <IconTrendingUp className='size-4' />
</div>
<div className='text-muted-foreground'>
Visitors for the last 6 months
</div>
</CardFooter>
</Card>
<Card className='@container/card'>
<CardHeader>
<CardDescription>New Customers</CardDescription>
<CardTitle className='text-2xl font-semibold tabular-nums @[250px]/card:text-3xl'>
1,234
</CardTitle>
<CardAction>
<Badge variant='outline'>
<IconTrendingDown />
-20%
</Badge>
</CardAction>
</CardHeader>
<CardFooter className='flex-col items-start gap-1.5 text-sm'>
<div className='line-clamp-1 flex gap-2 font-medium'>
Down 20% this period <IconTrendingDown className='size-4' />
</div>
<div className='text-muted-foreground'>
Acquisition needs attention
</div>
</CardFooter>
</Card>
<Card className='@container/card'>
<CardHeader>
<CardDescription>Active Accounts</CardDescription>
<CardTitle className='text-2xl font-semibold tabular-nums @[250px]/card:text-3xl'>
45,678
</CardTitle>
<CardAction>
<Badge variant='outline'>
<IconTrendingUp />
+12.5%
</Badge>
</CardAction>
</CardHeader>
<CardFooter className='flex-col items-start gap-1.5 text-sm'>
<div className='line-clamp-1 flex gap-2 font-medium'>
Strong user retention <IconTrendingUp className='size-4' />
</div>
<div className='text-muted-foreground'>
Engagement exceed targets
</div>
</CardFooter>
</Card>
<Card className='@container/card'>
<CardHeader>
<CardDescription>Growth Rate</CardDescription>
<CardTitle className='text-2xl font-semibold tabular-nums @[250px]/card:text-3xl'>
4.5%
</CardTitle>
<CardAction>
<Badge variant='outline'>
<IconTrendingUp />
+4.5%
</Badge>
</CardAction>
</CardHeader>
<CardFooter className='flex-col items-start gap-1.5 text-sm'>
<div className='line-clamp-1 flex gap-2 font-medium'>
Steady performance increase{' '}
<IconTrendingUp className='size-4' />
</div>
<div className='text-muted-foreground'>
Meets growth projections
</div>
</CardFooter>
</Card>
</div>
<div className='grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-7'>
<div className='col-span-4'>{bar_stats}</div>
<div className='col-span-4 md:col-span-3'>
{sales}
</div>
<div className='col-span-4'>{area_stats}</div>
<div className='col-span-4 md:col-span-3'>{pie_stats}</div>
</div>
</div>
</PageContainer>
);
}

View File

@@ -0,0 +1,9 @@
"use client";
export default function DashboardOverview() {
return (
<div>
Welcome to your dashboard!
</div>
);
}

View File

@@ -0,0 +1,10 @@
"use client";
export default function Dashboard() {
return (
<div>
{/* Main dashboard UI here */}
Welcome to your dashboard!
</div>
);
}

View File

@@ -0,0 +1,23 @@
import FormCardSkeleton from '@/components/form-card-skeleton';
import PageContainer from '@/components/layout/page-container';
import { Suspense } from 'react';
import ProductViewPage from '@/features/products/components/product-view-page';
export const metadata = {
title: 'Dashboard : Product View'
};
type PageProps = { params: Promise<{ productId: string }> };
export default async function Page(props: PageProps) {
const params = await props.params;
return (
<PageContainer scrollable>
<div className='flex-1 space-y-4'>
<Suspense fallback={<FormCardSkeleton />}>
<ProductViewPage productId={params.productId} />
</Suspense>
</div>
</PageContainer>
);
}

View File

@@ -0,0 +1,54 @@
import PageContainer from '@/components/layout/page-container';
import { buttonVariants } from '@/components/ui/button';
import { Heading } from '@/components/ui/heading';
import { Separator } from '@/components/ui/separator';
import { DataTableSkeleton } from '@/components/ui/table/data-table-skeleton';
import ProductListingPage from '@/features/products/components/product-listing';
import { searchParamsCache, serialize } from '@/lib/searchparams';
import { cn } from '@/lib/utils';
import { IconPlus } from '@tabler/icons-react';
import Link from 'next/link';
import { SearchParams } from 'nuqs/server';
import { Suspense } from 'react';
export const metadata = {
title: 'Dashboard: Products'
};
type pageProps = {
searchParams: Promise<SearchParams>;
};
export default async function Page(props: pageProps) {
const searchParams = await props.searchParams;
searchParamsCache.parse(searchParams);
return (
<PageContainer scrollable={false}>
<div className='flex flex-1 flex-col space-y-4'>
<div className='flex items-start justify-between'>
<Heading
title='Products'
description='Manage products (Server side table functionalities.)'
/>
<Link
href='/dashboard/product/new'
className={cn(buttonVariants(), 'text-xs md:text-sm')}
>
<IconPlus className='mr-2 h-4 w-4' /> Add New
</Link>
</div>
<Separator />
<Suspense
// key={key}
fallback={
<DataTableSkeleton columnCount={5} rowCount={8} filterCount={2} />
}
>
<ProductListingPage />
</Suspense>
</div>
</PageContainer>
);
}

View File

@@ -0,0 +1,9 @@
import ProfileViewPage from '@/features/profile/components/profile-view-page';
export const metadata = {
title: 'Dashboard : Profile'
};
export default async function Page() {
return <ProfileViewPage />;
}

View File

@@ -0,0 +1,23 @@
import FormCardSkeleton from '@/components/form-card-skeleton';
import PageContainer from '@/components/layout/page-container';
import { Suspense } from 'react';
import TicketViewPage from '@/features/tickets/components/ticket-data-table';
export const metadata = {
title: 'Dashboard : Ticket View'
};
type PageProps = { params: Promise<{ ticketId: string }> };
export default async function Page(props: PageProps) {
const params = await props.params;
return (
<PageContainer scrollable>
<div className='flex-1 space-y-4'>
<Suspense fallback={<FormCardSkeleton />}>
<TicketViewPage ticketId={params.ticketId} />
</Suspense>
</div>
</PageContainer>
);
}

View File

@@ -0,0 +1,20 @@
import PageContainer from "@/components/layout/page-container";
import { Heading } from "@/components/ui/heading";
import { Separator } from "@/components/ui/separator";
import NewTicketForm from "@/features/tickets/components/new-ticket-form";
export const metadata = {
title: "Dashboard: New Ticket",
};
export default function NewTicketPage() {
return (
<PageContainer>
<div className="flex flex-col gap-4 max-w-2xl mx-auto">
<Heading title="Create Ticket" description="Add a new support ticket" />
<Separator />
<NewTicketForm />
</div>
</PageContainer>
);
}

View File

@@ -0,0 +1,44 @@
import PageContainer from "@/components/layout/page-container"
import { buttonVariants } from "@/components/ui/button"
import { Heading } from "@/components/ui/heading"
import { Separator } from "@/components/ui/separator"
import { DataTableSkeleton } from "@/components/ui/table/data-table-skeleton"
import TicketDataTable from "@/features/tickets/components/ticket-data-table"
import { searchParamsCache } from "@/lib/searchparams"
import { cn } from "@/lib/utils"
import { IconPlus } from "@tabler/icons-react"
import Link from "next/link"
import { SearchParams } from "nuqs/server"
import { Suspense } from "react"
export const metadata = {
title: "Dashboard: Tickets",
}
type pageProps = {
searchParams: Promise<SearchParams>
}
export default async function Page(props: pageProps) {
const searchParams = await props.searchParams
searchParamsCache.parse(searchParams)
return (
<PageContainer scrollable={false}>
<div className="flex flex-1 flex-col space-y-4">
<div className="flex items-start justify-between">
<Heading title="Tickets" description="Manage tickets" />
<Link href="/dashboard/tickets/new" className={cn(buttonVariants(), "text-xs md:text-sm")}>
<IconPlus className="mr-2 h-4 w-4" /> Add New
</Link>
</div>
<Separator />
<div className="flex-1 w-full">
<Suspense fallback={<DataTableSkeleton columnCount={8} rowCount={8} filterCount={2} />}>
<TicketDataTable />
</Suspense>
</div>
</div>
</PageContainer>
)
}

View File

@@ -0,0 +1,23 @@
import FormCardSkeleton from '@/components/form-card-skeleton';
import PageContainer from '@/components/layout/page-container';
import { Suspense } from 'react';
// import UserViewPage from '@/features/users/components/user-view-page';
export const metadata = {
title: 'Dashboard : User View'
};
type PageProps = { params: Promise<{ userId: string }> };
export default async function Page(props: PageProps) {
const params = await props.params;
return (
<PageContainer scrollable>
<div className='flex-1 space-y-4'>
<Suspense fallback={<FormCardSkeleton />}>
{/* <UserViewPage userId={params.userId} /> */}
</Suspense>
</div>
</PageContainer>
);
}

View File

@@ -0,0 +1,44 @@
import PageContainer from "@/components/layout/page-container"
import { buttonVariants } from "@/components/ui/button"
import { Heading } from "@/components/ui/heading"
import { Separator } from "@/components/ui/separator"
import { DataTableSkeleton } from "@/components/ui/table/data-table-skeleton"
import UsersDataTable from "@/features/users/components/user-data-table"
import { searchParamsCache } from "@/lib/searchparams"
import { cn } from "@/lib/utils"
import { IconPlus } from "@tabler/icons-react"
import Link from "next/link"
import { SearchParams } from "nuqs/server"
import { Suspense } from "react"
export const metadata = {
title: "Dashboard: Users",
}
type pageProps = {
searchParams: Promise<SearchParams>
}
export default async function Page(props: pageProps) {
const searchParams = await props.searchParams
searchParamsCache.parse(searchParams)
return (
<PageContainer scrollable={false}>
<div className="flex flex-1 flex-col space-y-4">
<div className="flex items-start justify-between">
<Heading title="Users" description="Manage users" />
<Link href="/dashboard/users/new" className={cn(buttonVariants(), "text-xs md:text-sm")}>
<IconPlus className="mr-2 h-4 w-4" /> Add New
</Link>
</div>
<Separator />
<div className="flex-1 w-full overflow-auto">
<Suspense fallback={<DataTableSkeleton columnCount={8} rowCount={8} filterCount={2} />}>
<UsersDataTable />
</Suspense>
</div>
</div>
</PageContainer>
)
}

BIN
src/app/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

27
src/app/global-error.tsx Normal file
View File

@@ -0,0 +1,27 @@
'use client';
import * as Sentry from '@sentry/nextjs';
import NextError from 'next/error';
import { useEffect } from 'react';
export default function GlobalError({
error
}: {
error: Error & { digest?: string };
}) {
useEffect(() => {
Sentry.captureException(error);
}, [error]);
return (
<html>
<body>
{/* `NextError` is the default Next.js error page component. Its type
definition requires a `statusCode` prop. However, since the App Router
does not expose status codes for errors, we simply pass 0 to render a
generic error message. */}
<NextError statusCode={0} />
</body>
</html>
);
}

158
src/app/globals.css Normal file
View File

@@ -0,0 +1,158 @@
@import 'tailwindcss';
@import 'tw-animate-css';
@custom-variant dark (&:is(.dark *));
@import './theme.css';
:root {
--radius: 0.625rem;
--background: oklch(1 0 0);
--foreground: oklch(0.145 0 0);
--card: oklch(1 0 0);
--card-foreground: oklch(0.145 0 0);
--popover: oklch(1 0 0);
--popover-foreground: oklch(0.145 0 0);
--primary: oklch(0.205 0 0);
--primary-foreground: oklch(0.985 0 0);
--secondary: oklch(0.97 0 0);
--secondary-foreground: oklch(0.205 0 0);
--muted: oklch(0.97 0 0);
--muted-foreground: oklch(0.556 0 0);
--accent: oklch(0.97 0 0);
--accent-foreground: oklch(0.205 0 0);
--destructive: oklch(0.577 0.245 27.325);
--border: oklch(0.922 0 0);
--input: oklch(0.922 0 0);
--ring: oklch(0.708 0 0);
--chart-1: oklch(0.646 0.222 41.116);
--chart-2: oklch(0.6 0.118 184.704);
--chart-3: oklch(0.398 0.07 227.392);
--chart-4: oklch(0.828 0.189 84.429);
--chart-5: oklch(0.769 0.188 70.08);
--sidebar: oklch(0.985 0 0);
--sidebar-foreground: oklch(0.145 0 0);
--sidebar-primary: oklch(0.205 0 0);
--sidebar-primary-foreground: oklch(0.985 0 0);
--sidebar-accent: oklch(0.97 0 0);
--sidebar-accent-foreground: oklch(0.205 0 0);
--sidebar-border: oklch(0.922 0 0);
--sidebar-ring: oklch(0.708 0 0);
}
.dark {
--background: oklch(0.145 0 0);
--foreground: oklch(0.985 0 0);
--card: oklch(0.205 0 0);
--card-foreground: oklch(0.985 0 0);
--popover: oklch(0.269 0 0);
--popover-foreground: oklch(0.985 0 0);
--primary: oklch(0.922 0 0);
--primary-foreground: oklch(0.205 0 0);
--secondary: oklch(0.269 0 0);
--secondary-foreground: oklch(0.985 0 0);
--muted: oklch(0.269 0 0);
--muted-foreground: oklch(0.708 0 0);
--accent: oklch(0.371 0 0);
--accent-foreground: oklch(0.985 0 0);
--destructive: oklch(0.704 0.191 22.216);
--border: oklch(1 0 0 / 10%);
--input: oklch(1 0 0 / 15%);
--ring: oklch(0.556 0 0);
--chart-1: oklch(0.488 0.243 264.376);
--chart-2: oklch(0.696 0.17 162.48);
--chart-3: oklch(0.769 0.188 70.08);
--chart-4: oklch(0.627 0.265 303.9);
--chart-5: oklch(0.645 0.246 16.439);
--sidebar: oklch(0.205 0 0);
--sidebar-foreground: oklch(0.985 0 0);
--sidebar-primary: oklch(0.488 0.243 264.376);
--sidebar-primary-foreground: oklch(0.985 0 0);
--sidebar-accent: oklch(0.269 0 0);
--sidebar-accent-foreground: oklch(0.985 0 0);
--sidebar-border: oklch(1 0 0 / 10%);
--sidebar-ring: oklch(0.439 0 0);
}
@theme inline {
--radius-sm: calc(var(--radius) - 4px);
--radius-md: calc(var(--radius) - 2px);
--radius-lg: var(--radius);
--radius-xl: calc(var(--radius) + 4px);
--color-background: var(--background);
--color-foreground: var(--foreground);
--color-card: var(--card);
--color-card-foreground: var(--card-foreground);
--color-popover: var(--popover);
--color-popover-foreground: var(--popover-foreground);
--color-primary: var(--primary);
--color-primary-foreground: var(--primary-foreground);
--color-secondary: var(--secondary);
--color-secondary-foreground: var(--secondary-foreground);
--color-muted: var(--muted);
--color-muted-foreground: var(--muted-foreground);
--color-accent: var(--accent);
--color-accent-foreground: var(--accent-foreground);
--color-destructive: var(--destructive);
--color-border: var(--border);
--color-input: var(--input);
--color-ring: var(--ring);
--color-chart-1: var(--chart-1);
--color-chart-2: var(--chart-2);
--color-chart-3: var(--chart-3);
--color-chart-4: var(--chart-4);
--color-chart-5: var(--chart-5);
--color-sidebar: var(--sidebar);
--color-sidebar-foreground: var(--sidebar-foreground);
--color-sidebar-primary: var(--sidebar-primary);
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
--color-sidebar-accent: var(--sidebar-accent);
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
--color-sidebar-border: var(--sidebar-border);
--color-sidebar-ring: var(--sidebar-ring);
}
@layer base {
* {
@apply border-border outline-ring/50;
}
body {
@apply bg-background text-foreground;
}
}
/* View Transition Wave Effect */
::view-transition-old(root),
::view-transition-new(root) {
animation: none;
mix-blend-mode: normal;
}
::view-transition-old(root) {
/* Ensure the outgoing view (old theme) is beneath */
z-index: 0;
}
::view-transition-new(root) {
/* Ensure the incoming view (new theme) is always on top */
z-index: 1;
}
@keyframes reveal {
from {
/* Use CSS variables for the origin, defaulting to center if not set */
clip-path: circle(0% at var(--x, 50%) var(--y, 50%));
opacity: 0.7;
}
to {
/* Use CSS variables for the origin, defaulting to center if not set */
clip-path: circle(150% at var(--x, 50%) var(--y, 50%));
opacity: 1;
}
}
::view-transition-new(root) {
/* Apply the reveal animation */
animation: reveal 0.4s ease-in-out forwards;
}

82
src/app/layout.tsx Normal file
View File

@@ -0,0 +1,82 @@
import { Providers } from '@/components/layout/providers';
import { Toaster } from '@/components/ui/sonner';
import { fontVariables } from '@/lib/font';
import ThemeProvider from '@/components/layout/ThemeToggle/theme-provider';
import { ActiveThemeProvider } from '@/components/active-theme'; // <-- Add this import
import { cn } from '@/lib/utils';
import type { Metadata, Viewport } from 'next';
import { cookies } from 'next/headers';
import NextTopLoader from 'nextjs-toploader';
import { NuqsAdapter } from 'nuqs/adapters/next/app';
import './globals.css';
import './theme.css';
const META_THEME_COLORS = {
light: '#ffffff',
dark: '#09090b'
};
export const metadata: Metadata = {
title: 'Next Shadcn',
description: 'Basic dashboard with Next.js and Shadcn'
};
export const viewport: Viewport = {
themeColor: META_THEME_COLORS.light
};
export default async function RootLayout({
children
}: {
children: React.ReactNode;
}) {
const cookieStore = await cookies();
const activeThemeValue = cookieStore.get('active_theme')?.value;
const isScaled = activeThemeValue?.endsWith('-scaled');
return (
<html lang='en' suppressHydrationWarning>
<head>
<script
dangerouslySetInnerHTML={{
__html: `
try {
if (localStorage.theme === 'dark' || ((!('theme' in localStorage) || localStorage.theme === 'system') && window.matchMedia('(prefers-color-scheme: dark)').matches)) {
document.querySelector('meta[name="theme-color"]').setAttribute('content', '${META_THEME_COLORS.dark}')
}
} catch (_) {}
`
}}
/>
</head>
<body
className={cn(
'bg-background overflow-hidden overscroll-none font-sans antialiased',
activeThemeValue ? `theme-${activeThemeValue}` : '',
isScaled ? 'theme-scaled' : '',
fontVariables
)}
>
<NextTopLoader showSpinner={false} />
<NuqsAdapter>
<ThemeProvider
attribute='class'
defaultTheme='system'
enableSystem
disableTransitionOnChange
enableColorScheme
>
{/* Add ActiveThemeProvider here */}
<ActiveThemeProvider initialTheme={activeThemeValue}>
<Providers>
<Toaster />
{/* <TokenRefresh /> */}
{children}
</Providers>
</ActiveThemeProvider>
</ThemeProvider>
</NuqsAdapter>
</body>
</html>
);
}

36
src/app/not-found.tsx Normal file
View File

@@ -0,0 +1,36 @@
'use client';
import { useRouter } from 'next/navigation';
import { Button } from '@/components/ui/button';
export default function NotFound() {
const router = useRouter();
return (
<div className='absolute top-1/2 left-1/2 mb-16 -translate-x-1/2 -translate-y-1/2 items-center justify-center text-center'>
<span className='from-foreground bg-linear-to-b to-transparent bg-clip-text text-[10rem] leading-none font-extrabold text-transparent'>
404
</span>
<h2 className='font-heading my-2 text-2xl font-bold'>
Something&apos;s missing
</h2>
<p>
Sorry, the page you are looking for doesn&apos;t exist or has been
moved.
</p>
<div className='mt-8 flex justify-center gap-2'>
<Button onClick={() => router.back()} variant='default' size='lg'>
Go back
</Button>
<Button
onClick={() => router.push('/dashboard')}
variant='ghost'
size='lg'
>
Back to Home
</Button>
</div>
</div>
);
}

13
src/app/page.tsx Normal file
View File

@@ -0,0 +1,13 @@
import { cookies } from 'next/headers';
import { redirect } from 'next/navigation';
export default async function Dashboard() {
const cookieStore = await cookies();
const accessToken = cookieStore.get('access_token')?.value;
if (!accessToken) {
redirect('/auth/sign-in');
} else {
redirect('/dashboard/overview');
}
}

149
src/app/theme.css Normal file
View File

@@ -0,0 +1,149 @@
body {
@apply overscroll-none bg-transparent;
}
:root {
--font-sans: var(--font-inter);
--header-height: calc(var(--spacing) * 12 + 1px);
}
.theme-scaled {
@media (min-width: 1024px) {
--radius: 0.6rem;
--text-lg: 1.05rem;
--text-base: 0.85rem;
--text-sm: 0.8rem;
--spacing: 0.222222rem;
}
[data-slot='card'] {
--spacing: 0.16rem;
}
[data-slot='select-trigger'],
[data-slot='toggle-group-item'] {
--spacing: 0.222222rem;
}
}
.theme-default,
.theme-default-scaled {
--primary: var(--color-neutral-600);
--primary-foreground: var(--color-neutral-50);
@variant dark {
--primary: var(--color-neutral-500);
--primary-foreground: var(--color-neutral-50);
}
}
.theme-blue,
.theme-blue-scaled {
--primary: var(--color-blue-600);
--primary-foreground: var(--color-blue-50);
@variant dark {
--primary: var(--color-blue-500);
--primary-foreground: var(--color-blue-50);
}
}
.theme-green,
.theme-green-scaled {
--primary: var(--color-lime-600);
--primary-foreground: var(--color-lime-50);
@variant dark {
--primary: var(--color-lime-600);
--primary-foreground: var(--color-lime-50);
}
}
.theme-amber,
.theme-amber-scaled {
--primary: var(--color-amber-600);
--primary-foreground: var(--color-amber-50);
@variant dark {
--primary: var(--color-amber-500);
--primary-foreground: var(--color-amber-50);
}
}
.theme-pink,
.theme-pink-scaled {
--primary: var(--color-pink-600);
--primary-foreground: var(--color-pink-50);
@variant dark {
--primary: var(--color-pink-500);
--primary-foreground: var(--color-pink-50);
}
}
.theme-red,
.theme-red-scaled {
--primary: var(--color-red-600);
--primary-foreground: var(--color-red-50);
@variant dark {
--primary: var(--color-red-500);
--primary-foreground: var(--color-red-50);
}
}
.theme-purple,
.theme-purple-scaled {
--primary: var(--color-purple-600);
--primary-foreground: var(--color-purple-50);
@variant dark {
--primary: var(--color-purple-500);
--primary-foreground: var(--color-purple-50);
}
}
.theme-mono,
.theme-mono-scaled {
--font-sans: var(--font-mono);
--primary: var(--color-neutral-600);
--primary-foreground: var(--color-neutral-50);
@variant dark {
--primary: var(--color-neutral-500);
--primary-foreground: var(--color-neutral-50);
}
.rounded-xs,
.rounded-sm,
.rounded-md,
.rounded-lg,
.rounded-xl {
@apply !rounded-none;
border-radius: 0;
}
.shadow-xs,
.shadow-sm,
.shadow-md,
.shadow-lg,
.shadow-xl {
@apply !shadow-none;
}
[data-slot='toggle-group'],
[data-slot='toggle-group-item'] {
@apply !rounded-none !shadow-none;
}
}
.theme-gold,
.theme-gold-scaled {
--primary: var(--color-amber-500, #FFD700);
--primary-foreground: var(--color-amber-50, #fff8e1);
@variant dark {
--primary: var(--color-amber-400, #FFD700);
--primary-foreground: var(--color-amber-50, #fff8e1);
}
}

View File

@@ -0,0 +1,15 @@
import AppSidebar from '@/components/layout/app-sidebar';
import { SidebarInset, SidebarProvider } from '@/components/ui/sidebar';
import VideoChat from '../../features/video-chat-room/components/video-chat-room';
export default function VideoChatRoomPage() {
return (
<SidebarProvider>
<AppSidebar />
<SidebarInset>
<VideoChat />
</SidebarInset>
</SidebarProvider>
);
}

View File

@@ -0,0 +1,67 @@
'use client';
import {
ReactNode,
createContext,
useContext,
useEffect,
useState
} from 'react';
const COOKIE_NAME = 'active_theme';
const DEFAULT_THEME = 'default';
function setThemeCookie(theme: string) {
if (typeof window === 'undefined') return;
document.cookie = `${COOKIE_NAME}=${theme}; path=/; max-age=31536000; SameSite=Lax; ${window.location.protocol === 'https:' ? 'Secure;' : ''}`;
}
type ThemeContextType = {
activeTheme: string;
setActiveTheme: (theme: string) => void;
};
const ThemeContext = createContext<ThemeContextType | undefined>(undefined);
export function ActiveThemeProvider({
children,
initialTheme
}: {
children: ReactNode;
initialTheme?: string;
}) {
const [activeTheme, setActiveTheme] = useState<string>(
() => initialTheme || DEFAULT_THEME
);
useEffect(() => {
setThemeCookie(activeTheme);
Array.from(document.body.classList)
.filter((className) => className.startsWith('theme-'))
.forEach((className) => {
document.body.classList.remove(className);
});
document.body.classList.add(`theme-${activeTheme}`);
if (activeTheme.endsWith('-scaled')) {
document.body.classList.add('theme-scaled');
}
}, [activeTheme]);
return (
<ThemeContext.Provider value={{ activeTheme, setActiveTheme }}>
{children}
</ThemeContext.Provider>
);
}
export function useThemeConfig() {
const context = useContext(ThemeContext);
if (context === undefined) {
throw new Error(
'useThemeConfig must be used within an ActiveThemeProvider'
);
}
return context;
}

View File

@@ -0,0 +1,41 @@
'use client';
import {
Breadcrumb,
BreadcrumbItem,
BreadcrumbLink,
BreadcrumbList,
BreadcrumbPage,
BreadcrumbSeparator
} from '@/components/ui/breadcrumb';
import { useBreadcrumbs } from '@/hooks/use-breadcrumbs';
import { IconSlash } from '@tabler/icons-react';
import { Fragment } from 'react';
export function Breadcrumbs() {
const items = useBreadcrumbs();
if (items.length === 0) return null;
return (
<Breadcrumb>
<BreadcrumbList>
{items.map((item, index) => (
<Fragment key={item.title}>
{index !== items.length - 1 && (
<BreadcrumbItem className='hidden md:block'>
<BreadcrumbLink href={item.link}>{item.title}</BreadcrumbLink>
</BreadcrumbItem>
)}
{index < items.length - 1 && (
<BreadcrumbSeparator className='hidden md:block'>
<IconSlash />
</BreadcrumbSeparator>
)}
{index === items.length - 1 && (
<BreadcrumbPage>{item.title}</BreadcrumbPage>
)}
</Fragment>
))}
</BreadcrumbList>
</Breadcrumb>
);
}

View File

@@ -0,0 +1,316 @@
'use client';
import { IconX, IconUpload } from '@tabler/icons-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<HTMLDivElement> {
/**
* Value of the uploader.
* @type File[]
* @default undefined
* @example value={files}
*/
value?: File[];
/**
* Function to be called when the value changes.
* @type React.Dispatch<React.SetStateAction<File[]>>
* @default undefined
* @example onValueChange={(files) => setFiles(files)}
*/
onValueChange?: React.Dispatch<React.SetStateAction<File[]>>;
/**
* Function to be called when files are uploaded.
* @type (files: File[]) => Promise<void>
* @default undefined
* @example onUpload={(files) => uploadFiles(files)}
*/
onUpload?: (files: File[]) => Promise<void>;
/**
* Progress of the uploaded files.
* @type Record<string, number> | undefined
* @default undefined
* @example progresses={{ "file1.png": 50 }}
*/
progresses?: Record<string, number>;
/**
* 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 (
<div className='relative flex flex-col gap-6 overflow-hidden'>
<Dropzone
onDrop={onDrop}
accept={accept}
maxSize={maxSize}
maxFiles={maxFiles}
multiple={maxFiles > 1 || multiple}
disabled={isDisabled}
>
{({ getRootProps, getInputProps, isDragActive }) => (
<div
{...getRootProps()}
className={cn(
'group border-muted-foreground/25 hover:bg-muted/25 relative grid h-52 w-full cursor-pointer place-items-center rounded-lg border-2 border-dashed px-5 py-2.5 text-center transition',
'ring-offset-background focus-visible:ring-ring focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-hidden',
isDragActive && 'border-muted-foreground/50',
isDisabled && 'pointer-events-none opacity-60',
className
)}
{...dropzoneProps}
>
<input {...getInputProps()} />
{isDragActive ? (
<div className='flex flex-col items-center justify-center gap-4 sm:px-5'>
<div className='rounded-full border border-dashed p-3'>
<IconUpload
className='text-muted-foreground size-7'
aria-hidden='true'
/>
</div>
<p className='text-muted-foreground font-medium'>
Drop the files here
</p>
</div>
) : (
<div className='flex flex-col items-center justify-center gap-4 sm:px-5'>
<div className='rounded-full border border-dashed p-3'>
<IconUpload
className='text-muted-foreground size-7'
aria-hidden='true'
/>
</div>
<div className='space-y-px'>
<p className='text-muted-foreground font-medium'>
Drag {`'n'`} drop files here, or click to select files
</p>
<p className='text-muted-foreground/70 text-sm'>
You can upload
{maxFiles > 1
? ` ${maxFiles === Infinity ? 'multiple' : maxFiles}
files (up to ${formatBytes(maxSize)} each)`
: ` a file with ${formatBytes(maxSize)}`}
</p>
</div>
</div>
)}
</div>
)}
</Dropzone>
{files?.length ? (
<ScrollArea className='h-fit w-full px-3'>
<div className='max-h-48 space-y-4'>
{files?.map((file, index) => (
<FileCard
key={index}
file={file}
onRemove={() => onRemove(index)}
progress={progresses?.[file.name]}
/>
))}
</div>
</ScrollArea>
) : null}
</div>
);
}
interface FileCardProps {
file: File;
onRemove: () => void;
progress?: number;
}
function FileCard({ file, progress, onRemove }: FileCardProps) {
return (
<div className='relative flex items-center space-x-4'>
<div className='flex flex-1 space-x-4'>
{isFileWithPreview(file) ? (
<Image
src={file.preview}
alt={file.name}
width={48}
height={48}
loading='lazy'
className='aspect-square shrink-0 rounded-md object-cover'
/>
) : null}
<div className='flex w-full flex-col gap-2'>
<div className='space-y-px'>
<p className='text-foreground/80 line-clamp-1 text-sm font-medium'>
{file.name}
</p>
<p className='text-muted-foreground text-xs'>
{formatBytes(file.size)}
</p>
</div>
{progress ? <Progress value={progress} /> : null}
</div>
</div>
<div className='flex items-center gap-2'>
<Button
type='button'
variant='ghost'
size='icon'
onClick={onRemove}
disabled={progress !== undefined && progress < 100}
className='size-8 rounded-full'
>
<IconX className='text-muted-foreground' />
<span className='sr-only'>Remove file</span>
</Button>
</div>
</div>
);
}
function isFileWithPreview(file: File): file is File & { preview: string } {
return 'preview' in file && typeof file.preview === 'string';
}

View File

@@ -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 (
<Card className='mx-auto w-full'>
<CardHeader>
<Skeleton className='h-8 w-48' /> {/* Title */}
</CardHeader>
<CardContent>
<div className='space-y-8'>
{/* Image upload area skeleton */}
<div className='space-y-6'>
<Skeleton className='h-4 w-16' /> {/* Label */}
<Skeleton className='h-32 w-full rounded-lg' /> {/* Upload area */}
</div>
{/* Grid layout for form fields */}
<div className='grid grid-cols-1 gap-6 md:grid-cols-2'>
{/* Product Name field */}
<div className='space-y-2'>
<Skeleton className='h-4 w-24' /> {/* Label */}
<Skeleton className='h-10 w-full' /> {/* Input */}
</div>
{/* Category field */}
<div className='space-y-2'>
<Skeleton className='h-4 w-20' /> {/* Label */}
<Skeleton className='h-10 w-full' /> {/* Select */}
</div>
{/* Price field */}
<div className='space-y-2'>
<Skeleton className='h-4 w-16' /> {/* Label */}
<Skeleton className='h-10 w-full' /> {/* Input */}
</div>
</div>
{/* Description field */}
<div className='space-y-2'>
<Skeleton className='h-4 w-24' /> {/* Label */}
<Skeleton className='h-32 w-full' /> {/* Textarea */}
</div>
{/* Submit button */}
<Skeleton className='h-10 w-28' />
</div>
</CardContent>
</Card>
);
}

78
src/components/icons.tsx Normal file
View File

@@ -0,0 +1,78 @@
import {
IconAlertTriangle,
IconArrowRight,
IconCalendar,
IconCheck,
IconChevronLeft,
IconChevronRight,
IconCommand,
IconCreditCard,
IconFile,
IconFileText,
IconHelpCircle,
IconPhoto,
IconDeviceLaptop,
IconLayoutDashboard,
IconLoader2,
IconLogin,
IconProps,
IconShoppingBag,
IconMoon,
IconDotsVertical,
IconPizza,
IconPlus,
IconSettings,
IconSun,
IconTrash,
IconBrandTwitter,
IconUser,
IconUserCircle,
IconUserEdit,
IconUserX,
IconX,
IconLayoutKanban,
IconBrandGithub,
IconMessageCircle,
IconUsers,
IconTicket
} from '@tabler/icons-react';
export type Icon = React.ComponentType<IconProps>;
export const Icons = {
dashboard: IconLayoutDashboard,
logo: IconCommand,
login: IconLogin,
calendar: IconCalendar,
close: IconX,
product: IconShoppingBag,
spinner: IconLoader2,
kanban: IconLayoutKanban,
chevronLeft: IconChevronLeft,
chevronRight: IconChevronRight,
trash: IconTrash,
employee: IconUserX,
post: IconFileText,
page: IconFile,
userPen: IconUserEdit,
user2: IconUserCircle,
media: IconPhoto,
settings: IconSettings,
billing: IconCreditCard,
ellipsis: IconDotsVertical,
add: IconPlus,
warning: IconAlertTriangle,
user: IconUser,
arrowRight: IconArrowRight,
help: IconHelpCircle,
pizza: IconPizza,
sun: IconSun,
moon: IconMoon,
laptop: IconDeviceLaptop,
github: IconBrandGithub,
twitter: IconBrandTwitter,
check: IconCheck,
message: IconMessageCircle,
users: IconUsers,
ticket: IconTicket
};

View File

@@ -0,0 +1,83 @@
'use client';
import { navItems } from '@/constants/data';
import {
KBarAnimator,
KBarPortal,
KBarPositioner,
KBarProvider,
KBarSearch
} from 'kbar';
import { useRouter } from 'next/navigation';
import { useMemo } from 'react';
import RenderResults from './render-result';
import useThemeSwitching from './use-theme-switching';
export default function KBar({ children }: { children: React.ReactNode }) {
const router = useRouter();
// These action are for the navigation
const actions = useMemo(() => {
// Define navigateTo inside the useMemo callback to avoid dependency array issues
const navigateTo = (url: string) => {
router.push(url);
};
return navItems.flatMap((navItem) => {
// Only include base action if the navItem has a real URL and is not just a container
const baseAction =
navItem.url !== '#'
? {
id: `${navItem.title.toLowerCase()}Action`,
name: navItem.title,
shortcut: navItem.shortcut,
keywords: navItem.title.toLowerCase(),
section: 'Navigation',
subtitle: `Go to ${navItem.title}`,
perform: () => navigateTo(navItem.url)
}
: null;
// Map child items into actions
const childActions =
navItem.items?.map((childItem) => ({
id: `${childItem.title.toLowerCase()}Action`,
name: childItem.title,
shortcut: childItem.shortcut,
keywords: childItem.title.toLowerCase(),
section: navItem.title,
subtitle: `Go to ${childItem.title}`,
perform: () => navigateTo(childItem.url)
})) ?? [];
// Return only valid actions (ignoring null base actions for containers)
return baseAction ? [baseAction, ...childActions] : childActions;
});
}, [router]);
return (
<KBarProvider actions={actions}>
<KBarComponent>{children}</KBarComponent>
</KBarProvider>
);
}
const KBarComponent = ({ children }: { children: React.ReactNode }) => {
useThemeSwitching();
return (
<>
<KBarPortal>
<KBarPositioner className='bg-background/80 fixed inset-0 z-99999 p-0! backdrop-blur-sm'>
<KBarAnimator className='bg-card text-card-foreground relative mt-64! w-full max-w-[600px] -translate-y-12! overflow-hidden rounded-lg border shadow-lg'>
<div className='bg-card border-border sticky top-0 z-10 border-b'>
<KBarSearch className='bg-card w-full border-none px-6 py-4 text-lg outline-hidden focus:ring-0 focus:ring-offset-0 focus:outline-hidden' />
</div>
<div className='max-h-[400px]'>
<RenderResults />
</div>
</KBarAnimator>
</KBarPositioner>
</KBarPortal>
{children}
</>
);
};

View File

@@ -0,0 +1,25 @@
import { KBarResults, useMatches } from 'kbar';
import ResultItem from './result-item';
export default function RenderResults() {
const { results, rootActionId } = useMatches();
return (
<KBarResults
items={results}
onRender={({ item, active }) =>
typeof item === 'string' ? (
<div className='text-primary-foreground px-4 py-2 text-sm uppercase opacity-50'>
{item}
</div>
) : (
<ResultItem
action={item}
active={active}
currentRootActionId={rootActionId ?? ''}
/>
)
}
/>
);
}

View File

@@ -0,0 +1,77 @@
import type { ActionId, ActionImpl } from 'kbar';
import * as React from 'react';
const ResultItem = React.forwardRef(
(
{
action,
active,
currentRootActionId
}: {
action: ActionImpl;
active: boolean;
currentRootActionId: ActionId;
},
ref: React.Ref<HTMLDivElement>
) => {
const ancestors = React.useMemo(() => {
if (!currentRootActionId) return action.ancestors;
const index = action.ancestors.findIndex(
(ancestor) => ancestor.id === currentRootActionId
);
return action.ancestors.slice(index + 1);
}, [action.ancestors, currentRootActionId]);
return (
<div
ref={ref}
className={`relative z-10 flex cursor-pointer items-center justify-between px-4 py-3`}
>
{active && (
<div
id='kbar-result-item'
className='border-primary bg-accent/50 absolute inset-0 z-[-1]! border-l-4'
></div>
)}
<div className='relative z-10 flex items-center gap-2'>
{action.icon && action.icon}
<div className='flex flex-col'>
<div>
{ancestors.length > 0 &&
ancestors.map((ancestor) => (
<React.Fragment key={ancestor.id}>
<span className='text-muted-foreground mr-2'>
{ancestor.name}
</span>
<span className='mr-2'>&rsaquo;</span>
</React.Fragment>
))}
<span>{action.name}</span>
</div>
{action.subtitle && (
<span className='text-muted-foreground text-sm'>
{action.subtitle}
</span>
)}
</div>
</div>
{action.shortcut?.length ? (
<div className='relative z-10 grid grid-flow-col gap-1'>
{action.shortcut.map((sc, i) => (
<kbd
key={sc + i}
className='bg-muted flex h-5 items-center gap-1 rounded-md border px-1.5 text-[10px] font-medium'
>
{sc}
</kbd>
))}
</div>
) : null}
</div>
);
}
);
ResultItem.displayName = 'KBarResultItem';
export default ResultItem;

View File

@@ -0,0 +1,36 @@
import { useRegisterActions } from 'kbar';
import { useTheme } from 'next-themes';
const useThemeSwitching = () => {
const { theme, setTheme } = useTheme();
const toggleTheme = () => {
setTheme(theme === 'light' ? 'dark' : 'light');
};
const themeAction = [
{
id: 'toggleTheme',
name: 'Toggle Theme',
shortcut: ['t', 't'],
section: 'Theme',
perform: toggleTheme
},
{
id: 'setLightTheme',
name: 'Set Light Theme',
section: 'Theme',
perform: () => setTheme('light')
},
{
id: 'setDarkTheme',
name: 'Set Dark Theme',
section: 'Theme',
perform: () => setTheme('dark')
}
];
useRegisterActions(themeAction, [theme]);
};
export default useThemeSwitching;

View File

@@ -0,0 +1,13 @@
'use client';
import {
ThemeProvider as NextThemesProvider,
ThemeProviderProps
} from 'next-themes';
export default function ThemeProvider({
children,
...props
}: ThemeProviderProps) {
return <NextThemesProvider {...props}>{children}</NextThemesProvider>;
}

View File

@@ -0,0 +1,46 @@
'use client';
import { IconBrightness } from '@tabler/icons-react';
import { useTheme } from 'next-themes';
import * as React from 'react';
import { Button } from '@/components/ui/button';
export function ModeToggle() {
const { setTheme, resolvedTheme } = useTheme();
const handleThemeToggle = React.useCallback(
(e?: React.MouseEvent) => {
const newMode = resolvedTheme === 'dark' ? 'light' : 'dark';
const root = document.documentElement;
if (!document.startViewTransition) {
setTheme(newMode);
return;
}
// Set coordinates from the click event
if (e) {
root.style.setProperty('--x', `${e.clientX}px`);
root.style.setProperty('--y', `${e.clientY}px`);
}
document.startViewTransition(() => {
setTheme(newMode);
});
},
[resolvedTheme, setTheme]
);
return (
<Button
variant='secondary'
size='icon'
className='group/toggle size-8'
onClick={handleThemeToggle}
>
<IconBrightness />
<span className='sr-only'>Toggle theme</span>
</Button>
);
}

View File

@@ -0,0 +1,239 @@
'use client';
import { useState, useEffect } from "react";
import {
Collapsible,
CollapsibleContent,
CollapsibleTrigger
} from '@/components/ui/collapsible';
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuGroup,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuTrigger
} from '@/components/ui/dropdown-menu';
import {
Sidebar,
SidebarContent,
SidebarFooter,
SidebarGroup,
SidebarGroupLabel,
SidebarHeader,
SidebarMenu,
SidebarMenuButton,
SidebarMenuItem,
SidebarMenuSub,
SidebarMenuSubButton,
SidebarMenuSubItem,
SidebarRail
} from '@/components/ui/sidebar';
import { UserAvatarProfile } from '@/components/user-avatar-profile';
import { navItems } from '@/constants/data';
import { useMediaQuery } from '@/hooks/use-media-query';
import {
IconBell,
IconChevronRight,
IconChevronsDown,
IconCreditCard,
IconLogout,
IconPhotoUp,
IconUserCircle,
IconMessage
} from '@tabler/icons-react';
import Link from 'next/link';
import { usePathname, useRouter } from 'next/navigation';
import * as React from 'react';
import { Icons } from '../icons';
import { OrgSwitcher } from '../org-switcher';
import { useAuth } from "@/hooks/useAuth"; // <-- Use new hook
export const company = {
name: 'Acme Inc',
logo: IconPhotoUp,
plan: 'Enterprise'
};
const tenants = [
{ id: '1', name: 'Acme Inc' },
{ id: '2', name: 'Beta Corp' },
{ id: '3', name: 'Gamma Ltd' }
];
const API_URL = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:8080/api/v1';
export default function AppSidebar() {
const pathname = usePathname();
const { isOpen } = useMediaQuery();
const router = useRouter();
const { user, signOut } = useAuth(); // <-- Use new hook
const [hasMounted, setHasMounted] = useState(false);
useEffect(() => {
setHasMounted(true);
}, []);
useEffect(() => {
// ...your sidebar logic...
}, [isOpen]);
if (!hasMounted) return null;
const handleSwitchTenant = (_tenantId: string) => {
// Tenant switching functionality would be implemented here
};
const activeTenant = tenants[0];
return (
<Sidebar collapsible='icon'>
<SidebarHeader>
<OrgSwitcher
tenants={tenants}
defaultTenant={activeTenant}
onTenantSwitch={handleSwitchTenant}
/>
</SidebarHeader>
<SidebarContent className='overflow-x-hidden'>
<SidebarGroup>
<SidebarGroupLabel>Overview</SidebarGroupLabel>
<SidebarMenu>
{navItems.map((item) => {
const Icon = item.icon ? Icons[item.icon] : Icons.logo;
return item?.items && item?.items?.length > 0 ? (
<Collapsible
key={item.title}
asChild
defaultOpen={item.isActive}
className='group/collapsible'
>
<SidebarMenuItem>
<CollapsibleTrigger asChild>
<SidebarMenuButton
tooltip={item.title}
isActive={pathname === item.url}
>
{item.icon && <Icon />}
<span>{item.title}</span>
<IconChevronRight className='ml-auto transition-transform duration-200 group-data-[state=open]/collapsible:rotate-90' />
</SidebarMenuButton>
</CollapsibleTrigger>
<CollapsibleContent>
<SidebarMenuSub>
{item.items?.map((subItem) => (
<SidebarMenuSubItem key={subItem.title}>
<SidebarMenuSubButton
asChild
isActive={pathname === subItem.url}
>
<Link href={subItem.url}>
<span>{subItem.title}</span>
</Link>
</SidebarMenuSubButton>
</SidebarMenuSubItem>
))}
</SidebarMenuSub>
</CollapsibleContent>
</SidebarMenuItem>
</Collapsible>
) : (
<SidebarMenuItem key={item.title}>
<SidebarMenuButton
asChild
tooltip={item.title}
isActive={pathname === item.url}
>
<Link href={item.url}>
<Icon />
<span>{item.title}</span>
</Link>
</SidebarMenuButton>
</SidebarMenuItem>
);
})}
</SidebarMenu>
</SidebarGroup>
</SidebarContent>
<SidebarFooter>
<SidebarMenu>
<SidebarMenuItem>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<SidebarMenuButton
size='lg'
className='data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground'
>
{user && (
<UserAvatarProfile
className='h-8 w-8 rounded-lg'
showInfo
user={{
user_id: user.id,
fullName: user.name || user.username || "",
emailAddresses: [{ emailAddress: user.email }],
}}
/>
)}
<IconChevronsDown className='ml-auto size-4' />
</SidebarMenuButton>
</DropdownMenuTrigger>
<DropdownMenuContent
className='w-(--radix-dropdown-menu-trigger-width) min-w-56 rounded-lg'
side='bottom'
align='end'
sideOffset={4}
>
<DropdownMenuLabel className='p-0 font-normal'>
<div className='px-1 py-1.5'>
{user && (
<UserAvatarProfile
className='h-8 w-8 rounded-lg'
showInfo
user={{
user_id: user.id,
fullName: user.name || user.username || "",
emailAddresses: [{ emailAddress: user.email }],
}}
/>
)}
</div>
</DropdownMenuLabel>
<DropdownMenuSeparator />
<DropdownMenuGroup>
<DropdownMenuItem
onClick={() => router.push('/dashboard/profile')}
>
<IconUserCircle className='mr-2 h-4 w-4' />
Profile
</DropdownMenuItem>
<DropdownMenuItem>
<IconCreditCard className='mr-2 h-4 w-4' />
Billing
</DropdownMenuItem>
<DropdownMenuItem>
<IconBell className='mr-2 h-4 w-4' />
Notifications
</DropdownMenuItem>
</DropdownMenuGroup>
<DropdownMenuSeparator />
<DropdownMenuItem
onClick={async () => {
await signOut();
router.push('/auth/sign-in');
}}
>
<IconLogout className='mr-2 h-4 w-4' />
Log out
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</SidebarMenuItem>
</SidebarMenu>
</SidebarFooter>
<SidebarRail />
</Sidebar>
);
}

View File

@@ -0,0 +1,18 @@
import React from 'react';
import { Button } from '@/components/ui/button';
import { IconBrandGithub } from '@tabler/icons-react';
export default function CtaGithub() {
return (
<Button variant='ghost' asChild size='sm' className='hidden sm:flex'>
<a
href='https://github.com/Kiranism/next-shadcn-dashboard-starter'
rel='noopener noreferrer'
target='_blank'
className='dark:text-foreground'
>
<IconBrandGithub />
</a>
</Button>
);
}

View File

@@ -0,0 +1,31 @@
import React from 'react';
import { SidebarTrigger } from '../ui/sidebar';
import { Separator } from '../ui/separator';
import { Breadcrumbs } from '../breadcrumbs';
import SearchInput from '../search-input';
import { UserNav } from './user-nav';
import { ThemeSelector } from '../theme-selector';
import { ModeToggle } from './ThemeToggle/theme-toggle';
import CtaGithub from './cta-github';
export default function Header() {
return (
<header className='flex h-16 shrink-0 items-center justify-between gap-2 transition-[width,height] ease-linear group-has-data-[collapsible=icon]/sidebar-wrapper:h-12'>
<div className='flex items-center gap-2 px-4'>
<SidebarTrigger className='-ml-1' />
<Separator orientation='vertical' className='mr-2 h-4' />
<Breadcrumbs />
</div>
<div className='flex items-center gap-2 px-4'>
{/* <CtaGithub /> */}
<div className='hidden md:flex'>
<SearchInput />
</div>
<UserNav />
<ModeToggle />
<ThemeSelector />
</div>
</header>
);
}

View File

@@ -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 ? (
<ScrollArea className='h-[calc(100dvh-52px)]'>
<div className='flex w-full min-w-0 flex-1 p-4 md:px-6'>{children}</div>
</ScrollArea>
) : (
<div className='flex w-full min-w-0 flex-1 p-4 md:px-6'>{children}</div>
)}
</>
);
}

View File

@@ -0,0 +1,16 @@
'use client';
import { useTheme } from 'next-themes';
import React from 'react';
// You can create your own provider or just use React's context/provider as needed
export function Providers({ children }: { children: React.ReactNode }) {
// Example: useTheme for dark/light mode
const { theme } = useTheme();
// Add your own context providers here if needed
return (
<React.Fragment>
{children}
</React.Fragment>
);
}

View File

@@ -0,0 +1,96 @@
'use client';
import { useEffect, useState } from 'react';
import { Button } from '@/components/ui/button';
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuGroup,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuTrigger
} from '@/components/ui/dropdown-menu';
import { UserAvatarProfile } from '@/components/user-avatar-profile';
import { useRouter } from 'next/navigation';
const API_URL = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:8080/api/v1';
export function UserNav() {
const router = useRouter();
const [user, setUser] = useState<{
user_id: string | number;
fullName: string;
email: string;
avatar?: string;
} | null>(null);
useEffect(() => {
const userId = localStorage.getItem('user_id');
if (!userId) return;
fetch(`${API_URL}/users/${userId}`, { credentials: 'include' })
.then(res => res.json())
.then(data => {
setUser({
user_id: data.user_id || userId,
fullName: data.name || data.username || '',
email: data.email,
});
});
}, []);
if (!user) return null;
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant='ghost' className='relative h-8 w-8 rounded-full'>
<UserAvatarProfile
user={{
user_id: user.user_id,
fullName: user.fullName,
emailAddresses: [{ emailAddress: user.email }],
}}
/>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent
className='w-56'
align='end'
sideOffset={10}
forceMount
>
<DropdownMenuLabel className='font-normal'>
<div className='flex flex-col space-y-1'>
<p className='text-sm leading-none font-medium'>
{user.fullName}
</p>
<p className='text-muted-foreground text-xs leading-none'>
{user.email}
</p>
</div>
</DropdownMenuLabel>
<DropdownMenuSeparator />
<DropdownMenuGroup>
<DropdownMenuItem onClick={() => router.push('/dashboard/profile')}>
Profile
</DropdownMenuItem>
<DropdownMenuItem>Billing</DropdownMenuItem>
<DropdownMenuItem>Settings</DropdownMenuItem>
<DropdownMenuItem>New Team</DropdownMenuItem>
</DropdownMenuGroup>
<DropdownMenuSeparator />
<DropdownMenuItem
onClick={async () => {
await fetch(`${API_URL}/signout`, {
method: 'POST',
credentials: 'include',
});
router.push('/auth/sign-in');
}}
>
Log out
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
);
}

View File

@@ -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<AlertModalProps> = ({
isOpen,
onClose,
onConfirm,
loading
}) => {
const [isMounted, setIsMounted] = useState(false);
useEffect(() => {
setIsMounted(true);
}, []);
if (!isMounted) {
return null;
}
return (
<Modal
title='Are you sure?'
description='This action cannot be undone.'
isOpen={isOpen}
onClose={onClose}
>
<div className='flex w-full items-center justify-end space-x-2 pt-6'>
<Button disabled={loading} variant='outline' onClick={onClose}>
Cancel
</Button>
<Button disabled={loading} variant='destructive' onClick={onConfirm}>
Continue
</Button>
</div>
</Modal>
);
};

View File

@@ -0,0 +1,85 @@
'use client';
import { IconChevronRight } from '@tabler/icons-react';
import {
Collapsible,
CollapsibleContent,
CollapsibleTrigger
} from '@/components/ui/collapsible';
import {
SidebarGroup,
SidebarGroupContent,
SidebarGroupLabel,
SidebarMenu,
SidebarMenuButton,
SidebarMenuItem,
SidebarMenuSub,
SidebarMenuSubButton,
SidebarMenuSubItem
} from '@/components/ui/sidebar';
import { Icon } from '@/components/icons';
export function NavMain({
items
}: {
items: {
title: string;
url: string;
icon?: Icon;
isActive?: boolean;
items?: {
title: string;
url: string;
}[];
}[];
}) {
return (
<SidebarGroup>
<SidebarGroupLabel>Platform</SidebarGroupLabel>
<SidebarGroupContent className='flex flex-col gap-2'>
<SidebarMenu>
<SidebarMenuItem>
<SidebarMenuButton tooltip="Video Chat" className="bg-primary text-primary-foreground hover:bg-primary/90 hover:text-primary-foreground active:bg-primary/90 active:text-primary-foreground min-w-8 duration-200 ease-linear">
<a href="/video-chat-room">Video Chat</a>
</SidebarMenuButton>
</SidebarMenuItem>
{items.map((item) => (
<Collapsible
key={item.title}
asChild
defaultOpen={item.isActive}
className='group/collapsible'
>
<SidebarMenuItem>
<CollapsibleTrigger asChild>
<SidebarMenuButton
tooltip={item.title}
className='bg-primary text-primary-foreground hover:bg-primary/90 hover:text-primary-foreground active:bg-primary/90 active:text-primary-foreground min-w-8 duration-200 ease-linear'
>
{item.icon && <item.icon />}
<span>{item.title}</span>
<IconChevronRight className='ml-auto transition-transform duration-200 group-data-[state=open]/collapsible:rotate-90' />
</SidebarMenuButton>
</CollapsibleTrigger>
<CollapsibleContent>
<SidebarMenuSub>
{item.items?.map((subItem) => (
<SidebarMenuSubItem key={subItem.title}>
<SidebarMenuSubButton asChild>
<a href={subItem.url}>
<span>{subItem.title}</span>
</a>
</SidebarMenuSubButton>
</SidebarMenuSubItem>
))}
</SidebarMenuSub>
</CollapsibleContent>
</SidebarMenuItem>
</Collapsible>
))}
</SidebarMenu>
</SidebarGroupContent>
</SidebarGroup>
);
}

View File

@@ -0,0 +1,89 @@
'use client';
import {
IconFolder,
IconShare,
IconDots,
IconTrash
} from '@tabler/icons-react';
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuSeparator,
DropdownMenuTrigger
} from '@/components/ui/dropdown-menu';
import {
SidebarGroup,
SidebarGroupLabel,
SidebarMenu,
SidebarMenuAction,
SidebarMenuButton,
SidebarMenuItem,
useSidebar
} from '@/components/ui/sidebar';
import { Icon } from '@/components/icons';
export function NavProjects({
projects
}: {
projects: {
name: string;
url: string;
icon: Icon;
}[];
}) {
const { isMobile } = useSidebar();
return (
<SidebarGroup className='group-data-[collapsible=icon]:hidden'>
<SidebarGroupLabel>Projects</SidebarGroupLabel>
<SidebarMenu>
{projects.map((item) => (
<SidebarMenuItem key={item.name}>
<SidebarMenuButton asChild>
<a href={item.url}>
<item.icon />
<span>{item.name}</span>
</a>
</SidebarMenuButton>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<SidebarMenuAction showOnHover>
<IconDots />
<span className='sr-only'>More</span>
</SidebarMenuAction>
</DropdownMenuTrigger>
<DropdownMenuContent
className='w-48 rounded-lg'
side={isMobile ? 'bottom' : 'right'}
align={isMobile ? 'end' : 'start'}
>
<DropdownMenuItem>
<IconFolder className='text-muted-foreground mr-2 h-4 w-4' />
<span>View Project</span>
</DropdownMenuItem>
<DropdownMenuItem>
<IconShare className='text-muted-foreground mr-2 h-4 w-4' />
<span>Share Project</span>
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem>
<IconTrash className='text-muted-foreground mr-2 h-4 w-4' />
<span>Delete Project</span>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</SidebarMenuItem>
))}
<SidebarMenuItem>
<SidebarMenuButton className='text-sidebar-foreground/70'>
<IconDots className='text-sidebar-foreground/70' />
<span>More</span>
</SidebarMenuButton>
</SidebarMenuItem>
</SidebarMenu>
</SidebarGroup>
);
}

146
src/components/nav-user.tsx Normal file
View File

@@ -0,0 +1,146 @@
'use client';
import { useEffect, useState } from 'react';
import {
IconCircleCheck,
IconBell,
IconChevronsDown,
IconCreditCard,
IconLogout,
IconSparkles
} from '@tabler/icons-react';
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuGroup,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuTrigger
} from '@/components/ui/dropdown-menu';
import {
SidebarMenu,
SidebarMenuButton,
SidebarMenuItem,
useSidebar
} from '@/components/ui/sidebar';
const API_URL = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:8080/api/v1';
export function NavUser() {
const { isMobile } = useSidebar();
const [user, setUser] = useState<{
username: string;
email: string;
user_id?: string | number;
} | null>(null);
useEffect(() => {
const userId = localStorage.getItem('user_id');
if (!userId) return;
fetch(`${API_URL}/users/${userId}`, { credentials: 'include' })
.then(res => res.json())
.then(data => {
setUser({
user_id: data.user_id || userId,
username: data.username || '',
email: data.email || '',
});
if (data.username) {
localStorage.setItem('username', data.username);
}
});
}, []);
if (!user) return null;
const profilePhotoUrl = user.user_id
? `${API_URL}/users/${user.user_id}/profile_photo?${Date.now()}`
: '';
const fallbackInitials =
user.username?.slice(0, 2).toUpperCase() ||
user.email?.slice(0, 2).toUpperCase() ||
'U';
return (
<SidebarMenu>
<SidebarMenuItem>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<SidebarMenuButton
size='lg'
className='data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground'
>
<Avatar className='h-8 w-8 rounded-lg'>
<AvatarImage src={profilePhotoUrl} alt={user.username} crossOrigin="use-credentials" />
<AvatarFallback className='rounded-lg'>{fallbackInitials}</AvatarFallback>
</Avatar>
<div className='grid flex-1 text-left text-sm leading-tight'>
<span className='truncate font-semibold'>{user.username}</span>
<span className='truncate text-xs'>{user.email}</span>
</div>
<IconChevronsDown className='ml-auto size-4' />
</SidebarMenuButton>
</DropdownMenuTrigger>
<DropdownMenuContent
className='w-(--radix-dropdown-menu-trigger-width) min-w-56 rounded-lg'
side={isMobile ? 'bottom' : 'right'}
align='end'
sideOffset={4}
>
<DropdownMenuLabel className='p-0 font-normal'>
<div className='flex items-center gap-2 px-1 py-1.5 text-left text-sm'>
<Avatar className='h-8 w-8 rounded-lg'>
<AvatarImage src={profilePhotoUrl} alt={user.username} crossOrigin="use-credentials" />
<AvatarFallback className='rounded-lg'>{fallbackInitials}</AvatarFallback>
</Avatar>
<div className='grid flex-1 text-left text-sm leading-tight'>
<span className='truncate font-semibold'>{user.username}</span>
<span className='truncate text-xs'>{user.email}</span>
</div>
</div>
</DropdownMenuLabel>
<DropdownMenuSeparator />
<DropdownMenuGroup>
<DropdownMenuItem>
<IconSparkles className='mr-2 h-4 w-4' />
Upgrade to Pro
</DropdownMenuItem>
</DropdownMenuGroup>
<DropdownMenuSeparator />
<DropdownMenuGroup>
<DropdownMenuItem>
<IconCircleCheck className='mr-2 h-4 w-4' />
Account
</DropdownMenuItem>
<DropdownMenuItem>
<IconCreditCard className='mr-2 h-4 w-4' />
Billing
</DropdownMenuItem>
<DropdownMenuItem>
<IconBell className='mr-2 h-4 w-4' />
Notifications
</DropdownMenuItem>
</DropdownMenuGroup>
<DropdownMenuSeparator />
<DropdownMenuItem
onClick={async () => {
await fetch(`${API_URL}/signout`, {
method: 'POST',
credentials: 'include',
});
window.location.href = '/auth/sign-in';
}}
>
<IconLogout className='mr-2 h-4 w-4' />
Log out
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</SidebarMenuItem>
</SidebarMenu>
);
}

View File

@@ -0,0 +1,85 @@
'use client';
import { Check, ChevronsUpDown, GalleryVerticalEnd } from 'lucide-react';
import * as React from 'react';
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger
} from '@/components/ui/dropdown-menu';
import {
SidebarMenu,
SidebarMenuButton,
SidebarMenuItem
} from '@/components/ui/sidebar';
interface Tenant {
id: string;
name: string;
}
export function OrgSwitcher({
tenants,
defaultTenant,
onTenantSwitch
}: {
tenants: Tenant[];
defaultTenant: Tenant;
onTenantSwitch?: (tenantId: string) => void;
}) {
const [selectedTenant, setSelectedTenant] = React.useState<
Tenant | undefined
>(defaultTenant || (tenants.length > 0 ? tenants[0] : undefined));
const handleTenantSwitch = (tenant: Tenant) => {
setSelectedTenant(tenant);
if (onTenantSwitch) {
onTenantSwitch(tenant.id);
}
};
if (!selectedTenant) {
return null;
}
return (
<SidebarMenu>
<SidebarMenuItem>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<SidebarMenuButton
size='lg'
className='data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground'
>
<div className='bg-primary text-sidebar-primary-foreground flex aspect-square size-8 items-center justify-center rounded-lg'>
<GalleryVerticalEnd className='size-4' />
</div>
<div className='flex flex-col gap-0.5 leading-none'>
<span className='font-semibold'>Next Starter</span>
<span className=''>{selectedTenant.name}</span>
</div>
<ChevronsUpDown className='ml-auto' />
</SidebarMenuButton>
</DropdownMenuTrigger>
<DropdownMenuContent
className='w-[--radix-dropdown-menu-trigger-width]'
align='start'
>
{tenants.map((tenant) => (
<DropdownMenuItem
key={tenant.id}
onSelect={() => handleTenantSwitch(tenant)}
>
{tenant.name}{' '}
{tenant.id === selectedTenant.id && (
<Check className='ml-auto' />
)}
</DropdownMenuItem>
))}
</DropdownMenuContent>
</DropdownMenu>
</SidebarMenuItem>
</SidebarMenu>
);
}

View File

@@ -0,0 +1,23 @@
'use client';
import { useKBar } from 'kbar';
import { IconSearch } from '@tabler/icons-react';
import { Button } from './ui/button';
export default function SearchInput() {
const { query } = useKBar();
return (
<div className='w-full space-y-2'>
<Button
variant='outline'
className='bg-background text-muted-foreground relative h-9 w-full justify-start rounded-[0.5rem] text-sm font-normal shadow-none sm:pr-12 md:w-40 lg:w-64'
onClick={query.toggle}
>
<IconSearch className='mr-2 h-4 w-4' />
Search...
<kbd className='bg-muted pointer-events-none absolute top-[0.3rem] right-[0.3rem] hidden h-6 items-center gap-1 rounded border px-1.5 font-mono text-[10px] font-medium opacity-100 select-none sm:flex'>
<span className='text-xs'>⌘</span>K
</kbd>
</Button>
</div>
);
}

View File

@@ -0,0 +1,94 @@
'use client';
import { useThemeConfig } from '@/components/active-theme';
import { Label } from '@/components/ui/label';
import {
Select,
SelectContent,
SelectGroup,
SelectItem,
SelectLabel,
SelectSeparator,
SelectTrigger,
SelectValue
} from '@/components/ui/select';
const DEFAULT_THEMES = [
{ name: 'Default', value: 'default' },
{ name: 'Blue', value: 'blue' },
{ name: 'Green', value: 'green' },
{ name: 'Amber', value: 'amber' },
{ name: 'Pink', value: 'pink' },
{ name: 'Red', value: 'red' },
{ name: 'Purple', value: 'purple' },
{ name: 'Gold', value: 'gold' } // <-- Added gold
];
const SCALED_THEMES = [
{ name: 'Default', value: 'default-scaled' },
{ name: 'Blue', value: 'blue-scaled' },
{ name: 'Green', value: 'green-scaled' },
{ name: 'Amber', value: 'amber-scaled' },
{ name: 'Pink', value: 'pink-scaled' },
{ name: 'Red', value: 'red-scaled' },
{ name: 'Purple', value: 'purple-scaled' },
{ name: 'Gold', value: 'gold-scaled' } // <-- Added gold
];
const MONO_THEMES = [
{
name: 'Mono',
value: 'mono-scaled'
}
];
export function ThemeSelector() {
const { activeTheme, setActiveTheme } = useThemeConfig();
return (
<div className='flex items-center gap-2'>
<Label htmlFor='theme-selector' className='sr-only'>
Theme
</Label>
<Select value={activeTheme} onValueChange={setActiveTheme}>
<SelectTrigger
id='theme-selector'
className='justify-start *:data-[slot=select-value]:w-12'
>
<span className='text-muted-foreground hidden sm:block'>
Select a theme:
</span>
<span className='text-muted-foreground block sm:hidden'>Theme</span>
<SelectValue placeholder='Select a theme' />
</SelectTrigger>
<SelectContent align='end'>
<SelectGroup>
<SelectLabel>Default</SelectLabel>
{DEFAULT_THEMES.map((theme) => (
<SelectItem key={theme.name} value={theme.value}>
{theme.name}
</SelectItem>
))}
</SelectGroup>
<SelectSeparator />
<SelectGroup>
<SelectLabel>Scaled</SelectLabel>
{SCALED_THEMES.map((theme) => (
<SelectItem key={theme.name} value={theme.value}>
{theme.name}
</SelectItem>
))}
</SelectGroup>
<SelectGroup>
<SelectLabel>Monospaced</SelectLabel>
{MONO_THEMES.map((theme) => (
<SelectItem key={theme.name} value={theme.value}>
{theme.name}
</SelectItem>
))}
</SelectGroup>
</SelectContent>
</Select>
</div>
);
}

View File

@@ -0,0 +1,66 @@
'use client';
import * as React from 'react';
import * as AccordionPrimitive from '@radix-ui/react-accordion';
import { ChevronDownIcon } from 'lucide-react';
import { cn } from '@/lib/utils';
function Accordion({
...props
}: React.ComponentProps<typeof AccordionPrimitive.Root>) {
return <AccordionPrimitive.Root data-slot='accordion' {...props} />;
}
function AccordionItem({
className,
...props
}: React.ComponentProps<typeof AccordionPrimitive.Item>) {
return (
<AccordionPrimitive.Item
data-slot='accordion-item'
className={cn('border-b last:border-b-0', className)}
{...props}
/>
);
}
function AccordionTrigger({
className,
children,
...props
}: React.ComponentProps<typeof AccordionPrimitive.Trigger>) {
return (
<AccordionPrimitive.Header className='flex'>
<AccordionPrimitive.Trigger
data-slot='accordion-trigger'
className={cn(
'focus-visible:border-ring focus-visible:ring-ring/50 flex flex-1 items-start justify-between gap-4 rounded-md py-4 text-left text-sm font-medium transition-all outline-none hover:underline focus-visible:ring-[3px] disabled:pointer-events-none disabled:opacity-50 [&[data-state=open]>svg]:rotate-180',
className
)}
{...props}
>
{children}
<ChevronDownIcon className='text-muted-foreground pointer-events-none size-4 shrink-0 translate-y-0.5 transition-transform duration-200' />
</AccordionPrimitive.Trigger>
</AccordionPrimitive.Header>
);
}
function AccordionContent({
className,
children,
...props
}: React.ComponentProps<typeof AccordionPrimitive.Content>) {
return (
<AccordionPrimitive.Content
data-slot='accordion-content'
className='data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down overflow-hidden text-sm'
{...props}
>
<div className={cn('pt-0 pb-4', className)}>{children}</div>
</AccordionPrimitive.Content>
);
}
export { Accordion, AccordionItem, AccordionTrigger, AccordionContent };

View File

@@ -0,0 +1,157 @@
'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';
function AlertDialog({
...props
}: React.ComponentProps<typeof AlertDialogPrimitive.Root>) {
return <AlertDialogPrimitive.Root data-slot='alert-dialog' {...props} />;
}
function AlertDialogTrigger({
...props
}: React.ComponentProps<typeof AlertDialogPrimitive.Trigger>) {
return (
<AlertDialogPrimitive.Trigger data-slot='alert-dialog-trigger' {...props} />
);
}
function AlertDialogPortal({
...props
}: React.ComponentProps<typeof AlertDialogPrimitive.Portal>) {
return (
<AlertDialogPrimitive.Portal data-slot='alert-dialog-portal' {...props} />
);
}
function AlertDialogOverlay({
className,
...props
}: React.ComponentProps<typeof AlertDialogPrimitive.Overlay>) {
return (
<AlertDialogPrimitive.Overlay
data-slot='alert-dialog-overlay'
className={cn(
'data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50',
className
)}
{...props}
/>
);
}
function AlertDialogContent({
className,
...props
}: React.ComponentProps<typeof AlertDialogPrimitive.Content>) {
return (
<AlertDialogPortal>
<AlertDialogOverlay />
<AlertDialogPrimitive.Content
data-slot='alert-dialog-content'
className={cn(
'bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 sm:max-w-lg',
className
)}
{...props}
/>
</AlertDialogPortal>
);
}
function AlertDialogHeader({
className,
...props
}: React.ComponentProps<'div'>) {
return (
<div
data-slot='alert-dialog-header'
className={cn('flex flex-col gap-2 text-center sm:text-left', className)}
{...props}
/>
);
}
function AlertDialogFooter({
className,
...props
}: React.ComponentProps<'div'>) {
return (
<div
data-slot='alert-dialog-footer'
className={cn(
'flex flex-col-reverse gap-2 sm:flex-row sm:justify-end',
className
)}
{...props}
/>
);
}
function AlertDialogTitle({
className,
...props
}: React.ComponentProps<typeof AlertDialogPrimitive.Title>) {
return (
<AlertDialogPrimitive.Title
data-slot='alert-dialog-title'
className={cn('text-lg font-semibold', className)}
{...props}
/>
);
}
function AlertDialogDescription({
className,
...props
}: React.ComponentProps<typeof AlertDialogPrimitive.Description>) {
return (
<AlertDialogPrimitive.Description
data-slot='alert-dialog-description'
className={cn('text-muted-foreground text-sm', className)}
{...props}
/>
);
}
function AlertDialogAction({
className,
...props
}: React.ComponentProps<typeof AlertDialogPrimitive.Action>) {
return (
<AlertDialogPrimitive.Action
className={cn(buttonVariants(), className)}
{...props}
/>
);
}
function AlertDialogCancel({
className,
...props
}: React.ComponentProps<typeof AlertDialogPrimitive.Cancel>) {
return (
<AlertDialogPrimitive.Cancel
className={cn(buttonVariants({ variant: 'outline' }), className)}
{...props}
/>
);
}
export {
AlertDialog,
AlertDialogPortal,
AlertDialogOverlay,
AlertDialogTrigger,
AlertDialogContent,
AlertDialogHeader,
AlertDialogFooter,
AlertDialogTitle,
AlertDialogDescription,
AlertDialogAction,
AlertDialogCancel
};

View File

@@ -0,0 +1,66 @@
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 grid has-[>svg]:grid-cols-[calc(var(--spacing)*4)_1fr] grid-cols-[0_1fr] has-[>svg]:gap-x-3 gap-y-0.5 items-start [&>svg]:size-4 [&>svg]:translate-y-0.5 [&>svg]:text-current',
{
variants: {
variant: {
default: 'bg-card text-card-foreground',
destructive:
'text-destructive bg-card [&>svg]:text-current *:data-[slot=alert-description]:text-destructive/90'
}
},
defaultVariants: {
variant: 'default'
}
}
);
function Alert({
className,
variant,
...props
}: React.ComponentProps<'div'> & VariantProps<typeof alertVariants>) {
return (
<div
data-slot='alert'
role='alert'
className={cn(alertVariants({ variant }), className)}
{...props}
/>
);
}
function AlertTitle({ className, ...props }: React.ComponentProps<'div'>) {
return (
<div
data-slot='alert-title'
className={cn(
'col-start-2 line-clamp-1 min-h-4 font-medium tracking-tight',
className
)}
{...props}
/>
);
}
function AlertDescription({
className,
...props
}: React.ComponentProps<'div'>) {
return (
<div
data-slot='alert-description'
className={cn(
'text-muted-foreground col-start-2 grid justify-items-start gap-1 text-sm [&_p]:leading-relaxed',
className
)}
{...props}
/>
);
}
export { Alert, AlertTitle, AlertDescription };

View File

@@ -0,0 +1,11 @@
'use client';
import * as AspectRatioPrimitive from '@radix-ui/react-aspect-ratio';
function AspectRatio({
...props
}: React.ComponentProps<typeof AspectRatioPrimitive.Root>) {
return <AspectRatioPrimitive.Root data-slot='aspect-ratio' {...props} />;
}
export { AspectRatio };

View File

@@ -0,0 +1,53 @@
'use client';
import * as React from 'react';
import * as AvatarPrimitive from '@radix-ui/react-avatar';
import { cn } from '@/lib/utils';
function Avatar({
className,
...props
}: React.ComponentProps<typeof AvatarPrimitive.Root>) {
return (
<AvatarPrimitive.Root
data-slot='avatar'
className={cn(
'relative flex size-8 shrink-0 overflow-hidden rounded-full',
className
)}
{...props}
/>
);
}
function AvatarImage({
className,
...props
}: React.ComponentProps<typeof AvatarPrimitive.Image>) {
return (
<AvatarPrimitive.Image
data-slot='avatar-image'
className={cn('aspect-square size-full', className)}
{...props}
/>
);
}
function AvatarFallback({
className,
...props
}: React.ComponentProps<typeof AvatarPrimitive.Fallback>) {
return (
<AvatarPrimitive.Fallback
data-slot='avatar-fallback'
className={cn(
'bg-muted flex size-full items-center justify-center rounded-full',
className
)}
{...props}
/>
);
}
export { Avatar, AvatarImage, AvatarFallback };

View File

@@ -0,0 +1,46 @@
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 badgeVariants = cva(
'inline-flex items-center justify-center rounded-md border px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive transition-[color,box-shadow] overflow-hidden',
{
variants: {
variant: {
default:
'border-transparent bg-primary text-primary-foreground [a&]:hover:bg-primary/90',
secondary:
'border-transparent bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90',
destructive:
'border-transparent bg-destructive text-white [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60',
outline:
'text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground'
}
},
defaultVariants: {
variant: 'default'
}
}
);
function Badge({
className,
variant,
asChild = false,
...props
}: React.ComponentProps<'span'> &
VariantProps<typeof badgeVariants> & { asChild?: boolean }) {
const Comp = asChild ? Slot : 'span';
return (
<Comp
data-slot='badge'
className={cn(badgeVariants({ variant }), className)}
{...props}
/>
);
}
export { Badge, badgeVariants };

View File

@@ -0,0 +1,109 @@
import * as React from 'react';
import { Slot } from '@radix-ui/react-slot';
import { ChevronRight, MoreHorizontal } from 'lucide-react';
import { cn } from '@/lib/utils';
function Breadcrumb({ ...props }: React.ComponentProps<'nav'>) {
return <nav aria-label='breadcrumb' data-slot='breadcrumb' {...props} />;
}
function BreadcrumbList({ className, ...props }: React.ComponentProps<'ol'>) {
return (
<ol
data-slot='breadcrumb-list'
className={cn(
'text-muted-foreground flex flex-wrap items-center gap-1.5 text-sm break-words sm:gap-2.5',
className
)}
{...props}
/>
);
}
function BreadcrumbItem({ className, ...props }: React.ComponentProps<'li'>) {
return (
<li
data-slot='breadcrumb-item'
className={cn('inline-flex items-center gap-1.5', className)}
{...props}
/>
);
}
function BreadcrumbLink({
asChild,
className,
...props
}: React.ComponentProps<'a'> & {
asChild?: boolean;
}) {
const Comp = asChild ? Slot : 'a';
return (
<Comp
data-slot='breadcrumb-link'
className={cn('hover:text-foreground transition-colors', className)}
{...props}
/>
);
}
function BreadcrumbPage({ className, ...props }: React.ComponentProps<'span'>) {
return (
<span
data-slot='breadcrumb-page'
role='link'
aria-disabled='true'
aria-current='page'
className={cn('text-foreground font-normal', className)}
{...props}
/>
);
}
function BreadcrumbSeparator({
children,
className,
...props
}: React.ComponentProps<'li'>) {
return (
<li
data-slot='breadcrumb-separator'
role='presentation'
aria-hidden='true'
className={cn('[&>svg]:size-3.5', className)}
{...props}
>
{children ?? <ChevronRight />}
</li>
);
}
function BreadcrumbEllipsis({
className,
...props
}: React.ComponentProps<'span'>) {
return (
<span
data-slot='breadcrumb-ellipsis'
role='presentation'
aria-hidden='true'
className={cn('flex size-9 items-center justify-center', className)}
{...props}
>
<MoreHorizontal className='size-4' />
<span className='sr-only'>More</span>
</span>
);
}
export {
Breadcrumb,
BreadcrumbList,
BreadcrumbItem,
BreadcrumbLink,
BreadcrumbPage,
BreadcrumbSeparator,
BreadcrumbEllipsis
};

View File

@@ -0,0 +1,59 @@
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 gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
{
variants: {
variant: {
default:
'bg-primary text-primary-foreground shadow-xs hover:bg-primary/90',
destructive:
'bg-destructive text-white shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60',
outline:
'border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50',
secondary:
'bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80',
ghost:
'hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50',
link: 'text-primary underline-offset-4 hover:underline'
},
size: {
default: 'h-9 px-4 py-2 has-[>svg]:px-3',
sm: 'h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5',
lg: 'h-10 rounded-md px-6 has-[>svg]:px-4',
icon: 'size-9'
}
},
defaultVariants: {
variant: 'default',
size: 'default'
}
}
);
function Button({
className,
variant,
size,
asChild = false,
...props
}: React.ComponentProps<'button'> &
VariantProps<typeof buttonVariants> & {
asChild?: boolean;
}) {
const Comp = asChild ? Slot : 'button';
return (
<Comp
data-slot='button'
className={cn(buttonVariants({ variant, size, className }))}
{...props}
/>
);
}
export { Button, buttonVariants };

View File

@@ -0,0 +1,76 @@
'use client';
import * as React from 'react';
import { DayPicker } from 'react-day-picker';
import type { ComponentProps } from 'react';
import { cn } from '@/lib/utils';
import { buttonVariants } from '@/components/ui/button';
import { ChevronLeftIcon, ChevronRightIcon } from '@radix-ui/react-icons';
// Custom icons that meet the DayPicker requirements
const LeftIcon = () => <ChevronLeftIcon className='size-4' />;
const RightIcon = () => <ChevronRightIcon className='size-4' />;
function Calendar({
className,
classNames,
showOutsideDays = true,
...props
}: ComponentProps<typeof DayPicker>) {
return (
<DayPicker
showOutsideDays={showOutsideDays}
className={cn('p-3', className)}
classNames={{
months: 'flex flex-col sm:flex-row gap-2',
month: 'flex flex-col gap-4',
caption: 'flex justify-center pt-1 relative items-center w-full',
caption_label: 'text-sm font-medium',
nav: 'flex items-center gap-1',
nav_button: cn(
buttonVariants({ variant: 'outline' }),
'size-7 bg-transparent p-0 opacity-50 hover:opacity-100'
),
nav_button_previous: 'absolute left-1',
nav_button_next: 'absolute right-1',
table: 'w-full border-collapse space-x-1',
head_row: 'flex',
head_cell:
'text-muted-foreground rounded-md w-8 font-normal text-[0.8rem]',
row: 'flex w-full mt-2',
cell: cn(
'relative p-0 text-center text-sm focus-within:relative focus-within:z-20 [&:has([aria-selected])]:bg-accent [&:has([aria-selected].day-range-end)]:rounded-r-md',
props.mode === 'range'
? '[&:has(>.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' }),
'size-8 p-0 font-normal aria-selected:opacity-100'
),
day_range_start:
'day-range-start aria-selected:bg-primary aria-selected:text-primary-foreground',
day_range_end:
'day-range-end aria-selected:bg-primary aria-selected:text-primary-foreground',
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:
'day-outside text-muted-foreground aria-selected:text-muted-foreground',
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: LeftIcon,
IconRight: RightIcon
}}
{...props}
/>
);
}
export { Calendar };

View File

@@ -0,0 +1,92 @@
import * as React from 'react';
import { cn } from '@/lib/utils';
function Card({ className, ...props }: React.ComponentProps<'div'>) {
return (
<div
data-slot='card'
className={cn(
'bg-card text-card-foreground flex flex-col gap-6 rounded-xl border py-6 shadow-sm',
className
)}
{...props}
/>
);
}
function CardHeader({ className, ...props }: React.ComponentProps<'div'>) {
return (
<div
data-slot='card-header'
className={cn(
'@container/card-header grid auto-rows-min grid-rows-[auto_auto] items-start gap-1.5 px-6 has-data-[slot=card-action]:grid-cols-[1fr_auto] [.border-b]:pb-6',
className
)}
{...props}
/>
);
}
function CardTitle({ className, ...props }: React.ComponentProps<'div'>) {
return (
<div
data-slot='card-title'
className={cn('leading-none font-semibold', className)}
{...props}
/>
);
}
function CardDescription({ className, ...props }: React.ComponentProps<'div'>) {
return (
<div
data-slot='card-description'
className={cn('text-muted-foreground text-sm', className)}
{...props}
/>
);
}
function CardAction({ className, ...props }: React.ComponentProps<'div'>) {
return (
<div
data-slot='card-action'
className={cn(
'col-start-2 row-span-2 row-start-1 self-start justify-self-end',
className
)}
{...props}
/>
);
}
function CardContent({ className, ...props }: React.ComponentProps<'div'>) {
return (
<div
data-slot='card-content'
className={cn('px-6', className)}
{...props}
/>
);
}
function CardFooter({ className, ...props }: React.ComponentProps<'div'>) {
return (
<div
data-slot='card-footer'
className={cn('flex items-center px-6 [.border-t]:pt-6', className)}
{...props}
/>
);
}
export {
Card,
CardHeader,
CardFooter,
CardTitle,
CardAction,
CardDescription,
CardContent
};

Some files were not shown because too many files have changed in this diff Show More