1 Commits

Author SHA1 Message Date
2f69ac14c5 prototype 1.0 2026-05-07 17:29:59 +03:00
291 changed files with 34857 additions and 5683 deletions

View File

@@ -1,5 +0,0 @@
VITE_API_BASE_URL=http://localhost:8080
VITE_PROXY_V1_TARGET=http://localhost:8080
VITE_PROXY_API_TARGET=http://localhost:8445
VITE_PROXY_WS_TARGET=ws://localhost:8446
VITE_USE_TRAEFIK=false

16
.env.example Normal file
View File

@@ -0,0 +1,16 @@
# Environment Variables for Vite React Template
# Google Tag Manager ID (optional)
# Only set this in production deployments to enable analytics tracking
# Leave empty or omit for development/open-source usage
VITE_GTM_ID=
# Example for your production deployment:
# VITE_GTM_ID=GTM-XXXXXXXX
# Base path for deployment (optional)
# Used when deploying to a subdirectory like /templates/dashboard/
VITE_BASENAME=
# Example for subdirectory deployment:
# VITE_BASENAME=/templates/dashboard/shadcn-dashboard-landing-template

27
.gitignore vendored
View File

@@ -1,24 +1,3 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
/node_modules/
/repo/
package-lock.json

View File

@@ -1,12 +0,0 @@
FROM node:20-alpine as build
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
COPY . .
RUN npm run build
FROM nginx:alpine
COPY --from=build /app/dist /usr/share/nginx/html
COPY nginx.conf /etc/nginx/conf.d/default.conf
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]

View File

@@ -1,16 +0,0 @@
.PHONY: install dev dev-direct build clean
install:
npm install
dev:
npm run dev
dev-direct:
npm run dev:direct
build:
npm run build
clean:
rm -rf dist node_modules

View File

