@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.
Files changed (66) hide show
  1. package/README.md +197 -0
  2. package/dist/cli.d.ts +2 -0
  3. package/dist/cli.js +1439 -0
  4. package/package.json +43 -0
  5. package/templates/features/chat/src/app/api/v1/chat/route.ts +40 -0
  6. package/templates/features/chat/src/features/chat/api/use-chat-history.ts +18 -0
  7. package/templates/features/chat/src/features/chat/api/use-realtime-sync.ts +15 -0
  8. package/templates/features/chat/src/features/chat/api/use-send-message.ts +35 -0
  9. package/templates/features/chat/src/features/chat/components/ChatWidget.tsx +40 -0
  10. package/templates/features/chat/src/features/chat/services.ts +27 -0
  11. package/templates/features/seo/public/robots.txt +3 -0
  12. package/templates/features/seo/public/sitemap.xml +6 -0
  13. package/templates/features/seo/src/app/robots.ts +13 -0
  14. package/templates/features/seo/src/app/sitemap.ts +21 -0
  15. package/templates/features/seo/src/components/seo/json-ld.tsx +14 -0
  16. package/templates/features/supabase/src/lib/supabase/client.ts +9 -0
  17. package/templates/features/supabase/src/lib/supabase/storage-config.ts +69 -0
  18. package/templates/features/supabase/src/lib/supabase/storage.ts +167 -0
  19. package/templates/features/supabase-realtime/src/features/supabase-realtime/client.ts +9 -0
  20. package/templates/features/supabase-realtime/src/features/supabase-realtime/use-supabase-channel.ts +19 -0
  21. package/templates/next-base/.env +11 -0
  22. package/templates/next-base/.env.development +11 -0
  23. package/templates/next-base/.env.example +11 -0
  24. package/templates/next-base/eslint.config.mjs +20 -0
  25. package/templates/next-base/middleware.ts +10 -0
  26. package/templates/next-base/next-env.d.ts +4 -0
  27. package/templates/next-base/next.config.ts +7 -0
  28. package/templates/next-base/package.json +45 -0
  29. package/templates/next-base/prisma/migrations/.gitkeep +1 -0
  30. package/templates/next-base/prisma/schema.prisma +72 -0
  31. package/templates/next-base/prisma.config.ts +16 -0
  32. package/templates/next-base/src/app/(auth)/.gitkeep +1 -0
  33. package/templates/next-base/src/app/(auth)/sign-in/page.tsx +11 -0
  34. package/templates/next-base/src/app/(dashboard)/account/page.tsx +14 -0
  35. package/templates/next-base/src/app/(dashboard)/example/page.tsx +10 -0
  36. package/templates/next-base/src/app/api/auth/[...all]/route.ts +4 -0
  37. package/templates/next-base/src/app/api/v1/auth/login/route.ts +60 -0
  38. package/templates/next-base/src/app/api/v1/auth/logout/route.ts +28 -0
  39. package/templates/next-base/src/app/api/v1/auth/me/route.ts +26 -0
  40. package/templates/next-base/src/app/api/v1/auth/refresh/route.ts +32 -0
  41. package/templates/next-base/src/app/api/v1/example/route.ts +34 -0
  42. package/templates/next-base/src/app/layout.tsx +28 -0
  43. package/templates/next-base/src/app/page.tsx +21 -0
  44. package/templates/next-base/src/app/styles.css +12 -0
  45. package/templates/next-base/src/components/providers/query-provider.tsx +17 -0
  46. package/templates/next-base/src/components/ui/button.tsx +16 -0
  47. package/templates/next-base/src/example/api/use-example.ts +21 -0
  48. package/templates/next-base/src/example/api/use-mutations.ts +20 -0
  49. package/templates/next-base/src/example/components/example-table.tsx +66 -0
  50. package/templates/next-base/src/example/services.ts +9 -0
  51. package/templates/next-base/src/example/validations.ts +8 -0
  52. package/templates/next-base/src/features/auth/components/account-panel.tsx +62 -0
  53. package/templates/next-base/src/features/auth/components/sign-in-form.tsx +77 -0
  54. package/templates/next-base/src/features/auth/validations.ts +8 -0
  55. package/templates/next-base/src/hooks/index.ts +1 -0
  56. package/templates/next-base/src/i18n/request.ts +8 -0
  57. package/templates/next-base/src/lib/api-response.ts +49 -0
  58. package/templates/next-base/src/lib/auth-client.ts +7 -0
  59. package/templates/next-base/src/lib/auth-cookies.ts +15 -0
  60. package/templates/next-base/src/lib/auth.ts +20 -0
  61. package/templates/next-base/src/lib/axios-instance.ts +140 -0
  62. package/templates/next-base/src/lib/prisma.ts +13 -0
  63. package/templates/next-base/src/lib/token-store.ts +13 -0
  64. package/templates/next-base/src/types/index.ts +40 -0
  65. package/templates/next-base/src/utils/cn.ts +6 -0
  66. package/templates/next-base/tsconfig.json +24 -0
