@thinhnguyencth1204/nextcli 0.8.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.
Files changed (112) hide show
  1. package/README.md +27 -24
  2. package/dist/cli.js +168 -107
  3. package/package.json +1 -1
  4. package/templates/features/api/src/lib/api/axios.ts +1 -90
  5. package/templates/features/auth/messages/vi/auth.json +2 -1
  6. package/templates/features/auth/src/app/(auth)/change-password/page.tsx +5 -4
  7. package/templates/features/auth/src/app/(auth)/layout.tsx +2 -5
  8. package/templates/features/auth/src/app/(auth)/sign-in/page.tsx +5 -4
  9. package/templates/features/auth/src/app/api/v1/auth/login/route.ts +24 -29
  10. package/templates/features/auth/src/app/api/v1/auth/logout/route.ts +0 -5
  11. package/templates/features/auth/src/components/layout/auth/auth-shell.tsx +24 -0
  12. package/templates/features/auth/src/features/auth/components/account-panel.tsx +15 -3
  13. package/templates/features/auth/src/features/auth/components/change-password-form.tsx +27 -30
  14. package/templates/features/auth/src/features/auth/components/sign-in-form.tsx +33 -42
  15. package/templates/features/auth/src/lib/auth/client.ts +2 -2
  16. package/templates/features/auth/src/lib/auth/server.ts +2 -2
  17. package/templates/features/dashboard/src/app/(dashboard)/account/page.tsx +9 -7
  18. package/templates/features/dashboard/src/app/(dashboard)/dashboard/page.tsx +24 -10
  19. package/templates/features/dashboard/src/components/layout/private/app-sidebar.tsx +1 -13
  20. package/templates/features/dashboard/src/components/layout/private/dashboard-layout.tsx +31 -22
  21. package/templates/features/dashboard/src/components/layout/private/page-shell.tsx +40 -0
  22. package/templates/features/database/prisma/schema.prisma +1 -0
  23. package/templates/features/example/messages/vi/example.json +11 -1
  24. package/templates/features/example/src/app/(dashboard)/example/page.tsx +92 -3
  25. package/templates/features/example/src/example/components/example-table.tsx +15 -2
  26. package/templates/next-base/.env +16 -0
  27. package/templates/next-base/.env.development +16 -0
  28. package/templates/next-base/.env.example +16 -0
  29. package/templates/next-base/SETUP.md +62 -10
  30. package/templates/next-base/bun.lock +407 -0
  31. package/templates/next-base/messages/vi/auth.json +43 -0
  32. package/templates/next-base/messages/vi/common.json +53 -0
  33. package/templates/next-base/messages/vi/example.json +20 -0
  34. package/templates/next-base/next-env.d.ts +1 -1
  35. package/templates/next-base/next.config.ts +4 -1
  36. package/templates/next-base/nextcli.json +12 -4
  37. package/templates/next-base/package.json +24 -5
  38. package/templates/next-base/prisma/schema.prisma +85 -0
  39. package/templates/next-base/prisma.config.ts +16 -0
  40. package/templates/next-base/src/app/(auth)/.gitkeep +1 -0
  41. package/templates/next-base/src/app/(auth)/change-password/layout.tsx +21 -0
  42. package/templates/next-base/src/app/(auth)/change-password/page.tsx +15 -0
  43. package/templates/next-base/src/app/(auth)/layout.tsx +6 -0
  44. package/templates/next-base/src/app/(auth)/sign-in/layout.tsx +17 -0
  45. package/templates/next-base/src/app/(auth)/sign-in/page.tsx +15 -0
  46. package/templates/next-base/src/app/(dashboard)/account/page.tsx +20 -0
  47. package/templates/next-base/src/app/(dashboard)/dashboard/page.tsx +31 -0
  48. package/templates/next-base/src/app/(dashboard)/example/page.tsx +102 -0
  49. package/templates/next-base/src/app/(dashboard)/layout.tsx +22 -0
  50. package/templates/next-base/src/app/api/auth/[...all]/route.ts +4 -0
  51. package/templates/next-base/src/app/api/v1/auth/change-password/route.ts +55 -0
  52. package/templates/next-base/src/app/api/v1/auth/login/route.ts +65 -0
  53. package/templates/next-base/src/app/api/v1/auth/logout/route.ts +23 -0
  54. package/templates/next-base/src/app/api/v1/auth/me/route.ts +24 -0
  55. package/templates/next-base/src/app/api/v1/example/route.ts +34 -0
  56. package/templates/next-base/src/app/api/v1/users/[id]/route.ts +104 -0
  57. package/templates/next-base/src/app/api/v1/users/route.ts +58 -0
  58. package/templates/next-base/src/app/layout.tsx +14 -6
  59. package/templates/next-base/src/app/page.tsx +2 -25
  60. package/templates/next-base/src/components/branding/logo.tsx +27 -4
  61. package/templates/next-base/src/components/layout/auth/auth-shell.tsx +24 -0
  62. package/templates/next-base/src/components/layout/private/app-sidebar.tsx +32 -0
  63. package/templates/next-base/src/components/layout/private/dashboard-layout.tsx +63 -0
  64. package/templates/next-base/src/components/layout/private/locale-switcher.tsx +45 -0
  65. package/templates/next-base/src/components/layout/private/nav-sidebar.tsx +55 -0
  66. package/templates/next-base/src/components/layout/private/nav-user.tsx +99 -0
  67. package/templates/next-base/src/components/layout/private/page-shell.tsx +40 -0
  68. package/templates/next-base/src/components/providers/query-provider.tsx +17 -0
  69. package/templates/next-base/src/components/ui/data-table/data-table-column-header.tsx +23 -0
  70. package/templates/next-base/src/components/ui/data-table/data-table-filter-list.tsx +3 -0
  71. package/templates/next-base/src/components/ui/data-table/data-table-pagination.tsx +35 -0
  72. package/templates/next-base/src/components/ui/data-table/data-table-skeleton.tsx +11 -0
  73. package/templates/next-base/src/components/ui/data-table/data-table-toolbar.tsx +14 -0
  74. package/templates/next-base/src/components/ui/data-table/data-table-view-options.tsx +3 -0
  75. package/templates/next-base/src/components/ui/data-table/data-table.tsx +72 -0
  76. package/templates/next-base/src/components/ui/sidebar.tsx +215 -0
  77. package/templates/next-base/src/data/sidebar-modules.ts +11 -0
  78. package/templates/next-base/src/example/api/use-example.ts +21 -0
  79. package/templates/next-base/src/example/api/use-mutations.ts +20 -0
  80. package/templates/next-base/src/example/components/example-table.tsx +64 -0
  81. package/templates/next-base/src/example/services.ts +9 -0
  82. package/templates/next-base/src/example/validations.ts +8 -0
  83. package/templates/next-base/src/features/auth/components/account-panel.tsx +92 -0
  84. package/templates/next-base/src/features/auth/components/change-password-form.tsx +79 -0
  85. package/templates/next-base/src/features/auth/components/sign-in-form.tsx +86 -0
  86. package/templates/next-base/src/features/auth/validations.ts +14 -0
  87. package/templates/next-base/src/features/users/services.ts +132 -0
  88. package/templates/next-base/src/features/users/validations.ts +21 -0
  89. package/templates/next-base/src/hooks/table/use-data-table.ts +33 -0
  90. package/templates/next-base/src/hooks/use-mobile.ts +25 -0
  91. package/templates/next-base/src/i18n/config.ts +7 -0
  92. package/templates/next-base/src/i18n/namespaces.ts +5 -0
  93. package/templates/next-base/src/i18n/request.ts +25 -0
  94. package/templates/next-base/src/instrumentation.ts +14 -0
  95. package/templates/next-base/src/lib/api/axios.ts +56 -0
  96. package/templates/next-base/src/lib/api/response.ts +45 -0
  97. package/templates/next-base/src/lib/auth/bootstrap.ts +95 -0
  98. package/templates/next-base/src/lib/auth/client.ts +7 -0
  99. package/templates/next-base/src/lib/auth/index.ts +1 -0
  100. package/templates/next-base/src/lib/auth/rbac.ts +59 -0
  101. package/templates/next-base/src/lib/auth/server.ts +21 -0
  102. package/templates/next-base/src/lib/constants.ts +10 -0
  103. package/templates/next-base/src/lib/db/prisma.ts +23 -0
  104. package/templates/next-base/src/lib/prisma.ts +23 -0
  105. package/templates/next-base/src/lib/supabase/client.ts +6 -0
  106. package/templates/next-base/src/lib/supabase/rich-text-image-sync.ts +28 -0
  107. package/templates/next-base/src/lib/supabase/storage-config.ts +69 -0
  108. package/templates/next-base/src/lib/supabase/storage.ts +164 -0
  109. package/templates/next-base/src/types/data-table.ts +4 -0
  110. package/templates/features/api/src/lib/api/token-store.ts +0 -13
  111. package/templates/features/auth/src/app/api/v1/auth/refresh/route.ts +0 -32
  112. package/templates/features/auth/src/lib/auth/cookies.ts +0 -15