@@ -1,73 +0,0 @@
# React + TypeScript + Vite
This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
Currently, two official plugins are available:
- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Oxc](https://oxc.rs)
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/)
## React Compiler
The React Compiler is not enabled on this template because of its impact on dev & build performances. To add it, see [this documentation](https://react.dev/learn/react-compiler/installation).
## Expanding the ESLint configuration
If you are developing a production application, we recommend updating the configuration to enable type-aware lint rules:
```js
export default defineConfig([
globalIgnores(['dist']),
{
files: ['**/*.{ts,tsx}'],
extends: [
// Other configs...
// Remove tseslint.configs.recommended and replace with this
tseslint.configs.recommendedTypeChecked,
// Alternatively, use this for stricter rules
tseslint.configs.strictTypeChecked,
// Optionally, add this for stylistic rules
tseslint.configs.stylisticTypeChecked,
// Other configs...
],
languageOptions: {
parserOptions: {
project: ['./tsconfig.node.json', './tsconfig.app.json'],
tsconfigRootDir: import.meta.dirname,
},
// other options...
},
},
])
```
You can also install [eslint-plugin-react-x](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x) and [eslint-plugin-react-dom](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-dom) for React-specific lint rules:
```js
// eslint.config.js
import reactX from 'eslint-plugin-react-x'
import reactDom from 'eslint-plugin-react-dom'
export default defineConfig([
globalIgnores(['dist']),
{
files: ['**/*.{ts,tsx}'],
extends: [
// Other configs...
// Enable lint rules for React
reactX.configs['recommended-typescript'],
// Enable lint rules for React DOM
reactDom.configs.recommended,
],
languageOptions: {
parserOptions: {
project: ['./tsconfig.node.json', './tsconfig.app.json'],
tsconfigRootDir: import.meta.dirname,
},
// other options...
},
},
])
```

21
components.json Normal file
View File

@@ -0,0 +1,21 @@
{
"$schema": "https://ui.shadcn.com/schema.json",
"style": "new-york",
"rsc": false,
"tsx": true,
"tailwind": {
"config": "",
"css": "src/index.css",
"baseColor": "neutral",
"cssVariables": true,
"prefix": ""
},
"aliases": {
"components": "@/components",
"utils": "@/lib/utils",
"ui": "@/components/ui",
"lib": "@/lib",
"hooks": "@/hooks"
},
"iconLibrary": "lucide"
}

View File

@@ -3,19 +3,20 @@ import globals from 'globals'
import reactHooks from 'eslint-plugin-react-hooks'
import reactRefresh from 'eslint-plugin-react-refresh'
import tseslint from 'typescript-eslint'
import { defineConfig, globalIgnores } from 'eslint/config'
import { globalIgnores } from 'eslint/config'
export default defineConfig([
export default tseslint.config([
globalIgnores(['dist']),
{
files: ['**/*.{ts,tsx}'],
extends: [
js.configs.recommended,
tseslint.configs.recommended,
reactHooks.configs.flat.recommended,
reactHooks.configs['recommended-latest'],
reactRefresh.configs.vite,
],
languageOptions: {
ecmaVersion: 2020,
globals: globals.browser,
},
},

View File

@@ -2,9 +2,40 @@
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<link rel="icon" href="favicon.png" type="image/x-icon">
<link rel="icon" href="favicon-dark.png" type="image/png" media="(prefers-color-scheme: dark)">
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>eventhubfrontadmin</title>
<!-- Primary Meta Tags -->
<title>Shadcn Dashboard & Landing Template</title>
<meta name="title" content="Shadcn Dashboard & Landing Template" />
<meta name="description" content="Open-source Shadcn UI dashboard + landing page template built with React (Vite) and Next.js. Clean, modern, and production-ready." />
<!-- Canonical (points to the main template URL) -->
<link rel="canonical" href="https://shadcnstore.com/templates/dashboard/shadcn-dashboard-landing-template/" />
<!-- Open Graph / Facebook / LinkedIn -->
<meta property="og:type" content="website" />
<meta property="og:site_name" content="ShadcnStore" />
<meta property="og:title" content="Shadcn Dashboard & Landing Template" />
<meta property="og:description" content="Open-source Shadcn UI dashboard + landing page template built with React (Vite) and Next.js. Clean, modern, and production-ready." />
<meta property="og:url" content="https://shadcnstore.com/templates/dashboard/shadcn-dashboard-landing-template/" />
<!-- Use an absolute URL so scrapers can fetch the image reliably -->
<meta property="og:image" content="https://shadcnstore.com/templates/dashboard/shadcn-dashboard-landing-template/og-image.png" />
<meta property="og:image:alt" content="Screenshot of the Shadcn Dashboard & Landing Template" />
<!-- Optional: dimensions (leave generic if not exact) -->
<meta property="og:image:width" content="1200" />
<meta property="og:image:height" content="630" />
<!-- Twitter Card -->
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:title" content="Shadcn Dashboard & Landing Template" />
<meta name="twitter:description" content="Open-source Shadcn UI dashboard + landing page template built with React (Vite) and Next.js. Clean, modern, and production-ready." />
<meta name="twitter:image" content="https://shadcnstore.com/templates/dashboard/shadcn-dashboard-landing-template/og-image.png" />
<meta name="twitter:image:alt" content="Screenshot of the Shadcn Dashboard & Landing Template" />
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@100..900&display=swap" rel="stylesheet">
</head>
<body>
<div id="root"></div>

View File

@@ -1,8 +0,0 @@
server {
listen 80;
root /usr/share/nginx/html;
index index.html;
location / {
try_files $uri /index.html;
}
}

4542
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,34 +1,77 @@
{
"name": "eventhubfrontadmin",
"name": "shadcn-dashboard-vite",
"private": true,
"version": "0.0.0",
"version": "1.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"dev:cluster": "vite",
"dev:direct": "vite --mode development.direct",
"build": "tsc && vite build",
"build": "tsc -b && vite build",
"lint": "eslint .",
"preview": "vite preview"
},
"dependencies": {
"ra-data-simple-rest": "^5.14.6",
"react": "^19.2.5",
"react-admin": "^5.14.6",
"react-dom": "^19.2.5",
"recharts": "^3.8.1"
"@dnd-kit/core": "^6.3.1",
"@dnd-kit/modifiers": "^9.0.0",
"@dnd-kit/sortable": "^10.0.0",
"@dnd-kit/utilities": "^3.2.2",
"@hookform/resolvers": "^5.2.1",
"@radix-ui/react-accordion": "^1.2.11",
"@radix-ui/react-avatar": "^1.1.10",
"@radix-ui/react-checkbox": "^1.3.2",
"@radix-ui/react-collapsible": "^1.1.11",
"@radix-ui/react-dialog": "^1.1.14",
"@radix-ui/react-dropdown-menu": "^2.1.15",
"@radix-ui/react-hover-card": "^1.1.15",
"@radix-ui/react-label": "^2.1.7",
"@radix-ui/react-navigation-menu": "^1.2.14",
"@radix-ui/react-popover": "^1.1.14",
"@radix-ui/react-progress": "^1.1.7",
"@radix-ui/react-radio-group": "^1.3.7",
"@radix-ui/react-scroll-area": "^1.2.9",
"@radix-ui/react-select": "^2.2.5",
"@radix-ui/react-separator": "^1.1.7",
"@radix-ui/react-slot": "^1.2.3",
"@radix-ui/react-switch": "^1.2.5",
"@radix-ui/react-tabs": "^1.1.12",
"@radix-ui/react-toggle": "^1.1.9",
"@radix-ui/react-toggle-group": "^1.1.10",
"@radix-ui/react-tooltip": "^1.2.7",
"@tailwindcss/vite": "^4.1.11",
"@tanstack/react-table": "^8.21.3",
"add": "^2.0.6",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"cmdk": "^1.1.1",
"date-fns": "^4.1.0",
"lucide-react": "^0.536.0",
"next-themes": "^0.4.6",
"react": "^19.1.0",
"react-day-picker": "^9.8.1",
"react-dom": "^19.1.0",
"react-hook-form": "^7.62.0",
"react-resizable-panels": "^3.0.4",
"react-router-dom": "^7.7.1",
"recharts": "2.15.4",
"sonner": "^2.0.7",
"tailwind-merge": "^3.3.1",
"tailwindcss": "^4.1.11",
"vaul": "^1.1.2",
"zod": "^4.0.15",
"zustand": "^5.0.7"
},
"devDependencies": {
"@eslint/js": "^10.0.1",
"@types/node": "^24.12.2",
"@types/react": "^19.2.14",
"@types/react-dom": "^19.2.3",
"@vitejs/plugin-react": "^6.0.1",
"eslint": "^10.2.1",
"eslint-plugin-react-hooks": "^7.1.1",
"eslint-plugin-react-refresh": "^0.5.2",
"globals": "^17.5.0",
"typescript": "~6.0.2",
"typescript-eslint": "^8.58.2",
"vite": "^8.0.10"
"@eslint/js": "^9.30.1",
"@types/node": "^24.2.0",
"@types/react": "^19.1.8",
"@types/react-dom": "^19.1.6",
"@vitejs/plugin-react": "^4.6.0",
"eslint": "^9.30.1",
"eslint-plugin-react-hooks": "^5.2.0",
"eslint-plugin-react-refresh": "^0.4.20",
"globals": "^16.3.0",
"tw-animate-css": "^1.3.6",
"typescript": "~5.8.3",
"typescript-eslint": "^8.35.1",
"vite": "^7.0.4"
}
}

4495
pnpm-lock.yaml generated Normal file

File diff suppressed because it is too large Load Diff

BIN
public/apps.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 842 KiB

BIN
public/customizer.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 444 KiB

BIN
public/dashboard-dark.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 459 KiB

BIN
public/dashboard-light.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 475 KiB

BIN
public/dashboard.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 768 KiB

BIN
public/favicon-dark.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 551 B

BIN
public/favicon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 655 B

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 9.3 KiB

BIN
public/feature-1-dark.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 94 KiB

BIN
public/feature-1-light.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 100 KiB

BIN
public/feature-2-dark.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 110 KiB

BIN
public/feature-2-light.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 112 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 938 KiB

View File

@@ -1,24 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg">
<symbol id="bluesky-icon" viewBox="0 0 16 17">
<g clip-path="url(#bluesky-clip)"><path fill="#08060d" d="M7.75 7.735c-.693-1.348-2.58-3.86-4.334-5.097-1.68-1.187-2.32-.981-2.74-.79C.188 2.065.1 2.812.1 3.251s.241 3.602.398 4.13c.52 1.744 2.367 2.333 4.07 2.145-2.495.37-4.71 1.278-1.805 4.512 3.196 3.309 4.38-.71 4.987-2.746.608 2.036 1.307 5.91 4.93 2.746 2.72-2.746.747-4.143-1.747-4.512 1.702.189 3.55-.4 4.07-2.145.156-.528.397-3.691.397-4.13s-.088-1.186-.575-1.406c-.42-.19-1.06-.395-2.741.79-1.755 1.24-3.64 3.752-4.334 5.099"/></g>
<defs><clipPath id="bluesky-clip"><path fill="#fff" d="M.1.85h15.3v15.3H.1z"/></clipPath></defs>
</symbol>
<symbol id="discord-icon" viewBox="0 0 20 19">
<path fill="#08060d" d="M16.224 3.768a14.5 14.5 0 0 0-3.67-1.153c-.158.286-.343.67-.47.976a13.5 13.5 0 0 0-4.067 0c-.128-.306-.317-.69-.476-.976A14.4 14.4 0 0 0 3.868 3.77C1.546 7.28.916 10.703 1.231 14.077a14.7 14.7 0 0 0 4.5 2.306q.545-.748.965-1.587a9.5 9.5 0 0 1-1.518-.74q.191-.14.372-.293c2.927 1.369 6.107 1.369 8.999 0q.183.152.372.294-.723.437-1.52.74.418.838.963 1.588a14.6 14.6 0 0 0 4.504-2.308c.37-3.911-.63-7.302-2.644-10.309m-9.13 8.234c-.878 0-1.599-.82-1.599-1.82 0-.998.705-1.82 1.6-1.82.894 0 1.614.82 1.599 1.82.001 1-.705 1.82-1.6 1.82m5.91 0c-.878 0-1.599-.82-1.599-1.82 0-.998.705-1.82 1.6-1.82.893 0 1.614.82 1.599 1.82 0 1-.706 1.82-1.6 1.82"/>
</symbol>
<symbol id="documentation-icon" viewBox="0 0 21 20">
<path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="m15.5 13.333 1.533 1.322c.645.555.967.833.967 1.178s-.322.623-.967 1.179L15.5 18.333m-3.333-5-1.534 1.322c-.644.555-.966.833-.966 1.178s.322.623.966 1.179l1.534 1.321"/>
<path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M17.167 10.836v-4.32c0-1.41 0-2.117-.224-2.68-.359-.906-1.118-1.621-2.08-1.96-.599-.21-1.349-.21-2.848-.21-2.623 0-3.935 0-4.983.369-1.684.591-3.013 1.842-3.641 3.428C3 6.449 3 7.684 3 10.154v2.122c0 2.558 0 3.838.706 4.726q.306.383.713.671c.76.536 1.79.64 3.581.66"/>
<path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M3 10a2.78 2.78 0 0 1 2.778-2.778c.555 0 1.209.097 1.748-.047.48-.129.854-.503.982-.982.145-.54.048-1.194.048-1.749a2.78 2.78 0 0 1 2.777-2.777"/>
</symbol>
<symbol id="github-icon" viewBox="0 0 19 19">
<path fill="#08060d" fill-rule="evenodd" d="M9.356 1.85C5.05 1.85 1.57 5.356 1.57 9.694a7.84 7.84 0 0 0 5.324 7.44c.387.079.528-.168.528-.376 0-.182-.013-.805-.013-1.454-2.165.467-2.616-.935-2.616-.935-.349-.91-.864-1.143-.864-1.143-.71-.48.051-.48.051-.48.787.051 1.2.805 1.2.805.695 1.194 1.817.857 2.268.649.064-.507.27-.857.49-1.052-1.728-.182-3.545-.857-3.545-3.87 0-.857.31-1.558.8-2.104-.078-.195-.349-1 .077-2.078 0 0 .657-.208 2.14.805a7.5 7.5 0 0 1 1.946-.26c.657 0 1.328.092 1.946.26 1.483-1.013 2.14-.805 2.14-.805.426 1.078.155 1.883.078 2.078.502.546.799 1.247.799 2.104 0 3.013-1.818 3.675-3.558 3.87.284.247.528.714.528 1.454 0 1.052-.012 1.896-.012 2.156 0 .208.142.455.528.377a7.84 7.84 0 0 0 5.324-7.441c.013-4.338-3.48-7.844-7.773-7.844" clip-rule="evenodd"/>
</symbol>
<symbol id="social-icon" viewBox="0 0 20 20">
<path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M12.5 6.667a4.167 4.167 0 1 0-8.334 0 4.167 4.167 0 0 0 8.334 0"/>
<path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M2.5 16.667a5.833 5.833 0 0 1 8.75-5.053m3.837.474.513 1.035c.07.144.257.282.414.309l.93.155c.596.1.736.536.307.965l-.723.73a.64.64 0 0 0-.152.531l.207.903c.164.715-.213.991-.84.618l-.872-.52a.63.63 0 0 0-.577 0l-.872.52c-.624.373-1.003.094-.84-.618l.207-.903a.64.64 0 0 0-.152-.532l-.723-.729c-.426-.43-.289-.864.306-.964l.93-.156a.64.64 0 0 0 .412-.31l.513-1.034c.28-.562.735-.562 1.012 0"/>
</symbol>
<symbol id="x-icon" viewBox="0 0 19 19">
<path fill="#08060d" fill-rule="evenodd" d="M1.893 1.98c.052.072 1.245 1.769 2.653 3.77l2.892 4.114c.183.261.333.48.333.486s-.068.089-.152.183l-.522.593-.765.867-3.597 4.087c-.375.426-.734.834-.798.905a1 1 0 0 0-.118.148c0 .01.236.017.664.017h.663l.729-.83c.4-.457.796-.906.879-.999a692 692 0 0 0 1.794-2.038c.034-.037.301-.34.594-.675l.551-.624.345-.392a7 7 0 0 1 .34-.374c.006 0 .93 1.306 2.052 2.903l2.084 2.965.045.063h2.275c1.87 0 2.273-.003 2.266-.021-.008-.02-1.098-1.572-3.894-5.547-2.013-2.862-2.28-3.246-2.273-3.266.008-.019.282-.332 2.085-2.38l2-2.274 1.567-1.782c.022-.028-.016-.03-.65-.03h-.674l-.3.342a871 871 0 0 1-1.782 2.025c-.067.075-.405.458-.75.852a100 100 0 0 1-.803.91c-.148.172-.299.344-.99 1.127-.304.343-.32.358-.345.327-.015-.019-.904-1.282-1.976-2.808L6.365 1.85H1.8zm1.782.91 8.078 11.294c.772 1.08 1.413 1.973 1.425 1.984.016.017.241.02 1.05.017l1.03-.004-2.694-3.766L7.796 5.75 5.722 2.852l-1.039-.004-1.039-.004z" clip-rule="evenodd"/>
</symbol>
</svg>

Before

Width:  |  Height:  |  Size: 4.9 KiB

BIN
public/og-image.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 909 KiB

1
public/vite.svg Normal file
View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@@ -1,184 +1,42 @@
.counter {
font-size: 16px;
padding: 5px 10px;
border-radius: 5px;
color: var(--accent);
background: var(--accent-bg);
border: 2px solid transparent;
transition: border-color 0.3s;
margin-bottom: 24px;
&:hover {
border-color: var(--accent-border);
}
&:focus-visible {
outline: 2px solid var(--accent);
outline-offset: 2px;
}
}
.hero {
position: relative;
.base,
.framework,
.vite {
inset-inline: 0;
#root {
max-width: 1280px;
margin: 0 auto;
}
.base {
width: 170px;
position: relative;
z-index: 0;
}
.framework,
.vite {
position: absolute;
}
.framework {
z-index: 1;
top: 34px;
height: 28px;
transform: perspective(2000px) rotateZ(300deg) rotateX(44deg) rotateY(39deg)
scale(1.4);
}
.vite {
z-index: 0;
top: 107px;
height: 26px;
width: auto;
transform: perspective(2000px) rotateZ(300deg) rotateX(40deg) rotateY(39deg)
scale(0.8);
}
}
#center {
display: flex;
flex-direction: column;
gap: 25px;
place-content: center;
place-items: center;
flex-grow: 1;
@media (max-width: 1024px) {
padding: 32px 20px 24px;
gap: 18px;
}
}
#next-steps {
display: flex;
border-top: 1px solid var(--border);
text-align: left;
& > div {
flex: 1 1 0;
padding: 32px;
@media (max-width: 1024px) {
padding: 24px 20px;
}
}
.icon {
margin-bottom: 16px;
width: 22px;
height: 22px;
}
@media (max-width: 1024px) {
flex-direction: column;
padding: 2rem;
text-align: center;
}
.logo {
height: 6em;
padding: 1.5em;
will-change: filter;
transition: filter 300ms;
}
.logo:hover {
filter: drop-shadow(0 0 2em #646cffaa);
}
.logo.react:hover {
filter: drop-shadow(0 0 2em #61dafbaa);
}
@keyframes logo-spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
#docs {
border-right: 1px solid var(--border);
@media (max-width: 1024px) {
border-right: none;
border-bottom: 1px solid var(--border);
@media (prefers-reduced-motion: no-preference) {
a:nth-of-type(2) .logo {
animation: logo-spin infinite 20s linear;
}
}
#next-steps ul {
list-style: none;
padding: 0;
display: flex;
gap: 8px;
margin: 32px 0 0;
.logo {
height: 18px;
}
a {
color: var(--text-h);
font-size: 16px;
border-radius: 6px;
background: var(--social-bg);
display: flex;
padding: 6px 12px;
align-items: center;
gap: 8px;
text-decoration: none;
transition: box-shadow 0.3s;
&:hover {
box-shadow: var(--shadow);
}
.button-icon {
height: 18px;
width: 18px;
}
}
@media (max-width: 1024px) {
margin-top: 20px;
flex-wrap: wrap;
justify-content: center;
li {
flex: 1 1 calc(50% - 8px);
}
a {
width: 100%;
justify-content: center;
box-sizing: border-box;
}
}
.card {
padding: 2em;
}
#spacer {
height: 88px;
border-top: 1px solid var(--border);
@media (max-width: 1024px) {
height: 48px;
}
}
.ticks {
position: relative;
width: 100%;
&::before,
&::after {
content: '';
position: absolute;
top: -4.5px;
border: 5px solid transparent;
}
&::before {
left: 0;
border-left-color: var(--border);
}
&::after {
right: 0;
border-right-color: var(--border);
}
.read-the-docs {
color: #888;
}

View File

@@ -1,40 +1,30 @@
import React from 'react';
import { Admin, Resource, Layout } from 'react-admin';
import { authProvider } from './authProvider';
import { dataProvider } from './dataProvider';
import { MyLayout } from './layout/MyLayout';
import Dashboard from './resources/dashboard/Dashboard';
import { UserList, UserEdit } from './resources/users';
import { ReportList, ReportEdit } from './resources/reports';
import { TicketList, TicketEdit } from './resources/tickets';
import { AdminList, AdminEdit } from './resources/admins';
import { BannedWordList, BannedWordEdit } from './resources/banned-words';
import { SubscriptionList, SubscriptionEdit } from './resources/subscriptions';
import { ReviewList, ReviewEdit } from './resources/reviews';
import { AuditList } from './resources/audit';
import { BrowserRouter as Router } from 'react-router-dom'
import { ThemeProvider } from '@/components/theme-provider'
import { SidebarConfigProvider } from '@/contexts/sidebar-context'
import { AppRouter } from '@/components/router/app-router'
import { useEffect } from 'react'
import { initGTM } from '@/utils/analytics'
const App = () => (
<Admin
dashboard={Dashboard}
authProvider={authProvider}
dataProvider={dataProvider}
requireAuth
>
{(permissions) => [
// Доступно всем администраторам
<Resource name="users" list={UserList} edit={UserEdit} />,
<Resource name="reports" list={ReportList} edit={ReportEdit} />,
<Resource name="tickets" list={TicketList} edit={TicketEdit} />,
<Resource name="banned-words" list={BannedWordList} edit={BannedWordEdit} />,
<Resource name="subscriptions" list={SubscriptionList} edit={SubscriptionEdit} />,
<Resource name="reviews" list={ReviewList} edit={ReviewEdit} />,
<Resource name="audit" list={AuditList} />,
// Только superadmin
permissions === 'superadmin' ? (
<Resource name="admins" list={AdminList} edit={AdminEdit} />
) : null,
]}
</Admin>
);
// Get basename from environment (for deployment) or use empty string for development
const basename = import.meta.env.VITE_BASENAME || ''
export default App;
function App() {
// Initialize GTM on app load
useEffect(() => {
initGTM();
}, []);
return (
<div className="font-sans antialiased" style={{ fontFamily: 'var(--font-inter)' }}>
<ThemeProvider defaultTheme="system" storageKey="vite-ui-theme">
<SidebarConfigProvider>
<Router basename={basename}>
<AppRouter />
</Router>
</SidebarConfigProvider>
</ThemeProvider>
</div>
)
}
export default App

View File

@@ -0,0 +1,37 @@
"use client"
import { cn } from "@/lib/utils"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"
export function ForgotPasswordForm2({
className,
...props
}: React.ComponentProps<"form">) {
return (
<form className={cn("flex flex-col gap-6", className)} {...props}>
<div className="flex flex-col items-center gap-2 text-center">
<h1 className="text-2xl font-bold">Forgot your password?</h1>
<p className="text-muted-foreground text-sm text-balance">
Enter your email address and we'll send you a link to reset your password
</p>
</div>
<div className="grid gap-6">
<div className="grid gap-3">
<Label htmlFor="email">Email</Label>
<Input id="email" type="email" placeholder="m@example.com" required />
</div>
<Button type="submit" className="w-full cursor-pointer">
Send Reset Link
</Button>
</div>
<div className="text-center text-sm">
Remember your password?{" "}
<a href="/auth/sign-in-2" className="underline underline-offset-4">
Back to sign in
</a>
</div>
</form>
)
}

View File

@@ -0,0 +1,31 @@
import { ForgotPasswordForm2 } from "./components/forgot-password-form-2"
import { Logo } from "@/components/logo"
export default function ForgotPassword2Page() {
return (
<div className="grid min-h-svh lg:grid-cols-2">
<div className="flex flex-col gap-4 p-6 md:p-10">
<div className="flex justify-center gap-2 md:justify-start">
<a href="/" className="flex items-center gap-2 font-medium">
<div className="bg-primary text-primary-foreground flex size-8 items-center justify-center rounded-md">
<Logo size={24} />
</div>
ShadcnStore
</a>
</div>
<div className="flex flex-1 items-center justify-center">
<div className="w-full max-w-md">
<ForgotPasswordForm2 />
</div>
</div>
</div>
<div className="bg-muted relative hidden lg:block">
<img
src="https://ui.shadcn.com/placeholder.svg"
alt="Image"
className="absolute inset-0 h-full w-full object-cover dark:brightness-[0.95] dark:invert"
/>
</div>
</div>
)
}

View File

@@ -0,0 +1,69 @@
"use client"
import { cn } from "@/lib/utils"
import { Button } from "@/components/ui/button"
import { Card, CardContent } from "@/components/ui/card"
import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"
import { Logo } from "@/components/logo"
export function ForgotPasswordForm3({
className,
...props
}: React.ComponentProps<"div">) {
return (
<div className={cn("flex flex-col gap-6", className)} {...props}>
<Card className="overflow-hidden p-0">
<CardContent className="grid p-0 md:grid-cols-2">
<form className="p-6 md:p-8">
<div className="flex flex-col gap-6">
<div className="flex justify-center mb-2">
<a href="/" className="flex items-center gap-2 font-medium">
<div className="bg-primary text-primary-foreground flex size-8 items-center justify-center rounded-md">
<Logo size={24} />
</div>
<span className="text-xl">ShadcnStore</span>
</a>
</div>
<div className="flex flex-col items-center text-center">
<h1 className="text-2xl font-bold">Forgot your password?</h1>
<p className="text-muted-foreground text-balance">
Enter your email to reset your ShadcnStore account password
</p>
</div>
<div className="grid gap-3">
<Label htmlFor="email">Email</Label>
<Input
id="email"
type="email"
placeholder="m@example.com"
required
/>
</div>
<Button type="submit" className="w-full cursor-pointer">
Send Reset Link
</Button>
<div className="text-center text-sm">
Remember your password?{" "}
<a href="/auth/sign-in-3" className="underline underline-offset-4">
Back to sign in
</a>
</div>
</div>
</form>
<div className="bg-muted relative hidden md:block">
<img
src="https://ui.shadcn.com/placeholder.svg"
alt="Image"
className="absolute inset-0 h-full w-full object-cover dark:brightness-[0.95] dark:invert"
/>
</div>
</CardContent>
</Card>
<div className="text-muted-foreground *:[a]:hover:text-primary text-center text-xs text-balance *:[a]:underline *:[a]:underline-offset-4">
By clicking continue, you agree to our <a href="#">Terms of Service</a>{" "}
and <a href="#">Privacy Policy</a>.
</div>
</div>
)
}

View File

@@ -0,0 +1,9 @@
import { ForgotPasswordForm3 } from "./components/forgot-password-form-3"
export default function ForgotPassword3Page() {
return (
<div className="min-h-screen flex items-center justify-center bg-background p-4">
<ForgotPasswordForm3 className="w-full max-w-sm md:max-w-4xl" />
</div>
)
}

View File

@@ -0,0 +1,57 @@
"use client"
import { cn } from "@/lib/utils"
import { Button } from "@/components/ui/button"
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card"
import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"
export function ForgotPasswordForm1({
className,
...props
}: React.ComponentProps<"div">) {
return (
<div className={cn("flex flex-col gap-6", className)} {...props}>
<Card>
<CardHeader className="text-center">
<CardTitle className="text-xl">Forgot your password?</CardTitle>
<CardDescription>
Enter your email address and we'll send you a link to reset your password
</CardDescription>
</CardHeader>
<CardContent>
<form>
<div className="grid gap-6">
<div className="grid gap-6">
<div className="grid gap-3">
<Label htmlFor="email">Email</Label>
<Input
id="email"
type="email"
placeholder="m@example.com"
required
/>
</div>
<Button type="submit" className="w-full cursor-pointer">
Send Reset Link
</Button>
</div>
<div className="text-center text-sm">
Remember your password?{" "}
<a href="/auth/sign-in" className="underline underline-offset-4">
Back to sign in
</a>
</div>
</div>
</form>
</CardContent>
</Card>
</div>
)
}

View File

@@ -0,0 +1,18 @@
import { ForgotPasswordForm1 } from "./components/forgot-password-form-1"
import { Logo } from "@/components/logo"
export default function ForgotPasswordPage() {
return (
<div className="bg-muted flex min-h-svh flex-col items-center justify-center gap-6 p-6 md:p-10">
<div className="flex w-full max-w-sm flex-col gap-6">
<a href="/" className="flex items-center gap-2 self-center font-medium">
<div className="bg-primary text-primary-foreground flex size-9 items-center justify-center rounded-md">
<Logo size={24} />
</div>
ShadcnStore
</a>
<ForgotPasswordForm1 />
</div>
</div>
)
}

View File

@@ -0,0 +1,63 @@
"use client"
import { cn } from "@/lib/utils"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"
export function LoginForm2({
className,
...props
}: React.ComponentProps<"form">) {
return (
<form className={cn("flex flex-col gap-6", className)} {...props} action="/dashboard">
<div className="flex flex-col items-center gap-2 text-center">
<h1 className="text-2xl font-bold">Login to your account</h1>
<p className="text-muted-foreground text-sm text-balance">
Enter your email below to login to your account
</p>
</div>
<div className="grid gap-6">
<div className="grid gap-3">
<Label htmlFor="email">Email</Label>
<Input id="email" type="email" placeholder="test@example.com" defaultValue="test@example.com" required />
</div>
<div className="grid gap-3">
<div className="flex items-center">
<Label htmlFor="password">Password</Label>
<a
href="/auth/forgot-password-2"
className="ml-auto text-sm underline-offset-4 hover:underline"
>
Forgot your password?
</a>
</div>
<Input id="password" type="password" defaultValue="password" required />
</div>
<Button type="submit" className="w-full cursor-pointer">
Login
</Button>
<div className="after:border-border relative text-center text-sm after:absolute after:inset-0 after:top-1/2 after:z-0 after:flex after:items-center after:border-t">
<span className="bg-background text-muted-foreground relative z-10 px-2">
Or continue with
</span>
</div>
<Button variant="outline" className="w-full cursor-pointer">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<path
d="M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12"
fill="currentColor"
/>
</svg>
Login with GitHub
</Button>
</div>
<div className="text-center text-sm">
Don&apos;t have an account?{" "}
<a href="/auth/sign-up-2" className="underline underline-offset-4">
Sign up
</a>
</div>
</form>
)
}

View File

@@ -0,0 +1,31 @@
import { LoginForm2 } from "./components/login-form-2"
import { Logo } from "@/components/logo"
export default function LoginPage() {
return (
<div className="grid min-h-svh lg:grid-cols-2">
<div className="flex flex-col gap-4 p-6 md:p-10">
<div className="flex justify-center gap-2 md:justify-start">
<a href="/" className="flex items-center gap-2 font-medium">
<div className="bg-primary text-primary-foreground flex size-8 items-center justify-center rounded-md">
<Logo size={24} />
</div>
ShadcnStore
</a>
</div>
<div className="flex flex-1 items-center justify-center">
<div className="w-full max-w-md">
<LoginForm2 />
</div>
</div>
</div>
<div className="bg-muted relative hidden lg:block">
<img
src="https://ui.shadcn.com/placeholder.svg"
alt="Image"
className="absolute inset-0 h-full w-full object-cover dark:brightness-[0.95] dark:invert"
/>
</div>
</div>
)
}

View File

@@ -0,0 +1,116 @@
"use client"
import { cn } from "@/lib/utils"
import { Button } from "@/components/ui/button"
import { Card, CardContent } from "@/components/ui/card"
import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"
import { Logo } from "@/components/logo"
export function LoginForm3({
className,
...props
}: React.ComponentProps<"div">) {
return (
<div className={cn("flex flex-col gap-6", className)} {...props}>
<Card className="overflow-hidden p-0">
<CardContent className="grid p-0 md:grid-cols-2">
<form className="p-6 md:p-8" action="/dashboard">
<div className="flex flex-col gap-6">
<div className="flex justify-center mb-2">
<a href="/" className="flex items-center gap-2 font-medium">
<div className="bg-primary text-primary-foreground flex size-8 items-center justify-center rounded-md">
<Logo size={24} />
</div>
<span className="text-xl">ShadcnStore</span>
</a>
</div>
<div className="flex flex-col items-center text-center">
<h1 className="text-2xl font-bold">Welcome back</h1>
<p className="text-muted-foreground text-balance">
Login to your ShadcnStore account
</p>
</div>
<div className="grid gap-3">
<Label htmlFor="email">Email</Label>
<Input
id="email"
type="email"
placeholder="test@example.com"
defaultValue="test@example.com"
required
/>
</div>
<div className="grid gap-3">
<div className="flex items-center">
<Label htmlFor="password">Password</Label>
<a
href="/auth/forgot-password-3"
className="ml-auto text-sm underline-offset-2 hover:underline"
>
Forgot your password?
</a>
</div>
<Input id="password" type="password" defaultValue="password" required />
</div>
<Button type="submit" className="w-full cursor-pointer">
Login
</Button>
<div className="after:border-border relative text-center text-sm after:absolute after:inset-0 after:top-1/2 after:z-0 after:flex after:items-center after:border-t">
<span className="bg-card text-muted-foreground relative z-10 px-2">
Or continue with
</span>
</div>
<div className="grid grid-cols-3 gap-4">
<Button variant="outline" type="button" className="w-full cursor-pointer">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<path
d="M12.152 6.896c-.948 0-2.415-1.078-3.96-1.04-2.04.027-3.91 1.183-4.961 3.014-2.117 3.675-.546 9.103 1.519 12.09 1.013 1.454 2.208 3.09 3.792 3.039 1.52-.065 2.09-.987 3.935-.987 1.831 0 2.35.987 3.96.948 1.637-.026 2.676-1.48 3.676-2.948 1.156-1.688 1.636-3.325 1.662-3.415-.039-.013-3.182-1.221-3.22-4.857-.026-3.04 2.48-4.494 2.597-4.559-1.429-2.09-3.623-2.324-4.39-2.376-2-.156-3.675 1.09-4.61 1.09zM15.53 3.83c.843-1.012 1.4-2.427 1.245-3.83-1.207.052-2.662.805-3.532 1.818-.78.896-1.454 2.338-1.273 3.714 1.338.104 2.715-.688 3.559-1.701"
fill="currentColor"
/>
</svg>
<span className="sr-only">Login with Apple</span>
</Button>
<Button variant="outline" type="button" className="w-full cursor-pointer">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<path
d="M12.48 10.92v3.28h7.84c-.24 1.84-.853 3.187-1.787 4.133-1.147 1.147-2.933 2.4-6.053 2.4-4.827 0-8.6-3.893-8.6-8.72s3.773-8.72 8.6-8.72c2.6 0 4.507 1.027 5.907 2.347l2.307-2.307C18.747 1.44 16.133 0 12.48 0 5.867 0 .307 5.387.307 12s5.56 12 12.173 12c3.573 0 6.267-1.173 8.373-3.36 2.16-2.16 2.84-5.213 2.84-7.667 0-.76-.053-1.467-.173-2.053H12.48z"
fill="currentColor"
/>
</svg>
<span className="sr-only">Login with Google</span>
</Button>
<Button variant="outline" type="button" className="w-full cursor-pointer">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<path
d="M6.915 4.03c-1.968 0-3.683 1.28-4.871 3.113C.704 9.208 0 11.883 0 14.449c0 .706.07 1.369.21 1.973a6.624 6.624 0 0 0 .265.86 5.297 5.297 0 0 0 .371.761c.696 1.159 1.818 1.927 3.593 1.927 1.497 0 2.633-.671 3.965-2.444.76-1.012 1.144-1.626 2.663-4.32l.756-1.339.186-.325c.061.1.121.196.183.3l2.152 3.595c.724 1.21 1.665 2.556 2.47 3.314 1.046.987 1.992 1.22 3.06 1.22 1.075 0 1.876-.355 2.455-.843a3.743 3.743 0 0 0 .81-.973c.542-.939.861-2.127.861-3.745 0-2.72-.681-5.357-2.084-7.45-1.282-1.912-2.957-2.93-4.716-2.93-1.047 0-2.088.467-3.053 1.308-.652.57-1.257 1.29-1.82 2.05-.69-.875-1.335-1.547-1.958-2.056-1.182-.966-2.315-1.303-3.454-1.303zm10.16 2.053c1.147 0 2.188.758 2.992 1.999 1.132 1.748 1.647 4.195 1.647 6.4 0 1.548-.368 2.9-1.839 2.9-.58 0-1.027-.23-1.664-1.004-.496-.601-1.343-1.878-2.832-4.358l-.617-1.028a44.908 44.908 0 0 0-1.255-1.98c.07-.109.141-.224.211-.327 1.12-1.667 2.118-2.602 3.358-2.602zm-10.201.553c1.265 0 2.058.791 2.675 1.446.307.327.737.871 1.234 1.579l-1.02 1.566c-.757 1.163-1.882 3.017-2.837 4.338-1.191 1.649-1.81 1.817-2.486 1.817-.524 0-1.038-.237-1.383-.794-.263-.426-.464-1.13-.464-2.046 0-2.221.63-4.535 1.66-6.088.454-.687.964-1.226 1.533-1.533a2.264 2.264 0 0 1 1.088-.285z"
fill="currentColor"
/>
</svg>
<span className="sr-only">Login with Meta</span>
</Button>
</div>
<div className="text-center text-sm">
Don&apos;t have an account?{" "}
<a href="/auth/sign-up-3" className="underline underline-offset-4">
Sign up
</a>
</div>
</div>
</form>
<div className="bg-muted relative hidden md:block">
<img
src="https://ui.shadcn.com/placeholder.svg"
alt="Image"
className="absolute inset-0 h-full w-full object-cover dark:brightness-[0.95] dark:invert"
/>
</div>
</CardContent>
</Card>
<div className="text-muted-foreground *:[a]:hover:text-primary text-center text-xs text-balance *:[a]:underline *:[a]:underline-offset-4">
By clicking continue, you agree to our <a href="#">Terms of Service</a>{" "}
and <a href="#">Privacy Policy</a>.
</div>
</div>
)
}

View File

@@ -0,0 +1,11 @@
import { LoginForm3 } from "./components/login-form-3"
export default function LoginPage() {
return (
<div className="bg-muted flex min-h-svh flex-col items-center justify-center p-6 md:p-10">
<div className="w-full max-w-sm md:max-w-4xl">
<LoginForm3 />
</div>
</div>
)
}

View File

@@ -0,0 +1,127 @@
"use client"
import { zodResolver } from "@hookform/resolvers/zod"
import { useForm } from "react-hook-form"
import { z } from "zod"
import { cn } from "@/lib/utils"
import { Button } from "@/components/ui/button"
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card"
import { Input } from "@/components/ui/input"
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/components/ui/form"
const loginFormSchema = z.object({
email: z.string().email("Invalid email address"),
password: z.string().min(6, "Password must be at least 6 characters"),
})
type LoginFormValues = z.infer<typeof loginFormSchema>
export function LoginForm1({
className,
...props
}: React.ComponentProps<"div">) {
const form = useForm<LoginFormValues>({
resolver: zodResolver(loginFormSchema),
defaultValues: {
email: "test@example.com",
password: "password",
},
})
return (
<div className={cn("flex flex-col gap-6", className)} {...props}>
<Card>
<CardHeader className="text-center">
<CardTitle className="text-xl">Welcome back</CardTitle>
<CardDescription>
Enter your email below to login to your account
</CardDescription>
</CardHeader>
<CardContent>
<Form {...form}>
<form action="/">
<div className="grid gap-6">
<div className="grid gap-4">
<FormField
control={form.control}
name="email"
render={({ field }) => (
<FormItem>
<FormLabel>Email</FormLabel>
<FormControl>
<Input
type="email"
placeholder="test@example.com"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="password"
render={({ field }) => (
<FormItem>
<div className="flex items-center">
<FormLabel>Password</FormLabel>
<a
href="/auth/forgot-password"
className="ml-auto text-sm underline-offset-4 hover:underline"
>
Forgot your password?
</a>
</div>
<FormControl>
<Input type="password" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<Button type="submit" className="w-full cursor-pointer">
Login
</Button>
<Button variant="outline" className="w-full cursor-pointer" type="button">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<path
d="M12.48 10.92v3.28h7.84c-.24 1.84-.853 3.187-1.787 4.133-1.147 1.147-2.933 2.4-6.053 2.4-4.827 0-8.6-3.893-8.6-8.72s3.773-8.72 8.6-8.72c2.6 0 4.507 1.027 5.907 2.347l2.307-2.307C18.747 1.44 16.133 0 12.48 0 5.867 0 .307 5.387.307 12s5.56 12 12.173 12c3.573 0 6.267-1.173 8.373-3.36 2.16-2.16 2.84-5.213 2.84-7.667 0-.76-.053-1.467-.173-2.053H12.48z"
fill="currentColor"
/>
</svg>
Login with Google
</Button>
</div>
<div className="text-center text-sm">
Don&apos;t have an account?{" "}
<a href="/auth/sign-up" className="underline underline-offset-4">
Sign up
</a>
</div>
</div>
</form>
</Form>
</CardContent>
</Card>
<div className="text-muted-foreground *:[a]:hover:text-primary text-center text-xs text-balance *:[a]:underline *:[a]:underline-offset-4">
By clicking continue, you agree to our <a href="#">Terms of Service</a>{" "}
and <a href="#">Privacy Policy</a>.
</div>
</div>
)
}

View File

@@ -0,0 +1,18 @@
import { LoginForm1 } from "./components/login-form-1"
import { Logo } from "@/components/logo"
export default function Page() {
return (
<div className="bg-muted flex min-h-svh flex-col items-center justify-center gap-6 p-6 md:p-10">
<div className="flex w-full max-w-sm flex-col gap-6">
<a href="/" className="flex items-center gap-2 self-center font-medium">
<div className="bg-primary text-primary-foreground flex size-9 items-center justify-center rounded-md">
<Logo size={24} />
</div>
ShadcnStore
</a>
<LoginForm1 />
</div>
</div>
)
}

View File

@@ -0,0 +1,83 @@
"use client"
import { cn } from "@/lib/utils"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"
import { Checkbox } from "@/components/ui/checkbox"
export function SignupForm2({
className,
...props
}: React.ComponentProps<"form">) {
return (
<form className={cn("flex flex-col gap-6", className)} {...props}>
<div className="flex flex-col items-center gap-2 text-center">
<h1 className="text-2xl font-bold">Create your account</h1>
<p className="text-muted-foreground text-sm text-balance">
Enter your information to create a new account
</p>
</div>
<div className="grid gap-6">
<div className="grid grid-cols-2 gap-3">
<div className="grid gap-3">
<Label htmlFor="firstName">First Name</Label>
<Input id="firstName" placeholder="John" required />
</div>
<div className="grid gap-3">
<Label htmlFor="lastName">Last Name</Label>
<Input id="lastName" placeholder="Doe" required />
</div>
</div>
<div className="grid gap-3">
<Label htmlFor="email">Email</Label>
<Input id="email" type="email" placeholder="m@example.com" required />
</div>
<div className="grid gap-3">
<Label htmlFor="password">Password</Label>
<Input id="password" type="password" required />
</div>
<div className="grid gap-3">
<Label htmlFor="confirmPassword">Confirm Password</Label>
<Input id="confirmPassword" type="password" required />
</div>
<div className="flex items-center space-x-2">
<Checkbox id="terms" required />
<Label htmlFor="terms" className="text-sm">
I agree to the{" "}
<a href="#" className="underline underline-offset-4 hover:text-primary">
Terms of Service
</a>{" "}
and{" "}
<a href="#" className="underline underline-offset-4 hover:text-primary">
Privacy Policy
</a>
</Label>
</div>
<Button type="submit" className="w-full cursor-pointer">
Create Account
</Button>
<div className="after:border-border relative text-center text-sm after:absolute after:inset-0 after:top-1/2 after:z-0 after:flex after:items-center after:border-t">
<span className="bg-background text-muted-foreground relative z-10 px-2">
Or continue with
</span>
</div>
<Button variant="outline" className="w-full cursor-pointer">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<path
d="M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12"
fill="currentColor"
/>
</svg>
Sign up with GitHub
</Button>
</div>
<div className="text-center text-sm">
Already have an account?{" "}
<a href="/auth/sign-in-2" className="underline underline-offset-4">
Sign in
</a>
</div>
</form>
)
}

View File

@@ -0,0 +1,31 @@
import { SignupForm2 } from "./components/signup-form-2"
import { Logo } from "@/components/logo"
export default function SignUp2Page() {
return (
<div className="grid min-h-svh lg:grid-cols-2">
<div className="flex flex-col gap-4 p-6 md:p-10">
<div className="flex justify-center gap-2 md:justify-start">
<a href="/" className="flex items-center gap-2 font-medium">
<div className="bg-primary text-primary-foreground flex size-8 items-center justify-center rounded-md">
<Logo size={24} />
</div>
ShadcnStore
</a>
</div>
<div className="flex flex-1 items-center justify-center">
<div className="w-full max-w-md">
<SignupForm2 />
</div>
</div>
</div>
<div className="bg-muted relative hidden lg:block">
<img
src="https://ui.shadcn.com/placeholder.svg"
alt="Image"
className="absolute inset-0 h-full w-full object-cover dark:brightness-[0.95] dark:invert"
/>
</div>
</div>
)
}

View File

@@ -0,0 +1,143 @@
"use client"
import { cn } from "@/lib/utils"
import { Button } from "@/components/ui/button"
import { Card, CardContent } from "@/components/ui/card"
import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"
import { Checkbox } from "@/components/ui/checkbox"
import { Logo } from "@/components/logo"
export function SignupForm3({
className,
...props
}: React.ComponentProps<"div">) {
return (
<div className={cn("flex flex-col gap-6", className)} {...props}>
<Card className="overflow-hidden p-0">
<CardContent className="grid p-0 md:grid-cols-2">
<form className="p-6 md:p-8">
<div className="flex flex-col gap-6">
<div className="flex justify-center mb-2">
<a href="/" className="flex items-center gap-2 font-medium">
<div className="bg-primary text-primary-foreground flex size-8 items-center justify-center rounded-md">
<Logo size={24} />
</div>
<span className="text-xl">ShadcnStore</span>
</a>
</div>
<div className="flex flex-col items-center text-center">
<h1 className="text-2xl font-bold">Create your account</h1>
<p className="text-muted-foreground text-balance">
Enter your information to create a new account
</p>
</div>
<div className="grid grid-cols-2 gap-3">
<div className="grid gap-3">
<Label htmlFor="firstName">First Name</Label>
<Input
id="firstName"
placeholder="John"
required
/>
</div>
<div className="grid gap-3">
<Label htmlFor="lastName">Last Name</Label>
<Input
id="lastName"
placeholder="Doe"
required
/>
</div>
</div>
<div className="grid gap-3">
<Label htmlFor="email">Email</Label>
<Input
id="email"
type="email"
placeholder="m@example.com"
required
/>
</div>
<div className="grid gap-3">
<Label htmlFor="password">Password</Label>
<Input id="password" type="password" required />
</div>
<div className="grid gap-3">
<Label htmlFor="confirmPassword">Confirm Password</Label>
<Input id="confirmPassword" type="password" required />
</div>
<div className="flex items-center space-x-2">
<Checkbox id="terms" required />
<Label htmlFor="terms" className="text-sm">
I agree to the{" "}
<a href="#" className="underline underline-offset-4 hover:text-primary">
Terms of Service
</a>{" "}
and{" "}
<a href="#" className="underline underline-offset-4 hover:text-primary">
Privacy Policy
</a>
</Label>
</div>
<Button type="submit" className="w-full cursor-pointer">
Create Account
</Button>
<div className="after:border-border relative text-center text-sm after:absolute after:inset-0 after:top-1/2 after:z-0 after:flex after:items-center after:border-t">
<span className="bg-card text-muted-foreground relative z-10 px-2">
Or continue with
</span>
</div>
<div className="grid grid-cols-3 gap-4">
<Button variant="outline" type="button" className="w-full cursor-pointer">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<path
d="M12.152 6.896c-.948 0-2.415-1.078-3.96-1.04-2.04.027-3.91 1.183-4.961 3.014-2.117 3.675-.546 9.103 1.519 12.09 1.013 1.454 2.208 3.09 3.792 3.039 1.52-.065 2.09-.987 3.935-.987 1.831 0 2.35.987 3.96.948 1.637-.026 2.676-1.48 3.676-2.948 1.156-1.688 1.636-3.325 1.662-3.415-.039-.013-3.182-1.221-3.22-4.857-.026-3.04 2.48-4.494 2.597-4.559-1.429-2.09-3.623-2.324-4.39-2.376-2-.156-3.675 1.09-4.61 1.09zM15.53 3.83c.843-1.012 1.4-2.427 1.245-3.83-1.207.052-2.662.805-3.532 1.818-.78.896-1.454 2.338-1.273 3.714 1.338.104 2.715-.688 3.559-1.701"
fill="currentColor"
/>
</svg>
<span className="sr-only">Sign up with Apple</span>
</Button>
<Button variant="outline" type="button" className="w-full cursor-pointer">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<path
d="M12.48 10.92v3.28h7.84c-.24 1.84-.853 3.187-1.787 4.133-1.147 1.147-2.933 2.4-6.053 2.4-4.827 0-8.6-3.893-8.6-8.72s3.773-8.72 8.6-8.72c2.6 0 4.507 1.027 5.907 2.347l2.307-2.307C18.747 1.44 16.133 0 12.48 0 5.867 0 .307 5.387.307 12s5.56 12 12.173 12c3.573 0 6.267-1.173 8.373-3.36 2.16-2.16 2.84-5.213 2.84-7.667 0-.76-.053-1.467-.173-2.053H12.48z"
fill="currentColor"
/>
</svg>
<span className="sr-only">Sign up with Google</span>
</Button>
<Button variant="outline" type="button" className="w-full cursor-pointer">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<path
d="M6.915 4.03c-1.968 0-3.683 1.28-4.871 3.113C.704 9.208 0 11.883 0 14.449c0 .706.07 1.369.21 1.973a6.624 6.624 0 0 0 .265.86 5.297 5.297 0 0 0 .371.761c.696 1.159 1.818 1.927 3.593 1.927 1.497 0 2.633-.671 3.965-2.444.76-1.012 1.144-1.626 2.663-4.32l.756-1.339.186-.325c.061.1.121.196.183.3l2.152 3.595c.724 1.21 1.665 2.556 2.47 3.314 1.046.987 1.992 1.22 3.06 1.22 1.075 0 1.876-.355 2.455-.843a3.743 3.743 0 0 0 .81-.973c.542-.939.861-2.127.861-3.745 0-2.72-.681-5.357-2.084-7.45-1.282-1.912-2.957-2.93-4.716-2.93-1.047 0-2.088.467-3.053 1.308-.652.57-1.257 1.29-1.82 2.05-.69-.875-1.335-1.547-1.958-2.056-1.182-.966-2.315-1.303-3.454-1.303zm10.16 2.053c1.147 0 2.188.758 2.992 1.999 1.132 1.748 1.647 4.195 1.647 6.4 0 1.548-.368 2.9-1.839 2.9-.58 0-1.027-.23-1.664-1.004-.496-.601-1.343-1.878-2.832-4.358l-.617-1.028a44.908 44.908 0 0 0-1.255-1.98c.07-.109.141-.224.211-.327 1.12-1.667 2.118-2.602 3.358-2.602zm-10.201.553c1.265 0 2.058.791 2.675 1.446.307.327.737.871 1.234 1.579l-1.02 1.566c-.757 1.163-1.882 3.017-2.837 4.338-1.191 1.649-1.81 1.817-2.486 1.817-.524 0-1.038-.237-1.383-.794-.263-.426-.464-1.13-.464-2.046 0-2.221.63-4.535 1.66-6.088.454-.687.964-1.226 1.533-1.533a2.264 2.264 0 0 1 1.088-.285z"
fill="currentColor"
/>
</svg>
<span className="sr-only">Sign up with Meta</span>
</Button>
</div>
<div className="text-center text-sm">
Already have an account?{" "}
<a href="/auth/sign-in-3" className="underline underline-offset-4">
Sign in
</a>
</div>
</div>
</form>
<div className="bg-muted relative hidden md:block">
<img
src="https://ui.shadcn.com/placeholder.svg"
alt="Image"
className="absolute inset-0 h-full w-full object-cover dark:brightness-[0.95] dark:invert"
/>
</div>
</CardContent>
</Card>
<div className="text-muted-foreground *:[a]:hover:text-primary text-center text-xs text-balance *:[a]:underline *:[a]:underline-offset-4">
By clicking continue, you agree to our <a href="#">Terms of Service</a>{" "}
and <a href="#">Privacy Policy</a>.
</div>
</div>
)
}

View File

@@ -0,0 +1,9 @@
import { SignupForm3 } from "./components/signup-form-3"
export default function SignUp3Page() {
return (
<div className="min-h-screen flex items-center justify-center bg-background p-4">
<SignupForm3 className="w-full max-w-5xl" />
</div>
)
}

View File

@@ -0,0 +1,195 @@
"use client"
import { zodResolver } from "@hookform/resolvers/zod"
import { useForm } from "react-hook-form"
import { z } from "zod"
import { cn } from "@/lib/utils"
import { Button } from "@/components/ui/button"
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card"
import { Input } from "@/components/ui/input"
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/components/ui/form"
import { Checkbox } from "@/components/ui/checkbox"
const signupFormSchema = z.object({
firstName: z.string().min(1, "First name is required"),
lastName: z.string().min(1, "Last name is required"),
email: z.string().email("Invalid email address"),
password: z.string().min(6, "Password must be at least 6 characters"),
confirmPassword: z.string().min(6, "Please confirm your password"),
terms: z.boolean().refine(val => val === true, "You must agree to the terms"),
}).refine((data) => data.password === data.confirmPassword, {
message: "Passwords don't match",
path: ["confirmPassword"],
})
type SignupFormValues = z.infer<typeof signupFormSchema>
export function SignupForm1({
className,
...props
}: React.ComponentProps<"div">) {
const form = useForm<SignupFormValues>({
resolver: zodResolver(signupFormSchema),
defaultValues: {
firstName: "",
lastName: "",
email: "",
password: "",
confirmPassword: "",
terms: false,
},
})
function onSubmit(data: SignupFormValues) {
console.log("Signup attempt:", data)
// Here you would typically handle the signup
}
return (
<div className={cn("flex flex-col gap-6", className)} {...props}>
<Card>
<CardHeader className="text-center">
<CardTitle className="text-xl">Create Account</CardTitle>
<CardDescription>
Enter your information to create a new account
</CardDescription>
</CardHeader>
<CardContent>
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)}>
<div className="grid gap-6">
<div className="grid gap-4">
<div className="grid grid-cols-2 gap-3">
<FormField
control={form.control}
name="firstName"
render={({ field }) => (
<FormItem>
<FormLabel>First Name</FormLabel>
<FormControl>
<Input placeholder="John" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="lastName"
render={({ field }) => (
<FormItem>
<FormLabel>Last Name</FormLabel>
<FormControl>
<Input placeholder="Doe" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</div>
<FormField
control={form.control}
name="email"
render={({ field }) => (
<FormItem>
<FormLabel>Email</FormLabel>
<FormControl>
<Input
type="email"
placeholder="m@example.com"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="password"
render={({ field }) => (
<FormItem>
<FormLabel>Password</FormLabel>
<FormControl>
<Input type="password" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="confirmPassword"
render={({ field }) => (
<FormItem>
<FormLabel>Confirm Password</FormLabel>
<FormControl>
<Input type="password" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="terms"
render={({ field }) => (
<FormItem className="flex items-start space-x-2">
<FormControl>
<Checkbox
checked={field.value}
onCheckedChange={field.onChange}
className="mt-0.5"
/>
</FormControl>
<FormLabel className="text-sm">
I agree to the terms of service and privacy policy
</FormLabel>
</FormItem>
)}
/>
<Button type="submit" className="w-full cursor-pointer">
Create Account
</Button>
<Button variant="outline" className="w-full cursor-pointer" type="button">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<path
d="M12.48 10.92v3.28h7.84c-.24 1.84-.853 3.187-1.787 4.133-1.147 1.147-2.933 2.4-6.053 2.4-4.827 0-8.6-3.893-8.6-8.72s3.773-8.72 8.6-8.72c2.6 0 4.507 1.027 5.907 2.347l2.307-2.307C18.747 1.44 16.133 0 12.48 0 5.867 0 .307 5.387.307 12s5.56 12 12.173 12c3.573 0 6.267-1.173 8.373-3.36 2.16-2.16 2.84-5.213 2.84-7.667 0-.76-.053-1.467-.173-2.053H12.48z"
fill="currentColor"
/>
</svg>
Sign up with Google
</Button>
</div>
<div className="text-center text-sm">
Already have an account?{" "}
<a href="/auth/sign-in" className="underline underline-offset-4">
Sign in
</a>
</div>
</div>
</form>
</Form>
</CardContent>
</Card>
<div className="text-muted-foreground *:[a]:hover:text-primary text-center text-xs text-balance *:[a]:underline *:[a]:underline-offset-4">
By clicking continue, you agree to our <a href="#">Terms of Service</a>{" "}
and <a href="#">Privacy Policy</a>.
</div>
</div>
)
}

View File

@@ -0,0 +1,18 @@
import { SignupForm1 } from "./components/signup-form-1"
import { Logo } from "@/components/logo"
export default function SignUpPage() {
return (
<div className="bg-muted flex min-h-svh flex-col items-center justify-center gap-6 p-6 md:p-10">
<div className="flex w-full max-w-sm flex-col gap-6">
<a href="/" className="flex items-center gap-2 self-center font-medium">
<div className="bg-primary text-primary-foreground flex size-9 items-center justify-center rounded-md">
<Logo size={24} />
</div>
ShadcnStore
</a>
<SignupForm1 />
</div>
</div>
)
}

View File

@@ -0,0 +1,347 @@
"use client"
import { useState } from "react"
import {
ChevronLeft,
ChevronRight,
Calendar as CalendarIcon,
Clock,
MapPin,
Users,
MoreHorizontal,
Search,
Grid3X3,
List,
ChevronDown,
Menu
} from "lucide-react"
import { format, addMonths, subMonths, startOfMonth, endOfMonth, eachDayOfInterval, isSameMonth, isToday, isSameDay } from "date-fns"
import { Button } from "@/components/ui/button"
import { Card, CardContent } from "@/components/ui/card"
import { Badge } from "@/components/ui/badge"
import { Avatar, AvatarFallback } from "@/components/ui/avatar"
import { Input } from "@/components/ui/input"
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger
} from "@/components/ui/dropdown-menu"
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle
} from "@/components/ui/dialog"
import { cn } from "@/lib/utils"
import { type CalendarEvent } from "../types"
// Import data
import eventsData from "../data/events.json"
interface CalendarMainProps {
selectedDate?: Date
onDateSelect?: (date: Date) => void
onMenuClick?: () => void
events?: CalendarEvent[]
onEventClick?: (event: CalendarEvent) => void
}
export function CalendarMain({ selectedDate, onDateSelect, onMenuClick, events, onEventClick }: CalendarMainProps) {
// Convert JSON events to CalendarEvent objects with proper Date objects, fallback to imported data
const sampleEvents: CalendarEvent[] = events || eventsData.map(event => ({
...event,
date: new Date(event.date),
type: event.type as "meeting" | "event" | "personal" | "task" | "reminder"
}))
const [currentDate, setCurrentDate] = useState(selectedDate || new Date())
const [viewMode, setViewMode] = useState<"month" | "week" | "day" | "list">("month")
const [showEventDialog, setShowEventDialog] = useState(false)
const [selectedEvent, setSelectedEvent] = useState<CalendarEvent | null>(null)
const monthStart = startOfMonth(currentDate)
const monthEnd = endOfMonth(currentDate)
// Extend to show full weeks (including previous/next month days)
const calendarStart = new Date(monthStart)
calendarStart.setDate(calendarStart.getDate() - monthStart.getDay())
const calendarEnd = new Date(monthEnd)
calendarEnd.setDate(calendarEnd.getDate() + (6 - monthEnd.getDay()))
const calendarDays = eachDayOfInterval({ start: calendarStart, end: calendarEnd })
const getEventsForDay = (date: Date) => {
return sampleEvents.filter(event => isSameDay(event.date, date))
}
const navigateMonth = (direction: "prev" | "next") => {
setCurrentDate(direction === "prev" ? subMonths(currentDate, 1) : addMonths(currentDate, 1))
}
const goToToday = () => {
setCurrentDate(new Date())
}
const handleEventClick = (event: CalendarEvent) => {
if (onEventClick) {
onEventClick(event)
} else {
setSelectedEvent(event)
setShowEventDialog(true)
}
}
const renderCalendarGrid = () => {
const weekDays = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']
return (
<div className="flex-1 bg-background">
{/* Calendar Header */}
<div className="grid grid-cols-7 border-b">
{weekDays.map(day => (
<div key={day} className="p-4 text-center font-medium text-sm text-muted-foreground border-r last:border-r-0">
{day}
</div>
))}
</div>
{/* Calendar Body */}
<div className="grid grid-cols-7 flex-1">
{calendarDays.map(day => {
const dayEvents = getEventsForDay(day)
const isCurrentMonth = isSameMonth(day, currentDate)
const isDayToday = isToday(day)
const isSelected = selectedDate && isSameDay(day, selectedDate)
return (
<div
key={day.toISOString()}
className={cn(
"min-h-[120px] border-r border-b last:border-r-0 p-2 cursor-pointer transition-colors",
isCurrentMonth ? "bg-background hover:bg-accent/50" : "bg-muted/30 text-muted-foreground",
isSelected && "ring-2 ring-primary ring-inset",
isDayToday && "bg-accent/20"
)}
onClick={() => onDateSelect?.(day)}
>
<div className="flex items-center justify-between mb-1">
<span className={cn(
"text-sm font-medium",
isDayToday && "bg-primary text-primary-foreground rounded-md w-6 h-6 flex items-center justify-center text-xs"
)}>
{format(day, 'd')}
</span>
{dayEvents.length > 2 && (
<span className="text-xs text-muted-foreground">
+{dayEvents.length - 2}
</span>
)}
</div>
<div className="space-y-1">
{dayEvents.slice(0, 2).map(event => (
<div
key={event.id}
className={cn(
"text-xs p-1 rounded-sm text-white cursor-pointer truncate",
event.color
)}
onClick={(e) => {
e.stopPropagation()
handleEventClick(event)
}}
>
<div className="flex items-center gap-1">
<Clock className="w-3 h-3" />
<span className="truncate">{event.title}</span>
</div>
</div>
))}
</div>
</div>
)
})}
</div>
</div>
)
}
const renderListView = () => {
const upcomingEvents = sampleEvents
.filter(event => event.date >= new Date())
.sort((a, b) => a.date.getTime() - b.date.getTime())
return (
<div className="flex-1 p-6">
<div className="space-y-4">
{upcomingEvents.map(event => (
<Card key={event.id} className="cursor-pointer hover:shadow-md transition-shadow" onClick={() => handleEventClick(event)}>
<CardContent className="px-4">
<div className="flex items-start justify-between">
<div className="flex items-start gap-3">
<div className={cn("w-3 h-3 rounded-full mt-1.5", event.color)} />
<div className="flex-1">
<h3 className="font-medium">{event.title}</h3>
<div className="flex items-center gap-4 mt-2 text-sm text-muted-foreground">
<div className="flex items-center flex-wrap gap-1">
<CalendarIcon className="w-4 h-4" />
{format(event.date, 'MMM d, yyyy')}
</div>
<div className="flex items-center flex-wrap gap-1">
<Clock className="w-4 h-4" />
{event.time}
</div>
<div className="flex items-center flex-wrap gap-1">
<MapPin className="w-4 h-4" />
{event.location}
</div>
</div>
</div>
</div>
<div className="flex items-center gap-2">
<div className="flex -space-x-2">
{event.attendees.slice(0, 3).map((attendee, index) => (
<Avatar key={index} className="border-2 border-background">
<AvatarFallback className="text-xs">{attendee}</AvatarFallback>
</Avatar>
))}
</div>
<Button variant="ghost" size="sm" className="cursor-pointer">
<MoreHorizontal className="w-4 h-4" />
</Button>
</div>
</div>
</CardContent>
</Card>
))}
</div>
</div>
)
}
return (
<div className="flex flex-col h-full">
{/* Header */}
<div className="flex flex-col flex-wrap gap-4 p-6 border-b md:flex-row md:items-center md:justify-between">
<div className="flex items-center gap-4 flex-wrap">
{/* Mobile Menu Button */}
<Button
variant="outline"
size="sm"
className="xl:hidden cursor-pointer"
onClick={onMenuClick}
>
<Menu className="w-4 h-4" />
</Button>
<div className="flex items-center gap-2">
<Button variant="outline" size="sm" onClick={() => navigateMonth("prev")} className="cursor-pointer">
<ChevronLeft className="w-4 h-4" />
</Button>
<Button variant="outline" size="sm" onClick={() => navigateMonth("next")} className="cursor-pointer">
<ChevronRight className="w-4 h-4" />
</Button>
<Button variant="outline" size="sm" onClick={goToToday} className="cursor-pointer">
Today
</Button>
</div>
<h1 className="text-2xl font-semibold">
{format(currentDate, 'MMMM yyyy')}
</h1>
</div>
<div className="flex flex-col gap-3 md:flex-row md:items-center">
{/* Search */}
<div className="relative">
<Search className="w-4 h-4 absolute left-3 top-1/2 transform -translate-y-1/2 text-muted-foreground" />
<Input placeholder="Search events..." className="pl-10 w-64" />
</div>
{/* View Mode Toggle */}
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="outline" className="cursor-pointer">
{viewMode === "month" && <Grid3X3 className="w-4 h-4 mr-2" />}
{viewMode === "list" && <List className="w-4 h-4 mr-2" />}
{viewMode.charAt(0).toUpperCase() + viewMode.slice(1)}
<ChevronDown className="w-4 h-4 ml-2" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent>
<DropdownMenuItem onClick={() => setViewMode("month")} className="cursor-pointer">
<Grid3X3 className="w-4 h-4 mr-2" />
Month
</DropdownMenuItem>
<DropdownMenuItem onClick={() => setViewMode("list")} className="cursor-pointer">
<List className="w-4 h-4 mr-2" />
List
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
</div>
{/* Calendar Content */}
{viewMode === "month" ? renderCalendarGrid() : renderListView()}
{/* Event Detail Dialog */}
<Dialog open={showEventDialog} onOpenChange={setShowEventDialog}>
<DialogContent className="max-w-md">
<DialogHeader>
<DialogTitle>{selectedEvent?.title || "Event Details"}</DialogTitle>
<DialogDescription>
View and manage this calendar event
</DialogDescription>
</DialogHeader>
{selectedEvent && (
<div className="space-y-4">
<div className="flex items-center gap-2">
<CalendarIcon className="w-4 h-4 text-muted-foreground" />
<span>{format(selectedEvent.date, 'EEEE, MMMM d, yyyy')}</span>
</div>
<div className="flex items-center gap-2">
<Clock className="w-4 h-4 text-muted-foreground" />
<span>{selectedEvent.time} ({selectedEvent.duration})</span>
</div>
<div className="flex items-center gap-2">
<MapPin className="w-4 h-4 text-muted-foreground" />
<span>{selectedEvent.location}</span>
</div>
<div className="flex items-center gap-2">
<Users className="w-4 h-4 text-muted-foreground" />
<div className="flex items-center gap-2">
<span>Attendees:</span>
<div className="flex -space-x-2">
{selectedEvent.attendees.map((attendee: string, index: number) => (
<Avatar key={index} className="w-6 h-6 border-2 border-background">
<AvatarFallback className="text-xs">{attendee}</AvatarFallback>
</Avatar>
))}
</div>
</div>
</div>
<div className="flex items-center gap-2">
<Badge variant="secondary" className={cn("text-white", selectedEvent.color)}>
{selectedEvent.type}
</Badge>
</div>
<div className="flex gap-2 pt-4">
<Button variant="outline" className="flex-1 cursor-pointer" onClick={() => {
setShowEventDialog(false)
}}>Edit</Button>
<Button variant="destructive" className="flex-1 cursor-pointer" onClick={() => {
setShowEventDialog(false)
}}>Delete</Button>
</div>
</div>
)}
</DialogContent>
</Dialog>
</div>
)
}

View File

@@ -0,0 +1,78 @@
"use client"
import { Plus } from "lucide-react"
import { Calendars } from "./calendars"
import { DatePicker } from "./date-picker"
import { Button } from "@/components/ui/button"
import { Separator } from "@/components/ui/separator"
interface CalendarSidebarProps {
selectedDate?: Date
onDateSelect?: (date: Date) => void
onNewCalendar?: () => void
onNewEvent?: () => void
events?: Array<{ date: Date; count: number }>
className?: string
}
export function CalendarSidebar({
selectedDate,
onDateSelect,
onNewCalendar,
onNewEvent,
events = [],
className
}: CalendarSidebarProps) {
return (
<div className={`flex flex-col h-full bg-background rounded-lg ${className}`}>
{/* Add New Event Button */}
<div className="p-6 border-b">
<Button
className="w-full cursor-pointer"
onClick={onNewEvent}
>
<Plus className="w-4 h-4 mr-2" />
Add New Event
</Button>
</div>
{/* Date Picker */}
<DatePicker
selectedDate={selectedDate}
onDateSelect={onDateSelect}
events={events}
/>
<Separator />
{/* Calendars */}
<div className="flex-1 p-4">
<Calendars
onNewCalendar={onNewCalendar}
onCalendarToggle={(calendarId, visible) => {
console.log(`Calendar ${calendarId} visibility: ${visible}`)
}}
onCalendarEdit={(calendarId) => {
console.log(`Edit calendar: ${calendarId}`)
}}
onCalendarDelete={(calendarId) => {
console.log(`Delete calendar: ${calendarId}`)
}}
/>
</div>
{/* Footer */}
<div className="p-4 border-t">
<Button
variant="outline"
className="w-full justify-start cursor-pointer"
onClick={onNewCalendar}
>
<Plus className="w-4 h-4 mr-2" />
New Calendar
</Button>
</div>
</div>
)
}

View File

@@ -0,0 +1,381 @@
"use client"
import { useState } from "react"
import {
ChevronLeft,
ChevronRight,
Calendar as CalendarIcon,
Clock,
MapPin,
Users,
Search,
Grid3X3,
List,
ChevronDown,
Menu,
Plus
} from "lucide-react"
import { format, addMonths, subMonths, startOfMonth, endOfMonth, eachDayOfInterval, isSameMonth, isToday, isSameDay } from "date-fns"
import { Button } from "@/components/ui/button"
import { Badge } from "@/components/ui/badge"
import { Avatar, AvatarFallback } from "@/components/ui/avatar"
import { Calendar } from "@/components/ui/calendar"
import { Sheet, SheetContent, SheetDescription, SheetHeader, SheetTitle } from "@/components/ui/sheet"
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger
} from "@/components/ui/dropdown-menu"
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle
} from "@/components/ui/dialog"
import { cn } from "@/lib/utils"
import { type CalendarEvent } from "../types"
// Import data
import eventsData from "../data/events.json"
import calendarsData from "../data/calendars.json"
interface CalendarMainProps {
eventDates?: Array<{ date: Date; count: number }>
}
export function CalendarMain({ eventDates = [] }: CalendarMainProps) {
const [selectedDate, setSelectedDate] = useState<Date>(new Date())
const [currentDate, setCurrentDate] = useState(new Date())
const [viewMode, setViewMode] = useState<"month" | "week" | "day" | "list">("month")
const [showEventDialog, setShowEventDialog] = useState(false)
const [selectedEvent, setSelectedEvent] = useState<CalendarEvent | null>(null)
const [showCalendarSheet, setShowCalendarSheet] = useState(false)
// Convert JSON events to CalendarEvent objects with proper Date objects
const sampleEvents: CalendarEvent[] = eventsData.map(event => ({
...event,
date: new Date(event.date),
type: event.type as "meeting" | "event" | "personal" | "task" | "reminder"
}))
const monthStart = startOfMonth(currentDate)
const monthEnd = endOfMonth(currentDate)
// Extend to show full weeks (including previous/next month days)
const calendarStart = new Date(monthStart)
calendarStart.setDate(calendarStart.getDate() - monthStart.getDay())
const calendarEnd = new Date(monthEnd)
calendarEnd.setDate(calendarEnd.getDate() + (6 - monthEnd.getDay()))
const calendarDays = eachDayOfInterval({ start: calendarStart, end: calendarEnd })
const getEventsForDay = (date: Date) => {
return sampleEvents.filter(event => isSameDay(event.date, date))
}
const navigateMonth = (direction: "prev" | "next") => {
setCurrentDate(direction === "prev" ? subMonths(currentDate, 1) : addMonths(currentDate, 1))
}
const goToToday = () => {
setCurrentDate(new Date())
}
const handleEventClick = (event: CalendarEvent) => {
setSelectedEvent(event)
setShowEventDialog(true)
}
const handleDateSelect = (date: Date) => {
setSelectedDate(date)
}
const handleNewCalendar = () => {
console.log("Creating new calendar")
// In a real app, this would open a new calendar form
}
const handleNewEvent = () => {
console.log("Creating new event")
// In a real app, this would open event form
}
const renderCalendarGrid = () => {
const weekDays = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']
return (
<div className="flex-1 bg-background">
{/* Calendar Header */}
<div className="grid grid-cols-7 border-b">
{weekDays.map(day => (
<div key={day} className="p-4 text-center font-medium text-sm text-muted-foreground border-r last:border-r-0">
{day}
</div>
))}
</div>
{/* Calendar Grid */}
<div className="grid grid-cols-7 min-h-[600px]">
{calendarDays.map((day) => {
const dayEvents = getEventsForDay(day)
const isCurrentMonth = isSameMonth(day, currentDate)
const isDayToday = isToday(day)
const isSelected = isSameDay(day, selectedDate)
return (
<div
key={day.toISOString()}
className={cn(
"relative border-r border-b last:border-r-0 p-2 min-h-[120px] hover:bg-muted/50 cursor-pointer transition-colors",
!isCurrentMonth && "text-muted-foreground bg-muted/20",
isDayToday && "bg-blue-50 dark:bg-blue-900/20",
isSelected && "bg-blue-100 dark:bg-blue-800/30"
)}
onClick={() => handleDateSelect(day)}
>
{/* Date Number */}
<div className={cn(
"text-sm font-medium mb-1",
isDayToday && "text-blue-600 dark:text-blue-400"
)}>
{format(day, 'd')}
</div>
{/* Events */}
<div className="space-y-1">
{dayEvents.slice(0, 3).map((event) => (
<div
key={event.id}
className={cn(
"text-xs px-2 py-1 rounded text-white cursor-pointer hover:opacity-80 transition-opacity truncate",
event.color
)}
onClick={(e) => {
e.stopPropagation()
handleEventClick(event)
}}
>
{event.time} {event.title}
</div>
))}
{dayEvents.length > 3 && (
<div className="text-xs text-muted-foreground px-2">
+{dayEvents.length - 3} more
</div>
)}
</div>
</div>
)
})}
</div>
</div>
)
}
const renderSidebar = () => (
<div className="w-full h-full bg-background border-r">
<div className="p-4 border-b">
<div className="flex items-center justify-between mb-4">
<h2 className="font-semibold">Calendar</h2>
<Button size="sm" onClick={handleNewEvent}>
<Plus className="h-4 w-4 mr-1" />
Event
</Button>
</div>
{/* Date Picker */}
<Calendar
mode="single"
selected={selectedDate}
onSelect={(date) => date && handleDateSelect(date)}
className="rounded-md border"
modifiers={{
eventDay: eventDates.map(ed => ed.date)
}}
modifiersStyles={{
eventDay: { fontWeight: 'bold' }
}}
/>
</div>
{/* Mini Calendars List */}
<div className="p-4">
<div className="flex items-center justify-between mb-3">
<h3 className="text-sm font-medium">My Calendars</h3>
<Button variant="ghost" size="sm" onClick={handleNewCalendar}>
<Plus className="h-4 w-4" />
</Button>
</div>
<div className="space-y-2">
{calendarsData.map((calendar) => (
<div key={calendar.id} className="flex items-center space-x-2">
<div className={cn("w-3 h-3 rounded-full", calendar.color)} />
<span className="text-sm">{calendar.name}</span>
</div>
))}
</div>
</div>
</div>
)
return (
<div className="border rounded-lg bg-background relative">
<div className="flex min-h-[800px]">
{/* Desktop Sidebar */}
<div className="hidden xl:block w-80 flex-shrink-0">
{renderSidebar()}
</div>
{/* Main Calendar Panel */}
<div className="flex-1 min-w-0">
{/* Calendar Toolbar */}
<div className="border-b bg-background px-4 py-3">
<div className="flex items-center justify-between">
<div className="flex items-center space-x-4">
{/* Mobile Menu Button */}
<Button
variant="ghost"
size="sm"
className="xl:hidden"
onClick={() => setShowCalendarSheet(true)}
>
<Menu className="h-4 w-4" />
</Button>
{/* Month Navigation */}
<div className="flex items-center space-x-2">
<Button variant="ghost" size="sm" onClick={() => navigateMonth("prev")}>
<ChevronLeft className="h-4 w-4" />
</Button>
<h2 className="text-lg font-semibold min-w-[140px] text-center">
{format(currentDate, 'MMMM yyyy')}
</h2>
<Button variant="ghost" size="sm" onClick={() => navigateMonth("next")}>
<ChevronRight className="h-4 w-4" />
</Button>
</div>
<Button variant="outline" size="sm" onClick={goToToday}>
Today
</Button>
</div>
<div className="flex items-center space-x-2">
<div className="hidden sm:flex items-center space-x-2">
<Button variant="ghost" size="sm" className="text-xs">
<Search className="h-4 w-4 mr-1" />
Search
</Button>
</div>
{/* View Mode Toggle */}
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="outline" size="sm">
<Grid3X3 className="h-4 w-4 mr-1" />
{viewMode === "month" ? "Month" : viewMode === "week" ? "Week" : viewMode === "day" ? "Day" : "List"}
<ChevronDown className="h-4 w-4 ml-1" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem onClick={() => setViewMode("month")}>
<Grid3X3 className="h-4 w-4 mr-2" />
Month
</DropdownMenuItem>
<DropdownMenuItem onClick={() => setViewMode("week")}>
<List className="h-4 w-4 mr-2" />
Week
</DropdownMenuItem>
<DropdownMenuItem onClick={() => setViewMode("day")}>
<CalendarIcon className="h-4 w-4 mr-2" />
Day
</DropdownMenuItem>
<DropdownMenuItem onClick={() => setViewMode("list")}>
<List className="h-4 w-4 mr-2" />
List
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
</div>
</div>
{/* Calendar Content */}
{renderCalendarGrid()}
</div>
</div>
{/* Mobile/Tablet Sheet */}
<Sheet open={showCalendarSheet} onOpenChange={setShowCalendarSheet}>
<SheetContent side="left" className="w-80 p-0">
<SheetHeader className="p-4 pb-2">
<SheetTitle>Calendar</SheetTitle>
<SheetDescription>
Browse dates and manage your calendar events
</SheetDescription>
</SheetHeader>
{renderSidebar()}
</SheetContent>
</Sheet>
{/* Event Details Dialog */}
<Dialog open={showEventDialog} onOpenChange={setShowEventDialog}>
<DialogContent className="sm:max-w-md">
<DialogHeader>
<DialogTitle>{selectedEvent?.title}</DialogTitle>
<DialogDescription>
Event details and information
</DialogDescription>
</DialogHeader>
{selectedEvent && (
<div className="space-y-4 pt-4">
<div className="flex items-center space-x-2 text-sm">
<Clock className="h-4 w-4 text-muted-foreground" />
<span>{selectedEvent.time} {selectedEvent.duration}</span>
</div>
{selectedEvent.location && (
<div className="flex items-center space-x-2 text-sm">
<MapPin className="h-4 w-4 text-muted-foreground" />
<span>{selectedEvent.location}</span>
</div>
)}
{selectedEvent.attendees.length > 0 && (
<div className="flex items-center space-x-2 text-sm">
<Users className="h-4 w-4 text-muted-foreground" />
<div className="flex space-x-1">
{selectedEvent.attendees.map((attendee, index) => (
<Avatar key={index} className="h-6 w-6">
<AvatarFallback className="text-xs">
{attendee}
</AvatarFallback>
</Avatar>
))}
</div>
</div>
)}
{selectedEvent.description && (
<div className="text-sm text-muted-foreground">
{selectedEvent.description}
</div>
)}
<div className="flex items-center space-x-2 pt-4">
<Badge variant="secondary" className={cn("text-white", selectedEvent.color)}>
{selectedEvent.type}
</Badge>
</div>
</div>
)}
</DialogContent>
</Dialog>
</div>
)
}

View File

@@ -0,0 +1,77 @@
"use client"
import { CalendarSidebar } from "./calendar-sidebar"
import { CalendarMain } from "./calendar-main"
import { EventForm } from "./event-form"
import { Sheet, SheetContent, SheetDescription, SheetHeader, SheetTitle } from "@/components/ui/sheet"
import { type CalendarEvent } from "../types"
import { useCalendar } from "../use-calendar"
interface CalendarProps {
events: CalendarEvent[]
eventDates: Array<{ date: Date; count: number }>
}
export function Calendar({ events, eventDates }: CalendarProps) {
const calendar = useCalendar(events)
return (
<>
<div className="border rounded-lg bg-background relative">
<div className="flex min-h-[800px]">
{/* Desktop Sidebar - Hidden on mobile/tablet, shown on extra large screens */}
<div className="hidden xl:block w-80 flex-shrink-0 border-r">
<CalendarSidebar
selectedDate={calendar.selectedDate}
onDateSelect={calendar.handleDateSelect}
onNewCalendar={calendar.handleNewCalendar}
onNewEvent={calendar.handleNewEvent}
events={eventDates}
className="h-full"
/>
</div>
{/* Main Calendar Panel */}
<div className="flex-1 min-w-0">
<CalendarMain
selectedDate={calendar.selectedDate}
onDateSelect={calendar.handleDateSelect}
onMenuClick={() => calendar.setShowCalendarSheet(true)}
events={calendar.events}
onEventClick={calendar.handleEditEvent}
/>
</div>
</div>
{/* Mobile/Tablet Sheet - Positioned relative to calendar container */}
<Sheet open={calendar.showCalendarSheet} onOpenChange={calendar.setShowCalendarSheet}>
<SheetContent side="left" className="w-80 p-0" style={{ position: 'absolute' }}>
<SheetHeader className="p-4 pb-2">
<SheetTitle>Calendar</SheetTitle>
<SheetDescription>
Browse dates and manage your calendar events
</SheetDescription>
</SheetHeader>
<CalendarSidebar
selectedDate={calendar.selectedDate}
onDateSelect={calendar.handleDateSelect}
onNewCalendar={calendar.handleNewCalendar}
onNewEvent={calendar.handleNewEvent}
events={eventDates}
className="h-full"
/>
</SheetContent>
</Sheet>
</div>
{/* Event Form Dialog */}
<EventForm
event={calendar.editingEvent}
open={calendar.showEventForm}
onOpenChange={calendar.setShowEventForm}
onSave={calendar.handleSaveEvent}
onDelete={calendar.handleDeleteEvent}
/>
</>
)
}

View File

@@ -0,0 +1,203 @@
"use client"
import { useState } from "react"
import { Check, ChevronRight, Plus, Eye, EyeOff, MoreHorizontal } from "lucide-react"
import {
Collapsible,
CollapsibleContent,
CollapsibleTrigger,
} from "@/components/ui/collapsible"
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuSeparator,
DropdownMenuTrigger
} from "@/components/ui/dropdown-menu"
import { cn } from "@/lib/utils"
interface CalendarItem {
id: string
name: string
color: string
visible: boolean
type: "personal" | "work" | "shared"
}
interface CalendarGroup {
name: string
items: CalendarItem[]
}
interface CalendarsProps {
calendars?: {
name: string
items: string[]
}[]
onCalendarToggle?: (calendarId: string, visible: boolean) => void
onCalendarEdit?: (calendarId: string) => void
onCalendarDelete?: (calendarId: string) => void
onNewCalendar?: () => void
}
// Enhanced calendar data with colors and visibility
const enhancedCalendars: CalendarGroup[] = [
{
name: "My Calendars",
items: [
{ id: "personal", name: "Personal", color: "bg-blue-500", visible: true, type: "personal" },
{ id: "work", name: "Work", color: "bg-green-500", visible: true, type: "work" },
{ id: "family", name: "Family", color: "bg-pink-500", visible: true, type: "personal" }
]
},
{
name: "Favorites",
items: [
{ id: "holidays", name: "Holidays", color: "bg-red-500", visible: true, type: "shared" },
{ id: "birthdays", name: "Birthdays", color: "bg-purple-500", visible: true, type: "personal" }
]
},
{
name: "Other",
items: [
{ id: "travel", name: "Travel", color: "bg-orange-500", visible: false, type: "personal" },
{ id: "reminders", name: "Reminders", color: "bg-yellow-500", visible: true, type: "personal" },
{ id: "deadlines", name: "Deadlines", color: "bg-red-600", visible: true, type: "work" }
]
}
]
export function Calendars({
onCalendarToggle,
onCalendarEdit,
onCalendarDelete,
onNewCalendar
}: CalendarsProps) {
const [calendarData, setCalendarData] = useState(enhancedCalendars)
const handleToggleVisibility = (calendarId: string) => {
setCalendarData(prev => prev.map(group => ({
...group,
items: group.items.map(item =>
item.id === calendarId
? { ...item, visible: !item.visible }
: item
)
})))
const calendar = calendarData.flatMap(g => g.items).find(c => c.id === calendarId)
if (calendar) {
onCalendarToggle?.(calendarId, !calendar.visible)
}
}
return (
<div className="space-y-4">
{calendarData.map((calendar, index) => (
<div key={calendar.name}>
<Collapsible
defaultOpen={index === 0}
className="group/collapsible"
>
<CollapsibleTrigger className="flex items-center justify-between w-full p-2 hover:bg-accent hover:text-accent-foreground rounded-md cursor-pointer">
<span className="text-sm font-medium">{calendar.name}</span>
<div className="flex items-center gap-1">
{index === 0 && (
<div
className="h-5 w-5 flex items-center justify-center opacity-0 group-hover/collapsible:opacity-100 cursor-pointer hover:bg-accent rounded-sm"
onClick={(e) => {
e.stopPropagation()
onNewCalendar?.()
}}
>
<Plus className="h-3 w-3" />
</div>
)}
<ChevronRight className="h-4 w-4 transition-transform group-data-[state=open]/collapsible:rotate-90" />
</div>
</CollapsibleTrigger>
<CollapsibleContent>
<div className="mt-2 space-y-1">
{calendar.items.map((item) => (
<div key={item.id} className="group/calendar-item">
<div className="flex items-center justify-between p-2 hover:bg-accent/50 rounded-md">
<div className="flex items-center gap-3 flex-1">
{/* Calendar Color & Visibility Toggle */}
<button
onClick={() => handleToggleVisibility(item.id)}
className={cn(
"flex aspect-square size-4 shrink-0 items-center justify-center rounded-sm border transition-all cursor-pointer",
item.visible
? cn("border-transparent text-white", item.color)
: "border-border bg-transparent"
)}
>
{item.visible && <Check className="size-3" />}
</button>
{/* Calendar Name */}
<span
className={cn(
"flex-1 truncate text-sm cursor-pointer",
!item.visible && "text-muted-foreground"
)}
onClick={() => handleToggleVisibility(item.id)}
>
{item.name}
</span>
{/* Visibility Icon */}
<div className="opacity-0 group-hover/calendar-item:opacity-100">
{item.visible ? (
<Eye className="h-3 w-3 text-muted-foreground" />
) : (
<EyeOff className="h-3 w-3 text-muted-foreground" />
)}
</div>
{/* More Options */}
<DropdownMenu>
<DropdownMenuTrigger asChild>
<div
className="h-5 w-5 flex items-center justify-center p-0 opacity-0 group-hover/calendar-item:opacity-100 cursor-pointer hover:bg-accent rounded-sm"
onClick={(e) => e.stopPropagation()}
>
<MoreHorizontal className="h-3 w-3" />
</div>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" side="right">
<DropdownMenuItem
onClick={() => onCalendarEdit?.(item.id)}
className="cursor-pointer"
>
Edit calendar
</DropdownMenuItem>
<DropdownMenuItem
onClick={() => handleToggleVisibility(item.id)}
className="cursor-pointer"
>
{item.visible ? "Hide" : "Show"} calendar
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem
onClick={() => onCalendarDelete?.(item.id)}
className="cursor-pointer text-destructive"
>
Delete calendar
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
</div>
</div>
))}
</div>
</CollapsibleContent>
</Collapsible>
</div>
))}
</div>
)
}

View File

@@ -0,0 +1,48 @@
"use client"
import { useState } from "react"
import { Calendar } from "@/components/ui/calendar"
interface DatePickerProps {
selectedDate?: Date
onDateSelect?: (date: Date) => void
events?: Array<{ date: Date; count: number }>
}
export function DatePicker({ selectedDate, onDateSelect, events = [] }: DatePickerProps) {
const [date, setDate] = useState<Date | undefined>(selectedDate || new Date())
const handleDateSelect = (selectedDate: Date | undefined) => {
if (selectedDate) {
setDate(selectedDate)
onDateSelect?.(selectedDate)
}
}
// Create a map of dates with events for styling
const eventDates = events.reduce((acc, event) => {
const dateKey = event.date.toDateString()
acc[dateKey] = event.count
return acc
}, {} as Record<string, number>)
return (
<div className="flex justify-center">
<Calendar
mode="single"
selected={date}
onSelect={handleDateSelect}
className="w-full [&_[role=gridcell]_button]:cursor-pointer [&_button]:cursor-pointer"
modifiers={{
hasEvents: (date) => {
const eventCount = eventDates[date.toDateString()]
return Boolean(eventCount && eventCount > 0)
}
}}
modifiersClassNames={{
hasEvents: "relative after:absolute after:bottom-1 after:right-1 after:w-1.5 after:h-1.5 after:bg-primary after:rounded-full"
}}
/>
</div>
)
}

View File

@@ -0,0 +1,339 @@
"use client"
import { useState } from "react"
import { CalendarIcon, Clock, MapPin, Users, Type, Tag } from "lucide-react"
import { format } from "date-fns"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"
import { Textarea } from "@/components/ui/textarea"
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue
} from "@/components/ui/select"
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle
} from "@/components/ui/dialog"
import {
Popover,
PopoverContent,
PopoverTrigger
} from "@/components/ui/popover"
import { Calendar } from "@/components/ui/calendar"
import { Badge } from "@/components/ui/badge"
import { Switch } from "@/components/ui/switch"
import { Avatar, AvatarFallback } from "@/components/ui/avatar"
import { cn } from "@/lib/utils"
import { type CalendarEvent } from "../types"
interface EventFormProps {
event?: CalendarEvent | null
open: boolean
onOpenChange: (open: boolean) => void
onSave: (event: Partial<CalendarEvent>) => void
onDelete?: (eventId: number) => void
}
const eventTypes = [
{ value: "meeting", label: "Meeting", color: "bg-blue-500" },
{ value: "event", label: "Event", color: "bg-green-500" },
{ value: "personal", label: "Personal", color: "bg-pink-500" },
{ value: "task", label: "Task", color: "bg-orange-500" },
{ value: "reminder", label: "Reminder", color: "bg-purple-500" }
]
const timeSlots = [
"9:00 AM", "9:30 AM", "10:00 AM", "10:30 AM", "11:00 AM", "11:30 AM",
"12:00 PM", "12:30 PM", "1:00 PM", "1:30 PM", "2:00 PM", "2:30 PM",
"3:00 PM", "3:30 PM", "4:00 PM", "4:30 PM", "5:00 PM", "5:30 PM",
"6:00 PM", "6:30 PM", "7:00 PM", "7:30 PM", "8:00 PM", "8:30 PM"
]
const durationOptions = [
"15 min", "30 min", "45 min", "1 hour", "1.5 hours", "2 hours", "3 hours", "All day"
]
export function EventForm({ event, open, onOpenChange, onSave, onDelete }: EventFormProps) {
const [formData, setFormData] = useState({
title: event?.title || "",
date: event?.date || new Date(),
time: event?.time || "9:00 AM",
duration: event?.duration || "1 hour",
type: event?.type || "meeting",
location: event?.location || "",
description: event?.description || "",
attendees: event?.attendees || [],
allDay: false,
reminder: true
})
const [showCalendar, setShowCalendar] = useState(false)
const [newAttendee, setNewAttendee] = useState("")
const handleSave = () => {
const eventData: Partial<CalendarEvent> = {
...formData,
id: event?.id,
color: eventTypes.find(t => t.value === formData.type)?.color || "bg-blue-500"
}
onSave(eventData)
onOpenChange(false)
}
const handleDelete = () => {
if (event?.id && onDelete) {
onDelete(event.id)
onOpenChange(false)
}
}
const addAttendee = () => {
if (newAttendee.trim() && !formData.attendees.includes(newAttendee.trim())) {
setFormData(prev => ({
...prev,
attendees: [...prev.attendees, newAttendee.trim()]
}))
setNewAttendee("")
}
}
const removeAttendee = (attendee: string) => {
setFormData(prev => ({
...prev,
attendees: prev.attendees.filter(a => a !== attendee)
}))
}
const selectedEventType = eventTypes.find(t => t.value === formData.type)
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="max-w-2xl max-h-[90vh] overflow-y-auto">
<DialogHeader>
<DialogTitle className="flex items-center gap-2">
<div className={cn("w-3 h-3 rounded-full", selectedEventType?.color)} />
{event ? "Edit Event" : "Create New Event"}
</DialogTitle>
<DialogDescription>
{event ? "Make changes to this event" : "Add a new event to your calendar"}
</DialogDescription>
</DialogHeader>
<div className="space-y-6 py-4">
{/* Event Title */}
<div className="space-y-2">
<Label htmlFor="title" className="flex items-center gap-2">
<Type className="w-4 h-4" />
Event Title
</Label>
<Input
id="title"
placeholder="Enter event title..."
value={formData.title}
onChange={(e) => setFormData(prev => ({ ...prev, title: e.target.value }))}
className="text-lg font-medium"
/>
</div>
{/* Event Type */}
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label className="flex items-center gap-2">
<Tag className="w-4 h-4" />
Event Type
</Label>
<Select value={formData.type} onValueChange={(value) => setFormData(prev => ({ ...prev, type: value as CalendarEvent["type"] }))}>
<SelectTrigger className="w-full">
<SelectValue />
</SelectTrigger>
<SelectContent>
{eventTypes.map(type => (
<SelectItem key={type.value} value={type.value}>
<div className="flex items-center gap-2">
<div className={cn("w-3 h-3 rounded-full", type.color)} />
{type.label}
</div>
</SelectItem>
))}
</SelectContent>
</Select>
</div>
</div>
{/* Date and Time */}
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label className="flex items-center gap-2">
<CalendarIcon className="w-4 h-4" />
Date
</Label>
<Popover open={showCalendar} onOpenChange={setShowCalendar}>
<PopoverTrigger asChild>
<Button variant="outline" className="w-full justify-start text-left font-normal">
{format(formData.date, "PPP")}
</Button>
</PopoverTrigger>
<PopoverContent className="w-auto p-0" align="start">
<Calendar
mode="single"
selected={formData.date}
onSelect={(date) => {
if (date) {
setFormData(prev => ({ ...prev, date }))
setShowCalendar(false)
}
}}
initialFocus
/>
</PopoverContent>
</Popover>
</div>
<div className="space-y-2">
<Label className="flex items-center gap-2">
<Clock className="w-4 h-4" />
Time
</Label>
<Select value={formData.time} onValueChange={(value) => setFormData(prev => ({ ...prev, time: value }))}>
<SelectTrigger className="w-full">
<SelectValue />
</SelectTrigger>
<SelectContent>
{timeSlots.map(time => (
<SelectItem key={time} value={time}>{time}</SelectItem>
))}
</SelectContent>
</Select>
</div>
</div>
{/* Duration and All Day */}
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label>Duration</Label>
<Select value={formData.duration} onValueChange={(value) => setFormData(prev => ({ ...prev, duration: value }))}>
<SelectTrigger className="w-full">
<SelectValue />
</SelectTrigger>
<SelectContent>
{durationOptions.map(duration => (
<SelectItem key={duration} value={duration}>{duration}</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<Label>Options</Label>
<div className="flex items-center space-x-4 h-10">
<div className="flex items-center space-x-2">
<Switch
id="all-day"
checked={formData.allDay}
onCheckedChange={(checked) => setFormData(prev => ({ ...prev, allDay: checked }))}
/>
<Label htmlFor="all-day" className="text-sm">All day</Label>
</div>
<div className="flex items-center space-x-2">
<Switch
id="reminder"
checked={formData.reminder}
onCheckedChange={(checked) => setFormData(prev => ({ ...prev, reminder: checked }))}
/>
<Label htmlFor="reminder" className="text-sm">Reminder</Label>
</div>
</div>
</div>
</div>
{/* Location */}
<div className="space-y-2">
<Label htmlFor="location" className="flex items-center gap-2">
<MapPin className="w-4 h-4" />
Location
</Label>
<Input
id="location"
placeholder="Add location..."
value={formData.location}
onChange={(e) => setFormData(prev => ({ ...prev, location: e.target.value }))}
/>
</div>
{/* Attendees */}
<div className="space-y-2">
<Label className="flex items-center gap-2">
<Users className="w-4 h-4" />
Attendees
</Label>
<div className="flex gap-2">
<Input
placeholder="Add attendee..."
value={newAttendee}
onChange={(e) => setNewAttendee(e.target.value)}
onKeyPress={(e) => e.key === "Enter" && addAttendee()}
/>
<Button onClick={addAttendee} variant="outline" className="cursor-pointer">Add</Button>
</div>
{formData.attendees.length > 0 && (
<div className="flex flex-wrap gap-2 mt-2">
{formData.attendees.map((attendee, index) => (
<Badge key={index} variant="secondary" className="flex items-center gap-2 px-2 py-1">
<Avatar className="w-5 h-5">
<AvatarFallback className="text-[10px] font-medium">
{attendee.split(' ').map(n => n[0]).join('').slice(0, 2).toUpperCase()}
</AvatarFallback>
</Avatar>
<span className="text-sm">{attendee}</span>
<button
onClick={() => removeAttendee(attendee)}
className="text-muted-foreground hover:text-foreground cursor-pointer"
type="button"
>
×
</button>
</Badge>
))}
</div>
)}
</div>
{/* Description */}
<div className="space-y-2">
<Label htmlFor="description">Description</Label>
<Textarea
id="description"
placeholder="Add description..."
value={formData.description}
onChange={(e) => setFormData(prev => ({ ...prev, description: e.target.value }))}
rows={3}
/>
</div>
{/* Actions */}
<div className="flex gap-3 pt-6">
<Button onClick={handleSave} className="flex-1 cursor-pointer">
{event ? "Update Event" : "Create Event"}
</Button>
{event && onDelete && (
<Button onClick={handleDelete} variant="destructive" className="cursor-pointer">
Delete
</Button>
)}
<Button onClick={() => onOpenChange(false)} variant="outline" className="cursor-pointer">
Cancel
</Button>
</div>
</div>
</DialogContent>
</Dialog>
)
}

View File

@@ -0,0 +1,152 @@
"use client"
import {
Clock,
Users,
Plus,
Settings,
Download,
Share,
Bell
} from "lucide-react"
import { Button } from "@/components/ui/button"
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
import { Badge } from "@/components/ui/badge"
import { Separator } from "@/components/ui/separator"
interface QuickActionsProps {
onNewEvent?: () => void
onNewMeeting?: () => void
onNewReminder?: () => void
onSettings?: () => void
}
export function QuickActions({
onNewEvent,
onNewMeeting,
onNewReminder,
onSettings
}: QuickActionsProps) {
const quickStats = [
{ label: "Today's Events", value: "3", color: "bg-blue-500" },
{ label: "This Week", value: "12", color: "bg-green-500" },
{ label: "Pending", value: "2", color: "bg-orange-500" }
]
return (
<div className="space-y-4">
{/* Quick Stats */}
<Card>
<CardHeader className="pb-3">
<CardTitle className="text-sm font-medium">Overview</CardTitle>
</CardHeader>
<CardContent className="space-y-3">
{quickStats.map((stat, index) => (
<div key={index} className="flex items-center justify-between">
<div className="flex items-center gap-2">
<div className={`w-2 h-2 rounded-full ${stat.color}`} />
<span className="text-sm text-muted-foreground">{stat.label}</span>
</div>
<Badge variant="secondary">{stat.value}</Badge>
</div>
))}
</CardContent>
</Card>
{/* Quick Actions */}
<Card>
<CardHeader className="pb-3">
<CardTitle className="text-sm font-medium">Quick Actions</CardTitle>
</CardHeader>
<CardContent className="space-y-2">
<Button
variant="outline"
className="w-full justify-start cursor-pointer"
onClick={onNewEvent}
>
<Plus className="w-4 h-4 mr-2" />
New Event
</Button>
<Button
variant="outline"
className="w-full justify-start cursor-pointer"
onClick={onNewMeeting}
>
<Users className="w-4 h-4 mr-2" />
Schedule Meeting
</Button>
<Button
variant="outline"
className="w-full justify-start cursor-pointer"
onClick={onNewReminder}
>
<Bell className="w-4 h-4 mr-2" />
Set Reminder
</Button>
<Separator className="my-3" />
<Button
variant="ghost"
size="sm"
className="w-full justify-start cursor-pointer"
>
<Share className="w-4 h-4 mr-2" />
Share Calendar
</Button>
<Button
variant="ghost"
size="sm"
className="w-full justify-start cursor-pointer"
>
<Download className="w-4 h-4 mr-2" />
Export
</Button>
<Button
variant="ghost"
size="sm"
className="w-full justify-start cursor-pointer"
onClick={onSettings}
>
<Settings className="w-4 h-4 mr-2" />
Settings
</Button>
</CardContent>
</Card>
{/* Upcoming Events */}
<Card>
<CardHeader className="pb-3">
<CardTitle className="text-sm font-medium flex items-center gap-2">
<Clock className="w-4 h-4" />
Next Up
</CardTitle>
</CardHeader>
<CardContent className="space-y-3">
<div className="space-y-2">
<div className="flex items-start gap-3">
<div className="w-2 h-2 bg-blue-500 rounded-full mt-2" />
<div className="flex-1 min-w-0">
<p className="text-sm font-medium truncate">Team Standup</p>
<p className="text-xs text-muted-foreground">9:00 AM Conference Room A</p>
</div>
</div>
<div className="flex items-start gap-3">
<div className="w-2 h-2 bg-purple-500 rounded-full mt-2" />
<div className="flex-1 min-w-0">
<p className="text-sm font-medium truncate">Design Review</p>
<p className="text-xs text-muted-foreground">2:00 PM Virtual</p>
</div>
</div>
</div>
</CardContent>
</Card>
</div>
)
}

55
src/app/calendar/data.ts Normal file
View File

@@ -0,0 +1,55 @@
import { type CalendarEvent, type Calendar } from "./types"
// Import JSON data
import eventsData from "./data/events.json"
import eventDatesData from "./data/event-dates.json"
import calendarsData from "./data/calendars.json"
// Convert JSON events to CalendarEvent objects with proper Date objects
// Always use current month and year, but preserve day and time from JSON
export const events: CalendarEvent[] = eventsData.map(event => {
const now = new Date()
const currentYear = now.getFullYear()
const currentMonth = now.getMonth() // 0-based month
// Parse the day from the date string (format: "11T09:00:00.000Z")
const dayAndTime = event.date.split('T')
const day = parseInt(dayAndTime[0])
const timeStr = dayAndTime[1] // "09:00:00.000Z"
// Parse hours and minutes from time string
const timeParts = timeStr.split(':')
const hours = parseInt(timeParts[0])
const minutes = parseInt(timeParts[1])
// Create date with current year/month but original day and time
const eventDate = new Date(currentYear, currentMonth, day, hours, minutes)
return {
...event,
date: eventDate,
type: event.type as "meeting" | "event" | "personal" | "task" | "reminder"
}
})
// Convert event dates for calendar picker - also use current month/year
export const eventDates = eventDatesData.map(item => {
const now = new Date()
const currentYear = now.getFullYear()
const currentMonth = now.getMonth()
// Parse day from date string
const day = parseInt(item.date.split('T')[0])
const eventDate = new Date(currentYear, currentMonth, day)
return {
date: eventDate,
count: item.count
}
})
// Calendars data
export const calendars: Calendar[] = calendarsData as Calendar[]
// Export individual collections for convenience
export { eventsData, eventDatesData, calendarsData }

View File

@@ -0,0 +1,37 @@
[
{
"id": "personal",
"name": "Personal",
"color": "bg-blue-500",
"visible": true,
"type": "personal"
},
{
"id": "work",
"name": "Work",
"color": "bg-green-500",
"visible": true,
"type": "work"
},
{
"id": "shared",
"name": "Team Calendar",
"color": "bg-purple-500",
"visible": true,
"type": "shared"
},
{
"id": "meetings",
"name": "Meetings",
"color": "bg-orange-500",
"visible": true,
"type": "work"
},
{
"id": "events",
"name": "Events",
"color": "bg-pink-500",
"visible": true,
"type": "shared"
}
]

View File

@@ -0,0 +1,30 @@
[
{
"date": "11T00:00:00.000Z",
"count": 2
},
{
"date": "15T00:00:00.000Z",
"count": 1
},
{
"date": "18T00:00:00.000Z",
"count": 1
},
{
"date": "20T00:00:00.000Z",
"count": 1
},
{
"date": "22T00:00:00.000Z",
"count": 1
},
{
"date": "25T00:00:00.000Z",
"count": 1
},
{
"date": "27T00:00:00.000Z",
"count": 1
}
]

View File

@@ -0,0 +1,62 @@
[
{
"id": 1,
"title": "Team Standup",
"date": "11T09:00:00.000Z",
"time": "9:00 AM",
"duration": "30 min",
"type": "meeting",
"attendees": ["JD", "SM", "AR"],
"location": "Conference Room A",
"color": "bg-blue-500",
"description": "Daily team standup meeting to discuss progress and blockers"
},
{
"id": 2,
"title": "Design Review",
"date": "11T14:00:00.000Z",
"time": "2:00 PM",
"duration": "1 hour",
"type": "meeting",
"attendees": ["ER", "LC"],
"location": "Virtual",
"color": "bg-purple-500",
"description": "Review new UI designs and provide feedback"
},
{
"id": 3,
"title": "Product Launch",
"date": "15T10:00:00.000Z",
"time": "10:00 AM",
"duration": "2 hours",
"type": "event",
"attendees": ["TL", "ST"],
"location": "Main Hall",
"color": "bg-green-500",
"description": "Official product launch event with stakeholders"
},
{
"id": 4,
"title": "Client Presentation",
"date": "18T15:00:00.000Z",
"time": "3:00 PM",
"duration": "1 hour",
"type": "meeting",
"attendees": ["AT", "SM"],
"location": "Client Office",
"color": "bg-orange-500",
"description": "Present project progress to client stakeholders"
},
{
"id": 5,
"title": "Birthday Party 🎉",
"date": "20T19:00:00.000Z",
"time": "7:00 PM",
"duration": "3 hours",
"type": "personal",
"attendees": ["PB", "VB"],
"location": "Home",
"color": "bg-pink-500",
"description": "Birthday celebration with friends and family"
}
]

16
src/app/calendar/page.tsx Normal file
View File

@@ -0,0 +1,16 @@
import { BaseLayout } from "@/components/layouts/base-layout"
import { Calendar } from "./components/calendar"
import { events, eventDates } from "./data"
export default function CalendarPage() {
return (
<BaseLayout
title="Calendar"
description="Manage your schedule and events"
>
<div className="px-4 lg:px-6">
<Calendar events={events} eventDates={eventDates} />
</div>
</BaseLayout>
)
}

20
src/app/calendar/types.ts Normal file
View File

@@ -0,0 +1,20 @@
export interface CalendarEvent {
id: number
title: string
date: Date
time: string
duration: string
type: "meeting" | "event" | "personal" | "task" | "reminder"
attendees: string[]
location: string
color: string
description?: string
}
export interface Calendar {
id: string
name: string
color: string
visible: boolean
type: "personal" | "work" | "shared"
}

View File

@@ -0,0 +1,90 @@
"use client"
import { useState, useCallback } from "react"
import { type CalendarEvent } from "./types"
export interface UseCalendarState {
selectedDate: Date
showEventForm: boolean
editingEvent: CalendarEvent | null
showCalendarSheet: boolean
events: CalendarEvent[]
}
export interface UseCalendarActions {
setSelectedDate: (date: Date) => void
setShowEventForm: (show: boolean) => void
setEditingEvent: (event: CalendarEvent | null) => void
setShowCalendarSheet: (show: boolean) => void
handleDateSelect: (date: Date) => void
handleNewEvent: () => void
handleNewCalendar: () => void
handleSaveEvent: (eventData: Partial<CalendarEvent>) => void
handleDeleteEvent: (eventId: number) => void
handleEditEvent: (event: CalendarEvent) => void
}
export interface UseCalendarReturn extends UseCalendarState, UseCalendarActions {}
export function useCalendar(initialEvents: CalendarEvent[] = []): UseCalendarReturn {
const [selectedDate, setSelectedDate] = useState<Date>(new Date())
const [showEventForm, setShowEventForm] = useState(false)
const [editingEvent, setEditingEvent] = useState<CalendarEvent | null>(null)
const [showCalendarSheet, setShowCalendarSheet] = useState(false)
const [events] = useState<CalendarEvent[]>(initialEvents)
const handleDateSelect = useCallback((date: Date) => {
setSelectedDate(date)
// Auto-close mobile sheet when date is selected
setShowCalendarSheet(false)
}, [])
const handleNewEvent = useCallback(() => {
setEditingEvent(null)
setShowEventForm(true)
}, [])
const handleNewCalendar = useCallback(() => {
console.log("Creating new calendar")
// In a real app, this would open a new calendar form
}, [])
const handleSaveEvent = useCallback((eventData: Partial<CalendarEvent>) => {
console.log("Saving event:", eventData)
// In a real app, this would save to a backend
setShowEventForm(false)
setEditingEvent(null)
}, [])
const handleDeleteEvent = useCallback((eventId: number) => {
console.log("Deleting event:", eventId)
// In a real app, this would delete from backend
setShowEventForm(false)
setEditingEvent(null)
}, [])
const handleEditEvent = useCallback((event: CalendarEvent) => {
setEditingEvent(event)
setShowEventForm(true)
}, [])
return {
// State
selectedDate,
showEventForm,
editingEvent,
showCalendarSheet,
events,
// Actions
setSelectedDate,
setShowEventForm,
setEditingEvent,
setShowCalendarSheet,
handleDateSelect,
handleNewEvent,
handleNewCalendar,
handleSaveEvent,
handleDeleteEvent,
handleEditEvent,
}
}

View File

@@ -0,0 +1,233 @@
"use client"
import {
Phone,
Video,
Info,
Search,
MoreVertical,
Users,
Bell,
BellOff
} from "lucide-react"
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"
import { Button } from "@/components/ui/button"
import { Badge } from "@/components/ui/badge"
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuSeparator,
DropdownMenuTrigger
} from "@/components/ui/dropdown-menu"
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger
} from "@/components/ui/tooltip"
import { type Conversation, type User } from "@/app/chat/use-chat"
interface ChatHeaderProps {
conversation: Conversation | null
users: User[]
onToggleMute?: () => void
onToggleInfo?: () => void
}
export function ChatHeader({
conversation,
users,
onToggleMute,
onToggleInfo
}: ChatHeaderProps) {
if (!conversation) {
return (
<div className="flex items-center justify-center h-full">
<p className="text-muted-foreground">Select a conversation to start chatting</p>
</div>
)
}
const getConversationUsers = () => {
if (conversation.type === "direct") {
return users.filter(user => conversation.participants.includes(user.id))
}
return users.filter(user => conversation.participants.includes(user.id))
}
const conversationUsers = getConversationUsers()
const primaryUser = conversationUsers[0]
const getStatusText = () => {
if (conversation.type === "group") {
const onlineCount = conversationUsers.filter(user => user.status === "online").length
return `${conversation.participants.length} members, ${onlineCount} online`
} else if (primaryUser) {
switch (primaryUser.status) {
case "online":
return "Active now"
case "away":
return "Away"
case "offline":
return `Last seen ${new Date(primaryUser.lastSeen).toLocaleDateString()}`
default:
return ""
}
}
return ""
}
const getStatusColor = () => {
if (conversation.type === "group") return "text-muted-foreground"
switch (primaryUser?.status) {
case "online":
return "text-green-600"
case "away":
return "text-yellow-600"
case "offline":
return "text-muted-foreground"
default:
return "text-muted-foreground"
}
}
return (
<div className="flex items-center justify-between h-full">
{/* Left side - Avatar and info */}
<div className="flex items-center gap-3">
<Avatar className="h-10 w-10 cursor-pointer">
<AvatarImage src={conversation.avatar} alt={conversation.name} />
<AvatarFallback>
{conversation.type === "group" ? (
<Users className="h-5 w-5" />
) : (
conversation.name.split(' ').map(n => n[0]).join('').slice(0, 2)
)}
</AvatarFallback>
</Avatar>
<div className="min-w-0 flex-1">
<div className="flex items-center gap-2">
<h2 className="font-semibold truncate">{conversation.name}</h2>
{conversation.isMuted && (
<BellOff className="h-4 w-4 text-muted-foreground" />
)}
{conversation.type === "group" && (
<Badge variant="secondary" className="text-xs cursor-pointer">
Group
</Badge>
)}
</div>
<p className={`text-sm ${getStatusColor()}`}>
{getStatusText()}
</p>
</div>
</div>
{/* Right side - Action buttons */}
<div className="flex items-center gap-1">
<TooltipProvider>
{/* Search */}
<Tooltip>
<TooltipTrigger asChild>
<Button variant="ghost" size="icon" className="cursor-pointer">
<Search className="h-4 w-4" />
</Button>
</TooltipTrigger>
<TooltipContent>
<p>Search in conversation</p>
</TooltipContent>
</Tooltip>
{/* Phone call */}
<Tooltip>
<TooltipTrigger asChild>
<Button variant="ghost" size="icon" className="cursor-pointer">
<Phone className="h-4 w-4" />
</Button>
</TooltipTrigger>
<TooltipContent>
<p>Voice call</p>
</TooltipContent>
</Tooltip>
{/* Video call */}
<Tooltip>
<TooltipTrigger asChild>
<Button variant="ghost" size="icon" className="cursor-pointer">
<Video className="h-4 w-4" />
</Button>
</TooltipTrigger>
<TooltipContent>
<p>Video call</p>
</TooltipContent>
</Tooltip>
{/* Info */}
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="ghost"
size="icon"
onClick={onToggleInfo}
className="cursor-pointer"
>
<Info className="h-4 w-4" />
</Button>
</TooltipTrigger>
<TooltipContent>
<p>Conversation info</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
{/* More options */}
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" size="icon" className="cursor-pointer">
<MoreVertical className="h-4 w-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem
onClick={onToggleMute}
className="cursor-pointer"
>
{conversation.isMuted ? (
<>
<Bell className="h-4 w-4 mr-2" />
Unmute conversation
</>
) : (
<>
<BellOff className="h-4 w-4 mr-2" />
Mute conversation
</>
)}
</DropdownMenuItem>
<DropdownMenuItem className="cursor-pointer">
<Search className="h-4 w-4 mr-2" />
Search messages
</DropdownMenuItem>
{conversation.type === "group" && (
<>
<DropdownMenuSeparator />
<DropdownMenuItem className="cursor-pointer">
<Users className="h-4 w-4 mr-2" />
Manage members
</DropdownMenuItem>
</>
)}
<DropdownMenuSeparator />
<DropdownMenuItem className="cursor-pointer text-destructive">
Delete conversation
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
</div>
)
}

View File

@@ -0,0 +1,186 @@
"use client"
import { useEffect, useState } from "react"
import { Menu, X } from "lucide-react"
import { TooltipProvider } from "@/components/ui/tooltip"
import { Button } from "@/components/ui/button"
import { ConversationList } from "./conversation-list"
import { ChatHeader } from "./chat-header"
import { MessageList } from "./message-list"
import { MessageInput } from "./message-input"
import { useChat, type Conversation, type Message, type User } from "@/app/chat/use-chat"
interface ChatProps {
conversations: Conversation[]
messages: Record<string, Message[]>
users: User[]
}
export function Chat({
conversations,
messages,
users,
}: ChatProps) {
const {
selectedConversation,
setSelectedConversation,
setConversations,
setMessages,
setUsers,
addMessage,
toggleMute,
} = useChat()
const [isSidebarOpen, setIsSidebarOpen] = useState(false)
// Close sidebar when clicking outside on mobile
useEffect(() => {
const handleResize = () => {
if (window.innerWidth >= 1024) { // lg breakpoint
setIsSidebarOpen(false)
}
}
window.addEventListener('resize', handleResize)
return () => window.removeEventListener('resize', handleResize)
}, [])
// Initialize data
useEffect(() => {
setConversations(conversations)
setUsers(users)
// Set messages for all conversations
Object.entries(messages).forEach(([conversationId, conversationMessages]) => {
setMessages(conversationId, conversationMessages)
})
// Auto-select first conversation if none selected
if (!selectedConversation && conversations.length > 0) {
setSelectedConversation(conversations[0].id)
}
}, [conversations, messages, users, selectedConversation, setConversations, setMessages, setUsers, setSelectedConversation])
const currentConversation = conversations.find(conv => conv.id === selectedConversation)
const currentMessages = selectedConversation ? messages[selectedConversation] || [] : []
const handleSendMessage = (content: string) => {
if (!selectedConversation) return
const newMessage = {
id: `msg-${Date.now()}`,
content,
timestamp: new Date().toISOString(),
senderId: "current-user",
type: "text" as const,
isEdited: false,
reactions: [],
replyTo: null,
}
addMessage(selectedConversation, newMessage)
}
const handleToggleMute = () => {
if (selectedConversation) {
toggleMute(selectedConversation)
}
}
return (
<TooltipProvider delayDuration={0}>
<div className="h-full min-h-[600px] max-h-[calc(100vh-200px)] flex rounded-lg border overflow-hidden bg-background">
{/* Mobile Sidebar Overlay */}
{isSidebarOpen && (
<div
className="fixed inset-0 bg-black/50 z-40 lg:hidden"
onClick={() => setIsSidebarOpen(false)}
/>
)}
{/* Conversations Sidebar - Responsive */}
<div className={`
w-100 border-r bg-background flex-shrink-0
${isSidebarOpen ? 'translate-x-0' : '-translate-x-full lg:translate-x-0'}
lg:relative lg:block
fixed inset-y-0 left-0 z-50
transition-transform duration-300 ease-in-out
`}>
{/* Sidebar Header with Close Button (Mobile Only) */}
<div className="lg:hidden p-4 border-b flex items-center justify-between bg-background">
<h2 className="text-lg font-semibold">Messages</h2>
<Button
variant="ghost"
size="sm"
onClick={() => setIsSidebarOpen(false)}
className="cursor-pointer"
>
<X className="h-4 w-4" />
</Button>
</div>
<ConversationList
conversations={conversations}
selectedConversation={selectedConversation}
onSelectConversation={(id) => {
setSelectedConversation(id)
setIsSidebarOpen(false) // Close sidebar on mobile after selection
}}
/>
</div>
{/* Chat Panel - Flexible Width */}
<div className="flex-1 flex flex-col min-w-0 bg-background">
{/* Chat Header with Hamburger Menu */}
<div className="flex items-center h-16 px-4 border-b bg-background">
{/* Hamburger Menu Button - Only visible when sidebar is hidden on mobile */}
<Button
variant="ghost"
size="sm"
onClick={() => setIsSidebarOpen(true)}
className="cursor-pointer lg:hidden mr-2"
>
<Menu className="h-4 w-4" />
</Button>
<div className="flex-1">
<ChatHeader
conversation={currentConversation || null}
users={users}
onToggleMute={handleToggleMute}
/>
</div>
</div>
{/* Messages */}
<div className="flex-1 flex flex-col min-h-0">
{selectedConversation ? (
<>
<MessageList
messages={currentMessages}
users={users}
/>
{/* Message Input */}
<MessageInput
onSendMessage={handleSendMessage}
placeholder={`Message ${currentConversation?.name || ""}...`}
/>
</>
) : (
<div className="flex-1 flex items-center justify-center">
<div className="text-center">
<h3 className="text-lg font-semibold mb-2">Welcome to Chat</h3>
<p className="text-muted-foreground">
Select a conversation to start messaging
</p>
</div>
</div>
)}
</div>
</div>
</div>
</TooltipProvider>
)
}

View File

@@ -0,0 +1,221 @@
"use client"
import { format, isToday, isYesterday, isThisWeek, isThisYear } from "date-fns"
import {
Search,
Pin,
VolumeX,
MoreHorizontal,
Users,
Hash
} from "lucide-react"
import { cn } from "@/lib/utils"
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"
import { Badge } from "@/components/ui/badge"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { ScrollArea } from "@/components/ui/scroll-area"
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuSeparator,
DropdownMenuTrigger
} from "@/components/ui/dropdown-menu"
import { useChat, type Conversation } from "@/app/chat/use-chat"
interface ConversationListProps {
conversations: Conversation[]
selectedConversation: string | null
onSelectConversation: (conversationId: string) => void
}
// Enhanced time formatting function
function formatMessageTime(timestamp: string): string {
const date = new Date(timestamp)
if (isToday(date)) {
return format(date, 'h:mm a') // 3:30 PM
} else if (isYesterday(date)) {
return 'Yesterday'
} else if (isThisWeek(date)) {
return format(date, 'EEEE') // Day name
} else if (isThisYear(date)) {
return format(date, 'MMM d') // Jan 15
} else {
return format(date, 'dd/MM/yy') // 15/01/24
}
}
export function ConversationList({
conversations,
selectedConversation,
onSelectConversation
}: ConversationListProps) {
const { searchQuery, setSearchQuery, togglePin, toggleMute } = useChat()
const filteredConversations = conversations.filter((conversation) =>
conversation.name.toLowerCase().includes(searchQuery.toLowerCase())
)
const sortedConversations = filteredConversations.sort((a, b) => {
// Pinned conversations first
if (a.isPinned && !b.isPinned) return -1
if (!a.isPinned && b.isPinned) return 1
// Then by last message timestamp
return new Date(b.lastMessage.timestamp).getTime() - new Date(a.lastMessage.timestamp).getTime()
})
const getOnlineStatus = (conversation: Conversation) => {
if (conversation.type === "direct" && conversation.participants.length === 1) {
// In a real app, you'd check user online status
return Math.random() > 0.5 // Mock online status
}
return false
}
return (
<div className="flex flex-col h-full overflow-hidden">
{/* Header */}
<div className="flex items-center justify-between p-4 border-b flex-shrink-0">
<h2 className="text-lg font-semibold">Messages</h2>
</div>
{/* Search */}
<div className="p-4 border-b flex-shrink-0">
<div className="relative">
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-muted-foreground" />
<Input
type="text"
placeholder="Search conversations..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="pl-9 cursor-text"
/>
</div>
</div>
{/* Conversations */}
<ScrollArea className="flex-1">
<div className="p-2">
{sortedConversations.map((conversation) => (
<div
key={conversation.id}
className={cn(
"flex items-center gap-3 p-3 rounded-lg cursor-pointer relative group overflow-hidden hover:bg-accent/50 transition-colors",
selectedConversation === conversation.id
? "bg-accent text-accent-foreground"
: ""
)}
onClick={() => onSelectConversation(conversation.id)}
>
{/* Avatar with online indicator */}
<div className="relative flex-shrink-0">
<Avatar className={cn(
"h-12 w-12",
selectedConversation === conversation.id && "ring-2 ring-background"
)}>
<AvatarImage src={conversation.avatar} alt={conversation.name} />
<AvatarFallback className="text-sm">
{conversation.type === "group" ? (
<Users className="h-5 w-5" />
) : (
conversation.name.split(' ').map(n => n[0]).join('').slice(0, 2)
)}
</AvatarFallback>
</Avatar>
{/* Online indicator for direct messages */}
{conversation.type === "direct" && getOnlineStatus(conversation) && (
<div className="absolute -bottom-1 -right-1 h-4 w-4 bg-green-500 border-2 border-background rounded-full" />
)}
{/* Group indicator */}
{conversation.type === "group" && (
<div className="absolute -bottom-1 -right-1 h-4 w-4 bg-blue-500 border-2 border-background rounded-full flex items-center justify-center">
<Hash className="h-2 w-2 text-white" />
</div>
)}
</div>
{/* Content */}
<div className="flex-1 min-w-0 overflow-hidden">
<div className="flex items-center justify-between mb-1 min-w-0">
<div className="flex items-center gap-1 min-w-0 flex-1 overflow-hidden">
<h3 className="font-medium truncate min-w-0 max-w-[180px]">{conversation.name}</h3>
{conversation.isPinned && (
<Pin className="h-3 w-3 text-muted-foreground flex-shrink-0" />
)}
{conversation.isMuted && (
<VolumeX className="h-3 w-3 text-muted-foreground flex-shrink-0" />
)}
</div>
<span className="text-xs text-muted-foreground flex-shrink-0 ml-2 whitespace-nowrap">
{formatMessageTime(conversation.lastMessage.timestamp)}
</span>
</div>
<div className="flex items-center justify-between gap-2 min-w-0">
<p className="text-sm text-muted-foreground truncate flex-1 min-w-0 max-w-[200px]">
{conversation.lastMessage.content}
</p>
{/* Unread count */}
{conversation.unreadCount > 0 && (
<Badge variant="default" className="ml-2 min-w-[20px] h-5 text-xs cursor-pointer flex-shrink-0">
{conversation.unreadCount > 99 ? "99+" : conversation.unreadCount}
</Badge>
)}
</div>
</div>
{/* Actions menu */}
<div className="opacity-0 group-hover:opacity-100 ml-2 flex-shrink-0">
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
variant="ghost"
size="sm"
className="h-8 w-8 p-0 cursor-pointer"
onClick={(e) => e.stopPropagation()}
>
<MoreHorizontal className="h-4 w-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem
onClick={(e) => {
e.stopPropagation()
togglePin(conversation.id)
}}
className="cursor-pointer"
>
<Pin className="h-4 w-4 mr-2" />
{conversation.isPinned ? "Unpin" : "Pin"}
</DropdownMenuItem>
<DropdownMenuItem
onClick={(e) => {
e.stopPropagation()
toggleMute(conversation.id)
}}
className="cursor-pointer"
>
<VolumeX className="h-4 w-4 mr-2" />
{conversation.isMuted ? "Unmute" : "Mute"}
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem className="cursor-pointer text-destructive">
Delete conversation
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
</div>
))}
</div>
</ScrollArea>
</div>
)
}

View File

@@ -0,0 +1,208 @@
"use client"
import { format, isToday, isYesterday, isThisWeek, isThisYear } from "date-fns"
import {
Search,
Pin,
VolumeX,
MoreVertical,
Users,
Hash,
Settings,
UserPlus,
Filter
} from "lucide-react"
import { cn } from "@/lib/utils"
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"
import { Badge } from "@/components/ui/badge"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { ScrollArea } from "@/components/ui/scroll-area"
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuSeparator,
DropdownMenuTrigger
} from "@/components/ui/dropdown-menu"
import { useChat, type Conversation } from "@/app/chat/use-chat"
interface ConversationListProps {
conversations: Conversation[]
selectedConversation: string | null
onSelectConversation: (conversationId: string) => void
}
// Enhanced time formatting function
function formatMessageTime(timestamp: string): string {
const date = new Date(timestamp)
if (isToday(date)) {
return format(date, 'h:mm a') // 3:30 PM
} else if (isYesterday(date)) {
return 'Yesterday'
} else if (isThisWeek(date)) {
return format(date, 'EEEE') // Day name
} else if (isThisYear(date)) {
return format(date, 'MMM d') // Jan 15
} else {
return format(date, 'dd/MM/yy') // 15/01/24
}
}
export function ConversationList({
conversations,
selectedConversation,
onSelectConversation
}: ConversationListProps) {
const { searchQuery, setSearchQuery } = useChat()
const filteredConversations = conversations.filter((conversation) =>
conversation.name.toLowerCase().includes(searchQuery.toLowerCase())
)
const sortedConversations = filteredConversations.sort((a, b) => {
// Pinned conversations first
if (a.isPinned && !b.isPinned) return -1
if (!a.isPinned && b.isPinned) return 1
// Then by last message timestamp
return new Date(b.lastMessage.timestamp).getTime() - new Date(a.lastMessage.timestamp).getTime()
})
const getOnlineStatus = (conversation: Conversation) => {
if (conversation.type === "direct" && conversation.participants.length === 1) {
// In a real app, you'd check user online status
return Math.random() > 0.5 // Mock online status
}
return false
}
return (
<div className="flex flex-col h-full overflow-hidden">
{/* Header - Hidden on mobile (handled by parent) */}
<div className="hidden lg:flex items-center justify-between h-16 px-4 border-b flex-shrink-0">
<h2 className="text-lg font-semibold">Messages</h2>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
variant="ghost"
size="sm"
className="h-8 w-8 p-0 cursor-pointer"
>
<MoreVertical className="h-4 w-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem className="cursor-pointer">
<UserPlus className="h-4 w-4 mr-2" />
New Chat
</DropdownMenuItem>
<DropdownMenuItem className="cursor-pointer">
<Filter className="h-4 w-4 mr-2" />
Filter Messages
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem className="cursor-pointer">
<Settings className="h-4 w-4 mr-2" />
Chat Settings
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
{/* Search */}
<div className="px-4 py-3 border-b flex-shrink-0">
<div className="relative">
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-muted-foreground" />
<Input
type="text"
placeholder="Search conversations..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="pl-9 cursor-text"
/>
</div>
</div>
{/* Conversations */}
<ScrollArea className="flex-1">
<div className="p-2">
{sortedConversations.map((conversation) => (
<div
key={conversation.id}
className={cn(
"flex items-center gap-3 p-3 rounded-lg cursor-pointer relative overflow-hidden hover:bg-accent/50 transition-colors",
selectedConversation === conversation.id
? "bg-accent text-accent-foreground"
: ""
)}
onClick={() => onSelectConversation(conversation.id)}
>
{/* Avatar with online indicator */}
<div className="relative flex-shrink-0">
<Avatar className={cn(
"h-12 w-12",
selectedConversation === conversation.id && "ring-2 ring-background"
)}>
<AvatarImage src={conversation.avatar} alt={conversation.name} />
<AvatarFallback className="text-sm">
{conversation.type === "group" ? (
<Users className="h-5 w-5" />
) : (
conversation.name.split(' ').map(n => n[0]).join('').slice(0, 2)
)}
</AvatarFallback>
</Avatar>
{/* Online indicator for direct messages */}
{conversation.type === "direct" && getOnlineStatus(conversation) && (
<div className="absolute -bottom-1 -right-1 h-4 w-4 bg-green-500 border-2 border-background rounded-full" />
)}
{/* Group indicator */}
{conversation.type === "group" && (
<div className="absolute -bottom-1 -right-1 h-4 w-4 bg-blue-500 border-2 border-background rounded-full flex items-center justify-center">
<Hash className="h-2 w-2 text-white" />
</div>
)}
</div>
{/* Content */}
<div className="flex-1 min-w-0 overflow-hidden">
<div className="flex items-center justify-between mb-1 min-w-0">
<div className="flex items-center gap-1 min-w-0 flex-1 overflow-hidden pr-2">
<h3 className="font-medium truncate min-w-0 max-w-[160px] lg:max-w-[180px]">{conversation.name}</h3>
{conversation.isPinned && (
<Pin className="h-3 w-3 text-muted-foreground flex-shrink-0" />
)}
{conversation.isMuted && (
<VolumeX className="h-3 w-3 text-muted-foreground flex-shrink-0" />
)}
</div>
<span className="text-xs text-muted-foreground flex-shrink-0 whitespace-nowrap">
{formatMessageTime(conversation.lastMessage.timestamp)}
</span>
</div>
<div className="flex items-center justify-between gap-2 min-w-0">
<p className="text-sm text-muted-foreground truncate flex-1 min-w-0 max-w-[180px] lg:max-w-[200px] pr-2">
{conversation.lastMessage.content}
</p>
{/* Unread count */}
{conversation.unreadCount > 0 && (
<Badge variant="default" className="min-w-[20px] h-5 text-xs cursor-pointer flex-shrink-0">
{conversation.unreadCount > 99 ? "99+" : conversation.unreadCount}
</Badge>
)}
</div>
</div>
</div>
))}
</div>
</ScrollArea>
</div>
)
}

View File

@@ -0,0 +1,225 @@
"use client"
import { useState, useRef } from "react"
import {
Send,
Paperclip,
Smile,
Image as ImageIcon,
FileText,
Mic,
MoreHorizontal
} from "lucide-react"
import { cn } from "@/lib/utils"
import { Button } from "@/components/ui/button"
import { Textarea } from "@/components/ui/textarea"
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger
} from "@/components/ui/dropdown-menu"
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger
} from "@/components/ui/tooltip"
interface MessageInputProps {
onSendMessage: (content: string) => void
disabled?: boolean
placeholder?: string
}
export function MessageInput({
onSendMessage,
disabled = false,
placeholder = "Type a message..."
}: MessageInputProps) {
const [message, setMessage] = useState("")
const [isTyping, setIsTyping] = useState(false)
const textareaRef = useRef<HTMLTextAreaElement>(null)
const handleSendMessage = () => {
const trimmedMessage = message.trim()
if (trimmedMessage && !disabled) {
onSendMessage(trimmedMessage)
setMessage("")
setIsTyping(false)
// Reset textarea height
if (textareaRef.current) {
textareaRef.current.style.height = "auto"
}
}
}
const handleKeyPress = (e: React.KeyboardEvent) => {
if (e.key === "Enter" && !e.shiftKey) {
e.preventDefault()
handleSendMessage()
}
}
const handleTextareaChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
const value = e.target.value
setMessage(value)
// Auto-resize textarea
if (textareaRef.current) {
textareaRef.current.style.height = "auto"
textareaRef.current.style.height = `${Math.min(textareaRef.current.scrollHeight, 120)}px`
}
// Handle typing indicator
if (value.trim() && !isTyping) {
setIsTyping(true)
} else if (!value.trim() && isTyping) {
setIsTyping(false)
}
}
const handleFileUpload = (type: "image" | "file") => {
// In a real app, this would open a file picker
console.log(`Upload ${type}`)
}
return (
<div className="border-t p-4">
<div className="flex items-end gap-2">
{/* Attachment button */}
<TooltipProvider>
<DropdownMenu>
<Tooltip>
<TooltipTrigger asChild>
<DropdownMenuTrigger asChild>
<Button
variant="ghost"
size="icon"
disabled={disabled}
className="cursor-pointer disabled:cursor-not-allowed"
>
<Paperclip className="h-4 w-4" />
</Button>
</DropdownMenuTrigger>
</TooltipTrigger>
<TooltipContent>
<p>Attach file</p>
</TooltipContent>
</Tooltip>
<DropdownMenuContent side="top" align="start">
<DropdownMenuItem
onClick={() => handleFileUpload("image")}
className="cursor-pointer"
>
<ImageIcon className="h-4 w-4 mr-2" />
Photo or video
</DropdownMenuItem>
<DropdownMenuItem
onClick={() => handleFileUpload("file")}
className="cursor-pointer"
>
<FileText className="h-4 w-4 mr-2" />
Document
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</TooltipProvider>
{/* Message input */}
<div className="flex-1 relative">
<Textarea
ref={textareaRef}
placeholder={placeholder}
value={message}
onChange={handleTextareaChange}
onKeyDown={handleKeyPress}
disabled={disabled}
className={cn(
"min-h-[40px] max-h-[120px] resize-none cursor-text disabled:cursor-not-allowed",
"pr-20" // Space for emoji and more buttons
)}
rows={1}
/>
{/* Input action buttons */}
<div className="absolute right-2 bottom-2 flex items-center gap-1">
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="ghost"
size="sm"
disabled={disabled}
className="h-6 w-6 p-0 cursor-pointer disabled:cursor-not-allowed"
>
<Smile className="h-4 w-4" />
</Button>
</TooltipTrigger>
<TooltipContent>
<p>Add emoji</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="ghost"
size="sm"
disabled={disabled}
className="h-6 w-6 p-0 cursor-pointer disabled:cursor-not-allowed"
>
<MoreHorizontal className="h-4 w-4" />
</Button>
</TooltipTrigger>
<TooltipContent>
<p>More options</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</div>
</div>
{/* Voice message or send button */}
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
{message.trim() ? (
<Button
onClick={handleSendMessage}
disabled={disabled}
className="cursor-pointer disabled:cursor-not-allowed"
>
<Send className="h-4 w-4" />
</Button>
) : (
<Button
variant="ghost"
size="icon"
disabled={disabled}
className="cursor-pointer disabled:cursor-not-allowed"
>
<Mic className="h-4 w-4" />
</Button>
)}
</TooltipTrigger>
<TooltipContent>
<p>{message.trim() ? "Send message" : "Voice message"}</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</div>
{/* Typing indicator */}
{isTyping && (
<div className="text-xs text-muted-foreground mt-2">
You are typing...
</div>
)}
</div>
)
}

View File

@@ -0,0 +1,296 @@
"use client"
import { useEffect, useRef } from "react"
import { format, isToday, isYesterday } from "date-fns"
import { CheckCheck, MoreHorizontal, Reply, Copy, Trash2 } from "lucide-react"
import { cn } from "@/lib/utils"
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"
import { Button } from "@/components/ui/button"
import { ScrollArea } from "@/components/ui/scroll-area"
import { assetUrl } from "@/lib/utils"
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuSeparator,
DropdownMenuTrigger
} from "@/components/ui/dropdown-menu"
import { type Message, type User } from "@/app/chat/use-chat"
interface MessageListProps {
messages: Message[]
users: User[]
currentUserId?: string
}
export function MessageList({ messages, users, currentUserId = "current-user" }: MessageListProps) {
const scrollAreaRef = useRef<HTMLDivElement>(null)
const bottomRef = useRef<HTMLDivElement>(null)
const previousMessageCountRef = useRef(0)
const isInitialLoadRef = useRef(true)
const previousConversationRef = useRef<string | null>(null)
// Reset scroll behavior when switching conversations
useEffect(() => {
const currentConversationId = messages.length > 0 ? messages[0]?.id?.split('-')[0] : null
if (currentConversationId !== previousConversationRef.current) {
isInitialLoadRef.current = true
previousConversationRef.current = currentConversationId
}
}, [messages])
// Auto-scroll to bottom only when new messages are added (not on initial load)
useEffect(() => {
// Skip auto-scroll on initial load
if (isInitialLoadRef.current) {
isInitialLoadRef.current = false
previousMessageCountRef.current = messages.length
return
}
// Only auto-scroll if new messages were added
if (messages.length > previousMessageCountRef.current && bottomRef.current) {
bottomRef.current.scrollIntoView({ behavior: "smooth" })
}
previousMessageCountRef.current = messages.length
}, [messages])
const getUserById = (userId: string) => {
if (userId === currentUserId) {
return {
id: currentUserId,
name: "You",
avatar: assetUrl("avatars/current-user.png"),
status: "online" as const,
email: "you@example.com",
lastSeen: new Date().toISOString(),
role: "Developer",
department: "Engineering"
}
}
return users.find(user => user.id === userId)
}
const formatMessageTime = (timestamp: string) => {
const date = new Date(timestamp)
if (isToday(date)) {
return format(date, "HH:mm")
} else if (isYesterday(date)) {
return `Yesterday ${format(date, "HH:mm")}`
} else {
return format(date, "MMM d, HH:mm")
}
}
const shouldShowAvatar = (message: Message, index: number) => {
if (message.senderId === currentUserId) return false
if (index === 0) return true
const prevMessage = messages[index - 1]
return prevMessage.senderId !== message.senderId
}
const shouldShowName = (message: Message, index: number) => {
if (message.senderId === currentUserId) return false
if (index === 0) return true
const prevMessage = messages[index - 1]
return prevMessage.senderId !== message.senderId
}
const isConsecutiveMessage = (message: Message, index: number) => {
if (index === 0) return false
const prevMessage = messages[index - 1]
const timeDiff = new Date(message.timestamp).getTime() - new Date(prevMessage.timestamp).getTime()
return prevMessage.senderId === message.senderId && timeDiff < 5 * 60 * 1000 // 5 minutes
}
const groupMessagesByDay = (messages: Message[]) => {
const groups: { date: string; messages: Message[] }[] = []
messages.forEach((message) => {
const messageDate = format(new Date(message.timestamp), "yyyy-MM-dd")
const lastGroup = groups[groups.length - 1]
if (lastGroup && lastGroup.date === messageDate) {
lastGroup.messages.push(message)
} else {
groups.push({
date: messageDate,
messages: [message]
})
}
})
return groups
}
const formatDateHeader = (dateString: string) => {
const date = new Date(dateString)
if (isToday(date)) {
return "Today"
} else if (isYesterday(date)) {
return "Yesterday"
} else {
return format(date, "EEEE, MMMM d")
}
}
const messageGroups = groupMessagesByDay(messages)
return (
<ScrollArea className="flex-1 px-4" ref={scrollAreaRef}>
<div className="space-y-4 py-4">
{messageGroups.map((group) => (
<div key={group.date}>
{/* Date separator */}
<div className="flex items-center justify-center py-2">
<div className="text-xs text-muted-foreground bg-background px-3 py-1 rounded-full border">
{formatDateHeader(group.date)}
</div>
</div>
{/* Messages for this day */}
<div className="space-y-1">
{group.messages.map((message, messageIndex) => {
const user = getUserById(message.senderId)
const isOwnMessage = message.senderId === currentUserId
const showAvatar = shouldShowAvatar(message, messageIndex)
const showName = shouldShowName(message, messageIndex)
const isConsecutive = isConsecutiveMessage(message, messageIndex)
return (
<div
key={message.id}
className={cn(
"flex gap-3 group",
isOwnMessage && "flex-row-reverse",
isConsecutive && !isOwnMessage && "ml-12"
)}
>
{/* Avatar */}
{!isOwnMessage && (
<div className="w-8">
{showAvatar && user && (
<Avatar className="h-8 w-8 cursor-pointer">
<AvatarImage src={user.avatar} alt={user.name} />
<AvatarFallback className="text-xs">
{user.name.split(' ').map(n => n[0]).join('').slice(0, 2)}
</AvatarFallback>
</Avatar>
)}
</div>
)}
{/* Message content */}
<div className={cn("flex-1 max-w-[70%]", isOwnMessage && "flex flex-col items-end")}>
{/* Sender name for group messages */}
{showName && user && !isOwnMessage && (
<div className="text-sm font-medium text-foreground mb-1">
{user.name}
</div>
)}
{/* Message bubble */}
<div className="relative group/message">
<div
className={cn(
"rounded-lg px-3 py-2 text-sm break-words",
isOwnMessage
? "bg-primary text-primary-foreground"
: "bg-muted",
isConsecutive && "mt-1"
)}
>
<p>{message.content}</p>
{/* Message reactions */}
{message.reactions.length > 0 && (
<div className="flex gap-1 mt-2">
{message.reactions.map((reaction, idx) => (
<div
key={idx}
className={cn(
"inline-flex items-center gap-1 px-2 py-1 rounded-full text-xs border cursor-pointer",
"bg-background/90 backdrop-blur-sm shadow-sm"
)}
>
<span>{reaction.emoji}</span>
<span className="text-muted-foreground">{reaction.count}</span>
</div>
))}
</div>
)}
{/* Timestamp and status */}
<div className={cn(
"flex items-center gap-1 mt-1 text-xs",
isOwnMessage
? "text-primary-foreground/70 justify-end"
: "text-muted-foreground"
)}>
<span>{formatMessageTime(message.timestamp)}</span>
{message.isEdited && (
<span className="italic">(edited)</span>
)}
{isOwnMessage && (
<div className="flex">
{/* Message status indicators */}
<CheckCheck className="h-3 w-3" />
</div>
)}
</div>
</div>
{/* Message actions */}
<div className="absolute top-0 right-0 opacity-0 group-hover/message:opacity-100">
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
variant="ghost"
size="sm"
className="h-6 w-6 p-0 cursor-pointer"
>
<MoreHorizontal className="h-3 w-3" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem className="cursor-pointer">
<Reply className="h-4 w-4 mr-2" />
Reply
</DropdownMenuItem>
<DropdownMenuItem className="cursor-pointer">
<Copy className="h-4 w-4 mr-2" />
Copy
</DropdownMenuItem>
{isOwnMessage && (
<>
<DropdownMenuSeparator />
<DropdownMenuItem className="cursor-pointer text-destructive">
<Trash2 className="h-4 w-4 mr-2" />
Delete
</DropdownMenuItem>
</>
)}
</DropdownMenuContent>
</DropdownMenu>
</div>
</div>
</div>
</div>
)
})}
</div>
</div>
))}
{/* Scroll anchor */}
<div ref={bottomRef} />
</div>
</ScrollArea>
)
}

View File

@@ -0,0 +1,98 @@
[
{
"id": "conv-1",
"type": "direct",
"participants": ["1"],
"name": "Sarah Mitchell",
"avatar": "/avatars/01.png",
"lastMessage": {
"id": "msg-1-4",
"content": "Thanks for the quick update! The dashboard looks amazing 🎉",
"timestamp": "2025-08-11T15:30:00Z",
"senderId": "1"
},
"unreadCount": 2,
"isPinned": true,
"isMuted": false
},
{
"id": "conv-2",
"type": "group",
"participants": ["2", "3", "5"],
"name": "Project Alpha",
"avatar": "/avatars/team-alpha.png",
"lastMessage": {
"id": "msg-2-8",
"content": "David: Marketing campaign is scheduled for next week",
"timestamp": "2025-08-11T08:15:00Z",
"senderId": "2"
},
"unreadCount": 0,
"isPinned": false,
"isMuted": false
},
{
"id": "conv-3",
"type": "group",
"participants": ["2", "3", "5"],
"name": "Frontend Team",
"avatar": "/avatars/team-frontend.png",
"lastMessage": {
"id": "msg-3-6",
"content": "Alex: The new component library is ready for testing",
"timestamp": "2025-08-11T23:45:00Z",
"senderId": "3"
},
"unreadCount": 1,
"isPinned": false,
"isMuted": false
},
{
"id": "conv-4",
"type": "direct",
"participants": ["3"],
"name": "Emily Rodriguez",
"avatar": "/avatars/03.png",
"lastMessage": {
"id": "msg-4-3",
"content": "Let's review the wireframes together tomorrow",
"timestamp": "2025-08-10T16:30:00Z",
"senderId": "3"
},
"unreadCount": 1,
"isPinned": false,
"isMuted": false
},
{
"id": "conv-5",
"type": "direct",
"participants": ["5"],
"name": "Lisa Chen",
"avatar": "/avatars/05.png",
"lastMessage": {
"id": "msg-5-3",
"content": "Found a few edge cases in the new feature",
"timestamp": "2025-08-06T14:20:00Z",
"senderId": "5"
},
"unreadCount": 0,
"isPinned": false,
"isMuted": true
},
{
"id": "conv-6",
"type": "direct",
"participants": ["2"],
"name": "Alex Thompson",
"avatar": "/avatars/02.png",
"lastMessage": {
"id": "msg-6-3",
"content": "Code review completed, looks good to merge! 👍",
"timestamp": "2025-01-15T17:45:00Z",
"senderId": "2"
},
"unreadCount": 0,
"isPinned": false,
"isMuted": false
}
]

View File

@@ -0,0 +1,224 @@
{
"conv-1": [
{
"id": "msg-1-1",
"content": "Hey! How's the new dashboard coming along?",
"timestamp": "2024-01-15T10:15:00Z",
"senderId": "1",
"type": "text",
"isEdited": false,
"reactions": [],
"replyTo": null
},
{
"id": "msg-1-2",
"content": "It's going great! We've implemented the new design system and it looks fantastic.",
"timestamp": "2024-01-15T10:17:00Z",
"senderId": "current-user",
"type": "text",
"isEdited": false,
"reactions": [{"emoji": "👍", "users": ["1"], "count": 1}],
"replyTo": null
},
{
"id": "msg-1-3",
"content": "That's awesome! Can you share a preview?",
"timestamp": "2024-01-15T10:18:00Z",
"senderId": "1",
"type": "text",
"isEdited": false,
"reactions": [],
"replyTo": null
},
{
"id": "msg-1-4",
"content": "Thanks for the quick update! The dashboard looks amazing 🎉",
"timestamp": "2024-01-15T10:30:00Z",
"senderId": "1",
"type": "text",
"isEdited": false,
"reactions": [{"emoji": "❤️", "users": ["current-user"], "count": 1}],
"replyTo": null
}
],
"conv-2": [
{
"id": "msg-2-1",
"content": "Hey team! The component library update is ready",
"timestamp": "2024-01-15T09:00:00Z",
"senderId": "2",
"type": "text",
"isEdited": false,
"reactions": [],
"replyTo": null
},
{
"id": "msg-2-2",
"content": "Awesome work Alex! 🚀",
"timestamp": "2024-01-15T09:05:00Z",
"senderId": "3",
"type": "text",
"isEdited": false,
"reactions": [],
"replyTo": null
},
{
"id": "msg-2-3",
"content": "I've tested the new Button and Input components, they work perfectly",
"timestamp": "2024-01-15T09:10:00Z",
"senderId": "5",
"type": "text",
"isEdited": false,
"reactions": [{"emoji": "✅", "users": ["2", "3"], "count": 2}],
"replyTo": null
},
{
"id": "msg-2-4",
"content": "Great! I'll start integrating them into the main app",
"timestamp": "2024-01-15T09:15:00Z",
"senderId": "current-user",
"type": "text",
"isEdited": false,
"reactions": [],
"replyTo": null
}
],
"conv-3": [
{
"id": "msg-3-1",
"content": "Hi! I've completed the wireframes for the new user onboarding flow",
"timestamp": "2024-01-15T09:30:00Z",
"senderId": "3",
"type": "text",
"isEdited": false,
"reactions": [],
"replyTo": null
},
{
"id": "msg-3-2",
"content": "That's fantastic Emily! When can we review them?",
"timestamp": "2024-01-15T09:32:00Z",
"senderId": "current-user",
"type": "text",
"isEdited": false,
"reactions": [],
"replyTo": null
},
{
"id": "msg-3-3",
"content": "How about tomorrow at 2 PM? I'll share my screen and walk through the designs",
"timestamp": "2024-01-15T09:35:00Z",
"senderId": "3",
"type": "text",
"isEdited": false,
"reactions": [{"emoji": "👍", "users": ["current-user"], "count": 1}],
"replyTo": null
},
{
"id": "msg-3-4",
"content": "Perfect! Looking forward to it",
"timestamp": "2024-01-15T09:40:00Z",
"senderId": "current-user",
"type": "text",
"isEdited": false,
"reactions": [],
"replyTo": null
}
],
"conv-4": [
{
"id": "msg-4-1",
"content": "Hi! I've been working on the wireframes for the new feature",
"timestamp": "2025-08-10T14:15:00Z",
"senderId": "3",
"type": "text",
"isEdited": false,
"reactions": [],
"replyTo": null
},
{
"id": "msg-4-2",
"content": "That's great! I'd love to take a look at them",
"timestamp": "2025-08-10T14:18:00Z",
"senderId": "current-user",
"type": "text",
"isEdited": false,
"reactions": [],
"replyTo": null
},
{
"id": "msg-4-3",
"content": "Let's review the wireframes together tomorrow",
"timestamp": "2025-08-10T16:30:00Z",
"senderId": "3",
"type": "text",
"isEdited": false,
"reactions": [{"emoji": "👍", "users": ["current-user"], "count": 1}],
"replyTo": null
}
],
"conv-5": [
{
"id": "msg-5-1",
"content": "I've been testing the new feature and it looks good overall",
"timestamp": "2025-08-06T13:45:00Z",
"senderId": "5",
"type": "text",
"isEdited": false,
"reactions": [],
"replyTo": null
},
{
"id": "msg-5-2",
"content": "Thanks for testing it! Any issues you found?",
"timestamp": "2025-08-06T14:10:00Z",
"senderId": "current-user",
"type": "text",
"isEdited": false,
"reactions": [],
"replyTo": null
},
{
"id": "msg-5-3",
"content": "Found a few edge cases in the new feature",
"timestamp": "2025-08-06T14:20:00Z",
"senderId": "5",
"type": "text",
"isEdited": false,
"reactions": [],
"replyTo": null
}
],
"conv-6": [
{
"id": "msg-6-1",
"content": "Hey! I've finished the code review for the latest PR",
"timestamp": "2025-01-15T16:30:00Z",
"senderId": "2",
"type": "text",
"isEdited": false,
"reactions": [],
"replyTo": null
},
{
"id": "msg-6-2",
"content": "Thanks for the quick review! Any feedback?",
"timestamp": "2025-01-15T17:15:00Z",
"senderId": "current-user",
"type": "text",
"isEdited": false,
"reactions": [],
"replyTo": null
},
{
"id": "msg-6-3",
"content": "Code review completed, looks good to merge! 👍",
"timestamp": "2025-01-15T17:45:00Z",
"senderId": "2",
"type": "text",
"isEdited": false,
"reactions": [{"emoji": "🎉", "users": ["current-user"], "count": 1}],
"replyTo": null
}
]
}

View File

@@ -0,0 +1,52 @@
[
{
"id": "1",
"name": "Sarah Mitchell",
"email": "sarah.mitchell@example.com",
"avatar": "/avatars/01.png",
"status": "online",
"lastSeen": "2024-01-15T10:30:00Z",
"role": "Project Manager",
"department": "Product"
},
{
"id": "2",
"name": "Alex Thompson",
"email": "alex.thompson@example.com",
"avatar": "/avatars/02.png",
"status": "away",
"lastSeen": "2024-01-15T09:45:00Z",
"role": "Senior Developer",
"department": "Engineering"
},
{
"id": "3",
"name": "Emily Rodriguez",
"email": "emily.rodriguez@example.com",
"avatar": "/avatars/03.png",
"status": "online",
"lastSeen": "2024-01-15T10:25:00Z",
"role": "UX Designer",
"department": "Design"
},
{
"id": "4",
"name": "David Kim",
"email": "david.kim@example.com",
"avatar": "/avatars/04.png",
"status": "offline",
"lastSeen": "2024-01-14T18:30:00Z",
"role": "Marketing Lead",
"department": "Marketing"
},
{
"id": "5",
"name": "Lisa Chen",
"email": "lisa.chen@example.com",
"avatar": "/avatars/05.png",
"status": "online",
"lastSeen": "2024-01-15T10:20:00Z",
"role": "QA Engineer",
"department": "Engineering"
}
]

58
src/app/chat/page.tsx Normal file
View File

@@ -0,0 +1,58 @@
"use client"
import { useEffect, useState } from "react"
import { BaseLayout } from "@/components/layouts/base-layout"
import { Chat } from "./components/chat"
import { type Conversation, type Message, type User } from "./use-chat"
// Import static data
import conversationsData from "./data/conversations.json"
import messagesData from "./data/messages.json"
import usersData from "./data/users.json"
export default function ChatPage() {
const [conversations, setConversations] = useState<Conversation[]>([])
const [messages, setMessages] = useState<Record<string, Message[]>>({})
const [users, setUsers] = useState<User[]>([])
const [loading, setLoading] = useState(true)
useEffect(() => {
const loadData = async () => {
try {
// In a real app, these would be API calls
setConversations(conversationsData as Conversation[])
setMessages(messagesData as Record<string, Message[]>)
setUsers(usersData as User[])
} catch (error) {
console.error("Failed to load chat data:", error)
} finally {
setLoading(false)
}
}
loadData()
}, [])
if (loading) {
return (
<BaseLayout title="Chat" description="Team communication and messaging">
<div className="flex items-center justify-center h-96">
<div className="text-muted-foreground">Loading chat...</div>
</div>
</BaseLayout>
)
}
return (
<BaseLayout title="Chat" description="Team communication and messaging">
<div className="px-4 md:px-6">
<Chat
conversations={conversations}
messages={messages}
users={users}
/>
</div>
</BaseLayout>
)
}

149
src/app/chat/use-chat.ts Normal file
View File

@@ -0,0 +1,149 @@
"use client"
import { create } from "zustand"
export interface User {
id: string
name: string
email: string
avatar: string
status: "online" | "away" | "offline"
lastSeen: string
role: string
department: string
}
export interface Message {
id: string
content: string
timestamp: string
senderId: string
type: "text" | "image" | "file"
isEdited: boolean
reactions: Array<{
emoji: string
users: string[]
count: number
}>
replyTo: string | null
}
export interface Conversation {
id: string
type: "direct" | "group"
participants: string[]
name: string
avatar: string
lastMessage: {
id: string
content: string
timestamp: string
senderId: string
}
unreadCount: number
isPinned: boolean
isMuted: boolean
}
interface ChatState {
conversations: Conversation[]
messages: Record<string, Message[]>
users: User[]
selectedConversation: string | null
searchQuery: string
isTyping: Record<string, boolean>
onlineUsers: string[]
}
interface ChatActions {
setConversations: (conversations: Conversation[]) => void
setMessages: (conversationId: string, messages: Message[]) => void
setUsers: (users: User[]) => void
setSelectedConversation: (conversationId: string | null) => void
setSearchQuery: (query: string) => void
addMessage: (conversationId: string, message: Message) => void
markAsRead: (conversationId: string) => void
togglePin: (conversationId: string) => void
toggleMute: (conversationId: string) => void
setTyping: (conversationId: string, isTyping: boolean) => void
setOnlineUsers: (userIds: string[]) => void
}
export const useChat = create<ChatState & ChatActions>((set, get) => ({
// State
conversations: [],
messages: {},
users: [],
selectedConversation: null,
searchQuery: "",
isTyping: {},
onlineUsers: [],
// Actions
setConversations: (conversations) => set({ conversations }),
setMessages: (conversationId, messages) =>
set((state) => ({
messages: { ...state.messages, [conversationId]: messages }
})),
setUsers: (users) => set({ users }),
setSelectedConversation: (conversationId) => {
set({ selectedConversation: conversationId })
if (conversationId) {
get().markAsRead(conversationId)
}
},
setSearchQuery: (query) => set({ searchQuery: query }),
addMessage: (conversationId, message) =>
set((state) => ({
messages: {
...state.messages,
[conversationId]: [...(state.messages[conversationId] || []), message]
},
conversations: state.conversations.map((conv) =>
conv.id === conversationId
? {
...conv,
lastMessage: {
id: message.id,
content: message.content,
timestamp: message.timestamp,
senderId: message.senderId
}
}
: conv
)
})),
markAsRead: (conversationId) =>
set((state) => ({
conversations: state.conversations.map((conv) =>
conv.id === conversationId ? { ...conv, unreadCount: 0 } : conv
)
})),
togglePin: (conversationId) =>
set((state) => ({
conversations: state.conversations.map((conv) =>
conv.id === conversationId ? { ...conv, isPinned: !conv.isPinned } : conv
)
})),
toggleMute: (conversationId) =>
set((state) => ({
conversations: state.conversations.map((conv) =>
conv.id === conversationId ? { ...conv, isMuted: !conv.isMuted } : conv
)
})),
setTyping: (conversationId, isTyping) =>
set((state) => ({
isTyping: { ...state.isTyping, [conversationId]: isTyping }
})),
setOnlineUsers: (userIds) => set({ onlineUsers: userIds }),
}))

View File

@@ -0,0 +1,248 @@
"use client"
import { useState } from "react"
import { BarChart, Bar, XAxis, YAxis, CartesianGrid } from "recharts"
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
import { ChartContainer, ChartTooltip, ChartTooltipContent } from "@/components/ui/chart"
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"
import { Button } from "@/components/ui/button"
import { Users, MapPin, TrendingUp, Target, ArrowUpIcon, UserIcon } from "lucide-react"
const customerGrowthData = [
{ month: "Jan", new: 245, returning: 890, churn: 45 },
{ month: "Feb", new: 312, returning: 934, churn: 52 },
{ month: "Mar", new: 289, returning: 1023, churn: 38 },
{ month: "Apr", new: 456, returning: 1156, churn: 61 },
{ month: "May", new: 523, returning: 1298, churn: 47 },
{ month: "Jun", new: 634, returning: 1445, churn: 55 },
]
const chartConfig = {
new: {
label: "New Customers",
color: "var(--chart-1)",
},
returning: {
label: "Returning",
color: "var(--chart-2)",
},
churn: {
label: "Churned",
color: "var(--chart-3)",
},
}
const demographicsData = [
{ ageGroup: "18-24", customers: 2847, percentage: "18.0%", growth: "+15.2%", growthColor: "text-green-600" },
{ ageGroup: "25-34", customers: 4521, percentage: "28.5%", growth: "+8.7%", growthColor: "text-green-600" },
{ ageGroup: "35-44", customers: 3982, percentage: "25.1%", growth: "+3.4%", growthColor: "text-blue-600" },
{ ageGroup: "45-54", customers: 2734, percentage: "17.2%", growth: "+1.2%", growthColor: "text-orange-600" },
{ ageGroup: "55+", customers: 1763, percentage: "11.2%", growth: "-2.1%", growthColor: "text-red-600" },
]
const regionsData = [
{ region: "North America", customers: 6847, revenue: "$847,523", growth: "+12.3%", growthColor: "text-green-600" },
{ region: "Europe", customers: 4521, revenue: "$563,891", growth: "+9.7%", growthColor: "text-green-600" },
{ region: "Asia Pacific", customers: 2892, revenue: "$321,456", growth: "+18.4%", growthColor: "text-blue-600" },
{ region: "Latin America", customers: 1123, revenue: "$187,234", growth: "+15.8%", growthColor: "text-green-600" },
{ region: "Others", customers: 464, revenue: "$67,891", growth: "+5.2%", growthColor: "text-orange-600" },
]
export function CustomerInsights() {
const [activeTab, setActiveTab] = useState("growth")
return (
<Card className="h-fit">
<CardHeader>
<CardTitle>Customer Insights</CardTitle>
<CardDescription>Growth trends and demographics</CardDescription>
</CardHeader>
<CardContent>
<Tabs value={activeTab} onValueChange={setActiveTab} className="w-full">
<TabsList className="grid w-full grid-cols-3 bg-muted/50 p-1 rounded-lg h-12">
<TabsTrigger
value="growth"
className="cursor-pointer flex items-center gap-2 rounded-md px-4 py-2 text-sm font-medium transition-all data-[state=active]:bg-background data-[state=active]:shadow-sm data-[state=active]:text-foreground"
>
<TrendingUp className="h-4 w-4" />
<span className="hidden sm:inline">Growth</span>
</TabsTrigger>
<TabsTrigger
value="demographics"
className="cursor-pointer flex items-center gap-2 rounded-md px-4 py-2 text-sm font-medium transition-all data-[state=active]:bg-background data-[state=active]:shadow-sm data-[state=active]:text-foreground"
>
<UserIcon className="h-4 w-4" />
<span className="hidden sm:inline">Demographics</span>
</TabsTrigger>
<TabsTrigger
value="regions"
className="cursor-pointer flex items-center gap-2 rounded-md px-4 py-2 text-sm font-medium transition-all data-[state=active]:bg-background data-[state=active]:shadow-sm data-[state=active]:text-foreground"
>
<MapPin className="h-4 w-4" />
<span className="hidden sm:inline">Regions</span>
</TabsTrigger>
</TabsList>
<TabsContent value="growth" className="mt-8 space-y-6">
<div className="grid gap-6">
{/* Chart and Key Metrics Side by Side */}
<div className="grid grid-cols-10 gap-6">
{/* Chart Area - 70% */}
<div className="col-span-10 xl:col-span-7">
<h3 className="text-sm font-medium text-muted-foreground mb-6">Customer Growth Trends</h3>
<ChartContainer config={chartConfig} className="h-[375px] w-full">
<BarChart data={customerGrowthData} margin={{ top: 20, right: 20, bottom: 20, left: 0 }}>
<CartesianGrid strokeDasharray="3 3" className="stroke-muted" />
<XAxis
dataKey="month"
className="text-xs"
tick={{ fontSize: 12 }}
tickLine={{ stroke: 'var(--border)' }}
axisLine={{ stroke: 'var(--border)' }}
/>
<YAxis
className="text-xs"
tick={{ fontSize: 12 }}
tickLine={{ stroke: 'var(--border)' }}
axisLine={{ stroke: 'var(--border)' }}
domain={[0, 'dataMax']}
/>
<ChartTooltip content={<ChartTooltipContent />} />
<Bar dataKey="new" fill="var(--color-new)" radius={[2, 2, 0, 0]} />
<Bar dataKey="returning" fill="var(--color-returning)" radius={[2, 2, 0, 0]} />
<Bar dataKey="churn" fill="var(--color-churn)" radius={[2, 2, 0, 0]} />
</BarChart>
</ChartContainer>
</div>
{/* Key Metrics - 30% */}
<div className="col-span-10 xl:col-span-3 space-y-5">
<h3 className="text-sm font-medium text-muted-foreground mb-6">Key Metrics</h3>
<div className="grid grid-cols-3 gap-5">
<div className="p-4 rounded-lg max-lg:col-span-3 xl:col-span-3 border">
<div className="flex items-center gap-2 mb-2">
<TrendingUp className="h-4 w-4 text-primary" />
<span className="text-sm font-medium">Total Customers</span>
</div>
<div className="text-2xl font-bold">15,847</div>
<div className="text-xs text-green-600 flex items-center gap-1 mt-1">
<ArrowUpIcon className="h-3 w-3" />
+12.5% from last month
</div>
</div>
<div className="p-4 rounded-lg max-lg:col-span-3 xl:col-span-3 border">
<div className="flex items-center gap-2 mb-2">
<Users className="h-4 w-4 text-muted-foreground" />
<span className="text-sm font-medium">Retention Rate</span>
</div>
<div className="text-2xl font-bold">92.4%</div>
<div className="text-xs text-green-600 flex items-center gap-1 mt-1">
<ArrowUpIcon className="h-3 w-3" />
+2.1% improvement
</div>
</div>
<div className="p-4 rounded-lg max-lg:col-span-3 xl:col-span-3 border">
<div className="flex items-center gap-2 mb-2">
<Target className="h-4 w-4 text-muted-foreground" />
<span className="text-sm font-medium">Avg. LTV</span>
</div>
<div className="text-2xl font-bold">$2,847</div>
<div className="text-xs text-green-600 flex items-center gap-1 mt-1">
<ArrowUpIcon className="h-3 w-3" />
+8.3% growth
</div>
</div>
</div>
</div>
</div>
</div>
</TabsContent>
<TabsContent value="demographics" className="mt-8">
<div className="rounded-lg border bg-card">
<Table>
<TableHeader>
<TableRow className="border-b">
<TableHead className="py-5 px-6 font-semibold">Age Group</TableHead>
<TableHead className="text-right py-5 px-6 font-semibold">Customers</TableHead>
<TableHead className="text-right py-5 px-6 font-semibold">Percentage</TableHead>
<TableHead className="text-right py-5 px-6 font-semibold">Growth</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{demographicsData.map((row, index) => (
<TableRow key={index} className="hover:bg-muted/30 transition-colors">
<TableCell className="font-medium py-5 px-6">{row.ageGroup}</TableCell>
<TableCell className="text-right py-5 px-6">{row.customers.toLocaleString()}</TableCell>
<TableCell className="text-right py-5 px-6">{row.percentage}</TableCell>
<TableCell className="text-right py-5 px-6">
<span className={`font-medium ${row.growthColor}`}>{row.growth}</span>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</div>
<div className="flex items-center justify-end space-x-2 py-6">
<div className="text-muted-foreground text-sm hidden sm:block">
0 of {demographicsData.length} row(s) selected.
</div>
<div className="space-x-2 space-y-2">
<Button variant="outline" size="sm" disabled>
Previous
</Button>
<Button variant="outline" size="sm" disabled>
Next
</Button>
</div>
</div>
</TabsContent>
<TabsContent value="regions" className="mt-8">
<div className="rounded-lg border bg-card">
<Table>
<TableHeader>
<TableRow className="border-b">
<TableHead className="py-5 px-6 font-semibold">Region</TableHead>
<TableHead className="text-right py-5 px-6 font-semibold">Customers</TableHead>
<TableHead className="text-right py-5 px-6 font-semibold">Revenue</TableHead>
<TableHead className="text-right py-5 px-6 font-semibold">Growth</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{regionsData.map((row, index) => (
<TableRow key={index} className="hover:bg-muted/30 transition-colors">
<TableCell className="font-medium py-5 px-6">{row.region}</TableCell>
<TableCell className="text-right py-5 px-6">{row.customers.toLocaleString()}</TableCell>
<TableCell className="text-right py-5 px-6">{row.revenue}</TableCell>
<TableCell className="text-right py-5 px-6">
<span className={`font-medium ${row.growthColor}`}>{row.growth}</span>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</div>
<div className="flex items-center justify-end space-x-2 py-6">
<div className="text-muted-foreground text-sm hidden sm:block">
0 of {regionsData.length} row(s) selected.
</div>
<div className="space-x-2 space-y-2">
<Button variant="outline" size="sm" disabled>
Previous
</Button>
<Button variant="outline" size="sm" disabled>
Next
</Button>
</div>
</div>
</TabsContent>
</Tabs>
</CardContent>
</Card>
)
}

View File

@@ -0,0 +1,69 @@
"use client"
import { useState } from "react"
import { Calendar, Clock, RefreshCw, Filter } from "lucide-react"
import { Card, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
import { Button } from "@/components/ui/button"
import { Badge } from "@/components/ui/badge"
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
import { Separator } from "@/components/ui/separator"
export function DashboardHeader() {
const [dateRange, setDateRange] = useState("30d")
const lastUpdated = new Date().toLocaleString()
return (
<Card>
<CardHeader>
<div className="flex items-center justify-between">
<div>
<CardTitle className="text-3xl font-bold">Business Dashboard</CardTitle>
<CardDescription className="text-base mt-2">
Comprehensive overview of your business performance and key metrics
</CardDescription>
</div>
<div className="flex items-center space-x-2">
<Badge variant="outline" className="cursor-pointer">
<Clock className="h-3 w-3 mr-1" />
Live Data
</Badge>
<Button variant="outline" size="sm" className="cursor-pointer">
<RefreshCw className="h-4 w-4 mr-2" />
Refresh
</Button>
</div>
</div>
<Separator className="my-4" />
<div className="flex items-center justify-between">
<div className="flex items-center space-x-4">
<div className="flex items-center space-x-2">
<Calendar className="h-4 w-4 text-muted-foreground" />
<span className="text-sm text-muted-foreground">Date Range:</span>
<Select value={dateRange} onValueChange={setDateRange}>
<SelectTrigger className="w-40 cursor-pointer">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="7d" className="cursor-pointer">Last 7 days</SelectItem>
<SelectItem value="30d" className="cursor-pointer">Last 30 days</SelectItem>
<SelectItem value="90d" className="cursor-pointer">Last 90 days</SelectItem>
<SelectItem value="1y" className="cursor-pointer">Last year</SelectItem>
</SelectContent>
</Select>
</div>
<Button variant="outline" size="sm" className="cursor-pointer">
<Filter className="h-4 w-4 mr-2" />
Filters
</Button>
</div>
<div className="text-sm text-muted-foreground">
Last updated: {lastUpdated}
</div>
</div>
</CardHeader>
</Card>
)
}

View File

@@ -0,0 +1,90 @@
"use client"
import {
TrendingUp,
TrendingDown,
DollarSign,
Users,
ShoppingCart,
BarChart3
} from "lucide-react"
import { Card, CardAction, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card"
import { Badge } from "@/components/ui/badge"
const metrics = [
{
title: "Total Revenue",
value: "$54,230",
description: "Monthly revenue",
change: "+12%",
trend: "up",
icon: DollarSign,
footer: "Trending up this month",
subfooter: "Revenue for the last 6 months"
},
{
title: "Active Customers",
value: "2,350",
description: "Total active users",
change: "+5.2%",
trend: "up",
icon: Users,
footer: "Strong user retention",
subfooter: "Engagement exceeds targets"
},
{
title: "Total Orders",
value: "1,247",
description: "Orders this month",
change: "-2.1%",
trend: "down",
icon: ShoppingCart,
footer: "Down 2% this period",
subfooter: "Order volume needs attention"
},
{
title: "Conversion Rate",
value: "3.24%",
description: "Average conversion",
change: "+8.3%",
trend: "up",
icon: BarChart3,
footer: "Steady performance increase",
subfooter: "Meets conversion projections"
},
]
export function MetricsOverview() {
return (
<div className="*:data-[slot=card]:from-primary/5 *:data-[slot=card]:to-card dark:*:data-[slot=card]:bg-card *:data-[slot=card]:bg-gradient-to-t *:data-[slot=card]:shadow-xs grid gap-4 sm:grid-cols-2 @5xl:grid-cols-4">
{metrics.map((metric) => {
const TrendIcon = metric.trend === "up" ? TrendingUp : TrendingDown
return (
<Card key={metric.title} className=" cursor-pointer">
<CardHeader>
<CardDescription>{metric.title}</CardDescription>
<CardTitle className="text-2xl font-semibold tabular-nums @[250px]/card:text-3xl">
{metric.value}
</CardTitle>
<CardAction>
<Badge variant="outline">
<TrendIcon className="h-4 w-4" />
{metric.change}
</Badge>
</CardAction>
</CardHeader>
<CardFooter className="flex-col items-start gap-1.5 text-sm">
<div className="line-clamp-1 flex gap-2 font-medium">
{metric.footer} <TrendIcon className="size-4" />
</div>
<div className="text-muted-foreground">
{metric.subfooter}
</div>
</CardFooter>
</Card>
)
})}
</div>
)
}

View File

@@ -0,0 +1,39 @@
"use client"
import { Plus, Settings, FileText, Download } from "lucide-react"
import { Button } from "@/components/ui/button"
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuTrigger } from "@/components/ui/dropdown-menu"
export function QuickActions() {
return (
<div className="flex items-center space-x-2">
<Button className="cursor-pointer">
<Plus className="h-4 w-4 mr-2" />
New Sale
</Button>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="outline" className="cursor-pointer">
<Settings className="h-4 w-4 mr-2" />
Actions
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem className="cursor-pointer">
<FileText className="h-4 w-4 mr-2" />
Generate Report
</DropdownMenuItem>
<DropdownMenuItem className="cursor-pointer">
<Download className="h-4 w-4 mr-2" />
Export Data
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem className="cursor-pointer">
<Settings className="h-4 w-4 mr-2" />
Dashboard Settings
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
)
}

View File

@@ -0,0 +1,131 @@
"use client"
import { Eye, MoreHorizontal } from "lucide-react"
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
import { Button } from "@/components/ui/button"
import { Badge } from "@/components/ui/badge"
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@/components/ui/dropdown-menu"
import { assetUrl } from "@/lib/utils"
const transactions = [
{
id: "TXN-001",
customer: {
name: "Olivia Martin",
email: "olivia.martin@email.com",
avatar: assetUrl("avatars/01.png"),
},
amount: "$1,999.00",
status: "completed",
date: "2 hours ago",
},
{
id: "TXN-002",
customer: {
name: "Jackson Lee",
email: "jackson.lee@email.com",
avatar: assetUrl("avatars/02.png"),
},
amount: "$2,999.00",
status: "pending",
date: "5 hours ago",
},
{
id: "TXN-003",
customer: {
name: "Isabella Nguyen",
email: "isabella.nguyen@email.com",
avatar: assetUrl("avatars/03.png"),
},
amount: "$39.00",
status: "completed",
date: "1 day ago",
},
{
id: "TXN-004",
customer: {
name: "William Kim",
email: "will@email.com",
avatar: assetUrl("avatars/04.png"),
},
amount: "$299.00",
status: "failed",
date: "2 days ago",
},
{
id: "TXN-005",
customer: {
name: "Sofia Davis",
email: "sofia.davis@email.com",
avatar: assetUrl("avatars/05.png"),
},
amount: "$99.00",
status: "completed",
date: "3 days ago",
},
]
export function RecentTransactions() {
return (
<Card className="cursor-pointer">
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-4">
<div>
<CardTitle>Recent Transactions</CardTitle>
<CardDescription>Latest customer transactions</CardDescription>
</div>
<Button variant="outline" size="sm" className="cursor-pointer">
<Eye className="h-4 w-4 mr-2" />
View All
</Button>
</CardHeader>
<CardContent className="space-y-4">
{transactions.map((transaction) => (
<div key={transaction.id} >
<div className="flex p-3 rounded-lg border gap-2">
<Avatar className="h-8 w-8">
<AvatarImage src={transaction.customer.avatar} alt={transaction.customer.name} />
<AvatarFallback>{transaction.customer.name.split(" ").map(n => n[0]).join("")}</AvatarFallback>
</Avatar>
<div className="flex flex-1 items-center flex-wrap justify-between gap-1">
<div className="flex items-center space-x-3">
<div className="min-w-0 flex-1">
<p className="text-sm font-medium truncate">{transaction.customer.name}</p>
<p className="text-xs text-muted-foreground truncate">{transaction.customer.email}</p>
</div>
</div>
<div className="flex items-center space-x-3">
<Badge
variant={
transaction.status === "completed" ? "default" :
transaction.status === "pending" ? "secondary" : "destructive"
}
className="cursor-pointer"
>
{transaction.status}
</Badge>
<div className="text-right">
<p className="text-sm font-medium">{transaction.amount}</p>
<p className="text-xs text-muted-foreground">{transaction.date}</p>
</div>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" size="sm" className="h-8 w-8 p-0 cursor-pointer">
<MoreHorizontal className="h-4 w-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem className="cursor-pointer">View Details</DropdownMenuItem>
<DropdownMenuItem className="cursor-pointer">Download Receipt</DropdownMenuItem>
<DropdownMenuItem className="cursor-pointer">Contact Customer</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
</div>
</div>
</div>
))}
</CardContent>
</Card>
)
}

View File

@@ -0,0 +1,205 @@
"use client"
import * as React from "react"
import { Label, Pie, PieChart, Sector } from "recharts"
import type { PieSectorDataItem } from "recharts/types/polar/Pie"
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
import { ChartContainer, ChartStyle, ChartTooltip, ChartTooltipContent } from "@/components/ui/chart"
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
import { Button } from "@/components/ui/button"
const revenueData = [
{ category: "subscriptions", value: 45, amount: 24500, fill: "var(--color-subscriptions)" },
{ category: "sales", value: 30, amount: 16300, fill: "var(--color-sales)" },
{ category: "services", value: 15, amount: 8150, fill: "var(--color-services)" },
{ category: "partnerships", value: 10, amount: 5430, fill: "var(--color-partnerships)" },
]
const chartConfig = {
revenue: {
label: "Revenue",
},
amount: {
label: "Amount",
},
subscriptions: {
label: "Subscriptions",
color: "var(--chart-1)",
},
sales: {
label: "One-time Sales",
color: "var(--chart-2)",
},
services: {
label: "Services",
color: "var(--chart-3)",
},
partnerships: {
label: "Partnerships",
color: "var(--chart-4)",
},
}
export function RevenueBreakdown() {
const id = "revenue-breakdown"
const [activeCategory, setActiveCategory] = React.useState("sales")
const activeIndex = React.useMemo(
() => revenueData.findIndex((item) => item.category === activeCategory),
[activeCategory]
)
const categories = React.useMemo(() => revenueData.map((item) => item.category), [])
return (
<Card data-chart={id} className="flex flex-col cursor-pointer">
<ChartStyle id={id} config={chartConfig} />
<CardHeader className="flex flex-col space-y-2 sm:flex-row sm:items-center sm:justify-between sm:space-y-0 pb-2">
<div>
<CardTitle>Revenue Breakdown</CardTitle>
<CardDescription>Revenue distribution by source</CardDescription>
</div>
<div className="flex items-center space-x-2">
<Select value={activeCategory} onValueChange={setActiveCategory}>
<SelectTrigger
className="w-[175px] rounded-lg cursor-pointer"
aria-label="Select a category"
>
<SelectValue placeholder="Select category" />
</SelectTrigger>
<SelectContent align="end" className="rounded-lg">
{categories.map((key) => {
const config = chartConfig[key as keyof typeof chartConfig]
if (!config) {
return null
}
return (
<SelectItem
key={key}
value={key}
className="rounded-md [&_span]:flex cursor-pointer"
>
<div className="flex items-center gap-2">
<span
className="flex h-3 w-3 shrink-0 "
style={{
backgroundColor: `var(--color-${key})`,
}}
/>
{config?.label}
</div>
</SelectItem>
)
})}
</SelectContent>
</Select>
<Button variant="outline" className="cursor-pointer">
Export
</Button>
</div>
</CardHeader>
<CardContent className="flex flex-1 justify-center">
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6 w-full">
<div className="flex justify-center">
<ChartContainer
id={id}
config={chartConfig}
className="mx-auto aspect-square w-full max-w-[300px]"
>
<PieChart>
<ChartTooltip
cursor={false}
content={<ChartTooltipContent hideLabel />}
/>
<Pie
data={revenueData}
dataKey="amount"
nameKey="category"
innerRadius={60}
strokeWidth={5}
activeIndex={activeIndex}
activeShape={({
outerRadius = 0,
...props
}: PieSectorDataItem) => (
<g>
<Sector {...props} outerRadius={outerRadius + 10} />
<Sector
{...props}
outerRadius={outerRadius + 25}
innerRadius={outerRadius + 12}
/>
</g>
)}
>
<Label
content={({ viewBox }) => {
if (viewBox && "cx" in viewBox && "cy" in viewBox) {
return (
<text
x={viewBox.cx}
y={viewBox.cy}
textAnchor="middle"
dominantBaseline="middle"
>
<tspan
x={viewBox.cx}
y={viewBox.cy}
className="fill-foreground text-3xl font-bold"
>
${(revenueData[activeIndex].amount / 1000).toFixed(0)}K
</tspan>
<tspan
x={viewBox.cx}
y={(viewBox.cy || 0) + 24}
className="fill-muted-foreground"
>
Revenue
</tspan>
</text>
)
}
}}
/>
</Pie>
</PieChart>
</ChartContainer>
</div>
<div className="flex flex-col justify-center space-y-4">
{revenueData.map((item, index) => {
const config = chartConfig[item.category as keyof typeof chartConfig]
const isActive = index === activeIndex
return (
<div
key={item.category}
className={`flex items-center justify-between p-3 rounded-lg transition-colors cursor-pointer ${
isActive ? 'bg-muted' : 'hover:bg-muted/50'
}`}
onClick={() => setActiveCategory(item.category)}
>
<div className="flex items-center gap-3">
<span
className="flex h-3 w-3 shrink-0 rounded-full"
style={{
backgroundColor: `var(--color-${item.category})`,
}}
/>
<span className="font-medium">{config?.label}</span>
</div>
<div className="text-right">
<div className="font-bold">${(item.amount / 1000).toFixed(1)}K</div>
<div className="text-sm text-muted-foreground">{item.value}%</div>
</div>
</div>
)
})}
</div>
</div>
</CardContent>
</Card>
)
}

View File

@@ -0,0 +1,115 @@
"use client"
import { useState } from "react"
import { Area, AreaChart, CartesianGrid, XAxis, YAxis } from "recharts"
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
import { ChartContainer, ChartTooltip, ChartTooltipContent } from "@/components/ui/chart"
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
import { Button } from "@/components/ui/button"
const salesData = [
{ month: "Jan", sales: 12500, target: 15000 },
{ month: "Feb", sales: 18200, target: 15000 },
{ month: "Mar", sales: 16800, target: 15000 },
{ month: "Apr", sales: 22400, target: 20000 },
{ month: "May", sales: 24600, target: 20000 },
{ month: "Jun", sales: 28200, target: 25000 },
{ month: "Jul", sales: 31500, target: 25000 },
{ month: "Aug", sales: 29800, target: 25000 },
{ month: "Sep", sales: 33200, target: 30000 },
{ month: "Oct", sales: 35100, target: 30000 },
{ month: "Nov", sales: 38900, target: 35000 },
{ month: "Dec", sales: 42300, target: 35000 },
]
const chartConfig = {
sales: {
label: "Sales",
color: "var(--primary)",
},
target: {
label: "Target",
color: "var(--primary)",
},
}
export function SalesChart() {
const [timeRange, setTimeRange] = useState("12m")
return (
<Card className="cursor-pointer">
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<div>
<CardTitle>Sales Performance</CardTitle>
<CardDescription>Monthly sales vs targets</CardDescription>
</div>
<div className="flex items-center space-x-2">
<Select value={timeRange} onValueChange={setTimeRange}>
<SelectTrigger className="w-32 cursor-pointer">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="3m" className="cursor-pointer">Last 3 months</SelectItem>
<SelectItem value="6m" className="cursor-pointer">Last 6 months</SelectItem>
<SelectItem value="12m" className="cursor-pointer">Last 12 months</SelectItem>
</SelectContent>
</Select>
<Button variant="outline" className="cursor-pointer">
Export
</Button>
</div>
</CardHeader>
<CardContent className="p-0 pt-6">
<div className="px-6 pb-6">
<ChartContainer config={chartConfig} className="h-[350px] w-full">
<AreaChart data={salesData} margin={{ top: 10, right: 10, left: 10, bottom: 0 }}>
<defs>
<linearGradient id="colorSales" x1="0" y1="0" x2="0" y2="1">
<stop offset="5%" stopColor="var(--color-sales)" stopOpacity={0.4} />
<stop offset="95%" stopColor="var(--color-sales)" stopOpacity={0.05} />
</linearGradient>
<linearGradient id="colorTarget" x1="0" y1="0" x2="0" y2="1">
<stop offset="5%" stopColor="var(--color-target)" stopOpacity={0.2} />
<stop offset="95%" stopColor="var(--color-target)" stopOpacity={0} />
</linearGradient>
</defs>
<CartesianGrid strokeDasharray="3 3" className="stroke-muted/30" />
<XAxis
dataKey="month"
axisLine={false}
tickLine={false}
className="text-xs"
tick={{ fontSize: 12 }}
/>
<YAxis
axisLine={false}
tickLine={false}
className="text-xs"
tick={{ fontSize: 12 }}
tickFormatter={(value) => `$${value.toLocaleString()}`}
/>
<ChartTooltip content={<ChartTooltipContent />} />
<Area
type="monotone"
dataKey="target"
stackId="1"
stroke="var(--color-target)"
fill="url(#colorTarget)"
strokeDasharray="5 5"
strokeWidth={1}
/>
<Area
type="monotone"
dataKey="sales"
stackId="2"
stroke="var(--color-sales)"
fill="url(#colorSales)"
strokeWidth={1}
/>
</AreaChart>
</ChartContainer>
</div>
</CardContent>
</Card>
)
}

View File

@@ -0,0 +1,123 @@
"use client"
import { Eye, Star, TrendingUp } from "lucide-react"
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
import { Button } from "@/components/ui/button"
import { Badge } from "@/components/ui/badge"
import { Progress } from "@/components/ui/progress"
const products = [
{
id: 1,
name: "Premium Dashboard",
sales: 2847,
revenue: "$142,350",
growth: "+23%",
rating: 4.8,
stock: 145,
category: "Software",
},
{
id: 2,
name: "Analytics Pro",
sales: 1923,
revenue: "$96,150",
growth: "+18%",
rating: 4.6,
stock: 67,
category: "Tools",
},
{
id: 3,
name: "Mobile App Suite",
sales: 1456,
revenue: "$72,800",
growth: "+12%",
rating: 4.9,
stock: 234,
category: "Mobile",
},
{
id: 4,
name: "Enterprise License",
sales: 892,
revenue: "$178,400",
growth: "+8%",
rating: 4.7,
stock: 12,
category: "Enterprise",
},
{
id: 5,
name: "Basic Subscription",
sales: 3421,
revenue: "$68,420",
growth: "+31%",
rating: 4.4,
stock: 999,
category: "Subscription",
},
]
export function TopProducts() {
return (
<Card className="cursor-pointer">
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-4">
<div>
<CardTitle>Top Products</CardTitle>
<CardDescription>Best performing products this month</CardDescription>
</div>
<Button variant="outline" size="sm" className="cursor-pointer">
<Eye className="h-4 w-4 mr-2" />
View All
</Button>
</CardHeader>
<CardContent className="space-y-4">
{products.map((product, index) => (
<div key={product.id} className="flex items-center p-3 rounded-lg border gap-2">
<div className="flex items-center justify-center w-8 h-8 rounded-full bg-primary/10 text-primary font-semibold text-sm">
#{index + 1}
</div>
<div className="flex gap-2 items-center justify-between space-x-3 flex-1 flex-wrap">
<div className="">
<div className="flex items-center space-x-2">
<p className="text-sm font-medium truncate">{product.name}</p>
<Badge variant="outline" className="text-xs">
{product.category}
</Badge>
</div>
<div className="flex items-center space-x-2 mt-1">
<div className="flex items-center space-x-1">
<Star className="h-3 w-3 fill-yellow-400 text-yellow-400" />
<span className="text-xs text-muted-foreground">{product.rating}</span>
</div>
<span className="text-xs text-muted-foreground"></span>
<span className="text-xs text-muted-foreground">{product.sales} sales</span>
</div>
</div>
<div className="text-right space-y-1">
<div className="flex items-center space-x-2">
<p className="text-sm font-medium">{product.revenue}</p>
<Badge
variant="outline"
className="text-green-600 border-green-200 cursor-pointer"
>
<TrendingUp className="h-3 w-3 mr-1" />
{product.growth}
</Badge>
</div>
<div className="flex items-center space-x-2">
<span className="text-xs text-muted-foreground">Stock: {product.stock}</span>
<Progress
value={product.stock > 100 ? 100 : (product.stock / 100) * 100}
className="w-12 h-1"
/>
</div>
</div>
</div>
</div>
))}
</CardContent>
</Card>
)
}

View File

@@ -0,0 +1,39 @@
{
"totalRevenue": 54231.89,
"revenueChange": 12.5,
"activeCustomers": 2350,
"customerChange": 5.2,
"totalOrders": 1247,
"orderChange": -2.1,
"conversionRate": 3.24,
"conversionChange": 8.3,
"salesData": [
{ "month": "Jan", "sales": 12500, "target": 15000 },
{ "month": "Feb", "sales": 18200, "target": 15000 },
{ "month": "Mar", "sales": 16800, "target": 15000 },
{ "month": "Apr", "sales": 22400, "target": 20000 },
{ "month": "May", "sales": 24600, "target": 20000 },
{ "month": "Jun", "sales": 28200, "target": 25000 },
{ "month": "Jul", "sales": 31500, "target": 25000 },
{ "month": "Aug", "sales": 29800, "target": 25000 },
{ "month": "Sep", "sales": 33200, "target": 30000 },
{ "month": "Oct", "sales": 35100, "target": 30000 },
{ "month": "Nov", "sales": 38900, "target": 35000 },
{ "month": "Dec", "sales": 42300, "target": 35000 }
],
"revenueBreakdown": [
{ "name": "Subscriptions", "value": 45, "amount": 24500, "color": "hsl(210, 100%, 50%)" },
{ "name": "One-time Sales", "value": 30, "amount": 16300, "color": "hsl(280, 100%, 70%)" },
{ "name": "Services", "value": 15, "amount": 8150, "color": "hsl(120, 100%, 40%)" },
{ "name": "Partnerships", "value": 10, "amount": 5430, "color": "hsl(30, 100%, 50%)" }
],
"customerGrowth": [
{ "month": "Jan", "new": 245, "returning": 890, "churn": 45 },
{ "month": "Feb", "new": 312, "returning": 934, "churn": 52 },
{ "month": "Mar", "new": 289, "returning": 1023, "churn": 38 },
{ "month": "Apr", "new": 456, "returning": 1156, "churn": 61 },
{ "month": "May", "new": 523, "returning": 1298, "churn": 47 },
{ "month": "Jun", "new": 634, "returning": 1445, "churn": 55 }
],
"lastUpdated": "2025-08-12T15:30:00Z"
}

View File

@@ -0,0 +1,50 @@
import { BaseLayout } from "@/components/layouts/base-layout"
import { MetricsOverview } from "./components/metrics-overview"
import { SalesChart } from "./components/sales-chart"
import { RecentTransactions } from "./components/recent-transactions"
import { TopProducts } from "./components/top-products"
import { CustomerInsights } from "./components/customer-insights"
import { QuickActions } from "./components/quick-actions"
import { RevenueBreakdown } from "./components/revenue-breakdown"
export default function Dashboard2() {
return (
<BaseLayout>
<div className="flex-1 space-y-6 px-6 pt-0">
{/* Enhanced Header */}
<div className="flex md:flex-row flex-col md:items-center justify-between gap-4 md:gap-6">
<div className="flex flex-col gap-2">
<h1 className="text-2xl font-bold tracking-tight">Business Dashboard</h1>
<p className="text-muted-foreground">
Monitor your business performance and key metrics in real-time
</p>
</div>
<QuickActions />
</div>
{/* Main Dashboard Grid */}
<div className="@container/main space-y-6">
{/* Top Row - Key Metrics */}
<MetricsOverview />
{/* Second Row - Charts in 6-6 columns */}
<div className="grid gap-6 grid-cols-1 @5xl:grid-cols-2">
<SalesChart />
<RevenueBreakdown />
</div>
{/* Third Row - Two Column Layout */}
<div className="grid gap-6 grid-cols-1 @5xl:grid-cols-2">
<RecentTransactions />
<TopProducts />
</div>
{/* Fourth Row - Customer Insights and Team Performance */}
<CustomerInsights />
</div>
</div>
</BaseLayout>
)
}

View File

@@ -0,0 +1,291 @@
"use client"
import * as React from "react"
import { Area, AreaChart, CartesianGrid, XAxis } from "recharts"
import { useIsMobile } from "@/hooks/use-mobile"
import {
Card,
CardAction,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card"
import {
type ChartConfig,
ChartContainer,
ChartTooltip,
ChartTooltipContent,
} from "@/components/ui/chart"
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select"
import {
ToggleGroup,
ToggleGroupItem,
} from "@/components/ui/toggle-group"
export const description = "An interactive area chart"
const chartData = [
{ date: "2024-04-01", desktop: 222, mobile: 150 },
{ date: "2024-04-02", desktop: 97, mobile: 180 },
{ date: "2024-04-03", desktop: 167, mobile: 120 },
{ date: "2024-04-04", desktop: 242, mobile: 260 },
{ date: "2024-04-05", desktop: 373, mobile: 290 },
{ date: "2024-04-06", desktop: 301, mobile: 340 },
{ date: "2024-04-07", desktop: 245, mobile: 180 },
{ date: "2024-04-08", desktop: 409, mobile: 320 },
{ date: "2024-04-09", desktop: 59, mobile: 110 },
{ date: "2024-04-10", desktop: 261, mobile: 190 },
{ date: "2024-04-11", desktop: 327, mobile: 350 },
{ date: "2024-04-12", desktop: 292, mobile: 210 },
{ date: "2024-04-13", desktop: 342, mobile: 380 },
{ date: "2024-04-14", desktop: 137, mobile: 220 },
{ date: "2024-04-15", desktop: 120, mobile: 170 },
{ date: "2024-04-16", desktop: 138, mobile: 190 },
{ date: "2024-04-17", desktop: 446, mobile: 360 },
{ date: "2024-04-18", desktop: 364, mobile: 410 },
{ date: "2024-04-19", desktop: 243, mobile: 180 },
{ date: "2024-04-20", desktop: 89, mobile: 150 },
{ date: "2024-04-21", desktop: 137, mobile: 200 },
{ date: "2024-04-22", desktop: 224, mobile: 170 },
{ date: "2024-04-23", desktop: 138, mobile: 230 },
{ date: "2024-04-24", desktop: 387, mobile: 290 },
{ date: "2024-04-25", desktop: 215, mobile: 250 },
{ date: "2024-04-26", desktop: 75, mobile: 130 },
{ date: "2024-04-27", desktop: 383, mobile: 420 },
{ date: "2024-04-28", desktop: 122, mobile: 180 },
{ date: "2024-04-29", desktop: 315, mobile: 240 },
{ date: "2024-04-30", desktop: 454, mobile: 380 },
{ date: "2024-05-01", desktop: 165, mobile: 220 },
{ date: "2024-05-02", desktop: 293, mobile: 310 },
{ date: "2024-05-03", desktop: 247, mobile: 190 },
{ date: "2024-05-04", desktop: 385, mobile: 420 },
{ date: "2024-05-05", desktop: 481, mobile: 390 },
{ date: "2024-05-06", desktop: 498, mobile: 520 },
{ date: "2024-05-07", desktop: 388, mobile: 300 },
{ date: "2024-05-08", desktop: 149, mobile: 210 },
{ date: "2024-05-09", desktop: 227, mobile: 180 },
{ date: "2024-05-10", desktop: 293, mobile: 330 },
{ date: "2024-05-11", desktop: 335, mobile: 270 },
{ date: "2024-05-12", desktop: 197, mobile: 240 },
{ date: "2024-05-13", desktop: 197, mobile: 160 },
{ date: "2024-05-14", desktop: 448, mobile: 490 },
{ date: "2024-05-15", desktop: 473, mobile: 380 },
{ date: "2024-05-16", desktop: 338, mobile: 400 },
{ date: "2024-05-17", desktop: 499, mobile: 420 },
{ date: "2024-05-18", desktop: 315, mobile: 350 },
{ date: "2024-05-19", desktop: 235, mobile: 180 },
{ date: "2024-05-20", desktop: 177, mobile: 230 },
{ date: "2024-05-21", desktop: 82, mobile: 140 },
{ date: "2024-05-22", desktop: 81, mobile: 120 },
{ date: "2024-05-23", desktop: 252, mobile: 290 },
{ date: "2024-05-24", desktop: 294, mobile: 220 },
{ date: "2024-05-25", desktop: 201, mobile: 250 },
{ date: "2024-05-26", desktop: 213, mobile: 170 },
{ date: "2024-05-27", desktop: 420, mobile: 460 },
{ date: "2024-05-28", desktop: 233, mobile: 190 },
{ date: "2024-05-29", desktop: 78, mobile: 130 },
{ date: "2024-05-30", desktop: 340, mobile: 280 },
{ date: "2024-05-31", desktop: 178, mobile: 230 },
{ date: "2024-06-01", desktop: 178, mobile: 200 },
{ date: "2024-06-02", desktop: 470, mobile: 410 },
{ date: "2024-06-03", desktop: 103, mobile: 160 },
{ date: "2024-06-04", desktop: 439, mobile: 380 },
{ date: "2024-06-05", desktop: 88, mobile: 140 },
{ date: "2024-06-06", desktop: 294, mobile: 250 },
{ date: "2024-06-07", desktop: 323, mobile: 370 },
{ date: "2024-06-08", desktop: 385, mobile: 320 },
{ date: "2024-06-09", desktop: 438, mobile: 480 },
{ date: "2024-06-10", desktop: 155, mobile: 200 },
{ date: "2024-06-11", desktop: 92, mobile: 150 },
{ date: "2024-06-12", desktop: 492, mobile: 420 },
{ date: "2024-06-13", desktop: 81, mobile: 130 },
{ date: "2024-06-14", desktop: 426, mobile: 380 },
{ date: "2024-06-15", desktop: 307, mobile: 350 },
{ date: "2024-06-16", desktop: 371, mobile: 310 },
{ date: "2024-06-17", desktop: 475, mobile: 520 },
{ date: "2024-06-18", desktop: 107, mobile: 170 },
{ date: "2024-06-19", desktop: 341, mobile: 290 },
{ date: "2024-06-20", desktop: 408, mobile: 450 },
{ date: "2024-06-21", desktop: 169, mobile: 210 },
{ date: "2024-06-22", desktop: 317, mobile: 270 },
{ date: "2024-06-23", desktop: 480, mobile: 530 },
{ date: "2024-06-24", desktop: 132, mobile: 180 },
{ date: "2024-06-25", desktop: 141, mobile: 190 },
{ date: "2024-06-26", desktop: 434, mobile: 380 },
{ date: "2024-06-27", desktop: 448, mobile: 490 },
{ date: "2024-06-28", desktop: 149, mobile: 200 },
{ date: "2024-06-29", desktop: 103, mobile: 160 },
{ date: "2024-06-30", desktop: 446, mobile: 400 },
]
const chartConfig = {
visitors: {
label: "Visitors",
},
desktop: {
label: "Desktop",
color: "var(--primary)",
},
mobile: {
label: "Mobile",
color: "var(--primary)",
},
} satisfies ChartConfig
export function ChartAreaInteractive() {
const isMobile = useIsMobile()
const [timeRange, setTimeRange] = React.useState("90d")
React.useEffect(() => {
if (isMobile) {
setTimeRange("7d")
}
}, [isMobile])
const filteredData = chartData.filter((item) => {
const date = new Date(item.date)
const referenceDate = new Date("2024-06-30")
let daysToSubtract = 90
if (timeRange === "30d") {
daysToSubtract = 30
} else if (timeRange === "7d") {
daysToSubtract = 7
}
const startDate = new Date(referenceDate)
startDate.setDate(startDate.getDate() - daysToSubtract)
return date >= startDate
})
return (
<Card className="@container/card">
<CardHeader>
<CardTitle>Total Visitors</CardTitle>
<CardDescription>
<span className="hidden @[540px]/card:block">
Total for the last 3 months
</span>
<span className="@[540px]/card:hidden">Last 3 months</span>
</CardDescription>
<CardAction>
<ToggleGroup
type="single"
value={timeRange}
onValueChange={setTimeRange}
variant="outline"
className="hidden *:data-[slot=toggle-group-item]:!px-4 @[767px]/card:flex"
>
<ToggleGroupItem value="90d">Last 3 months</ToggleGroupItem>
<ToggleGroupItem value="30d">Last 30 days</ToggleGroupItem>
<ToggleGroupItem value="7d">Last 7 days</ToggleGroupItem>
</ToggleGroup>
<Select value={timeRange} onValueChange={setTimeRange}>
<SelectTrigger
className="flex w-40 **:data-[slot=select-value]:block **:data-[slot=select-value]:truncate @[767px]/card:hidden"
size="sm"
aria-label="Select a value"
>
<SelectValue placeholder="Last 3 months" />
</SelectTrigger>
<SelectContent className="rounded-xl">
<SelectItem value="90d" className="rounded-lg">
Last 3 months
</SelectItem>
<SelectItem value="30d" className="rounded-lg">
Last 30 days
</SelectItem>
<SelectItem value="7d" className="rounded-lg">
Last 7 days
</SelectItem>
</SelectContent>
</Select>
</CardAction>
</CardHeader>
<CardContent className="px-2 pt-4 sm:px-6 sm:pt-6">
<ChartContainer
config={chartConfig}
className="aspect-auto h-[250px] w-full"
>
<AreaChart data={filteredData}>
<defs>
<linearGradient id="fillDesktop" x1="0" y1="0" x2="0" y2="1">
<stop
offset="5%"
stopColor="var(--color-desktop)"
stopOpacity={1.0}
/>
<stop
offset="95%"
stopColor="var(--color-desktop)"
stopOpacity={0.1}
/>
</linearGradient>
<linearGradient id="fillMobile" x1="0" y1="0" x2="0" y2="1">
<stop
offset="5%"
stopColor="var(--color-mobile)"
stopOpacity={0.8}
/>
<stop
offset="95%"
stopColor="var(--color-mobile)"
stopOpacity={0.1}
/>
</linearGradient>
</defs>
<CartesianGrid vertical={false} />
<XAxis
dataKey="date"
tickLine={false}
axisLine={false}
tickMargin={8}
minTickGap={32}
tickFormatter={(value) => {
const date = new Date(value)
return date.toLocaleDateString("en-US", {
month: "short",
day: "numeric",
})
}}
/>
<ChartTooltip
cursor={false}
content={
<ChartTooltipContent
labelFormatter={(value) => {
return new Date(value).toLocaleDateString("en-US", {
month: "short",
day: "numeric",
})
}}
indicator="dot"
/>
}
/>
<Area
dataKey="mobile"
type="natural"
fill="url(#fillMobile)"
stroke="var(--color-mobile)"
stackId="a"
/>
<Area
dataKey="desktop"
type="natural"
fill="url(#fillDesktop)"
stroke="var(--color-desktop)"
stackId="a"
/>
</AreaChart>
</ChartContainer>
</CardContent>
</Card>
)
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,102 @@
import { TrendingDown, TrendingUp } from "lucide-react"
import { Badge } from "@/components/ui/badge"
import {
Card,
CardAction,
CardDescription,
CardFooter,
CardHeader,
CardTitle,
} from "@/components/ui/card"
export function SectionCards() {
return (
<div className="*:data-[slot=card]:from-primary/5 *:data-[slot=card]:to-card dark:*:data-[slot=card]:bg-card *:data-[slot=card]:bg-gradient-to-t *:data-[slot=card]:shadow-xs grid gap-4 sm:grid-cols-2 xl: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">
<TrendingUp />
+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 <TrendingUp 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">
<TrendingDown />
-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 <TrendingDown 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">
<TrendingUp />
+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 <TrendingUp 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">
<TrendingUp />
+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 <TrendingUp className="size-4" />
</div>
<div className="text-muted-foreground">Meets growth projections</div>
</CardFooter>
</Card>
</div>
)
}

View File

@@ -0,0 +1,614 @@
[
{
"id": 1,
"header": "Cover page",
"type": "Cover page",
"status": "In Process",
"target": "18",
"limit": "5",
"reviewer": "Eddie Lake"
},
{
"id": 2,
"header": "Table of contents",
"type": "Table of contents",
"status": "Done",
"target": "29",
"limit": "24",
"reviewer": "Eddie Lake"
},
{
"id": 3,
"header": "Executive summary",
"type": "Narrative",
"status": "Done",
"target": "10",
"limit": "13",
"reviewer": "Eddie Lake"
},
{
"id": 4,
"header": "Technical approach",
"type": "Narrative",
"status": "Done",
"target": "27",
"limit": "23",
"reviewer": "Jamik Tashpulatov"
},
{
"id": 5,
"header": "Design",
"type": "Narrative",
"status": "In Process",
"target": "2",
"limit": "16",
"reviewer": "Jamik Tashpulatov"
},
{
"id": 6,
"header": "Capabilities",
"type": "Narrative",
"status": "In Process",
"target": "20",
"limit": "8",
"reviewer": "Jamik Tashpulatov"
},
{
"id": 7,
"header": "Integration with existing systems",
"type": "Narrative",
"status": "In Process",
"target": "19",
"limit": "21",
"reviewer": "Jamik Tashpulatov"
},
{
"id": 8,
"header": "Innovation and Advantages",
"type": "Narrative",
"status": "Done",
"target": "25",
"limit": "26",
"reviewer": "Assign reviewer"
},
{
"id": 9,
"header": "Overview of EMR's Innovative Solutions",
"type": "Technical content",
"status": "Done",
"target": "7",
"limit": "23",
"reviewer": "Assign reviewer"
},
{
"id": 10,
"header": "Advanced Algorithms and Machine Learning",
"type": "Narrative",
"status": "Done",
"target": "30",
"limit": "28",
"reviewer": "Assign reviewer"
},
{
"id": 11,
"header": "Adaptive Communication Protocols",
"type": "Narrative",
"status": "Done",
"target": "9",
"limit": "31",
"reviewer": "Assign reviewer"
},
{
"id": 12,
"header": "Advantages Over Current Technologies",
"type": "Narrative",
"status": "Done",
"target": "12",
"limit": "0",
"reviewer": "Assign reviewer"
},
{
"id": 13,
"header": "Past Performance",
"type": "Narrative",
"status": "Done",
"target": "22",
"limit": "33",
"reviewer": "Assign reviewer"
},
{
"id": 14,
"header": "Customer Feedback and Satisfaction Levels",
"type": "Narrative",
"status": "Done",
"target": "15",
"limit": "34",
"reviewer": "Assign reviewer"
},
{
"id": 15,
"header": "Implementation Challenges and Solutions",
"type": "Narrative",
"status": "Done",
"target": "3",
"limit": "35",
"reviewer": "Assign reviewer"
},
{
"id": 16,
"header": "Security Measures and Data Protection Policies",
"type": "Narrative",
"status": "In Process",
"target": "6",
"limit": "36",
"reviewer": "Assign reviewer"
},
{
"id": 17,
"header": "Scalability and Future Proofing",
"type": "Narrative",
"status": "Done",
"target": "4",
"limit": "37",
"reviewer": "Assign reviewer"
},
{
"id": 18,
"header": "Cost-Benefit Analysis",
"type": "Plain language",
"status": "Done",
"target": "14",
"limit": "38",
"reviewer": "Assign reviewer"
},
{
"id": 19,
"header": "User Training and Onboarding Experience",
"type": "Narrative",
"status": "Done",
"target": "17",
"limit": "39",
"reviewer": "Assign reviewer"
},
{
"id": 20,
"header": "Future Development Roadmap",
"type": "Narrative",
"status": "Done",
"target": "11",
"limit": "40",
"reviewer": "Assign reviewer"
},
{
"id": 21,
"header": "System Architecture Overview",
"type": "Technical content",
"status": "In Process",
"target": "24",
"limit": "18",
"reviewer": "Maya Johnson"
},
{
"id": 22,
"header": "Risk Management Plan",
"type": "Narrative",
"status": "Done",
"target": "15",
"limit": "22",
"reviewer": "Carlos Rodriguez"
},
{
"id": 23,
"header": "Compliance Documentation",
"type": "Legal",
"status": "In Process",
"target": "31",
"limit": "27",
"reviewer": "Sarah Chen"
},
{
"id": 24,
"header": "API Documentation",
"type": "Technical content",
"status": "Done",
"target": "8",
"limit": "12",
"reviewer": "Raj Patel"
},
{
"id": 25,
"header": "User Interface Mockups",
"type": "Visual",
"status": "In Process",
"target": "19",
"limit": "25",
"reviewer": "Leila Ahmadi"
},
{
"id": 26,
"header": "Database Schema",
"type": "Technical content",
"status": "Done",
"target": "22",
"limit": "20",
"reviewer": "Thomas Wilson"
},
{
"id": 27,
"header": "Testing Methodology",
"type": "Technical content",
"status": "In Process",
"target": "17",
"limit": "14",
"reviewer": "Assign reviewer"
},
{
"id": 28,
"header": "Deployment Strategy",
"type": "Narrative",
"status": "Done",
"target": "26",
"limit": "30",
"reviewer": "Eddie Lake"
},
{
"id": 29,
"header": "Budget Breakdown",
"type": "Financial",
"status": "In Process",
"target": "13",
"limit": "16",
"reviewer": "Jamik Tashpulatov"
},
{
"id": 30,
"header": "Market Analysis",
"type": "Research",
"status": "Done",
"target": "29",
"limit": "32",
"reviewer": "Sophia Martinez"
},
{
"id": 31,
"header": "Competitor Comparison",
"type": "Research",
"status": "In Process",
"target": "21",
"limit": "19",
"reviewer": "Assign reviewer"
},
{
"id": 32,
"header": "Maintenance Plan",
"type": "Technical content",
"status": "Done",
"target": "16",
"limit": "23",
"reviewer": "Alex Thompson"
},
{
"id": 33,
"header": "User Personas",
"type": "Research",
"status": "In Process",
"target": "27",
"limit": "24",
"reviewer": "Nina Patel"
},
{
"id": 34,
"header": "Accessibility Compliance",
"type": "Legal",
"status": "Done",
"target": "18",
"limit": "21",
"reviewer": "Assign reviewer"
},
{
"id": 35,
"header": "Performance Metrics",
"type": "Technical content",
"status": "In Process",
"target": "23",
"limit": "26",
"reviewer": "David Kim"
},
{
"id": 36,
"header": "Disaster Recovery Plan",
"type": "Technical content",
"status": "Done",
"target": "14",
"limit": "17",
"reviewer": "Jamik Tashpulatov"
},
{
"id": 37,
"header": "Third-party Integrations",
"type": "Technical content",
"status": "In Process",
"target": "25",
"limit": "28",
"reviewer": "Eddie Lake"
},
{
"id": 38,
"header": "User Feedback Summary",
"type": "Research",
"status": "Done",
"target": "20",
"limit": "15",
"reviewer": "Assign reviewer"
},
{
"id": 39,
"header": "Localization Strategy",
"type": "Narrative",
"status": "In Process",
"target": "12",
"limit": "19",
"reviewer": "Maria Garcia"
},
{
"id": 40,
"header": "Mobile Compatibility",
"type": "Technical content",
"status": "Done",
"target": "28",
"limit": "31",
"reviewer": "James Wilson"
},
{
"id": 41,
"header": "Data Migration Plan",
"type": "Technical content",
"status": "In Process",
"target": "19",
"limit": "22",
"reviewer": "Assign reviewer"
},
{
"id": 42,
"header": "Quality Assurance Protocols",
"type": "Technical content",
"status": "Done",
"target": "30",
"limit": "33",
"reviewer": "Priya Singh"
},
{
"id": 43,
"header": "Stakeholder Analysis",
"type": "Research",
"status": "In Process",
"target": "11",
"limit": "14",
"reviewer": "Eddie Lake"
},
{
"id": 44,
"header": "Environmental Impact Assessment",
"type": "Research",
"status": "Done",
"target": "24",
"limit": "27",
"reviewer": "Assign reviewer"
},
{
"id": 45,
"header": "Intellectual Property Rights",
"type": "Legal",
"status": "In Process",
"target": "17",
"limit": "20",
"reviewer": "Sarah Johnson"
},
{
"id": 46,
"header": "Customer Support Framework",
"type": "Narrative",
"status": "Done",
"target": "22",
"limit": "25",
"reviewer": "Jamik Tashpulatov"
},
{
"id": 47,
"header": "Version Control Strategy",
"type": "Technical content",
"status": "In Process",
"target": "15",
"limit": "18",
"reviewer": "Assign reviewer"
},
{
"id": 48,
"header": "Continuous Integration Pipeline",
"type": "Technical content",
"status": "Done",
"target": "26",
"limit": "29",
"reviewer": "Michael Chen"
},
{
"id": 49,
"header": "Regulatory Compliance",
"type": "Legal",
"status": "In Process",
"target": "13",
"limit": "16",
"reviewer": "Assign reviewer"
},
{
"id": 50,
"header": "User Authentication System",
"type": "Technical content",
"status": "Done",
"target": "28",
"limit": "31",
"reviewer": "Eddie Lake"
},
{
"id": 51,
"header": "Data Analytics Framework",
"type": "Technical content",
"status": "In Process",
"target": "21",
"limit": "24",
"reviewer": "Jamik Tashpulatov"
},
{
"id": 52,
"header": "Cloud Infrastructure",
"type": "Technical content",
"status": "Done",
"target": "16",
"limit": "19",
"reviewer": "Assign reviewer"
},
{
"id": 53,
"header": "Network Security Measures",
"type": "Technical content",
"status": "In Process",
"target": "29",
"limit": "32",
"reviewer": "Lisa Wong"
},
{
"id": 54,
"header": "Project Timeline",
"type": "Planning",
"status": "Done",
"target": "14",
"limit": "17",
"reviewer": "Eddie Lake"
},
{
"id": 55,
"header": "Resource Allocation",
"type": "Planning",
"status": "In Process",
"target": "27",
"limit": "30",
"reviewer": "Assign reviewer"
},
{
"id": 56,
"header": "Team Structure and Roles",
"type": "Planning",
"status": "Done",
"target": "20",
"limit": "23",
"reviewer": "Jamik Tashpulatov"
},
{
"id": 57,
"header": "Communication Protocols",
"type": "Planning",
"status": "In Process",
"target": "15",
"limit": "18",
"reviewer": "Assign reviewer"
},
{
"id": 58,
"header": "Success Metrics",
"type": "Planning",
"status": "Done",
"target": "30",
"limit": "33",
"reviewer": "Eddie Lake"
},
{
"id": 59,
"header": "Internationalization Support",
"type": "Technical content",
"status": "In Process",
"target": "23",
"limit": "26",
"reviewer": "Jamik Tashpulatov"
},
{
"id": 60,
"header": "Backup and Recovery Procedures",
"type": "Technical content",
"status": "Done",
"target": "18",
"limit": "21",
"reviewer": "Assign reviewer"
},
{
"id": 61,
"header": "Monitoring and Alerting System",
"type": "Technical content",
"status": "In Process",
"target": "25",
"limit": "28",
"reviewer": "Daniel Park"
},
{
"id": 62,
"header": "Code Review Guidelines",
"type": "Technical content",
"status": "Done",
"target": "12",
"limit": "15",
"reviewer": "Eddie Lake"
},
{
"id": 63,
"header": "Documentation Standards",
"type": "Technical content",
"status": "In Process",
"target": "27",
"limit": "30",
"reviewer": "Jamik Tashpulatov"
},
{
"id": 64,
"header": "Release Management Process",
"type": "Planning",
"status": "Done",
"target": "22",
"limit": "25",
"reviewer": "Assign reviewer"
},
{
"id": 65,
"header": "Feature Prioritization Matrix",
"type": "Planning",
"status": "In Process",
"target": "19",
"limit": "22",
"reviewer": "Emma Davis"
},
{
"id": 66,
"header": "Technical Debt Assessment",
"type": "Technical content",
"status": "Done",
"target": "24",
"limit": "27",
"reviewer": "Eddie Lake"
},
{
"id": 67,
"header": "Capacity Planning",
"type": "Planning",
"status": "In Process",
"target": "21",
"limit": "24",
"reviewer": "Jamik Tashpulatov"
},
{
"id": 68,
"header": "Service Level Agreements",
"type": "Legal",
"status": "Done",
"target": "26",
"limit": "29",
"reviewer": "Assign reviewer"
}
]

View File

@@ -0,0 +1,47 @@
[
{
"id": 1,
"header": "Technical Specifications Document v2.1",
"type": "Technical Document",
"status": "Final",
"target": "100%",
"limit": "100%",
"reviewer": "Eddie Lake"
},
{
"id": 2,
"header": "Security Compliance Report Q4 2024",
"type": "Compliance Document",
"status": "Under Review",
"target": "95%",
"limit": "100%",
"reviewer": "Jamik Tashpulatov"
},
{
"id": 3,
"header": "Project Management Plan v3.0",
"type": "Management Document",
"status": "Final",
"target": "100%",
"limit": "100%",
"reviewer": "Emily Whalen"
},
{
"id": 4,
"header": "Risk Assessment Matrix 2025",
"type": "Risk Document",
"status": "Draft",
"target": "80%",
"limit": "90%",
"reviewer": "Eddie Lake"
},
{
"id": 5,
"header": "Quality Assurance Protocol v1.5",
"type": "QA Document",
"status": "Final",
"target": "100%",
"limit": "100%",
"reviewer": "Jamik Tashpulatov"
}
]

View File

@@ -0,0 +1,47 @@
[
{
"id": 1,
"header": "Dr. Sarah Mitchell",
"type": "Project Manager",
"status": "Active",
"target": "15 years",
"limit": "20 years",
"reviewer": "Eddie Lake"
},
{
"id": 2,
"header": "James Thompson",
"type": "Lead Engineer",
"status": "Active",
"target": "12 years",
"limit": "15 years",
"reviewer": "Jamik Tashpulatov"
},
{
"id": 3,
"header": "Maria Rodriguez",
"type": "Security Specialist",
"status": "Active",
"target": "8 years",
"limit": "10 years",
"reviewer": "Emily Whalen"
},
{
"id": 4,
"header": "David Chen",
"type": "Systems Architect",
"status": "Active",
"target": "10 years",
"limit": "12 years",
"reviewer": "Eddie Lake"
},
{
"id": 5,
"header": "Lisa Johnson",
"type": "Quality Assurance Lead",
"status": "Active",
"target": "6 years",
"limit": "8 years",
"reviewer": "Jamik Tashpulatov"
}
]

View File

@@ -0,0 +1,47 @@
[
{
"id": 1,
"header": "Federal Communications Commission - Network Infrastructure Modernization",
"type": "Government Contract",
"status": "Completed",
"target": "95%",
"limit": "100%",
"reviewer": "Eddie Lake"
},
{
"id": 2,
"header": "Department of Defense - Cybersecurity Enhancement Program",
"type": "Defense Contract",
"status": "Completed",
"target": "98%",
"limit": "100%",
"reviewer": "Jamik Tashpulatov"
},
{
"id": 3,
"header": "NASA - Satellite Communication System Upgrade",
"type": "Space Technology",
"status": "Completed",
"target": "92%",
"limit": "95%",
"reviewer": "Emily Whalen"
},
{
"id": 4,
"header": "Department of Homeland Security - Border Security Tech",
"type": "Security Contract",
"status": "In Progress",
"target": "85%",
"limit": "90%",
"reviewer": "Eddie Lake"
},
{
"id": 5,
"header": "GSA - Cloud Infrastructure Migration",
"type": "IT Services",
"status": "Completed",
"target": "96%",
"limit": "98%",
"reviewer": "Jamik Tashpulatov"
}
]

View File

@@ -0,0 +1,28 @@
import { BaseLayout } from "@/components/layouts/base-layout"
import { ChartAreaInteractive } from "./components/chart-area-interactive"
import { DataTable } from "./components/data-table"
import { SectionCards } from "./components/section-cards"
import data from "./data/data.json"
import pastPerformanceData from "./data/past-performance-data.json"
import keyPersonnelData from "./data/key-personnel-data.json"
import focusDocumentsData from "./data/focus-documents-data.json"
export default function Page() {
return (
<BaseLayout title="Dashboard" description="Welcome to your admin dashboard">
<div className="@container/main px-4 lg:px-6 space-y-6">
<SectionCards />
<ChartAreaInteractive />
</div>
<div className="@container/main">
<DataTable
data={data}
pastPerformanceData={pastPerformanceData}
keyPersonnelData={keyPersonnelData}
focusDocumentsData={focusDocumentsData}
/>
</div>
</BaseLayout>
)
}

View File

@@ -0,0 +1,13 @@
import { z } from "zod"
export const schema = z.object({
id: z.number(),
header: z.string(),
type: z.string(),
status: z.string(),
target: z.string(),
limit: z.string(),
reviewer: z.string(),
})
export type Task = z.infer<typeof schema>

View File

@@ -0,0 +1,29 @@
"use client"
import { Button } from "@/components/ui/button"
import { useNavigate } from "react-router-dom"
export function ForbiddenError() {
const navigate = useNavigate()
return (
<div className='mx-auto flex min-h-dvh flex-col items-center justify-center gap-8 p-8 md:gap-12 md:p-16'>
<img
src='https://ui.shadcn.com/placeholder.svg'
alt='placeholder image'
className='aspect-video w-240 rounded-xl object-cover dark:brightness-[0.95] dark:invert'
/>
<div className='text-center'>
<h1 className='mb-4 text-3xl font-bold'>403</h1>
<h2 className="mb-3 text-2xl font-semibold">Forbidden</h2>
<p>Access to this resource is forbidden. You don't have the necessary permissions to view this page.</p>
<div className='mt-6 flex items-center justify-center gap-4 md:mt-8'>
<Button className='cursor-pointer' onClick={() => navigate('/dashboard')}>Go Back Home</Button>
<Button variant='outline' className='flex cursor-pointer items-center gap-1' onClick={() => navigate('#')}>
Contact Us
</Button>
</div>
</div>
</div>
)
}

View File

@@ -0,0 +1,5 @@
import { ForbiddenError } from "./components/forbidden-error"
export default function ForbiddenPage() {
return <ForbiddenError />
}

View File

@@ -0,0 +1,29 @@
"use client"
import { Button } from "@/components/ui/button"
import { useNavigate } from "react-router-dom"
export function InternalServerError() {
const navigate = useNavigate()
return (
<div className='mx-auto flex min-h-dvh flex-col items-center justify-center gap-8 p-8 md:gap-12 md:p-16'>
<img
src='https://ui.shadcn.com/placeholder.svg'
alt='placeholder image'
className='aspect-video w-240 rounded-xl object-cover dark:brightness-[0.95] dark:invert'
/>
<div className='text-center'>
<h1 className='mb-4 text-3xl font-bold'>500</h1>
<h2 className="mb-3 text-2xl font-semibold">Internal Server Error</h2>
<p>Something went wrong on our end. We're working to fix the issue. Please try again later.</p>
<div className='mt-6 flex items-center justify-center gap-4 md:mt-8'>
<Button className='cursor-pointer' onClick={() => navigate('/dashboard')}>Go Back Home</Button>
<Button variant='outline' className='flex cursor-pointer items-center gap-1' onClick={() => navigate('')}>
Contact Us
</Button>
</div>
</div>
</div>
)
}

View File

@@ -0,0 +1,5 @@
import { InternalServerError } from "./components/internal-server-error"
export default function InternalServerErrorPage() {
return <InternalServerError />
}

View File

@@ -0,0 +1,29 @@
"use client"
import { Button } from "@/components/ui/button"
import { useNavigate } from "react-router-dom"
export function NotFoundError() {
const navigate = useNavigate()
return (
<div className='mx-auto flex min-h-dvh flex-col items-center justify-center gap-8 p-8 md:gap-12 md:p-16'>
<img
src='https://ui.shadcn.com/placeholder.svg'
alt='placeholder image'
className='aspect-video w-240 rounded-xl object-cover dark:brightness-[0.95] dark:invert'
/>
<div className='text-center'>
<h1 className='mb-4 text-3xl font-bold'>404</h1>
<h2 className="mb-3 text-2xl font-semibold">Page Not Found</h2>
<p>The page you are looking for doesn't exist or has been moved to another location.</p>
<div className='mt-6 flex items-center justify-center gap-4 md:mt-8'>
<Button className='cursor-pointer' onClick={() => navigate('/dashboard')}>Go Back Home</Button>
<Button variant='outline' className='flex cursor-pointer items-center gap-1' onClick={() => navigate('#')}>
Contact Us
</Button>
</div>
</div>
</div>
)
}

View File

@@ -0,0 +1,5 @@
import { NotFoundError } from "./components/not-found-error"
export default function NotFoundPage() {
return <NotFoundError />
}

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