@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.
Files changed (68) hide show
  1. package/dist/cli.js +632 -90
  2. package/package.json +1 -1
  3. package/templates/next-base/components.json +21 -0
  4. package/templates/next-base/messages/vi/auth.json +28 -0
  5. package/templates/next-base/messages/vi/common.json +34 -0
  6. package/templates/next-base/messages/vi/example.json +10 -0
  7. package/templates/next-base/next.config.ts +11 -1
  8. package/templates/next-base/nextcli.json +8 -0
  9. package/templates/next-base/package.json +21 -1
  10. package/templates/next-base/postcss.config.mjs +5 -0
  11. package/templates/next-base/src/app/(auth)/layout.tsx +9 -0
  12. package/templates/next-base/src/app/(auth)/sign-in/page.tsx +6 -3
  13. package/templates/next-base/src/app/(dashboard)/account/page.tsx +9 -5
  14. package/templates/next-base/src/app/(dashboard)/dashboard/page.tsx +17 -0
  15. package/templates/next-base/src/app/(dashboard)/example/page.tsx +5 -2
  16. package/templates/next-base/src/app/(dashboard)/layout.tsx +10 -0
  17. package/templates/next-base/src/app/globals.css +107 -0
  18. package/templates/next-base/src/app/layout.tsx +18 -8
  19. package/templates/next-base/src/app/page.tsx +2 -18
  20. package/templates/next-base/src/components/layout/private/app-sidebar.tsx +45 -0
  21. package/templates/next-base/src/components/layout/private/dashboard-layout.tsx +53 -0
  22. package/templates/next-base/src/components/layout/private/locale-switcher.tsx +45 -0
  23. package/templates/next-base/src/components/layout/private/nav-sidebar.tsx +55 -0
  24. package/templates/next-base/src/components/layout/private/nav-user.tsx +99 -0
  25. package/templates/next-base/src/components/providers/theme-provider.tsx +11 -0
  26. package/templates/next-base/src/components/ui/alert-dialog.tsx +11 -0
  27. package/templates/next-base/src/components/ui/avatar.tsx +45 -0
  28. package/templates/next-base/src/components/ui/badge.tsx +29 -0
  29. package/templates/next-base/src/components/ui/button.tsx +47 -7
  30. package/templates/next-base/src/components/ui/card.tsx +54 -0
  31. package/templates/next-base/src/components/ui/data-table/data-table-column-header.tsx +23 -0
  32. package/templates/next-base/src/components/ui/data-table/data-table-filter-list.tsx +3 -0
  33. package/templates/next-base/src/components/ui/data-table/data-table-pagination.tsx +35 -0
  34. package/templates/next-base/src/components/ui/data-table/data-table-skeleton.tsx +11 -0
  35. package/templates/next-base/src/components/ui/data-table/data-table-toolbar.tsx +14 -0
  36. package/templates/next-base/src/components/ui/data-table/data-table-view-options.tsx +3 -0
  37. package/templates/next-base/src/components/ui/data-table/data-table.tsx +72 -0
  38. package/templates/next-base/src/components/ui/dialog.tsx +105 -0
  39. package/templates/next-base/src/components/ui/dropdown-menu.tsx +44 -0
  40. package/templates/next-base/src/components/ui/input.tsx +19 -0
  41. package/templates/next-base/src/components/ui/label.tsx +15 -0
  42. package/templates/next-base/src/components/ui/popover.tsx +30 -0
  43. package/templates/next-base/src/components/ui/scroll-area.tsx +47 -0
  44. package/templates/next-base/src/components/ui/select.tsx +76 -0
  45. package/templates/next-base/src/components/ui/separator.tsx +23 -0
  46. package/templates/next-base/src/components/ui/sheet.tsx +117 -0
  47. package/templates/next-base/src/components/ui/sidebar.tsx +215 -0
  48. package/templates/next-base/src/components/ui/skeleton.tsx +10 -0
  49. package/templates/next-base/src/components/ui/sonner.tsx +3 -0
  50. package/templates/next-base/src/components/ui/table.tsx +54 -0
  51. package/templates/next-base/src/components/ui/tabs.tsx +52 -0
  52. package/templates/next-base/src/components/ui/textarea.tsx +17 -0
  53. package/templates/next-base/src/components/ui/tooltip.tsx +26 -0
  54. package/templates/next-base/src/data/sidebar-modules.ts +11 -0
  55. package/templates/next-base/src/example/components/example-table.tsx +25 -40
  56. package/templates/next-base/src/features/auth/components/account-panel.tsx +21 -8
  57. package/templates/next-base/src/features/auth/components/sign-in-form.tsx +43 -30
  58. package/templates/next-base/src/hooks/index.ts +1 -1
  59. package/templates/next-base/src/hooks/table/use-data-table.ts +33 -0
  60. package/templates/next-base/src/hooks/use-mobile.ts +25 -0
  61. package/templates/next-base/src/i18n/config.ts +7 -0
  62. package/templates/next-base/src/i18n/namespaces.ts +5 -0
  63. package/templates/next-base/src/i18n/request.ts +19 -2
  64. package/templates/next-base/src/lib/prisma.ts +11 -1
  65. package/templates/next-base/src/types/data-table.ts +4 -0
  66. package/templates/next-base/src/types/index.ts +2 -0
  67. package/templates/next-base/middleware.ts +0 -10
  68. package/templates/next-base/src/app/styles.css +0 -12
