@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
@@ -0,0 +1,99 @@
1
+ "use client";
2
+
3
+ import { LogOut, Monitor, Moon, Sun } from "lucide-react";
4
+ import { useTheme } from "next-themes";
5
+ import { useTranslations } from "next-intl";
6
+ import { authClient } from "@/lib/auth/client";
7
+ import { Avatar, AvatarFallback } from "@/components/ui/avatar";
8
+ import { Button } from "@/components/ui/button";
9
+ import { ScrollArea } from "@/components/ui/scroll-area";
10
+ import {
11
+ Sheet,
12
+ SheetContent,
13
+ SheetFooter,
14
+ SheetHeader,
15
+ SheetTitle,
16
+ SheetTrigger,
17
+ } from "@/components/ui/sheet";
18
+ import { LocaleSwitcher } from "@/components/layout/private/locale-switcher";
19
+
20
+ export function NavUser() {
21
+ const { theme, setTheme } = useTheme();
22
+ const t = useTranslations("common");
23
+
24
+ const handleLogout = async () => {
25
+ await authClient.signOut();
26
+ window.location.href = "/sign-in";
27
+ };
28
+
29
+ return (
30
+ <Sheet>
31
+ <SheetTrigger asChild>
32
+ <Button variant="ghost" className="h-fit w-fit px-2 py-1">
33
+ <div className="grid text-right text-sm">
34
+ <span className="font-semibold">{t("userMenu.anonymousName")}</span>
35
+ <span className="text-xs text-muted-foreground">
36
+ {t("userMenu.anonymousEmail")}
37
+ </span>
38
+ </div>
39
+ <Avatar className="h-9 w-9">
40
+ <AvatarFallback>U</AvatarFallback>
41
+ </Avatar>
42
+ </Button>
43
+ </SheetTrigger>
44
+ <SheetContent side="right" className="w-full max-w-sm p-0">
45
+ <SheetHeader className="border-b p-4">
46
+ <SheetTitle>{t("userMenu.title")}</SheetTitle>
47
+ </SheetHeader>
48
+
49
+ <ScrollArea className="h-[calc(100vh-10rem)] p-4">
50
+ <div className="space-y-4">
51
+ <div className="rounded-md border bg-card p-4">
52
+ <p className="mb-3 text-sm font-medium">
53
+ {t("userMenu.appearance")}
54
+ </p>
55
+ <div className="grid grid-cols-3 gap-2">
56
+ <Button
57
+ variant={theme === "light" ? "default" : "outline"}
58
+ size="sm"
59
+ onClick={() => setTheme("light")}
60
+ >
61
+ <Sun className="h-4 w-4" />
62
+ {t("header.themeLight")}
63
+ </Button>
64
+ <Button
65
+ variant={theme === "dark" ? "default" : "outline"}
66
+ size="sm"
67
+ onClick={() => setTheme("dark")}
68
+ >
69
+ <Moon className="h-4 w-4" />
70
+ {t("header.themeDark")}
71
+ </Button>
72
+ <Button
73
+ variant={theme === "system" ? "default" : "outline"}
74
+ size="sm"
75
+ onClick={() => setTheme("system")}
76
+ >
77
+ <Monitor className="h-4 w-4" />
78
+ {t("header.themeSystem")}
79
+ </Button>
80
+ </div>
81
+ </div>
82
+ <LocaleSwitcher />
83
+ </div>
84
+ </ScrollArea>
85
+
86
+ <SheetFooter className="border-t p-0">
87
+ <Button
88
+ variant="ghost"
89
+ className="w-full justify-center rounded-none py-6 text-destructive hover:bg-destructive/10"
90
+ onClick={handleLogout}
91
+ >
92
+ <LogOut className="h-4 w-4" />
93
+ {t("userMenu.logout")}
94
+ </Button>
95
+ </SheetFooter>
96
+ </SheetContent>
97
+ </Sheet>
98
+ );
99
+ }
@@ -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
+ }
@@ -0,0 +1,17 @@
1
+ "use client";
2
+
3
+ import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
4
+ import { ReactQueryDevtools } from "@tanstack/react-query-devtools";
5
+ import { useState } from "react";
6
+ import type { ReactNode } from "react";
7
+
8
+ export function QueryProvider({ children }: { children: ReactNode }) {
9
+ const [queryClient] = useState(() => new QueryClient());
10
+
11
+ return (
12
+ <QueryClientProvider client={queryClient}>
13
+ {children}
14
+ <ReactQueryDevtools initialIsOpen={false} />
15
+ </QueryClientProvider>
16
+ );
17
+ }
@@ -0,0 +1,23 @@
1
+ "use client";
2
+
3
+ import type { Column } from "@tanstack/react-table";
4
+ import { Button } from "@/components/ui/button";
5
+
6
+ export function DataTableColumnHeader<TData, TValue>({
7
+ column,
8
+ title,
9
+ }: {
10
+ column: Column<TData, TValue>;
11
+ title: string;
12
+ }) {
13
+ return (
14
+ <Button
15
+ variant="ghost"
16
+ size="sm"
17
+ className="h-8 px-2"
18
+ onClick={() => column.toggleSorting(column.getIsSorted() === "asc")}
19
+ >
20
+ {title}
21
+ </Button>
22
+ );
23
+ }
@@ -0,0 +1,3 @@
1
+ export function DataTableFilterList() {
2
+ return null;
3
+ }
@@ -0,0 +1,35 @@
1
+ "use client";
2
+
3
+ import type { Table } from "@tanstack/react-table";
4
+ import { useTranslations } from "next-intl";
5
+ import { Button } from "@/components/ui/button";
6
+
7
+ export function DataTablePagination<TData>({ table }: { table: Table<TData> }) {
8
+ const t = useTranslations("common.table");
9
+ const pageCount = table.getPageCount();
10
+ const pageIndex = table.getState().pagination.pageIndex;
11
+
12
+ return (
13
+ <div className="flex items-center justify-end gap-2">
14
+ <span className="text-xs text-muted-foreground">
15
+ {pageCount === 0 ? 0 : pageIndex + 1}/{Math.max(pageCount, 1)}
16
+ </span>
17
+ <Button
18
+ variant="outline"
19
+ size="sm"
20
+ onClick={() => table.previousPage()}
21
+ disabled={!table.getCanPreviousPage()}
22
+ >
23
+ {t("previous")}
24
+ </Button>
25
+ <Button
26
+ variant="outline"
27
+ size="sm"
28
+ onClick={() => table.nextPage()}
29
+ disabled={!table.getCanNextPage()}
30
+ >
31
+ {t("next")}
32
+ </Button>
33
+ </div>
34
+ );
35
+ }
@@ -0,0 +1,11 @@
1
+ import { Skeleton } from "@/components/ui/skeleton";
2
+
3
+ export function DataTableSkeleton() {
4
+ return (
5
+ <div className="space-y-2">
6
+ <Skeleton className="h-8 w-full" />
7
+ <Skeleton className="h-8 w-full" />
8
+ <Skeleton className="h-8 w-full" />
9
+ </div>
10
+ );
11
+ }
@@ -0,0 +1,14 @@
1
+ "use client";
2
+
3
+ import type { Table } from "@tanstack/react-table";
4
+ import type { ReactNode } from "react";
5
+
6
+ export function DataTableToolbar<TData>({
7
+ _table,
8
+ children,
9
+ }: {
10
+ _table: Table<TData>;
11
+ children?: ReactNode;
12
+ }) {
13
+ return <div className="flex items-center justify-between">{children}</div>;
14
+ }
@@ -0,0 +1,3 @@
1
+ export function DataTableViewOptions() {
2
+ return null;
3
+ }
@@ -0,0 +1,72 @@
1
+ "use client";
2
+
3
+ import { flexRender, type Table as TanstackTable } from "@tanstack/react-table";
4
+ import { useTranslations } from "next-intl";
5
+ import { DataTablePagination } from "@/components/ui/data-table/data-table-pagination";
6
+ import {
7
+ Table,
8
+ TableBody,
9
+ TableCell,
10
+ TableHead,
11
+ TableHeader,
12
+ TableRow,
13
+ } from "@/components/ui/table";
14
+
15
+ type DataTableProps<TData> = {
16
+ table: TanstackTable<TData>;
17
+ };
18
+
19
+ export function DataTable<TData>({ table }: DataTableProps<TData>) {
20
+ const t = useTranslations("common.table");
21
+
22
+ return (
23
+ <div className="space-y-3">
24
+ <div className="overflow-hidden rounded-md border">
25
+ <Table>
26
+ <TableHeader>
27
+ {table.getHeaderGroups().map((headerGroup) => (
28
+ <TableRow key={headerGroup.id}>
29
+ {headerGroup.headers.map((header) => (
30
+ <TableHead key={header.id}>
31
+ {header.isPlaceholder
32
+ ? null
33
+ : flexRender(
34
+ header.column.columnDef.header,
35
+ header.getContext(),
36
+ )}
37
+ </TableHead>
38
+ ))}
39
+ </TableRow>
40
+ ))}
41
+ </TableHeader>
42
+ <TableBody>
43
+ {table.getRowModel().rows.length > 0 ? (
44
+ table.getRowModel().rows.map((row) => (
45
+ <TableRow key={row.id}>
46
+ {row.getVisibleCells().map((cell) => (
47
+ <TableCell key={cell.id}>
48
+ {flexRender(
49
+ cell.column.columnDef.cell,
50
+ cell.getContext(),
51
+ )}
52
+ </TableCell>
53
+ ))}
54
+ </TableRow>
55
+ ))
56
+ ) : (
57
+ <TableRow>
58
+ <TableCell
59
+ colSpan={table.getAllColumns().length}
60
+ className="h-24 text-center"
61
+ >
62
+ {t("noResults")}
63
+ </TableCell>
64
+ </TableRow>
65
+ )}
66
+ </TableBody>
67
+ </Table>
68
+ </div>
69
+ <DataTablePagination table={table} />
70
+ </div>
71
+ );
72
+ }
@@ -0,0 +1,215 @@
1
+ "use client";
2
+
3
+ import * as React from "react";
4
+ import { PanelLeft } from "lucide-react";
5
+ import { Slot } from "@radix-ui/react-slot";
6
+ import { cn } from "@/utils/cn";
7
+ import { Button } from "@/components/ui/button";
8
+ import { Sheet, SheetContent } from "@/components/ui/sheet";
9
+ import { useIsMobile } from "@/hooks/use-mobile";
10
+
11
+ type SidebarContextValue = {
12
+ open: boolean;
13
+ setOpen: (value: boolean) => void;
14
+ openMobile: boolean;
15
+ setOpenMobile: (value: boolean) => void;
16
+ isMobile: boolean;
17
+ state: "expanded" | "collapsed";
18
+ toggleSidebar: () => void;
19
+ };
20
+
21
+ const SidebarContext = React.createContext<SidebarContextValue | null>(null);
22
+
23
+ export function useSidebar() {
24
+ const context = React.useContext(SidebarContext);
25
+ if (!context) {
26
+ throw new Error("useSidebar must be used within SidebarProvider");
27
+ }
28
+ return context;
29
+ }
30
+
31
+ export function SidebarProvider({
32
+ defaultOpen = true,
33
+ children,
34
+ }: {
35
+ defaultOpen?: boolean;
36
+ children: React.ReactNode;
37
+ }) {
38
+ const isMobile = useIsMobile();
39
+ const [open, setOpen] = React.useState(defaultOpen);
40
+ const [openMobile, setOpenMobile] = React.useState(false);
41
+ const state = open ? "expanded" : "collapsed";
42
+
43
+ const toggleSidebar = React.useCallback(() => {
44
+ if (isMobile) {
45
+ setOpenMobile((value) => !value);
46
+ return;
47
+ }
48
+ setOpen((value) => !value);
49
+ }, [isMobile]);
50
+
51
+ return (
52
+ <SidebarContext.Provider
53
+ value={{
54
+ open,
55
+ setOpen,
56
+ openMobile,
57
+ setOpenMobile,
58
+ isMobile,
59
+ state,
60
+ toggleSidebar,
61
+ }}
62
+ >
63
+ <div className="group/sidebar-wrapper flex min-h-svh w-full">
64
+ {children}
65
+ </div>
66
+ </SidebarContext.Provider>
67
+ );
68
+ }
69
+
70
+ export function Sidebar({
71
+ className,
72
+ children,
73
+ collapsible = "offcanvas",
74
+ }: React.ComponentProps<"div"> & { collapsible?: "offcanvas" | "none" }) {
75
+ const { isMobile, openMobile, setOpenMobile, state } = useSidebar();
76
+
77
+ if (collapsible === "none") {
78
+ return (
79
+ <aside
80
+ className={cn(
81
+ "flex h-full w-64 flex-col border-r bg-sidebar text-sidebar-foreground",
82
+ className,
83
+ )}
84
+ >
85
+ {children}
86
+ </aside>
87
+ );
88
+ }
89
+
90
+ if (isMobile) {
91
+ return (
92
+ <Sheet open={openMobile} onOpenChange={setOpenMobile}>
93
+ <SheetContent side="left" className={cn("w-[18rem] p-0", className)}>
94
+ <div className="flex h-full flex-col bg-sidebar text-sidebar-foreground">
95
+ {children}
96
+ </div>
97
+ </SheetContent>
98
+ </Sheet>
99
+ );
100
+ }
101
+
102
+ return (
103
+ <aside
104
+ data-state={state}
105
+ className={cn(
106
+ "hidden h-screen border-r bg-sidebar text-sidebar-foreground md:flex md:flex-col",
107
+ state === "collapsed" ? "w-14" : "w-64",
108
+ className,
109
+ )}
110
+ >
111
+ {children}
112
+ </aside>
113
+ );
114
+ }
115
+
116
+ export function SidebarTrigger({
117
+ className,
118
+ children,
119
+ ...props
120
+ }: React.ComponentProps<typeof Button>) {
121
+ const { toggleSidebar } = useSidebar();
122
+ return (
123
+ <Button
124
+ variant="ghost"
125
+ size="icon"
126
+ className={cn("h-8 w-8", className)}
127
+ onClick={(event) => {
128
+ props.onClick?.(event);
129
+ toggleSidebar();
130
+ }}
131
+ {...props}
132
+ >
133
+ {children ?? <PanelLeft className="h-4 w-4" />}
134
+ </Button>
135
+ );
136
+ }
137
+
138
+ export function SidebarHeader({
139
+ className,
140
+ ...props
141
+ }: React.ComponentProps<"div">) {
142
+ return <div className={cn("border-b p-3", className)} {...props} />;
143
+ }
144
+
145
+ export function SidebarContent({
146
+ className,
147
+ ...props
148
+ }: React.ComponentProps<"div">) {
149
+ return (
150
+ <div className={cn("flex-1 overflow-y-auto p-2", className)} {...props} />
151
+ );
152
+ }
153
+
154
+ export function SidebarGroup({
155
+ className,
156
+ ...props
157
+ }: React.ComponentProps<"div">) {
158
+ return <div className={cn("mb-2", className)} {...props} />;
159
+ }
160
+
161
+ export function SidebarGroupLabel({
162
+ className,
163
+ ...props
164
+ }: React.ComponentProps<"div">) {
165
+ return (
166
+ <div
167
+ className={cn(
168
+ "px-2 py-1 text-xs font-medium text-muted-foreground",
169
+ className,
170
+ )}
171
+ {...props}
172
+ />
173
+ );
174
+ }
175
+
176
+ export function SidebarGroupContent({
177
+ className,
178
+ ...props
179
+ }: React.ComponentProps<"div">) {
180
+ return <div className={cn("space-y-1", className)} {...props} />;
181
+ }
182
+
183
+ export function SidebarMenu({
184
+ className,
185
+ ...props
186
+ }: React.ComponentProps<"ul">) {
187
+ return <ul className={cn("space-y-1", className)} {...props} />;
188
+ }
189
+
190
+ export function SidebarMenuItem({
191
+ className,
192
+ ...props
193
+ }: React.ComponentProps<"li">) {
194
+ return <li className={cn("list-none", className)} {...props} />;
195
+ }
196
+
197
+ export function SidebarMenuButton({
198
+ className,
199
+ asChild = false,
200
+ isActive = false,
201
+ ...props
202
+ }: React.ComponentProps<"button"> & { asChild?: boolean; isActive?: boolean }) {
203
+ const Comp = asChild ? Slot : "button";
204
+ return (
205
+ <Comp
206
+ className={cn(
207
+ "flex h-9 w-full items-center gap-2 rounded-md px-2 text-sm hover:bg-sidebar-accent hover:text-sidebar-accent-foreground",
208
+ isActive &&
209
+ "bg-sidebar-primary text-sidebar-primary-foreground hover:bg-sidebar-primary/90",
210
+ className,
211
+ )}
212
+ {...props}
213
+ />
214
+ );
215
+ }
@@ -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
+ ];
@@ -0,0 +1,21 @@
1
+ "use client";
2
+
3
+ import { useQuery } from "@tanstack/react-query";
4
+ import { api } from "@/lib/api/axios";
5
+ import type { ApiSuccess } from "@/types";
6
+
7
+ type ExampleItem = {
8
+ id: string;
9
+ name: string;
10
+ description?: string | null;
11
+ };
12
+
13
+ export function useExample() {
14
+ return useQuery({
15
+ queryKey: ["example"],
16
+ queryFn: async () => {
17
+ const { data } = await api.get("/api/v1/example");
18
+ return (data as ApiSuccess<{ items: ExampleItem[] }>).data.items;
19
+ },
20
+ });
21
+ }
@@ -0,0 +1,20 @@
1
+ "use client";
2
+
3
+ import { useMutation, useQueryClient } from "@tanstack/react-query";
4
+ import { api } from "@/lib/api/axios";
5
+ import type { CreateExampleInput } from "@/example/validations";
6
+ import type { ApiSuccess } from "@/types";
7
+
8
+ export function useCreateExample() {
9
+ const queryClient = useQueryClient();
10
+
11
+ return useMutation({
12
+ mutationFn: async (payload: CreateExampleInput) => {
13
+ const { data } = await api.post("/api/v1/example", payload);
14
+ return (data as ApiSuccess<{ id: string }>).data;
15
+ },
16
+ onSuccess: async () => {
17
+ await queryClient.invalidateQueries({ queryKey: ["example"] });
18
+ },
19
+ });
20
+ }
@@ -0,0 +1,64 @@
1
+ "use client";
2
+
3
+ import { useMemo } from "react";
4
+ import { useTranslations } from "next-intl";
5
+ import {
6
+ createColumnHelper,
7
+ getCoreRowModel,
8
+ getPaginationRowModel,
9
+ useReactTable,
10
+ } from "@tanstack/react-table";
11
+ import { useExample } from "@/example/api/use-example";
12
+ import { DataTable } from "@/components/ui/data-table/data-table";
13
+ import { Card, CardContent } from "@/components/ui/card";
14
+
15
+ type ExampleItem = {
16
+ id: string;
17
+ name: string;
18
+ description?: string | null;
19
+ };
20
+
21
+ const columnHelper = createColumnHelper<ExampleItem>();
22
+
23
+ export function ExampleTable() {
24
+ const t = useTranslations("example.table");
25
+ const { data, isLoading } = useExample();
26
+ const rows = useMemo(() => (Array.isArray(data) ? data : []), [data]);
27
+
28
+ const translatedColumns = useMemo(
29
+ () => [
30
+ columnHelper.accessor("name", {
31
+ header: t("name"),
32
+ }),
33
+ columnHelper.accessor("description", {
34
+ header: t("description"),
35
+ }),
36
+ ],
37
+ [t],
38
+ );
39
+
40
+ const table = useReactTable({
41
+ data: rows,
42
+ columns: translatedColumns,
43
+ getCoreRowModel: getCoreRowModel(),
44
+ getPaginationRowModel: getPaginationRowModel(),
45
+ });
46
+
47
+ if (isLoading) {
48
+ return (
49
+ <Card>
50
+ <CardContent className="py-8 text-sm text-muted-foreground">
51
+ {t("loading")}
52
+ </CardContent>
53
+ </Card>
54
+ );
55
+ }
56
+
57
+ return (
58
+ <Card>
59
+ <CardContent className="pt-6">
60
+ <DataTable table={table} />
61
+ </CardContent>
62
+ </Card>
63
+ );
64
+ }
@@ -0,0 +1,9 @@
1
+ import prisma from "@/lib/db/prisma";
2
+
3
+ export async function listExamples() {
4
+ return prisma.example.findMany({
5
+ orderBy: {
6
+ createdAt: "desc",
7
+ },
8
+ });
9
+ }
@@ -0,0 +1,8 @@
1
+ import { z } from "zod";
2
+
3
+ export const createExampleSchema = z.object({
4
+ name: z.string().min(2),
5
+ description: z.string().optional(),
6
+ });
7
+
8
+ export type CreateExampleInput = z.infer<typeof createExampleSchema>;