nextjs-hackathon-stack 0.1.10 → 0.1.12

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 (35) hide show
  1. package/dist/index.js +7 -0
  2. package/package.json +1 -1
  3. package/template/.husky/pre-commit +1 -0
  4. package/template/_env.example +7 -5
  5. package/template/package.json.tmpl +8 -2
  6. package/template/src/app/(protected)/page.tsx +9 -1
  7. package/template/src/app/__tests__/auth-callback.test.ts +72 -0
  8. package/template/src/app/__tests__/error.test.tsx +71 -0
  9. package/template/src/app/__tests__/layout.test.tsx +22 -0
  10. package/template/src/app/__tests__/login-page.test.tsx +34 -0
  11. package/template/src/app/__tests__/protected-layout.test.tsx +43 -0
  12. package/template/src/app/__tests__/protected-page.test.tsx +41 -0
  13. package/template/src/app/api/chat/route.ts +3 -1
  14. package/template/src/features/auth/__tests__/login-form.test.tsx +53 -2
  15. package/template/src/features/auth/__tests__/login.action.test.ts +82 -0
  16. package/template/src/features/auth/__tests__/logout.action.test.ts +32 -0
  17. package/template/src/features/auth/actions/login.action.ts +1 -1
  18. package/template/src/features/auth/components/login-form.tsx +4 -3
  19. package/template/src/features/chat/__tests__/chat-ui.test.tsx +97 -7
  20. package/template/src/features/chat/__tests__/route.test.ts +66 -3
  21. package/template/src/features/chat/__tests__/use-chat.test.ts +15 -0
  22. package/template/src/features/chat/components/chat-ui.tsx +1 -1
  23. package/template/src/features/tts/__tests__/route.test.ts +78 -3
  24. package/template/src/features/tts/__tests__/tts-player.test.tsx +65 -3
  25. package/template/src/features/video/__tests__/route.test.ts +78 -3
  26. package/template/src/features/video/__tests__/video-generator.test.tsx +65 -3
  27. package/template/src/shared/__tests__/middleware.test.ts +34 -0
  28. package/template/src/shared/__tests__/schema.test.ts +16 -3
  29. package/template/src/shared/__tests__/supabase-middleware.test.ts +162 -0
  30. package/template/src/shared/__tests__/supabase-server.test.ts +76 -3
  31. package/template/src/shared/__tests__/ui-button.test.tsx +52 -0
  32. package/template/src/shared/__tests__/ui-card.test.tsx +78 -0
  33. package/template/src/shared/__tests__/utils.test.ts +30 -0
  34. package/template/src/shared/lib/ai.ts +4 -4
  35. package/template/vitest.config.ts +5 -0
package/dist/index.js CHANGED
@@ -112,6 +112,13 @@ async function scaffold(projectName, skipInstall) {
112
112
  p3.log.error(stderr || "pnpm install failed");
113
113
  p3.log.info("You can install manually: cd " + projectName + " && pnpm install");
114
114
  }
115
+ spinner2.start("Setting up husky pre-commit hooks");
116
+ try {
117
+ execSync("pnpm exec husky", { cwd: targetDir, stdio: "pipe" });
118
+ spinner2.stop("Husky pre-commit hooks set up");
119
+ } catch {
120
+ spinner2.stop("Husky setup failed \u2014 run manually: pnpm exec husky");
121
+ }
115
122
  spinner2.start("Initializing shadcn/ui");