@@ -0,0 +1,52 @@
1
+ "use client";
2
+
3
+ import * as React from "react";
4
+ import * as TabsPrimitive from "@radix-ui/react-tabs";
5
+ import { cn } from "@/utils/cn";
6
+
7
+ export const Tabs = TabsPrimitive.Root;
8
+
9
+ export function TabsList({
10
+ className,
11
+ ...props
12
+ }: React.ComponentProps<typeof TabsPrimitive.List>) {
13
+ return (
14
+ <TabsPrimitive.List
15
+ className={cn(
16
+ "inline-flex h-9 items-center justify-center rounded-md bg-muted p-1 text-muted-foreground",
17
+ className,
18
+ )}
19
+ {...props}
20
+ />
21
+ );
22
+ }
23
+
24
+ export function TabsTrigger({
25
+ className,
26
+ ...props
27
+ }: React.ComponentProps<typeof TabsPrimitive.Trigger>) {
28
+ return (
29
+ <TabsPrimitive.Trigger
30
+ className={cn(
31
+ "inline-flex items-center justify-center whitespace-nowrap rounded-sm px-3 py-1 text-sm font-medium ring-offset-background transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring data-[state=active]:bg-background data-[state=active]:text-foreground data-[state=active]:shadow-sm",
32
+ className,
33
+ )}
34
+ {...props}
35
+ />
36
+ );
37
+ }
38
+
39
+ export function TabsContent({
40
+ className,
41
+ ...props
42
+ }: React.ComponentProps<typeof TabsPrimitive.Content>) {
43
+ return (
44
+ <TabsPrimitive.Content
45
+ className={cn(
46
+ "mt-2 ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
47
+ className,
48
+ )}
49
+ {...props}
50
+ />
51
+ );
52
+ }
@@ -0,0 +1,17 @@
1
+ import * as React from "react";
2
+ import { cn } from "@/utils/cn";
3
+
4
+ export function Textarea({
5
+ className,
6
+ ...props
7
+ }: React.ComponentProps<"textarea">) {
8
+ return (
9
+ <textarea
10
+ className={cn(
11
+ "flex min-h-[80px] w-full rounded-md border border-input bg-background px-3 py-2 text-sm placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50",
12
+ className,
13
+ )}
14
+ {...props}
15
+ />
16
+ );
17
+ }
@@ -0,0 +1,26 @@
1
+ "use client";
2
+
3
+ import * as React from "react";
4
+ import * as TooltipPrimitive from "@radix-ui/react-tooltip";
5
+ import { cn } from "@/utils/cn";
6
+
7
+ export const TooltipProvider = TooltipPrimitive.Provider;
8
+ export const Tooltip = TooltipPrimitive.Root;
9
+ export const TooltipTrigger = TooltipPrimitive.Trigger;
10
+
11
+ export function TooltipContent({
12
+ className,
13
+ sideOffset = 4,
14
+ ...props
15
+ }: React.ComponentProps<typeof TooltipPrimitive.Content>) {
16
+ return (
17
+ <TooltipPrimitive.Content
18
+ sideOffset={sideOffset}
19
+ className={cn(
20
+ "z-50 overflow-hidden rounded-md bg-primary px-3 py-1.5 text-xs text-primary-foreground",
21
+ className,
22
+ )}
23
+ {...props}
24
+ />
25
+ );
26
+ }
@@ -0,0 +1,11 @@
1
+ export type SidebarModule = {
2
+ id: "dashboard" | "example" | "account";
3
+ url: string;
4
+ icon: "layout-dashboard" | "table" | "user";
5
+ };
6
+
7
+ export const sidebarModules: SidebarModule[] = [
8
+ { id: "dashboard", url: "/dashboard", icon: "layout-dashboard" },
9
+ { id: "example", url: "/example", icon: "table" },
10
+ { id: "account", url: "/account", icon: "user" },
11
+ ];
@@ -1,8 +1,15 @@
1
1
  "use client";
