nextjs-hackathon-stack 0.1.23 → 0.1.24

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nextjs-hackathon-stack",
3
- "version": "0.1.23",
3
+ "version": "0.1.24",
4
4
  "description": "Scaffold a full-stack Next.js hackathon starter",
5
5
  "type": "module",
6
6
  "bin": {
@@ -61,6 +61,10 @@ export default tseslint.config(
61
61
  },
62
62
  },
63
63
  {
64
- ignores: [".next/**", "node_modules/**", "dist/**", "eslint.config.ts", "next-env.d.ts", "next.config.ts", "playwright.config.ts", "drizzle.config.ts", "vitest.config.ts"],
64
+ files: ["**/error.tsx"],
65
+ rules: { "no-console": "off" },
66
+ },
67
+ {
68
+ ignores: [".next/**", "node_modules/**", "dist/**", "eslint.config.ts", "next-env.d.ts", "next.config.ts", "playwright.config.ts", "drizzle.config.ts", "vitest.config.ts", "postcss.config.mjs"],
65
69
  }
66
70
  );
@@ -1,4 +1,5 @@
1
- import { type NextRequest } from "next/server";
1
+ import type { NextRequest } from "next/server";
2
+
2
3
  import { updateSession } from "@/shared/lib/supabase/middleware";
3
4
 
