@thinhnguyencth1204/nextcli 0.1.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 +197 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +1439 -0
- package/package.json +43 -0
- package/templates/features/chat/src/app/api/v1/chat/route.ts +40 -0
- package/templates/features/chat/src/features/chat/api/use-chat-history.ts +18 -0
- package/templates/features/chat/src/features/chat/api/use-realtime-sync.ts +15 -0
- package/templates/features/chat/src/features/chat/api/use-send-message.ts +35 -0
- package/templates/features/chat/src/features/chat/components/ChatWidget.tsx +40 -0
- package/templates/features/chat/src/features/chat/services.ts +27 -0
- package/templates/features/seo/public/robots.txt +3 -0
- package/templates/features/seo/public/sitemap.xml +6 -0
- package/templates/features/seo/src/app/robots.ts +13 -0
- package/templates/features/seo/src/app/sitemap.ts +21 -0
- package/templates/features/seo/src/components/seo/json-ld.tsx +14 -0
- package/templates/features/supabase/src/lib/supabase/client.ts +9 -0
- package/templates/features/supabase/src/lib/supabase/storage-config.ts +69 -0
- package/templates/features/supabase/src/lib/supabase/storage.ts +167 -0
- package/templates/features/supabase-realtime/src/features/supabase-realtime/client.ts +9 -0
- package/templates/features/supabase-realtime/src/features/supabase-realtime/use-supabase-channel.ts +19 -0
- package/templates/next-base/.env +11 -0
- package/templates/next-base/.env.development +11 -0
- package/templates/next-base/.env.example +11 -0
- package/templates/next-base/eslint.config.mjs +20 -0
- package/templates/next-base/middleware.ts +10 -0
- package/templates/next-base/next-env.d.ts +4 -0
- package/templates/next-base/next.config.ts +7 -0
- package/templates/next-base/package.json +45 -0
- package/templates/next-base/prisma/migrations/.gitkeep +1 -0
- package/templates/next-base/prisma/schema.prisma +72 -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)/sign-in/page.tsx +11 -0
- package/templates/next-base/src/app/(dashboard)/account/page.tsx +14 -0
- package/templates/next-base/src/app/(dashboard)/example/page.tsx +10 -0
- package/templates/next-base/src/app/api/auth/[...all]/route.ts +4 -0
- package/templates/next-base/src/app/api/v1/auth/login/route.ts +60 -0
- package/templates/next-base/src/app/api/v1/auth/logout/route.ts +28 -0
- package/templates/next-base/src/app/api/v1/auth/me/route.ts +26 -0
- package/templates/next-base/src/app/api/v1/auth/refresh/route.ts +32 -0
- package/templates/next-base/src/app/api/v1/example/route.ts +34 -0
- package/templates/next-base/src/app/layout.tsx +28 -0
- package/templates/next-base/src/app/page.tsx +21 -0
- package/templates/next-base/src/app/styles.css +12 -0
- package/templates/next-base/src/components/providers/query-provider.tsx +17 -0
- package/templates/next-base/src/components/ui/button.tsx +16 -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 +66 -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 +62 -0
- package/templates/next-base/src/features/auth/components/sign-in-form.tsx +77 -0
- package/templates/next-base/src/features/auth/validations.ts +8 -0
- package/templates/next-base/src/hooks/index.ts +1 -0
- package/templates/next-base/src/i18n/request.ts +8 -0
- package/templates/next-base/src/lib/api-response.ts +49 -0
- package/templates/next-base/src/lib/auth-client.ts +7 -0
- package/templates/next-base/src/lib/auth-cookies.ts +15 -0
- package/templates/next-base/src/lib/auth.ts +20 -0
- package/templates/next-base/src/lib/axios-instance.ts +140 -0
- package/templates/next-base/src/lib/prisma.ts +13 -0
- package/templates/next-base/src/lib/token-store.ts +13 -0
- package/templates/next-base/src/types/index.ts +40 -0
- package/templates/next-base/src/utils/cn.ts +6 -0
- package/templates/next-base/tsconfig.json +24 -0
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "__PROJECT_NAME__",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"private": true,
|
|
5
|
+
"type": "module",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"dev": "next dev",
|
|
8
|
+
"build": "next build",
|
|
9
|
+
"start": "next start",
|
|
10
|
+
"lint": "eslint .",
|
|
11
|
+
"db:generate": "prisma generate",
|
|
12
|
+
"db:migrate": "prisma migrate dev",
|
|
13
|
+
"db:studio": "prisma studio"
|
|
14
|
+
},
|
|
15
|
+
"dependencies": {
|
|
16
|
+
"@prisma/client": "^7.8.0",
|
|
17
|
+
"@tanstack/react-form": "^1.0.3",
|
|
18
|
+
"@tanstack/react-query": "^5.56.2",
|
|
19
|
+
"@tanstack/react-query-devtools": "^5.56.2",
|
|
20
|
+
"@tanstack/react-table": "^8.20.5",
|
|
21
|
+
"axios": "^1.7.7",
|
|
22
|
+
"better-auth": "^1.6.11",
|
|
23
|
+
"class-variance-authority": "^0.7.0",
|
|
24
|
+
"clsx": "^2.1.1",
|
|
25
|
+
"date-fns": "^3.6.0",
|
|
26
|
+
"next": "^16.1.6",
|
|
27
|
+
"next-intl": "^4.13.0",
|
|
28
|
+
"react": "^19.0.0",
|
|
29
|
+
"react-dom": "^19.0.0",
|
|
30
|
+
"sonner": "^1.7.1",
|
|
31
|
+
"tailwind-merge": "^2.5.3",
|
|
32
|
+
"zod": "^3.23.8"
|
|
33
|
+
},
|
|
34
|
+
"devDependencies": {
|
|
35
|
+
"dotenv": "^17.4.2",
|
|
36
|
+
"@types/node": "^22.7.4",
|
|
37
|
+
"@types/react": "^19.0.0",
|
|
38
|
+
"@types/react-dom": "^19.0.0",
|
|
39
|
+
"eslint": "^9.11.1",
|
|
40
|
+
"eslint-config-next": "^16.1.6",
|
|
41
|
+
"prisma": "^7.8.0",
|
|
42
|
+
"tailwindcss": "^3.4.13",
|
|
43
|
+
"typescript": "^5.6.2"
|
|
44
|
+
}
|
|
45
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
generator client {
|
|
2
|
+
provider = "prisma-client-js"
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
datasource db {
|
|
6
|
+
provider = "postgresql"
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
model User {
|
|
10
|
+
id String @id @default(cuid())
|
|
11
|
+
email String @unique
|
|
12
|
+
name String?
|
|
13
|
+
image String?
|
|
14
|
+
emailVerified Boolean @default(false)
|
|
15
|
+
createdAt DateTime @default(now())
|
|
16
|
+
updatedAt DateTime @updatedAt
|
|
17
|
+
sessions Session[]
|
|
18
|
+
accounts Account[]
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// Starter demo table for template onboarding only.
|
|
22
|
+
// Rename/remove this `Example` model before production migration.
|
|
23
|
+
// Keep this block under review to avoid creating unwanted prod tables.
|
|
24
|
+
model Example {
|
|
25
|
+
id String @id @default(cuid())
|
|
26
|
+
name String
|
|
27
|
+
description String?
|
|
28
|
+
createdAt DateTime @default(now())
|
|
29
|
+
updatedAt DateTime @updatedAt
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
model Session {
|
|
33
|
+
id String @id @default(cuid())
|
|
34
|
+
token String @unique
|
|
35
|
+
expiresAt DateTime
|
|
36
|
+
ipAddress String?
|
|
37
|
+
userAgent String?
|
|
38
|
+
userId String
|
|
39
|
+
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
|
40
|
+
createdAt DateTime @default(now())
|
|
41
|
+
updatedAt DateTime @updatedAt
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
model Account {
|
|
45
|
+
id String @id @default(cuid())
|
|
46
|
+
accountId String
|
|
47
|
+
providerId String
|
|
48
|
+
userId String
|
|
49
|
+
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
|
50
|
+
accessToken String?
|
|
51
|
+
refreshToken String?
|
|
52
|
+
idToken String?
|
|
53
|
+
accessTokenExpiresAt DateTime?
|
|
54
|
+
refreshTokenExpiresAt DateTime?
|
|
55
|
+
scope String?
|
|
56
|
+
password String?
|
|
57
|
+
createdAt DateTime @default(now())
|
|
58
|
+
updatedAt DateTime @updatedAt
|
|
59
|
+
|
|
60
|
+
@@unique([providerId, accountId])
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
model Verification {
|
|
64
|
+
id String @id @default(cuid())
|
|
65
|
+
identifier String
|
|
66
|
+
value String
|
|
67
|
+
expiresAt DateTime
|
|
68
|
+
createdAt DateTime? @default(now())
|
|
69
|
+
updatedAt DateTime? @updatedAt
|
|
70
|
+
}
|
|
71
|
+
// Optional Chatbox models are appended by NexTCLI only when adding the chat feature.
|
|
72
|
+
// Review generated schema changes before running migrations on production databases.
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import "dotenv/config";
|
|
2
|
+
import { defineConfig, env } from "prisma/config";
|
|
3
|
+
|
|
4
|
+
type Env = {
|
|
5
|
+
DATABASE_URL: string;
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
export default defineConfig({
|
|
9
|
+
schema: "prisma/schema.prisma",
|
|
10
|
+
migrations: {
|
|
11
|
+
path: "prisma/migrations",
|
|
12
|
+
},
|
|
13
|
+
datasource: {
|
|
14
|
+
url: env<Env>("DATABASE_URL"),
|
|
15
|
+
},
|
|
16
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { SignInForm } from "@/features/auth/components/sign-in-form";
|
|
2
|
+
|
|
3
|
+
export default function SignInPage() {
|
|
4
|
+
return (
|
|
5
|
+
<main style={{ padding: 24 }}>
|
|
6
|
+
<h1>Sign in</h1>
|
|
7
|
+
<p>Use your email and password to get access.</p>
|
|
8
|
+
<SignInForm />
|
|
9
|
+
</main>
|
|
10
|
+
);
|
|
11
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import Link from "next/link";
|
|
2
|
+
import { AccountPanel } from "@/features/auth/components/account-panel";
|
|
3
|
+
|
|
4
|
+
export default function AccountPage() {
|
|
5
|
+
return (
|
|
6
|
+
<main style={{ padding: 24 }}>
|
|
7
|
+
<h1>Account</h1>
|
|
8
|
+
<AccountPanel />
|
|
9
|
+
<p style={{ marginTop: 16 }}>
|
|
10
|
+
<Link href="/sign-in">Go to sign in</Link>
|
|
11
|
+
</p>
|
|
12
|
+
</main>
|
|
13
|
+
);
|
|
14
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { getRefreshCookieName, refreshCookieOptions } from "@/lib/auth-cookies";
|
|
2
|
+
import { fail, ok } from "@/lib/api-response";
|
|
3
|
+
|
|
4
|
+
const authBaseUrl =
|
|
5
|
+
process.env.BETTER_AUTH_URL ??
|
|
6
|
+
process.env.NEXT_PUBLIC_APP_URL ??
|
|
7
|
+
"http://localhost:3000";
|
|
8
|
+
|
|
9
|
+
export async function POST(request: Request) {
|
|
10
|
+
const payload = (await request.json().catch(() => ({}))) as {
|
|
11
|
+
email?: string;
|
|
12
|
+
password?: string;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
if (!payload.email || !payload.password) {
|
|
16
|
+
return fail("BAD_REQUEST", "Email and password are required.", { status: 400 });
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const signInResponse = await fetch(`${authBaseUrl}/api/auth/sign-in/email`, {
|
|
20
|
+
method: "POST",
|
|
21
|
+
headers: {
|
|
22
|
+
"Content-Type": "application/json",
|
|
23
|
+
},
|
|
24
|
+
body: JSON.stringify({
|
|
25
|
+
email: payload.email,
|
|
26
|
+
password: payload.password,
|
|
27
|
+
}),
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
if (!signInResponse.ok) {
|
|
31
|
+
return fail("UNAUTHORIZED", "Invalid credentials.", {
|
|
32
|
+
status: signInResponse.status,
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const cookieHeader = signInResponse.headers.get("set-cookie");
|
|
37
|
+
|
|
38
|
+
const tokenResponse = await fetch(`${authBaseUrl}/api/auth/token`, {
|
|
39
|
+
method: "GET",
|
|
40
|
+
headers: cookieHeader ? { cookie: cookieHeader } : undefined,
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
if (!tokenResponse.ok) {
|
|
44
|
+
return fail("UPSTREAM_ERROR", "Unable to issue access token.", {
|
|
45
|
+
status: tokenResponse.status,
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const tokenPayload = await tokenResponse.json();
|
|
50
|
+
const response = ok({
|
|
51
|
+
accessToken: tokenPayload.token,
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
if (cookieHeader) {
|
|
55
|
+
response.headers.set("set-cookie", cookieHeader);
|
|
56
|
+
}
|
|
57
|
+
response.cookies.set(getRefreshCookieName(), crypto.randomUUID(), refreshCookieOptions());
|
|
58
|
+
|
|
59
|
+
return response;
|
|
60
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { getRefreshCookieName, refreshCookieOptions } from "@/lib/auth-cookies";
|
|
2
|
+
import { fail, ok } from "@/lib/api-response";
|
|
3
|
+
|
|
4
|
+
const authBaseUrl =
|
|
5
|
+
process.env.BETTER_AUTH_URL ??
|
|
6
|
+
process.env.NEXT_PUBLIC_APP_URL ??
|
|
7
|
+
"http://localhost:3000";
|
|
8
|
+
|
|
9
|
+
export async function POST(request: Request) {
|
|
10
|
+
const incomingCookie = request.headers.get("cookie") ?? "";
|
|
11
|
+
const signOutResponse = await fetch(`${authBaseUrl}/api/auth/sign-out`, {
|
|
12
|
+
method: "POST",
|
|
13
|
+
headers: {
|
|
14
|
+
cookie: incomingCookie,
|
|
15
|
+
},
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
const response = signOutResponse.ok
|
|
19
|
+
? ok({ loggedOut: true })
|
|
20
|
+
: fail("UPSTREAM_ERROR", "Failed to sign out.", {
|
|
21
|
+
status: signOutResponse.status,
|
|
22
|
+
});
|
|
23
|
+
response.cookies.set(getRefreshCookieName(), "", {
|
|
24
|
+
...refreshCookieOptions(),
|
|
25
|
+
maxAge: 0,
|
|
26
|
+
});
|
|
27
|
+
return response;
|
|
28
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { fail, ok } from "@/lib/api-response";
|
|
2
|
+
|
|
3
|
+
const authBaseUrl =
|
|
4
|
+
process.env.BETTER_AUTH_URL ??
|
|
5
|
+
process.env.NEXT_PUBLIC_APP_URL ??
|
|
6
|
+
"http://localhost:3000";
|
|
7
|
+
|
|
8
|
+
export async function GET(request: Request) {
|
|
9
|
+
const incomingCookie = request.headers.get("cookie") ?? "";
|
|
10
|
+
|
|
11
|
+
const sessionResponse = await fetch(`${authBaseUrl}/api/auth/get-session`, {
|
|
12
|
+
method: "GET",
|
|
13
|
+
headers: {
|
|
14
|
+
cookie: incomingCookie,
|
|
15
|
+
},
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
if (!sessionResponse.ok) {
|
|
19
|
+
return fail("UNAUTHORIZED", "Unauthorized", {
|
|
20
|
+
status: sessionResponse.status,
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const payload = await sessionResponse.json();
|
|
25
|
+
return ok(payload);
|
|
26
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { getRefreshCookieName } from "@/lib/auth-cookies";
|
|
2
|
+
import { fail, ok } from "@/lib/api-response";
|
|
3
|
+
|
|
4
|
+
const authBaseUrl =
|
|
5
|
+
process.env.BETTER_AUTH_URL ??
|
|
6
|
+
process.env.NEXT_PUBLIC_APP_URL ??
|
|
7
|
+
"http://localhost:3000";
|
|
8
|
+
|
|
9
|
+
export async function POST(request: Request) {
|
|
10
|
+
const incomingCookie = request.headers.get("cookie") ?? "";
|
|
11
|
+
if (!incomingCookie.includes(getRefreshCookieName())) {
|
|
12
|
+
return fail("UNAUTHORIZED", "Refresh cookie is missing.", { status: 401 });
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const tokenResponse = await fetch(`${authBaseUrl}/api/auth/token`, {
|
|
16
|
+
method: "GET",
|
|
17
|
+
headers: {
|
|
18
|
+
cookie: incomingCookie,
|
|
19
|
+
},
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
if (!tokenResponse.ok) {
|
|
23
|
+
return fail("UNAUTHORIZED", "Failed to refresh access token.", {
|
|
24
|
+
status: tokenResponse.status,
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const tokenPayload = await tokenResponse.json();
|
|
29
|
+
return ok({
|
|
30
|
+
accessToken: tokenPayload.token,
|
|
31
|
+
});
|
|
32
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { createExampleSchema } from "@/example/validations";
|
|
2
|
+
import { listExamples } from "@/example/services";
|
|
3
|
+
import { fail, ok } from "@/lib/api-response";
|
|
4
|
+
import prisma from "@/lib/prisma";
|
|
5
|
+
|
|
6
|
+
export async function GET() {
|
|
7
|
+
try {
|
|
8
|
+
const examples = await listExamples();
|
|
9
|
+
return ok({ items: examples });
|
|
10
|
+
} catch {
|
|
11
|
+
return fail("INTERNAL_ERROR", "Failed to fetch examples.", { status: 500 });
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export async function POST(request: Request) {
|
|
16
|
+
const payload = await request.json().catch(() => null);
|
|
17
|
+
const parsed = createExampleSchema.safeParse(payload);
|
|
18
|
+
|
|
19
|
+
if (!parsed.success) {
|
|
20
|
+
return fail("VALIDATION_ERROR", "Invalid example payload.", {
|
|
21
|
+
status: 400,
|
|
22
|
+
details: parsed.error.flatten(),
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
try {
|
|
27
|
+
const created = await prisma.example.create({
|
|
28
|
+
data: parsed.data,
|
|
29
|
+
});
|
|
30
|
+
return ok(created, { status: 201 });
|
|
31
|
+
} catch {
|
|
32
|
+
return fail("INTERNAL_ERROR", "Failed to create example.", { status: 500 });
|
|
33
|
+
}
|
|
34
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { Metadata } from "next";
|
|
2
|
+
import { Inter } from "next/font/google";
|
|
3
|
+
import { Toaster } from "sonner";
|
|
4
|
+
import type { ReactNode } from "react";
|
|
5
|
+
import { QueryProvider } from "@/components/providers/query-provider";
|
|
6
|
+
import "@/app/styles.css";
|
|
7
|
+
|
|
8
|
+
const inter = Inter({ subsets: ["latin"] });
|
|
9
|
+
|
|
10
|
+
export const metadata: Metadata = {
|
|
11
|
+
title: "__PROJECT_NAME__",
|
|
12
|
+
description: "Outsource-ready Next.js scaffolded by NexTCLI",
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export default function RootLayout({
|
|
16
|
+
children,
|
|
17
|
+
}: Readonly<{
|
|
18
|
+
children: ReactNode;
|
|
19
|
+
}>) {
|
|
20
|
+
return (
|
|
21
|
+
<html lang="en">
|
|
22
|
+
<body className={inter.className}>
|
|
23
|
+
<QueryProvider>{children}</QueryProvider>
|
|
24
|
+
<Toaster richColors position="top-right" />
|
|
25
|
+
</body>
|
|
26
|
+
</html>
|
|
27
|
+
);
|
|
28
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import Link from "next/link";
|
|
2
|
+
|
|
3
|
+
export default function HomePage() {
|
|
4
|
+
return (
|
|
5
|
+
<main style={{ padding: 24 }}>
|
|
6
|
+
<h1>NexTCLI Base Template</h1>
|
|
7
|
+
<p>Ship outsource projects faster with standardized architecture.</p>
|
|
8
|
+
<ul style={{ display: "grid", gap: 8 }}>
|
|
9
|
+
<li>
|
|
10
|
+
<Link href="/example">Go to example dashboard page</Link>
|
|
11
|
+
</li>
|
|
12
|
+
<li>
|
|
13
|
+
<Link href="/sign-in">Go to sign-in page</Link>
|
|
14
|
+
</li>
|
|
15
|
+
<li>
|
|
16
|
+
<Link href="/account">Go to account page</Link>
|
|
17
|
+
</li>
|
|
18
|
+
</ul>
|
|
19
|
+
</main>
|
|
20
|
+
);
|
|
21
|
+
}
|
|
@@ -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,16 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { cn } from "@/utils/cn";
|
|
3
|
+
|
|
4
|
+
export type ButtonProps = React.ButtonHTMLAttributes<HTMLButtonElement>;
|
|
5
|
+
|
|
6
|
+
export function Button({ className, ...props }: ButtonProps) {
|
|
7
|
+
return (
|
|
8
|
+
<button
|
|
9
|
+
className={cn(
|
|
10
|
+
"inline-flex items-center justify-center rounded-md border px-3 py-2 text-sm",
|
|
11
|
+
className,
|
|
12
|
+
)}
|
|
13
|
+
{...props}
|
|
14
|
+
/>
|
|
15
|
+
);
|
|
16
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useQuery } from "@tanstack/react-query";
|
|
4
|
+
import { api } from "@/lib/axios-instance";
|
|
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/axios-instance";
|
|
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,66 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useMemo } from "react";
|
|
4
|
+
import { createColumnHelper, getCoreRowModel, useReactTable } from "@tanstack/react-table";
|
|
5
|
+
import { useExample } from "@/example/api/use-example";
|
|
6
|
+
|
|
7
|
+
type ExampleItem = {
|
|
8
|
+
id: string;
|
|
9
|
+
name: string;
|
|
10
|
+
description?: string | null;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
const columnHelper = createColumnHelper<ExampleItem>();
|
|
14
|
+
|
|
15
|
+
const columns = [
|
|
16
|
+
columnHelper.accessor("name", {
|
|
17
|
+
header: "Name",
|
|
18
|
+
}),
|
|
19
|
+
columnHelper.accessor("description", {
|
|
20
|
+
header: "Description",
|
|
21
|
+
}),
|
|
22
|
+
];
|
|
23
|
+
|
|
24
|
+
export function ExampleTable() {
|
|
25
|
+
const { data, isLoading } = useExample();
|
|
26
|
+
const rows = useMemo(() => (Array.isArray(data) ? data : []), [data]);
|
|
27
|
+
|
|
28
|
+
const table = useReactTable({
|
|
29
|
+
data: rows,
|
|
30
|
+
columns,
|
|
31
|
+
getCoreRowModel: getCoreRowModel(),
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
if (isLoading) {
|
|
35
|
+
return <p>Loading examples...</p>;
|
|
36
|
+
}
|
|
37
|
+
|
|
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
|
+
);
|
|
66
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useEffect, useState } from "react";
|
|
4
|
+
import { publicApi } from "@/lib/axios-instance";
|
|
5
|
+
import type { ApiSuccess } from "@/types";
|
|
6
|
+
|
|
7
|
+
type SessionPayload = {
|
|
8
|
+
user?: {
|
|
9
|
+
id: string;
|
|
10
|
+
email: string;
|
|
11
|
+
name?: string | null;
|
|
12
|
+
};
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export function AccountPanel() {
|
|
16
|
+
const [session, setSession] = useState<SessionPayload | null>(null);
|
|
17
|
+
const [loading, setLoading] = useState(true);
|
|
18
|
+
|
|
19
|
+
useEffect(() => {
|
|
20
|
+
let mounted = true;
|
|
21
|
+
const run = async () => {
|
|
22
|
+
try {
|
|
23
|
+
const response = await publicApi.get("/api/v1/auth/me", {
|
|
24
|
+
withCredentials: true,
|
|
25
|
+
});
|
|
26
|
+
if (mounted) {
|
|
27
|
+
setSession((response.data as ApiSuccess<SessionPayload>).data);
|
|
28
|
+
}
|
|
29
|
+
} catch {
|
|
30
|
+
if (mounted) {
|
|
31
|
+
setSession(null);
|
|
32
|
+
}
|
|
33
|
+
} finally {
|
|
34
|
+
if (mounted) {
|
|
35
|
+
setLoading(false);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
void run();
|
|
41
|
+
return () => {
|
|
42
|
+
mounted = false;
|
|
43
|
+
};
|
|
44
|
+
}, []);
|
|
45
|
+
|
|
46
|
+
if (loading) {
|
|
47
|
+
return <p>Loading account...</p>;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (!session?.user) {
|
|
51
|
+
return <p>No active session. Please sign in first.</p>;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
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>
|
|
61
|
+
);
|
|
62
|
+
}
|