@thinhnguyencth1204/nextcli 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +632 -90
- package/package.json +1 -1
- package/templates/next-base/components.json +21 -0
- package/templates/next-base/messages/vi/auth.json +28 -0
- package/templates/next-base/messages/vi/common.json +34 -0
- package/templates/next-base/messages/vi/example.json +10 -0
- package/templates/next-base/next.config.ts +11 -1
- package/templates/next-base/nextcli.json +8 -0
- package/templates/next-base/package.json +21 -1
- package/templates/next-base/postcss.config.mjs +5 -0
- package/templates/next-base/src/app/(auth)/layout.tsx +9 -0
- package/templates/next-base/src/app/(auth)/sign-in/page.tsx +6 -3
- package/templates/next-base/src/app/(dashboard)/account/page.tsx +9 -5
- package/templates/next-base/src/app/(dashboard)/dashboard/page.tsx +17 -0
- package/templates/next-base/src/app/(dashboard)/example/page.tsx +5 -2
- package/templates/next-base/src/app/(dashboard)/layout.tsx +10 -0
- package/templates/next-base/src/app/globals.css +107 -0
- package/templates/next-base/src/app/layout.tsx +18 -8
- package/templates/next-base/src/app/page.tsx +2 -18
- package/templates/next-base/src/components/layout/private/app-sidebar.tsx +45 -0
- package/templates/next-base/src/components/layout/private/dashboard-layout.tsx +53 -0
- package/templates/next-base/src/components/layout/private/locale-switcher.tsx +45 -0
- package/templates/next-base/src/components/layout/private/nav-sidebar.tsx +55 -0
- package/templates/next-base/src/components/layout/private/nav-user.tsx +99 -0
- package/templates/next-base/src/components/providers/theme-provider.tsx +11 -0
- package/templates/next-base/src/components/ui/alert-dialog.tsx +11 -0
- package/templates/next-base/src/components/ui/avatar.tsx +45 -0
- package/templates/next-base/src/components/ui/badge.tsx +29 -0
- package/templates/next-base/src/components/ui/button.tsx +47 -7
- package/templates/next-base/src/components/ui/card.tsx +54 -0
- package/templates/next-base/src/components/ui/data-table/data-table-column-header.tsx +23 -0
- package/templates/next-base/src/components/ui/data-table/data-table-filter-list.tsx +3 -0
- package/templates/next-base/src/components/ui/data-table/data-table-pagination.tsx +35 -0
- package/templates/next-base/src/components/ui/data-table/data-table-skeleton.tsx +11 -0
- package/templates/next-base/src/components/ui/data-table/data-table-toolbar.tsx +14 -0
- package/templates/next-base/src/components/ui/data-table/data-table-view-options.tsx +3 -0
- package/templates/next-base/src/components/ui/data-table/data-table.tsx +72 -0
- package/templates/next-base/src/components/ui/dialog.tsx +105 -0
- package/templates/next-base/src/components/ui/dropdown-menu.tsx +44 -0
- package/templates/next-base/src/components/ui/input.tsx +19 -0
- package/templates/next-base/src/components/ui/label.tsx +15 -0
- package/templates/next-base/src/components/ui/popover.tsx +30 -0
- package/templates/next-base/src/components/ui/scroll-area.tsx +47 -0
- package/templates/next-base/src/components/ui/select.tsx +76 -0
- package/templates/next-base/src/components/ui/separator.tsx +23 -0
- package/templates/next-base/src/components/ui/sheet.tsx +117 -0
- package/templates/next-base/src/components/ui/sidebar.tsx +215 -0
- package/templates/next-base/src/components/ui/skeleton.tsx +10 -0
- package/templates/next-base/src/components/ui/sonner.tsx +3 -0
- package/templates/next-base/src/components/ui/table.tsx +54 -0
- package/templates/next-base/src/components/ui/tabs.tsx +52 -0
- package/templates/next-base/src/components/ui/textarea.tsx +17 -0
- package/templates/next-base/src/components/ui/tooltip.tsx +26 -0
- package/templates/next-base/src/data/sidebar-modules.ts +11 -0
- package/templates/next-base/src/example/components/example-table.tsx +25 -40
- package/templates/next-base/src/features/auth/components/account-panel.tsx +21 -8
- package/templates/next-base/src/features/auth/components/sign-in-form.tsx +43 -30
- package/templates/next-base/src/hooks/index.ts +1 -1
- package/templates/next-base/src/hooks/table/use-data-table.ts +33 -0
- package/templates/next-base/src/hooks/use-mobile.ts +25 -0
- package/templates/next-base/src/i18n/config.ts +7 -0
- package/templates/next-base/src/i18n/namespaces.ts +5 -0
- package/templates/next-base/src/i18n/request.ts +19 -2
- package/templates/next-base/src/lib/prisma.ts +11 -1
- package/templates/next-base/src/types/data-table.ts +4 -0
- package/templates/next-base/src/types/index.ts +2 -0
- package/templates/next-base/middleware.ts +0 -10
- package/templates/next-base/src/app/styles.css +0 -12
package/package.json
CHANGED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://ui.shadcn.com/schema.json",
|
|
3
|
+
"style": "new-york",
|
|
4
|
+
"rsc": true,
|
|
5
|
+
"tsx": true,
|
|
6
|
+
"tailwind": {
|
|
7
|
+
"config": "",
|
|
8
|
+
"css": "src/app/globals.css",
|
|
9
|
+
"baseColor": "neutral",
|
|
10
|
+
"cssVariables": true,
|
|
11
|
+
"prefix": ""
|
|
12
|
+
},
|
|
13
|
+
"iconLibrary": "lucide",
|
|
14
|
+
"aliases": {
|
|
15
|
+
"components": "@/components",
|
|
16
|
+
"utils": "@/utils/cn",
|
|
17
|
+
"ui": "@/components/ui",
|
|
18
|
+
"lib": "@/lib",
|
|
19
|
+
"hooks": "@/hooks"
|
|
20
|
+
}
|
|
21
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
{
|
|
2
|
+
"signInPage": {
|
|
3
|
+
"title": "Đăng nhập",
|
|
4
|
+
"description": "Dùng email và mật khẩu để truy cập hệ thống."
|
|
5
|
+
},
|
|
6
|
+
"signInForm": {
|
|
7
|
+
"email": "Email",
|
|
8
|
+
"password": "Mật khẩu",
|
|
9
|
+
"emailPlaceholder": "ban@example.com",
|
|
10
|
+
"passwordPlaceholder": "********",
|
|
11
|
+
"submit": "Đăng nhập",
|
|
12
|
+
"submitting": "Đang đăng nhập...",
|
|
13
|
+
"invalidInput": "Vui lòng nhập email và mật khẩu hợp lệ.",
|
|
14
|
+
"missingAccessToken": "Không tìm thấy access token.",
|
|
15
|
+
"success": "Đăng nhập thành công.",
|
|
16
|
+
"failed": "Đăng nhập thất bại."
|
|
17
|
+
},
|
|
18
|
+
"account": {
|
|
19
|
+
"title": "Tài khoản",
|
|
20
|
+
"loading": "Đang tải thông tin tài khoản...",
|
|
21
|
+
"noSession": "Chưa có phiên đăng nhập hoạt động.",
|
|
22
|
+
"userId": "Mã người dùng",
|
|
23
|
+
"email": "Email",
|
|
24
|
+
"name": "Tên",
|
|
25
|
+
"na": "N/A",
|
|
26
|
+
"goToSignIn": "Đi tới trang đăng nhập"
|
|
27
|
+
}
|
|
28
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
{
|
|
2
|
+
"appName": "__PROJECT_NAME__",
|
|
3
|
+
"sidebar": {
|
|
4
|
+
"groupGeneral": "Tổng quan",
|
|
5
|
+
"dashboard": "Bảng điều khiển",
|
|
6
|
+
"example": "Ví dụ",
|
|
7
|
+
"account": "Tài khoản"
|
|
8
|
+
},
|
|
9
|
+
"header": {
|
|
10
|
+
"themeLight": "Sáng",
|
|
11
|
+
"themeDark": "Tối",
|
|
12
|
+
"themeSystem": "Theo hệ thống",
|
|
13
|
+
"toggleTheme": "Chuyển giao diện"
|
|
14
|
+
},
|
|
15
|
+
"userMenu": {
|
|
16
|
+
"title": "Tài khoản",
|
|
17
|
+
"anonymousName": "Người dùng",
|
|
18
|
+
"anonymousEmail": "user@example.com",
|
|
19
|
+
"appearance": "Giao diện",
|
|
20
|
+
"logout": "Đăng xuất"
|
|
21
|
+
},
|
|
22
|
+
"locale": {
|
|
23
|
+
"label": "Ngôn ngữ",
|
|
24
|
+
"vi": "Tiếng Việt",
|
|
25
|
+
"en": "English",
|
|
26
|
+
"ja": "日本語",
|
|
27
|
+
"ko": "한국어"
|
|
28
|
+
},
|
|
29
|
+
"table": {
|
|
30
|
+
"noResults": "Không có dữ liệu.",
|
|
31
|
+
"previous": "Trước",
|
|
32
|
+
"next": "Sau"
|
|
33
|
+
}
|
|
34
|
+
}
|
|
@@ -1,7 +1,17 @@
|
|
|
1
1
|
import type { NextConfig } from "next";
|
|
2
|
+
import createNextIntlPlugin from "next-intl/plugin";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
|
|
6
|
+
const rootDir = path.dirname(fileURLToPath(import.meta.url));
|
|
2
7
|
|
|
3
8
|
const nextConfig: NextConfig = {
|
|
4
9
|
reactStrictMode: true,
|
|
10
|
+
turbopack: {
|
|
11
|
+
root: rootDir,
|
|
12
|
+
},
|
|
5
13
|
};
|
|
6
14
|
|
|
7
|
-
|
|
15
|
+
const withNextIntl = createNextIntlPlugin("./src/i18n/request.ts");
|
|
16
|
+
|
|
17
|
+
export default withNextIntl(nextConfig);
|
|
@@ -8,30 +8,50 @@
|
|
|
8
8
|
"build": "next build",
|
|
9
9
|
"start": "next start",
|
|
10
10
|
"lint": "eslint .",
|
|
11
|
+
"postinstall": "prisma generate",
|
|
11
12
|
"db:generate": "prisma generate",
|
|
12
13
|
"db:migrate": "prisma migrate dev",
|
|
13
14
|
"db:studio": "prisma studio"
|
|
14
15
|
},
|
|
15
16
|
"dependencies": {
|
|
16
17
|
"@prisma/client": "^7.8.0",
|
|
18
|
+
"@prisma/adapter-pg": "^7.8.0",
|
|
19
|
+
"@radix-ui/react-avatar": "^1.1.10",
|
|
20
|
+
"@radix-ui/react-dialog": "^1.1.15",
|
|
21
|
+
"@radix-ui/react-dropdown-menu": "^2.1.16",
|
|
22
|
+
"@radix-ui/react-label": "^2.1.7",
|
|
23
|
+
"@radix-ui/react-popover": "^1.1.15",
|
|
24
|
+
"@radix-ui/react-scroll-area": "^1.2.10",
|
|
25
|
+
"@radix-ui/react-select": "^2.2.6",
|
|
26
|
+
"@radix-ui/react-separator": "^1.1.7",
|
|
27
|
+
"@radix-ui/react-slot": "^1.2.3",
|
|
28
|
+
"@radix-ui/react-tabs": "^1.1.13",
|
|
29
|
+
"@radix-ui/react-tooltip": "^1.2.8",
|
|
17
30
|
"@tanstack/react-form": "^1.0.3",
|
|
18
31
|
"@tanstack/react-query": "^5.56.2",
|
|
19
32
|
"@tanstack/react-query-devtools": "^5.56.2",
|
|
20
33
|
"@tanstack/react-table": "^8.20.5",
|
|
34
|
+
"@dnd-kit/core": "^6.3.1",
|
|
21
35
|
"axios": "^1.7.7",
|
|
22
36
|
"better-auth": "^1.6.11",
|
|
23
37
|
"class-variance-authority": "^0.7.0",
|
|
24
38
|
"clsx": "^2.1.1",
|
|
25
39
|
"date-fns": "^3.6.0",
|
|
40
|
+
"lucide-react": "^0.525.0",
|
|
26
41
|
"next": "^16.1.6",
|
|
27
42
|
"next-intl": "^4.13.0",
|
|
43
|
+
"next-themes": "^0.4.6",
|
|
44
|
+
"nuqs": "^2.8.1",
|
|
28
45
|
"react": "^19.0.0",
|
|
29
46
|
"react-dom": "^19.0.0",
|
|
47
|
+
"pg": "^8.16.3",
|
|
30
48
|
"sonner": "^1.7.1",
|
|
31
49
|
"tailwind-merge": "^2.5.3",
|
|
50
|
+
"tw-animate-css": "^1.3.0",
|
|
32
51
|
"zod": "^3.23.8"
|
|
33
52
|
},
|
|
34
53
|
"devDependencies": {
|
|
54
|
+
"@tailwindcss/postcss": "^4.1.11",
|
|
35
55
|
"dotenv": "^17.4.2",
|
|
36
56
|
"@types/node": "^22.7.4",
|
|
37
57
|
"@types/react": "^19.0.0",
|
|
@@ -39,7 +59,7 @@
|
|
|
39
59
|
"eslint": "^9.11.1",
|
|
40
60
|
"eslint-config-next": "^16.1.6",
|
|
41
61
|
"prisma": "^7.8.0",
|
|
42
|
-
"tailwindcss": "^
|
|
62
|
+
"tailwindcss": "^4.1.11",
|
|
43
63
|
"typescript": "^5.6.2"
|
|
44
64
|
}
|
|
45
65
|
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
|
|
3
|
+
export default function AuthLayout({ children }: { children: ReactNode }) {
|
|
4
|
+
return (
|
|
5
|
+
<main className="flex min-h-screen items-center justify-center bg-muted px-4 py-8">
|
|
6
|
+
<div className="w-full max-w-md">{children}</div>
|
|
7
|
+
</main>
|
|
8
|
+
);
|
|
9
|
+
}
|
|
@@ -1,10 +1,13 @@
|
|
|
1
|
+
import { useTranslations } from "next-intl";
|
|
1
2
|
import { SignInForm } from "@/features/auth/components/sign-in-form";
|
|
2
3
|
|
|
3
4
|
export default function SignInPage() {
|
|
5
|
+
const t = useTranslations("auth.signInPage");
|
|
6
|
+
|
|
4
7
|
return (
|
|
5
|
-
<main
|
|
6
|
-
<h1>
|
|
7
|
-
<p
|
|
8
|
+
<main className="space-y-3">
|
|
9
|
+
<h1 className="text-2xl font-semibold">{t("title")}</h1>
|
|
10
|
+
<p className="text-sm text-muted-foreground">{t("description")}</p>
|
|
8
11
|
<SignInForm />
|
|
9
12
|
</main>
|
|
10
13
|
);
|
|
@@ -1,14 +1,18 @@
|
|
|
1
1
|
import Link from "next/link";
|
|
2
|
+
import { useTranslations } from "next-intl";
|
|
2
3
|
import { AccountPanel } from "@/features/auth/components/account-panel";
|
|
4
|
+
import { Button } from "@/components/ui/button";
|
|
3
5
|
|
|
4
6
|
export default function AccountPage() {
|
|
7
|
+
const t = useTranslations("auth.account");
|
|
8
|
+
|
|
5
9
|
return (
|
|
6
|
-
<main
|
|
7
|
-
<h1>
|
|
10
|
+
<main className="space-y-4">
|
|
11
|
+
<h1 className="text-2xl font-semibold">{t("title")}</h1>
|
|
8
12
|
<AccountPanel />
|
|
9
|
-
<
|
|
10
|
-
<Link href="/sign-in">
|
|
11
|
-
</
|
|
13
|
+
<Button asChild variant="outline">
|
|
14
|
+
<Link href="/sign-in">{t("goToSignIn")}</Link>
|
|
15
|
+
</Button>
|
|
12
16
|
</main>
|
|
13
17
|
);
|
|
14
18
|
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { useTranslations } from "next-intl";
|
|
2
|
+
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
|
3
|
+
|
|
4
|
+
export default function DashboardPage() {
|
|
5
|
+
const t = useTranslations("common.sidebar");
|
|
6
|
+
|
|
7
|
+
return (
|
|
8
|
+
<Card>
|
|
9
|
+
<CardHeader>
|
|
10
|
+
<CardTitle>{t("dashboard")}</CardTitle>
|
|
11
|
+
</CardHeader>
|
|
12
|
+
<CardContent className="text-sm text-muted-foreground">
|
|
13
|
+
Starter dashboard page. Add your own widgets and metrics here.
|
|
14
|
+
</CardContent>
|
|
15
|
+
</Card>
|
|
16
|
+
);
|
|
17
|
+
}
|
|
@@ -1,9 +1,12 @@
|
|
|
1
|
+
import { useTranslations } from "next-intl";
|
|
1
2
|
import { ExampleTable } from "@/example/components/example-table";
|
|
2
3
|
|
|
3
4
|
export default function ExamplePage() {
|
|
5
|
+
const t = useTranslations("example.page");
|
|
6
|
+
|
|
4
7
|
return (
|
|
5
|
-
<main
|
|
6
|
-
<h1>
|
|
8
|
+
<main className="space-y-4">
|
|
9
|
+
<h1 className="text-2xl font-semibold">{t("title")}</h1>
|
|
7
10
|
<ExampleTable />
|
|
8
11
|
</main>
|
|
9
12
|
);
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
import { DashboardLayout } from "@/components/layout/private/dashboard-layout";
|
|
3
|
+
|
|
4
|
+
export default function DashboardRouteLayout({
|
|
5
|
+
children,
|
|
6
|
+
}: {
|
|
7
|
+
children: ReactNode;
|
|
8
|
+
}) {
|
|
9
|
+
return <DashboardLayout>{children}</DashboardLayout>;
|
|
10
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
@import "tailwindcss";
|
|
2
|
+
@import "tw-animate-css";
|
|
3
|
+
|
|
4
|
+
:root {
|
|
5
|
+
--background: oklch(1 0 0);
|
|
6
|
+
--foreground: oklch(0.145 0 0);
|
|
7
|
+
--card: oklch(1 0 0);
|
|
8
|
+
--card-foreground: oklch(0.145 0 0);
|
|
9
|
+
--popover: oklch(1 0 0);
|
|
10
|
+
--popover-foreground: oklch(0.145 0 0);
|
|
11
|
+
--primary: oklch(0.205 0 0);
|
|
12
|
+
--primary-foreground: oklch(0.985 0 0);
|
|
13
|
+
--secondary: oklch(0.97 0 0);
|
|
14
|
+
--secondary-foreground: oklch(0.205 0 0);
|
|
15
|
+
--muted: oklch(0.97 0 0);
|
|
16
|
+
--muted-foreground: oklch(0.556 0 0);
|
|
17
|
+
--accent: oklch(0.97 0 0);
|
|
18
|
+
--accent-foreground: oklch(0.205 0 0);
|
|
19
|
+
--destructive: oklch(0.577 0.245 27.325);
|
|
20
|
+
--destructive-foreground: oklch(0.985 0 0);
|
|
21
|
+
--border: oklch(0.922 0 0);
|
|
22
|
+
--input: oklch(0.922 0 0);
|
|
23
|
+
--ring: oklch(0.708 0 0);
|
|
24
|
+
--radius: 0.625rem;
|
|
25
|
+
--sidebar: oklch(0.985 0 0);
|
|
26
|
+
--sidebar-foreground: oklch(0.145 0 0);
|
|
27
|
+
--sidebar-primary: oklch(0.205 0 0);
|
|
28
|
+
--sidebar-primary-foreground: oklch(0.985 0 0);
|
|
29
|
+
--sidebar-accent: oklch(0.97 0 0);
|
|
30
|
+
--sidebar-accent-foreground: oklch(0.205 0 0);
|
|
31
|
+
--sidebar-border: oklch(0.922 0 0);
|
|
32
|
+
--sidebar-ring: oklch(0.708 0 0);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
.dark {
|
|
36
|
+
--background: oklch(0.145 0 0);
|
|
37
|
+
--foreground: oklch(0.985 0 0);
|
|
38
|
+
--card: oklch(0.205 0 0);
|
|
39
|
+
--card-foreground: oklch(0.985 0 0);
|
|
40
|
+
--popover: oklch(0.205 0 0);
|
|
41
|
+
--popover-foreground: oklch(0.985 0 0);
|
|
42
|
+
--primary: oklch(0.922 0 0);
|
|
43
|
+
--primary-foreground: oklch(0.205 0 0);
|
|
44
|
+
--secondary: oklch(0.269 0 0);
|
|
45
|
+
--secondary-foreground: oklch(0.985 0 0);
|
|
46
|
+
--muted: oklch(0.269 0 0);
|
|
47
|
+
--muted-foreground: oklch(0.708 0 0);
|
|
48
|
+
--accent: oklch(0.269 0 0);
|
|
49
|
+
--accent-foreground: oklch(0.985 0 0);
|
|
50
|
+
--destructive: oklch(0.704 0.191 22.216);
|
|
51
|
+
--destructive-foreground: oklch(0.985 0 0);
|
|
52
|
+
--border: oklch(1 0 0 / 10%);
|
|
53
|
+
--input: oklch(1 0 0 / 15%);
|
|
54
|
+
--ring: oklch(0.556 0 0);
|
|
55
|
+
--sidebar: oklch(0.205 0 0);
|
|
56
|
+
--sidebar-foreground: oklch(0.985 0 0);
|
|
57
|
+
--sidebar-primary: oklch(0.488 0.243 264.376);
|
|
58
|
+
--sidebar-primary-foreground: oklch(0.985 0 0);
|
|
59
|
+
--sidebar-accent: oklch(0.269 0 0);
|
|
60
|
+
--sidebar-accent-foreground: oklch(0.985 0 0);
|
|
61
|
+
--sidebar-border: oklch(1 0 0 / 10%);
|
|
62
|
+
--sidebar-ring: oklch(0.556 0 0);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
@theme inline {
|
|
66
|
+
--color-background: var(--background);
|
|
67
|
+
--color-foreground: var(--foreground);
|
|
68
|
+
--color-card: var(--card);
|
|
69
|
+
--color-card-foreground: var(--card-foreground);
|
|
70
|
+
--color-popover: var(--popover);
|
|
71
|
+
--color-popover-foreground: var(--popover-foreground);
|
|
72
|
+
--color-primary: var(--primary);
|
|
73
|
+
--color-primary-foreground: var(--primary-foreground);
|
|
74
|
+
--color-secondary: var(--secondary);
|
|
75
|
+
--color-secondary-foreground: var(--secondary-foreground);
|
|
76
|
+
--color-muted: var(--muted);
|
|
77
|
+
--color-muted-foreground: var(--muted-foreground);
|
|
78
|
+
--color-accent: var(--accent);
|
|
79
|
+
--color-accent-foreground: var(--accent-foreground);
|
|
80
|
+
--color-destructive: var(--destructive);
|
|
81
|
+
--color-destructive-foreground: var(--destructive-foreground);
|
|
82
|
+
--color-border: var(--border);
|
|
83
|
+
--color-input: var(--input);
|
|
84
|
+
--color-ring: var(--ring);
|
|
85
|
+
--color-sidebar: var(--sidebar);
|
|
86
|
+
--color-sidebar-foreground: var(--sidebar-foreground);
|
|
87
|
+
--color-sidebar-primary: var(--sidebar-primary);
|
|
88
|
+
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
|
|
89
|
+
--color-sidebar-accent: var(--sidebar-accent);
|
|
90
|
+
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
|
|
91
|
+
--color-sidebar-border: var(--sidebar-border);
|
|
92
|
+
--color-sidebar-ring: var(--sidebar-ring);
|
|
93
|
+
--radius-sm: calc(var(--radius) - 4px);
|
|
94
|
+
--radius-md: calc(var(--radius) - 2px);
|
|
95
|
+
--radius-lg: var(--radius);
|
|
96
|
+
--radius-xl: calc(var(--radius) + 4px);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
@layer base {
|
|
100
|
+
* {
|
|
101
|
+
@apply border-border outline-ring/50;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
body {
|
|
105
|
+
@apply bg-background text-foreground antialiased;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
@@ -1,27 +1,37 @@
|
|
|
1
1
|
import type { Metadata } from "next";
|
|
2
|
-
import {
|
|
2
|
+
import { Be_Vietnam_Pro } from "next/font/google";
|
|
3
|
+
import { getLocale, getMessages } from "next-intl/server";
|
|
4
|
+
import { NextIntlClientProvider } from "next-intl";
|
|
3
5
|
import { Toaster } from "sonner";
|
|
4
6
|
import type { ReactNode } from "react";
|
|
5
7
|
import { QueryProvider } from "@/components/providers/query-provider";
|
|
6
|
-
import "@/
|
|
8
|
+
import { ThemeProvider } from "@/components/providers/theme-provider";
|
|
9
|
+
import "@/app/globals.css";
|
|
7
10
|
|
|
8
|
-
const
|
|
11
|
+
const beVietnamPro = Be_Vietnam_Pro({ subsets: ["latin"] });
|
|
9
12
|
|
|
10
13
|
export const metadata: Metadata = {
|
|
11
14
|
title: "__PROJECT_NAME__",
|
|
12
15
|
description: "Outsource-ready Next.js scaffolded by NexTCLI",
|
|
13
16
|
};
|
|
14
17
|
|
|
15
|
-
export default function RootLayout({
|
|
18
|
+
export default async function RootLayout({
|
|
16
19
|
children,
|
|
17
20
|
}: Readonly<{
|
|
18
21
|
children: ReactNode;
|
|
19
22
|
}>) {
|
|
23
|
+
const locale = await getLocale();
|
|
24
|
+
const messages = await getMessages();
|
|
25
|
+
|
|
20
26
|
return (
|
|
21
|
-
<html lang=
|
|
22
|
-
<body className={
|
|
23
|
-
<
|
|
24
|
-
|
|
27
|
+
<html lang={locale} suppressHydrationWarning>
|
|
28
|
+
<body className={beVietnamPro.className}>
|
|
29
|
+
<NextIntlClientProvider locale={locale} messages={messages}>
|
|
30
|
+
<ThemeProvider attribute="class" defaultTheme="light" enableSystem>
|
|
31
|
+
<QueryProvider>{children}</QueryProvider>
|
|
32
|
+
<Toaster richColors position="top-right" />
|
|
33
|
+
</ThemeProvider>
|
|
34
|
+
</NextIntlClientProvider>
|
|
25
35
|
</body>
|
|
26
36
|
</html>
|
|
27
37
|
);
|
|
@@ -1,21 +1,5 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { redirect } from "next/navigation";
|
|
2
2
|
|
|
3
3
|
export default function HomePage() {
|
|
4
|
-
|
|
5
|
-
<main style={{ padding: 24 }}>
|
|
6
|
-
<h1>NexTCLI Base Template</h1>
|
|
7
|
-
<p>Ship outsource projects faster with standardized architecture.</p>
|
|
8
|
-
<ul style={{ display: "grid", gap: 8 }}>
|
|
9
|
-
<li>
|
|
10
|
-
<Link href="/example">Go to example dashboard page</Link>
|
|
11
|
-
</li>
|
|
12
|
-
<li>
|
|
13
|
-
<Link href="/sign-in">Go to sign-in page</Link>
|
|
14
|
-
</li>
|
|
15
|
-
<li>
|
|
16
|
-
<Link href="/account">Go to account page</Link>
|
|
17
|
-
</li>
|
|
18
|
-
</ul>
|
|
19
|
-
</main>
|
|
20
|
-
);
|
|
4
|
+
redirect("/dashboard");
|
|
21
5
|
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { ChevronLeft, ChevronRight } from "lucide-react";
|
|
4
|
+
import Link from "next/link";
|
|
5
|
+
import { useTranslations } from "next-intl";
|
|
6
|
+
import {
|
|
7
|
+
useSidebar,
|
|
8
|
+
Sidebar,
|
|
9
|
+
SidebarHeader,
|
|
10
|
+
SidebarTrigger,
|
|
11
|
+
} from "@/components/ui/sidebar";
|
|
12
|
+
import { NavSidebar } from "@/components/layout/private/nav-sidebar";
|
|
13
|
+
import { cn } from "@/utils/cn";
|
|
14
|
+
|
|
15
|
+
export function AppSidebar() {
|
|
16
|
+
const { state, isMobile } = useSidebar();
|
|
17
|
+
const t = useTranslations("common");
|
|
18
|
+
const isCollapsed = state === "collapsed";
|
|
19
|
+
|
|
20
|
+
return (
|
|
21
|
+
<Sidebar collapsible="offcanvas">
|
|
22
|
+
{!isMobile && (
|
|
23
|
+
<div className="absolute -right-4 top-20 z-10">
|
|
24
|
+
<SidebarTrigger
|
|
25
|
+
className={cn(
|
|
26
|
+
"h-8 w-4 rounded-l-none border border-border bg-background p-0 hover:bg-accent",
|
|
27
|
+
)}
|
|
28
|
+
>
|
|
29
|
+
{isCollapsed ? (
|
|
30
|
+
<ChevronRight className="h-4 w-4" />
|
|
31
|
+
) : (
|
|
32
|
+
<ChevronLeft className="h-4 w-4" />
|
|
33
|
+
)}
|
|
34
|
+
</SidebarTrigger>
|
|
35
|
+
</div>
|
|
36
|
+
)}
|
|
37
|
+
<SidebarHeader className="p-3">
|
|
38
|
+
<Link href="/" className="font-semibold">
|
|
39
|
+
{t("appName")}
|
|
40
|
+
</Link>
|
|
41
|
+
</SidebarHeader>
|
|
42
|
+
<NavSidebar />
|
|
43
|
+
</Sidebar>
|
|
44
|
+
);
|
|
45
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { Moon, Sun } from "lucide-react";
|
|
4
|
+
import { useTheme } from "next-themes";
|
|
5
|
+
import { useTranslations } from "next-intl";
|
|
6
|
+
import { SidebarProvider, SidebarTrigger } from "@/components/ui/sidebar";
|
|
7
|
+
import { ScrollArea } from "@/components/ui/scroll-area";
|
|
8
|
+
import { Button } from "@/components/ui/button";
|
|
9
|
+
import { AppSidebar } from "@/components/layout/private/app-sidebar";
|
|
10
|
+
import { NavUser } from "@/components/layout/private/nav-user";
|
|
11
|
+
import { useIsMobile } from "@/hooks/use-mobile";
|
|
12
|
+
|
|
13
|
+
export function DashboardLayout({ children }: { children: React.ReactNode }) {
|
|
14
|
+
const { theme, setTheme } = useTheme();
|
|
15
|
+
const isMobile = useIsMobile();
|
|
16
|
+
const t = useTranslations("common.header");
|
|
17
|
+
|
|
18
|
+
return (
|
|
19
|
+
<SidebarProvider>
|
|
20
|
+
<div className="flex h-screen w-full flex-col">
|
|
21
|
+
<header className="flex h-14 items-center justify-between border-b bg-card px-4">
|
|
22
|
+
<div className="flex items-center gap-2">
|
|
23
|
+
{isMobile && <SidebarTrigger />}
|
|
24
|
+
<p className="font-semibold">__PROJECT_NAME__</p>
|
|
25
|
+
</div>
|
|
26
|
+
|
|
27
|
+
<div className="flex items-center gap-2">
|
|
28
|
+
<Button
|
|
29
|
+
variant="ghost"
|
|
30
|
+
size="icon"
|
|
31
|
+
onClick={() => setTheme(theme === "dark" ? "light" : "dark")}
|
|
32
|
+
title={t("toggleTheme")}
|
|
33
|
+
>
|
|
34
|
+
{theme === "dark" ? (
|
|
35
|
+
<Sun className="h-5 w-5" />
|
|
36
|
+
) : (
|
|
37
|
+
<Moon className="h-5 w-5" />
|
|
38
|
+
)}
|
|
39
|
+
</Button>
|
|
40
|
+
<NavUser />
|
|
41
|
+
</div>
|
|
42
|
+
</header>
|
|
43
|
+
|
|
44
|
+
<div className="flex flex-1 overflow-hidden">
|
|
45
|
+
<AppSidebar />
|
|
46
|
+
<ScrollArea className="flex-1">
|
|
47
|
+
<main className="p-4 md:p-6">{children}</main>
|
|
48
|
+
</ScrollArea>
|
|
49
|
+
</div>
|
|
50
|
+
</div>
|
|
51
|
+
</SidebarProvider>
|
|
52
|
+
);
|
|
53
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useRouter } from "next/navigation";
|
|
4
|
+
import { useLocale, useTranslations } from "next-intl";
|
|
5
|
+
import { locales, type AppLocale } from "@/i18n/config";
|
|
6
|
+
import {
|
|
7
|
+
Select,
|
|
8
|
+
SelectContent,
|
|
9
|
+
SelectItem,
|
|
10
|
+
SelectTrigger,
|
|
11
|
+
SelectValue,
|
|
12
|
+
} from "@/components/ui/select";
|
|
13
|
+
|
|
14
|
+
export function LocaleSwitcher() {
|
|
15
|
+
const locale = useLocale() as AppLocale;
|
|
16
|
+
const router = useRouter();
|
|
17
|
+
const t = useTranslations("common.locale");
|
|
18
|
+
|
|
19
|
+
if (locales.length <= 1) {
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const setLocale = (nextLocale: string) => {
|
|
24
|
+
document.cookie = `NEXT_LOCALE=${nextLocale}; path=/; max-age=31536000`;
|
|
25
|
+
router.refresh();
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
return (
|
|
29
|
+
<div className="space-y-2">
|
|
30
|
+
<p className="text-xs font-medium text-muted-foreground">{t("label")}</p>
|
|
31
|
+
<Select value={locale} onValueChange={setLocale}>
|
|
32
|
+
<SelectTrigger>
|
|
33
|
+
<SelectValue />
|
|
34
|
+
</SelectTrigger>
|
|
35
|
+
<SelectContent>
|
|
36
|
+
{locales.map((item) => (
|
|
37
|
+
<SelectItem key={item} value={item}>
|
|
38
|
+
{t(item)}
|
|
39
|
+
</SelectItem>
|
|
40
|
+
))}
|
|
41
|
+
</SelectContent>
|
|
42
|
+
</Select>
|
|
43
|
+
</div>
|
|
44
|
+
);
|
|
45
|
+
}
|