4
5
  export async function middleware(request: NextRequest) {
@@ -1,17 +1,20 @@
1
- import { TodoList } from "@/features/todos/components/todo-list";
2
- import { AddTodoForm } from "@/features/todos/components/add-todo-form";
1
+ import { eq } from "drizzle-orm";
2
+ import { redirect } from "next/navigation";
3
+
3
4
  import { logoutAction } from "@/features/auth/actions/logout.action";
4
- import { createClient } from "@/shared/lib/supabase/server";
5
+ import { AddTodoForm } from "@/features/todos/components/add-todo-form";
6
+ import { TodoList } from "@/features/todos/components/todo-list";
5
7
  import { db } from "@/shared/db";
6
8
  import { todos } from "@/shared/db/schema";
7
- import { eq } from "drizzle-orm";
9
+ import { createClient } from "@/shared/lib/supabase/server";
8
10
 
9
11
  export default async function HomePage() {
10
12
  const supabase = await createClient();
11
13
  const {
12
14
  data: { user },
13
15
  } = await supabase.auth.getUser();
14
- const items = await db.select().from(todos).where(eq(todos.userId, user!.id));
16
+ if (!user) redirect("/login");
17
+ const items = await db.select().from(todos).where(eq(todos.userId, user.id));
15
18
 
16
19
  return (
17
20
  <main className="container mx-auto max-w-2xl p-8">
@@ -12,7 +12,7 @@ vi.mock("@/shared/lib/supabase/server", () => ({
12
12
  }));
13
13
 
14
14
  vi.mock("next/navigation", () => ({
15
- redirect: (url: string) => mockRedirect(url),
15
+ redirect: (url: string): unknown => mockRedirect(url),
16
16
  }));
17
17
 
18
18
  describe("ProtectedLayout", () => {
@@ -1,7 +1,8 @@
1
- import { describe, it, expect, vi } from "vitest";
1
+ import { describe, it, expect, vi, beforeEach } from "vitest";
2
2
 
3
3
  const mockGetUser = vi.fn();
4
4
  const mockSelect = vi.fn();
5
+ const mockRedirect = vi.fn();
5
6
 
6
7
  vi.mock("@/shared/lib/supabase/server", () => ({
7
8
  createClient: vi.fn(() =>
@@ -29,7 +30,26 @@ vi.mock("@/features/todos/components/add-todo-form", () => ({
29
30
  AddTodoForm: () => <div data-testid="add-todo-form" />,
30
31
  }));
31
32
 
33
+ vi.mock("next/navigation", () => ({
34
+ redirect: (url: string): unknown => mockRedirect(url),
35
+ }));
36
+
32
37
  describe("HomePage (protected)", () => {
38
+ beforeEach(() => {
39
+ vi.clearAllMocks();
40
+ mockRedirect.mockImplementation(() => { throw new Error("NEXT_REDIRECT"); });
41
+ });
42
+
43
+ it("redirects to /login when user is not authenticated", async () => {
44
+ // Arrange
45
+ mockGetUser.mockResolvedValue({ data: { user: null } });
46
+ const { default: HomePage } = await import("../(protected)/page");
47
+
48
+ // Act + Assert
49
+ await expect(HomePage()).rejects.toThrow("NEXT_REDIRECT");
50
+ expect(mockRedirect).toHaveBeenCalledWith("/login");
51
+ });
52
+
33
53
  it("renderiza la página con las tareas del usuario", async () => {
34
54
  // Arrange
35
55
  mockGetUser.mockResolvedValue({ data: { user: { id: "u1", email: "user@example.com" } } });
@@ -7,7 +7,7 @@ import { LoginForm } from "../components/login-form";
7
7
  const mockLoginAction = vi.fn().mockResolvedValue({ status: "idle" });
8
8
 
9
9
  vi.mock("../actions/login.action", () => ({
10
- loginAction: (...args: unknown[]) => mockLoginAction(...args),
10
+ loginAction: (...args: unknown[]): unknown => mockLoginAction(...args),
11
11
  }));
12
12
 
13
13
  describe("LoginForm", () => {
@@ -12,7 +12,7 @@ vi.mock("@/shared/lib/supabase/server", () => ({
12
12
  }));
13
13
 
14
14
  vi.mock("next/navigation", () => ({
15
- redirect: (url: string) => mockRedirect(url),
15
+ redirect: (url: string): unknown => mockRedirect(url),
16
16
  }));
17
17
 
18
18
  describe("loginAction", () => {
@@ -12,7 +12,7 @@ vi.mock("@/shared/lib/supabase/server", () => ({
12
12
  }));
13
13
 
14
14
  vi.mock("next/navigation", () => ({
15
- redirect: (url: string) => mockRedirect(url),
15
+ redirect: (url: string): unknown => mockRedirect(url),
16
16
  }));
17
17
 
18
18
  describe("logoutAction", () => {
@@ -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 };
28
+ return { status: "error", message: (parsed.error.issues[0] as { message: string }).message };
29
29
  }
30
30
 
31
31
  const supabase = await createClient();
@@ -35,7 +35,7 @@ export function LoginForm() {
35
35
  const fd = new FormData();
36
36
  fd.set("email", data.email);
37
37
  fd.set("password", data.password);
38
- startTransition(() => formAction(fd));
38
+ startTransition(() => { formAction(fd); });
39
39
  });
40
40
 
41
41
  return (
@@ -7,7 +7,7 @@ import { AddTodoForm } from "../components/add-todo-form";
7
7
  const mockAdd = vi.fn().mockResolvedValue(undefined);
8
8
 
9
9
  vi.mock("../actions/todos.action", () => ({
10
- addTodoAction: (...args: unknown[]) => mockAdd(...args),
10
+ addTodoAction: (...args: unknown[]): unknown => mockAdd(...args),
11
11
  }));
12
12
 
13
13
  describe("AddTodoForm", () => {
@@ -8,8 +8,8 @@ const mockToggle = vi.fn().mockResolvedValue(undefined);
8
8
  const mockDelete = vi.fn().mockResolvedValue(undefined);
9
9
 
10
10
  vi.mock("../actions/todos.action", () => ({
11
- toggleTodoAction: (...args: unknown[]) => mockToggle(...args),
12
- deleteTodoAction: (...args: unknown[]) => mockDelete(...args),
11
+ toggleTodoAction: (...args: unknown[]): unknown => mockToggle(...args),
12
+ deleteTodoAction: (...args: unknown[]): unknown => mockDelete(...args),
13
13
  }));
14
14
 
15
15
  const baseTodo = {
@@ -8,7 +8,7 @@ const mockRevalidatePath = vi.fn();
8
8
  const mockGetUser = vi.fn();
9
9
 
10
10
  vi.mock("next/cache", () => ({
11
- revalidatePath: (path: string) => mockRevalidatePath(path),
11
+ revalidatePath: (path: string): unknown => mockRevalidatePath(path),
12
12
  }));
13
13
 
14
14
  vi.mock("@/shared/lib/supabase/server", () => ({
@@ -1,7 +1,8 @@
1
1
  "use server";
2
2
 
3
- import { revalidatePath } from "next/cache";
4
3
  import { eq } from "drizzle-orm";
4
+ import { revalidatePath } from "next/cache";
5
+
5
6
  import { db } from "@/shared/db";
6
7
  import { todos } from "@/shared/db/schema";
7
8
  import { createClient } from "@/shared/lib/supabase/server";
@@ -1,14 +1,16 @@
1
1
  "use client";
2
2
 
3
3
  import { useRef, useTransition } from "react";
4
+
4
5
  import { addTodoAction } from "../actions/todos.action";
6
+
5
7
  import { Button } from "@/shared/components/ui/button";
6
8
 
7
9
  export function AddTodoForm() {
8
10
  const [isPending, startTransition] = useTransition();
9
11
  const inputRef = useRef<HTMLInputElement>(null);
10
12
 
11
- function handleSubmit(e: React.FormEvent<HTMLFormElement>) {
13
+ function handleSubmit(e: React.SyntheticEvent<HTMLFormElement>) {
12
14
  e.preventDefault();
13
15
  const formData = new FormData(e.currentTarget);
14
16
  startTransition(async () => {
@@ -1,9 +1,11 @@
1
1
  "use client";
2
2
 
3
3
  import { useTransition } from "react";
4
- import { toggleTodoAction, deleteTodoAction } from "../actions/todos.action";
5
- import type { SelectTodo } from "@/shared/db/schema";
4
+
5
+ import { deleteTodoAction, toggleTodoAction } from "../actions/todos.action";
6
+
6
7
  import { Spinner } from "@/shared/components/ui/spinner";
8
+ import type { SelectTodo } from "@/shared/db/schema";
7
9
 
8
10
  interface TodoListProps {
9
11
  items: SelectTodo[];
@@ -33,7 +35,7 @@ export function TodoList({ items }: TodoListProps) {
33
35
  checked={todo.completed}
34
36
  disabled={isPending}
35
37
  onChange={() => {
36
- startTransition(() => toggleTodoAction(todo.id));
38
+ startTransition(() => { void toggleTodoAction(todo.id); });
37
39
  }}
38
40
  aria-label={`Marcar "${todo.title}" como completada`}
39
41
  />
@@ -46,7 +48,7 @@ export function TodoList({ items }: TodoListProps) {
46
48
  type="button"
47
49
  disabled={isPending}
48
50
  onClick={() => {
49
- startTransition(() => deleteTodoAction(todo.id));
51
+ startTransition(() => { void deleteTodoAction(todo.id); });
50
52
  }}
51
53
  className="rounded px-2 py-1 text-sm text-red-500 hover:bg-red-50"
52
54
  aria-label={`Eliminar "${todo.title}"`}
@@ -1,10 +1,10 @@
1
- import { describe, it, expect, vi } from "vitest";
2
1
  import { NextRequest } from "next/server";
2
+ import { describe, it, expect, vi } from "vitest";
3
3
 
4
4
  const mockUpdateSession = vi.fn();
5
5
 
6
6
  vi.mock("@/shared/lib/supabase/middleware", () => ({
7
- updateSession: (...args: unknown[]) => mockUpdateSession(...args),
7
+ updateSession: (...args: unknown[]): unknown => mockUpdateSession(...args),
8
8
  }));
9
9
 
10
10
  describe("middleware", () => {
@@ -12,11 +12,11 @@ describe("middleware", () => {
12
12
  // Arrange
13
13
  const mockResponse = new Response(null, { status: 200 });
14
14
  mockUpdateSession.mockResolvedValue(mockResponse);
15
- const { middleware } = await import("../../middleware");
15
+ const mod = await import("../../../middleware") as { middleware: (req: NextRequest) => Promise<Response> };
16
16
  const req = new NextRequest("http://localhost/dashboard");
17
17
 
18
18
  // Act
19
- const response = await middleware(req);
19
+ const response = await mod.middleware(req);
20
20
 
21
21
  // Assert
22
22
  expect(mockUpdateSession).toHaveBeenCalledWith(req);
@@ -25,10 +25,10 @@ describe("middleware", () => {
25
25
 
26
26
  it("exports a matcher config", async () => {
27
27
  // Arrange + Act
28
- const { config } = await import("../../middleware");
28
+ const mod = await import("../../../middleware") as { config: { matcher: string[] } };
29
29
 
30
30
  // Assert
31
- expect(config.matcher).toBeDefined();
32
- expect(Array.isArray(config.matcher)).toBe(true);
31
+ expect(mod.config.matcher).toBeDefined();
32
+ expect(Array.isArray(mod.config.matcher)).toBe(true);
33
33
  });
34
34
  });
@@ -1,31 +1,34 @@
1
+ import type * as NextServerTypes from "next/server";
1
2
  import { describe, it, expect, vi, beforeEach } from "vitest";
2
3
 
3
4
  const mockGetUser = vi.fn();
4
5
  const mockCreateServerClient = vi.fn();
5
6
  const mockNextResponseNext = vi.fn();
6
7
  const mockNextResponseRedirect = vi.fn();
8
+ const mockRequestGetAll = vi.fn(() => []);
9
+ const mockRequestSet = vi.fn();
7
10
 
8
11
  vi.mock("@supabase/ssr", () => ({
9
- createServerClient: (...args: unknown[]) => mockCreateServerClient(...args),
12
+ createServerClient: (...args: unknown[]): unknown => mockCreateServerClient(...args),
10
13
  }));
11
14
 
12
15
  vi.mock("next/server", async () => {
13
- const actual = await vi.importActual<typeof import("next/server")>("next/server");
16
+ const actual = await vi.importActual<typeof NextServerTypes>("next/server");
14
17
  return {
15
18
  ...actual,
16
19
  NextResponse: {
17
- next: (...args: unknown[]) => mockNextResponseNext(...args),
18
- redirect: (...args: unknown[]) => mockNextResponseRedirect(...args),
20
+ next: (...args: unknown[]): unknown => mockNextResponseNext(...args),
21
+ redirect: (...args: unknown[]): unknown => mockNextResponseRedirect(...args),
19
22
  },
20
23
  };
21
24
  });
22
25
 
23
- type CookieOptions = { path?: string; maxAge?: number };
24
- type CookieItem = { name: string; value: string; options: CookieOptions };
25
- type CookieCallbacks = {
26
+ interface CookieOptions { path?: string; maxAge?: number }
27
+ interface CookieItem { name: string; value: string; options: CookieOptions }
28
+ interface CookieCallbacks {
26
29
  getAll: () => CookieItem[];
27
30
  setAll: (cookies: CookieItem[]) => void;
28
- };
31
+ }
29
32
 
30
33
  function makeRequest(pathname: string) {
31
34
  return {
@@ -33,10 +36,10 @@ function makeRequest(pathname: string) {
33
36
  pathname,
34
37
  clone: () => ({ pathname, toString: () => `http://localhost${pathname}` }),
35
38
  },
36
- cookies: { getAll: vi.fn(() => []), set: vi.fn() },
39
+ cookies: { getAll: mockRequestGetAll, set: mockRequestSet },
37
40
  headers: new Headers(),
38
41
  url: `http://localhost${pathname}`,
39
- } as unknown as import("next/server").NextRequest;
42
+ } as unknown as NextServerTypes.NextRequest;
40
43
  }
41
44
 
42
45
  describe("supabase middleware (updateSession)", () => {
@@ -85,10 +88,11 @@ describe("supabase middleware (updateSession)", () => {
85
88
  await updateSession(req);
86
89
 
87
90
  // Act
88
- capturedCookieCallbacks!.getAll();
91
+ if (!capturedCookieCallbacks) throw new Error("capturedCookieCallbacks not set");
92
+ capturedCookieCallbacks.getAll();
89
93
 
90
94
  // Assert
91
- expect(req.cookies.getAll).toHaveBeenCalled();
95
+ expect(mockRequestGetAll).toHaveBeenCalled();
92
96
  });
93
97
 
94
98
  it("invokes setAll cookie callback and updates request + response cookies", async () => {
@@ -99,12 +103,13 @@ describe("supabase middleware (updateSession)", () => {
99
103
  await updateSession(req);
100
104
 
101
105
  // Act
102
- capturedCookieCallbacks!.setAll([
106
+ if (!capturedCookieCallbacks) throw new Error("capturedCookieCallbacks not set");
107
+ capturedCookieCallbacks.setAll([
103
108
  { name: "sb-token", value: "abc", options: { path: "/" } },
104
109
  ]);
105
110
 
106
111
  // Assert
107
- expect(req.cookies.set).toHaveBeenCalledWith("sb-token", "abc");
112
+ expect(mockRequestSet).toHaveBeenCalledWith("sb-token", "abc");
108
113
  expect(fakeResponse.cookies.set).toHaveBeenCalledWith("sb-token", "abc", { path: "/" });
109
114
  });
110
115
 
@@ -5,7 +5,7 @@ const mockGetAll = vi.fn().mockReturnValue([]);
5
5
  const mockCreateServerClient = vi.fn();
6
6
 
7
7
  vi.mock("@supabase/ssr", () => ({
8
- createServerClient: (...args: unknown[]) => mockCreateServerClient(...args),
8
+ createServerClient: (...args: unknown[]): unknown => mockCreateServerClient(...args),
9
9
  }));
10
10
 
11
11
  vi.mock("next/headers", () => ({
@@ -43,7 +43,8 @@ describe("supabase server", () => {
43
43
 
44
44
  // Act
45
45
  await createClient();
46
- capturedGetAll!();
46
+ if (!capturedGetAll) throw new Error("capturedGetAll not set");
47
+ capturedGetAll();
47
48
 
48
49
  // Assert
49
50
  expect(mockGetAll).toHaveBeenCalled();
@@ -64,7 +65,8 @@ describe("supabase server", () => {
64
65
  await createClient();
65
66
 
66
67
  // Act
67
- capturedSetAll!([{ name: "token", value: "abc", options: { path: "/" } }]);
68
+ if (!capturedSetAll) throw new Error("capturedSetAll not set");
69
+ capturedSetAll([{ name: "token", value: "abc", options: { path: "/" } }]);
68
70
 
69
71
  // Assert
70
72
  expect(mockSet).toHaveBeenCalledWith("token", "abc", { path: "/" });
@@ -88,8 +90,10 @@ describe("supabase server", () => {
88
90
  await createClient();
89
91
 
90
92
  // Act + Assert
91
- expect(() =>
92
- capturedSetAll!([{ name: "token", value: "abc", options: {} }])
93
- ).not.toThrow();
93
+ if (!capturedSetAll) throw new Error("capturedSetAll not set");
94
+ const setAll = capturedSetAll;
95
+ expect(() => {
96
+ setAll([{ name: "token", value: "abc", options: {} }]);
97
+ }).not.toThrow();
94
98
  });
95
99
  });
@@ -9,8 +9,11 @@ describe("cn", () => {
9
9
  });
10
10
 
11
11
  it("omits falsy conditional classes", () => {
12
- // Arrange + Act + Assert
13
- expect(cn("foo", false && "bar", "baz")).toBe("foo baz");
12
+ // Arrange
13
+ const falsy = false as boolean;
14
+
15
+ // Act + Assert
16
+ expect(cn("foo", falsy && "bar", "baz")).toBe("foo baz");
14
17
  });
15
18
 
16
19
  it("handles undefined values", () => {
@@ -2,9 +2,10 @@ import { Slot } from "@radix-ui/react-slot"
2
2
  import { cva, type VariantProps } from "class-variance-authority"
3
3
  import * as React from "react"
4
4
 
5
- import { cn } from "@/shared/lib/utils"
6
5
  import { Spinner } from "./spinner"
7
6
 
7
+ import { cn } from "@/shared/lib/utils"
8
+
8
9
  const buttonVariants = cva(
9
10
  "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
10
11
  {
@@ -1,4 +1,5 @@
1
1
  import { LoaderCircle } from "lucide-react";
2
+
2
3
  import { cn } from "@/shared/lib/utils";
3
4
 
4
5
  export function Spinner({ className }: { className?: string }) {