nextjs-hackathon-stack 0.1.3 → 0.1.5

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 (37) hide show
  1. package/package.json +1 -1
  2. package/template/eslint.config.ts +1 -1
  3. package/template/next.config.ts +3 -5
  4. package/template/package.json.tmpl +8 -1
  5. package/template/playwright.config.ts +1 -1
  6. package/template/src/app/(protected)/layout.tsx +1 -0
  7. package/template/src/app/(protected)/page.tsx +1 -1
  8. package/template/src/app/api/auth/callback/route.ts +1 -0
  9. package/template/src/app/layout.tsx +1 -0
  10. package/template/src/features/auth/__tests__/login-form.test.tsx +1 -0
  11. package/template/src/features/auth/__tests__/use-auth.test.ts +5 -4
  12. package/template/src/features/auth/actions/login.action.ts +1 -0
  13. package/template/src/features/auth/actions/logout.action.ts +1 -0
  14. package/template/src/features/auth/components/login-form.tsx +5 -3
  15. package/template/src/features/auth/hooks/use-auth.ts +1 -0
  16. package/template/src/features/chat/__tests__/chat-ui.test.tsx +1 -0
  17. package/template/src/features/chat/api/route.ts +3 -2
  18. package/template/src/features/chat/components/chat-ui.tsx +2 -1
  19. package/template/src/features/tts/__tests__/tts-player.test.tsx +1 -0
  20. package/template/src/features/tts/components/tts-player.tsx +3 -3
  21. package/template/src/features/video/__tests__/video-generator.test.tsx +1 -0
  22. package/template/src/features/video/components/video-generator.tsx +3 -3
  23. package/template/src/shared/__tests__/ai.test.ts +1 -0
  24. package/template/src/shared/__tests__/minimax-media.test.ts +3 -2
  25. package/template/src/shared/__tests__/providers.test.tsx +1 -0
  26. package/template/src/shared/__tests__/schema.test.ts +1 -0
  27. package/template/src/shared/components/ui/button.tsx +59 -0
  28. package/template/src/shared/components/ui/card.tsx +92 -0
  29. package/template/src/shared/components/ui/input.tsx +19 -0
  30. package/template/src/shared/components/ui/label.tsx +22 -0
  31. package/template/src/shared/db/index.ts +2 -1
  32. package/template/src/shared/lib/ai.ts +2 -2
  33. package/template/src/shared/lib/minimax-media.ts +2 -2
  34. package/template/src/shared/lib/supabase/client.ts +2 -2
  35. package/template/src/shared/lib/supabase/middleware.ts +9 -8
  36. package/template/src/shared/lib/supabase/server.ts +6 -4
  37. package/template/src/shared/lib/utils.ts +6 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nextjs-hackathon-stack",
3
- "version": "0.1.3",
3
+ "version": "0.1.5",
4
4
  "description": "Scaffold a full-stack Next.js hackathon starter",
5
5
  "type": "module",
6
6
  "bin": {
@@ -61,6 +61,6 @@ export default tseslint.config(
61
61
  },
62
62
  },
63
63
  {
64
- ignores: [".next/**", "node_modules/**", "dist/**"],
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"],
65
65
  }
66
66
  );
@@ -1,10 +1,8 @@
1
1
  import type { NextConfig } from "next";
2
2
 
3
3
  const nextConfig: NextConfig = {
4
- experimental: {
5
- typedRoutes: true,
6
- },
7
- headers: async () => [
4
+ typedRoutes: true,
5
+ headers: () => Promise.resolve([
8
6
  {
9
7
  source: "/(.*)",
10
8
  headers: [
@@ -25,7 +23,7 @@ const nextConfig: NextConfig = {
25
23
  },
26
24
  ],
27
25
  },
28
- ],
26
+ ]),
29
27
  };
30
28
 
31
29
  export default nextConfig;
@@ -31,7 +31,14 @@
31
31
  "@hookform/resolvers": "^3",
32
32
  "zod": "^3",
