@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.
- package/README.md +27 -24
- package/dist/cli.js +168 -107
- package/package.json +1 -1
- package/templates/features/api/src/lib/api/axios.ts +1 -90
- package/templates/features/auth/messages/vi/auth.json +2 -1
- package/templates/features/auth/src/app/(auth)/change-password/page.tsx +5 -4
- package/templates/features/auth/src/app/(auth)/layout.tsx +2 -5
- package/templates/features/auth/src/app/(auth)/sign-in/page.tsx +5 -4
- package/templates/features/auth/src/app/api/v1/auth/login/route.ts +24 -29
- package/templates/features/auth/src/app/api/v1/auth/logout/route.ts +0 -5
- package/templates/features/auth/src/components/layout/auth/auth-shell.tsx +24 -0
- package/templates/features/auth/src/features/auth/components/account-panel.tsx +15 -3
- package/templates/features/auth/src/features/auth/components/change-password-form.tsx +27 -30
- package/templates/features/auth/src/features/auth/components/sign-in-form.tsx +33 -42
- package/templates/features/auth/src/lib/auth/client.ts +2 -2
- package/templates/features/auth/src/lib/auth/server.ts +2 -2
- package/templates/features/dashboard/src/app/(dashboard)/account/page.tsx +9 -7
- package/templates/features/dashboard/src/app/(dashboard)/dashboard/page.tsx +24 -10
- package/templates/features/dashboard/src/components/layout/private/app-sidebar.tsx +1 -13
- package/templates/features/dashboard/src/components/layout/private/dashboard-layout.tsx +31 -22
- package/templates/features/dashboard/src/components/layout/private/page-shell.tsx +40 -0
- package/templates/features/database/prisma/schema.prisma +1 -0
- package/templates/features/example/messages/vi/example.json +11 -1
- package/templates/features/example/src/app/(dashboard)/example/page.tsx +92 -3
- package/templates/features/example/src/example/components/example-table.tsx +15 -2
- package/templates/next-base/.env +16 -0
- package/templates/next-base/.env.development +16 -0
- package/templates/next-base/.env.example +16 -0
- package/templates/next-base/SETUP.md +62 -10
- package/templates/next-base/bun.lock +407 -0
- package/templates/next-base/messages/vi/auth.json +43 -0
- package/templates/next-base/messages/vi/common.json +53 -0
- package/templates/next-base/messages/vi/example.json +20 -0
- package/templates/next-base/next-env.d.ts +1 -1
- package/templates/next-base/next.config.ts +4 -1
- package/templates/next-base/nextcli.json +12 -4
- package/templates/next-base/package.json +24 -5
- package/templates/next-base/prisma/schema.prisma +85 -0
- package/templates/next-base/prisma.config.ts +16 -0
- package/templates/next-base/src/app/(auth)/.gitkeep +1 -0
- package/templates/next-base/src/app/(auth)/change-password/layout.tsx +21 -0
- package/templates/next-base/src/app/(auth)/change-password/page.tsx +15 -0
- package/templates/next-base/src/app/(auth)/layout.tsx +6 -0
- package/templates/next-base/src/app/(auth)/sign-in/layout.tsx +17 -0
- package/templates/next-base/src/app/(auth)/sign-in/page.tsx +15 -0
- package/templates/next-base/src/app/(dashboard)/account/page.tsx +20 -0
- package/templates/next-base/src/app/(dashboard)/dashboard/page.tsx +31 -0
- package/templates/next-base/src/app/(dashboard)/example/page.tsx +102 -0
- package/templates/next-base/src/app/(dashboard)/layout.tsx +22 -0
- package/templates/next-base/src/app/api/auth/[...all]/route.ts +4 -0
- package/templates/next-base/src/app/api/v1/auth/change-password/route.ts +55 -0
- package/templates/next-base/src/app/api/v1/auth/login/route.ts +65 -0
- package/templates/next-base/src/app/api/v1/auth/logout/route.ts +23 -0
- package/templates/next-base/src/app/api/v1/auth/me/route.ts +24 -0
- package/templates/next-base/src/app/api/v1/example/route.ts +34 -0
- package/templates/next-base/src/app/api/v1/users/[id]/route.ts +104 -0
- package/templates/next-base/src/app/api/v1/users/route.ts +58 -0
- package/templates/next-base/src/app/layout.tsx +14 -6
- package/templates/next-base/src/app/page.tsx +2 -25
- package/templates/next-base/src/components/branding/logo.tsx +27 -4
- package/templates/next-base/src/components/layout/auth/auth-shell.tsx +24 -0
- package/templates/next-base/src/components/layout/private/app-sidebar.tsx +32 -0
- package/templates/next-base/src/components/layout/private/dashboard-layout.tsx +63 -0
- package/templates/next-base/src/components/layout/private/locale-switcher.tsx +45 -0
- package/templates/next-base/src/components/layout/private/nav-sidebar.tsx +55 -0
- package/templates/next-base/src/components/layout/private/nav-user.tsx +99 -0
- package/templates/next-base/src/components/layout/private/page-shell.tsx +40 -0
- package/templates/next-base/src/components/providers/query-provider.tsx +17 -0
- package/templates/next-base/src/components/ui/data-table/data-table-column-header.tsx +23 -0
- package/templates/next-base/src/components/ui/data-table/data-table-filter-list.tsx +3 -0
- package/templates/next-base/src/components/ui/data-table/data-table-pagination.tsx +35 -0
- package/templates/next-base/src/components/ui/data-table/data-table-skeleton.tsx +11 -0
- package/templates/next-base/src/components/ui/data-table/data-table-toolbar.tsx +14 -0
- package/templates/next-base/src/components/ui/data-table/data-table-view-options.tsx +3 -0
- package/templates/next-base/src/components/ui/data-table/data-table.tsx +72 -0
- package/templates/next-base/src/components/ui/sidebar.tsx +215 -0
- package/templates/next-base/src/data/sidebar-modules.ts +11 -0
- package/templates/next-base/src/example/api/use-example.ts +21 -0
- package/templates/next-base/src/example/api/use-mutations.ts +20 -0
- package/templates/next-base/src/example/components/example-table.tsx +64 -0
- package/templates/next-base/src/example/services.ts +9 -0
- package/templates/next-base/src/example/validations.ts +8 -0
- package/templates/next-base/src/features/auth/components/account-panel.tsx +92 -0
- package/templates/next-base/src/features/auth/components/change-password-form.tsx +79 -0
- package/templates/next-base/src/features/auth/components/sign-in-form.tsx +86 -0
- package/templates/next-base/src/features/auth/validations.ts +14 -0
- package/templates/next-base/src/features/users/services.ts +132 -0
- package/templates/next-base/src/features/users/validations.ts +21 -0
- package/templates/next-base/src/hooks/table/use-data-table.ts +33 -0
- package/templates/next-base/src/hooks/use-mobile.ts +25 -0
- package/templates/next-base/src/i18n/config.ts +7 -0
- package/templates/next-base/src/i18n/namespaces.ts +5 -0
- package/templates/next-base/src/i18n/request.ts +25 -0
- package/templates/next-base/src/instrumentation.ts +14 -0
- package/templates/next-base/src/lib/api/axios.ts +56 -0
- package/templates/next-base/src/lib/api/response.ts +45 -0
- package/templates/next-base/src/lib/auth/bootstrap.ts +95 -0
- package/templates/next-base/src/lib/auth/client.ts +7 -0
- package/templates/next-base/src/lib/auth/index.ts +1 -0
- package/templates/next-base/src/lib/auth/rbac.ts +59 -0
- package/templates/next-base/src/lib/auth/server.ts +21 -0
- package/templates/next-base/src/lib/constants.ts +10 -0
- package/templates/next-base/src/lib/db/prisma.ts +23 -0
- package/templates/next-base/src/lib/prisma.ts +23 -0
- package/templates/next-base/src/lib/supabase/client.ts +6 -0
- package/templates/next-base/src/lib/supabase/rich-text-image-sync.ts +28 -0
- package/templates/next-base/src/lib/supabase/storage-config.ts +69 -0
- package/templates/next-base/src/lib/supabase/storage.ts +164 -0
- package/templates/next-base/src/types/data-table.ts +4 -0
- package/templates/features/api/src/lib/api/token-store.ts +0 -13
- package/templates/features/auth/src/app/api/v1/auth/refresh/route.ts +0 -32
- package/templates/features/auth/src/lib/auth/cookies.ts +0 -15
|
@@ -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,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,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
|
+
}
|