@@ -1,17 +1,8 @@
1
1
  import axios from "axios";
2
- import {
3
- clearAccessToken,
4
- getAccessToken,
5
- setAccessToken,
6
- } from "@/lib/api/token-store";
7
- import type { ApiErrorResponse, ApiSuccess } from "@/types";
2
+ import type { ApiErrorResponse } from "@/types";
8
3
 
9
4
  const baseURL = process.env.NEXT_PUBLIC_APP_URL ?? "http://localhost:3000";
10
5
 
11
- type RetryableRequestConfig = {
12
- _retry?: boolean;
13
- };
14
-
15
6
  export const publicApi = axios.create({
16
7
  baseURL,
17
8
  withCredentials: true,
@@ -28,86 +19,6 @@ export const protectedApi = axios.create({
28
19
  },
29
20
  });
30
21
 
31
- protectedApi.interceptors.request.use((config) => {
32
- const token = getAccessToken();
33
- if (token) {
34
- config.headers = config.headers ?? {};
35
- config.headers.Authorization = `Bearer ${token}`;
36
- }
37
- return config;
38
- });
39
-
40
- let isRefreshing = false;
41
- let waitingQueue: Array<(token: string | null) => void> = [];
42
-
43
- function flushQueue(token: string | null): void {
44
- for (const resolve of waitingQueue) {
45
- resolve(token);
46
- }
47
- waitingQueue = [];
48
- }
49
-
50
- async function refreshAccessToken(): Promise<string | null> {
51
- try {
52
- const response = await publicApi.post("/api/v1/auth/refresh", null, {
53
- withCredentials: true,
54
- });
55
- const token = (response.data as ApiSuccess<{ accessToken: string }>).data
56
- ?.accessToken;
57
- if (!token) {
58
- clearAccessToken();
59
- return null;
60
- }
61
- setAccessToken(token);
62
- return token;
63
- } catch {
64
- clearAccessToken();
65
- return null;
66
- }
67
- }
68
-
69
- protectedApi.interceptors.response.use(
70
- (response) => response,
71
- async (error) => {
72
- const config = error.config as typeof error.config & RetryableRequestConfig;
73
- if (!config || config._retry || error.response?.status !== 401) {
74
- return Promise.reject(error);
75
- }
76
-
77
- if (isRefreshing) {
78
- return new Promise((resolve, reject) => {
79
- waitingQueue.push((token) => {
80
- if (!token) {
81
- reject(error);
82
- return;
83
- }
84
-
85
- config.headers = config.headers ?? {};
86
- config.headers.Authorization = `Bearer ${token}`;
87
- resolve(protectedApi(config));
88
- });
89
- });
90
- }
91
-
92
- config._retry = true;
93
- isRefreshing = true;
94
-
95
- try {
96
- const token = await refreshAccessToken();
97
- flushQueue(token);
98
- if (!token) {
99
- return Promise.reject(error);
100
- }
101
-
102
- config.headers = config.headers ?? {};
103
- config.headers.Authorization = `Bearer ${token}`;
104
- return protectedApi(config);
105
- } finally {
106
- isRefreshing = false;
107
- }
108
- },
109
- );
110
-
111
22
  function extractApiErrorMessage(error: unknown): string {
112
23
  if (!axios.isAxiosError(error)) {
113
24
  return "Unexpected error occurred.";
@@ -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",
@@ -5,10 +5,11 @@ export default function ChangePasswordPage() {
5
5
  const t = useTranslations("auth.changePasswordPage");
6
6
 
7
7
  return (
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
+ <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
- </main>
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
- <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
+ <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
- </main>
13
+ </div>
13
14
  );
14
15
  }
@@ -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(`${authBaseUrl}/api/auth/sign-in/username`, {
23
- method: "POST",
24
- headers: {
25
- "Content-Type": "application/json",
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
- body: JSON.stringify({
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 cookieHeader = signInResponse.headers.get("set-cookie");
40
-
41
- const tokenResponse = await fetch(`${authBaseUrl}/api/auth/token`, {
42
- method: "GET",
43
- headers: cookieHeader ? { cookie: cookieHeader } : undefined,
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 (cookieHeader) {
65
- response.headers.set("set-cookie", cookieHeader);
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
  }
@@ -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
+ }
@@ -49,17 +49,29 @@ export function AccountPanel() {
49
49
  }, []);
50
50
 
51
51
  if (loading) {
52
- return <p>{t("loading")}</p>;
52
+ return (
53
+ <Card className="max-w-xl">
54
+ <CardContent className="py-8 text-sm text-muted-foreground">
55
+ {t("loading")}
56
+ </CardContent>
57
+ </Card>
58
+ );
53
59
  }
54
60
 
55
61
  if (!session?.user) {
56
- return <p>{t("noSession")}</p>;
62
+ return (
63
+ <Card className="max-w-xl">
64
+ <CardContent className="py-8 text-sm text-muted-foreground">
65
+ {t("noSession")}
66
+ </CardContent>
67
+ </Card>
68
+ );
57
69
  }
58
70
 
59
71
  return (
60
72
  <Card className="max-w-xl">
61
73
  <CardHeader>
62
- <CardTitle>{t("title")}</CardTitle>
74
+ <CardTitle>{t("panelTitle")}</CardTitle>
63
75
  </CardHeader>
64
76
  <CardContent className="space-y-2 text-sm">
65
77
  <p>
@@ -8,7 +8,6 @@ import { toast } from "sonner";
8
8
  import { protectedApi } from "@/lib/api/axios";
9
9
  import { changePasswordSchema } from "@/features/auth/validations";
10
10
  import { Button } from "@/components/ui/button";
11
- import { Card, CardContent } from "@/components/ui/card";
12
11
  import { Input } from "@/components/ui/input";
13
12
  import { Label } from "@/components/ui/label";
14
13
 
@@ -49,34 +48,32 @@ export function ChangePasswordForm() {
49
48
  };
50
49
 
51
50
  return (
52
- <Card>
53
- <CardContent className="pt-6">
54
- <form onSubmit={onSubmit} className="grid gap-4">
55
- <div className="grid gap-2">
56
- <Label htmlFor="currentPassword">{t("currentPassword")}</Label>
57
- <Input
58
- id="currentPassword"
59
- type="password"
60
- value={currentPassword}
61
- onChange={(event) => setCurrentPassword(event.target.value)}
62
- required
63
- />
64
- </div>
65
- <div className="grid gap-2">
66
- <Label htmlFor="newPassword">{t("newPassword")}</Label>
67
- <Input
68
- id="newPassword"
69
- type="password"
70
- value={newPassword}
71
- onChange={(event) => setNewPassword(event.target.value)}
72
- required
73
- />
74
- </div>
75
- <Button type="submit" disabled={isSubmitting}>
76
- {isSubmitting ? t("submitting") : t("submit")}
77
- </Button>
78
- </form>
79
- </CardContent>
80
- </Card>
51
+ <form onSubmit={onSubmit} className="space-y-6">
52
+ <div className="space-y-2">
53
+ <Label htmlFor="currentPassword">{t("currentPassword")}</Label>
54
+ <Input
55
+ id="currentPassword"
56
+ type="password"
57
+ value={currentPassword}
58
+ onChange={(event) => setCurrentPassword(event.target.value)}
59
+ className="h-12"
60
+ required
61
+ />
62
+ </div>
63
+ <div className="space-y-2">
64
+ <Label htmlFor="newPassword">{t("newPassword")}</Label>
65
+ <Input
66
+ id="newPassword"
67
+ type="password"
68
+ value={newPassword}
69
+ onChange={(event) => setNewPassword(event.target.value)}
70
+ className="h-12"
71
+ required
72
+ />
73
+ </div>
74
+ <Button type="submit" className="h-12 w-full" disabled={isSubmitting}>
75
+ {isSubmitting ? t("submitting") : t("submit")}
76
+ </Button>
77
+ </form>
81
78
  );
82
79
  }
@@ -6,16 +6,13 @@ import { useRouter } from "next/navigation";
6
6
  import { useTranslations } from "next-intl";
7
7
  import { toast } from "sonner";
8
8
  import { publicApi } from "@/lib/api/axios";
9
- import { setAccessToken } from "@/lib/api/token-store";
10
9
  import { signInSchema } from "@/features/auth/validations";
11
10
  import type { ApiSuccess } from "@/types";
12
11
  import { Button } from "@/components/ui/button";
13
- import { Card, CardContent } from "@/components/ui/card";
14
12
  import { Input } from "@/components/ui/input";
15
13
  import { Label } from "@/components/ui/label";
16
14
 
17
15
  type LoginResponse = {
18
- accessToken: string;
19
16
  requirePasswordChange: boolean;
20
17
  };
21
18
 
@@ -41,14 +38,10 @@ export function SignInForm() {
41
38
  withCredentials: true,
42
39
  });
43
40
  const payload = (response.data as ApiSuccess<LoginResponse>).data;
44
- if (!payload?.accessToken) {
45
- toast.error(t("missingAccessToken"));
46
- return;
47
- }
48
-
49
- setAccessToken(payload.accessToken);
50
41
  toast.success(t("success"));
51
- router.push(payload.requirePasswordChange ? "/change-password" : "/dashboard");
42
+ router.push(
43
+ payload.requirePasswordChange ? "/change-password" : "/dashboard",
44
+ );
52
45
  router.refresh();
53
46
  } catch (error) {
54
47
  const message = error instanceof Error ? error.message : t("failed");
@@ -59,37 +52,35 @@ export function SignInForm() {
59
52
  };
60
53
 
61
54
  return (
62
- <Card>
63
- <CardContent className="pt-6">
64
- <form onSubmit={onSubmit} className="grid gap-4">
65
- <div className="grid gap-2">
66
- <Label htmlFor="username">{t("username")}</Label>
67
- <Input
68
- id="username"
69
- value={username}
70
- onChange={(event) => setUsername(event.target.value)}
71
- placeholder={t("usernamePlaceholder")}
72
- autoComplete="username"
73
- required
74
- />
75
- </div>
76
- <div className="grid gap-2">
77
- <Label htmlFor="password">{t("password")}</Label>
78
- <Input
79
- id="password"
80
- type="password"
81
- value={password}
82
- onChange={(event) => setPassword(event.target.value)}
83
- placeholder={t("passwordPlaceholder")}
84
- autoComplete="current-password"
85
- required
86
- />
87
- </div>
88
- <Button type="submit" disabled={isSubmitting}>
89
- {isSubmitting ? t("submitting") : t("submit")}
90
- </Button>
91
- </form>
92
- </CardContent>
93
- </Card>
55
+ <form onSubmit={onSubmit} className="space-y-6">
56
+ <div className="space-y-2">
57
+ <Label htmlFor="username">{t("username")}</Label>
58
+ <Input
59
+ id="username"
60
+ value={username}
61
+ onChange={(event) => setUsername(event.target.value)}
62
+ placeholder={t("usernamePlaceholder")}
63
+ autoComplete="username"
64
+ className="h-12"
65
+ required
66
+ />
67
+ </div>
68
+ <div className="space-y-2">
69
+ <Label htmlFor="password">{t("password")}</Label>
70
+ <Input
71
+ id="password"
72
+ type="password"
73
+ value={password}
74
+ onChange={(event) => setPassword(event.target.value)}
75
+ placeholder={t("passwordPlaceholder")}
76
+ autoComplete="current-password"
77
+ className="h-12"
78
+ required
79
+ />
80
+ </div>
81
+ <Button type="submit" className="h-12 w-full" disabled={isSubmitting}>
82
+ {isSubmitting ? t("submitting") : t("submit")}
83
+ </Button>
84
+ </form>
94
85
  );
95
86
  }
@@ -1,7 +1,7 @@
1
1
  import { createAuthClient } from "better-auth/react";
2
- import { jwtClient, usernameClient } from "better-auth/client/plugins";
2
+ import { usernameClient } from "better-auth/client/plugins";
3
3
 
4
4
  export const authClient = createAuthClient({
5
5
  baseURL: process.env.NEXT_PUBLIC_APP_URL ?? "http://localhost:3000",
6
- plugins: [jwtClient(), usernameClient()],
6
+ plugins: [usernameClient()],
7
7
  });
@@ -1,6 +1,6 @@
1
1
  import { betterAuth } from "better-auth/minimal";
2
2
  import { prismaAdapter } from "better-auth/adapters/prisma";
3
- import { jwt, username } from "better-auth/plugins";
3
+ import { username } from "better-auth/plugins";
4
4
  import prisma from "@/lib/db/prisma";
5
5
 
6
6
  const socialProviders = {
@@ -12,7 +12,7 @@ export const auth = betterAuth({
12
12
  database: prismaAdapter(prisma, {
13
13
  provider: "postgresql",
14
14
  }),
15
- plugins: [jwt(), username()],
15
+ plugins: [username()],
16
16
  emailAndPassword: {
17
17
  enabled: true,
18
18
  minPasswordLength: 8,
@@ -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
- <main className="space-y-4">
11
- <h1 className="text-2xl font-semibold">{t("title")}</h1>
12
- <AccountPanel />
13
- <Button asChild variant="outline">
14
- <Link href="/sign-in">{t("goToSignIn")}</Link>
15
- </Button>
16
- </main>
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 { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
2
+ import { PageShell } from "@/components/layout/private/page-shell";
3
3
 
4
4
  export default function DashboardPage() {
5
- const t = useTranslations("common.sidebar");
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
- <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>
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,14 +1,7 @@
1
1
  "use client";
2
2
 
3
3
  import { ChevronLeft, ChevronRight } from "lucide-react";
4
- import Link from "next/link";
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
  );