@thinhnguyencth1204/nextcli 0.2.1 → 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.
- package/dist/cli.js +632 -90
- package/package.json +1 -1
- package/templates/next-base/components.json +21 -0
- package/templates/next-base/messages/vi/auth.json +28 -0
- package/templates/next-base/messages/vi/common.json +34 -0
- package/templates/next-base/messages/vi/example.json +10 -0
- package/templates/next-base/next.config.ts +11 -1
- package/templates/next-base/nextcli.json +8 -0
- package/templates/next-base/package.json +21 -1
- package/templates/next-base/postcss.config.mjs +5 -0
- package/templates/next-base/src/app/(auth)/layout.tsx +9 -0
- package/templates/next-base/src/app/(auth)/sign-in/page.tsx +6 -3
- package/templates/next-base/src/app/(dashboard)/account/page.tsx +9 -5
- package/templates/next-base/src/app/(dashboard)/dashboard/page.tsx +17 -0
- package/templates/next-base/src/app/(dashboard)/example/page.tsx +5 -2
- package/templates/next-base/src/app/(dashboard)/layout.tsx +10 -0
- package/templates/next-base/src/app/globals.css +107 -0
- package/templates/next-base/src/app/layout.tsx +18 -8
- package/templates/next-base/src/app/page.tsx +2 -18
- package/templates/next-base/src/components/layout/private/app-sidebar.tsx +45 -0
- package/templates/next-base/src/components/layout/private/dashboard-layout.tsx +53 -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/providers/theme-provider.tsx +11 -0
- package/templates/next-base/src/components/ui/alert-dialog.tsx +11 -0
- package/templates/next-base/src/components/ui/avatar.tsx +45 -0
- package/templates/next-base/src/components/ui/badge.tsx +29 -0
- package/templates/next-base/src/components/ui/button.tsx +47 -7
- package/templates/next-base/src/components/ui/card.tsx +54 -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/dialog.tsx +105 -0
- package/templates/next-base/src/components/ui/dropdown-menu.tsx +44 -0
- package/templates/next-base/src/components/ui/input.tsx +19 -0
- package/templates/next-base/src/components/ui/label.tsx +15 -0
- package/templates/next-base/src/components/ui/popover.tsx +30 -0
- package/templates/next-base/src/components/ui/scroll-area.tsx +47 -0
- package/templates/next-base/src/components/ui/select.tsx +76 -0
- package/templates/next-base/src/components/ui/separator.tsx +23 -0
- package/templates/next-base/src/components/ui/sheet.tsx +117 -0
- package/templates/next-base/src/components/ui/sidebar.tsx +215 -0
- package/templates/next-base/src/components/ui/skeleton.tsx +10 -0
- package/templates/next-base/src/components/ui/sonner.tsx +3 -0
- package/templates/next-base/src/components/ui/table.tsx +54 -0
- package/templates/next-base/src/components/ui/tabs.tsx +52 -0
- package/templates/next-base/src/components/ui/textarea.tsx +17 -0
- package/templates/next-base/src/components/ui/tooltip.tsx +26 -0
- package/templates/next-base/src/data/sidebar-modules.ts +11 -0
- package/templates/next-base/src/example/components/example-table.tsx +25 -40
- package/templates/next-base/src/features/auth/components/account-panel.tsx +21 -8
- package/templates/next-base/src/features/auth/components/sign-in-form.tsx +43 -30
- package/templates/next-base/src/hooks/index.ts +1 -1
- 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 +19 -2
- package/templates/next-base/src/lib/prisma.ts +11 -1
- package/templates/next-base/src/types/data-table.ts +4 -0
- package/templates/next-base/src/types/index.ts +2 -0
- package/templates/next-base/middleware.ts +0 -10
- 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 {
|
|
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>
|
|
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>
|
|
50
|
+
return <p>{t("loading")}</p>;
|
|
48
51
|
}
|
|
49
52
|
|
|
50
53
|
if (!session?.user) {
|
|
51
|
-
return <p>
|
|
54
|
+
return <p>{t("noSession")}</p>;
|
|
52
55
|
}
|
|
53
56
|
|
|
54
57
|
return (
|
|
55
|
-
<
|
|
56
|
-
<
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
<
|
|
60
|
-
|
|
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("
|
|
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 }>)
|
|
38
|
+
const accessToken = (response.data as ApiSuccess<{ accessToken: string }>)
|
|
39
|
+
.data?.accessToken;
|
|
33
40
|
if (!accessToken) {
|
|
34
|
-
toast.error("
|
|
41
|
+
toast.error(t("missingAccessToken"));
|
|
35
42
|
return;
|
|
36
43
|
}
|
|
37
44
|
|
|
38
45
|
setAccessToken(accessToken);
|
|
39
|
-
toast.success("
|
|
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 : "
|
|
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
|
-
<
|
|
52
|
-
<
|
|
53
|
-
<
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
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
|
+
}
|
|
@@ -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
|
|
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
|
|
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;
|