1 Commits

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

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

7
.gitignore vendored
View File

@@ -1,4 +1,3 @@
node_modules/
dist/
.env
.DS_Store
/node_modules/
/repo/
package-lock.json

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

23
eslint.config.js Normal file
View File

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

44
index.html Normal file
View File

@@ -0,0 +1,44 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<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" />
<!-- 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>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>

77
package.json Normal file
View File

@@ -0,0 +1,77 @@
{
"name": "shadcn-dashboard-vite",
"private": true,
"version": "1.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc -b && vite build",
"lint": "eslint .",
"preview": "vite preview"
},
"dependencies": {
"@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": "^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

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

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

42
src/App.css Normal file
View File

@@ -0,0 +1,42 @@
#root {
max-width: 1280px;
margin: 0 auto;
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);
}
}
@media (prefers-reduced-motion: no-preference) {
a:nth-of-type(2) .logo {
animation: logo-spin infinite 20s linear;
}
}
.card {
padding: 2em;
}
.read-the-docs {
color: #888;
}

30
src/App.tsx Normal file
View File

@@ -0,0 +1,30 @@
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'
// Get basename from environment (for deployment) or use empty string for development
const basename = import.meta.env.VITE_BASENAME || ''
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 />
}

View File

@@ -0,0 +1,29 @@
"use client"
import { Button } from "@/components/ui/button"
import { useNavigate } from "react-router-dom"
export function UnauthorizedError() {
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'>401</h1>
<h2 className="mb-3 text-2xl font-semibold">Unauthorized</h2>
<p>You don't have permission to access this resource. Please sign in or contact your administrator.</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 { UnauthorizedError } from "./components/unauthorized-error"
export default function UnauthorizedPage() {
return <UnauthorizedError />
}

View File

@@ -0,0 +1,29 @@
"use client"
import { Button } from "@/components/ui/button"
import { useNavigate } from "react-router-dom"
export function UnderMaintenanceError() {
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'>503</h1>
<h2 className="mb-3 text-2xl font-semibold">Under Maintenance</h2>
<p>The service is currently unavailable. 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 { UnderMaintenanceError } from "./components/under-maintenance-error"
export default function UnderMaintenancePage() {
return <UnderMaintenanceError />
}

View File

@@ -0,0 +1,129 @@
"use client"
import { useState } from "react"
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
import { Input } from "@/components/ui/input"
import { Badge } from "@/components/ui/badge"
import { ScrollArea } from "@/components/ui/scroll-area"
import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from '@/components/ui/accordion'
import { cn } from "@/lib/utils"
import { Search } from "lucide-react"
interface FAQ {
id: number
question: string
answer: string
category: string
}
interface Category {
name: string
count: number
}
interface FAQListProps {
faqs: FAQ[]
categories: Category[]
}
export function FAQList({ faqs, categories }: FAQListProps) {
const [selectedCategory, setSelectedCategory] = useState("All")
const [searchQuery, setSearchQuery] = useState("")
// Filter FAQs based on selected category and search query
const filteredFaqs = faqs.filter(faq => {
const matchesCategory = selectedCategory === "All" || faq.category === selectedCategory
const matchesSearch = searchQuery === "" ||
faq.question.toLowerCase().includes(searchQuery.toLowerCase()) ||
faq.answer.toLowerCase().includes(searchQuery.toLowerCase())
return matchesCategory && matchesSearch
})
return (
<div className="grid grid-cols-1 lg:grid-cols-6 xl:grid-cols-4 gap-6">
{/* Categories Sidebar */}
<Card className="lg:col-span-2 xl:col-span-1">
<CardHeader>
<CardTitle className="text-lg">Categories</CardTitle>
<div className="relative">
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-muted-foreground" />
<Input
placeholder="Search FAQs..."
className="pl-10 cursor-pointer"
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
/>
</div>
</CardHeader>
<CardContent className="space-y-2">
{categories.map((category) => (
<div
key={category.name}
className={cn(
"flex items-center justify-between px-3 py-2 rounded-lg hover:bg-muted cursor-pointer transition-colors group",
selectedCategory === category.name && "bg-muted"
)}
onClick={() => setSelectedCategory(category.name)}
>
<span className="font-medium">{category.name}</span>
<Badge
variant="secondary"
className={cn(
"transition-colors",
selectedCategory === category.name && "bg-background"
)}
>
{category.name === "All" ? faqs.length : category.count}
</Badge>
</div>
))}
</CardContent>
</Card>
{/* FAQs List */}
<div className="lg:col-span-4 xl:col-span-3">
<Card>
<CardHeader>
<CardTitle className="text-lg">
{selectedCategory === "All" ? "All FAQs" : `${selectedCategory} FAQs`}
<span className="text-sm font-normal text-muted-foreground ml-2">
({filteredFaqs.length} {filteredFaqs.length === 1 ? 'question' : 'questions'})
</span>
</CardTitle>
</CardHeader>
<CardContent>
<ScrollArea className="h-[570px] pr-4">
{filteredFaqs.length === 0 ? (
<div className="text-center py-8 text-muted-foreground">
<p>No FAQs found matching your search criteria.</p>
</div>
) : (
<Accordion type='single' className='space-y-4' defaultValue="item-1">
{filteredFaqs.map((item) => (
<AccordionItem
key={item.id}
value={`item-${item.id}`}
className='rounded-md !border'
>
<AccordionTrigger className='cursor-pointer px-4 hover:no-underline'>
<div className="flex items-start text-left">
<span>{item.question}</span>
<Badge variant="outline" className="ms-3 mt-0.5 shrink-0 text-xs">
{item.category}
</Badge>
</div>
</AccordionTrigger>
<AccordionContent className='text-muted-foreground px-4'>
{item.answer}
</AccordionContent>
</AccordionItem>
))}
</Accordion>
)}
</ScrollArea>
</CardContent>
</Card>
</div>
</div>
)
}

View File

@@ -0,0 +1,54 @@
import { Card, CardContent } from "@/components/ui/card"
import { Badge } from "@/components/ui/badge"
import { Button } from '@/components/ui/button'
import { ArrowRight, Sparkles, Shield, Truck, Clock } from 'lucide-react'
interface FeatureItem {
id: number
title: string
description: string
icon: string
}
interface FeaturesGridProps {
features: FeatureItem[]
}
const iconMap = {
Sparkles,
Shield,
Truck,
Clock,
}
export function FeaturesGrid({ features }: FeaturesGridProps) {
return (
<div className='grid gap-4 sm:grid-cols-2 sm:gap-6 xl:grid-cols-4 mt-8'>
{features.map(feature => {
const IconComponent = iconMap[feature.icon as keyof typeof iconMap]
return (
<article key={feature.id} className='group'>
<Card className='relative h-full overflow-hidden transition-all hover:shadow-md'>
<CardContent className='px-6'>
<Badge variant='secondary' className='mb-4 inline-flex size-12 items-center justify-center'>
<IconComponent className='!size-5' aria-hidden='true' />
</Badge>
<h3 className='mb-2 text-lg font-semibold'>{feature.title}</h3>
<p className='text-muted-foreground mb-4 text-sm'>{feature.description}</p>
<Button
variant='link'
size='sm'
className='text-muted-foreground hover:text-foreground h-auto cursor-pointer !p-0 text-sm'
>
Learn more
<ArrowRight className='ms-1.5 size-4' />
</Button>
</CardContent>
</Card>
</article>
)
})}
</div>
)
}

View File

@@ -0,0 +1,10 @@
[
{ "name": "All", "count": 46 },
{ "name": "General", "count": 8 },
{ "name": "Account", "count": 6 },
{ "name": "Billing", "count": 8 },
{ "name": "Technical", "count": 9 },
{ "name": "Privacy", "count": 5 },
{ "name": "Security", "count": 4 },
{ "name": "Support", "count": 6 }
]

278
src/app/faqs/data/faqs.json Normal file
View File

@@ -0,0 +1,278 @@
[
{
"id": 1,
"question": "What is ShadcnStore Admin?",
"answer": "ShadcnStore Admin is a comprehensive admin dashboard template built with React, TypeScript, and shadcn/ui components. It provides a complete solution for managing your e-commerce store or business operations.",
"category": "General"
},
{
"id": 2,
"question": "How do I get started?",
"answer": "You can get started by signing up for an account, choosing a plan that fits your needs, and following our quick setup guide to configure your dashboard.",
"category": "General"
},
{
"id": 3,
"question": "Do you offer a free trial?",
"answer": "Yes, we offer a 14-day free trial for all new users. No credit card is required to start the trial, and you can explore all features during this period.",
"category": "General"
},
{
"id": 4,
"question": "What browsers are supported?",
"answer": "We support all modern browsers including Chrome, Firefox, Safari, and Edge. For the best experience, we recommend using the latest version of your preferred browser.",
"category": "General"
},
{
"id": 5,
"question": "How do I contact support?",
"answer": "You can contact our support team through the support page, by email at support@shadcnstore.com, or through the live chat feature available 24/7.",
"category": "General"
},
{
"id": 6,
"question": "Is there a mobile app available?",
"answer": "Currently, we offer a responsive web application that works great on mobile devices. A dedicated mobile app is planned for future release.",
"category": "General"
},
{
"id": 7,
"question": "Can I customize the dashboard?",
"answer": "Yes, the dashboard is highly customizable. You can modify themes, layouts, add custom components, and configure various settings to match your brand.",
"category": "General"
},
{
"id": 8,
"question": "What integrations are available?",
"answer": "We offer integrations with popular services like Stripe, PayPal, Shopify, WooCommerce, Google Analytics, and many more through our API.",
"category": "General"
},
{
"id": 9,
"question": "How do I reset my password?",
"answer": "You can reset your password by clicking on the 'Forgot Password' link on the login page. Enter your email address, and we'll send you instructions to reset your password.",
"category": "Account"
},
{
"id": 10,
"question": "How do I change my email address?",
"answer": "You can change your email address in your account settings under the 'User Settings' section. You'll need to verify the new email address before the change takes effect.",
"category": "Account"
},
{
"id": 11,
"question": "Can I have multiple team members?",
"answer": "Yes, depending on your plan, you can invite team members and assign different roles and permissions to manage your store collaboratively.",
"category": "Account"
},
{
"id": 12,
"question": "How do I delete my account?",
"answer": "To delete your account, go to your account settings and select 'Delete Account'. Please note that this action is irreversible and all data will be permanently removed.",
"category": "Account"
},
{
"id": 13,
"question": "Can I change my username?",
"answer": "Yes, you can change your username in the account settings. Keep in mind that some features might reference your old username temporarily.",
"category": "Account"
},
{
"id": 14,
"question": "How do I enable two-factor authentication?",
"answer": "You can enable two-factor authentication in your account security settings. We support both SMS and authenticator app methods for added security.",
"category": "Account"
},
{
"id": 15,
"question": "What payment methods do you accept?",
"answer": "We accept all major credit cards (Visa, MasterCard, American Express), PayPal, and bank transfers for enterprise customers. All payments are processed securely.",
"category": "Billing"
},
{
"id": 16,
"question": "How can I upgrade my plan?",
"answer": "You can upgrade your plan at any time from your account settings. Go to 'Plans & Billing' and select the plan that best fits your needs. Changes take effect immediately.",
"category": "Billing"
},
{
"id": 17,
"question": "Can I downgrade my plan?",
"answer": "Yes, you can downgrade your plan at any time. The change will take effect at the start of your next billing cycle to ensure you don't lose access to premium features.",
"category": "Billing"
},
{
"id": 18,
"question": "Do you offer refunds?",
"answer": "We offer a 30-day money-back guarantee for all plans. If you're not satisfied, contact our support team for a full refund within 30 days of purchase.",
"category": "Billing"
},
{
"id": 19,
"question": "How does billing work?",
"answer": "Billing is processed monthly or annually depending on your chosen plan. You'll receive an invoice before each billing cycle, and payment is automatically charged to your selected method.",
"category": "Billing"
},
{
"id": 20,
"question": "Can I change my billing cycle?",
"answer": "Yes, you can switch between monthly and annual billing at any time. Annual billing offers significant savings compared to monthly billing.",
"category": "Billing"
},
{
"id": 21,
"question": "What happens if payment fails?",
"answer": "If a payment fails, we'll attempt to charge your card again after 3 days. You'll receive email notifications, and your account will remain active during this grace period.",
"category": "Billing"
},
{
"id": 22,
"question": "How do I view my billing history?",
"answer": "You can view your complete billing history in the 'Plans & Billing' section of your account settings. All invoices and receipts are available for download.",
"category": "Billing"
},
{
"id": 23,
"question": "Can I export my data?",
"answer": "Yes, you can export your data at any time from your account settings. We provide exports in multiple formats including CSV, JSON, and PDF for different data types.",
"category": "Technical"
},
{
"id": 24,
"question": "What APIs do you provide?",
"answer": "We provide comprehensive REST APIs for all major features including product management, order processing, customer data, and analytics. Full documentation is available.",
"category": "Technical"
},
{
"id": 25,
"question": "How do I backup my data?",
"answer": "We automatically backup all your data daily. You can also create manual backups anytime from your settings, and restore from any backup point within the last 30 days.",
"category": "Technical"
},
{
"id": 26,
"question": "Is there a rate limit on API calls?",
"answer": "Yes, API rate limits vary by plan. Basic plans have 1000 calls/hour, Professional plans have 10,000 calls/hour, and Enterprise plans have unlimited calls.",
"category": "Technical"
},
{
"id": 27,
"question": "How do I set up webhooks?",
"answer": "Webhooks can be configured in the 'Connections' section of your settings. You can set up webhooks for various events like new orders, payment confirmations, and inventory updates.",
"category": "Technical"
},
{
"id": 28,
"question": "What about system maintenance?",
"answer": "We perform maintenance during low-traffic hours (typically Sunday 2-4 AM UTC). You'll be notified at least 48 hours in advance of any scheduled maintenance.",
"category": "Technical"
},
{
"id": 29,
"question": "How do I troubleshoot connection issues?",
"answer": "First, check your internet connection and try refreshing the page. If issues persist, check our status page or contact support with specific error messages.",
"category": "Technical"
},
{
"id": 30,
"question": "Can I use custom domains?",
"answer": "Yes, Professional and Enterprise plans support custom domains. You can configure your custom domain in the 'Connections' section of your account settings.",
"category": "Technical"
},
{
"id": 31,
"question": "What databases do you support?",
"answer": "We support integration with MySQL, PostgreSQL, MongoDB, and other popular databases through our Database Sync feature available in higher-tier plans.",
"category": "Technical"
},
{
"id": 32,
"question": "How do you handle my personal data?",
"answer": "We follow strict data protection policies and comply with GDPR, CCPA, and other privacy regulations. Your personal data is never shared with third parties without your consent.",
"category": "Privacy"
},
{
"id": 33,
"question": "Can I request my data?",
"answer": "Yes, you can request a complete copy of your personal data at any time. We'll provide it in a machine-readable format within 30 days of your request.",
"category": "Privacy"
},
{
"id": 34,
"question": "How long do you retain data?",
"answer": "We retain your data as long as your account is active. After account deletion, personal data is removed within 30 days, though some anonymized analytics may be retained.",
"category": "Privacy"
},
{
"id": 35,
"question": "Do you use cookies?",
"answer": "Yes, we use essential cookies for functionality and optional cookies for analytics and personalization. You can manage your cookie preferences in your account settings.",
"category": "Privacy"
},
{
"id": 36,
"question": "Is my data encrypted?",
"answer": "Yes, all data is encrypted both in transit (using TLS 1.3) and at rest (using AES-256 encryption). We use industry-standard security practices to protect your information.",
"category": "Privacy"
},
{
"id": 37,
"question": "How secure is my data?",
"answer": "We implement bank-level security with end-to-end encryption, regular security audits, and compliance with SOC 2 Type II standards. Your data security is our top priority.",
"category": "Security"
},
{
"id": 38,
"question": "Do you support SSO?",
"answer": "Yes, Enterprise plans include Single Sign-On (SSO) support with popular providers like Google, Microsoft Azure AD, and Okta for seamless team access.",
"category": "Security"
},
{
"id": 39,
"question": "What about password requirements?",
"answer": "We require strong passwords with at least 8 characters, including uppercase, lowercase, numbers, and special characters. We also highly recommend enabling two-factor authentication.",
"category": "Security"
},
{
"id": 40,
"question": "How do you handle security incidents?",
"answer": "We have a comprehensive incident response plan. In case of any security issues, we immediately investigate, contain the issue, and notify affected users within 24 hours.",
"category": "Security"
},
{
"id": 41,
"question": "What support channels are available?",
"answer": "We offer email support, live chat, and phone support (for Enterprise customers). Our knowledge base and community forums are also available 24/7.",
"category": "Support"
},
{
"id": 42,
"question": "What are your support hours?",
"answer": "Email and chat support are available 24/7. Phone support for Enterprise customers is available Monday-Friday, 9 AM-6 PM in your local timezone.",
"category": "Support"
},
{
"id": 43,
"question": "How quickly will I get a response?",
"answer": "Response times vary by plan: Basic (24 hours), Professional (12 hours), Enterprise (2 hours). Critical issues are prioritized and responded to immediately.",
"category": "Support"
},
{
"id": 44,
"question": "Do you offer training?",
"answer": "Yes, we provide comprehensive onboarding for all plans, video tutorials, documentation, and personalized training sessions for Enterprise customers.",
"category": "Support"
},
{
"id": 45,
"question": "Can you help with custom implementations?",
"answer": "Enterprise customers get access to our professional services team for custom implementations, integrations, and consulting services.",
"category": "Support"
},
{
"id": 46,
"question": "Is there a community forum?",
"answer": "Yes, we have an active community forum where users share tips, ask questions, and get help from both our team and other community members.",
"category": "Support"
}
]

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