@thinhnguyencth1204/nextcli 0.9.0 → 1.0.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 +3 -3
- package/package.json +1 -1
- package/templates/features/api/src/lib/api/axios.ts +1 -90
- package/templates/features/auth/messages/vi/auth.json +2 -1
- package/templates/features/auth/src/app/(auth)/change-password/page.tsx +5 -4
- package/templates/features/auth/src/app/(auth)/layout.tsx +2 -5
- package/templates/features/auth/src/app/(auth)/sign-in/page.tsx +5 -4
- package/templates/features/auth/src/app/api/v1/auth/login/route.ts +24 -29
- package/templates/features/auth/src/app/api/v1/auth/logout/route.ts +0 -5
- package/templates/features/auth/src/components/layout/auth/auth-shell.tsx +24 -0
- package/templates/features/auth/src/features/auth/components/account-panel.tsx +15 -3
- package/templates/features/auth/src/features/auth/components/change-password-form.tsx +27 -30
- package/templates/features/auth/src/features/auth/components/sign-in-form.tsx +33 -42
- package/templates/features/auth/src/lib/auth/client.ts +2 -2
- package/templates/features/auth/src/lib/auth/server.ts +2 -2
- package/templates/features/dashboard/src/app/(dashboard)/account/page.tsx +9 -7
- package/templates/features/dashboard/src/app/(dashboard)/dashboard/page.tsx +24 -10
- package/templates/features/dashboard/src/components/layout/private/app-sidebar.tsx +1 -13
- package/templates/features/dashboard/src/components/layout/private/dashboard-layout.tsx +31 -22
- package/templates/features/dashboard/src/components/layout/private/page-shell.tsx +40 -0
- package/templates/features/database/prisma/schema.prisma +1 -0
- package/templates/features/example/messages/vi/example.json +11 -1
- package/templates/features/example/src/app/(dashboard)/example/page.tsx +92 -3
- package/templates/features/example/src/example/components/example-table.tsx +15 -2
- package/templates/next-base/bun.lock +407 -0
- package/templates/next-base/messages/vi/auth.json +2 -1
- package/templates/next-base/messages/vi/common.json +19 -0
- package/templates/next-base/messages/vi/example.json +11 -1
- package/templates/next-base/next-env.d.ts +1 -1
- package/templates/next-base/prisma/schema.prisma +1 -0
- package/templates/next-base/src/app/(auth)/change-password/page.tsx +5 -4
- package/templates/next-base/src/app/(auth)/layout.tsx +2 -5
- package/templates/next-base/src/app/(auth)/sign-in/page.tsx +5 -4
- package/templates/next-base/src/app/(dashboard)/account/page.tsx +9 -7
- package/templates/next-base/src/app/(dashboard)/dashboard/page.tsx +24 -10
- package/templates/next-base/src/app/(dashboard)/example/page.tsx +92 -3
- package/templates/next-base/src/app/api/v1/auth/login/route.ts +24 -29
- package/templates/next-base/src/app/api/v1/auth/logout/route.ts +0 -5
- package/templates/next-base/src/components/branding/logo.tsx +27 -4
- package/templates/next-base/src/components/layout/auth/auth-shell.tsx +24 -0
- package/templates/next-base/src/components/layout/private/app-sidebar.tsx +1 -13
- package/templates/next-base/src/components/layout/private/dashboard-layout.tsx +31 -22
- package/templates/next-base/src/components/layout/private/page-shell.tsx +40 -0
- package/templates/next-base/src/example/components/example-table.tsx +15 -2
- package/templates/next-base/src/features/auth/components/account-panel.tsx +15 -3
- package/templates/next-base/src/features/auth/components/change-password-form.tsx +27 -30
- package/templates/next-base/src/features/auth/components/sign-in-form.tsx +33 -42
- package/templates/next-base/src/lib/api/axios.ts +1 -90
- package/templates/next-base/src/lib/auth/client.ts +2 -2
- package/templates/next-base/src/lib/auth/server.ts +2 -2
- package/templates/features/api/src/lib/api/token-store.ts +0 -13
- package/templates/features/auth/src/app/api/v1/auth/refresh/route.ts +0 -32
- package/templates/features/auth/src/lib/auth/cookies.ts +0 -15
- package/templates/next-base/src/app/api/v1/auth/refresh/route.ts +0 -32
- package/templates/next-base/src/lib/api/token-store.ts +0 -13
- package/templates/next-base/src/lib/auth/cookies.ts +0 -15
|
@@ -11,7 +11,6 @@
|
|
|
11
11
|
"submit": "Đăng nhập",
|
|
12
12
|
"submitting": "Đang đăng nhập...",
|
|
13
13
|
"invalidInput": "Vui lòng nhập tên đăng nhập và mật khẩu hợp lệ.",
|
|
14
|
-
"missingAccessToken": "Không tìm thấy access token.",
|
|
15
14
|
"success": "Đăng nhập thành công.",
|
|
16
15
|
"failed": "Đăng nhập thất bại."
|
|
17
16
|
},
|
|
@@ -30,6 +29,8 @@
|
|
|
30
29
|
},
|
|
31
30
|
"account": {
|
|
32
31
|
"title": "Tài khoản",
|
|
32
|
+
"description": "Xem thông tin phiên đăng nhập hiện tại.",
|
|
33
|
+
"panelTitle": "Thông tin tài khoản",
|
|
33
34
|
"loading": "Đang tải thông tin tài khoản...",
|
|
34
35
|
"noSession": "Chưa có phiên đăng nhập hoạt động.",
|
|
35
36
|
"userId": "Mã người dùng",
|
|
@@ -12,6 +12,25 @@
|
|
|
12
12
|
"themeSystem": "Theo hệ thống",
|
|
13
13
|
"toggleTheme": "Chuyển giao diện"
|
|
14
14
|
},
|
|
15
|
+
"dashboardPage": {
|
|
16
|
+
"title": "Bảng điều khiển",
|
|
17
|
+
"description": "Tổng quan nhanh về hoạt động hệ thống.",
|
|
18
|
+
"placeholder": "Thêm widget và số liệu của bạn tại đây.",
|
|
19
|
+
"stats": {
|
|
20
|
+
"users": {
|
|
21
|
+
"title": "Người dùng",
|
|
22
|
+
"value": "—"
|
|
23
|
+
},
|
|
24
|
+
"records": {
|
|
25
|
+
"title": "Bản ghi",
|
|
26
|
+
"value": "—"
|
|
27
|
+
},
|
|
28
|
+
"activity": {
|
|
29
|
+
"title": "Hoạt động hôm nay",
|
|
30
|
+
"value": "—"
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
},
|
|
15
34
|
"userMenu": {
|
|
16
35
|
"title": "Tài khoản",
|
|
17
36
|
"anonymousName": "Người dùng",
|
|
@@ -1,6 +1,16 @@
|
|
|
1
1
|
{
|
|
2
2
|
"page": {
|
|
3
|
-
"title": "Ví dụ"
|
|
3
|
+
"title": "Ví dụ",
|
|
4
|
+
"description": "Trang mẫu với bảng dữ liệu, thẻ nội dung và nút thêm bản ghi.",
|
|
5
|
+
"add": "Thêm mới",
|
|
6
|
+
"createTitle": "Thêm bản ghi ví dụ",
|
|
7
|
+
"nameLabel": "Tên",
|
|
8
|
+
"descriptionLabel": "Mô tả",
|
|
9
|
+
"createSubmit": "Lưu",
|
|
10
|
+
"creating": "Đang lưu...",
|
|
11
|
+
"createSuccess": "Đã thêm bản ghi ví dụ.",
|
|
12
|
+
"createFailed": "Không thể thêm bản ghi ví dụ.",
|
|
13
|
+
"invalidInput": "Vui lòng nhập tên hợp lệ."
|
|
4
14
|
},
|
|
5
15
|
"table": {
|
|
6
16
|
"name": "Tên",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/// <reference types="next" />
|
|
2
2
|
/// <reference types="next/image-types/global" />
|
|
3
|
-
import "./.next/types/routes.d.ts";
|
|
3
|
+
import "./.next/dev/types/routes.d.ts";
|
|
4
4
|
|
|
5
5
|
// NOTE: This file should not be edited
|
|
6
6
|
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
|
|
@@ -70,6 +70,7 @@ model Verification {
|
|
|
70
70
|
createdAt DateTime? @default(now())
|
|
71
71
|
updatedAt DateTime? @updatedAt
|
|
72
72
|
}
|
|
73
|
+
|
|
73
74
|
// Optional Chatbox models are appended by NexTCLI only when adding the chat feature.
|
|
74
75
|
// Review generated schema changes before running migrations on production databases.
|
|
75
76
|
|
|
@@ -5,10 +5,11 @@ export default function ChangePasswordPage() {
|
|
|
5
5
|
const t = useTranslations("auth.changePasswordPage");
|
|
6
6
|
|
|
7
7
|
return (
|
|
8
|
-
<
|
|
9
|
-
<
|
|
10
|
-
|
|
8
|
+
<div className="space-y-6">
|
|
9
|
+
<div className="text-center sm:text-left">
|
|
10
|
+
<h1 className="text-2xl font-semibold tracking-tight">{t("title")}</h1>
|
|
11
|
+
</div>
|
|
11
12
|
<ChangePasswordForm />
|
|
12
|
-
</
|
|
13
|
+
</div>
|
|
13
14
|
);
|
|
14
15
|
}
|
|
@@ -1,9 +1,6 @@
|
|
|
1
1
|
import type { ReactNode } from "react";
|
|
2
|
+
import { AuthShell } from "@/components/layout/auth/auth-shell";
|
|
2
3
|
|
|
3
4
|
export default function AuthRouteLayout({ children }: { children: ReactNode }) {
|
|
4
|
-
return
|
|
5
|
-
<div className="flex min-h-screen items-center justify-center p-4">
|
|
6
|
-
<div className="w-full max-w-md">{children}</div>
|
|
7
|
-
</div>
|
|
8
|
-
);
|
|
5
|
+
return <AuthShell>{children}</AuthShell>;
|
|
9
6
|
}
|
|
@@ -5,10 +5,11 @@ export default function SignInPage() {
|
|
|
5
5
|
const t = useTranslations("auth.signInPage");
|
|
6
6
|
|
|
7
7
|
return (
|
|
8
|
-
<
|
|
9
|
-
<
|
|
10
|
-
|
|
8
|
+
<div className="space-y-6">
|
|
9
|
+
<div className="text-center sm:text-left">
|
|
10
|
+
<h1 className="text-2xl font-semibold tracking-tight">{t("title")}</h1>
|
|
11
|
+
</div>
|
|
11
12
|
<SignInForm />
|
|
12
|
-
</
|
|
13
|
+
</div>
|
|
13
14
|
);
|
|
14
15
|
}
|
|
@@ -1,18 +1,20 @@
|
|
|
1
1
|
import Link from "next/link";
|
|
2
2
|
import { useTranslations } from "next-intl";
|
|
3
3
|
import { AccountPanel } from "@/features/auth/components/account-panel";
|
|
4
|
+
import { PageShell } from "@/components/layout/private/page-shell";
|
|
4
5
|
import { Button } from "@/components/ui/button";
|
|
5
6
|
|
|
6
7
|
export default function AccountPage() {
|
|
7
8
|
const t = useTranslations("auth.account");
|
|
8
9
|
|
|
9
10
|
return (
|
|
10
|
-
<
|
|
11
|
-
<
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
11
|
+
<PageShell title={t("title")} description={t("description")}>
|
|
12
|
+
<div className="space-y-4">
|
|
13
|
+
<AccountPanel />
|
|
14
|
+
<Button asChild variant="outline">
|
|
15
|
+
<Link href="/sign-in">{t("goToSignIn")}</Link>
|
|
16
|
+
</Button>
|
|
17
|
+
</div>
|
|
18
|
+
</PageShell>
|
|
17
19
|
);
|
|
18
20
|
}
|
|
@@ -1,17 +1,31 @@
|
|
|
1
1
|
import { useTranslations } from "next-intl";
|
|
2
|
-
import {
|
|
2
|
+
import { PageShell } from "@/components/layout/private/page-shell";
|
|
3
3
|
|
|
4
4
|
export default function DashboardPage() {
|
|
5
|
-
const t = useTranslations("common.
|
|
5
|
+
const t = useTranslations("common.dashboardPage");
|
|
6
|
+
|
|
7
|
+
const stats = [
|
|
8
|
+
{ title: t("stats.users.title"), value: t("stats.users.value") },
|
|
9
|
+
{ title: t("stats.records.title"), value: t("stats.records.value") },
|
|
10
|
+
{ title: t("stats.activity.title"), value: t("stats.activity.value") },
|
|
11
|
+
];
|
|
6
12
|
|
|
7
13
|
return (
|
|
8
|
-
<
|
|
9
|
-
<
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
14
|
+
<PageShell title={t("title")} description={t("description")}>
|
|
15
|
+
<div className="grid grid-cols-1 gap-6 sm:grid-cols-2 lg:grid-cols-3">
|
|
16
|
+
{stats.map((stat) => (
|
|
17
|
+
<div
|
|
18
|
+
key={stat.title}
|
|
19
|
+
className="overflow-hidden rounded-lg border bg-card p-6 shadow-sm"
|
|
20
|
+
>
|
|
21
|
+
<h3 className="text-base font-semibold leading-6">{stat.title}</h3>
|
|
22
|
+
<p className="mt-2 text-3xl font-bold tracking-tight text-primary">
|
|
23
|
+
{stat.value}
|
|
24
|
+
</p>
|
|
25
|
+
</div>
|
|
26
|
+
))}
|
|
27
|
+
</div>
|
|
28
|
+
<p className="mt-8 text-sm text-muted-foreground">{t("placeholder")}</p>
|
|
29
|
+
</PageShell>
|
|
16
30
|
);
|
|
17
31
|
}
|
|
@@ -1,13 +1,102 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useState } from "react";
|
|
4
|
+
import type { FormEvent } from "react";
|
|
1
5
|
import { useTranslations } from "next-intl";
|
|
6
|
+
import { Plus } from "lucide-react";
|
|
7
|
+
import { toast } from "sonner";
|
|
8
|
+
import { useCreateExample } from "@/example/api/use-mutations";
|
|
9
|
+
import { createExampleSchema } from "@/example/validations";
|
|
2
10
|
import { ExampleTable } from "@/example/components/example-table";
|
|
11
|
+
import { PageShell } from "@/components/layout/private/page-shell";
|
|
12
|
+
import { Button } from "@/components/ui/button";
|
|
13
|
+
import {
|
|
14
|
+
Dialog,
|
|
15
|
+
DialogContent,
|
|
16
|
+
DialogFooter,
|
|
17
|
+
DialogHeader,
|
|
18
|
+
DialogTitle,
|
|
19
|
+
DialogTrigger,
|
|
20
|
+
} from "@/components/ui/dialog";
|
|
21
|
+
import { Input } from "@/components/ui/input";
|
|
22
|
+
import { Label } from "@/components/ui/label";
|
|
3
23
|
|
|
4
24
|
export default function ExamplePage() {
|
|
5
25
|
const t = useTranslations("example.page");
|
|
26
|
+
const createExample = useCreateExample();
|
|
27
|
+
const [open, setOpen] = useState(false);
|
|
28
|
+
const [name, setName] = useState("");
|
|
29
|
+
const [description, setDescription] = useState("");
|
|
30
|
+
|
|
31
|
+
const onSubmit = async (event: FormEvent<HTMLFormElement>) => {
|
|
32
|
+
event.preventDefault();
|
|
33
|
+
|
|
34
|
+
const parsed = createExampleSchema.safeParse({ name, description });
|
|
35
|
+
if (!parsed.success) {
|
|
36
|
+
toast.error(t("invalidInput"));
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
try {
|
|
41
|
+
await createExample.mutateAsync(parsed.data);
|
|
42
|
+
toast.success(t("createSuccess"));
|
|
43
|
+
setName("");
|
|
44
|
+
setDescription("");
|
|
45
|
+
setOpen(false);
|
|
46
|
+
} catch (error) {
|
|
47
|
+
const message =
|
|
48
|
+
error instanceof Error ? error.message : t("createFailed");
|
|
49
|
+
toast.error(message);
|
|
50
|
+
}
|
|
51
|
+
};
|
|
6
52
|
|
|
7
53
|
return (
|
|
8
|
-
<
|
|
9
|
-
|
|
54
|
+
<PageShell
|
|
55
|
+
title={t("title")}
|
|
56
|
+
description={t("description")}
|
|
57
|
+
actions={
|
|
58
|
+
<Dialog open={open} onOpenChange={setOpen}>
|
|
59
|
+
<DialogTrigger asChild>
|
|
60
|
+
<Button>
|
|
61
|
+
<Plus className="mr-2 h-4 w-4" />
|
|
62
|
+
{t("add")}
|
|
63
|
+
</Button>
|
|
64
|
+
</DialogTrigger>
|
|
65
|
+
<DialogContent>
|
|
66
|
+
<DialogHeader>
|
|
67
|
+
<DialogTitle>{t("createTitle")}</DialogTitle>
|
|
68
|
+
</DialogHeader>
|
|
69
|
+
<form onSubmit={onSubmit} className="space-y-4">
|
|
70
|
+
<div className="space-y-2">
|
|
71
|
+
<Label htmlFor="example-name">{t("nameLabel")}</Label>
|
|
72
|
+
<Input
|
|
73
|
+
id="example-name"
|
|
74
|
+
value={name}
|
|
75
|
+
onChange={(event) => setName(event.target.value)}
|
|
76
|
+
required
|
|
77
|
+
/>
|
|
78
|
+
</div>
|
|
79
|
+
<div className="space-y-2">
|
|
80
|
+
<Label htmlFor="example-description">
|
|
81
|
+
{t("descriptionLabel")}
|
|
82
|
+
</Label>
|
|
83
|
+
<Input
|
|
84
|
+
id="example-description"
|
|
85
|
+
value={description}
|
|
86
|
+
onChange={(event) => setDescription(event.target.value)}
|
|
87
|
+
/>
|
|
88
|
+
</div>
|
|
89
|
+
<DialogFooter>
|
|
90
|
+
<Button type="submit" disabled={createExample.isPending}>
|
|
91
|
+
{createExample.isPending ? t("creating") : t("createSubmit")}
|
|
92
|
+
</Button>
|
|
93
|
+
</DialogFooter>
|
|
94
|
+
</form>
|
|
95
|
+
</DialogContent>
|
|
96
|
+
</Dialog>
|
|
97
|
+
}
|
|
98
|
+
>
|
|
10
99
|
<ExampleTable />
|
|
11
|
-
</
|
|
100
|
+
</PageShell>
|
|
12
101
|
);
|
|
13
102
|
}
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { getRefreshCookieName, refreshCookieOptions } from "@/lib/auth/cookies";
|
|
2
1
|
import { fail, ok } from "@/lib/api/response";
|
|
3
2
|
import prisma from "@/lib/db/prisma";
|
|
4
3
|
|
|
@@ -19,16 +18,19 @@ export async function POST(request: Request) {
|
|
|
19
18
|
});
|
|
20
19
|
}
|
|
21
20
|
|
|
22
|
-
const signInResponse = await fetch(
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
21
|
+
const signInResponse = await fetch(
|
|
22
|
+
`${authBaseUrl}/api/auth/sign-in/username`,
|
|
23
|
+
{
|
|
24
|
+
method: "POST",
|
|
25
|
+
headers: {
|
|
26
|
+
"Content-Type": "application/json",
|
|
27
|
+
},
|
|
28
|
+
body: JSON.stringify({
|
|
29
|
+
username: payload.username,
|
|
30
|
+
password: payload.password,
|
|
31
|
+
}),
|
|
26
32
|
},
|
|
27
|
-
|
|
28
|
-
username: payload.username,
|
|
29
|
-
password: payload.password,
|
|
30
|
-
}),
|
|
31
|
-
});
|
|
33
|
+
);
|
|
32
34
|
|
|
33
35
|
if (!signInResponse.ok) {
|
|
34
36
|
return fail("UNAUTHORIZED", "Invalid credentials.", {
|
|
@@ -36,20 +38,12 @@ export async function POST(request: Request) {
|
|
|
36
38
|
});
|
|
37
39
|
}
|
|
38
40
|
|
|
39
|
-
const
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
if (!tokenResponse.ok) {
|
|
47
|
-
return fail("UPSTREAM_ERROR", "Unable to issue access token.", {
|
|
48
|
-
status: tokenResponse.status,
|
|
49
|
-
});
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
const tokenPayload = await tokenResponse.json();
|
|
41
|
+
const upstreamSetCookies =
|
|
42
|
+
typeof signInResponse.headers.getSetCookie === "function"
|
|
43
|
+
? signInResponse.headers.getSetCookie()
|
|
44
|
+
: [];
|
|
45
|
+
const cookieHeader =
|
|
46
|
+
upstreamSetCookies[0] ?? signInResponse.headers.get("set-cookie");
|
|
53
47
|
|
|
54
48
|
const dbUser = await prisma.user.findUnique({
|
|
55
49
|
where: { username: payload.username },
|
|
@@ -57,14 +51,15 @@ export async function POST(request: Request) {
|
|
|
57
51
|
});
|
|
58
52
|
|
|
59
53
|
const response = ok({
|
|
60
|
-
accessToken: tokenPayload.token,
|
|
61
54
|
requirePasswordChange: dbUser?.requirePasswordChange ?? false,
|
|
62
55
|
});
|
|
63
56
|
|
|
64
|
-
if (
|
|
65
|
-
|
|
57
|
+
if (upstreamSetCookies.length > 0) {
|
|
58
|
+
for (const setCookie of upstreamSetCookies) {
|
|
59
|
+
response.headers.append("set-cookie", setCookie);
|
|
60
|
+
}
|
|
61
|
+
} else if (cookieHeader) {
|
|
62
|
+
response.headers.append("set-cookie", cookieHeader);
|
|
66
63
|
}
|
|
67
|
-
response.cookies.set(getRefreshCookieName(), crypto.randomUUID(), refreshCookieOptions());
|
|
68
|
-
|
|
69
64
|
return response;
|
|
70
65
|
}
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { getRefreshCookieName, refreshCookieOptions } from "@/lib/auth/cookies";
|
|
2
1
|
import { fail, ok } from "@/lib/api/response";
|
|
3
2
|
|
|
4
3
|
const authBaseUrl =
|
|
@@ -20,9 +19,5 @@ export async function POST(request: Request) {
|
|
|
20
19
|
: fail("UPSTREAM_ERROR", "Failed to sign out.", {
|
|
21
20
|
status: signOutResponse.status,
|
|
22
21
|
});
|
|
23
|
-
response.cookies.set(getRefreshCookieName(), "", {
|
|
24
|
-
...refreshCookieOptions(),
|
|
25
|
-
maxAge: 0,
|
|
26
|
-
});
|
|
27
22
|
return response;
|
|
28
23
|
}
|
|
@@ -6,17 +6,40 @@ type LogoProps = {
|
|
|
6
6
|
className?: string;
|
|
7
7
|
size?: number;
|
|
8
8
|
showLabel?: boolean;
|
|
9
|
+
variant?: "default" | "auth" | "header";
|
|
9
10
|
};
|
|
10
11
|
|
|
11
|
-
export function Logo({
|
|
12
|
+
export function Logo({
|
|
13
|
+
className,
|
|
14
|
+
size = 28,
|
|
15
|
+
showLabel = false,
|
|
16
|
+
variant = "default",
|
|
17
|
+
}: LogoProps) {
|
|
18
|
+
if (variant === "auth") {
|
|
19
|
+
return (
|
|
20
|
+
<Image
|
|
21
|
+
src={branding.logoPath}
|
|
22
|
+
alt={branding.projectName}
|
|
23
|
+
width={200}
|
|
24
|
+
height={60}
|
|
25
|
+
className={cn("h-16 w-auto object-contain", className)}
|
|
26
|
+
priority
|
|
27
|
+
/>
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const imageSize = variant === "header" ? 120 : size;
|
|
32
|
+
const imageClassName =
|
|
33
|
+
variant === "header" ? "h-10 w-auto object-contain shrink-0" : "shrink-0";
|
|
34
|
+
|
|
12
35
|
return (
|
|
13
36
|
<span className={cn("inline-flex items-center gap-2", className)}>
|
|
14
37
|
<Image
|
|
15
38
|
src={branding.logoPath}
|
|
16
39
|
alt={branding.projectName}
|
|
17
|
-
width={
|
|
18
|
-
height={size}
|
|
19
|
-
className=
|
|
40
|
+
width={imageSize}
|
|
41
|
+
height={variant === "header" ? 40 : size}
|
|
42
|
+
className={imageClassName}
|
|
20
43
|
priority
|
|
21
44
|
/>
|
|
22
45
|
{showLabel ? (
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
import { Logo } from "@/components/branding/logo";
|
|
3
|
+
|
|
4
|
+
type AuthShellProps = {
|
|
5
|
+
children: ReactNode;
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
export function AuthShell({ children }: AuthShellProps) {
|
|
9
|
+
return (
|
|
10
|
+
<div className="flex min-h-screen flex-col items-center justify-center bg-muted/40 py-12 sm:px-6 lg:px-8">
|
|
11
|
+
<div className="sm:mx-auto sm:w-full sm:max-w-md">
|
|
12
|
+
<div className="flex justify-center">
|
|
13
|
+
<Logo variant="auth" />
|
|
14
|
+
</div>
|
|
15
|
+
</div>
|
|
16
|
+
|
|
17
|
+
<div className="mt-8 sm:mx-auto sm:w-full sm:max-w-md">
|
|
18
|
+
<div className="rounded-lg border bg-card px-4 py-8 shadow-sm sm:px-10">
|
|
19
|
+
{children}
|
|
20
|
+
</div>
|
|
21
|
+
</div>
|
|
22
|
+
</div>
|
|
23
|
+
);
|
|
24
|
+
}
|
|
@@ -1,14 +1,7 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
3
|
import { ChevronLeft, ChevronRight } from "lucide-react";
|
|
4
|
-
import
|
|
5
|
-
import {
|
|
6
|
-
useSidebar,
|
|
7
|
-
Sidebar,
|
|
8
|
-
SidebarHeader,
|
|
9
|
-
SidebarTrigger,
|
|
10
|
-
} from "@/components/ui/sidebar";
|
|
11
|
-
import { Logo } from "@/components/branding/logo";
|
|
4
|
+
import { useSidebar, Sidebar, SidebarTrigger } from "@/components/ui/sidebar";
|
|
12
5
|
import { NavSidebar } from "@/components/layout/private/nav-sidebar";
|
|
13
6
|
import { cn } from "@/utils/cn";
|
|
14
7
|
|
|
@@ -33,11 +26,6 @@ export function AppSidebar() {
|
|
|
33
26
|
</SidebarTrigger>
|
|
34
27
|
</div>
|
|
35
28
|
)}
|
|
36
|
-
<SidebarHeader className="p-3">
|
|
37
|
-
<Link href="/dashboard">
|
|
38
|
-
<Logo showLabel />
|
|
39
|
-
</Link>
|
|
40
|
-
</SidebarHeader>
|
|
41
29
|
<NavSidebar />
|
|
42
30
|
</Sidebar>
|
|
43
31
|
);
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
|
-
import { Moon, Sun } from "lucide-react";
|
|
3
|
+
import { Menu, Moon, Sun } from "lucide-react";
|
|
4
4
|
import { useTheme } from "next-themes";
|
|
5
5
|
import { useTranslations } from "next-intl";
|
|
6
6
|
import { SidebarProvider, SidebarTrigger } from "@/components/ui/sidebar";
|
|
@@ -19,33 +19,42 @@ export function DashboardLayout({ children }: { children: React.ReactNode }) {
|
|
|
19
19
|
return (
|
|
20
20
|
<SidebarProvider>
|
|
21
21
|
<div className="flex h-screen w-full flex-col">
|
|
22
|
-
<header className="flex h-
|
|
23
|
-
<div className="flex items-center
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
22
|
+
<header className="relative z-50 flex h-[3.75rem] shrink-0 items-center border-b border-border bg-card">
|
|
23
|
+
<div className="flex w-full items-center justify-between px-4 md:px-6">
|
|
24
|
+
<div className="flex items-center gap-2">
|
|
25
|
+
{isMobile ? (
|
|
26
|
+
<SidebarTrigger variant="outline" size="icon">
|
|
27
|
+
<Menu className="h-5 w-5" />
|
|
28
|
+
</SidebarTrigger>
|
|
29
|
+
) : null}
|
|
30
|
+
<Logo variant="header" />
|
|
31
|
+
</div>
|
|
27
32
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
33
|
+
<div className="flex items-center gap-2">
|
|
34
|
+
{!isMobile ? (
|
|
35
|
+
<Button
|
|
36
|
+
variant="ghost"
|
|
37
|
+
size="icon"
|
|
38
|
+
onClick={() => setTheme(theme === "dark" ? "light" : "dark")}
|
|
39
|
+
title={t("toggleTheme")}
|
|
40
|
+
className="rounded-full"
|
|
41
|
+
>
|
|
42
|
+
{theme === "dark" ? (
|
|
43
|
+
<Sun className="h-5 w-5" />
|
|
44
|
+
) : (
|
|
45
|
+
<Moon className="h-5 w-5" />
|
|
46
|
+
)}
|
|
47
|
+
</Button>
|
|
48
|
+
) : null}
|
|
49
|
+
<NavUser />
|
|
50
|
+
</div>
|
|
42
51
|
</div>
|
|
43
52
|
</header>
|
|
44
53
|
|
|
45
54
|
<div className="flex flex-1 overflow-hidden">
|
|
46
55
|
<AppSidebar />
|
|
47
|
-
<ScrollArea className="flex-1">
|
|
48
|
-
<main
|
|
56
|
+
<ScrollArea className="flex-1 bg-background">
|
|
57
|
+
<main>{children}</main>
|
|
49
58
|
</ScrollArea>
|
|
50
59
|
</div>
|
|
51
60
|
</div>
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
import { cn } from "@/utils/cn";
|
|
3
|
+
|
|
4
|
+
type PageShellProps = {
|
|
5
|
+
title: string;
|
|
6
|
+
description?: string;
|
|
7
|
+
actions?: ReactNode;
|
|
8
|
+
children: ReactNode;
|
|
9
|
+
className?: string;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export function PageShell({
|
|
13
|
+
title,
|
|
14
|
+
description,
|
|
15
|
+
actions,
|
|
16
|
+
children,
|
|
17
|
+
className,
|
|
18
|
+
}: PageShellProps) {
|
|
19
|
+
return (
|
|
20
|
+
<div
|
|
21
|
+
className={cn(
|
|
22
|
+
"mx-auto w-full max-w-screen-2xl px-4 py-10 sm:px-6 lg:px-10",
|
|
23
|
+
className,
|
|
24
|
+
)}
|
|
25
|
+
>
|
|
26
|
+
<div className="flex flex-col gap-4 pb-6 sm:flex-row sm:items-start sm:justify-between">
|
|
27
|
+
<div className="space-y-1">
|
|
28
|
+
<h1 className="text-3xl font-black tracking-tight">{title}</h1>
|
|
29
|
+
{description ? (
|
|
30
|
+
<p className="text-sm text-muted-foreground">{description}</p>
|
|
31
|
+
) : null}
|
|
32
|
+
</div>
|
|
33
|
+
{actions ? (
|
|
34
|
+
<div className="flex shrink-0 items-center gap-2">{actions}</div>
|
|
35
|
+
) : null}
|
|
36
|
+
</div>
|
|
37
|
+
{children}
|
|
38
|
+
</div>
|
|
39
|
+
);
|
|
40
|
+
}
|
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
} from "@tanstack/react-table";
|
|
11
11
|
import { useExample } from "@/example/api/use-example";
|
|
12
12
|
import { DataTable } from "@/components/ui/data-table/data-table";
|
|
13
|
+
import { Card, CardContent } from "@/components/ui/card";
|
|
13
14
|
|
|
14
15
|
type ExampleItem = {
|
|
15
16
|
id: string;
|
|
@@ -44,8 +45,20 @@ export function ExampleTable() {
|
|
|
44
45
|
});
|
|
45
46
|
|
|
46
47
|
if (isLoading) {
|
|
47
|
-
return
|
|
48
|
+
return (
|
|
49
|
+
<Card>
|
|
50
|
+
<CardContent className="py-8 text-sm text-muted-foreground">
|
|
51
|
+
{t("loading")}
|
|
52
|
+
</CardContent>
|
|
53
|
+
</Card>
|
|
54
|
+
);
|
|
48
55
|
}
|
|
49
56
|
|
|
50
|
-
return
|
|
57
|
+
return (
|
|
58
|
+
<Card>
|
|
59
|
+
<CardContent className="pt-6">
|
|
60
|
+
<DataTable table={table} />
|
|
61
|
+
</CardContent>
|
|
62
|
+
</Card>
|
|
63
|
+
);
|
|
51
64
|
}
|