package/package.json ADDED
@@ -0,0 +1,43 @@
1
+ {
2
+ "name": "@thinhnguyencth1204/nextcli",
3
+ "version": "0.1.0",
4
+ "description": "CLI scaffolder for outsourced Next.js projects",
5
+ "type": "module",
6
+ "bin": {
7
+ "nextcli": "dist/cli.js"
8
+ },
9
+ "files": [
10
+ "dist",
11
+ "templates",
12
+ "README.md"
13
+ ],
14
+ "scripts": {
15
+ "build": "tsup",
16
+ "dev": "tsup --watch",
17
+ "typecheck": "tsc --noEmit",
18
+ "smoke": "node dist/cli.js --help",
19
+ "prepublishOnly": "npm run build"
20
+ },
21
+ "keywords": [
22
+ "cli",
23
+ "nextjs",
24
+ "scaffold",
25
+ "template"
26
+ ],
27
+ "license": "MIT",
28
+ "publishConfig": {
29
+ "access": "public"
30
+ },
31
+ "engines": {
32
+ "node": ">=18.18.0"
33
+ },
34
+ "dependencies": {
35
+ "@clack/prompts": "^0.7.0",
36
+ "commander": "^12.1.0"
37
+ },
38
+ "devDependencies": {
39
+ "@types/node": "^22.15.29",
40
+ "tsup": "^8.1.0",
41
+ "typescript": "^5.5.4"
42
+ }
43
+ }
@@ -0,0 +1,40 @@
1
+ import { fail, ok } from "@/lib/api-response";
2
+
3
+ export async function GET(request: Request) {
4
+ const { searchParams } = new URL(request.url);
5
+ const conversationId = searchParams.get("conversationId");
6
+ if (!conversationId) {
7
+ return fail("VALIDATION_ERROR", "conversationId is required.", { status: 400 });
8
+ }
9
+
10
+ return ok({
11
+ conversationId,
12
+ items: [],
13
+ });
14
+ }
15
+
16
+ export async function POST(request: Request) {
17
+ const payload = await request.json().catch(() => null);
18
+ const message = typeof payload?.message === "string" ? payload.message.trim() : "";
19
+ const conversationId =
20
+ typeof payload?.conversationId === "string" ? payload.conversationId.trim() : "";
21
+ const senderId = typeof payload?.senderId === "string" ? payload.senderId.trim() : "";
22
+
23
+ if (!conversationId || !senderId || !message) {
24
+ return fail(
25
+ "VALIDATION_ERROR",
26
+ "conversationId, senderId, and message are required.",
27
+ { status: 400 },
28
+ );
29
+ }
30
+
31
+ return ok({
32
+ sent: true,
33
+ message: {
34
+ conversationId,
35
+ senderId,
36
+ content: message,
37
+ kind: "TEXT",
38
+ },
39
+ });
40
+ }
@@ -0,0 +1,18 @@
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
+ export function useChatHistory(conversationId: string) {
8
+ return useQuery({
9
+ queryKey: ["chat", "history", conversationId],
10
+ enabled: Boolean(conversationId),
11
+ queryFn: async () => {
12
+ const { data } = await api.get("/api/v1/chat", {
13
+ params: { conversationId },
14
+ });
15
+ return (data as ApiSuccess<{ items: unknown[] }>).data.items;
16
+ },
17
+ });
18
+ }
@@ -0,0 +1,15 @@
1
+ "use client";
2
+
3
+ import { useEffect } from "react";
4
+
5
+ export function useRealtimeSync(enabled: boolean) {
6
+ useEffect(() => {
7
+ if (!enabled) {
8
+ return;
9
+ }
10
+
11
+ // Replace with your WebSocket/SSE implementation.
12
+ const interval = setInterval(() => {}, 5000);
13
+ return () => clearInterval(interval);
14
+ }, [enabled]);
15
+ }
@@ -0,0 +1,35 @@
1
+ "use client";
2
+
3
+ import { useMutation, useQueryClient } from "@tanstack/react-query";
4
+ import { api } from "@/lib/axios-instance";
5
+ import type { ApiSuccess } from "@/types";
6
+
7
+ type SendMessageInput = {
8
+ conversationId: string;
9
+ senderId: string;
10
+ message: string;
11
+ };
12
+
13
+ export function useSendMessage() {
14
+ const queryClient = useQueryClient();
15
+
16
+ return useMutation({
17
+ mutationFn: async (payload: SendMessageInput) => {
18
+ const { data } = await api.post("/api/v1/chat", payload);
19
+ return (data as ApiSuccess<{
20
+ sent: boolean;
21
+ message: {
22
+ conversationId: string;
23
+ senderId: string;
24
+ content: string;
25
+ kind: string;
26
+ };
27
+ }>).data;
28
+ },
29
+ onSuccess: async (_data, variables) => {
30
+ await queryClient.invalidateQueries({
31
+ queryKey: ["chat", "history", variables.conversationId],
32
+ });
33
+ },
34
+ });
35
+ }
@@ -0,0 +1,40 @@
1
+ "use client";
2
+
3
+ import { useState } from "react";
4
+ import { useChatHistory } from "@/features/chat/api/use-chat-history";
5
+ import { useSendMessage } from "@/features/chat/api/use-send-message";
6
+
7
+ export function ChatWidget() {
8
+ const defaultConversationId = "default-conversation";
9
+ const defaultSenderId = "demo-user";
10
+ const [message, setMessage] = useState("");
11
+ const history = useChatHistory(defaultConversationId);
12
+ const sendMessage = useSendMessage();
13
+
14
+ return (
15
+ <aside style={{ position: "fixed", right: 16, bottom: 16, width: 320, border: "1px solid #ccc", padding: 12 }}>
16
+ <h3>Chat</h3>
17
+ <div style={{ minHeight: 120, maxHeight: 180, overflowY: "auto" }}>
18
+ <pre>{JSON.stringify(history.data ?? [], null, 2)}</pre>
19
+ </div>
20
+ <form
21
+ onSubmit={(event) => {
22
+ event.preventDefault();
23
+ sendMessage.mutate({
24
+ conversationId: defaultConversationId,
25
+ senderId: defaultSenderId,
26
+ message,
27
+ });
28
+ setMessage("");
29
+ }}
30
+ >
31
+ <input
32
+ value={message}
33
+ onChange={(event) => setMessage(event.target.value)}
34
+ placeholder="Type message..."
35
+ style={{ width: "100%" }}
36
+ />
37
+ </form>
38
+ </aside>
39
+ );
40
+ }
@@ -0,0 +1,27 @@
1
+ export type ChatHistoryItem = {
2
+ id: string;
3
+ conversationId: string;
4
+ senderId: string;
5
+ content: string;
6
+ kind: "TEXT" | "SYSTEM";
7
+ createdAt: string;
8
+ };
9
+
10
+ export async function getChatHistory(conversationId: string): Promise<ChatHistoryItem[]> {
11
+ void conversationId;
12
+ return [];
13
+ }
14
+
15
+ export async function sendMessage(input: {
16
+ conversationId: string;
17
+ senderId: string;
18
+ message: string;
19
+ }) {
20
+ return {
21
+ id: crypto.randomUUID(),
22
+ conversationId: input.conversationId,
23
+ senderId: input.senderId,
24
+ content: input.message,
25
+ kind: "TEXT" as const,
26
+ };
27
+ }
@@ -0,0 +1,3 @@
1
+ User-agent: *
2
+ Allow: /
3
+ Sitemap: http://localhost:3000/sitemap.xml
@@ -0,0 +1,6 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
3
+ <url>
4
+ <loc>http://localhost:3000</loc>
5
+ </url>
6
+ </urlset>
@@ -0,0 +1,13 @@
1
+ import type { MetadataRoute } from "next";
2
+
3
+ export default function robots(): MetadataRoute.Robots {
4
+ const url = process.env.NEXT_PUBLIC_APP_URL ?? "http://localhost:3000";
5
+
6
+ return {
7
+ rules: {
8
+ userAgent: "*",
9
+ allow: "/",
10
+ },
11
+ sitemap: `${url}/sitemap.xml`,
12
+ };
13
+ }
@@ -0,0 +1,21 @@
1
+ import type { MetadataRoute } from "next";
2
+
3
+ export default function sitemap(): MetadataRoute.Sitemap {
4
+ const url = process.env.NEXT_PUBLIC_APP_URL ?? "http://localhost:3000";
5
+ const now = new Date();
6
+
7
+ return [
8
+ {
9
+ url,
10
+ lastModified: now,
11
+ changeFrequency: "daily",
12
+ priority: 1,
13
+ },
14
+ {
15
+ url: `${url}/example`,
16
+ lastModified: now,
17
+ changeFrequency: "daily",
18
+ priority: 0.8,
19
+ },
20
+ ];
21
+ }
@@ -0,0 +1,14 @@
1
+ type JsonLdProps = {
2
+ data: Record<string, unknown>;
3
+ };
4
+
5
+ export function JsonLd({ data }: JsonLdProps) {
6
+ return (
7
+ <script
8
+ type="application/ld+json"
9
+ dangerouslySetInnerHTML={{
10
+ __html: JSON.stringify(data),
11
+ }}
12
+ />
13
+ );
14
+ }
@@ -0,0 +1,9 @@
1
+ import { createClient } from "@supabase/supabase-js";
2
+
3
+ const url = process.env.NEXT_PUBLIC_SUPABASE_URL;
4
+ const anonKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY;
5
+
6
+ export const supabase =
7
+ url && anonKey
8
+ ? createClient(url, anonKey)
9
+ : null;
@@ -0,0 +1,69 @@
1
+ export const DEFAULT_STORAGE_BUCKET =
2
+ process.env.NEXT_PUBLIC_SUPABASE_STORAGE_BUCKET?.trim() || "public";
3
+
4
+ export const STORAGE_RESOURCE_CATEGORIES = [
5
+ "images",
6
+ "documents",
7
+ "avatars",
8
+ "misc",
9
+ ] as const;
10
+
11
+ export type StorageResourceCategory =
12
+ (typeof STORAGE_RESOURCE_CATEGORIES)[number];
13
+
14
+ const CATEGORY_DEFAULTS: Record<
15
+ StorageResourceCategory,
16
+ { maxBytes: number; allowedMimePrefixes: string[] }
17
+ > = {
18
+ images: { maxBytes: 5 * 1024 * 1024, allowedMimePrefixes: ["image/"] },
19
+ documents: {
20
+ maxBytes: 10 * 1024 * 1024,
21
+ allowedMimePrefixes: [
22
+ "application/pdf",
23
+ "application/msword",
24
+ "application/vnd.",
25
+ "text/",
26
+ ],
27
+ },
28
+ avatars: { maxBytes: 2 * 1024 * 1024, allowedMimePrefixes: ["image/"] },
29
+ misc: { maxBytes: 10 * 1024 * 1024, allowedMimePrefixes: [] },
30
+ };
31
+
32
+ export function getStorageCategoryDefaults(category: StorageResourceCategory) {
33
+ return CATEGORY_DEFAULTS[category];
34
+ }
35
+
36
+ export function sanitizeStorageFileName(fileName: string): string {
37
+ const base = fileName.split(/[/\\]/).pop() ?? "file";
38
+ const sanitized = base
39
+ .normalize("NFKD")
40
+ .replace(/[^\w.\-]+/g, "-")
41
+ .replace(/-+/g, "-")
42
+ .replace(/^-+|-+$/g, "")
43
+ .slice(0, 120);
44
+
45
+ return sanitized || "file";
46
+ }
47
+
48
+ function sanitizeStorageScope(scope: string): string {
49
+ const sanitized = scope
50
+ .trim()
51
+ .replace(/[^a-zA-Z0-9_-]/g, "-")
52
+ .replace(/-+/g, "-")
53
+ .slice(0, 64);
54
+
55
+ return sanitized || "shared";
56
+ }
57
+
58
+ export function buildStorageObjectPath(input: {
59
+ scope: string;
60
+ category: StorageResourceCategory;
61
+ fileName: string;
62
+ uniqueId?: string;
63
+ }): string {
64
+ const scope = sanitizeStorageScope(input.scope);
65
+ const uniqueId = input.uniqueId ?? crypto.randomUUID();
66
+ const safeName = sanitizeStorageFileName(input.fileName);
67
+
68
+ return `${scope}/${input.category}/${uniqueId}-${safeName}`;
69
+ }
@@ -0,0 +1,167 @@
1
+ import { supabase } from "@/lib/supabase/client";
2
+ import {
3
+ DEFAULT_STORAGE_BUCKET,
4
+ buildStorageObjectPath,
5
+ getStorageCategoryDefaults,
6
+ type StorageResourceCategory,
7
+ } from "@/lib/supabase/storage-config";
8
+
9
+ export type UploadResourceInput = {
10
+ file: File | Blob;
11
+ fileName: string;
12
+ /** Tenant/user/feature namespace, e.g. user id or `shared`. */
13
+ scope: string;
14
+ category?: StorageResourceCategory;
15
+ bucket?: string;
16
+ upsert?: boolean;
17
+ cacheControl?: string;
18
+ contentType?: string;
19
+ };
20
+
21
+ export type UploadResourceResult = {
22
+ bucket: string;
23
+ path: string;
24
+ publicUrl: string;
25
+ };
26
+
27
+ export class StorageHelperError extends Error {
28
+ constructor(
29
+ message: string,
30
+ readonly code:
31
+ | "NOT_CONFIGURED"
32
+ | "VALIDATION"
33
+ | "UPLOAD_FAILED"
34
+ | "REMOVE_FAILED",
35
+ readonly cause?: unknown,
36
+ ) {
37
+ super(message);
38
+ this.name = "StorageHelperError";
39
+ }
40
+ }
41
+
42
+ function getClient() {
43
+ if (!supabase) {
44
+ throw new StorageHelperError(
45
+ "Supabase is not configured. Set NEXT_PUBLIC_SUPABASE_URL and NEXT_PUBLIC_SUPABASE_ANON_KEY.",
46
+ "NOT_CONFIGURED",
47
+ );
48
+ }
49
+
50
+ return supabase;
51
+ }
52
+
53
+ function resolveContentType(
54
+ file: File | Blob,
55
+ override?: string,
56
+ ): string | undefined {
57
+ if (override) {
58
+ return override;
59
+ }
60
+
61
+ if (file instanceof File && file.type) {
62
+ return file.type;
63
+ }
64
+
65
+ return undefined;
66
+ }
67
+
68
+ function assertCategoryRules(
69
+ category: StorageResourceCategory,
70
+ file: File | Blob,
71
+ contentType?: string,
72
+ ): void {
73
+ const { maxBytes, allowedMimePrefixes } =
74
+ getStorageCategoryDefaults(category);
75
+
76
+ if (file.size > maxBytes) {
77
+ throw new StorageHelperError(
78
+ `File exceeds ${category} limit of ${maxBytes} bytes.`,
79
+ "VALIDATION",
80
+ );
81
+ }
82
+
83
+ if (allowedMimePrefixes.length === 0) {
84
+ return;
85
+ }
86
+
87
+ const mime = contentType ?? (file instanceof File ? file.type : "");
88
+
89
+ if (
90
+ !mime ||
91
+ !allowedMimePrefixes.some((prefix) => mime.startsWith(prefix))
92
+ ) {
93
+ throw new StorageHelperError(
94
+ `File type "${mime || "unknown"}" is not allowed for category "${category}".`,
95
+ "VALIDATION",
96
+ );
97
+ }
98
+ }
99
+
100
+ export function getResourcePublicUrl(
101
+ path: string,
102
+ bucket: string = DEFAULT_STORAGE_BUCKET,
103
+ ): string {
104
+ const client = getClient();
105
+ const { data } = client.storage.from(bucket).getPublicUrl(path);
106
+
107
+ return data.publicUrl;
108
+ }
109
+
110
+ export async function uploadResource(
111
+ input: UploadResourceInput,
112
+ ): Promise<UploadResourceResult> {
113
+ const client = getClient();
114
+ const bucket = input.bucket ?? DEFAULT_STORAGE_BUCKET;
115
+ const category = input.category ?? "images";
116
+ const contentType = resolveContentType(input.file, input.contentType);
117
+
118
+ assertCategoryRules(category, input.file, contentType);
119
+
120
+ const path = buildStorageObjectPath({
121
+ scope: input.scope,
122
+ category,
123
+ fileName: input.fileName,
124
+ });
125
+
126
+ const { error } = await client.storage.from(bucket).upload(path, input.file, {
127
+ contentType,
128
+ cacheControl: input.cacheControl ?? "3600",
129
+ upsert: input.upsert ?? false,
130
+ });
131
+
132
+ if (error) {
133
+ throw new StorageHelperError(
134
+ error.message || "Upload failed.",
135
+ "UPLOAD_FAILED",
136
+ error,
137
+ );
138
+ }
139
+
140
+ return {
141
+ bucket,
142
+ path,
143
+ publicUrl: getResourcePublicUrl(path, bucket),
144
+ };
145
+ }
146
+
147
+ export async function uploadImage(
148
+ input: Omit<UploadResourceInput, "category">,
149
+ ): Promise<UploadResourceResult> {
150
+ return uploadResource({ ...input, category: "images" });
151
+ }
152
+
153
+ export async function removeResource(
154
+ path: string,
155
+ bucket: string = DEFAULT_STORAGE_BUCKET,
156
+ ): Promise<void> {
157
+ const client = getClient();
158
+ const { error } = await client.storage.from(bucket).remove([path]);
159
+
160
+ if (error) {
161
+ throw new StorageHelperError(
162
+ error.message || "Remove failed.",
163
+ "REMOVE_FAILED",
164
+ error,
165
+ );
166
+ }
167
+ }
@@ -0,0 +1,9 @@
1
+ import { createClient } from "@supabase/supabase-js";
2
+
3
+ const url = process.env.NEXT_PUBLIC_SUPABASE_URL;
4
+ const anonKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY;
5
+
6
+ export const supabaseRealtimeClient =
7
+ url && anonKey
8
+ ? createClient(url, anonKey)
9
+ : null;
@@ -0,0 +1,19 @@
1
+ "use client";
2
+
3
+ import { useEffect } from "react";
4
+ import { supabaseRealtimeClient } from "@/features/supabase-realtime/client";
5
+
6
+ export function useSupabaseChannel(channelName: string) {
7
+ useEffect(() => {
8
+ if (!supabaseRealtimeClient || !channelName) {
9
+ return;
10
+ }
11
+
12
+ const channel = supabaseRealtimeClient.channel(channelName);
13
+ channel.subscribe();
14
+
15
+ return () => {
16
+ channel.unsubscribe();
17
+ };
18
+ }, [channelName]);
19
+ }
@@ -0,0 +1,11 @@
1
+ DATABASE_URL="postgresql://postgres:postgres@localhost:5432/__PROJECT_NAME__"
2
+ BETTER_AUTH_SECRET="__BETTER_AUTH_SECRET__"
3
+ BETTER_AUTH_URL="http://localhost:3000"
4
+
5
+ NEXT_PUBLIC_APP_URL="http://localhost:3000"
6
+ NEXT_PUBLIC_ENABLE_CHAT=__ENABLE_CHAT__
7
+ NEXT_PUBLIC_DEFAULT_LOCALE="en"
8
+ GOOGLE_CLIENT_ID=""
9
+ GOOGLE_CLIENT_SECRET=""
10
+ FACEBOOK_CLIENT_ID=""
11
+ FACEBOOK_CLIENT_SECRET=""
@@ -0,0 +1,11 @@
1
+ DATABASE_URL="postgresql://postgres:postgres@localhost:5432/__PROJECT_NAME___dev"
2
+ BETTER_AUTH_SECRET="__BETTER_AUTH_SECRET__"
3
+ BETTER_AUTH_URL="http://localhost:3000"
4
+
5
+ NEXT_PUBLIC_APP_URL="http://localhost:3000"
6
+ NEXT_PUBLIC_ENABLE_CHAT=__ENABLE_CHAT__
7
+ NEXT_PUBLIC_DEFAULT_LOCALE="en"
8
+ GOOGLE_CLIENT_ID=""
9
+ GOOGLE_CLIENT_SECRET=""
10
+ FACEBOOK_CLIENT_ID=""
11
+ FACEBOOK_CLIENT_SECRET=""
@@ -0,0 +1,11 @@
1
+ DATABASE_URL="postgresql://postgres:postgres@localhost:5432/__PROJECT_NAME__"
2
+ BETTER_AUTH_SECRET="your-32-plus-char-random-secret"
3
+ BETTER_AUTH_URL="http://localhost:3000"
4
+
5
+ NEXT_PUBLIC_APP_URL="http://localhost:3000"
6
+ NEXT_PUBLIC_ENABLE_CHAT=__ENABLE_CHAT__
7
+ NEXT_PUBLIC_DEFAULT_LOCALE="en"
8
+ GOOGLE_CLIENT_ID=""
9
+ GOOGLE_CLIENT_SECRET=""
10
+ FACEBOOK_CLIENT_ID=""
11
+ FACEBOOK_CLIENT_SECRET=""
@@ -0,0 +1,20 @@
1
+ import js from "@eslint/js";
2
+ import nextPlugin from "eslint-config-next";
3
+
4
+ const config = [
5
+ js.configs.recommended,
6
+ ...nextPlugin,
7
+ {
8
+ ignores: [".next/**", "node_modules/**", "dist/**"],
9
+ },
10
+ {
11
+ files: ["**/*.{ts,tsx}"],
12
+ rules: {
13
+ "no-undef": "off",
14
+ "no-unused-vars": "off",
15
+ "react-hooks/incompatible-library": "off",
16
+ },
17
+ },
18
+ ];
19
+
20
+ export default config;
@@ -0,0 +1,10 @@
1
+ import createMiddleware from "next-intl/middleware";
2
+
3
+ export default createMiddleware({
4
+ locales: ["en", "vi"],
5
+ defaultLocale: "en",
6
+ });
7
+
8
+ export const config = {
9
+ matcher: ["/((?!api|_next|.*\\..*).*)"],
10
+ };
@@ -0,0 +1,4 @@
1
+ /// <reference types="next" />
2
+ /// <reference types="next/image-types/global" />
3
+
4
+ // This file is auto-generated by Next.js and should not be edited manually.
@@ -0,0 +1,7 @@
1
+ import type { NextConfig } from "next";
2
+
3
+ const nextConfig: NextConfig = {
4
+ reactStrictMode: true,
5
+ };
6
+
7
+ export default nextConfig;