116
123
  try {
117
124
  execSync("npx shadcn@latest init --yes --defaults --force --silent", {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nextjs-hackathon-stack",
3
- "version": "0.1.10",
3
+ "version": "0.1.12",
4
4
  "description": "Scaffold a full-stack Next.js hackathon starter",
5
5
  "type": "module",
6
6
  "bin": {
@@ -0,0 +1 @@
1
+ vitest run --coverage
@@ -9,11 +9,13 @@ NEXT_PUBLIC_SUPABASE_ANON_KEY=your-anon-key
9
9
  # Supabase DB — https://supabase.com > Project Settings > Database > Connection string (URI)
10
10
  DATABASE_URL=postgresql://postgres:[password]@db.your-project-id.supabase.co:5432/postgres
11
11
 
12
- # Vercel AI Gatewayhttps://vercel.com > AI > Gateways
13
- AI_GATEWAY_API_KEY=your-ai-gateway-api-key
14
-
15
- # Optional: override the gateway base URL (defaults to https://ai-gateway.vercel.sh/v1)
16
- # AI_GATEWAY_URL=https://ai-gateway.vercel.sh/v1
12
+ # AI Providerdefaults to MiniMax direct API
13
+ AI_API_KEY=your-api-key
14
+ # Optional: override base URL (default: https://api.minimax.io/v1)
15
+ # For Vercel Gateway: https://ai-gateway.vercel.sh/v1
16
+ # AI_BASE_URL=https://api.minimax.io/v1
17
+ # Optional: override model (default: MiniMax-M2.7)
18
+ # AI_MODEL=MiniMax-M2.7
17
19
 
18
20
  # =============================================================================
19
21
  # OPTIONAL
@@ -16,7 +16,8 @@
16
16
  "test:e2e": "playwright test",
17
17
  "db:generate": "drizzle-kit generate",
18
18
  "db:migrate": "drizzle-kit migrate",
19
- "db:studio": "drizzle-kit studio"
19
+ "db:studio": "drizzle-kit studio",
20
+ "prepare": "husky"
20
21
  },
21
22
  "dependencies": {
22
23
  "next": "^15",
@@ -64,6 +65,11 @@
64
65
  "eslint-plugin-react-hooks": "^5",
65
66
  "eslint-plugin-import-x": "^4",
66
67
  "eslint-plugin-vitest": "^0.5",
67
- "eslint-plugin-playwright": "^2"
68
+ "eslint-plugin-playwright": "^2",
69
+ "husky": "^9",
70
+ "lint-staged": "^15"
71
+ },
72
+ "lint-staged": {
73
+ "*.{ts,tsx}": ["vitest related --run"]
68
74
  }
69
75
  }
@@ -1,4 +1,5 @@
1
1
  import { ChatUi } from "@/features/chat/components/chat-ui";
2
+ import { logoutAction } from "@/features/auth/actions/logout.action";
2
3
  import { createClient } from "@/shared/lib/supabase/server";
3
4
 
4
5
  export default async function HomePage() {
@@ -9,7 +10,14 @@ export default async function HomePage() {
9
10
 
10
11
  return (
11
12
  <main className="container mx-auto p-8">
12
- <h1 className="mb-4 text-2xl font-bold">Welcome, {user?.email}</h1>
13
+ <div className="mb-4 flex items-center justify-between">
14
+ <h1 className="text-2xl font-bold">Welcome, {user?.email}</h1>
15
+ <form action={logoutAction}>
16
+ <button type="submit" className="rounded bg-gray-200 px-4 py-2 text-sm hover:bg-gray-300">
17
+ Log out
18
+ </button>
19
+ </form>
20
+ </div>
13
21
  <ChatUi />
14
22
  </main>
15
23
  );
@@ -0,0 +1,72 @@
1
+ import { describe, it, expect, vi, beforeEach } from "vitest";
2
+
3
+ const mockExchangeCodeForSession = vi.fn();
4
+
5
+ vi.mock("@/shared/lib/supabase/server", () => ({
6
+ createClient: vi.fn(() =>
7
+ Promise.resolve({
8
+ auth: { exchangeCodeForSession: mockExchangeCodeForSession },
9
+ })
10
+ ),
11
+ }));
12
+
13
+ describe("auth callback route", () => {
14
+ beforeEach(() => {
15
+ vi.clearAllMocks();
16
+ });
17
+
18
+ it("redirects to /login with error when code is missing", async () => {
19
+ // Arrange
20
+ const { GET } = await import("../api/auth/callback/route");
21
+ const req = new Request("http://localhost/api/auth/callback");
22
+
23
+ // Act
24
+ const response = await GET(req);
25
+
26
+ // Assert
27
+ expect(response.status).toBe(307);
28
+ expect(response.headers.get("location")).toContain("/login?error=no_code");
29
+ });
30
+
31
+ it("redirects to /login with error when exchange fails", async () => {
32
+ // Arrange
33
+ mockExchangeCodeForSession.mockResolvedValue({ error: { message: "bad code" } });
34
+ const { GET } = await import("../api/auth/callback/route");
35
+ const req = new Request("http://localhost/api/auth/callback?code=bad-code");
36
+
37
+ // Act
38
+ const response = await GET(req);
39
+
40
+ // Assert
41
+ expect(response.status).toBe(307);
42
+ expect(response.headers.get("location")).toContain("/login?error=auth_error");
43
+ });
44
+
45
+ it("redirects to / on successful exchange", async () => {
46
+ // Arrange
47
+ mockExchangeCodeForSession.mockResolvedValue({ error: null });
48
+ const { GET } = await import("../api/auth/callback/route");
49
+ const req = new Request("http://localhost/api/auth/callback?code=valid-code");
50
+
51
+ // Act
52
+ const response = await GET(req);
53
+
54
+ // Assert
55
+ expect(response.status).toBe(307);
56
+ expect(response.headers.get("location")).toBe("http://localhost/");
57
+ });
58
+
59
+ it("redirects to next param on successful exchange", async () => {
60
+ // Arrange
61
+ mockExchangeCodeForSession.mockResolvedValue({ error: null });
62
+ const { GET } = await import("../api/auth/callback/route");
63
+ const req = new Request("http://localhost/api/auth/callback?code=valid-code&next=/dashboard");
64
+
65
+ // Act
66
+ const response = await GET(req);
67
+
68
+ // Assert
69
+ expect(response.status).toBe(307);
70
+ expect(response.headers.get("location")).toContain("/dashboard");
71
+ });
72
+ });
@@ -0,0 +1,71 @@
1
+ import { render, screen } from "@testing-library/react";
2
+ import userEvent from "@testing-library/user-event";
3
+ import { describe, it, expect, vi } from "vitest";
4
+
5
+ import ErrorPage from "../error";
6
+
7
+ describe("ErrorPage", () => {
8
+ it("renders error heading", () => {
9
+ // Arrange
10
+ const error = new Error("Something failed");
11
+ const reset = vi.fn();
12
+
13
+ // Act
14
+ render(<ErrorPage error={error} reset={reset} />);
15
+
16
+ // Assert
17
+ expect(screen.getByText(/something went wrong/i)).toBeInTheDocument();
18
+ });
19
+
20
+ it("renders error message", () => {
21
+ // Arrange
22
+ const error = new Error("Specific error message");
23
+ const reset = vi.fn();
24
+
25
+ // Act
26
+ render(<ErrorPage error={error} reset={reset} />);
27
+
28
+ // Assert
29
+ expect(screen.getByText("Specific error message")).toBeInTheDocument();
30
+ });
31
+
32
+ it("renders Try again button", () => {
33
+ // Arrange
34
+ const error = new Error("test");
35
+ const reset = vi.fn();
36
+
37
+ // Act
38
+ render(<ErrorPage error={error} reset={reset} />);
39
+
40
+ // Assert
41
+ expect(screen.getByRole("button", { name: /try again/i })).toBeInTheDocument();
42
+ });
43
+
44
+ it("calls reset when Try again is clicked", async () => {
45
+ // Arrange
46
+ const error = new Error("test");
47
+ const reset = vi.fn();
48
+ const user = userEvent.setup();
49
+ render(<ErrorPage error={error} reset={reset} />);
50
+
51
+ // Act
52
+ await user.click(screen.getByRole("button", { name: /try again/i }));
53
+
54
+ // Assert
55
+ expect(reset).toHaveBeenCalledOnce();
56
+ });
57
+
58
+ it("logs error to console on mount", () => {
59
+ // Arrange
60
+ const error = new Error("logged error");
61
+ const reset = vi.fn();
62
+ const consoleSpy = vi.spyOn(console, "error").mockImplementation(() => undefined);
63
+
64
+ // Act
65
+ render(<ErrorPage error={error} reset={reset} />);
66
+
67
+ // Assert
68
+ expect(consoleSpy).toHaveBeenCalledWith(error);
69
+ consoleSpy.mockRestore();
70
+ });
71
+ });
@@ -0,0 +1,22 @@
1
+ import { render } from "@testing-library/react";
2
+ import { describe, it, expect, vi } from "vitest";
3
+
4
+ import RootLayout from "../layout";
5
+
6
+ vi.mock("@/shared/components/providers", () => ({
7
+ Providers: ({ children }: { children: React.ReactNode }) => <div>{children}</div>,
8
+ }));
9
+
10
+ describe("RootLayout", () => {
11
+ it("renders children inside body", () => {
12
+ // Arrange + Act
13
+ const { container } = render(
14
+ <RootLayout>
15
+ <p data-testid="child-content">Hello</p>
16
+ </RootLayout>
17
+ );
18
+
19
+ // Assert
20
+ expect(container.querySelector("[data-testid='child-content']")).toBeTruthy();
21
+ });
22
+ });
@@ -0,0 +1,34 @@
1
+ import { render, screen } from "@testing-library/react";
2
+ import { describe, it, expect, vi } from "vitest";
3
+
4
+ import LoginPage from "../(auth)/login/page";
5
+
6
+ vi.mock("@/features/auth/components/login-form", () => ({
7
+ LoginForm: () => <div data-testid="login-form" />,
8
+ }));
9
+
10
+ describe("LoginPage", () => {
11
+ it("renders the welcome heading", () => {
12
+ // Arrange + Act
13
+ render(<LoginPage />);
14
+
15
+ // Assert
16
+ expect(screen.getByText(/welcome back/i)).toBeInTheDocument();
17
+ });
18
+
19
+ it("renders the LoginForm component", () => {
20
+ // Arrange + Act
21
+ render(<LoginPage />);
22
+
23
+ // Assert
24
+ expect(screen.getByTestId("login-form")).toBeInTheDocument();
25
+ });
26
+
27
+ it("renders the sign-in description", () => {
28
+ // Arrange + Act
29
+ render(<LoginPage />);
30
+
31
+ // Assert
32
+ expect(screen.getByText(/sign in to your account/i)).toBeInTheDocument();
33
+ });
34
+ });
@@ -0,0 +1,43 @@
1
+ import { describe, it, expect, vi } from "vitest";
2
+
3
+ const mockGetUser = vi.fn();
4
+ const mockRedirect = vi.fn();
5
+
6
+ vi.mock("@/shared/lib/supabase/server", () => ({
7
+ createClient: vi.fn(() =>
8
+ Promise.resolve({
9
+ auth: { getUser: mockGetUser },
10
+ })
11
+ ),
12
+ }));
13
+
14
+ vi.mock("next/navigation", () => ({
15
+ redirect: (url: string) => mockRedirect(url),
16
+ }));
17
+
18
+ describe("ProtectedLayout", () => {
19
+ it("renders children when user is authenticated", async () => {
20
+ // Arrange
21
+ mockGetUser.mockResolvedValue({ data: { user: { email: "user@example.com" } } });
22
+ const { default: ProtectedLayout } = await import("../(protected)/layout");
23
+
24
+ // Act
25
+ const result = await ProtectedLayout({ children: <div data-testid="child" /> });
26
+
27
+ // Assert
28
+ expect(result).toBeTruthy();
29
+ expect(mockRedirect).not.toHaveBeenCalled();
30
+ });
31
+
32
+ it("redirects to /login when user is not authenticated", async () => {
33
+ // Arrange
34
+ mockGetUser.mockResolvedValue({ data: { user: null } });
35
+ const { default: ProtectedLayout } = await import("../(protected)/layout");
36
+
37
+ // Act
38
+ await ProtectedLayout({ children: <div /> });
39
+
40
+ // Assert
41
+ expect(mockRedirect).toHaveBeenCalledWith("/login");
42
+ });
43
+ });
@@ -0,0 +1,41 @@
1
+ import { describe, it, expect, vi } from "vitest";
2
+
3
+ const mockGetUser = vi.fn();
4
+
5
+ vi.mock("@/shared/lib/supabase/server", () => ({
6
+ createClient: vi.fn(() =>
7
+ Promise.resolve({
8
+ auth: { getUser: mockGetUser },
9
+ })
10
+ ),
11
+ }));
12
+
13
+ vi.mock("@/features/chat/components/chat-ui", () => ({
14
+ ChatUi: () => <div data-testid="chat-ui" />,
15
+ }));
16
+
17
+ describe("HomePage (protected)", () => {
18
+ it("renders welcome message with user email", async () => {
19
+ // Arrange
20
+ mockGetUser.mockResolvedValue({ data: { user: { email: "user@example.com" } } });
21
+ const { default: HomePage } = await import("../(protected)/page");
22
+
23
+ // Act
24
+ const result = await HomePage();
25
+
26
+ // Assert
27
+ expect(result).toBeTruthy();
28
+ });
29
+
30
+ it("renders with null user gracefully", async () => {
31
+ // Arrange
32
+ mockGetUser.mockResolvedValue({ data: { user: null } });
33
+ const { default: HomePage } = await import("../(protected)/page");
34
+
35
+ // Act
36
+ const result = await HomePage();
37
+
38
+ // Assert
39
+ expect(result).toBeTruthy();
40
+ });
41
+ });
@@ -16,7 +16,9 @@ export async function POST(req: Request) {
16
16
  return result.toDataStreamResponse({
17
17
  getErrorMessage: (error) => {
18
18
  console.error("[chat] stream error:", error);
19
- return error instanceof Error ? error.message : "AI request failed";
19
+ if (error instanceof Error) return error.message;
20
+ if (typeof error === "object" && error !== null) return JSON.stringify(error);
21
+ return String(error);
20
22
  },
21
23
  });
22
24
  }
@@ -1,16 +1,19 @@
1
- import { render, screen } from "@testing-library/react";
1
+ import { render, screen, waitFor } from "@testing-library/react";
2
2
  import userEvent from "@testing-library/user-event";
3
3
  import { describe, it, expect, vi, beforeEach } from "vitest";
4
4
 
5
5
  import { LoginForm } from "../components/login-form";
6
6
 
7
+ const mockLoginAction = vi.fn().mockResolvedValue({ status: "idle" });
8
+
7
9
  vi.mock("../actions/login.action", () => ({
8
- loginAction: vi.fn().mockResolvedValue({ status: "idle" }),
10
+ loginAction: (...args: unknown[]) => mockLoginAction(...args),
9
11
  }));
10
12
 
11
13
  describe("LoginForm", () => {
12
14
  beforeEach(() => {
13
15
  vi.clearAllMocks();
16
+ mockLoginAction.mockResolvedValue({ status: "idle" });
14
17
  });
15
18
 
16
19
  it("renders email and password fields", () => {
@@ -41,4 +44,52 @@ describe("LoginForm", () => {
41
44
  // Assert
42
45
  expect(await screen.findByText(/invalid email/i)).toBeInTheDocument();
43
46
  });
47
+
48
+ it("shows validation error for short password", async () => {
49
+ // Arrange
50
+ const user = userEvent.setup();
51
+ render(<LoginForm />);
52
+
53
+ // Act
54
+ await user.type(screen.getByLabelText(/email/i), "user@example.com");
55
+ await user.type(screen.getByLabelText(/password/i), "short");
56
+ await user.click(screen.getByRole("button", { name: /sign in/i }));
57
+
58
+ // Assert
59
+ expect(await screen.findByText(/at least 8 characters/i)).toBeInTheDocument();
60
+ });
61
+
62
+ it("submits with valid credentials and calls loginAction", async () => {
63
+ // Arrange
64
+ const user = userEvent.setup();
65
+ render(<LoginForm />);
66
+
67
+ // Act
68
+ await user.type(screen.getByLabelText(/email/i), "user@example.com");
69
+ await user.type(screen.getByLabelText(/password/i), "password123");
70
+ await user.click(screen.getByRole("button", { name: /sign in/i }));
71
+
72
+ // Assert
73
+ await waitFor(() => {
74
+ expect(mockLoginAction).toHaveBeenCalled();
75
+ });
76
+ });
77
+
78
+ it("displays server-side error message when action returns error", async () => {
79
+ // Arrange
80
+ mockLoginAction.mockResolvedValue({ status: "error", message: "Invalid credentials" });
81
+ const user = userEvent.setup();
82
+ render(<LoginForm />);
83
+
84
+ // Act
85
+ await user.type(screen.getByLabelText(/email/i), "user@example.com");
86
+ await user.type(screen.getByLabelText(/password/i), "password123");
87
+ await user.click(screen.getByRole("button", { name: /sign in/i }));
88
+
89
+ // Assert
90
+ await waitFor(() => {
91
+ expect(screen.getByRole("alert")).toBeInTheDocument();
92
+ expect(screen.getByText(/invalid credentials/i)).toBeInTheDocument();
93
+ });
94
+ });
44
95
  });
@@ -0,0 +1,82 @@
1
+ import { describe, it, expect, vi, beforeEach } from "vitest";
2
+
3
+ const mockSignInWithPassword = vi.fn();
4
+ const mockRedirect = vi.fn();
5
+
6
+ vi.mock("@/shared/lib/supabase/server", () => ({
7
+ createClient: vi.fn(() =>
8
+ Promise.resolve({
9
+ auth: { signInWithPassword: mockSignInWithPassword },
10
+ })
11
+ ),
12
+ }));
13
+
14
+ vi.mock("next/navigation", () => ({
15
+ redirect: (url: string) => mockRedirect(url),
16
+ }));
17
+
18
+ describe("loginAction", () => {
19
+ beforeEach(() => {
20
+ vi.clearAllMocks();
21
+ });
22
+
23
+ it("returns error when email is invalid", async () => {
24
+ // Arrange
25
+ const { loginAction } = await import("../actions/login.action");
26
+ const fd = new FormData();
27
+ fd.set("email", "not-an-email");
28
+ fd.set("password", "password123");
29
+
30
+ // Act
31
+ const result = await loginAction({ status: "idle" }, fd);
32
+
33
+ // Assert
34
+ expect(result.status).toBe("error");
35
+ });
36
+
37
+ it("returns error when password is too short", async () => {
38
+ // Arrange
39
+ const { loginAction } = await import("../actions/login.action");
40
+ const fd = new FormData();
41
+ fd.set("email", "user@example.com");
42
+ fd.set("password", "short");
43
+
44
+ // Act
45
+ const result = await loginAction({ status: "idle" }, fd);
46
+
47
+ // Assert
48
+ expect(result.status).toBe("error");
49
+ });
50
+
51
+ it("returns error on auth failure", async () => {
52
+ // Arrange
53
+ mockSignInWithPassword.mockResolvedValue({
54
+ error: { message: "Invalid credentials" },
55
+ });
56
+ const { loginAction } = await import("../actions/login.action");
57
+ const fd = new FormData();
58
+ fd.set("email", "user@example.com");
59
+ fd.set("password", "password123");
60
+
61
+ // Act
62
+ const result = await loginAction({ status: "idle" }, fd);
63
+
64
+ // Assert
65
+ expect(result).toEqual({ status: "error", message: "Invalid credentials" });
66
+ });
67
+
68
+ it("redirects to / on successful login", async () => {
69
+ // Arrange
70
+ mockSignInWithPassword.mockResolvedValue({ error: null });
71
+ const { loginAction } = await import("../actions/login.action");
72
+ const fd = new FormData();
73
+ fd.set("email", "user@example.com");
74
+ fd.set("password", "password123");
75
+
76
+ // Act
77
+ await loginAction({ status: "idle" }, fd);
78
+
79
+ // Assert
80
+ expect(mockRedirect).toHaveBeenCalledWith("/");
81
+ });
82
+ });
@@ -0,0 +1,32 @@
1
+ import { describe, it, expect, vi, beforeEach } from "vitest";
2
+
3
+ const mockSignOut = vi.fn();
4
+ const mockRedirect = vi.fn();
5
+
6
+ vi.mock("@/shared/lib/supabase/server", () => ({
7
+ createClient: vi.fn(() =>
8
+ Promise.resolve({
9
+ auth: { signOut: mockSignOut },
10
+ })
11
+ ),
12
+ }));
13
+
14
+ vi.mock("next/navigation", () => ({
15
+ redirect: (url: string) => mockRedirect(url),
16
+ }));
17
+
18
+ describe("logoutAction", () => {
19
+ beforeEach(() => {
20
+ vi.clearAllMocks();
21
+ mockSignOut.mockResolvedValue({});
22
+ });
23
+
24
+ it("calls signOut and redirects to /login", async () => {
25
+ const { logoutAction } = await import("../actions/logout.action");
26
+
27
+ await logoutAction();
28
+
29
+ expect(mockSignOut).toHaveBeenCalled();
30
+ expect(mockRedirect).toHaveBeenCalledWith("/login");
31
+ });
32
+ });
@@ -25,7 +25,7 @@ export async function loginAction(
25
25
  });
26
26
 
27
27
  if (!parsed.success) {
28
- return { status: "error", message: parsed.error.issues[0]?.message ?? "Invalid input" };
28
+ return { status: "error", message: parsed.error.issues[0]!.message };
29
29
  }
30
30
 
31
31
  const supabase = await createClient();
@@ -1,7 +1,7 @@
1
1
  "use client";
2
2
 
3
3
  import { zodResolver } from "@hookform/resolvers/zod";
4
- import { useActionState } from "react";
4
+ import { useActionState, useTransition } from "react";
5
5
  import { useForm } from "react-hook-form";
6
6
  import { z } from "zod";
7
7
 
@@ -22,7 +22,8 @@ type FormValues = z.infer<typeof schema>;
22
22
  const initialState: LoginActionState = { status: "idle" };
23
23
 
24
24
  export function LoginForm() {
25
- const [state, formAction, isPending] = useActionState(loginAction, initialState);
25
+ const [state, formAction] = useActionState(loginAction, initialState);
26
+ const [isPending, startTransition] = useTransition();
26
27
 
27
28
  const {
28
29
  register,
@@ -34,7 +35,7 @@ export function LoginForm() {
34
35
  const fd = new FormData();
35
36
  fd.set("email", data.email);
36
37
  fd.set("password", data.password);
37
- formAction(fd);
38
+ startTransition(() => formAction(fd));
38
39
  });
39
40
 
40
41
  return (