@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.
Files changed (56) hide show
  1. package/dist/cli.js +3 -3
  2. package/package.json +1 -1
  3. package/templates/features/api/src/lib/api/axios.ts +1 -90
  4. package/templates/features/auth/messages/vi/auth.json +2 -1
  5. package/templates/features/auth/src/app/(auth)/change-password/page.tsx +5 -4
  6. package/templates/features/auth/src/app/(auth)/layout.tsx +2 -5
  7. package/templates/features/auth/src/app/(auth)/sign-in/page.tsx +5 -4
  8. package/templates/features/auth/src/app/api/v1/auth/login/route.ts +24 -29
  9. package/templates/features/auth/src/app/api/v1/auth/logout/route.ts +0 -5
  10. package/templates/features/auth/src/components/layout/auth/auth-shell.tsx +24 -0
  11. package/templates/features/auth/src/features/auth/components/account-panel.tsx +15 -3
  12. package/templates/features/auth/src/features/auth/components/change-password-form.tsx +27 -30
  13. package/templates/features/auth/src/features/auth/components/sign-in-form.tsx +33 -42
  14. package/templates/features/auth/src/lib/auth/client.ts +2 -2
  15. package/templates/features/auth/src/lib/auth/server.ts +2 -2
  16. package/templates/features/dashboard/src/app/(dashboard)/account/page.tsx +9 -7
  17. package/templates/features/dashboard/src/app/(dashboard)/dashboard/page.tsx +24 -10
  18. package/templates/features/dashboard/src/components/layout/private/app-sidebar.tsx +1 -13
  19. package/templates/features/dashboard/src/components/layout/private/dashboard-layout.tsx +31 -22
  20. package/templates/features/dashboard/src/components/layout/private/page-shell.tsx +40 -0
  21. package/templates/features/database/prisma/schema.prisma +1 -0
  22. package/templates/features/example/messages/vi/example.json +11 -1
  23. package/templates/features/example/src/app/(dashboard)/example/page.tsx +92 -3
  24. package/templates/features/example/src/example/components/example-table.tsx +15 -2
  25. package/templates/next-base/bun.lock +407 -0
  26. package/templates/next-base/messages/vi/auth.json +2 -1
  27. package/templates/next-base/messages/vi/common.json +19 -0
  28. package/templates/next-base/messages/vi/example.json +11 -1
  29. package/templates/next-base/next-env.d.ts +1 -1
  30. package/templates/next-base/prisma/schema.prisma +1 -0
  31. package/templates/next-base/src/app/(auth)/change-password/page.tsx +5 -4
  32. package/templates/next-base/src/app/(auth)/layout.tsx +2 -5
  33. package/templates/next-base/src/app/(auth)/sign-in/page.tsx +5 -4
  34. package/templates/next-base/src/app/(dashboard)/account/page.tsx +9 -7
  35. package/templates/next-base/src/app/(dashboard)/dashboard/page.tsx +24 -10
  36. package/templates/next-base/src/app/(dashboard)/example/page.tsx +92 -3
  37. package/templates/next-base/src/app/api/v1/auth/login/route.ts +24 -29
  38. package/templates/next-base/src/app/api/v1/auth/logout/route.ts +0 -5
  39. package/templates/next-base/src/components/branding/logo.tsx +27 -4
  40. package/templates/next-base/src/components/layout/auth/auth-shell.tsx +24 -0
  41. package/templates/next-base/src/components/layout/private/app-sidebar.tsx +1 -13
  42. package/templates/next-base/src/components/layout/private/dashboard-layout.tsx +31 -22
  43. package/templates/next-base/src/components/layout/private/page-shell.tsx +40 -0
  44. package/templates/next-base/src/example/components/example-table.tsx +15 -2
  45. package/templates/next-base/src/features/auth/components/account-panel.tsx +15 -3
  46. package/templates/next-base/src/features/auth/components/change-password-form.tsx +27 -30
  47. package/templates/next-base/src/features/auth/components/sign-in-form.tsx +33 -42
  48. package/templates/next-base/src/lib/api/axios.ts +1 -90
  49. package/templates/next-base/src/lib/auth/client.ts +2 -2
  50. package/templates/next-base/src/lib/auth/server.ts +2 -2
  51. package/templates/features/api/src/lib/api/token-store.ts +0 -13
  52. package/templates/features/auth/src/app/api/v1/auth/refresh/route.ts +0 -32
  53. package/templates/features/auth/src/lib/auth/cookies.ts +0 -15
  54. package/templates/next-base/src/app/api/v1/auth/refresh/route.ts +0 -32
  55. package/templates/next-base/src/lib/api/token-store.ts +0 -13
  56. package/templates/next-base/src/lib/auth/cookies.ts +0 -15
package/dist/cli.js CHANGED
@@ -864,7 +864,7 @@ import { readdir as readdir3, readFile as readFile6, writeFile as writeFile6 } f
864
864
  import path7 from "path";
865
865
  import { readdir as readdir2, readFile as readFile5, writeFile as writeFile5 } from "fs/promises";
866
866
  var defaultManifest = {
867
- cli: "0.9.0",
867
+ cli: "1.0.0",
868
868
  defaultLocale: "vi",
869
869
  locales: ["vi"],
870
870
  namespaces: ["common", "auth", "example"],
@@ -2176,7 +2176,7 @@ function registerAddCommand(program2) {
2176
2176
  import { randomBytes as randomBytes2 } from "crypto";
2177
2177
  import { spawn as spawn2 } from "child_process";
2178
2178
  import path11 from "path";
2179
- var CLI_VERSION = "0.9.0";
2179
+ var CLI_VERSION = "1.0.0";
2180
2180
  async function runInstall(packageManager, cwd) {
2181
2181
  const installArgsMap = {
2182
2182
  npm: ["install"],
@@ -2555,7 +2555,7 @@ var NexTCLICommand = class _NexTCLICommand extends Command {
2555
2555
 
2556
2556
  // src/cli.ts
2557
2557
  var program = new NexTCLICommand();
2558
- program.name("nextcli").description("Scaffold outsource-ready Next.js projects").version("0.9.0");
2558
+ program.name("nextcli").description("Scaffold outsource-ready Next.js projects").version("1.0.0");
2559
2559
  registerCreateCommand(program);
2560
2560
  registerAddCommand(program);
2561
2561
  registerMigrateCommand(program);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@thinhnguyencth1204/nextcli",
3
- "version": "0.9.0",
3
+ "version": "1.0.0",
4
4
  "description": "CLI scaffolder for outsourced Next.js projects",
5
5
  "type": "module",
6
6
  "bin": {
@@ -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
  }