33
33
  "react": "^19",
34
- "react-dom": "^19"
34
+ "react-dom": "^19",
35
+ "postgres": "^3",
36
+ "@ai-sdk/openai": "^1",
37
+ "clsx": "^2",
38
+ "tailwind-merge": "^2",
39
+ "class-variance-authority": "^0.7",
40
+ "@radix-ui/react-slot": "^1",
41
+ "@radix-ui/react-label": "^2"
35
42
  },
36
43
  "devDependencies": {
37
44
  "typescript": "^5",
@@ -5,7 +5,7 @@ export default defineConfig({
5
5
  fullyParallel: true,
6
6
  forbidOnly: !!process.env["CI"],
7
7
  retries: process.env["CI"] ? 2 : 0,
8
- workers: process.env["CI"] ? 1 : undefined,
8
+ ...(process.env["CI"] ? { workers: 1 } : {}),
9
9
  reporter: "html",
10
10
  use: {
11
11
  baseURL: "http://localhost:3000",
@@ -1,4 +1,5 @@
1
1
  import { redirect } from "next/navigation";
2
+
2
3
  import { createClient } from "@/shared/lib/supabase/server";
3
4
 
4
5
  export default async function ProtectedLayout({
@@ -1,5 +1,5 @@
1
- import { createClient } from "@/shared/lib/supabase/server";
2
1
  import { ChatUi } from "@/features/chat/components/chat-ui";
2
+ import { createClient } from "@/shared/lib/supabase/server";
3
3
 
4
4
  export default async function HomePage() {
5
5
  const supabase = await createClient();
@@ -1,4 +1,5 @@
1
1
  import { NextResponse } from "next/server";
2
+
2
3
  import { createClient } from "@/shared/lib/supabase/server";
3
4
 
4
5
  export async function GET(request: Request) {
@@ -1,4 +1,5 @@
1
1
  import type { Metadata } from "next";
2
+
2
3
  import { Providers } from "@/shared/components/providers";
3
4
 
4
5
  export const metadata: Metadata = {
@@ -1,6 +1,7 @@
1
1
  import { render, screen } 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
  import { LoginForm } from "../components/login-form";
5
6
 
6
7
  vi.mock("../actions/login.action", () => ({
@@ -1,7 +1,8 @@
1
- import { renderHook, waitFor } from "@testing-library/react";
2
- import { describe, it, expect, vi } from "vitest";
3
1
  import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
2
+ import { renderHook, waitFor } from "@testing-library/react";
4
3
  import { createElement } from "react";
4
+ import { describe, it, expect, vi } from "vitest";
5
+
5
6
  import { useAuth } from "../hooks/use-auth";
6
7
 
7
8
  const mockGetSession = vi.fn();
@@ -29,7 +30,7 @@ describe("useAuth", () => {
29
30
  const { result } = renderHook(() => useAuth(), { wrapper });
30
31
 
31
32
  // Assert
32
- await waitFor(() => expect(result.current.isSuccess).toBe(true));
33
+ await waitFor(() => { expect(result.current.isSuccess).toBe(true); });
33
34
  expect(result.current.data).toEqual(mockSession);
34
35
  });
35
36
 
@@ -41,6 +42,6 @@ describe("useAuth", () => {
41
42
  const { result } = renderHook(() => useAuth(), { wrapper });
42
43
 
43
44
  // Assert
44
- await waitFor(() => expect(result.current.isError).toBe(true));
45
+ await waitFor(() => { expect(result.current.isError).toBe(true); });
45
46
  });
46
47
  });
@@ -2,6 +2,7 @@
2
2
 
3
3
  import { redirect } from "next/navigation";
4
4
  import { z } from "zod";
5
+
5
6
  import { createClient } from "@/shared/lib/supabase/server";
6
7
 
7
8
  const loginSchema = z.object({
@@ -1,6 +1,7 @@
1
1
  "use server";
2
2
 
3
3
  import { redirect } from "next/navigation";
4
+
4
5
  import { createClient } from "@/shared/lib/supabase/server";
5
6
 
6
7
  export async function logoutAction(): Promise<void> {
@@ -1,14 +1,16 @@
1
1
  "use client";
2
2
 
3
+ import { zodResolver } from "@hookform/resolvers/zod";
3
4
  import { useActionState } from "react";
4
5
  import { useForm } from "react-hook-form";
5
- import { zodResolver } from "@hookform/resolvers/zod";
6
6
  import { z } from "zod";
7
+
7
8
  import { loginAction, type LoginActionState } from "../actions/login.action";
9
+
8
10
  import { Button } from "@/shared/components/ui/button";
11
+ import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/shared/components/ui/card";
9
12
  import { Input } from "@/shared/components/ui/input";
10
13
  import { Label } from "@/shared/components/ui/label";
11
- import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/shared/components/ui/card";
12
14
 
13
15
  const schema = z.object({
14
16
  email: z.string().email("Invalid email"),
@@ -42,7 +44,7 @@ export function LoginForm() {
42
44
  <CardDescription>Enter your email and password below</CardDescription>
43
45
  </CardHeader>
44
46
  <CardContent>
45
- <form onSubmit={onSubmit} data-testid="login-form" className="space-y-4">
47
+ <form onSubmit={(e) => { void onSubmit(e); }} data-testid="login-form" className="space-y-4">
46
48
  {state.status === "error" && (
47
49
  <p role="alert" className="rounded-md bg-destructive/10 px-3 py-2 text-sm text-destructive">
48
50
  {state.message}
@@ -1,6 +1,7 @@
1
1
  "use client";
2
2
 
3
3
  import { useQuery } from "@tanstack/react-query";
4
+
4
5
  import { createClient } from "@/shared/lib/supabase/client";
5
6
 
6
7
  export function useAuth() {
@@ -1,5 +1,6 @@
1
1
  import { render, screen } from "@testing-library/react";
2
2
  import { describe, it, expect, vi } from "vitest";
3
+
3
4
  import { ChatUi } from "../components/chat-ui";
4
5
 
5
6
  vi.mock("@ai-sdk/react", () => ({
@@ -1,10 +1,11 @@
1
- import { streamText } from "ai";
1
+ import { streamText, type CoreMessage } from "ai";
2
+
2
3
  import { aiModel } from "@/shared/lib/ai";
3
4
 
4
5
  export const runtime = "edge";
5
6
 
6
7
  export async function POST(req: Request) {
7
- const { messages } = (await req.json()) as { messages: Array<{ role: string; content: string }> };
8
+ const { messages } = (await req.json()) as { messages: CoreMessage[] };
8
9
 
9
10
  const result = streamText({
10
11
  model: aiModel,
@@ -3,9 +3,10 @@
3
3
  import { useChat } from "@ai-sdk/react";
4
4
 
5
5
  export function ChatUi() {
6
- const { messages, input, handleInputChange, handleSubmit, isLoading } = useChat({
6
+ const { messages, input, handleInputChange, handleSubmit, status } = useChat({
7
7
  api: "/features/chat/api",
8
8
  });
9
+ const isLoading = status === "streaming" || status === "submitted";
9
10
 
10
11
  return (
11
12
  <div className="flex flex-col gap-4" data-testid="chat-ui">
@@ -1,5 +1,6 @@
1
1
  import { render, screen } from "@testing-library/react";
2
2
  import { describe, it, expect } from "vitest";
3
+
3
4
  import { TtsPlayer } from "../components/tts-player";
4
5
 
5
6
  describe("TtsPlayer", () => {
@@ -8,7 +8,7 @@ export function TtsPlayer() {
8
8
  const [isLoading, setIsLoading] = useState(false);
9
9
  const [error, setError] = useState<string | null>(null);
10
10
 
11
- const handleSubmit = async (e: React.FormEvent) => {
11
+ const handleSubmit = async (e: React.SyntheticEvent) => {
12
12
  e.preventDefault();
13
13
  setIsLoading(true);
14
14
  setError(null);
@@ -32,10 +32,10 @@ export function TtsPlayer() {
32
32
 
33
33
  return (
34
34
  <div className="space-y-4" data-testid="tts-player">
35
- <form onSubmit={handleSubmit} className="flex gap-2">
35
+ <form onSubmit={(e) => { void handleSubmit(e); }} className="flex gap-2">
36
36
  <textarea
37
37
  value={text}
38
- onChange={(e) => setText(e.target.value)}
38
+ onChange={(e) => { setText(e.target.value); }}
39
39
  placeholder="Enter text to speak..."
40
40
  className="flex-1 rounded border px-3 py-2"
41
41
  rows={3}
@@ -1,5 +1,6 @@
1
1
  import { render, screen } from "@testing-library/react";
2
2
  import { describe, it, expect } from "vitest";
3
+
3
4
  import { VideoGenerator } from "../components/video-generator";
4
5
 
5
6
  describe("VideoGenerator", () => {
@@ -8,7 +8,7 @@ export function VideoGenerator() {
8
8
  const [isLoading, setIsLoading] = useState(false);
9
9
  const [error, setError] = useState<string | null>(null);
10
10
 
11
- const handleSubmit = async (e: React.FormEvent) => {
11
+ const handleSubmit = async (e: React.SyntheticEvent) => {
12
12
  e.preventDefault();
13
13
  setIsLoading(true);
14
14
  setError(null);
@@ -32,10 +32,10 @@ export function VideoGenerator() {
32
32
 
33
33
  return (
34
34
  <div className="space-y-4" data-testid="video-generator">
35
- <form onSubmit={handleSubmit} className="flex gap-2">
35
+ <form onSubmit={(e) => { void handleSubmit(e); }} className="flex gap-2">
36
36
  <input
37
37
  value={prompt}
38
- onChange={(e) => setPrompt(e.target.value)}
38
+ onChange={(e) => { setPrompt(e.target.value); }}
39
39
  placeholder="Describe a video..."
40
40
  className="flex-1 rounded border px-3 py-2"
41
41
  />
@@ -1,4 +1,5 @@
1
1
  import { describe, it, expect } from "vitest";
2
+
2
3
  import { aiModel } from "../lib/ai";
3
4
 
4
5
  describe("aiModel", () => {
@@ -1,4 +1,5 @@
1
1
  import { describe, it, expect, vi, beforeEach } from "vitest";
2
+
2
3
  import { generateVideo, synthesizeSpeech } from "../lib/minimax-media";
3
4
 
4
5
  const mockFetch = vi.fn();
@@ -13,7 +14,7 @@ describe("generateVideo", () => {
13
14
  // Arrange
14
15
  mockFetch.mockResolvedValue({
15
16
  ok: true,
16
- json: async () => ({ task_id: "abc123" }),
17
+ json: () => Promise.resolve({ task_id: "abc123" }),
17
18
  });
18
19
 
19
20
  // Act
@@ -37,7 +38,7 @@ describe("synthesizeSpeech", () => {
37
38
  // Arrange
38
39
  mockFetch.mockResolvedValue({
39
40
  ok: true,
40
- json: async () => ({ audio_file: "https://example.com/audio.mp3" }),
41
+ json: () => Promise.resolve({ audio_file: "https://example.com/audio.mp3" }),
41
42
  });
42
43
 
43
44
  // Act
@@ -1,5 +1,6 @@
1
1
  import { render, screen } from "@testing-library/react";
2
2
  import { describe, it, expect } from "vitest";
3
+
3
4
  import { Providers } from "../components/providers";
4
5
 
5
6
  describe("Providers", () => {
@@ -1,4 +1,5 @@
1
1
  import { describe, it, expect } from "vitest";
2
+
2
3
  import { insertProfileSchema, selectProfileSchema } from "../db/schema";
3
4
 
4
5
  describe("profiles schema", () => {
@@ -0,0 +1,59 @@
1
+ import { Slot } from "@radix-ui/react-slot"
2
+ import { cva, type VariantProps } from "class-variance-authority"
3
+ import * as React from "react"
4
+
5
+ import { cn } from "@/shared/lib/utils"
6
+
7
+ const buttonVariants = cva(
8
+ "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",
9
+ {
10
+ variants: {
11
+ variant: {
12
+ default:
13
+ "bg-primary text-primary-foreground shadow-xs hover:bg-primary/90",
14
+ destructive:
15
+ "bg-destructive text-white shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
16
+ outline:
17
+ "border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50",
18
+ secondary:
19
+ "bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80",
20
+ ghost:
21
+ "hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
22
+ link: "text-primary underline-offset-4 hover:underline",
23
+ },
24
+ size: {
25
+ default: "h-9 px-4 py-2 has-[>svg]:px-3",
26
+ sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5",
27
+ lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
28
+ icon: "size-9",
29
+ },
30
+ },
31
+ defaultVariants: {
32
+ variant: "default",
33
+ size: "default",
34
+ },
35
+ }
36
+ )
37
+
38
+ function Button({
39
+ className,
40
+ variant,
41
+ size,
42
+ asChild = false,
43
+ ...props
44
+ }: React.ComponentProps<"button"> &
45
+ VariantProps<typeof buttonVariants> & {
46
+ asChild?: boolean
47
+ }) {
48
+ const Comp = asChild ? Slot : "button"
49
+
50
+ return (
51
+ <Comp
52
+ data-slot="button"
53
+ className={cn(buttonVariants({ variant, size, className }))}
54
+ {...props}
55
+ />
56
+ )
57
+ }
58
+
59
+ export { Button, buttonVariants }
@@ -0,0 +1,92 @@
1
+ import * as React from "react"
2
+
3
+ import { cn } from "@/shared/lib/utils"
4
+
5
+ function Card({ className, ...props }: React.ComponentProps<"div">) {
6
+ return (
7
+ <div
8
+ data-slot="card"
9
+ className={cn(
10
+ "bg-card text-card-foreground flex flex-col gap-6 rounded-xl border py-6 shadow-sm",
11
+ className
12
+ )}
13
+ {...props}
14
+ />
15
+ )
16
+ }
17
+
18
+ function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
19
+ return (
20
+ <div
21
+ data-slot="card-header"
22
+ className={cn(
23
+ "@container/card-header grid auto-rows-min grid-rows-[auto_auto] items-start gap-1.5 px-6 has-data-[slot=card-action]:grid-cols-[1fr_auto] [.border-b]:pb-6",
24
+ className
25
+ )}
26
+ {...props}
27
+ />
28
+ )
29
+ }
30
+
31
+ function CardTitle({ className, ...props }: React.ComponentProps<"div">) {
32
+ return (
33
+ <div
34
+ data-slot="card-title"
35
+ className={cn("leading-none font-semibold", className)}
36
+ {...props}
37
+ />
38
+ )
39
+ }
40
+
41
+ function CardDescription({ className, ...props }: React.ComponentProps<"div">) {
42
+ return (
43
+ <div
44
+ data-slot="card-description"
45
+ className={cn("text-muted-foreground text-sm", className)}
46
+ {...props}
47
+ />
48
+ )
49
+ }
50
+
51
+ function CardAction({ className, ...props }: React.ComponentProps<"div">) {
52
+ return (
53
+ <div
54
+ data-slot="card-action"
55
+ className={cn(
56
+ "col-start-2 row-span-2 row-start-1 self-start justify-self-end",
57
+ className
58
+ )}
59
+ {...props}
60
+ />
61
+ )
62
+ }
63
+
64
+ function CardContent({ className, ...props }: React.ComponentProps<"div">) {
65
+ return (
66
+ <div
67
+ data-slot="card-content"
68
+ className={cn("px-6", className)}
69
+ {...props}
70
+ />
71
+ )
72
+ }
73
+
74
+ function CardFooter({ className, ...props }: React.ComponentProps<"div">) {
75
+ return (
76
+ <div
77
+ data-slot="card-footer"
78
+ className={cn("flex items-center px-6 [.border-t]:pt-6", className)}
79
+ {...props}
80
+ />
81
+ )
82
+ }
83
+
84
+ export {
85
+ Card,
86
+ CardHeader,
87
+ CardFooter,
88
+ CardTitle,
89
+ CardAction,
90
+ CardDescription,
91
+ CardContent,
92
+ }
@@ -0,0 +1,19 @@
1
+ import * as React from "react"
2
+
3
+ import { cn } from "@/shared/lib/utils"
4
+
5
+ function Input({ className, type, ...props }: React.ComponentProps<"input">) {
6
+ return (
7
+ <input
8
+ type={type}
9
+ data-slot="input"
10
+ className={cn(
11
+ "file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 border-input flex h-9 w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm 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",
12
+ className
13
+ )}
14
+ {...props}
15
+ />
16
+ )
17
+ }
18
+
19
+ export { Input }
@@ -0,0 +1,22 @@
1
+ import * as LabelPrimitive from "@radix-ui/react-label"
2
+ import * as React from "react"
3
+
4
+ import { cn } from "@/shared/lib/utils"
5
+
6
+ function Label({
7
+ className,
8
+ ...props
9
+ }: React.ComponentProps<typeof LabelPrimitive.Root>) {
10
+ return (
11
+ <LabelPrimitive.Root
12
+ data-slot="label"
13
+ className={cn(
14
+ "flex items-center gap-2 text-sm leading-none font-medium select-none group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50 peer-disabled:cursor-not-allowed peer-disabled:opacity-50",
15
+ className
16
+ )}
17
+ {...props}
18
+ />
19
+ )
20
+ }
21
+
22
+ export { Label }
@@ -1,7 +1,8 @@
1
1
  import { drizzle } from "drizzle-orm/postgres-js";
2
2
  import postgres from "postgres";
3
+
3
4
  import * as schema from "./schema";
4
5
 
5
- const client = postgres(process.env["DATABASE_URL"] ?? "");
6
+ const client = postgres(process.env.DATABASE_URL ?? "");
6
7
 
7
8
  export const db = drizzle(client, { schema });
@@ -1,8 +1,8 @@
1
1
  import { createOpenAI } from "@ai-sdk/openai";
2
2
 
3
3
  const gateway = createOpenAI({
4
- baseURL: process.env["AI_GATEWAY_URL"] ?? "https://api.minimaxi.chat/v1",
5
- apiKey: process.env["MINIMAX_API_KEY"] ?? "",
4
+ baseURL: process.env.AI_GATEWAY_URL ?? "https://api.minimaxi.chat/v1",
5
+ apiKey: process.env.MINIMAX_API_KEY ?? "",
6
6
  });
7
7
 
8
8
  export const aiModel = gateway("minimax/minimax-m2.7");
@@ -23,7 +23,7 @@ export async function generateVideo(prompt: string): Promise<string> {
23
23
  const response = await fetch(`${MINIMAX_API_BASE}/video_generation`, {
24
24
  method: "POST",
25
25
  headers: {
26
- Authorization: `Bearer ${process.env["MINIMAX_API_KEY"] ?? ""}`,
26
+ Authorization: `Bearer ${process.env.MINIMAX_API_KEY ?? ""}`,
27
27
  "Content-Type": "application/json",
28
28
  },
29
29
  body: JSON.stringify({
@@ -44,7 +44,7 @@ export async function synthesizeSpeech(text: string, voiceId = "female-shaonv"):
44
44
  const response = await fetch(`${MINIMAX_API_BASE}/t2a_v2`, {
45
45
  method: "POST",
46
46
  headers: {
47
- Authorization: `Bearer ${process.env["MINIMAX_API_KEY"] ?? ""}`,
47
+ Authorization: `Bearer ${process.env.MINIMAX_API_KEY ?? ""}`,
48
48
  "Content-Type": "application/json",
49
49
  },
50
50
  body: JSON.stringify({
@@ -2,7 +2,7 @@ import { createBrowserClient } from "@supabase/ssr";
2
2
 
3
3
  export function createClient() {
4
4
  return createBrowserClient(
5
- process.env["NEXT_PUBLIC_SUPABASE_URL"]!,
6
- process.env["NEXT_PUBLIC_SUPABASE_ANON_KEY"]!
5
+ process.env.NEXT_PUBLIC_SUPABASE_URL ?? "",
6
+ process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY ?? ""
7
7
  );
8
8
  }
@@ -1,23 +1,24 @@
1
- import { createServerClient } from "@supabase/ssr";
1
+ import { createServerClient, type CookieOptions } from "@supabase/ssr";
2
2
  import { NextResponse, type NextRequest } from "next/server";
3
3
 
4
4
  export async function updateSession(request: NextRequest) {
5
5
  let supabaseResponse = NextResponse.next({ request });
6
6
 
7
+ // eslint-disable-next-line @typescript-eslint/no-deprecated
7
8
  const supabase = createServerClient(
8
- process.env["NEXT_PUBLIC_SUPABASE_URL"]!,
9
- process.env["NEXT_PUBLIC_SUPABASE_ANON_KEY"]!,
9
+ process.env.NEXT_PUBLIC_SUPABASE_URL ?? "",
10
+ process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY ?? "",
10
11
  {
11
12
  cookies: {
12
13
  getAll() {
13
14
  return request.cookies.getAll();
14
15
  },
15
- setAll(cookiesToSet) {
16
- cookiesToSet.forEach(({ name, value }) => request.cookies.set(name, value));
16
+ setAll(cookiesToSet: { name: string; value: string; options: CookieOptions }[]) {
17
+ cookiesToSet.forEach(({ name, value }) => { request.cookies.set(name, value); });
17
18
  supabaseResponse = NextResponse.next({ request });
18
- cookiesToSet.forEach(({ name, value, options }) =>
19
- supabaseResponse.cookies.set(name, value, options)
20
- );
19
+ cookiesToSet.forEach(({ name, value, options }) => {
20
+ supabaseResponse.cookies.set(name, value, options);
21
+ });
21
22
  },
22
23
  },
23
24
  }
@@ -1,23 +1,25 @@
1
- import { createServerClient } from "@supabase/ssr";
1
+ import { createServerClient, type CookieOptions } from "@supabase/ssr";
2
2
  import { cookies } from "next/headers";
3
3
 
4
4
  export async function createClient() {
5
5
  const cookieStore = await cookies();
6
6
 
7
+ // eslint-disable-next-line @typescript-eslint/no-deprecated
7
8
  return createServerClient(
8
- process.env["NEXT_PUBLIC_SUPABASE_URL"]!,
9
- process.env["NEXT_PUBLIC_SUPABASE_ANON_KEY"]!,
9
+ process.env.NEXT_PUBLIC_SUPABASE_URL ?? "",
10
+ process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY ?? "",
10
11
  {
11
12
  cookies: {
12
13
  getAll() {
13
14
  return cookieStore.getAll();
14
15
  },
15
- setAll(cookiesToSet) {
16
+ setAll(cookiesToSet: { name: string; value: string; options: CookieOptions }[]) {
16
17
  try {
17
18
  cookiesToSet.forEach(({ name, value, options }) =>
18
19
  cookieStore.set(name, value, options)
19
20
  );
20
21
  } catch {
22
+ // Server components cannot set cookies; middleware handles this
21
23
  }
22
24
  },
23
25
  },
@@ -0,0 +1,6 @@
1
+ import { type ClassValue, clsx } from "clsx";
2
+ import { twMerge } from "tailwind-merge";
3
+
4
+ export function cn(...inputs: ClassValue[]) {
5
+ return twMerge(clsx(inputs));
6
+ }