2
2
 
3
3
  import { useMemo } from "react";
4
- import { createColumnHelper, getCoreRowModel, useReactTable } from "@tanstack/react-table";
4
+ import { useTranslations } from "next-intl";
5
+ import {
6
+ createColumnHelper,
7
+ getCoreRowModel,
8
+ getPaginationRowModel,
9
+ useReactTable,
10
+ } from "@tanstack/react-table";
5
11
  import { useExample } from "@/example/api/use-example";
12
+ import { DataTable } from "@/components/ui/data-table/data-table";
6
13
 
7
14
  type ExampleItem = {
8
15
  id: string;
@@ -12,55 +19,33 @@ type ExampleItem = {
12
19
 
13
20
  const columnHelper = createColumnHelper<ExampleItem>();
14
21
 
15
- const columns = [
16
- columnHelper.accessor("name", {
17
- header: "Name",
18
- }),
19
- columnHelper.accessor("description", {
20
- header: "Description",
21
- }),
22
- ];
23
-
24
22
  export function ExampleTable() {
23
+ const t = useTranslations("example.table");
25
24
  const { data, isLoading } = useExample();
26
25
  const rows = useMemo(() => (Array.isArray(data) ? data : []), [data]);
27
26
 
27
+ const translatedColumns = useMemo(
28
+ () => [
29
+ columnHelper.accessor("name", {
30
+ header: t("name"),
31
+ }),
32
+ columnHelper.accessor("description", {
33
+ header: t("description"),
34
+ }),
35
+ ],
36
+ [t],
37
+ );
38
+
28
39
  const table = useReactTable({
29
40
  data: rows,
30
- columns,
41
+ columns: translatedColumns,
31
42
  getCoreRowModel: getCoreRowModel(),
43
+ getPaginationRowModel: getPaginationRowModel(),
32
44
  });
33
45
 
34
46
  if (isLoading) {
35
- return <p>Loading examples...</p>;
47
+ return <p>{t("loading")}</p>;
36
48
  }
37
49
 
38
- return (
39
- <table style={{ width: "100%", borderCollapse: "collapse" }}>
40
- <thead>
41
- {table.getHeaderGroups().map((headerGroup) => (
42
- <tr key={headerGroup.id}>
43
- {headerGroup.headers.map((header) => (
44
- <th key={header.id} style={{ textAlign: "left", borderBottom: "1px solid #ccc" }}>
45
- {typeof header.column.columnDef.header === "string"
46
- ? header.column.columnDef.header
47
- : null}
48
- </th>
49
- ))}
50
- </tr>
51
- ))}
52
- </thead>
53
- <tbody>
54
- {table.getRowModel().rows.map((row) => (
55
- <tr key={row.id}>
56
- {row.getVisibleCells().map((cell) => (
57
- <td key={cell.id} style={{ padding: "8px 0", borderBottom: "1px solid #eee" }}>
58
- {String(cell.getValue() ?? "")}
59
- </td>
60
- ))}
61
- </tr>
62
- ))}
63
- </tbody>
64
- </table>
65
- );
50
+ return <DataTable table={table} />;
66
51
  }
@@ -1,8 +1,10 @@
1
1
  "use client";
2
2
 
3
3
  import { useEffect, useState } from "react";
4
+ import { useTranslations } from "next-intl";
4
5
  import { publicApi } from "@/lib/axios-instance";
5
6
  import type { ApiSuccess } from "@/types";
7
+ import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
6
8
 
7
9
  type SessionPayload = {
8
10
  user?: {
@@ -13,6 +15,7 @@ type SessionPayload = {
13
15
  };
14
16
 
15
17
  export function AccountPanel() {
18
+ const t = useTranslations("auth.account");
16
19
  const [session, setSession] = useState<SessionPayload | null>(null);
17
20
  const [loading, setLoading] = useState(true);
18
21
 
@@ -44,19 +47,29 @@ export function AccountPanel() {
44
47
  }, []);
45
48
 
46
49
  if (loading) {
47
- return <p>Loading account...</p>;
50
+ return <p>{t("loading")}</p>;
48
51
  }
49
52
 
50
53
  if (!session?.user) {
51
- return <p>No active session. Please sign in first.</p>;
54
+ return <p>{t("noSession")}</p>;
52
55
  }
53
56
 
54
57
  return (
55
- <section style={{ border: "1px solid #ddd", padding: 16, borderRadius: 8, maxWidth: 420 }}>
56
- <h2>My account</h2>
57
- <p>User ID: {session.user.id}</p>
58
- <p>Email: {session.user.email}</p>
59
- <p>Name: {session.user.name ?? "N/A"}</p>
60
- </section>
58
+ <Card className="max-w-xl">
59
+ <CardHeader>
60
+ <CardTitle>{t("title")}</CardTitle>
61
+ </CardHeader>
62
+ <CardContent className="space-y-2 text-sm">
63
+ <p>
64
+ {t("userId")}: {session.user.id}
65
+ </p>
66
+ <p>
67
+ {t("email")}: {session.user.email}
68
+ </p>
69
+ <p>
70
+ {t("name")}: {session.user.name ?? t("na")}
71
+ </p>
72
+ </CardContent>
73
+ </Card>
61
74
  );
62
75
  }
@@ -3,14 +3,20 @@
3
3
  import { useState } from "react";
4
4
  import type { FormEvent } from "react";
5
5
  import { useRouter } from "next/navigation";
6
+ import { useTranslations } from "next-intl";
6
7
  import { toast } from "sonner";
7
8
  import { publicApi } from "@/lib/axios-instance";
8
9
  import { setAccessToken } from "@/lib/token-store";
9
10
  import { signInSchema } from "@/features/auth/validations";
10
11
  import type { ApiSuccess } from "@/types";
12
+ import { Button } from "@/components/ui/button";
13
+ import { Card, CardContent } from "@/components/ui/card";
14
+ import { Input } from "@/components/ui/input";
15
+ import { Label } from "@/components/ui/label";
11
16
 
12
17
  export function SignInForm() {
13
18
  const router = useRouter();
19
+ const t = useTranslations("auth.signInForm");
14
20
  const [email, setEmail] = useState("");
15
21
  const [password, setPassword] = useState("");
16
22
  const [isSubmitting, setIsSubmitting] = useState(false);
@@ -20,7 +26,7 @@ export function SignInForm() {
20
26
 
21
27
  const parsed = signInSchema.safeParse({ email, password });
22
28
  if (!parsed.success) {
23
- toast.error("Please provide a valid email and password.");
29
+ toast.error(t("invalidInput"));
24
30
  return;
25
31
  }
26
32
 
@@ -29,18 +35,19 @@ export function SignInForm() {
29
35
  const response = await publicApi.post("/api/v1/auth/login", parsed.data, {
30
36
  withCredentials: true,
31
37
  });
32
- const accessToken = (response.data as ApiSuccess<{ accessToken: string }>).data?.accessToken;
38
+ const accessToken = (response.data as ApiSuccess<{ accessToken: string }>)
39
+ .data?.accessToken;
33
40
  if (!accessToken) {
34
- toast.error("Access token is missing.");
41
+ toast.error(t("missingAccessToken"));
35
42
  return;
36
43
  }
37
44
 
38
45
  setAccessToken(accessToken);
39
- toast.success("Signed in successfully.");
46
+ toast.success(t("success"));
40
47
  router.push("/account");
41
48
  router.refresh();
42
49
  } catch (error) {
43
- const message = error instanceof Error ? error.message : "Sign in failed.";
50
+ const message = error instanceof Error ? error.message : t("failed");
44
51
  toast.error(message);
45
52
  } finally {
46
53
  setIsSubmitting(false);
@@ -48,30 +55,36 @@ export function SignInForm() {
48
55
  };
49
56
 
50
57
  return (
51
- <form onSubmit={onSubmit} style={{ display: "grid", gap: 12, maxWidth: 360 }}>
52
- <label style={{ display: "grid", gap: 4 }}>
53
- <span>Email</span>
54
- <input
55
- type="email"
56
- value={email}
57
- onChange={(event) => setEmail(event.target.value)}
58
- placeholder="you@example.com"
59
- required
60
- />
61
- </label>
62
- <label style={{ display: "grid", gap: 4 }}>
63
- <span>Password</span>
64
- <input
65
- type="password"
66
- value={password}
67
- onChange={(event) => setPassword(event.target.value)}
68
- placeholder="********"
69
- required
70
- />
71
- </label>
72
- <button type="submit" disabled={isSubmitting}>
73
- {isSubmitting ? "Signing in..." : "Sign in"}
74
- </button>
75
- </form>
58
+ <Card>
59
+ <CardContent className="pt-6">
60
+ <form onSubmit={onSubmit} className="grid gap-4">
61
+ <div className="grid gap-2">
62
+ <Label htmlFor="email">{t("email")}</Label>
63
+ <Input
64
+ id="email"
65
+ type="email"
66
+ value={email}
67
+ onChange={(event) => setEmail(event.target.value)}
68
+ placeholder={t("emailPlaceholder")}
69
+ required
70
+ />
71
+ </div>
72
+ <div className="grid gap-2">
73
+ <Label htmlFor="password">{t("password")}</Label>
74
+ <Input
75
+ id="password"
76
+ type="password"
77
+ value={password}
78
+ onChange={(event) => setPassword(event.target.value)}
79
+ placeholder={t("passwordPlaceholder")}
80
+ required
81
+ />
82
+ </div>
83
+ <Button type="submit" disabled={isSubmitting}>
84
+ {isSubmitting ? t("submitting") : t("submit")}
85
+ </Button>
86
+ </form>
87
+ </CardContent>
88
+ </Card>
76
89
  );
77
90
  }
@@ -1 +1 @@
1
- export {};
1
+ export * from "@/hooks/use-mobile";
@@ -0,0 +1,33 @@
1
+ "use client";
2
+
3
+ import * as React from "react";
4
+ import {
5
+ getCoreRowModel,
6
+ getPaginationRowModel,
7
+ type ColumnDef,
8
+ useReactTable,
9
+ } from "@tanstack/react-table";
10
+ import type { DataTablePaginationState } from "@/types/data-table";
11
+
12
+ export function useDataTable<TData>({
13
+ data,
14
+ columns,
15
+ initialState,
16
+ }: {
17
+ data: TData[];
18
+ columns: ColumnDef<TData, unknown>[];
19
+ initialState?: DataTablePaginationState;
20
+ }) {
21
+ const [pagination, setPagination] = React.useState<DataTablePaginationState>(
22
+ initialState ?? { pageIndex: 0, pageSize: 10 },
23
+ );
24
+
25
+ return useReactTable({
26
+ data,
27
+ columns,
28
+ state: { pagination },
29
+ onPaginationChange: setPagination,
30
+ getCoreRowModel: getCoreRowModel(),
31
+ getPaginationRowModel: getPaginationRowModel(),
32
+ });
33
+ }
@@ -0,0 +1,25 @@
1
+ "use client";
2
+
3
+ import * as React from "react";
4
+
5
+ const MOBILE_BREAKPOINT = 768;
6
+
7
+ export function useIsMobile() {
8
+ const [isMobile, setIsMobile] = React.useState<boolean | undefined>(
9
+ undefined,
10
+ );
11
+
12
+ React.useEffect(() => {
13
+ const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`);
14
+ const onChange = () => {
15
+ setIsMobile(window.innerWidth < MOBILE_BREAKPOINT);
16
+ };
17
+
18
+ mql.addEventListener("change", onChange);
19
+ setIsMobile(window.innerWidth < MOBILE_BREAKPOINT);
20
+
21
+ return () => mql.removeEventListener("change", onChange);
22
+ }, []);
23
+
24
+ return !!isMobile;
25
+ }
@@ -0,0 +1,7 @@
1
+ // nextcli:locales:start
2
+ export const locales = ["vi"] as const;
3
+ // nextcli:locales:end
4
+
5
+ export const defaultLocale = "vi";
6
+
7
+ export type AppLocale = (typeof locales)[number];
@@ -0,0 +1,5 @@
1
+ // nextcli:namespaces:start
2
+ export const namespaces = ["common", "auth", "example"] as const;
3
+ // nextcli:namespaces:end
4
+
5
+ export type MessageNamespace = (typeof namespaces)[number];
@@ -1,8 +1,25 @@
1
1
  import { getRequestConfig } from "next-intl/server";
2
+ import { cookies } from "next/headers";
3
+ import { defaultLocale, locales, type AppLocale } from "@/i18n/config";
4
+ import { namespaces } from "@/i18n/namespaces";
2
5
 
3
6
  export default getRequestConfig(async () => {
7
+ const cookieStore = await cookies();
8
+ const cookieLocale = cookieStore.get("NEXT_LOCALE")?.value;
9
+ const locale = (
10
+ locales.includes(cookieLocale as AppLocale) ? cookieLocale : defaultLocale
11
+ ) as AppLocale;
12
+
13
+ const namespaceMessages = await Promise.all(
14
+ namespaces.map(async (namespace) => {
15
+ const file = (await import(`../../messages/${locale}/${namespace}.json`))
16
+ .default;
17
+ return [namespace, file] as const;
18
+ }),
19
+ );
20
+
4
21
  return {
5
- locale: "en",
6
- messages: {},
22
+ locale,
23
+ messages: Object.fromEntries(namespaceMessages),
7
24
  };
8
25
  });
@@ -1,10 +1,20 @@
1
+ import { PrismaPg } from "@prisma/adapter-pg";
1
2
  import { PrismaClient } from "@prisma/client";
3
+ import { Pool } from "pg";
2
4
 
3
5
  declare global {
4
6
  var prisma: PrismaClient | undefined;
5
7
  }
6
8
 
7
- const prisma = global.prisma ?? new PrismaClient();
9
+ const connectionString = process.env.DATABASE_URL;
10
+ if (!connectionString) {
11
+ throw new Error(
12
+ "DATABASE_URL is missing. Please set it in your environment.",
13
+ );
14
+ }
15
+
16
+ const adapter = new PrismaPg(new Pool({ connectionString }));
17
+ const prisma = global.prisma ?? new PrismaClient({ adapter });
8
18
 
9
19
  if (process.env.NODE_ENV !== "production") {
10
20
  global.prisma = prisma;
@@ -0,0 +1,4 @@
1
+ export type DataTablePaginationState = {
2
+ pageIndex: number;
3
+ pageSize: number;
4
+ };
@@ -38,3 +38,5 @@ export type ApiErrorResponse = {
38
38
  };
39
39
 
40
40
  export type ApiResponse<T> = ApiSuccess<T> | ApiErrorResponse;
41
+
42
+ export * from "@/types/data-table";
@@ -1,10 +0,0 @@
1
- import createMiddleware from "next-intl/middleware";
2
-
3
- export default createMiddleware({
4
- locales: ["en", "vi"],
5
- defaultLocale: "en",
6
- });
7
-
8
- export const config = {
9
- matcher: ["/((?!api|_next|.*\\..*).*)"],
10
- };
@@ -1,12 +0,0 @@
1
- :root {
2
- color-scheme: light dark;
3
- }
4
-
5
- * {
6
- box-sizing: border-box;
7
- }
8
-
9
- body {
10
- margin: 0;
11
- font-family: Arial, sans-serif;
12
- }