mirror of
https://github.com/cfngc4594/monaco-editor-lsp-next.git
synced 2025-07-04 17:30:52 +00:00
refactor(layout): 重构侧边栏和用户导航组件
- 更新了 AppSidebar、NavMain 和 NavUser 组件的结构和样式 - 添加了新的导航项和图标 - 调整了用户信息的展示方式 - 优化了侧边栏的布局和样式
This commit is contained in:
parent
3df53658e8
commit
6df1f64376
20
bun.lock
20
bun.lock
@ -8,6 +8,10 @@
|
|||||||
"@ai-sdk/openai": "^1.3.0",
|
"@ai-sdk/openai": "^1.3.0",
|
||||||
"@ai-sdk/react": "^1.2.0",
|
"@ai-sdk/react": "^1.2.0",
|
||||||
"@auth/prisma-adapter": "^2.8.0",
|
"@auth/prisma-adapter": "^2.8.0",
|
||||||
|
"@dnd-kit/core": "^6.3.1",
|
||||||
|
"@dnd-kit/modifiers": "^9.0.0",
|
||||||
|
"@dnd-kit/sortable": "^10.0.0",
|
||||||
|
"@dnd-kit/utilities": "^3.2.2",
|
||||||
"@fontsource/fira-code": "^5.1.1",
|
"@fontsource/fira-code": "^5.1.1",
|
||||||
"@hookform/resolvers": "^5.1.1",
|
"@hookform/resolvers": "^5.1.1",
|
||||||
"@monaco-editor/react": "^4.7.0",
|
"@monaco-editor/react": "^4.7.0",
|
||||||
@ -38,7 +42,7 @@
|
|||||||
"@radix-ui/react-toggle": "^1.1.9",
|
"@radix-ui/react-toggle": "^1.1.9",
|
||||||
"@radix-ui/react-toggle-group": "^1.1.10",
|
"@radix-ui/react-toggle-group": "^1.1.10",
|
||||||
"@radix-ui/react-tooltip": "^1.2.7",
|
"@radix-ui/react-tooltip": "^1.2.7",
|
||||||
"@tanstack/react-table": "^8.21.2",
|
"@tanstack/react-table": "^8.21.3",
|
||||||
"@types/vscode": "^1.97.0",
|
"@types/vscode": "^1.97.0",
|
||||||
"ai": "^4.2.0",
|
"ai": "^4.2.0",
|
||||||
"bcryptjs": "^3.0.2",
|
"bcryptjs": "^3.0.2",
|
||||||
@ -83,7 +87,7 @@
|
|||||||
"vaul": "^1.1.2",
|
"vaul": "^1.1.2",
|
||||||
"vscode-languageclient": "^9.0.1",
|
"vscode-languageclient": "^9.0.1",
|
||||||
"vscode-ws-jsonrpc": "^3.4.0",
|
"vscode-ws-jsonrpc": "^3.4.0",
|
||||||
"zod": "^3.25.64",
|
"zod": "^3.25.67",
|
||||||
"zustand": "^5.0.3",
|
"zustand": "^5.0.3",
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
@ -141,6 +145,16 @@
|
|||||||
|
|
||||||
"@date-fns/tz": ["@date-fns/tz@1.2.0", "https://registry.npmmirror.com/@date-fns/tz/-/tz-1.2.0.tgz", {}, "sha512-LBrd7MiJZ9McsOgxqWX7AaxrDjcFVjWH/tIKJd7pnR7McaslGYOP1QmmiBXdJH/H/yLCT+rcQ7FaPBUxRGUtrg=="],
|
"@date-fns/tz": ["@date-fns/tz@1.2.0", "https://registry.npmmirror.com/@date-fns/tz/-/tz-1.2.0.tgz", {}, "sha512-LBrd7MiJZ9McsOgxqWX7AaxrDjcFVjWH/tIKJd7pnR7McaslGYOP1QmmiBXdJH/H/yLCT+rcQ7FaPBUxRGUtrg=="],
|
||||||
|
|
||||||
|
"@dnd-kit/accessibility": ["@dnd-kit/accessibility@3.1.1", "https://registry.npmmirror.com/@dnd-kit/accessibility/-/accessibility-3.1.1.tgz", { "dependencies": { "tslib": "^2.0.0" }, "peerDependencies": { "react": ">=16.8.0" } }, "sha512-2P+YgaXF+gRsIihwwY1gCsQSYnu9Zyj2py8kY5fFvUM1qm2WA2u639R6YNVfU4GWr+ZM5mqEsfHZZLoRONbemw=="],
|
||||||
|
|
||||||
|
"@dnd-kit/core": ["@dnd-kit/core@6.3.1", "https://registry.npmmirror.com/@dnd-kit/core/-/core-6.3.1.tgz", { "dependencies": { "@dnd-kit/accessibility": "^3.1.1", "@dnd-kit/utilities": "^3.2.2", "tslib": "^2.0.0" }, "peerDependencies": { "react": ">=16.8.0", "react-dom": ">=16.8.0" } }, "sha512-xkGBRQQab4RLwgXxoqETICr6S5JlogafbhNsidmrkVv2YRs5MLwpjoF2qpiGjQt8S9AoxtIV603s0GIUpY5eYQ=="],
|
||||||
|
|
||||||
|
"@dnd-kit/modifiers": ["@dnd-kit/modifiers@9.0.0", "https://registry.npmmirror.com/@dnd-kit/modifiers/-/modifiers-9.0.0.tgz", { "dependencies": { "@dnd-kit/utilities": "^3.2.2", "tslib": "^2.0.0" }, "peerDependencies": { "@dnd-kit/core": "^6.3.0", "react": ">=16.8.0" } }, "sha512-ybiLc66qRGuZoC20wdSSG6pDXFikui/dCNGthxv4Ndy8ylErY0N3KVxY2bgo7AWwIbxDmXDg3ylAFmnrjcbVvw=="],
|
||||||
|
|
||||||
|
"@dnd-kit/sortable": ["@dnd-kit/sortable@10.0.0", "https://registry.npmmirror.com/@dnd-kit/sortable/-/sortable-10.0.0.tgz", { "dependencies": { "@dnd-kit/utilities": "^3.2.2", "tslib": "^2.0.0" }, "peerDependencies": { "@dnd-kit/core": "^6.3.0", "react": ">=16.8.0" } }, "sha512-+xqhmIIzvAYMGfBYYnbKuNicfSsk4RksY2XdmJhT+HAC01nix6fHCztU68jooFiMUB01Ky3F0FyOvhG/BZrWkg=="],
|
||||||
|
|
||||||
|
"@dnd-kit/utilities": ["@dnd-kit/utilities@3.2.2", "https://registry.npmmirror.com/@dnd-kit/utilities/-/utilities-3.2.2.tgz", { "dependencies": { "tslib": "^2.0.0" }, "peerDependencies": { "react": ">=16.8.0" } }, "sha512-+MKAJEOfaBe5SmV6t34p80MMKhjvUz0vRrvVJbPT0WElzaOJ/1xs+D+KDv+tD/NE5ujfrChEcshd4fLn0wpiqg=="],
|
||||||
|
|
||||||
"@emnapi/core": ["@emnapi/core@1.4.3", "https://registry.npmmirror.com/@emnapi/core/-/core-1.4.3.tgz", { "dependencies": { "@emnapi/wasi-threads": "1.0.2", "tslib": "^2.4.0" } }, "sha512-4m62DuCE07lw01soJwPiBGC0nAww0Q+RY70VZ+n49yDIO13yyinhbWCeNnaob0lakDtWQzSdtNWzJeOJt2ma+g=="],
|
"@emnapi/core": ["@emnapi/core@1.4.3", "https://registry.npmmirror.com/@emnapi/core/-/core-1.4.3.tgz", { "dependencies": { "@emnapi/wasi-threads": "1.0.2", "tslib": "^2.4.0" } }, "sha512-4m62DuCE07lw01soJwPiBGC0nAww0Q+RY70VZ+n49yDIO13yyinhbWCeNnaob0lakDtWQzSdtNWzJeOJt2ma+g=="],
|
||||||
|
|
||||||
"@emnapi/runtime": ["@emnapi/runtime@1.4.3", "https://registry.npmmirror.com/@emnapi/runtime/-/runtime-1.4.3.tgz", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-pBPWdu6MLKROBX05wSNKcNb++m5Er+KQ9QkB+WVM+pW2Kx9hoSrVTnu3BdkI5eBLZoKu/J6mW/B6i6bJB2ytXQ=="],
|
"@emnapi/runtime": ["@emnapi/runtime@1.4.3", "https://registry.npmmirror.com/@emnapi/runtime/-/runtime-1.4.3.tgz", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-pBPWdu6MLKROBX05wSNKcNb++m5Er+KQ9QkB+WVM+pW2Kx9hoSrVTnu3BdkI5eBLZoKu/J6mW/B6i6bJB2ytXQ=="],
|
||||||
@ -1943,7 +1957,7 @@
|
|||||||
|
|
||||||
"yocto-queue": ["yocto-queue@0.1.0", "https://registry.npmmirror.com/yocto-queue/-/yocto-queue-0.1.0.tgz", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="],
|
"yocto-queue": ["yocto-queue@0.1.0", "https://registry.npmmirror.com/yocto-queue/-/yocto-queue-0.1.0.tgz", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="],
|
||||||
|
|
||||||
"zod": ["zod@3.25.64", "https://registry.npmmirror.com/zod/-/zod-3.25.64.tgz", {}, "sha512-hbP9FpSZf7pkS7hRVUrOjhwKJNyampPgtXKc3AN6DsWtoHsg2Sb4SQaS4Tcay380zSwd2VPo9G9180emBACp5g=="],
|
"zod": ["zod@3.25.67", "https://registry.npmmirror.com/zod/-/zod-3.25.67.tgz", {}, "sha512-idA2YXwpCdqUSKRCACDE6ItZD9TZzy3OZMtpfLoh6oPR47lipysRrJfjzMqFxQ3uJuUPyUeWe1r9vLH33xO/Qw=="],
|
||||||
|
|
||||||
"zod-to-json-schema": ["zod-to-json-schema@3.24.5", "https://registry.npmmirror.com/zod-to-json-schema/-/zod-to-json-schema-3.24.5.tgz", { "peerDependencies": { "zod": "^3.24.1" } }, "sha512-/AuWwMP+YqiPbsJx5D6TfgRTc4kTLjsh5SOcd4bLsfUg2RcEXrFMJl1DGgdHy2aCfsIA/cr/1JM0xcB2GZji8g=="],
|
"zod-to-json-schema": ["zod-to-json-schema@3.24.5", "https://registry.npmmirror.com/zod-to-json-schema/-/zod-to-json-schema-3.24.5.tgz", { "peerDependencies": { "zod": "^3.24.1" } }, "sha512-/AuWwMP+YqiPbsJx5D6TfgRTc4kTLjsh5SOcd4bLsfUg2RcEXrFMJl1DGgdHy2aCfsIA/cr/1JM0xcB2GZji8g=="],
|
||||||
|
|
||||||
|
@ -17,6 +17,10 @@
|
|||||||
"@ai-sdk/openai": "^1.3.0",
|
"@ai-sdk/openai": "^1.3.0",
|
||||||
"@ai-sdk/react": "^1.2.0",
|
"@ai-sdk/react": "^1.2.0",
|
||||||
"@auth/prisma-adapter": "^2.8.0",
|
"@auth/prisma-adapter": "^2.8.0",
|
||||||
|
"@dnd-kit/core": "^6.3.1",
|
||||||
|
"@dnd-kit/modifiers": "^9.0.0",
|
||||||
|
"@dnd-kit/sortable": "^10.0.0",
|
||||||
|
"@dnd-kit/utilities": "^3.2.2",
|
||||||
"@fontsource/fira-code": "^5.1.1",
|
"@fontsource/fira-code": "^5.1.1",
|
||||||
"@hookform/resolvers": "^5.1.1",
|
"@hookform/resolvers": "^5.1.1",
|
||||||
"@monaco-editor/react": "^4.7.0",
|
"@monaco-editor/react": "^4.7.0",
|
||||||
@ -47,7 +51,7 @@
|
|||||||
"@radix-ui/react-toggle": "^1.1.9",
|
"@radix-ui/react-toggle": "^1.1.9",
|
||||||
"@radix-ui/react-toggle-group": "^1.1.10",
|
"@radix-ui/react-toggle-group": "^1.1.10",
|
||||||
"@radix-ui/react-tooltip": "^1.2.7",
|
"@radix-ui/react-tooltip": "^1.2.7",
|
||||||
"@tanstack/react-table": "^8.21.2",
|
"@tanstack/react-table": "^8.21.3",
|
||||||
"@types/vscode": "^1.97.0",
|
"@types/vscode": "^1.97.0",
|
||||||
"ai": "^4.2.0",
|
"ai": "^4.2.0",
|
||||||
"bcryptjs": "^3.0.2",
|
"bcryptjs": "^3.0.2",
|
||||||
@ -92,7 +96,7 @@
|
|||||||
"vaul": "^1.1.2",
|
"vaul": "^1.1.2",
|
||||||
"vscode-languageclient": "^9.0.1",
|
"vscode-languageclient": "^9.0.1",
|
||||||
"vscode-ws-jsonrpc": "^3.4.0",
|
"vscode-ws-jsonrpc": "^3.4.0",
|
||||||
"zod": "^3.25.64",
|
"zod": "^3.25.67",
|
||||||
"zustand": "^5.0.3"
|
"zustand": "^5.0.3"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
122
src/app/dashboard/data.json
Normal file
122
src/app/dashboard/data.json
Normal file
@ -0,0 +1,122 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"id": "1",
|
||||||
|
"name": "张三",
|
||||||
|
"email": "zhangsan@example.com",
|
||||||
|
"createdAt": "2023-10-01T08:00:00Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "2",
|
||||||
|
"name": "李四",
|
||||||
|
"email": "lisi@example.com",
|
||||||
|
"createdAt": "2023-10-02T09:30:00Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "3",
|
||||||
|
"name": "王五",
|
||||||
|
"email": "wangwu@example.com",
|
||||||
|
"createdAt": "2023-10-03T11:45:00Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "4",
|
||||||
|
"name": "赵六",
|
||||||
|
"email": "zhaoliu@example.org",
|
||||||
|
"createdAt": "2023-10-04T14:00:00Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "5",
|
||||||
|
"name": "孙七",
|
||||||
|
"email": "sunqi@example.net",
|
||||||
|
"createdAt": "2023-10-05T16:15:00Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "6",
|
||||||
|
"name": "周八",
|
||||||
|
"email": "zhouba@example.org",
|
||||||
|
"createdAt": "2023-10-06T18:30:00Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "7",
|
||||||
|
"name": "吴九",
|
||||||
|
"email": "wujiu@example.net",
|
||||||
|
"createdAt": "2023-10-07T20:45:00Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "8",
|
||||||
|
"name": "郑十",
|
||||||
|
"email": "zhengshi@example.org",
|
||||||
|
"createdAt": "2023-10-08T23:00:00Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "9",
|
||||||
|
"name": "钱十一",
|
||||||
|
"email": "qian11@example.net",
|
||||||
|
"createdAt": "2023-10-09T01:15:00Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "10",
|
||||||
|
"name": "孙十二",
|
||||||
|
"email": "sun12@example.org",
|
||||||
|
"createdAt": "2023-10-10T03:30:00Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "11",
|
||||||
|
"name": "周十三",
|
||||||
|
"email": "zhou13@example.net",
|
||||||
|
"createdAt": "2023-10-11T05:45:00Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "12",
|
||||||
|
"name": "吴十四",
|
||||||
|
"email": "wushi14@example.org",
|
||||||
|
"createdAt": "2023-10-12T08:00:00Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "13",
|
||||||
|
"name": "郑十五",
|
||||||
|
"email": "zheng15@example.net",
|
||||||
|
"createdAt": "2023-10-13T10:15:00Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "14",
|
||||||
|
"name": "钱十六",
|
||||||
|
"email": "qian16@example.org",
|
||||||
|
"createdAt": "2023-10-14T12:30:00Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "15",
|
||||||
|
"name": "孙十七",
|
||||||
|
"email": "sun17@example.net",
|
||||||
|
"createdAt": "2023-10-15T14:45:00Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "16",
|
||||||
|
"name": "周十八",
|
||||||
|
"email": "zhou18@example.org",
|
||||||
|
"createdAt": "2023-10-16T17:00:00Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "17",
|
||||||
|
"name": "吴十九",
|
||||||
|
"email": "wujiu19@example.net",
|
||||||
|
"createdAt": "2023-10-17T19:15:00Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "18",
|
||||||
|
"name": "郑二十",
|
||||||
|
"email": "zheng20@example.org",
|
||||||
|
"createdAt": "2023-10-18T21:30:00Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "19",
|
||||||
|
"name": "周十九",
|
||||||
|
"email": "zhou19@example.org",
|
||||||
|
"createdAt": "2023-10-19T15:30:00Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "20",
|
||||||
|
"name": "郑二十",
|
||||||
|
"email": "zheng20@example.net",
|
||||||
|
"createdAt": "2023-10-20T17:45:00Z"
|
||||||
|
}
|
||||||
|
]
|
17
src/app/dashboard/page.tsx
Normal file
17
src/app/dashboard/page.tsx
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
import { DataTable } from "@/components/data-table"
|
||||||
|
import { SiteHeader } from "@/components/site-header"
|
||||||
|
|
||||||
|
import data from "./data.json"
|
||||||
|
|
||||||
|
export default function Page() {
|
||||||
|
return (
|
||||||
|
<div className="flex flex-1 flex-col">
|
||||||
|
<SiteHeader />
|
||||||
|
<div className="flex flex-1 flex-col">
|
||||||
|
<div className="flex flex-1 flex-col p-4">
|
||||||
|
<DataTable data={data} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
@ -33,13 +33,13 @@
|
|||||||
--chart-5: 213 16% 16%;
|
--chart-5: 213 16% 16%;
|
||||||
--radius: 0.5rem;
|
--radius: 0.5rem;
|
||||||
--sidebar-background: 0 0% 98%;
|
--sidebar-background: 0 0% 98%;
|
||||||
--sidebar-foreground: 213 13% 6%;
|
--sidebar-foreground: 240 5.3% 26.1%;
|
||||||
--sidebar-primary: 213 13% 16%;
|
--sidebar-primary: 240 5.9% 10%;
|
||||||
--sidebar-primary-foreground: 213 13% 76%;
|
--sidebar-primary-foreground: 0 0% 98%;
|
||||||
--sidebar-accent: 0 0% 85%;
|
--sidebar-accent: 240 4.8% 95.9%;
|
||||||
--sidebar-accent-foreground: 0 0% 25%;
|
--sidebar-accent-foreground: 240 5.9% 10%;
|
||||||
--sidebar-border: 0 0% 95%;
|
--sidebar-border: 220 13% 91%;
|
||||||
--sidebar-ring: 213 13% 16%;
|
--sidebar-ring: 217.2 91.2% 59.8%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dark {
|
.dark {
|
||||||
@ -67,14 +67,14 @@
|
|||||||
--chart-3: 216 28% 22%;
|
--chart-3: 216 28% 22%;
|
||||||
--chart-4: 210 7% 28%;
|
--chart-4: 210 7% 28%;
|
||||||
--chart-5: 210 20% 82%;
|
--chart-5: 210 20% 82%;
|
||||||
--sidebar-background: 216 28% 5%;
|
--sidebar-background: 240 5.9% 10%;
|
||||||
--sidebar-foreground: 210 17% 92%;
|
--sidebar-foreground: 240 4.8% 95.9%;
|
||||||
--sidebar-primary: 210 17% 82%;
|
--sidebar-primary: 224.3 76.3% 48%;
|
||||||
--sidebar-primary-foreground: 210 17% 22%;
|
--sidebar-primary-foreground: 0 0% 100%;
|
||||||
--sidebar-accent: 216 28% 22%;
|
--sidebar-accent: 240 3.7% 15.9%;
|
||||||
--sidebar-accent-foreground: 216 28% 82%;
|
--sidebar-accent-foreground: 240 4.8% 95.9%;
|
||||||
--sidebar-border: 216 18% 12%;
|
--sidebar-border: 240 3.7% 15.9%;
|
||||||
--sidebar-ring: 210 17% 82%;
|
--sidebar-ring: 217.2 91.2% 59.8%;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -119,3 +119,14 @@ code[data-theme*=" "] span {
|
|||||||
color: var(--shiki-dark);
|
color: var(--shiki-dark);
|
||||||
background-color: var(--shiki-dark-bg);
|
background-color: var(--shiki-dark-bg);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@layer base {
|
||||||
|
* {
|
||||||
|
@apply border-border;
|
||||||
|
}
|
||||||
|
body {
|
||||||
|
@apply bg-background text-foreground;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -1,129 +1,181 @@
|
|||||||
"use client";
|
"use client"
|
||||||
|
|
||||||
|
import * as React from "react"
|
||||||
import {
|
import {
|
||||||
AudioWaveform,
|
ArrowUpCircleIcon,
|
||||||
Bot,
|
BarChartIcon,
|
||||||
Command,
|
CameraIcon,
|
||||||
Frame,
|
ClipboardListIcon,
|
||||||
GalleryVerticalEnd,
|
DatabaseIcon,
|
||||||
Map,
|
FileCodeIcon,
|
||||||
PieChart,
|
FileIcon,
|
||||||
Settings2,
|
FileTextIcon,
|
||||||
SquareTerminal,
|
FolderIcon,
|
||||||
} from "lucide-react";
|
HelpCircleIcon,
|
||||||
import * as React from "react";
|
LayoutDashboardIcon,
|
||||||
|
ListIcon,
|
||||||
|
SearchIcon,
|
||||||
|
SettingsIcon,
|
||||||
|
UsersIcon,
|
||||||
|
} from "lucide-react"
|
||||||
|
|
||||||
|
import { NavDocuments } from "@/components/nav-documents"
|
||||||
|
import { NavMain } from "@/components/nav-main"
|
||||||
|
import { NavSecondary } from "@/components/nav-secondary"
|
||||||
|
import { NavUser } from "@/components/nav-user"
|
||||||
import {
|
import {
|
||||||
Sidebar,
|
Sidebar,
|
||||||
SidebarContent,
|
SidebarContent,
|
||||||
SidebarFooter,
|
SidebarFooter,
|
||||||
SidebarHeader,
|
SidebarHeader,
|
||||||
SidebarRail,
|
SidebarMenu,
|
||||||
} from "@/components/ui/sidebar";
|
SidebarMenuButton,
|
||||||
import { NavMain } from "@/components/nav-main";
|
SidebarMenuItem,
|
||||||
import { NavProjects } from "@/components/nav-projects";
|
} from "@/components/ui/sidebar"
|
||||||
import { TeamSwitcher } from "@/components/team-switcher";
|
|
||||||
import { NavUser, type NavUserProps } from "@/components/nav-user";
|
|
||||||
|
|
||||||
const data = {
|
const data = {
|
||||||
teams: [
|
user: {
|
||||||
{
|
name: "shadcn",
|
||||||
name: "Acme Inc",
|
email: "m@example.com",
|
||||||
logo: GalleryVerticalEnd,
|
avatar: "/avatars/shadcn.jpg",
|
||||||
plan: "Enterprise",
|
},
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Acme Corp.",
|
|
||||||
logo: AudioWaveform,
|
|
||||||
plan: "Startup",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Evil Corp.",
|
|
||||||
logo: Command,
|
|
||||||
plan: "Free",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
navMain: [
|
navMain: [
|
||||||
{
|
{
|
||||||
title: "Problemset",
|
title: "Dashboard",
|
||||||
url: "/dashboard/problemset",
|
url: "#",
|
||||||
icon: SquareTerminal,
|
icon: LayoutDashboardIcon,
|
||||||
isActive: true,
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "Models",
|
title: "Lifecycle",
|
||||||
|
url: "#",
|
||||||
|
icon: ListIcon,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Analytics",
|
||||||
|
url: "#",
|
||||||
|
icon: BarChartIcon,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Projects",
|
||||||
|
url: "#",
|
||||||
|
icon: FolderIcon,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Team",
|
||||||
|
url: "#",
|
||||||
|
icon: UsersIcon,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
navClouds: [
|
||||||
|
{
|
||||||
|
title: "Capture",
|
||||||
|
icon: CameraIcon,
|
||||||
|
isActive: true,
|
||||||
url: "#",
|
url: "#",
|
||||||
icon: Bot,
|
|
||||||
items: [
|
items: [
|
||||||
{
|
{
|
||||||
title: "Genesis",
|
title: "Active Proposals",
|
||||||
url: "#",
|
url: "#",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "Explorer",
|
title: "Archived",
|
||||||
url: "#",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "Quantum",
|
|
||||||
url: "#",
|
url: "#",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
title: "Proposal",
|
||||||
|
icon: FileTextIcon,
|
||||||
|
url: "#",
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
title: "Active Proposals",
|
||||||
|
url: "#",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Archived",
|
||||||
|
url: "#",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Prompts",
|
||||||
|
icon: FileCodeIcon,
|
||||||
|
url: "#",
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
title: "Active Proposals",
|
||||||
|
url: "#",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Archived",
|
||||||
|
url: "#",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
navSecondary: [
|
||||||
{
|
{
|
||||||
title: "Settings",
|
title: "Settings",
|
||||||
url: "/dashboard/settings",
|
url: "#",
|
||||||
icon: Settings2,
|
icon: SettingsIcon,
|
||||||
items: [
|
},
|
||||||
{
|
{
|
||||||
title: "General",
|
title: "Get Help",
|
||||||
url: "/general",
|
url: "#",
|
||||||
},
|
icon: HelpCircleIcon,
|
||||||
{
|
},
|
||||||
title: "Language Server",
|
{
|
||||||
url: "/language-server",
|
title: "Search",
|
||||||
},
|
url: "#",
|
||||||
],
|
icon: SearchIcon,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
projects: [
|
documents: [
|
||||||
{
|
{
|
||||||
name: "Design Engineering",
|
name: "Data Library",
|
||||||
url: "#",
|
url: "#",
|
||||||
icon: Frame,
|
icon: DatabaseIcon,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Sales & Marketing",
|
name: "Reports",
|
||||||
url: "#",
|
url: "#",
|
||||||
icon: PieChart,
|
icon: ClipboardListIcon,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Travel",
|
name: "Word Assistant",
|
||||||
url: "#",
|
url: "#",
|
||||||
icon: Map,
|
icon: FileIcon,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
};
|
|
||||||
|
|
||||||
interface AppSidebarProps extends React.ComponentProps<typeof Sidebar> {
|
|
||||||
user: NavUserProps["user"];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function AppSidebar({
|
export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
|
||||||
user,
|
|
||||||
...props
|
|
||||||
}: AppSidebarProps) {
|
|
||||||
return (
|
return (
|
||||||
<Sidebar collapsible="icon" {...props}>
|
<Sidebar collapsible="offcanvas" {...props}>
|
||||||
<SidebarHeader>
|
<SidebarHeader>
|
||||||
<TeamSwitcher teams={data.teams} />
|
<SidebarMenu>
|
||||||
|
<SidebarMenuItem>
|
||||||
|
<SidebarMenuButton
|
||||||
|
asChild
|
||||||
|
className="data-[slot=sidebar-menu-button]:!p-1.5"
|
||||||
|
>
|
||||||
|
<a href="#">
|
||||||
|
<ArrowUpCircleIcon className="h-5 w-5" />
|
||||||
|
<span className="text-base font-semibold">Acme Inc.</span>
|
||||||
|
</a>
|
||||||
|
</SidebarMenuButton>
|
||||||
|
</SidebarMenuItem>
|
||||||
|
</SidebarMenu>
|
||||||
</SidebarHeader>
|
</SidebarHeader>
|
||||||
<SidebarContent>
|
<SidebarContent>
|
||||||
<NavMain items={data.navMain} />
|
<NavMain items={data.navMain} />
|
||||||
<NavProjects projects={data.projects} />
|
<NavDocuments items={data.documents} />
|
||||||
|
<NavSecondary items={data.navSecondary} className="mt-auto" />
|
||||||
</SidebarContent>
|
</SidebarContent>
|
||||||
<SidebarFooter>
|
<SidebarFooter>
|
||||||
<NavUser user={user} />
|
<NavUser user={data.user} />
|
||||||
</SidebarFooter>
|
</SidebarFooter>
|
||||||
<SidebarRail />
|
|
||||||
</Sidebar>
|
</Sidebar>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
292
src/components/chart-area-interactive.tsx
Normal file
292
src/components/chart-area-interactive.tsx
Normal file
@ -0,0 +1,292 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import * as React from "react"
|
||||||
|
import { Area, AreaChart, CartesianGrid, XAxis } from "recharts"
|
||||||
|
|
||||||
|
import { useIsMobile } from "@/hooks/use-mobile"
|
||||||
|
import {
|
||||||
|
Card,
|
||||||
|
CardContent,
|
||||||
|
CardDescription,
|
||||||
|
CardHeader,
|
||||||
|
CardTitle,
|
||||||
|
} from "@/components/ui/card"
|
||||||
|
import {
|
||||||
|
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"
|
||||||
|
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: "hsl(var(--chart-1))",
|
||||||
|
},
|
||||||
|
mobile: {
|
||||||
|
label: "Mobile",
|
||||||
|
color: "hsl(var(--chart-2))",
|
||||||
|
},
|
||||||
|
} satisfies ChartConfig
|
||||||
|
|
||||||
|
export function ChartAreaInteractive() {
|
||||||
|
const isMobile = useIsMobile()
|
||||||
|
const [timeRange, setTimeRange] = React.useState("30d")
|
||||||
|
|
||||||
|
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 className="relative">
|
||||||
|
<CardTitle>Total Visitors</CardTitle>
|
||||||
|
<CardDescription>
|
||||||
|
<span className="@[540px]/card:block hidden">
|
||||||
|
Total for the last 3 months
|
||||||
|
</span>
|
||||||
|
<span className="@[540px]/card:hidden">Last 3 months</span>
|
||||||
|
</CardDescription>
|
||||||
|
<div className="absolute right-4 top-4">
|
||||||
|
<ToggleGroup
|
||||||
|
type="single"
|
||||||
|
value={timeRange}
|
||||||
|
onValueChange={setTimeRange}
|
||||||
|
variant="outline"
|
||||||
|
className="@[767px]/card:flex hidden"
|
||||||
|
>
|
||||||
|
<ToggleGroupItem value="90d" className="h-8 px-2.5">
|
||||||
|
Last 3 months
|
||||||
|
</ToggleGroupItem>
|
||||||
|
<ToggleGroupItem value="30d" className="h-8 px-2.5">
|
||||||
|
Last 30 days
|
||||||
|
</ToggleGroupItem>
|
||||||
|
<ToggleGroupItem value="7d" className="h-8 px-2.5">
|
||||||
|
Last 7 days
|
||||||
|
</ToggleGroupItem>
|
||||||
|
</ToggleGroup>
|
||||||
|
<Select value={timeRange} onValueChange={setTimeRange}>
|
||||||
|
<SelectTrigger
|
||||||
|
className="@[767px]/card:hidden flex w-40"
|
||||||
|
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>
|
||||||
|
</div>
|
||||||
|
</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>
|
||||||
|
)
|
||||||
|
}
|
548
src/components/data-table.tsx
Normal file
548
src/components/data-table.tsx
Normal file
@ -0,0 +1,548 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import * as React from "react"
|
||||||
|
import {
|
||||||
|
DndContext,
|
||||||
|
KeyboardSensor,
|
||||||
|
MouseSensor,
|
||||||
|
TouchSensor,
|
||||||
|
closestCenter,
|
||||||
|
useSensor,
|
||||||
|
useSensors,
|
||||||
|
type DragEndEvent,
|
||||||
|
type UniqueIdentifier,
|
||||||
|
} from "@dnd-kit/core"
|
||||||
|
import { restrictToVerticalAxis } from "@dnd-kit/modifiers"
|
||||||
|
import {
|
||||||
|
SortableContext,
|
||||||
|
arrayMove,
|
||||||
|
useSortable,
|
||||||
|
verticalListSortingStrategy,
|
||||||
|
} from "@dnd-kit/sortable"
|
||||||
|
import { CSS } from "@dnd-kit/utilities"
|
||||||
|
import {
|
||||||
|
ColumnDef,
|
||||||
|
ColumnFiltersState,
|
||||||
|
Row,
|
||||||
|
SortingState,
|
||||||
|
VisibilityState,
|
||||||
|
flexRender,
|
||||||
|
getCoreRowModel,
|
||||||
|
getFacetedRowModel,
|
||||||
|
getFacetedUniqueValues,
|
||||||
|
getFilteredRowModel,
|
||||||
|
getPaginationRowModel,
|
||||||
|
getSortedRowModel,
|
||||||
|
useReactTable,
|
||||||
|
} from "@tanstack/react-table"
|
||||||
|
import {
|
||||||
|
ChevronDownIcon,
|
||||||
|
ChevronLeftIcon,
|
||||||
|
ChevronRightIcon,
|
||||||
|
ChevronsLeftIcon,
|
||||||
|
ChevronsRightIcon,
|
||||||
|
ColumnsIcon,
|
||||||
|
GripVerticalIcon,
|
||||||
|
LoaderIcon,
|
||||||
|
MoreHorizontalIcon,
|
||||||
|
MoreVerticalIcon,
|
||||||
|
PlusIcon,
|
||||||
|
EyeIcon,
|
||||||
|
PencilIcon,
|
||||||
|
TrashIcon,
|
||||||
|
} from "lucide-react"
|
||||||
|
import { Area, AreaChart, CartesianGrid, XAxis } from "recharts"
|
||||||
|
import { toast } from "sonner"
|
||||||
|
import { z } from "zod"
|
||||||
|
|
||||||
|
import { useIsMobile } from "@/hooks/use-mobile"
|
||||||
|
import { Badge } from "@/components/ui/badge"
|
||||||
|
import { Button } from "@/components/ui/button"
|
||||||
|
import {
|
||||||
|
ChartConfig,
|
||||||
|
ChartContainer,
|
||||||
|
ChartTooltip,
|
||||||
|
ChartTooltipContent,
|
||||||
|
} from "@/components/ui/chart"
|
||||||
|
import { Checkbox } from "@/components/ui/checkbox"
|
||||||
|
import {
|
||||||
|
DropdownMenu,
|
||||||
|
DropdownMenuCheckboxItem,
|
||||||
|
DropdownMenuContent,
|
||||||
|
DropdownMenuItem,
|
||||||
|
DropdownMenuSeparator,
|
||||||
|
DropdownMenuTrigger,
|
||||||
|
} from "@/components/ui/dropdown-menu"
|
||||||
|
import { Input } from "@/components/ui/input"
|
||||||
|
import { Label } from "@/components/ui/label"
|
||||||
|
import {
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectItem,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
|
} from "@/components/ui/select"
|
||||||
|
import { Separator } from "@/components/ui/separator"
|
||||||
|
import {
|
||||||
|
Sheet,
|
||||||
|
SheetClose,
|
||||||
|
SheetContent,
|
||||||
|
SheetDescription,
|
||||||
|
SheetFooter,
|
||||||
|
SheetHeader,
|
||||||
|
SheetTitle,
|
||||||
|
SheetTrigger,
|
||||||
|
} from "@/components/ui/sheet"
|
||||||
|
import {
|
||||||
|
Table,
|
||||||
|
TableBody,
|
||||||
|
TableCell,
|
||||||
|
TableHead,
|
||||||
|
TableHeader,
|
||||||
|
TableRow,
|
||||||
|
} from "@/components/ui/table"
|
||||||
|
import {
|
||||||
|
Tabs,
|
||||||
|
TabsContent,
|
||||||
|
TabsList,
|
||||||
|
TabsTrigger,
|
||||||
|
} from "@/components/ui/tabs"
|
||||||
|
|
||||||
|
export const schema = z.object({
|
||||||
|
id: z.number(),
|
||||||
|
name: z.string(),
|
||||||
|
email: z.string(),
|
||||||
|
createdAt: z.string().datetime(),
|
||||||
|
})
|
||||||
|
|
||||||
|
// Create a separate component for the drag handle
|
||||||
|
function DragHandle({ id }: { id: number }) {
|
||||||
|
const { attributes, listeners } = useSortable({
|
||||||
|
id,
|
||||||
|
})
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
{...attributes}
|
||||||
|
{...listeners}
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
className="text-muted-foreground size-7 hover:bg-transparent cursor-grab active:cursor-grabbing"
|
||||||
|
>
|
||||||
|
<GripVerticalIcon className="text-muted-foreground size-3" />
|
||||||
|
<span className="sr-only">Drag to reorder</span>
|
||||||
|
</Button>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const columns: ColumnDef<z.infer<typeof schema>>[] = [
|
||||||
|
{
|
||||||
|
id: "select",
|
||||||
|
header: ({ table }) => (
|
||||||
|
<div className="flex items-center justify-center">
|
||||||
|
<Checkbox
|
||||||
|
checked={
|
||||||
|
table.getIsAllPageRowsSelected() ||
|
||||||
|
(table.getIsSomePageRowsSelected() && "indeterminate")
|
||||||
|
}
|
||||||
|
onCheckedChange={(value) => table.toggleAllPageRowsSelected(!!value)}
|
||||||
|
aria-label="Select all"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
cell: ({ row }) => (
|
||||||
|
<div className="flex items-center justify-center">
|
||||||
|
<Checkbox
|
||||||
|
checked={row.getIsSelected()}
|
||||||
|
onCheckedChange={(value) => row.toggleSelected(!!value)}
|
||||||
|
aria-label={`Select row ${row.original.id}`}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
enableSorting: false,
|
||||||
|
enableHiding: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: "id",
|
||||||
|
header: "ID",
|
||||||
|
cell: ({ row }) => <div className="w-12">{row.original.id}</div>,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: "name",
|
||||||
|
header: "姓名",
|
||||||
|
cell: ({ row }) => <TableCellViewer item={row.original} />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: "email",
|
||||||
|
header: "邮箱",
|
||||||
|
cell: ({ row }) => <div className="w-48">{row.original.email}</div>,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: "createdAt",
|
||||||
|
header: "创建时间",
|
||||||
|
cell: ({ row }) => (
|
||||||
|
<div className="w-36">
|
||||||
|
{new Date(row.original.createdAt).toLocaleDateString()}
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "actions",
|
||||||
|
enableHiding: false,
|
||||||
|
header: () => <div className="text-right">操作</div>,
|
||||||
|
cell: ({ row }) => {
|
||||||
|
const item = row.original
|
||||||
|
return (
|
||||||
|
<div className="flex justify-end gap-2">
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
className="h-8 w-8"
|
||||||
|
onClick={() => console.log('Edit', item.id)}
|
||||||
|
aria-label="Edit"
|
||||||
|
>
|
||||||
|
<PencilIcon className="size-4" />
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
className="h-8 w-8"
|
||||||
|
onClick={() => console.log('View', item.id)}
|
||||||
|
aria-label="View"
|
||||||
|
>
|
||||||
|
<EyeIcon className="size-4" />
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
className="h-8 w-8 text-destructive hover:text-destructive"
|
||||||
|
onClick={() => console.log('Delete', item.id)}
|
||||||
|
aria-label="Delete"
|
||||||
|
>
|
||||||
|
<TrashIcon className="size-4" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
function DraggableRow({ row }: { row: Row<z.infer<typeof schema>> }) {
|
||||||
|
const { transform, transition, setNodeRef, isDragging } = useSortable({
|
||||||
|
id: row.original.id,
|
||||||
|
})
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TableRow
|
||||||
|
data-state={row.getIsSelected() && "selected"}
|
||||||
|
data-dragging={isDragging}
|
||||||
|
ref={setNodeRef}
|
||||||
|
className="relative z-0 data-[dragging=true]:z-10 data-[dragging=true]:opacity-80"
|
||||||
|
style={{
|
||||||
|
transform: CSS.Transform.toString(transform),
|
||||||
|
transition: transition,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{row.getVisibleCells().map((cell) => (
|
||||||
|
<TableCell key={cell.id}>
|
||||||
|
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
||||||
|
</TableCell>
|
||||||
|
))}
|
||||||
|
</TableRow>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function TableCellViewer({ item }: { item: z.infer<typeof schema> }) {
|
||||||
|
const isMobile = useIsMobile()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Sheet>
|
||||||
|
<SheetTrigger asChild>
|
||||||
|
<Button variant="link" className="w-fit px-0 text-left text-foreground">
|
||||||
|
{item.name}
|
||||||
|
</Button>
|
||||||
|
</SheetTrigger>
|
||||||
|
<SheetContent side="right" className="flex flex-col">
|
||||||
|
<SheetHeader className="gap-1">
|
||||||
|
<SheetTitle>ID: {item.id}</SheetTitle>
|
||||||
|
<SheetDescription>姓名: {item.name}</SheetDescription>
|
||||||
|
</SheetHeader>
|
||||||
|
<div className="flex flex-1 flex-col gap-4 overflow-y-auto py-4 text-sm">
|
||||||
|
<form className="flex flex-col gap-4">
|
||||||
|
<div className="flex flex-col gap-3">
|
||||||
|
<Label htmlFor="id">ID</Label>
|
||||||
|
<Input id="id" defaultValue={item.id} disabled />
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col gap-3">
|
||||||
|
<Label htmlFor="name">姓名</Label>
|
||||||
|
<Input id="name" defaultValue={item.name} />
|
||||||
|
</div>
|
||||||
|
<div className="grid grid-cols-2 gap-4">
|
||||||
|
<div className="flex flex-col gap-3">
|
||||||
|
<Label htmlFor="email">邮箱</Label>
|
||||||
|
<Input id="email" defaultValue={item.email} />
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col gap-3">
|
||||||
|
<Label htmlFor="createdAt">创建时间</Label>
|
||||||
|
<Input id="createdAt" defaultValue={new Date(item.createdAt).toLocaleString()} disabled />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
<SheetFooter className="mt-auto flex gap-2 sm:flex-col sm:space-x-0">
|
||||||
|
<Button className="w-full">提交</Button>
|
||||||
|
<SheetClose asChild>
|
||||||
|
<Button variant="outline" className="w-full">
|
||||||
|
关闭
|
||||||
|
</Button>
|
||||||
|
</SheetClose>
|
||||||
|
</SheetFooter>
|
||||||
|
</SheetContent>
|
||||||
|
</Sheet>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function DataTable({
|
||||||
|
data: initialData,
|
||||||
|
}: {
|
||||||
|
data: z.infer<typeof schema>[]
|
||||||
|
}) {
|
||||||
|
const [data, setData] = React.useState(() => initialData)
|
||||||
|
const [rowSelection, setRowSelection] = React.useState({})
|
||||||
|
const [columnVisibility, setColumnVisibility] = React.useState<VisibilityState>({
|
||||||
|
drag: false,
|
||||||
|
// 仅保留必要列可见性控制
|
||||||
|
})
|
||||||
|
const [columnFilters, setColumnFilters] = React.useState<ColumnFiltersState>([])
|
||||||
|
const [sorting, setSorting] = React.useState<SortingState>([])
|
||||||
|
const [pagination, setPagination] = React.useState({
|
||||||
|
pageIndex: 0,
|
||||||
|
pageSize: 10,
|
||||||
|
})
|
||||||
|
const sortableId = React.useId()
|
||||||
|
const sensors = useSensors(
|
||||||
|
useSensor(MouseSensor, {}),
|
||||||
|
useSensor(TouchSensor, {}),
|
||||||
|
useSensor(KeyboardSensor, {})
|
||||||
|
)
|
||||||
|
|
||||||
|
const dataIds = React.useMemo<UniqueIdentifier[]>(
|
||||||
|
() => data?.map(({ id }) => id) || [],
|
||||||
|
[data]
|
||||||
|
)
|
||||||
|
|
||||||
|
const table = useReactTable({
|
||||||
|
data,
|
||||||
|
columns,
|
||||||
|
state: {
|
||||||
|
sorting,
|
||||||
|
columnVisibility,
|
||||||
|
rowSelection,
|
||||||
|
columnFilters,
|
||||||
|
pagination,
|
||||||
|
},
|
||||||
|
getRowId: (row) => row.id.toString(),
|
||||||
|
enableRowSelection: true,
|
||||||
|
onRowSelectionChange: setRowSelection,
|
||||||
|
onSortingChange: setSorting,
|
||||||
|
onColumnFiltersChange: setColumnFilters,
|
||||||
|
onColumnVisibilityChange: setColumnVisibility,
|
||||||
|
onPaginationChange: setPagination,
|
||||||
|
getCoreRowModel: getCoreRowModel(),
|
||||||
|
getFilteredRowModel: getFilteredRowModel(),
|
||||||
|
getPaginationRowModel: getPaginationRowModel(),
|
||||||
|
getSortedRowModel: getSortedRowModel(),
|
||||||
|
getFacetedRowModel: getFacetedRowModel(),
|
||||||
|
getFacetedUniqueValues: getFacetedUniqueValues(),
|
||||||
|
})
|
||||||
|
|
||||||
|
function handleDragEnd(event: DragEndEvent) {
|
||||||
|
const { active, over } = event
|
||||||
|
if (active && over && active.id !== over.id) {
|
||||||
|
setData((data) => {
|
||||||
|
const oldIndex = dataIds.indexOf(active.id)
|
||||||
|
const newIndex = dataIds.indexOf(over.id)
|
||||||
|
return arrayMove(data, oldIndex, newIndex)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Tabs
|
||||||
|
defaultValue="outline"
|
||||||
|
className="flex w-full flex-col gap-6"
|
||||||
|
>
|
||||||
|
<div className="flex items-center justify-between px-4 lg:px-6">
|
||||||
|
{/* 修改视图标题区域为左侧固定内容 */}
|
||||||
|
<div className="flex items-center gap-2 text-sm font-medium">
|
||||||
|
管理员列表
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<TabsContent
|
||||||
|
value="outline"
|
||||||
|
className="relative flex flex-col gap-4 overflow-auto px-4 lg:px-6"
|
||||||
|
>
|
||||||
|
<div className="overflow-hidden rounded-lg border">
|
||||||
|
<DndContext
|
||||||
|
collisionDetection={closestCenter}
|
||||||
|
modifiers={[restrictToVerticalAxis]}
|
||||||
|
onDragEnd={handleDragEnd}
|
||||||
|
sensors={sensors}
|
||||||
|
id={sortableId}
|
||||||
|
>
|
||||||
|
<Table>
|
||||||
|
<TableHeader className="sticky top-0 z-10 bg-muted">
|
||||||
|
{table.getHeaderGroups().map((headerGroup) => (
|
||||||
|
<TableRow key={headerGroup.id}>
|
||||||
|
{headerGroup.headers.map((header) => {
|
||||||
|
return (
|
||||||
|
<TableHead key={header.id} colSpan={header.colSpan}>
|
||||||
|
{header.isPlaceholder
|
||||||
|
? null
|
||||||
|
: flexRender(
|
||||||
|
header.column.columnDef.header,
|
||||||
|
header.getContext()
|
||||||
|
)}
|
||||||
|
</TableHead>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</TableRow>
|
||||||
|
))}
|
||||||
|
</TableHeader>
|
||||||
|
<TableBody className="**:data-[slot=table-cell]:first:w-8">
|
||||||
|
{table.getRowModel().rows?.length ? (
|
||||||
|
<SortableContext
|
||||||
|
items={dataIds}
|
||||||
|
strategy={verticalListSortingStrategy}
|
||||||
|
>
|
||||||
|
{table.getRowModel().rows.map((row) => (
|
||||||
|
<DraggableRow key={row.id} row={row} />
|
||||||
|
))}
|
||||||
|
</SortableContext>
|
||||||
|
) : (
|
||||||
|
<TableRow>
|
||||||
|
<TableCell
|
||||||
|
colSpan={columns.length}
|
||||||
|
className="h-24 text-center"
|
||||||
|
>
|
||||||
|
暂无数据
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
)}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</DndContext>
|
||||||
|
</div>
|
||||||
|
{/* 仅调整背景色,保持原有分页结构 */}
|
||||||
|
<div className="flex items-center justify-between border-t bg-white p-4">
|
||||||
|
<div className="flex items-center gap-2 text-sm">
|
||||||
|
{table.getFilteredSelectedRowModel().rows.length} / {table.getFilteredRowModel().rows.length} 行已选择
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-8">
|
||||||
|
<div className="hidden items-center gap-2 lg:flex">
|
||||||
|
<Label htmlFor="rows-per-page" className="text-sm font-medium">
|
||||||
|
每页行数
|
||||||
|
</Label>
|
||||||
|
<Select
|
||||||
|
value={`${table.getState().pagination.pageSize}`}
|
||||||
|
onValueChange={(value) => {
|
||||||
|
table.setPageSize(Number(value))
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<SelectTrigger className="w-20" id="rows-per-page">
|
||||||
|
<SelectValue
|
||||||
|
placeholder={table.getState().pagination.pageSize}
|
||||||
|
/>
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent side="top">
|
||||||
|
{[10, 20, 30, 40, 50].map((pageSize) => (
|
||||||
|
<SelectItem key={pageSize} value={`${pageSize}`}>
|
||||||
|
每页{pageSize}条
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
<div className="flex w-fit items-center text-sm font-medium">
|
||||||
|
第 {table.getState().pagination.pageIndex + 1} 页
|
||||||
|
共 {table.getPageCount()} 页
|
||||||
|
</div>
|
||||||
|
<div className="ml-auto flex items-center gap-2 sm:ml-0">
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
className="hidden h-8 w-8 p-0 lg:flex"
|
||||||
|
onClick={() => table.setPageIndex(0)}
|
||||||
|
disabled={!table.getCanPreviousPage()}
|
||||||
|
>
|
||||||
|
<ChevronsLeftIcon />
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
className="size-8"
|
||||||
|
size="icon"
|
||||||
|
onClick={() => table.previousPage()}
|
||||||
|
disabled={!table.getCanPreviousPage()}
|
||||||
|
>
|
||||||
|
<ChevronLeftIcon />
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
className="size-8"
|
||||||
|
size="icon"
|
||||||
|
onClick={() => table.nextPage()}
|
||||||
|
disabled={!table.getCanNextPage()}
|
||||||
|
>
|
||||||
|
<ChevronRightIcon />
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
className="hidden size-8 lg:flex"
|
||||||
|
size="icon"
|
||||||
|
onClick={() => table.setPageIndex(table.getPageCount() - 1)}
|
||||||
|
disabled={!table.getCanNextPage()}
|
||||||
|
>
|
||||||
|
<ChevronsRightIcon />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</TabsContent>
|
||||||
|
<TabsContent
|
||||||
|
value="past-performance"
|
||||||
|
className="flex flex-col px-4 lg:px-6"
|
||||||
|
>
|
||||||
|
<div className="aspect-video w-full flex-1 rounded-lg border border-dashed"></div>
|
||||||
|
</TabsContent>
|
||||||
|
<TabsContent value="key-personnel" className="flex flex-col px-4 lg:px-6">
|
||||||
|
<div className="aspect-video w-full flex-1 rounded-lg border border-dashed"></div>
|
||||||
|
</TabsContent>
|
||||||
|
<TabsContent
|
||||||
|
value="focus-documents"
|
||||||
|
className="flex flex-col px-4 lg:px-6"
|
||||||
|
>
|
||||||
|
<div className="aspect-video w-full flex-1 rounded-lg border border-dashed"></div>
|
||||||
|
</TabsContent>
|
||||||
|
</Tabs>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const chartData = [
|
||||||
|
{ month: "January", desktop: 186, mobile: 80 },
|
||||||
|
{ month: "February", desktop: 305, mobile: 200 },
|
||||||
|
{ month: "March", desktop: 237, mobile: 120 },
|
||||||
|
{ month: "April", desktop: 73, mobile: 190 },
|
||||||
|
{ month: "May", desktop: 209, mobile: 130 },
|
||||||
|
{ month: "June", desktop: 214, mobile: 140 },
|
||||||
|
]
|
||||||
|
|
||||||
|
const chartConfig = {
|
||||||
|
desktop: {
|
||||||
|
label: "Desktop",
|
||||||
|
color: "var(--primary)",
|
||||||
|
},
|
||||||
|
mobile: {
|
||||||
|
label: "Mobile",
|
||||||
|
color: "var(--primary)",
|
||||||
|
},
|
||||||
|
} satisfies ChartConfig
|
85
src/components/nav-documents.tsx
Normal file
85
src/components/nav-documents.tsx
Normal file
@ -0,0 +1,85 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import {
|
||||||
|
FolderIcon,
|
||||||
|
MoreHorizontalIcon,
|
||||||
|
ShareIcon,
|
||||||
|
type LucideIcon,
|
||||||
|
} from "lucide-react"
|
||||||
|
|
||||||
|
import {
|
||||||
|
DropdownMenu,
|
||||||
|
DropdownMenuContent,
|
||||||
|
DropdownMenuItem,
|
||||||
|
DropdownMenuTrigger,
|
||||||
|
} from "@/components/ui/dropdown-menu"
|
||||||
|
import {
|
||||||
|
SidebarGroup,
|
||||||
|
SidebarGroupLabel,
|
||||||
|
SidebarMenu,
|
||||||
|
SidebarMenuAction,
|
||||||
|
SidebarMenuButton,
|
||||||
|
SidebarMenuItem,
|
||||||
|
useSidebar,
|
||||||
|
} from "@/components/ui/sidebar"
|
||||||
|
|
||||||
|
export function NavDocuments({
|
||||||
|
items,
|
||||||
|
}: {
|
||||||
|
items: {
|
||||||
|
name: string
|
||||||
|
url: string
|
||||||
|
icon: LucideIcon
|
||||||
|
}[]
|
||||||
|
}) {
|
||||||
|
const { isMobile } = useSidebar()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SidebarGroup className="group-data-[collapsible=icon]:hidden">
|
||||||
|
<SidebarGroupLabel>Documents</SidebarGroupLabel>
|
||||||
|
<SidebarMenu>
|
||||||
|
{items.map((item) => (
|
||||||
|
<SidebarMenuItem key={item.name}>
|
||||||
|
<SidebarMenuButton asChild>
|
||||||
|
<a href={item.url}>
|
||||||
|
<item.icon />
|
||||||
|
<span>{item.name}</span>
|
||||||
|
</a>
|
||||||
|
</SidebarMenuButton>
|
||||||
|
<DropdownMenu>
|
||||||
|
<DropdownMenuTrigger asChild>
|
||||||
|
<SidebarMenuAction
|
||||||
|
showOnHover
|
||||||
|
className="rounded-sm data-[state=open]:bg-accent"
|
||||||
|
>
|
||||||
|
<MoreHorizontalIcon />
|
||||||
|
<span className="sr-only">More</span>
|
||||||
|
</SidebarMenuAction>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent
|
||||||
|
className="w-24 rounded-lg"
|
||||||
|
side={isMobile ? "bottom" : "right"}
|
||||||
|
align={isMobile ? "end" : "start"}
|
||||||
|
>
|
||||||
|
<DropdownMenuItem>
|
||||||
|
<FolderIcon />
|
||||||
|
<span>Open</span>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuItem>
|
||||||
|
<ShareIcon />
|
||||||
|
<span>Share</span>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
</SidebarMenuItem>
|
||||||
|
))}
|
||||||
|
<SidebarMenuItem>
|
||||||
|
<SidebarMenuButton className="text-sidebar-foreground/70">
|
||||||
|
<MoreHorizontalIcon className="text-sidebar-foreground/70" />
|
||||||
|
<span>More</span>
|
||||||
|
</SidebarMenuButton>
|
||||||
|
</SidebarMenuItem>
|
||||||
|
</SidebarMenu>
|
||||||
|
</SidebarGroup>
|
||||||
|
)
|
||||||
|
}
|
@ -1,83 +1,58 @@
|
|||||||
"use client";
|
"use client"
|
||||||
|
|
||||||
|
import { MailIcon, PlusCircleIcon, type LucideIcon } from "lucide-react"
|
||||||
|
|
||||||
|
import { Button } from "@/components/ui/button"
|
||||||
import {
|
import {
|
||||||
SidebarGroup,
|
SidebarGroup,
|
||||||
SidebarGroupLabel,
|
SidebarGroupContent,
|
||||||
SidebarMenu,
|
SidebarMenu,
|
||||||
SidebarMenuButton,
|
SidebarMenuButton,
|
||||||
SidebarMenuItem,
|
SidebarMenuItem,
|
||||||
SidebarMenuSub,
|
} from "@/components/ui/sidebar"
|
||||||
SidebarMenuSubButton,
|
|
||||||
SidebarMenuSubItem,
|
|
||||||
} from "@/components/ui/sidebar";
|
|
||||||
import {
|
|
||||||
Collapsible,
|
|
||||||
CollapsibleContent,
|
|
||||||
CollapsibleTrigger,
|
|
||||||
} from "@/components/ui/collapsible";
|
|
||||||
import { ChevronRight, type LucideIcon } from "lucide-react";
|
|
||||||
|
|
||||||
export interface NavMainProps {
|
export function NavMain({
|
||||||
|
items,
|
||||||
|
}: {
|
||||||
items: {
|
items: {
|
||||||
title: string;
|
title: string
|
||||||
url: string;
|
url: string
|
||||||
icon?: LucideIcon;
|
icon?: LucideIcon
|
||||||
isActive?: boolean;
|
}[]
|
||||||
items?: {
|
}) {
|
||||||
title: string;
|
|
||||||
url: string;
|
|
||||||
}[];
|
|
||||||
}[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export function NavMain({ items }: NavMainProps) {
|
|
||||||
return (
|
return (
|
||||||
<SidebarGroup>
|
<SidebarGroup>
|
||||||
<SidebarGroupLabel>Platform</SidebarGroupLabel>
|
<SidebarGroupContent className="flex flex-col gap-2">
|
||||||
<SidebarMenu>
|
<SidebarMenu>
|
||||||
{items.map((item) =>
|
<SidebarMenuItem className="flex items-center gap-2">
|
||||||
!item.items ? (
|
<SidebarMenuButton
|
||||||
|
tooltip="Quick Create"
|
||||||
|
className="min-w-8 bg-primary text-primary-foreground duration-200 ease-linear hover:bg-primary/90 hover:text-primary-foreground active:bg-primary/90 active:text-primary-foreground"
|
||||||
|
>
|
||||||
|
<PlusCircleIcon />
|
||||||
|
<span>Quick Create</span>
|
||||||
|
</SidebarMenuButton>
|
||||||
|
<Button
|
||||||
|
size="icon"
|
||||||
|
className="h-9 w-9 shrink-0 group-data-[collapsible=icon]:opacity-0"
|
||||||
|
variant="outline"
|
||||||
|
>
|
||||||
|
<MailIcon />
|
||||||
|
<span className="sr-only">Inbox</span>
|
||||||
|
</Button>
|
||||||
|
</SidebarMenuItem>
|
||||||
|
</SidebarMenu>
|
||||||
|
<SidebarMenu>
|
||||||
|
{items.map((item) => (
|
||||||
<SidebarMenuItem key={item.title}>
|
<SidebarMenuItem key={item.title}>
|
||||||
<SidebarMenuButton asChild tooltip={item.title}>
|
<SidebarMenuButton tooltip={item.title}>
|
||||||
<a href={item.url}>
|
{item.icon && <item.icon />}
|
||||||
{item.icon && <item.icon />}
|
<span>{item.title}</span>
|
||||||
<span>{item.title}</span>
|
|
||||||
</a>
|
|
||||||
</SidebarMenuButton>
|
</SidebarMenuButton>
|
||||||
</SidebarMenuItem>
|
</SidebarMenuItem>
|
||||||
) : (
|
))}
|
||||||
<Collapsible
|
</SidebarMenu>
|
||||||
key={item.title}
|
</SidebarGroupContent>
|
||||||
asChild
|
|
||||||
defaultOpen={item.isActive}
|
|
||||||
className="group/collapsible"
|
|
||||||
>
|
|
||||||
<SidebarMenuItem>
|
|
||||||
<CollapsibleTrigger asChild>
|
|
||||||
<SidebarMenuButton tooltip={item.title}>
|
|
||||||
{item.icon && <item.icon />}
|
|
||||||
<span>{item.title}</span>
|
|
||||||
<ChevronRight className="ml-auto transition-transform duration-200 group-data-[state=open]/collapsible:rotate-90" />
|
|
||||||
</SidebarMenuButton>
|
|
||||||
</CollapsibleTrigger>
|
|
||||||
<CollapsibleContent>
|
|
||||||
<SidebarMenuSub>
|
|
||||||
{item.items.map((subItem) => (
|
|
||||||
<SidebarMenuSubItem key={subItem.title}>
|
|
||||||
<SidebarMenuSubButton asChild>
|
|
||||||
<a href={`${item.url}${subItem.url}`}>
|
|
||||||
<span>{subItem.title}</span>
|
|
||||||
</a>
|
|
||||||
</SidebarMenuSubButton>
|
|
||||||
</SidebarMenuSubItem>
|
|
||||||
))}
|
|
||||||
</SidebarMenuSub>
|
|
||||||
</CollapsibleContent>
|
|
||||||
</SidebarMenuItem>
|
|
||||||
</Collapsible>
|
|
||||||
)
|
|
||||||
)}
|
|
||||||
</SidebarMenu>
|
|
||||||
</SidebarGroup>
|
</SidebarGroup>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
42
src/components/nav-secondary.tsx
Normal file
42
src/components/nav-secondary.tsx
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import * as React from "react"
|
||||||
|
import { LucideIcon } from "lucide-react"
|
||||||
|
|
||||||
|
import {
|
||||||
|
SidebarGroup,
|
||||||
|
SidebarGroupContent,
|
||||||
|
SidebarMenu,
|
||||||
|
SidebarMenuButton,
|
||||||
|
SidebarMenuItem,
|
||||||
|
} from "@/components/ui/sidebar"
|
||||||
|
|
||||||
|
export function NavSecondary({
|
||||||
|
items,
|
||||||
|
...props
|
||||||
|
}: {
|
||||||
|
items: {
|
||||||
|
title: string
|
||||||
|
url: string
|
||||||
|
icon: LucideIcon
|
||||||
|
}[]
|
||||||
|
} & React.ComponentPropsWithoutRef<typeof SidebarGroup>) {
|
||||||
|
return (
|
||||||
|
<SidebarGroup {...props}>
|
||||||
|
<SidebarGroupContent>
|
||||||
|
<SidebarMenu>
|
||||||
|
{items.map((item) => (
|
||||||
|
<SidebarMenuItem key={item.title}>
|
||||||
|
<SidebarMenuButton asChild>
|
||||||
|
<a href={item.url}>
|
||||||
|
<item.icon />
|
||||||
|
<span>{item.title}</span>
|
||||||
|
</a>
|
||||||
|
</SidebarMenuButton>
|
||||||
|
</SidebarMenuItem>
|
||||||
|
))}
|
||||||
|
</SidebarMenu>
|
||||||
|
</SidebarGroupContent>
|
||||||
|
</SidebarGroup>
|
||||||
|
)
|
||||||
|
}
|
@ -1,19 +1,18 @@
|
|||||||
"use client";
|
"use client"
|
||||||
|
|
||||||
import {
|
import {
|
||||||
BadgeCheck,
|
BellIcon,
|
||||||
Bell,
|
CreditCardIcon,
|
||||||
ChevronsUpDown,
|
LogOutIcon,
|
||||||
CreditCard,
|
MoreVerticalIcon,
|
||||||
LogOut,
|
UserCircleIcon,
|
||||||
Sparkles,
|
} from "lucide-react"
|
||||||
} from "lucide-react";
|
|
||||||
import {
|
import {
|
||||||
SidebarMenu,
|
Avatar,
|
||||||
SidebarMenuButton,
|
AvatarFallback,
|
||||||
SidebarMenuItem,
|
AvatarImage,
|
||||||
useSidebar,
|
} from "@/components/ui/avatar"
|
||||||
} from "@/components/ui/sidebar";
|
|
||||||
import {
|
import {
|
||||||
DropdownMenu,
|
DropdownMenu,
|
||||||
DropdownMenuContent,
|
DropdownMenuContent,
|
||||||
@ -22,21 +21,24 @@ import {
|
|||||||
DropdownMenuLabel,
|
DropdownMenuLabel,
|
||||||
DropdownMenuSeparator,
|
DropdownMenuSeparator,
|
||||||
DropdownMenuTrigger,
|
DropdownMenuTrigger,
|
||||||
} from "@/components/ui/dropdown-menu";
|
} from "@/components/ui/dropdown-menu"
|
||||||
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
|
import {
|
||||||
|
SidebarMenu,
|
||||||
export interface NavUserProps {
|
SidebarMenuButton,
|
||||||
user: {
|
SidebarMenuItem,
|
||||||
name: string;
|
useSidebar,
|
||||||
email: string;
|
} from "@/components/ui/sidebar"
|
||||||
avatar: string;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export function NavUser({
|
export function NavUser({
|
||||||
user,
|
user,
|
||||||
}: NavUserProps) {
|
}: {
|
||||||
const { isMobile } = useSidebar();
|
user: {
|
||||||
|
name: string
|
||||||
|
email: string
|
||||||
|
avatar: string
|
||||||
|
}
|
||||||
|
}) {
|
||||||
|
const { isMobile } = useSidebar()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SidebarMenu>
|
<SidebarMenu>
|
||||||
@ -47,15 +49,17 @@ export function NavUser({
|
|||||||
size="lg"
|
size="lg"
|
||||||
className="data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground"
|
className="data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground"
|
||||||
>
|
>
|
||||||
<Avatar className="h-8 w-8 rounded-lg">
|
<Avatar className="h-8 w-8 rounded-lg grayscale">
|
||||||
<AvatarImage src={user.avatar} alt={user.name} />
|
<AvatarImage src={user.avatar} alt={user.name} />
|
||||||
<AvatarFallback className="rounded-lg">CN</AvatarFallback>
|
<AvatarFallback className="rounded-lg">CN</AvatarFallback>
|
||||||
</Avatar>
|
</Avatar>
|
||||||
<div className="grid flex-1 text-left text-sm leading-tight">
|
<div className="grid flex-1 text-left text-sm leading-tight">
|
||||||
<span className="truncate font-semibold">{user.name}</span>
|
<span className="truncate font-medium">{user.name}</span>
|
||||||
<span className="truncate text-xs">{user.email}</span>
|
<span className="truncate text-xs text-muted-foreground">
|
||||||
|
{user.email}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<ChevronsUpDown className="ml-auto size-4" />
|
<MoreVerticalIcon className="ml-auto size-4" />
|
||||||
</SidebarMenuButton>
|
</SidebarMenuButton>
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuContent
|
<DropdownMenuContent
|
||||||
@ -71,41 +75,36 @@ export function NavUser({
|
|||||||
<AvatarFallback className="rounded-lg">CN</AvatarFallback>
|
<AvatarFallback className="rounded-lg">CN</AvatarFallback>
|
||||||
</Avatar>
|
</Avatar>
|
||||||
<div className="grid flex-1 text-left text-sm leading-tight">
|
<div className="grid flex-1 text-left text-sm leading-tight">
|
||||||
<span className="truncate font-semibold">{user.name}</span>
|
<span className="truncate font-medium">{user.name}</span>
|
||||||
<span className="truncate text-xs">{user.email}</span>
|
<span className="truncate text-xs text-muted-foreground">
|
||||||
|
{user.email}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</DropdownMenuLabel>
|
</DropdownMenuLabel>
|
||||||
<DropdownMenuSeparator />
|
<DropdownMenuSeparator />
|
||||||
<DropdownMenuGroup>
|
<DropdownMenuGroup>
|
||||||
<DropdownMenuItem>
|
<DropdownMenuItem>
|
||||||
<Sparkles />
|
<UserCircleIcon />
|
||||||
Upgrade to Pro
|
|
||||||
</DropdownMenuItem>
|
|
||||||
</DropdownMenuGroup>
|
|
||||||
<DropdownMenuSeparator />
|
|
||||||
<DropdownMenuGroup>
|
|
||||||
<DropdownMenuItem>
|
|
||||||
<BadgeCheck />
|
|
||||||
Account
|
Account
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
<DropdownMenuItem>
|
<DropdownMenuItem>
|
||||||
<CreditCard />
|
<CreditCardIcon />
|
||||||
Billing
|
Billing
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
<DropdownMenuItem>
|
<DropdownMenuItem>
|
||||||
<Bell />
|
<BellIcon />
|
||||||
Notifications
|
Notifications
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
</DropdownMenuGroup>
|
</DropdownMenuGroup>
|
||||||
<DropdownMenuSeparator />
|
<DropdownMenuSeparator />
|
||||||
<DropdownMenuItem>
|
<DropdownMenuItem>
|
||||||
<LogOut />
|
<LogOutIcon />
|
||||||
Log out
|
Log out
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
</DropdownMenuContent>
|
</DropdownMenuContent>
|
||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
</SidebarMenuItem>
|
</SidebarMenuItem>
|
||||||
</SidebarMenu>
|
</SidebarMenu>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
101
src/components/section-cards.tsx
Normal file
101
src/components/section-cards.tsx
Normal file
@ -0,0 +1,101 @@
|
|||||||
|
import { TrendingDownIcon, TrendingUpIcon } from "lucide-react"
|
||||||
|
|
||||||
|
import { Badge } from "@/components/ui/badge"
|
||||||
|
import {
|
||||||
|
Card,
|
||||||
|
CardDescription,
|
||||||
|
CardFooter,
|
||||||
|
CardHeader,
|
||||||
|
CardTitle,
|
||||||
|
} from "@/components/ui/card"
|
||||||
|
|
||||||
|
export function SectionCards() {
|
||||||
|
return (
|
||||||
|
<div className="*:data-[slot=card]:shadow-xs @xl/main:grid-cols-2 @5xl/main:grid-cols-4 grid grid-cols-1 gap-4 px-4 *:data-[slot=card]:bg-gradient-to-t *:data-[slot=card]:from-primary/5 *:data-[slot=card]:to-card dark:*:data-[slot=card]:bg-card lg:px-6">
|
||||||
|
<Card className="@container/card">
|
||||||
|
<CardHeader className="relative">
|
||||||
|
<CardDescription>Total Revenue</CardDescription>
|
||||||
|
<CardTitle className="@[250px]/card:text-3xl text-2xl font-semibold tabular-nums">
|
||||||
|
$1,250.00
|
||||||
|
</CardTitle>
|
||||||
|
<div className="absolute right-4 top-4">
|
||||||
|
<Badge variant="outline" className="flex gap-1 rounded-lg text-xs">
|
||||||
|
<TrendingUpIcon className="size-3" />
|
||||||
|
+12.5%
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
</CardHeader>
|
||||||
|
<CardFooter className="flex-col items-start gap-1 text-sm">
|
||||||
|
<div className="line-clamp-1 flex gap-2 font-medium">
|
||||||
|
Trending up this month <TrendingUpIcon className="size-4" />
|
||||||
|
</div>
|
||||||
|
<div className="text-muted-foreground">
|
||||||
|
Visitors for the last 6 months
|
||||||
|
</div>
|
||||||
|
</CardFooter>
|
||||||
|
</Card>
|
||||||
|
<Card className="@container/card">
|
||||||
|
<CardHeader className="relative">
|
||||||
|
<CardDescription>New Customers</CardDescription>
|
||||||
|
<CardTitle className="@[250px]/card:text-3xl text-2xl font-semibold tabular-nums">
|
||||||
|
1,234
|
||||||
|
</CardTitle>
|
||||||
|
<div className="absolute right-4 top-4">
|
||||||
|
<Badge variant="outline" className="flex gap-1 rounded-lg text-xs">
|
||||||
|
<TrendingDownIcon className="size-3" />
|
||||||
|
-20%
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
</CardHeader>
|
||||||
|
<CardFooter className="flex-col items-start gap-1 text-sm">
|
||||||
|
<div className="line-clamp-1 flex gap-2 font-medium">
|
||||||
|
Down 20% this period <TrendingDownIcon className="size-4" />
|
||||||
|
</div>
|
||||||
|
<div className="text-muted-foreground">
|
||||||
|
Acquisition needs attention
|
||||||
|
</div>
|
||||||
|
</CardFooter>
|
||||||
|
</Card>
|
||||||
|
<Card className="@container/card">
|
||||||
|
<CardHeader className="relative">
|
||||||
|
<CardDescription>Active Accounts</CardDescription>
|
||||||
|
<CardTitle className="@[250px]/card:text-3xl text-2xl font-semibold tabular-nums">
|
||||||
|
45,678
|
||||||
|
</CardTitle>
|
||||||
|
<div className="absolute right-4 top-4">
|
||||||
|
<Badge variant="outline" className="flex gap-1 rounded-lg text-xs">
|
||||||
|
<TrendingUpIcon className="size-3" />
|
||||||
|
+12.5%
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
</CardHeader>
|
||||||
|
<CardFooter className="flex-col items-start gap-1 text-sm">
|
||||||
|
<div className="line-clamp-1 flex gap-2 font-medium">
|
||||||
|
Strong user retention <TrendingUpIcon className="size-4" />
|
||||||
|
</div>
|
||||||
|
<div className="text-muted-foreground">Engagement exceed targets</div>
|
||||||
|
</CardFooter>
|
||||||
|
</Card>
|
||||||
|
<Card className="@container/card">
|
||||||
|
<CardHeader className="relative">
|
||||||
|
<CardDescription>Growth Rate</CardDescription>
|
||||||
|
<CardTitle className="@[250px]/card:text-3xl text-2xl font-semibold tabular-nums">
|
||||||
|
4.5%
|
||||||
|
</CardTitle>
|
||||||
|
<div className="absolute right-4 top-4">
|
||||||
|
<Badge variant="outline" className="flex gap-1 rounded-lg text-xs">
|
||||||
|
<TrendingUpIcon className="size-3" />
|
||||||
|
+4.5%
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
</CardHeader>
|
||||||
|
<CardFooter className="flex-col items-start gap-1 text-sm">
|
||||||
|
<div className="line-clamp-1 flex gap-2 font-medium">
|
||||||
|
Steady performance <TrendingUpIcon className="size-4" />
|
||||||
|
</div>
|
||||||
|
<div className="text-muted-foreground">Meets growth projections</div>
|
||||||
|
</CardFooter>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
11
src/components/site-header.tsx
Normal file
11
src/components/site-header.tsx
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
import { Separator } from "@/components/ui/separator"
|
||||||
|
|
||||||
|
export function SiteHeader() {
|
||||||
|
return (
|
||||||
|
<header className="group-has-data-[collapsible=icon]/sidebar-wrapper:h-12 flex h-12 shrink-0 items-center gap-2 border-b transition-[width,height] ease-linear">
|
||||||
|
<div className="flex w-full items-center gap-1 px-4 lg:gap-2 lg:px-6">
|
||||||
|
<h1 className="text-base font-medium">管理员</h1>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
)
|
||||||
|
}
|
@ -12,87 +12,89 @@ export default {
|
|||||||
"./src/lib/**/*.{js,ts,jsx,tsx,mdx}",
|
"./src/lib/**/*.{js,ts,jsx,tsx,mdx}",
|
||||||
],
|
],
|
||||||
theme: {
|
theme: {
|
||||||
extend: {
|
extend: {
|
||||||
colors: {
|
colors: {
|
||||||
background: "hsl(var(--background))",
|
background: 'hsl(var(--background))',
|
||||||
foreground: "hsl(var(--foreground))",
|
foreground: 'hsl(var(--foreground))',
|
||||||
card: {
|
card: {
|
||||||
DEFAULT: "hsl(var(--card))",
|
DEFAULT: 'hsl(var(--card))',
|
||||||
foreground: "hsl(var(--card-foreground))",
|
foreground: 'hsl(var(--card-foreground))'
|
||||||
},
|
},
|
||||||
popover: {
|
popover: {
|
||||||
DEFAULT: "hsl(var(--popover))",
|
DEFAULT: 'hsl(var(--popover))',
|
||||||
foreground: "hsl(var(--popover-foreground))",
|
foreground: 'hsl(var(--popover-foreground))'
|
||||||
},
|
},
|
||||||
primary: {
|
primary: {
|
||||||
DEFAULT: "hsl(var(--primary))",
|
DEFAULT: 'hsl(var(--primary))',
|
||||||
foreground: "hsl(var(--primary-foreground))",
|
foreground: 'hsl(var(--primary-foreground))'
|
||||||
},
|
},
|
||||||
secondary: {
|
secondary: {
|
||||||
DEFAULT: "hsl(var(--secondary))",
|
DEFAULT: 'hsl(var(--secondary))',
|
||||||
foreground: "hsl(var(--secondary-foreground))",
|
foreground: 'hsl(var(--secondary-foreground))'
|
||||||
},
|
},
|
||||||
muted: {
|
muted: {
|
||||||
DEFAULT: "hsl(var(--muted))",
|
DEFAULT: 'hsl(var(--muted))',
|
||||||
foreground: "hsl(var(--muted-foreground))",
|
foreground: 'hsl(var(--muted-foreground))'
|
||||||
},
|
},
|
||||||
accent: {
|
accent: {
|
||||||
DEFAULT: "hsl(var(--accent))",
|
DEFAULT: 'hsl(var(--accent))',
|
||||||
foreground: "hsl(var(--accent-foreground))",
|
foreground: 'hsl(var(--accent-foreground))'
|
||||||
},
|
},
|
||||||
destructive: {
|
destructive: {
|
||||||
DEFAULT: "hsl(var(--destructive))",
|
DEFAULT: 'hsl(var(--destructive))',
|
||||||
foreground: "hsl(var(--destructive-foreground))",
|
foreground: 'hsl(var(--destructive-foreground))'
|
||||||
},
|
},
|
||||||
border: "hsl(var(--border))",
|
border: 'hsl(var(--border))',
|
||||||
input: "hsl(var(--input))",
|
input: 'hsl(var(--input))',
|
||||||
ring: "hsl(var(--ring))",
|
ring: 'hsl(var(--ring))',
|
||||||
chart: {
|
chart: {
|
||||||
"1": "hsl(var(--chart-1))",
|
'1': 'hsl(var(--chart-1))',
|
||||||
"2": "hsl(var(--chart-2))",
|
'2': 'hsl(var(--chart-2))',
|
||||||
"3": "hsl(var(--chart-3))",
|
'3': 'hsl(var(--chart-3))',
|
||||||
"4": "hsl(var(--chart-4))",
|
'4': 'hsl(var(--chart-4))',
|
||||||
"5": "hsl(var(--chart-5))",
|
'5': 'hsl(var(--chart-5))'
|
||||||
},
|
},
|
||||||
sidebar: {
|
sidebar: {
|
||||||
DEFAULT: "hsl(var(--sidebar-background))",
|
DEFAULT: 'hsl(var(--sidebar-background))',
|
||||||
foreground: "hsl(var(--sidebar-foreground))",
|
foreground: 'hsl(var(--sidebar-foreground))',
|
||||||
primary: "hsl(var(--sidebar-primary))",
|
primary: 'hsl(var(--sidebar-primary))',
|
||||||
"primary-foreground": "hsl(var(--sidebar-primary-foreground))",
|
'primary-foreground': 'hsl(var(--sidebar-primary-foreground))',
|
||||||
accent: "hsl(var(--sidebar-accent))",
|
accent: 'hsl(var(--sidebar-accent))',
|
||||||
"accent-foreground": "hsl(var(--sidebar-accent-foreground))",
|
'accent-foreground': 'hsl(var(--sidebar-accent-foreground))',
|
||||||
border: "hsl(var(--sidebar-border))",
|
border: 'hsl(var(--sidebar-border))',
|
||||||
ring: "hsl(var(--sidebar-ring))",
|
ring: 'hsl(var(--sidebar-ring))',
|
||||||
},
|
'primary-foreground': 'hsl(var(--sidebar-primary-foreground))',
|
||||||
},
|
'accent-foreground': 'hsl(var(--sidebar-accent-foreground))'
|
||||||
borderRadius: {
|
}
|
||||||
lg: "var(--radius)",
|
},
|
||||||
md: "calc(var(--radius) - 2px)",
|
borderRadius: {
|
||||||
sm: "calc(var(--radius) - 4px)",
|
lg: 'var(--radius)',
|
||||||
},
|
md: 'calc(var(--radius) - 2px)',
|
||||||
keyframes: {
|
sm: 'calc(var(--radius) - 4px)'
|
||||||
"accordion-down": {
|
},
|
||||||
from: {
|
keyframes: {
|
||||||
height: "0",
|
'accordion-down': {
|
||||||
},
|
from: {
|
||||||
to: {
|
height: '0'
|
||||||
height: "var(--radix-accordion-content-height)",
|
},
|
||||||
},
|
to: {
|
||||||
},
|
height: 'var(--radix-accordion-content-height)'
|
||||||
"accordion-up": {
|
}
|
||||||
from: {
|
},
|
||||||
height: "var(--radix-accordion-content-height)",
|
'accordion-up': {
|
||||||
},
|
from: {
|
||||||
to: {
|
height: 'var(--radix-accordion-content-height)'
|
||||||
height: "0",
|
},
|
||||||
},
|
to: {
|
||||||
},
|
height: '0'
|
||||||
},
|
}
|
||||||
animation: {
|
}
|
||||||
"accordion-down": "accordion-down 0.2s ease-out",
|
},
|
||||||
"accordion-up": "accordion-up 0.2s ease-out",
|
animation: {
|
||||||
},
|
'accordion-down': 'accordion-down 0.2s ease-out',
|
||||||
},
|
'accordion-up': 'accordion-up 0.2s ease-out'
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
plugins: [animate],
|
plugins: [animate],
|
||||||
} satisfies Config;
|
} satisfies Config;
|
||||||
|
Loading…
Reference in New Issue
Block a user